mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
fix(web): reveal memory editor after edit click (#1827)
This commit is contained in:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user