fix(web): reveal memory editor after edit click (#1827)

This commit is contained in:
mehmet turac
2026-05-15 23:07:18 +08:00
committed by GitHub
parent 7d9488300e
commit 444f7d03eb
2 changed files with 73 additions and 0 deletions
+12
View File
@@ -2,6 +2,7 @@ import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
type CSSProperties,
} from 'react';
@@ -284,6 +285,9 @@ export function MemorySection() {
const [flash, setFlash] = useState<{ kind: FlashKind; key: number } | null>(
null,
);
const editorRef = useRef<HTMLDivElement | null>(null);
const editorNameRef = useRef<HTMLInputElement | null>(null);
const editingTarget = editing?.id ?? (editing ? 'new' : null);
// Recent LLM-extraction attempts, newest first. Driven by a one-shot
// fetch on mount + live SSE updates merged by id so phase transitions
// (running → success) replace the row in place.
@@ -299,6 +303,12 @@ export function MemorySection() {
return () => clearTimeout(id);
}, [flash]);
useEffect(() => {
if (!editingTarget) return;
editorRef.current?.scrollIntoView?.({ block: 'start', behavior: 'smooth' });
editorNameRef.current?.focus({ preventScroll: true });
}, [editingTarget]);
const flashLabel = useMemo<Record<FlashKind, string>>(
() => ({
created: t('settings.memoryFlashCreated'),
@@ -679,6 +689,7 @@ export function MemorySection() {
{editing ? (
<div
ref={editorRef}
className="library-card"
style={{
flexDirection: 'column',
@@ -747,6 +758,7 @@ export function MemorySection() {
{t('settings.memoryNameLabel')}
</label>
<input
ref={editorNameRef}
type="text"
placeholder={t('settings.memoryName')}
value={editing.name}
@@ -8,6 +8,7 @@ import { I18nProvider } from '../../src/i18n';
const originalFetch = globalThis.fetch;
const originalEventSource = globalThis.EventSource;
const originalScrollIntoView = HTMLElement.prototype.scrollIntoView;
class StubEventSource {
url: string;
@@ -51,6 +52,11 @@ describe('MemorySection', () => {
delete globalThis.EventSource;
}
vi.restoreAllMocks();
if (originalScrollIntoView) {
HTMLElement.prototype.scrollIntoView = originalScrollIntoView;
} else {
delete (HTMLElement.prototype as { scrollIntoView?: Element['scrollIntoView'] }).scrollIntoView;
}
});
it('shows the no-provider banner when the latest extraction skipped for missing credentials', async () => {
@@ -227,6 +233,61 @@ describe('MemorySection', () => {
expect(putBodies).toEqual(['# Memory\n\n- Existing bullet\n- New bullet\n']);
});
it('reveals the editor after editing an existing memory entry', async () => {
globalThis.EventSource = StubEventSource as unknown as typeof EventSource;
const scrollIntoView = vi.fn();
HTMLElement.prototype.scrollIntoView = scrollIntoView as Element['scrollIntoView'];
globalThis.fetch = vi.fn(async (input: RequestInfo | URL) => {
const url = input.toString();
if (url === '/api/memory') {
return new Response(JSON.stringify({
enabled: true,
rootDir: '/tmp/memory',
index: '# Memory\n',
entries: [
{
id: 'user_ui_preferences',
name: 'UI preferences',
description: 'Persistent UI rendering preferences',
type: 'user',
updatedAt: Date.now(),
},
],
extraction: null,
}), { status: 200, headers: { 'content-type': 'application/json' } });
}
if (url === '/api/memory/extractions') {
return new Response(JSON.stringify({ extractions: [] }), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
if (url === '/api/memory/user_ui_preferences') {
return new Response(JSON.stringify({
entry: {
id: 'user_ui_preferences',
name: 'UI preferences',
description: 'Persistent UI rendering preferences',
type: 'user',
body: '- Prefer dark mode',
updatedAt: Date.now(),
},
}), { status: 200, headers: { 'content-type': 'application/json' } });
}
return new Response(JSON.stringify({}), { status: 404 });
}) as typeof fetch;
renderMemorySection();
const entryCard = (await screen.findByText('UI preferences')).closest('.library-card') as HTMLElement;
fireEvent.click(within(entryCard).getByTitle('Edit'));
const nameInput = await screen.findByDisplayValue('UI preferences');
expect(scrollIntoView).toHaveBeenCalledWith({ block: 'start', behavior: 'smooth' });
expect(document.activeElement).toBe(nameInput);
});
it('uses the same expandable affordance for extraction history and memory index', async () => {
globalThis.EventSource = StubEventSource as unknown as typeof EventSource;
globalThis.fetch = vi.fn(async (input: RequestInfo | URL) => {