mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 14:25:51 +00:00
feat: 添加 UI 组件增强与测试覆盖
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,79 @@
|
||||
// Auto-generated stub — replace with real implementation
|
||||
import type React from 'react';
|
||||
import type { AgentMemoryScope } from '@claude-code-best/builtin-tools/tools/AgentTool/agentMemory.js';
|
||||
import React from 'react'
|
||||
import { Dialog, Text } from '@anthropic/ink'
|
||||
import type { AgentMemoryScope } from '@claude-code-best/builtin-tools/tools/AgentTool/agentMemory.js'
|
||||
import { Select } from '../CustomSelect/index.js'
|
||||
|
||||
export {};
|
||||
export const SnapshotUpdateDialog: React.FC<{
|
||||
agentType: string;
|
||||
scope: AgentMemoryScope;
|
||||
snapshotTimestamp: string;
|
||||
onComplete: (choice: 'merge' | 'keep' | 'replace') => void;
|
||||
onCancel: () => void;
|
||||
}> = (() => null);
|
||||
export const buildMergePrompt: (agentType: string, scope: AgentMemoryScope) => string = (() => '');
|
||||
interface SnapshotUpdateDialogProps {
|
||||
agentType: string
|
||||
scope: AgentMemoryScope
|
||||
snapshotTimestamp: string
|
||||
onComplete: (choice: 'merge' | 'keep' | 'replace') => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
// Ink uses React.createElement instead of JSX here so the real implementation
|
||||
// can live in a .ts file (bun's `.js` import resolver picks up .ts before
|
||||
// .tsx in this repo's layout, so co-locating both extensions would shadow
|
||||
// this module with an empty stub).
|
||||
export function SnapshotUpdateDialog({
|
||||
agentType,
|
||||
scope,
|
||||
snapshotTimestamp,
|
||||
onComplete,
|
||||
onCancel,
|
||||
}: SnapshotUpdateDialogProps): React.ReactElement {
|
||||
const children = [
|
||||
React.createElement(
|
||||
Text,
|
||||
{ dimColor: true, key: 'timestamp' },
|
||||
`Snapshot timestamp: ${snapshotTimestamp}`,
|
||||
),
|
||||
React.createElement(Select, {
|
||||
key: 'select',
|
||||
defaultFocusValue: 'merge',
|
||||
options: [
|
||||
{
|
||||
label: 'Merge snapshot into current memory',
|
||||
value: 'merge',
|
||||
description:
|
||||
'Keep current memory and ask Claude to merge in the snapshot changes.',
|
||||
},
|
||||
{
|
||||
label: 'Keep current memory',
|
||||
value: 'keep',
|
||||
description:
|
||||
'Ignore this snapshot update and continue with current memory.',
|
||||
},
|
||||
{
|
||||
label: 'Replace with snapshot',
|
||||
value: 'replace',
|
||||
description:
|
||||
'Overwrite current memory files with the snapshot contents.',
|
||||
},
|
||||
],
|
||||
onChange: onComplete as (value: unknown) => void,
|
||||
}),
|
||||
]
|
||||
return React.createElement(Dialog, {
|
||||
title: 'Agent memory snapshot update',
|
||||
subtitle: `A newer ${scope} memory snapshot is available for ${agentType}.`,
|
||||
onCancel,
|
||||
color: 'warning' as const,
|
||||
children,
|
||||
})
|
||||
}
|
||||
|
||||
export function buildMergePrompt(
|
||||
agentType: string,
|
||||
scope: AgentMemoryScope,
|
||||
): string {
|
||||
return `A newer ${scope} persistent memory snapshot is available for the "${agentType}" agent.
|
||||
|
||||
Please merge the snapshot update into the current ${scope} agent memory before continuing:
|
||||
- Preserve useful current memory entries.
|
||||
- Incorporate newer or more accurate information from the snapshot.
|
||||
- Resolve duplicates or conflicts in favor of the most current, specific information.
|
||||
- Keep the memory concise and relevant to future runs of this agent.
|
||||
|
||||
After merging, continue with the user's request.`
|
||||
}
|
||||
|
||||
115
src/components/agents/__tests__/SnapshotUpdateDialog.test.tsx
Normal file
115
src/components/agents/__tests__/SnapshotUpdateDialog.test.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import * as React from 'react';
|
||||
import { launchSnapshotUpdateDialog } from '../../../dialogLaunchers.js';
|
||||
import { buildMergePrompt, SnapshotUpdateDialog } from '../SnapshotUpdateDialog.js';
|
||||
import { Select } from '../../CustomSelect/index.js';
|
||||
|
||||
function getSnapshotDialogFromRenderedTree(rendered: React.ReactElement) {
|
||||
const appStateProvider = rendered as React.ReactElement<{
|
||||
children: React.ReactElement;
|
||||
}>;
|
||||
const keybindingSetup = appStateProvider.props.children as React.ReactElement<{
|
||||
children: React.ReactElement;
|
||||
}>;
|
||||
return keybindingSetup.props.children as React.ReactElement<{
|
||||
agentType: string;
|
||||
scope: string;
|
||||
snapshotTimestamp: string;
|
||||
onComplete: (choice: 'merge' | 'keep' | 'replace') => void;
|
||||
onCancel: () => void;
|
||||
}>;
|
||||
}
|
||||
|
||||
async function waitForRender(getRendered: () => React.ReactElement | null): Promise<React.ReactElement> {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const rendered = getRendered();
|
||||
if (rendered) return rendered;
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
}
|
||||
throw new Error('Snapshot update dialog was not rendered');
|
||||
}
|
||||
|
||||
describe('SnapshotUpdateDialog', () => {
|
||||
test('launchSnapshotUpdateDialog wires props and keep-on-cancel semantics through showSetupDialog', async () => {
|
||||
let rendered: React.ReactElement | null = null;
|
||||
const root = {
|
||||
render(node: React.ReactElement) {
|
||||
rendered = node;
|
||||
},
|
||||
} as any;
|
||||
|
||||
const resultPromise = launchSnapshotUpdateDialog(root, {
|
||||
agentType: 'researcher',
|
||||
scope: 'project',
|
||||
snapshotTimestamp: '2026-04-15T12:00:00.000Z',
|
||||
});
|
||||
|
||||
const dialogElement = getSnapshotDialogFromRenderedTree(await waitForRender(() => rendered));
|
||||
|
||||
expect(dialogElement.type).toBe(SnapshotUpdateDialog);
|
||||
expect(dialogElement.props.agentType).toBe('researcher');
|
||||
expect(dialogElement.props.scope).toBe('project');
|
||||
expect(dialogElement.props.snapshotTimestamp).toBe('2026-04-15T12:00:00.000Z');
|
||||
|
||||
dialogElement.props.onCancel();
|
||||
await expect(resultPromise).resolves.toBe('keep');
|
||||
});
|
||||
|
||||
test('launchSnapshotUpdateDialog forwards explicit completion choices', async () => {
|
||||
let rendered: React.ReactElement | null = null;
|
||||
const root = {
|
||||
render(node: React.ReactElement) {
|
||||
rendered = node;
|
||||
},
|
||||
} as any;
|
||||
|
||||
const resultPromise = launchSnapshotUpdateDialog(root, {
|
||||
agentType: 'researcher',
|
||||
scope: 'user',
|
||||
snapshotTimestamp: '2026-04-15T12:00:00.000Z',
|
||||
});
|
||||
|
||||
const dialogElement = getSnapshotDialogFromRenderedTree(await waitForRender(() => rendered));
|
||||
dialogElement.props.onComplete('replace');
|
||||
|
||||
await expect(resultPromise).resolves.toBe('replace');
|
||||
});
|
||||
|
||||
test('buildMergePrompt is non-empty and varies with both agentType and scope', () => {
|
||||
const projectPrompt = buildMergePrompt('researcher', 'project');
|
||||
const userPrompt = buildMergePrompt('researcher', 'user');
|
||||
const plannerPrompt = buildMergePrompt('planner', 'project');
|
||||
|
||||
expect(projectPrompt.trim().length).toBeGreaterThan(0);
|
||||
expect(projectPrompt).toContain('researcher');
|
||||
expect(projectPrompt).toContain('project');
|
||||
expect(projectPrompt.toLowerCase()).toContain('snapshot');
|
||||
expect(projectPrompt.toLowerCase()).toContain('merge');
|
||||
expect(projectPrompt).not.toBe(userPrompt);
|
||||
expect(projectPrompt).not.toBe(plannerPrompt);
|
||||
});
|
||||
|
||||
test('renders snapshot metadata and choice options from its public props', () => {
|
||||
const element = SnapshotUpdateDialog({
|
||||
agentType: 'researcher',
|
||||
scope: 'project',
|
||||
snapshotTimestamp: '2026-04-15T12:00:00.000Z',
|
||||
onComplete: () => {},
|
||||
onCancel: () => {},
|
||||
} as any) as React.ReactElement<{ title: string; subtitle: string; children: React.ReactNode[] }>;
|
||||
|
||||
expect(element.props.title).toBe('Agent memory snapshot update');
|
||||
expect(element.props.subtitle).toContain('researcher');
|
||||
expect(element.props.subtitle).toContain('project');
|
||||
|
||||
const [timestamp, select] = element.props.children as Array<React.ReactElement<Record<string, any>>>;
|
||||
expect(timestamp.props.children).toContain('2026-04-15T12:00:00.000Z');
|
||||
expect(select.type).toBe(Select);
|
||||
expect(select.props.options.map((option: { value: string }) => option.value)).toEqual(['merge', 'keep', 'replace']);
|
||||
expect(select.props.options.map((option: { label: string }) => option.label)).toEqual([
|
||||
'Merge snapshot into current memory',
|
||||
'Keep current memory',
|
||||
'Replace with snapshot',
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user