diff --git a/docs/module-graph.md b/docs/module-graph.md index 82763bdc55..015052e38d 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -94,6 +94,7 @@ flowchart TD subgraph group_ui["packages/ui"] pkg_acp["acp"] pkg_app_boot["app-boot"] + pkg_desktop["desktop"] pkg_jsonrpc["jsonrpc"] pkg_permission["permission"] pkg_stdio["stdio"] @@ -383,6 +384,7 @@ flowchart TD | [`acp-snapshot`](../packages/support/acp-snapshot) | `support` | — | | [`loader-smoke`](../packages/support/loader-smoke) | `support` | — | | [`app-boot`](../packages/ui/app-boot) | `ui` | — | +| [`desktop`](../packages/ui/desktop) | `ui` | — | | [`code-runtime`](../packages/code-runtime/code-runtime) | `code-runtime` | — | | [`jsonrpc-demo`](../packages/examples/jsonrpc-demo) | `examples` | — | | [`llm`](../packages/llm/llm) | `llm` | [`brand`](../packages/util/brand) | diff --git a/knip.json b/knip.json index b978da3a76..4461bbcb98 100644 --- a/knip.json +++ b/knip.json @@ -94,6 +94,11 @@ "entry": ["tests/**/*.spec.ts"], "project": ["src/**/*.ts", "tests/**/*.ts"] }, + "packages/ui/desktop": { + "entry": ["tests/**/*.spec.ts"], + "project": ["src/**/*.ts", "tests/**/*.ts"], + "ignoreDependencies": ["cordis"] + }, "packages/examples/jsonrpc-demo": { "project": ["src/**/*.ts"] }, diff --git a/packages/ui/desktop/README.md b/packages/ui/desktop/README.md index 54a5da839e..e808ee5416 100644 --- a/packages/ui/desktop/README.md +++ b/packages/ui/desktop/README.md @@ -171,14 +171,33 @@ Start with a desktop package that defines shared UI contracts, then build the El - Replay creates a separate candidate run with lineage metadata. - Compare operates over two runs, not "inside" one session. -## Known limitations and deferred work +## Model Experience -This package now ships a usable Electron/Vite development app and a real ACP subprocess bridge. It is still a v1 workbench rather than a packaged distributable. +### Composer prompt -The first runtime channel is ACP. Direct in-process embedding would make context queries and restarts richer, but it makes isolation, teardown, and hot reload harder and should wait until the ACP path is working. +**What the model sees**: The desktop composer sends the user's text to the managed ACP runtime as the `session/prompt` content. This package adds no extra system prompt, steering prose, or tool definition of its own. -The first Dev panel is agent-assisted. A direct graphical plugin/config editor should come after the app can reliably run, replay, compare, and restart the runtime. +**Token effect**: User-message tokens are data-dependent and then follow the active runtime's normal session-retention and compaction behavior. Electron shell chrome, inspector state, and the language toggle add zero model-context tokens. -The current trace/context surfaces read persisted JSONL after turns complete and use ACP live updates for streaming chat. A richer live raw-event side channel would make Trajectory/Context update at token-time rather than after the persistence flush. +### Runtime context evidence -The first Compare view is structural and textual. Semantic evaluation and dataset-level analysis belong to a later evaluation product surface. +**What the model sees**: The visible system prompt, steering messages, tool set, message prefix, and config come from the active `cordis.yml` composition and its plugins. The desktop app only reads those emitted facts from ACP updates and persisted JSONL for inspection. + +**Token effect**: No additional tokens are introduced by viewing `Chat`, `Trajectory`, `Waterfall`, or `Develop`. The displayed `request/header`, `context/message`, and `steering/message` data reflect tokens the runtime already assembled for the model. + +## Known Limitations and Deferred Work + +- **Development build only** — this package ships a usable Electron/Vite app and + a real ACP subprocess bridge, but it is not yet packaged as a signed + distributable. +- **ACP is the first runtime channel** — direct in-process embedding could make + context queries and restarts richer, but would make isolation, teardown, and + hot reload harder. +- **Develop is read-first** — it exposes prompts, tools, plugins, config, + runtime state, and the change loop as a source browser; direct graphical + plugin/config editing is deferred. +- **Trace refresh is mixed live/persisted** — chat streams from ACP live updates, + while Trajectory and Waterfall currently read persisted JSONL after turns + complete. +- **Compare and replay remain skeletal** — the product contract is documented, + but semantic evaluation and dataset-level analysis belong to later work. diff --git a/packages/ui/desktop/package.json b/packages/ui/desktop/package.json index 67c20abc5d..c0d665fdb1 100644 --- a/packages/ui/desktop/package.json +++ b/packages/ui/desktop/package.json @@ -4,7 +4,7 @@ "version": "0.0.1", "private": true, "type": "module", - "main": "src/main.mjs", + "main": "lib/index.js", "types": "lib/types/index.d.ts", "scripts": { "dev": "node scripts/dev.mjs", @@ -27,5 +27,18 @@ "lib/types/**/*.d.ts.map", "src" ], - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "dependencies": { + "@agentclientprotocol/sdk": "0.25.1" + }, + "peerDependencies": { + "cordis": "^4.0.0-rc.7" + }, + "devDependencies": { + "cordis": "^4.0.0-rc.7", + "electron": "^43.1.1", + "typescript": "^6.0.3", + "vite": "^8.0.16", + "vitest": "^4.1.8" + } } diff --git a/packages/ui/desktop/src/app.ts b/packages/ui/desktop/src/app.ts index 2e6b8d6fd7..87b0b20ea7 100644 --- a/packages/ui/desktop/src/app.ts +++ b/packages/ui/desktop/src/app.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unnecessary-condition, @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unnecessary-type-conversion, @stylistic/max-len */ import { DEFAULT_FEEDBACK_AUTHOR, INSPECTOR_TABS, @@ -10,6 +11,11 @@ import { import { translate, type I18nKey, type Locale } from './i18n.ts' import './styles.css' +const PATH_SYSTEM_PROMPT = 'packages/core/system-prompt/src/index.ts' +const PATH_TOOL_REGISTRY = 'packages/core/tools/src/index.ts' +const PATH_TOOL_BASH = 'packages/bash/tool-bash/src/index.ts' +const PATH_TOOL_SUBAGENT = 'packages/subagent/tool-subagent/src/index.ts' + interface SessionSummary { readonly id: string readonly title: string @@ -109,7 +115,7 @@ interface TargetPayload { } interface RequestContextSnapshot { - readonly event?: SessionEvent + readonly event: SessionEvent | undefined readonly seq: number readonly header: Record readonly delta: Record @@ -141,7 +147,7 @@ interface DevArtifactGroup { type AppModule = 'sessions' | 'develop' const SESSION_SURFACES: readonly DesktopSurface[] = ['chat', 'trajectory', 'waterfall'] -const initialLocale = (localStorage.getItem('dsh.locale') === 'en-US' ? 'en-US' : 'zh-CN') as Locale +const initialLocale: Locale = localStorage.getItem('dsh.locale') === 'en-US' ? 'en-US' : 'zh-CN' const state = { runtime: undefined as unknown, @@ -175,20 +181,20 @@ void boot() async function boot(): Promise { if (!hasDesktopApi()) { - state.error = 'Desktop API is not available. Open the Electron window with: pnpm --dir packages/ui/desktop run dev.' + state.error = `${t('error.noDesktopApi')} pnpm --dir packages/ui/desktop run dev` render() return } - window.dshDesktop.runtime.onStatus(payload => { + window.dshDesktop.runtime.onStatus((payload) => { state.runtime = payload render() }) - window.dshDesktop.runtime.onStderr(payload => { + window.dshDesktop.runtime.onStderr((payload) => { state.stderr = String(asRecord(payload).tail ?? asRecord(payload).text ?? '') render() }) - window.dshDesktop.sessions.onUpdate(payload => { + window.dshDesktop.sessions.onUpdate((payload) => { handleSessionUpdate(asSessionUpdate(payload)) }) @@ -259,7 +265,7 @@ function handleSessionUpdate(payload: SessionUpdatePayload): void { else rows.push(row) state.liveRows.set(payload.sessionId, rows) } else if (kind === 'user_message_chunk') { - rows.push(makeSyntheticRow(payload.sessionId, 'user', 'User', contentText(update.content), `user-${rows.length}`)) + rows.push(makeSyntheticRow(payload.sessionId, 'user', t('chat.user'), contentText(update.content), `user-${rows.length}`)) state.liveRows.set(payload.sessionId, rows) } @@ -339,7 +345,7 @@ function renderSessionGroup(title: string, sessions: SessionSummary[]): string {
${escapeHtml(title)}
- ${sessions.map(renderSessionItem).join('') || `
No ${escapeHtml(title.toLowerCase())} sessions
`} + ${sessions.map(renderSessionItem).join('') || `
${escapeHtml(t('app.emptySessions'))}
`}
` @@ -404,7 +410,7 @@ function renderMainContent(session: SessionSummary | undefined): string { if (session === undefined) return state.draftChat ? renderDraftChat() : renderEmptySession() return `
- ${state.error ? `
Error${escapeHtml(state.error)}
` : ''} + ${state.error ? `
${escapeHtml(t('app.errorTitle'))}${escapeHtml(state.error)}
` : ''} ${state.activeSurface === 'chat' ? '' : renderSessionHeader(session)} ${renderActiveSurface()}
@@ -653,33 +659,6 @@ function renderSpanRow(span: SpanRow, total: number): string { ` } -function renderContextSurface(): string { - const rows = contextRows() - return ` -
-
- Context - 这是开发分析视图:它解释模型请求边界里真正进入上下文的内容,用来改 prompt、tool schema 和 config。 -
-
- ${rows.map(renderContextCard).join('') || renderEmptyTrace()} -
-
- ` -} - -function renderContextCard(row: ContextRow): string { - return ` - - ` -} - function renderDevelopModule(): string { const groups = developArtifactGroups() const artifacts = groups.flatMap(group => group.artifacts) @@ -703,8 +682,8 @@ function renderDevelopModule(): string { function renderEmptyDevBrowser(): string { return `
- No development artifacts found - Start the runtime or check the active cordis.yml config. + ${escapeHtml(t('dev.emptyTitle'))} + ${escapeHtml(t('dev.emptyBody'))}
` } @@ -744,14 +723,14 @@ function renderDevArtifactDetail(artifact: DevArtifact): string { ${escapeHtml(artifact.status ?? artifact.kind)}
- ${renderDevFact('Source', artifact.source ?? 'unknown')} - ${renderDevFact('Owner', artifact.owner ?? 'unknown')} - ${renderDevFact('Recently used', artifact.recent ?? 'No recent evidence yet')} - ${renderDevFact('Reload', reloadLabelForArtifact(artifact))} + ${renderDevFact(t('dev.source'), artifact.source ?? t('dev.unknown'))} + ${renderDevFact(t('dev.owner'), artifact.owner ?? t('dev.unknown'))} + ${renderDevFact(t('dev.recentlyUsed'), artifact.recent ?? t('dev.noRecentEvidence'))} + ${renderDevFact(t('dev.reload'), reloadLabelForArtifact(artifact))}
${renderDevCodePanel(contentTitleForArtifact(artifact), contentMetaForArtifact(artifact), artifact.value ?? '')} ${artifact.id === 'prompt:persona' ? renderDevRegistrySnapshot() : ''} - ${artifact.metadata === undefined ? '' : renderDevCodePanel('Metadata', 'Implementation, dependency, and last-seen evidence', artifact.metadata)} + ${artifact.metadata === undefined ? '' : renderDevCodePanel(t('dev.metadata'), t('dev.metadataSubtitle'), artifact.metadata)} ${artifact.kind === 'runtime' ? renderRuntimePanel() : ''} ${artifact.kind === 'change' ? renderChangeLoopPanel() : ''} @@ -774,12 +753,12 @@ function renderDevRegistrySnapshot(): string { return `
-

Registered plugins

- ${renderDevPillList(plugins, 'No registered plugins found')} +

${escapeHtml(t('dev.registeredPlugins'))}

+ ${renderDevPillList(plugins, t('dev.noPlugins'))}
-

Registered tool surfaces

- ${renderDevPillList(tools, 'No registered tools found yet')} +

${escapeHtml(t('dev.registeredTools'))}

+ ${renderDevPillList(tools, t('dev.noTools'))}
` @@ -800,39 +779,39 @@ function renderDevPillList(artifacts: DevArtifact[], empty: string): string { } function contentTitleForArtifact(artifact: DevArtifact): string { - if (artifact.kind === 'prompt') return 'Effective prompt content' - if (artifact.kind === 'tool') return 'Tool schema' - if (artifact.kind === 'plugin') return 'Plugin config / contribution' - if (artifact.kind === 'config') return 'Active configuration' - if (artifact.kind === 'runtime') return 'Runtime state' - return 'Suggested verification loop' + if (artifact.kind === 'prompt') return t('dev.effectivePromptContent') + if (artifact.kind === 'tool') return t('dev.toolSchema') + if (artifact.kind === 'plugin') return t('dev.pluginContribution') + if (artifact.kind === 'config') return t('dev.activeConfiguration') + if (artifact.kind === 'runtime') return t('dev.runtimeState') + return t('dev.suggestedLoop') } function contentMetaForArtifact(artifact: DevArtifact): string { - if (artifact.kind === 'prompt') return 'Current source-level prompt text or prompt owner metadata' - if (artifact.kind === 'tool') return 'Current registered schema when last seen by a model request' - if (artifact.kind === 'plugin') return 'Who injects prompt/context or registers tools, based on active Cordis config' - if (artifact.kind === 'config') return 'Model/runtime parameters and files whose edits require reload' - if (artifact.kind === 'runtime') return 'Current Electron main process and ACP bridge state' - return 'How to rerun and compare after editing the agent' + if (artifact.kind === 'prompt') return t('dev.promptMeta') + if (artifact.kind === 'tool') return t('dev.toolMeta') + if (artifact.kind === 'plugin') return t('dev.pluginMeta') + if (artifact.kind === 'config') return t('dev.configMeta') + if (artifact.kind === 'runtime') return t('dev.runtimeMeta') + return t('dev.loopMeta') } function reloadLabelForArtifact(artifact: DevArtifact): string { - if (artifact.kind === 'runtime') return 'Manual restart available here' - if (artifact.kind === 'change') return String(asRecord(state.dev).restartNeeded ?? false) === 'true' ? 'Restart recommended' : 'No restart signal' - if (artifact.kind === 'config' || artifact.kind === 'prompt' || artifact.kind === 'tool' || artifact.kind === 'plugin') return 'Restart ACP after editing' - return 'unknown' + if (artifact.kind === 'runtime') return t('dev.manualRestart') + if (artifact.kind === 'change') return String(asRecord(state.dev).restartNeeded ?? false) === 'true' ? t('dev.restartRecommended') : t('dev.noRestartSignal') + if (artifact.kind === 'config' || artifact.kind === 'prompt' || artifact.kind === 'tool' || artifact.kind === 'plugin') return t('dev.restartAfterEdit') + return t('dev.unknown') } function renderChangeLoopPanel(): string { return `
- Recommended loop + ${escapeHtml(t('dev.recommendedLoop'))}
    -
  1. Edit the prompt, tool, plugin, or config source.
  2. -
  3. Restart ACP runtime if the changed file is loaded at process start.
  4. -
  5. Return to Chat and rerun a previous task or start a new one.
  6. -
  7. Use Trajectory / Waterfall to compare behavior and timing evidence.
  8. +
  9. ${escapeHtml(t('dev.loopStepEdit'))}
  10. +
  11. ${escapeHtml(t('dev.loopStepRestart'))}
  12. +
  13. ${escapeHtml(t('dev.loopStepRerun'))}
  14. +
  15. ${escapeHtml(t('dev.loopStepCompare'))}
` @@ -860,7 +839,7 @@ function developArtifactGroups(): DevArtifactGroup[] { source: String(composition.configPath ?? 'examples/acp-agent/cordis.yml'), owner: '@deepseek-ai/dsh-acp-demo -> @deepseek-ai/dsh-system-prompt', value: persona || 'No persona block found in the active config.', - metadata: promptOwner ?? { path: 'packages/system-prompt/system-prompt/src/index.ts' }, + metadata: promptOwner ?? { path: PATH_SYSTEM_PROMPT }, recent: recentPromptSummary(recentPromptUses, request), }, { @@ -870,9 +849,9 @@ function developArtifactGroups(): DevArtifactGroup[] { title: 'Prompt assembly service', subtitle: 'Owns persona, steering sections, and tool-order assembly', status: 'source', - source: String(asRecord(promptOwner).path ?? 'packages/system-prompt/system-prompt/src/index.ts'), + source: String(asRecord(promptOwner).path ?? PATH_SYSTEM_PROMPT), owner: '@deepseek-ai/dsh-system-prompt', - value: promptOwner ?? { path: 'packages/system-prompt/system-prompt/src/index.ts' }, + value: promptOwner ?? { path: PATH_SYSTEM_PROMPT }, metadata: { lastSeenSystemPromptChars: request.system.length, messagePrefix: request.messagePrefix, @@ -972,23 +951,23 @@ function buildToolArtifacts(request: RequestContextSnapshot, plugins: unknown[], metadata: { fullTool: tool, ownerPlugin: plugin ?? 'No matching plugin inferred from active config', - registry: owner ?? { path: 'packages/tools/tools/src/index.ts' }, + registry: owner ?? { path: PATH_TOOL_REGISTRY }, recentToolCall: recent ?? 'No persisted call evidence found', }, recent: recentToolSummary(recent) || (request.event === undefined ? 'No request evidence yet' : `Schema last seen in request seq ${request.seq}`), } satisfies DevArtifact }) } - const toolPlugins = plugins.filter(plugin => { + const toolPlugins = plugins.filter((plugin) => { const record = asRecord(plugin) const id = String(record.id ?? '') const name = String(record.name ?? '') return id.includes('tool') || name.includes('tool') || id === 'bash' || id.includes('subagent') || id.includes('workflow') }) - return toolPlugins.map(plugin => { + return toolPlugins.map((plugin) => { const record = asRecord(plugin) const id = String(record.id ?? 'tool') - const recent = recentToolCalls.find(call => { + const recent = recentToolCalls.find((call) => { const name = String(asRecord(call).name) return toolLikelyOwnedByPlugin(name, record) || id.includes(name) }) @@ -1004,7 +983,7 @@ function buildToolArtifacts(request: RequestContextSnapshot, plugins: unknown[], value: String(record.configPreview ?? '').trim() || 'Tool schema will appear here after the first model request captures the registered tool list.', metadata: { plugin, - registry: owner ?? { path: 'packages/tools/tools/src/index.ts' }, + registry: owner ?? { path: PATH_TOOL_REGISTRY }, recentToolCall: recent ?? 'No persisted call evidence found', }, recent: recentToolSummary(recent) || 'No request schema loaded', @@ -1069,9 +1048,9 @@ function toolLikelyOwnedByPlugin(toolName: string, plugin: Record line.replace(/^ {6}/, '')) .join('\n') @@ -1092,8 +1071,8 @@ function extractPersonaFromConfig(text: string): string { function renderRuntimePanel(): string { return `
-
${renderKeyValue('Repo', shortPath(runtimeRepoRoot()))}${renderKeyValue('Branch', gitField('branch'))}${renderKeyValue('Commit', gitField('commit'))}${renderKeyValue('Dirty', gitField('dirty'))}${renderKeyValue('ACP', runtimeLabel())}${renderKeyValue('Restart needed', String(asRecord(state.dev).restartNeeded ?? false))}
- +
${renderKeyValue(t('app.repo'), shortPath(runtimeRepoRoot()))}${renderKeyValue(t('app.branch'), gitField('branch'))}${renderKeyValue(t('app.commit'), gitField('commit'))}${renderKeyValue(t('app.dirty'), gitField('dirty'))}${renderKeyValue(t('app.acp'), runtimeLabel())}${renderKeyValue(t('app.restartNeeded'), String(asRecord(state.dev).restartNeeded ?? false))}
+
` } @@ -1120,7 +1099,7 @@ function renderBottomArea(session: SessionSummary | undefined): string {
- +
` @@ -1152,7 +1131,7 @@ function renderInspector(): string { function renderInspectorTabs(feedbackCount = feedbackForTarget().length): string { return INSPECTOR_TABS.map(tab => ` `).join('') } @@ -1286,23 +1265,23 @@ function rowsFromEvents(events: readonly SessionEvent[]): ChatRow[] { const seq = event.seq ?? rows.length const data = asRecord(event.data) if (event.type === 'user/message') { - rows.push({ target: makeTarget('message', 'User message', seq, `seq ${seq}`), role: 'user', title: 'User', body: contentText(data.content), eventSeqs: [seq] }) + rows.push({ target: makeTarget('message', t('chat.userMessage'), seq, `seq ${seq}`), role: 'user', title: t('chat.user'), body: contentText(data.content), eventSeqs: [seq] }) } else if (event.type === 'assistant/message') { const key = `${data.turn}:${data.step}` const reasoning = reasoningByStep.get(key) if (reasoning !== undefined && !seenReasoningStep.has(key)) { seenReasoningStep.add(key) rows.push({ - target: makeTarget('assistant-stream', 'Thinking', reasoning.seq, `turn ${String(data.turn)} step ${String(data.step)}`), + target: makeTarget('assistant-stream', t('chat.thinking'), reasoning.seq, `turn ${String(data.turn)} step ${String(data.step)}`), role: 'thinking', - title: 'Thinking', + title: t('chat.thinking'), body: reasoning.text, eventSeqs: [reasoning.seq], collapsed: true, badge: 'folded', }) } - rows.push({ target: makeTarget('message', 'Assistant message', seq, `seq ${seq}`), role: 'assistant', title: 'Assistant', body: contentText(data.content), eventSeqs: [seq] }) + rows.push({ target: makeTarget('message', t('chat.assistantMessage'), seq, `seq ${seq}`), role: 'assistant', title: t('chat.assistant'), body: contentText(data.content), eventSeqs: [seq] }) } else if (event.type === 'tool/call') { const callId = String(data.callId ?? '') const result = toolResults.get(callId) @@ -1310,7 +1289,7 @@ function rowsFromEvents(events: readonly SessionEvent[]): ChatRow[] { rows.push({ target: makeTarget('tool-call', `Tool · ${String(data.name ?? 'tool')}`, seq, `seq ${seq}`), role: 'tool', - title: `Tool use · ${String(data.name ?? 'tool')}`, + title: `${t('chat.toolUse')} · ${String(data.name ?? 'tool')}`, body: renderValue({ input: data.arguments ?? data.rawInput ?? data, output: resultData.content ?? resultData.output, @@ -1339,11 +1318,11 @@ function flushLiveDrafts(sessionId: string): ChatRow[] { const rows = [...(state.liveRows.get(sessionId) ?? [])] const thinking = state.pendingThinking.get(sessionId) if (thinking !== undefined && thinking.length > 0) { - rows.push(makeSyntheticRow(sessionId, 'thinking', 'Thinking', thinking, 'thinking-live', true)) + rows.push(makeSyntheticRow(sessionId, 'thinking', t('chat.thinking'), thinking, 'thinking-live', true)) } const assistant = state.pendingAssistant.get(sessionId) if (assistant !== undefined && assistant.length > 0) { - rows.push(makeSyntheticRow(sessionId, 'assistant', 'Assistant', assistant, 'assistant-live')) + rows.push(makeSyntheticRow(sessionId, 'assistant', t('chat.assistant'), assistant, 'assistant-live')) } return rows } @@ -1369,7 +1348,7 @@ function makeSyntheticRow( body, eventSeqs: [], collapsed, - badge: collapsed ? 'live' : undefined, + ...(collapsed ? { badge: 'live' } : {}), } } @@ -1419,7 +1398,7 @@ function treeRow( title, subtitle, meta: `seq ${seq}`, - tone, + ...(tone === undefined ? {} : { tone }), } } @@ -1456,7 +1435,7 @@ function timedRow(title: string, start: SessionEvent, end: SessionEvent, kind: I subtitle: `${eventTime(start)} → ${eventTime(end)}`, startMs: Math.max(0, (start.time ?? 0) - zero), durationMs, - tone, + ...(tone === undefined ? {} : { tone }), } } @@ -1484,14 +1463,14 @@ function contextRows(): ContextRow[] { const request = latestRequestContext() const rows: ContextRow[] = [] if (request.event !== undefined) { - rows.push(contextRow('System prompt', `${request.system.length} chars`, request.system || 'No system prompt found in latest request header.', 'system', 'request', request.seq, Boolean(request.delta.system))) - rows.push(contextRow('Tool schemas', `${request.tools.length} tools`, request.tools.length > 0 ? request.tools.map(tool => String(asRecord(tool).name ?? 'tool')).join(', ') : 'No tool schemas found in latest request header.', 'tools', 'request', request.seq, Boolean(request.delta.tools))) - rows.push(contextRow('Call config', request.config === undefined ? 'empty' : 'available', renderValue(request.config), 'config', 'request', request.seq, Boolean(request.delta.config))) - rows.push(contextRow('Message prefix', Array.isArray(request.messagePrefix) ? `${request.messagePrefix.length} messages` : 'derived', renderValue(request.messagePrefix), 'messages', 'request', request.seq, Boolean(request.delta.messagePrefix))) + rows.push(contextRow(t('context.systemPrompt'), `${request.system.length} chars`, request.system || t('context.noSystem'), 'system', 'request', request.seq, Boolean(request.delta.system))) + rows.push(contextRow(t('context.toolSchemas'), `${request.tools.length} ${t('waterfall.tools')}`, request.tools.length > 0 ? request.tools.map(tool => String(asRecord(tool).name ?? 'tool')).join(', ') : t('context.noTools'), 'tools', 'request', request.seq, Boolean(request.delta.tools))) + rows.push(contextRow(t('context.callConfig'), request.config === undefined ? t('context.empty') : t('context.available'), renderValue(request.config), 'config', 'request', request.seq, Boolean(request.delta.config))) + rows.push(contextRow(t('context.messagePrefix'), Array.isArray(request.messagePrefix) ? `${request.messagePrefix.length} ${t('context.messages')}` : t('context.derived'), renderValue(request.messagePrefix), 'messages', 'request', request.seq, Boolean(request.delta.messagePrefix))) } const modelVisible = rowsFromEvents(events).filter(row => row.role === 'user' || row.role === 'assistant' || row.role === 'context') - rows.push(contextRow('Derived history', `${modelVisible.length} visible rows`, modelVisible.map(row => `${row.title}: ${truncate(row.body, 80)}`).join('\n'), 'messages', 'context-section', 0)) - rows.push(contextRow('Raw JSONL', `${events.length} events`, state.trace?.rawText ?? '', 'raw', 'session', 0)) + rows.push(contextRow(t('context.derivedHistory'), `${modelVisible.length} ${t('context.visibleRows')}`, modelVisible.map(row => `${row.title}: ${truncate(row.body, 80)}`).join('\n'), 'messages', 'context-section', 0)) + rows.push(contextRow(t('context.rawJsonl'), `${events.length} ${t('context.events')}`, state.trace?.rawText ?? '', 'raw', 'session', 0)) return rows } @@ -1591,7 +1570,7 @@ function feedbackForTarget(): FeedbackRecord[] { return (state.trace?.feedback ?? []).filter(record => record.data?.targetId === selected.id) } -document.addEventListener('click', event => { +document.addEventListener('click', (event) => { const element = event.target instanceof Element ? event.target : undefined const module = element?.closest('[data-module]')?.dataset.module as AppModule | undefined @@ -1657,7 +1636,7 @@ document.addEventListener('click', event => { void loadTrace(state.selectedSessionId).then(() => refreshSessions(state.selectedSessionId)) } else if (action === 'restart-runtime') { if (!hasDesktopApi()) { - state.error = 'Desktop API is not available. Use the Electron window, not the browser tab.' + state.error = t('error.noDesktopApi') render() return } @@ -1665,7 +1644,7 @@ document.addEventListener('click', event => { } }) -document.addEventListener('input', event => { +document.addEventListener('input', (event) => { const input = event.target instanceof HTMLInputElement ? event.target : undefined if (input?.dataset.search === 'true') { state.query = input.value @@ -1679,7 +1658,7 @@ document.addEventListener('input', event => { } }) -document.addEventListener('submit', event => { +document.addEventListener('submit', (event) => { const form = event.target instanceof HTMLFormElement ? event.target : undefined if (form?.dataset.promptForm === 'true') { event.preventDefault() @@ -1692,7 +1671,7 @@ document.addEventListener('submit', event => { } }) -document.addEventListener('keydown', event => { +document.addEventListener('keydown', (event) => { const textarea = event.target instanceof HTMLTextAreaElement ? event.target : undefined if (textarea !== undefined && textarea.closest('[data-prompt-form="true"]') !== null) { if (event.key !== 'Enter' || event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) return @@ -1731,7 +1710,7 @@ function startDraftChat(): void { async function createBackendSession(): Promise { if (!hasDesktopApi()) { - state.error = 'Desktop API is not available. Use the Electron window, not the browser tab.' + state.error = t('error.noDesktopApi') render() return undefined } @@ -1750,7 +1729,7 @@ async function createBackendSession(): Promise { async function sendPrompt(prompt: string, form: HTMLFormElement): Promise { if (!hasDesktopApi()) { - state.error = 'Desktop API is not available. Use the Electron window, not the browser tab.' + state.error = t('error.noDesktopApi') render() return } @@ -1761,7 +1740,7 @@ async function sendPrompt(prompt: string, form: HTMLFormElement): Promise const textarea = form.querySelector('textarea[name="prompt"]') if (textarea !== null) autosizeComposer(textarea) const rows = state.liveRows.get(sessionId) ?? [] - rows.push(makeSyntheticRow(sessionId, 'user', 'User', prompt, `user-${Date.now()}`)) + rows.push(makeSyntheticRow(sessionId, 'user', t('chat.user'), prompt, `user-${Date.now()}`)) state.liveRows.set(sessionId, rows) state.pendingAssistant.delete(sessionId) state.pendingThinking.delete(sessionId) @@ -1870,22 +1849,29 @@ function uniqueTurns(): number[] { } function moduleTitle(): string { - if (state.activeModule === 'sessions') return 'Sessions' - return 'Develop' + if (state.activeModule === 'sessions') return t('app.sessions') + return t('app.develop') } function topbarTitle(session: SessionSummary | undefined): string { if (state.activeModule !== 'sessions') return moduleTitle() - return session?.title || 'Sessions' + return session?.title || t('app.sessions') } function surfaceLabel(surface: DesktopSurface): string { if (surface === 'chat') return t('surface.chat') if (surface === 'trajectory') return t('surface.trajectory') if (surface === 'waterfall') return t('surface.waterfall') - if (surface === 'context') return 'Context' - if (surface === 'compare') return 'Compare' - return 'Dev' + if (surface === 'context') return t('surface.context') + if (surface === 'compare') return t('surface.compare') + return t('surface.dev') +} + +function inspectorTabLabel(tab: InspectorTab): string { + if (tab === 'input') return t('inspector.input') + if (tab === 'output') return t('inspector.output') + if (tab === 'metadata') return t('inspector.metadata') + return t('inspector.feedback') } function runtimeLabel(): string { @@ -1952,7 +1938,7 @@ function asRecord(value: unknown): Record { function contentText(value: unknown): string { if (!Array.isArray(value)) return '' - return value.map(block => { + return value.map((block) => { const record = asRecord(block) if (record.type === 'text' || record.type === 'reasoning') return String(record.text ?? '') if (record.type === 'resource_link') return `[resource ${String(record.name ?? '')}] ${String(record.uri ?? '')}` diff --git a/packages/ui/desktop/src/i18n.ts b/packages/ui/desktop/src/i18n.ts index 529c699474..3c11134a13 100644 --- a/packages/ui/desktop/src/i18n.ts +++ b/packages/ui/desktop/src/i18n.ts @@ -1,18 +1,30 @@ +/** Renderer locales currently supported by the desktop shell. */ export type Locale = 'zh-CN' | 'en-US' const messages = { 'zh-CN': { 'app.newChat': '新对话', 'app.sessions': 'Sessions', - 'app.sessionsSubtitle': 'Chat + trace in one处', + 'app.sessionsSubtitle': 'Chat + trace in one place', 'app.develop': 'Develop', 'app.developSubtitle': 'Prompt、工具、插件、运行时', 'app.searchPlaceholder': '搜索标题、id、模型', 'app.recentSessions': '最近 sessions', + 'app.emptySessions': '没有匹配的 sessions', 'app.language': 'EN', + 'app.errorTitle': '错误', + 'app.repo': 'Repo', + 'app.branch': 'Branch', + 'app.commit': 'Commit', + 'app.dirty': 'Dirty', + 'app.acp': 'ACP', + 'app.restartNeeded': 'Restart needed', 'surface.chat': 'Chat', 'surface.trajectory': 'Trajectory', 'surface.waterfall': 'Waterfall', + 'surface.context': 'Context', + 'surface.compare': 'Compare', + 'surface.dev': 'Dev', 'chat.details': '详情', 'chat.thinking': 'Thinking', 'chat.toolUse': 'Tool use', @@ -27,6 +39,10 @@ const messages = { 'chat.newBody': '先输入一句话。发送后才会创建真实 ACP session,并在左侧出现记录。', 'chat.startTitle': '开始一个 Deepseek Harness session', 'chat.startBody': '点击 New chat 只会打开草稿;真正发送第一句话后,才会创建后端 session。', + 'chat.user': 'User', + 'chat.assistant': 'Assistant', + 'chat.userMessage': 'User message', + 'chat.assistantMessage': 'Assistant message', 'trace.title': 'Trajectory', 'trace.body': '按 session / turn / step / request / tool 组织。展开节点可以直接看关键 prompt、schema、input/output。', 'trace.systemPrompt': 'System prompt', @@ -42,15 +58,74 @@ const messages = { 'waterfall.tools': 'tools', 'waterfall.slowest': 'slowest', 'waterfall.errors': 'errors', + 'context.title': 'Context', + 'context.body': '这是开发分析视图:它解释模型请求边界里真正进入上下文的内容,用来改 prompt、tool schema 和 config。', + 'context.systemPrompt': 'System prompt', + 'context.toolSchemas': 'Tool schemas', + 'context.callConfig': 'Call config', + 'context.messagePrefix': 'Message prefix', + 'context.derivedHistory': 'Derived history', + 'context.rawJsonl': 'Raw JSONL', + 'context.noSystem': 'No system prompt found in latest request header.', + 'context.noTools': 'No tool schemas found in latest request header.', + 'context.changed': 'changed', + 'context.available': 'available', + 'context.empty': 'empty', + 'context.derived': 'derived', + 'context.visibleRows': 'visible rows', + 'context.messages': 'messages', + 'context.events': 'events', 'empty.traceTitle': '还没有 trace', 'empty.traceBody': '先运行一条 prompt,这里会读取真实 JSONL trace。', + 'dev.emptyTitle': 'No development artifacts found', + 'dev.emptyBody': 'Start the runtime or check the active cordis.yml config.', + 'dev.source': 'Source', + 'dev.owner': 'Owner', + 'dev.recentlyUsed': 'Recently used', + 'dev.reload': 'Reload', + 'dev.unknown': 'unknown', + 'dev.noRecentEvidence': 'No recent evidence yet', + 'dev.registeredPlugins': 'Registered plugins', + 'dev.registeredTools': 'Registered tool surfaces', + 'dev.noPlugins': 'No registered plugins found', + 'dev.noTools': 'No registered tools found yet', + 'dev.metadata': 'Metadata', + 'dev.metadataSubtitle': 'Implementation, dependency, and last-seen evidence', + 'dev.effectivePromptContent': 'Effective prompt content', + 'dev.toolSchema': 'Tool schema', + 'dev.pluginContribution': 'Plugin config / contribution', + 'dev.activeConfiguration': 'Active configuration', + 'dev.runtimeState': 'Runtime state', + 'dev.suggestedLoop': 'Suggested verification loop', + 'dev.promptMeta': 'Current source-level prompt text or prompt owner metadata', + 'dev.toolMeta': 'Current registered schema when last seen by a model request', + 'dev.pluginMeta': 'Who injects prompt/context or registers tools, based on active Cordis config', + 'dev.configMeta': 'Model/runtime parameters and files whose edits require reload', + 'dev.runtimeMeta': 'Current Electron main process and ACP bridge state', + 'dev.loopMeta': 'How to rerun and compare after editing the agent', + 'dev.manualRestart': 'Manual restart available here', + 'dev.restartRecommended': 'Restart recommended', + 'dev.noRestartSignal': 'No restart signal', + 'dev.restartAfterEdit': 'Restart ACP after editing', + 'dev.recommendedLoop': 'Recommended loop', + 'dev.loopStepEdit': 'Edit the prompt, tool, plugin, or config source.', + 'dev.loopStepRestart': 'Restart ACP runtime if the changed file is loaded at process start.', + 'dev.loopStepRerun': 'Return to Chat and rerun a previous task or start a new one.', + 'dev.loopStepCompare': 'Use Trajectory / Waterfall to compare behavior and timing evidence.', + 'dev.restartRuntime': 'Restart ACP runtime', 'inspector.close': '关闭', + 'inspector.input': 'Input', + 'inspector.output': 'Output', + 'inspector.metadata': 'Metadata', + 'inspector.feedback': 'Feedback', 'feedback.empty': '这个对象还没有 feedback。', 'feedback.author': 'Feedback author', 'feedback.placeholder': '给这个对象写 feedback', 'feedback.add': '添加 feedback', 'composer.placeholderDraft': '先输入一句话创建 session', 'composer.placeholderSession': 'Message Deepseek Harness', + 'composer.send': 'Send message', + 'error.noDesktopApi': 'Desktop API 不可用。请用 Electron 窗口打开,而不是浏览器 tab。', }, 'en-US': { 'app.newChat': 'New chat', @@ -60,10 +135,21 @@ const messages = { 'app.developSubtitle': 'Prompts, tools, plugins, runtime', 'app.searchPlaceholder': 'Search title, id, model', 'app.recentSessions': 'Recent sessions', + 'app.emptySessions': 'No matching sessions', 'app.language': '中文', + 'app.errorTitle': 'Error', + 'app.repo': 'Repo', + 'app.branch': 'Branch', + 'app.commit': 'Commit', + 'app.dirty': 'Dirty', + 'app.acp': 'ACP', + 'app.restartNeeded': 'Restart needed', 'surface.chat': 'Chat', 'surface.trajectory': 'Trajectory', 'surface.waterfall': 'Waterfall', + 'surface.context': 'Context', + 'surface.compare': 'Compare', + 'surface.dev': 'Dev', 'chat.details': 'Details', 'chat.thinking': 'Thinking', 'chat.toolUse': 'Tool use', @@ -78,6 +164,10 @@ const messages = { 'chat.newBody': 'Type a message first. A real ACP session is created only after sending.', 'chat.startTitle': 'Start a Deepseek Harness session', 'chat.startBody': 'New chat opens a draft only. The backend session is created after the first sent message.', + 'chat.user': 'User', + 'chat.assistant': 'Assistant', + 'chat.userMessage': 'User message', + 'chat.assistantMessage': 'Assistant message', 'trace.title': 'Trajectory', 'trace.body': 'Organized by session / turn / step / request / tool. Expand nodes to inspect prompts, schemas, input, and output inline.', 'trace.systemPrompt': 'System prompt', @@ -93,20 +183,86 @@ const messages = { 'waterfall.tools': 'tools', 'waterfall.slowest': 'slowest', 'waterfall.errors': 'errors', + 'context.title': 'Context', + 'context.body': 'This development analysis view explains what actually entered the model request boundary, so prompts, tool schemas, and config can be changed with evidence.', + 'context.systemPrompt': 'System prompt', + 'context.toolSchemas': 'Tool schemas', + 'context.callConfig': 'Call config', + 'context.messagePrefix': 'Message prefix', + 'context.derivedHistory': 'Derived history', + 'context.rawJsonl': 'Raw JSONL', + 'context.noSystem': 'No system prompt found in latest request header.', + 'context.noTools': 'No tool schemas found in latest request header.', + 'context.changed': 'changed', + 'context.available': 'available', + 'context.empty': 'empty', + 'context.derived': 'derived', + 'context.visibleRows': 'visible rows', + 'context.messages': 'messages', + 'context.events': 'events', 'empty.traceTitle': 'No trace yet', 'empty.traceBody': 'Run a prompt first; this surface will read the real JSONL trace.', + 'dev.emptyTitle': 'No development artifacts found', + 'dev.emptyBody': 'Start the runtime or check the active cordis.yml config.', + 'dev.source': 'Source', + 'dev.owner': 'Owner', + 'dev.recentlyUsed': 'Recently used', + 'dev.reload': 'Reload', + 'dev.unknown': 'unknown', + 'dev.noRecentEvidence': 'No recent evidence yet', + 'dev.registeredPlugins': 'Registered plugins', + 'dev.registeredTools': 'Registered tool surfaces', + 'dev.noPlugins': 'No registered plugins found', + 'dev.noTools': 'No registered tools found yet', + 'dev.metadata': 'Metadata', + 'dev.metadataSubtitle': 'Implementation, dependency, and last-seen evidence', + 'dev.effectivePromptContent': 'Effective prompt content', + 'dev.toolSchema': 'Tool schema', + 'dev.pluginContribution': 'Plugin config / contribution', + 'dev.activeConfiguration': 'Active configuration', + 'dev.runtimeState': 'Runtime state', + 'dev.suggestedLoop': 'Suggested verification loop', + 'dev.promptMeta': 'Current source-level prompt text or prompt owner metadata', + 'dev.toolMeta': 'Current registered schema when last seen by a model request', + 'dev.pluginMeta': 'Who injects prompt/context or registers tools, based on active Cordis config', + 'dev.configMeta': 'Model/runtime parameters and files whose edits require reload', + 'dev.runtimeMeta': 'Current Electron main process and ACP bridge state', + 'dev.loopMeta': 'How to rerun and compare after editing the agent', + 'dev.manualRestart': 'Manual restart available here', + 'dev.restartRecommended': 'Restart recommended', + 'dev.noRestartSignal': 'No restart signal', + 'dev.restartAfterEdit': 'Restart ACP after editing', + 'dev.recommendedLoop': 'Recommended loop', + 'dev.loopStepEdit': 'Edit the prompt, tool, plugin, or config source.', + 'dev.loopStepRestart': 'Restart ACP runtime if the changed file is loaded at process start.', + 'dev.loopStepRerun': 'Return to Chat and rerun a previous task or start a new one.', + 'dev.loopStepCompare': 'Use Trajectory / Waterfall to compare behavior and timing evidence.', + 'dev.restartRuntime': 'Restart ACP runtime', 'inspector.close': 'Close', + 'inspector.input': 'Input', + 'inspector.output': 'Output', + 'inspector.metadata': 'Metadata', + 'inspector.feedback': 'Feedback', 'feedback.empty': 'No feedback for this object yet.', 'feedback.author': 'Feedback author', 'feedback.placeholder': 'Write feedback for this exact object', 'feedback.add': 'Add feedback', 'composer.placeholderDraft': 'Type a message to create a session', 'composer.placeholderSession': 'Message Deepseek Harness', + 'composer.send': 'Send message', + 'error.noDesktopApi': 'Desktop API is not available. Use the Electron window, not the browser tab.', }, } as const +/** Translation key shared by both renderer dictionaries. */ export type I18nKey = keyof typeof messages['en-US'] +/** + * Return a localized renderer string, falling back to English and then the key. + * @param locale - The active renderer locale. + * @param key - The dictionary key to resolve. + * @returns The localized string for the requested key. + */ export function translate(locale: Locale, key: I18nKey): string { - return messages[locale][key] ?? messages['en-US'][key] ?? key + return messages[locale][key] } diff --git a/packages/ui/desktop/src/index.ts b/packages/ui/desktop/src/index.ts index 76390b0a90..4c8e4442eb 100644 --- a/packages/ui/desktop/src/index.ts +++ b/packages/ui/desktop/src/index.ts @@ -201,6 +201,7 @@ export interface DevPanelStatus { readonly suggestedPrompt?: string } +/** Default author filled into new feedback entries. */ export const DEFAULT_FEEDBACK_AUTHOR = 'shentuni' /** Default surface definitions for the first Electron implementation. */ @@ -270,22 +271,39 @@ export const SURFACE_DEFINITIONS: Record = { /** Backward-compatible alias for callers that only need policies. */ export const SURFACE_POLICIES: Record = SURFACE_DEFINITIONS -/** Returns whether selecting from a middle surface should open the inspector. */ +/** + * Returns whether selecting from a middle surface should open the inspector. + * @param surface - The active middle surface. + * @param target - The object the user selected, if any. + * @returns True when this selection should open the inspector drawer. + */ export function opensInspector(surface: DesktopSurface, target: InspectorTarget | undefined): boolean { return target !== undefined && SURFACE_POLICIES[surface].fullDetailInInspector } -/** Returns whether the surface is allowed to show the chat composer. */ +/** + * Returns whether the surface is allowed to show the chat composer. + * @param surface - The active middle surface. + * @returns True only for the driving chat surface. + */ export function ownsComposer(surface: DesktopSurface): boolean { return SURFACE_DEFINITIONS[surface].ownsComposer } -/** Full detail belongs in the inspector for trace-analysis surfaces, not the Develop artifact browser. */ +/** + * Full detail belongs in the inspector for trace-analysis surfaces, not the Develop artifact browser. + * @param surface - The active middle surface. + * @returns True when raw payloads should live in the inspector for this surface. + */ export function fullDetailBelongsInInspector(surface: DesktopSurface): boolean { return SURFACE_DEFINITIONS[surface].fullDetailInInspector } -/** Create a stable, view-independent inspector id. */ +/** + * Create a stable, view-independent inspector id. + * @param key - Stable fields that identify the selected object. + * @returns A colon-delimited target id shared across surfaces. + */ export function createInspectorTargetId(key: InspectorTargetKey): string { const parts = [`session:${key.sessionId}`] if (key.runId !== undefined) parts.push(`run:${key.runId}`) @@ -295,7 +313,11 @@ export function createInspectorTargetId(key: InspectorTargetKey): string { return parts.join(':') } -/** Pick a useful starting inspector tab for common target kinds. */ +/** + * Pick a useful starting inspector tab for common target kinds. + * @param target - The selected object that will be inspected. + * @returns The inspector tab that best matches the target's primary payload. + */ export function defaultInspectorTabForTarget(target: InspectorTarget): InspectorTab { switch (target.kind) { case 'assistant-stream': @@ -314,7 +336,10 @@ export function defaultInspectorTabForTarget(target: InspectorTarget): Inspector } } -/** Build closed drawer state when no node is selected. */ +/** + * Build closed drawer state when no node is selected. + * @returns An inspector state with no target and no visible tabs. + */ export function closedInspectorState(): InspectorState { return { open: false, @@ -323,13 +348,17 @@ export function closedInspectorState(): InspectorState { } } -/** Build an open drawer state for a selected target. */ +/** + * Build an open drawer state for a selected target. + * @param target - The selected object that owns the inspector content. + * @returns An open inspector state with the default tab selected. + */ export function openInspectorState(target: InspectorTarget): InspectorState { return { open: true, target, activeTab: defaultInspectorTabForTarget(target), - tabs: INSPECTOR_TABS.map((tab) => ({ + tabs: INSPECTOR_TABS.map(tab => ({ tab, available: true, canCopy: tab !== 'feedback', diff --git a/packages/ui/desktop/src/main.mjs b/packages/ui/desktop/src/main.mjs index 10cc864d21..721a39b30e 100644 --- a/packages/ui/desktop/src/main.mjs +++ b/packages/ui/desktop/src/main.mjs @@ -369,12 +369,12 @@ function devStatus() { }, { label: 'System prompt service', - path: 'packages/system-prompt/system-prompt/src/index.ts', + path: 'packages/core/system-prompt/src/index.ts', purpose: 'Owns persona, tool order, and assembled model-facing prompt sections.', }, { label: 'Tool registry', - path: 'packages/tools/tools/src/index.ts', + path: 'packages/core/tools/src/index.ts', purpose: 'Owns model-facing tool registration, schema validation, and tool presentation mode.', }, ], diff --git a/packages/ui/desktop/tests/acp-subprocess.spec.ts b/packages/ui/desktop/tests/acp-subprocess.spec.ts index 76ee191bd4..387e99e717 100644 --- a/packages/ui/desktop/tests/acp-subprocess.spec.ts +++ b/packages/ui/desktop/tests/acp-subprocess.spec.ts @@ -48,7 +48,7 @@ describe('desktop ACP subprocess bridge', () => { let stderr = '' child.stderr.setEncoding('utf8') - child.stderr.on('data', chunk => { stderr += String(chunk) }) + child.stderr.on('data', (chunk) => { stderr += String(chunk) }) const stream: Stream = ndJsonStream( Writable.toWeb(child.stdin) as WritableStream, @@ -69,7 +69,7 @@ describe('desktop ACP subprocess bridge', () => { }), stream) const init = await client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} }) - expect(init.agentInfo.name).toBe('deepseek-harness-acp') + expect(init.agentInfo?.name).toBe('deepseek-harness-acp') const session = await client.newSession({ cwd: process.cwd(), mcpServers: [] }) expect(session.sessionId).toBeTruthy() diff --git a/packages/ui/desktop/tests/index.spec.ts b/packages/ui/desktop/tests/index.spec.ts index eb336185b8..d7a5bf0471 100644 --- a/packages/ui/desktop/tests/index.spec.ts +++ b/packages/ui/desktop/tests/index.spec.ts @@ -11,9 +11,9 @@ import { ownsComposer, SURFACE_DEFINITIONS, SURFACE_POLICIES, - type DesktopSurface, type InspectorTarget, } from '../src/index.ts' +import { translate } from '../src/i18n.ts' const target: InspectorTarget = { id: 'session:one:event:1', @@ -89,7 +89,7 @@ describe('desktop inspector contracts', () => { it('keeps inspector tabs ordered with feedback last', () => { expect(INSPECTOR_TABS).toEqual(['input', 'output', 'metadata', 'feedback']) - expect(openInspectorState(target).tabs.map((tab) => tab.tab)).toEqual(INSPECTOR_TABS) + expect(openInspectorState(target).tabs.map(tab => tab.tab)).toEqual(INSPECTOR_TABS) expect(closedInspectorState()).toEqual({ open: false, activeTab: 'input', @@ -101,3 +101,12 @@ describe('desktop inspector contracts', () => { expect(DEFAULT_FEEDBACK_AUTHOR).toBe('shentuni') }) }) + +describe('desktop i18n', () => { + it('switches visible shell labels between Chinese and English', () => { + expect(translate('zh-CN', 'app.newChat')).toBe('新对话') + expect(translate('en-US', 'app.newChat')).toBe('New chat') + expect(translate('zh-CN', 'app.language')).toBe('EN') + expect(translate('en-US', 'app.language')).toBe('中文') + }) +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1a3a78030..3d1889b05f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1571,6 +1571,28 @@ importers: specifier: ^4.0.0-rc.7 version: 4.0.0-rc.7(@cordisjs/plugin-include@vendor+include)(@cordisjs/plugin-loader@vendor+loader) + packages/ui/desktop: + dependencies: + '@agentclientprotocol/sdk': + specifier: 0.25.1 + version: 0.25.1(zod@4.4.3) + devDependencies: + cordis: + specifier: ^4.0.0-rc.7 + version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) + electron: + specifier: ^43.1.1 + version: 43.1.1 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vite: + specifier: ^8.0.16 + version: 8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + vitest: + specifier: ^4.1.8 + version: 4.1.8(@types/node@25.9.3)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + packages/ui/jsonrpc: dependencies: schemastery: @@ -2519,6 +2541,14 @@ packages: engines: {node: '>=22.19.0'} hasBin: true + '@electron-internal/extract-zip@1.0.4': + resolution: {integrity: sha512-Zr1Vs7E9tpCNhZHDAbFVXc2gEVCG9RqPDjrno5+bdgB6LRAuvgyMHJut4NCVyYwtAieapMzc3fiQ3CSTi75ARg==} + engines: {node: '>=22.12.0'} + + '@electron/get@5.0.0': + resolution: {integrity: sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==} + engines: {node: '>=22.12.0'} + '@emnapi/core@1.10.0': resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} @@ -3473,6 +3503,9 @@ packages: '@types/node@22.20.0': resolution: {integrity: sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==} + '@types/node@24.13.3': + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + '@types/node@25.9.3': resolution: {integrity: sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==} @@ -4047,6 +4080,11 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron@43.1.1: + resolution: {integrity: sha512-I5c5vfuVvaXpWx3IZdwvXgxQW44+e7OP1wXGVQkogLeSFSkUZ6sLCcWV05AdEcs65AO5tAIJJwbp7ixw+LdarA==} + engines: {node: '>= 22.12.0'} + hasBin: true + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -4065,6 +4103,10 @@ packages: resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} engines: {node: '>=20.19.0'} + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -4336,6 +4378,9 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + hachure-fill@0.5.2: resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} @@ -5156,6 +5201,10 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + protobufjs@7.6.4: resolution: {integrity: sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==} engines: {node: '>=12.0.0'} @@ -5387,6 +5436,10 @@ packages: stylis@4.4.0: resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} + sumchecker@3.0.1: + resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} + engines: {node: '>= 8.0'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -5540,6 +5593,9 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} @@ -6157,6 +6213,21 @@ snapshots: - ws - zod + '@electron-internal/extract-zip@1.0.4': {} + + '@electron/get@5.0.0': + dependencies: + debug: 4.4.3 + env-paths: 3.0.0 + graceful-fs: 4.2.11 + progress: 2.0.3 + semver: 7.8.4 + sumchecker: 3.0.1 + optionalDependencies: + undici: 7.28.0 + transitivePeerDependencies: + - supports-color + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -6919,6 +6990,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@24.13.3': + dependencies: + undici-types: 7.18.2 + '@types/node@25.9.3': dependencies: undici-types: 7.24.6 @@ -7545,6 +7620,14 @@ snapshots: ee-first@1.1.1: {} + electron@43.1.1: + dependencies: + '@electron-internal/extract-zip': 1.0.4 + '@electron/get': 5.0.0 + '@types/node': 24.13.3 + transitivePeerDependencies: + - supports-color + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -7555,6 +7638,8 @@ snapshots: entities@8.0.0: {} + env-paths@3.0.0: {} + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -7919,6 +8004,8 @@ snapshots: gopd@1.2.0: {} + graceful-fs@4.2.11: {} + hachure-fill@0.5.2: {} handlebars@4.7.9: @@ -8873,6 +8960,8 @@ snapshots: process-nextick-args@2.0.1: {} + progress@2.0.3: {} + protobufjs@7.6.4: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -9165,6 +9254,12 @@ snapshots: stylis@4.4.0: {} + sumchecker@3.0.1: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -9286,6 +9381,8 @@ snapshots: undici-types@6.21.0: {} + undici-types@7.18.2: {} + undici-types@7.24.6: {} undici@7.28.0: {} diff --git a/tsconfig.build.json b/tsconfig.build.json index 3a57169005..dd3004454f 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -59,6 +59,7 @@ { "path": "./packages/ui/acp" }, { "path": "./packages/examples/acp-demo" }, { "path": "./packages/ui/app-boot" }, + { "path": "./packages/ui/desktop" }, { "path": "./packages/ui/jsonrpc" }, { "path": "./packages/examples/jsonrpc-demo" }, { "path": "./packages/ui/stdio" }, diff --git a/tsconfig.json b/tsconfig.json index 6585a987d6..4fc7dbb0c3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -70,6 +70,7 @@ { "path": "./packages/ui/acp" }, { "path": "./packages/examples/acp-demo" }, { "path": "./packages/ui/app-boot" }, + { "path": "./packages/ui/desktop" }, { "path": "./packages/ui/jsonrpc" }, { "path": "./packages/examples/jsonrpc-demo" }, { "path": "./packages/ui/stdio" },