Fix memory action alignment (#3175)

This commit is contained in:
初晨 2026-05-28 16:53:01 +08:00 committed by GitHub
parent 693176457e
commit a7e7d5db18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 159 additions and 51 deletions

View file

@ -1421,33 +1421,35 @@ export function MemorySection({
{entry.description || '—'}
</div>
</div>
<button
type="button"
className="library-card-expand"
onClick={() => openPreview(entry.id)}
title={t('settings.memoryPreview')}
>
<Icon
name={previewId === entry.id ? 'chevron-down' : 'chevron-right'}
size={14}
/>
</button>
<button
type="button"
className="ghost library-card-action"
onClick={() => startEdit(entry.id)}
title={t('settings.memoryEdit')}
>
<Icon name="edit" size={14} />
</button>
<button
type="button"
className="ghost library-card-action"
onClick={() => onDelete(entry.id)}
title={t('settings.memoryDelete')}
>
<Icon name="close" size={14} />
</button>
<div className="memory-card-actions">
<button
type="button"
className="library-card-expand"
onClick={() => openPreview(entry.id)}
title={t('settings.memoryPreview')}
>
<Icon
name={previewId === entry.id ? 'chevron-down' : 'chevron-right'}
size={14}
/>
</button>
<button
type="button"
className="ghost library-card-action"
onClick={() => startEdit(entry.id)}
title={t('settings.memoryEdit')}
>
<Icon name="edit" size={14} />
</button>
<button
type="button"
className="ghost library-card-action"
onClick={() => onDelete(entry.id)}
title={t('settings.memoryDelete')}
>
<Icon name="close" size={14} />
</button>
</div>
{previewId === entry.id && (
<div className="library-preview" style={{ width: '100%' }}>
{previewBody === null ? (
@ -1530,15 +1532,17 @@ export function MemorySection({
</div>
) : null}
</div>
<button
type="button"
className="ghost library-card-action"
onClick={() => void onDeleteExtraction(record.id)}
title={t('settings.memoryExtractionDelete')}
aria-label={t('settings.memoryExtractionDelete')}
>
<Icon name="close" size={14} />
</button>
<div className="memory-card-actions">
<button
type="button"
className="ghost library-card-action"
onClick={() => void onDeleteExtraction(record.id)}
title={t('settings.memoryExtractionDelete')}
aria-label={t('settings.memoryExtractionDelete')}
>
<Icon name="close" size={14} />
</button>
</div>
</div>
);
};
@ -2280,12 +2284,7 @@ export function MemorySection({
{children.map((child) => (
<li
key={child.id}
style={{
display: 'grid',
gridTemplateColumns: 'minmax(0, 1fr) auto',
alignItems: 'center',
gap: 8,
}}
className="memory-tree-child-row"
>
<span style={{ minWidth: 0 }}>
<span className="library-card-name">{child.name}</span>{' '}
@ -2299,14 +2298,16 @@ export function MemorySection({
</span>
) : null}
</span>
<button
type="button"
className="ghost library-card-action"
onClick={() => startEdit(child.id)}
title={t('settings.memoryEdit')}
>
<Icon name="edit" size={14} />
</button>
<div className="memory-card-actions">
<button
type="button"
className="ghost library-card-action"
onClick={() => startEdit(child.id)}
title={t('settings.memoryEdit')}
>
<Icon name="edit" size={14} />
</button>
</div>
</li>
))}
</ul>

View file

@ -1345,7 +1345,7 @@
.memory-records-section .library-card {
display: grid;
grid-template-columns: minmax(0, 1fr) repeat(3, 30px);
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
min-height: 58px;
@ -1394,6 +1394,21 @@
line-height: 1.35;
}
.memory-card-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 6px;
justify-self: end;
}
.memory-tree-child-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
}
.memory-records-section .library-card-expand,
.memory-records-section .library-card .library-card-action {
width: 30px;
@ -2212,6 +2227,15 @@
grid-column: 2;
width: fit-content;
}
.memory-records-section .library-card,
.memory-tree-child-row {
grid-template-columns: 1fr;
}
.memory-card-actions {
width: 100%;
}
}
/* Memory section: monospace path showing where the memory files live on

View file

@ -281,6 +281,89 @@ describe('MemorySection', () => {
expect(screen.getByDisplayValue('- Keep design-system extraction in the loop')).toBeTruthy();
});
it('anchors memory tree entry edit controls in the right-side action zone', async () => {
globalThis.EventSource = StubEventSource as unknown as typeof EventSource;
const entry = {
id: 'project_design_agent_goal',
name: 'Design agent goal',
description: 'Open Design should evolve from accepted work',
type: 'project',
body: '- Keep design-system extraction in the loop',
updatedAt: Date.now(),
};
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: [entry],
extraction: null,
}), { status: 200, headers: { 'content-type': 'application/json' } });
}
if (url === '/api/memory/tree') {
return new Response(JSON.stringify({
enabled: true,
rootDir: '/tmp/memory',
tree: [
{
id: 'folder:project',
parentId: null,
path: '/project',
name: 'Project',
kind: 'folder',
type: 'project',
scope: 'project',
sourcePacketIds: [],
proposalIds: [],
createdAt: new Date(entry.updatedAt).toISOString(),
updatedAt: new Date(entry.updatedAt).toISOString(),
childrenCount: 1,
},
{
id: entry.id,
parentId: 'folder:project',
path: `/project/${entry.id}`,
name: entry.name,
description: entry.description,
kind: 'entry',
type: 'project',
scope: 'project',
sourcePacketIds: [],
proposalIds: [],
createdAt: new Date(entry.updatedAt).toISOString(),
updatedAt: new Date(entry.updatedAt).toISOString(),
childrenCount: 0,
},
],
}), { 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' },
});
}
return new Response(JSON.stringify({}), { status: 404 });
}) as typeof fetch;
renderMemorySection();
const treeDetails = (await screen.findByText('Memory tree')).closest('details')!;
const childRow = within(treeDetails)
.getByText('Design agent goal')
.closest('.memory-tree-child-row') as HTMLElement;
const editButton = within(childRow).getByTitle('Edit');
const actionZone = editButton.closest('.memory-card-actions');
const childContent = childRow.firstElementChild as HTMLElement;
expect(actionZone).toBeTruthy();
expect(actionZone?.parentElement).toBe(childRow);
expect(childContent.contains(editButton)).toBe(false);
});
it('shows unsaved index state and saves the updated index', async () => {
globalThis.EventSource = StubEventSource as unknown as typeof EventSource;
let savedIndex = '# Memory\n\n- Existing bullet\n';