diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml index f8e974a4cd..bfe72f19a4 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md -2026-08-23-client-derived-tool-presentation.md: 957d2e6c1a79cb0b0a246066463e6ac960fb3d5b -2026-08-23-client-derived-tool-presentation.zh.md: 5fa1a350963ff74306520bad6a4fc89765c592a7 +2026-08-23-client-derived-tool-presentation.md: 77fb48552625c1b22908225b0116eeba14c6046c +2026-08-23-client-derived-tool-presentation.zh.md: 50cc447f9f4ca2e946de10d2df31ca18382d95ef diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md index 957d2e6c1a..77fb485526 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.md @@ -312,6 +312,8 @@ Standard shell results continue parsing trailing `[exit code: N]` and `[killed b Call `description` remains above the card and overrides the collapsed summary. Workdir continues handling absolute, relative, and missing values. Relative paths resolve against the Session cwd while preserving normalization for `.`, `..`, drive letters, and UNC roots. +For `terminal_send`, non-empty input and the session id remain verbatim tool data; the empty-input fallback and session label resolve through the render site's conversation locale. + Standard and persistent providers sharing the same tool name are a special compatibility point. The Client uses currently valid argument and result features to preserve their delivered differences. Input that cannot be identified unambiguously uses a Generic settled result rather than gaining new presentation. `TerminalBlock` ANSI handling, cursor replay, wide characters, line limits, expansion, copying, and assistive text remain unchanged. diff --git a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md index 5fa1a35096..50cc447f9f 100644 --- a/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-23-client-derived-tool-presentation.zh.md @@ -312,6 +312,8 @@ Client terminal model 从工具名称、调用参数、结果 content、error、 调用 `description` 继续显示在 card 上方并覆盖折叠摘要。workdir 继续按绝对、相对和缺失三种情况处理;相对路径基于 Session cwd,且保留 `.`、`..`、盘符与 UNC root 的归一化。 +对于 `terminal_send`,非空 input 与 session id 保持为逐字工具数据;空 input fallback 与 session label 通过 render site 的 conversation locale 解析。 + 同名普通与 persistent provider 是特殊兼容点。Client 使用当前有效参数与结果特征保留已交付差异;不足以无歧义识别的输入选择 Generic settled 结果,不增加新表现。 TerminalBlock 的 ANSI、光标重放、宽字符、行数上限、展开、复制与辅助技术文本完全不变。 diff --git a/packages/client/ui-conversation/src/client/locales.ts b/packages/client/ui-conversation/src/client/locales.ts index fc0d361ec2..3ab8410590 100644 --- a/packages/client/ui-conversation/src/client/locales.ts +++ b/packages/client/ui-conversation/src/client/locales.ts @@ -145,6 +145,8 @@ export const zh = { 'terminal.collapseAria': '收起输出', 'terminal.expandAria': '展开其余 {n} 行输出', 'terminal.expandRest': '… 其余 {n} 行', + 'terminal.sendInput': '(发送输入)', + 'terminal.session': '终端 {sessionId}', } satisfies Record /** The conversation namespace key union. */ @@ -288,4 +290,8 @@ export const en = { 'terminal.collapseAria': 'Collapse output', 'terminal.expandAria': 'Expand the remaining {n} output lines', 'terminal.expandRest': '… {n} more lines', + // The Host terminal_send presenter has no locale seat; keep its fallbacks + // aligned with these English values. + 'terminal.sendInput': '(send input)', + 'terminal.session': 'Terminal {sessionId}', } satisfies Record diff --git a/packages/client/ui-tool/src/client/tool/ToolDetails.tsx b/packages/client/ui-tool/src/client/tool/ToolDetails.tsx index 42ddd41824..c946f0ee7d 100644 --- a/packages/client/ui-tool/src/client/tool/ToolDetails.tsx +++ b/packages/client/ui-tool/src/client/tool/ToolDetails.tsx @@ -4,7 +4,9 @@ import type { ToolDetailsProps } from '../contract/slots.ts' import { diffCardModel } from './models/diff-card-model.ts' import { readCardModel } from './models/read-card-model.ts' import { searchCardModel } from './models/search-card-model.ts' -import { terminalBlockLabels, terminalCardModel } from './models/terminal-card-model.ts' +import { + localizeTerminalCardModel, terminalBlockLabels, terminalCardModel, +} from './models/terminal-card-model.ts' import { diffBlockLabels, readBlockLabels, searchBlockLabels, webBlockLabels, } from './models/primitive-labels.ts' @@ -22,8 +24,9 @@ export function ToolDetails({ block, cwd, useHostDescription, t, }: Pick) { const home = useHostDescription(description => description?.home) - const terminal = terminalCardModel(block, cwd) - if (terminal !== null) { + const terminalModel = terminalCardModel(block, cwd) + if (terminalModel !== null) { + const terminal = localizeTerminalCardModel(terminalModel, t) return ( <> {terminal.description !== undefined ? ( diff --git a/packages/client/ui-tool/src/client/tool/components/ToolRow.tsx b/packages/client/ui-tool/src/client/tool/components/ToolRow.tsx index 96b3213a04..8102965a6d 100644 --- a/packages/client/ui-tool/src/client/tool/components/ToolRow.tsx +++ b/packages/client/ui-tool/src/client/tool/components/ToolRow.tsx @@ -7,7 +7,9 @@ import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots' import { CHAT_DIFF_MAX_LINES, type DiffCardModel } from '../models/diff-card-model.ts' import { CHAT_READ_MAX_LINES, type ReadCardModel } from '../models/read-card-model.ts' import { CHAT_SEARCH_MAX_LINES, type SearchCardModel } from '../models/search-card-model.ts' -import { terminalBlockLabels, type TerminalCardModel } from '../models/terminal-card-model.ts' +import { + localizeTerminalCardModel, terminalBlockLabels, type TerminalCardModel, +} from '../models/terminal-card-model.ts' import { diffBlockLabels, readBlockLabels, searchBlockLabels, webBlockLabels, } from '../models/primitive-labels.ts' @@ -106,7 +108,9 @@ export function ToolRow({ const readLabels = useMemo(() => readBlockLabels(t), [t]) const searchLabels = useMemo(() => searchBlockLabels(t), [t]) const webLabels = useMemo(() => webBlockLabels(t), [t]) - const terminalBody = terminal ?? null + const terminalBody = terminal === undefined || terminal === null + ? null + : localizeTerminalCardModel(terminal, t) const diffBody = diff ?? null const readBody = read ?? null const searchBody = search ?? null @@ -118,7 +122,7 @@ export function ToolRow({ const status = stateStatus(state, t) // A failure must replace, not supplement, the normal summary. const failureLine = state === 'error' ? errorSummary ?? null : null - const summaryText = failureLine ?? summary + const summaryText = failureLine ?? terminalBody?.description ?? summary const suffix = failureLine === null ? summarySuffix ?? null : null const fileLink = filePath !== undefined && onOpenFile !== undefined && failureLine === null const toggleExpand = () => { diff --git a/packages/client/ui-tool/src/client/tool/models/terminal-card-model.ts b/packages/client/ui-tool/src/client/tool/models/terminal-card-model.ts index 548a024562..1956fe232e 100644 --- a/packages/client/ui-tool/src/client/tool/models/terminal-card-model.ts +++ b/packages/client/ui-tool/src/client/tool/models/terminal-card-model.ts @@ -37,16 +37,48 @@ export function terminalBlockLabels(t: TranslateNS<'conversation'>): TerminalBlo */ export interface TerminalCardModel { /** - * The props {@link TerminalBlock} draws. Held as a nested object so a render - * site spreads exactly the primitive's own surface and can never leak a - * neighbouring field into it. + * The locale-neutral props {@link TerminalBlock} draws. The render site adds + * `command` after resolving {@link copy} through its locale seat. */ - card: Pick + card: Pick /** - * The model-authored call description rendered above the card. Absent for - * persistent shells, whose parameter set has no description. + * Verbatim Tool data or semantic `terminal_send` data. Product copy stays + * unresolved until a render site supplies its locale seat. */ - description: string | undefined + copy: + | { readonly kind: 'shell'; readonly command: string; readonly description: string | undefined } + | { readonly kind: 'terminal-send'; readonly text: string; readonly sessionId: string } +} + +interface LocalizedTerminalCardModel { + readonly card: Pick + readonly description: string | undefined +} + +/** + * Resolve locale-owned `terminal_send` copy while preserving Tool-authored + * shell commands and descriptions verbatim. + * @param model - locale-neutral terminal card data. + * @param t - the render site's conversation locale seat. + * @returns terminal props and description ready for rendering. + */ +export function localizeTerminalCardModel( + model: TerminalCardModel, + t: TranslateNS<'conversation'>, +): LocalizedTerminalCardModel { + if (model.copy.kind === 'shell') { + return { + card: { command: model.copy.command, ...model.card }, + description: model.copy.description, + } + } + return { + card: { + command: model.copy.text === '' ? t('terminal.sendInput') : model.copy.text, + ...model.card, + }, + description: t('terminal.session', { sessionId: model.copy.sessionId }), + } } /** @@ -140,6 +172,7 @@ function collapse(body: string, rooted: boolean, separator = '/'): string { } interface ShellCall { + kind: 'shell' command: string description: string | undefined workdir: string | undefined @@ -159,10 +192,11 @@ function shellCall(name: string, args: Record): ShellCall | nul // Standard dsh-tool-bash and dsh-tool-pwsh schemas require `description`; // persistent shell providers omit it. Their parameter roots stay open, so // unrelated fields do not change their running-card behavior. - return { command, description: undefined, workdir: undefined, persistent: true, background: false } + return { kind: 'shell', command, description: undefined, workdir: undefined, persistent: true, background: false } } if (typeof description !== 'string' || description.trim() === '') return null return { + kind: 'shell', command, description, workdir, @@ -172,8 +206,9 @@ function shellCall(name: string, args: Record): ShellCall | nul } interface TerminalSendCall { - command: string - description: string + kind: 'terminal-send' + text: string + sessionId: string background: boolean } @@ -184,10 +219,9 @@ function terminalSendCall(name: string, args: Record): Terminal if (submit !== undefined && typeof submit !== 'boolean') return null if (background !== undefined && typeof background !== 'boolean') return null return { - // Keep this visible fallback aligned with dsh-tool-terminal's - // `terminal_send.presentCall` implementation. - command: text || '(send input)', - description: `Terminal ${sessionId}`, + kind: 'terminal-send', + text, + sessionId, background: background === true, } } @@ -213,7 +247,7 @@ function parseExitStatus(text: string): { output: string; exitCode?: number; sig * the generic path. * @param block - running or settled Tool block. * @param sessionCwd - session workspace root used to resolve workdir. - * @returns the terminal-card props, or null for the generic path. + * @returns locale-neutral terminal-card data, or null for the generic path. */ export function terminalCardModel( block: ToolCallBlock, @@ -222,19 +256,17 @@ export function terminalCardModel( if (block.parentCallId !== undefined) return null const parsed = parsedToolCall(block) if (parsed === null) return null - const shell = shellCall(parsed.name, parsed.args) - const send = terminalSendCall(parsed.name, parsed.args) - if (shell === null && send === null) return null - if (shell?.background === true || send?.background === true) return null + const call = shellCall(parsed.name, parsed.args) ?? terminalSendCall(parsed.name, parsed.args) + if (call === null || call.background) return null - const command = shell?.command ?? send?.command ?? '' - const description = shell?.description ?? send?.description - const cwd = resolveTerminalCwd(shell?.workdir, sessionCwd) + const copy: TerminalCardModel['copy'] = call.kind === 'shell' + ? { kind: 'shell', command: call.command, description: call.description } + : { kind: 'terminal-send', text: call.text, sessionId: call.sessionId } + const cwd = resolveTerminalCwd(call.kind === 'shell' ? call.workdir : undefined, sessionCwd) if (!('kind' in block)) { return { - description, + copy, card: { - command, cwd, output: undefined, exitCode: undefined, @@ -243,14 +275,13 @@ export function terminalCardModel( }, } } - if (block.isError || shell?.persistent === true) return null + if (block.isError || (call.kind === 'shell' && call.persistent)) return null const output = singleResultText(block) if (output === undefined) return null - const status = shell === null ? { output } : parseExitStatus(output) + const status = call.kind === 'terminal-send' ? { output } : parseExitStatus(output) return { - description, + copy, card: { - command, cwd, output: status.output, exitCode: status.exitCode, diff --git a/packages/client/ui-tool/src/client/tool/toolviews/GenericToolCard.tsx b/packages/client/ui-tool/src/client/tool/toolviews/GenericToolCard.tsx index 0fe0883e0a..41b13e20d6 100644 --- a/packages/client/ui-tool/src/client/tool/toolviews/GenericToolCard.tsx +++ b/packages/client/ui-tool/src/client/tool/toolviews/GenericToolCard.tsx @@ -47,7 +47,7 @@ export function GenericToolCard({ toolName, block, cwd, home, openFile, inspect, toolName={toolName} icon={VARIANT_ICONS[model.variant]} title={t(model.titleKey)} - summary={terminal?.description ?? model.summary} + summary={model.summary} // Single-file tools never expose an args body — the path link is the only // args interaction. A card is not an args body: a read/write/edit row is // single-file AND carries a card, so the card expands under the path link. diff --git a/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx b/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx index eaba61f015..321b156078 100644 --- a/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx +++ b/packages/client/ui-tool/src/client/tool/toolviews/bash-sample.tsx @@ -6,7 +6,9 @@ import { } from '@deepseek-ai/dsh-client-ui-primitives' import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' import type { ToolCallViewProps } from '../../contract/slots.ts' -import { terminalBlockLabels, terminalCardModel, terminalFailed } from '../models/terminal-card-model.ts' +import { + localizeTerminalCardModel, terminalBlockLabels, terminalCardModel, terminalFailed, +} from '../models/terminal-card-model.ts' import { toolRowModel, type ToolRowState } from '../models/tool-call-model.ts' import { CONVERSATION_NS as NS } from '../../locale.ts' import css from './bash-sample.module.css' @@ -38,10 +40,11 @@ export function BashRow({ toolName, block, sessionId, useSessions, inspect, t }: // An omitted shell workdir is the session workspace; relative values resolve // against it before reaching the terminal primitive. const cwd = useSessions(list => list.byId[sessionId]?.cwd) - const terminal = terminalCardModel(block, cwd) + const terminalModel = terminalCardModel(block, cwd) + const terminal = terminalModel === null ? null : localizeTerminalCardModel(terminalModel, t) // A failing exit status is the terminal card's own error signal (the call // itself settles isError:false), surfaced as the row's red state dot. - const state = model.state === 'ok' && terminal !== null && terminalFailed(terminal) + const state = model.state === 'ok' && terminalModel !== null && terminalFailed(terminalModel) ? 'error' : model.state const status = stateStatus(state, t) diff --git a/packages/client/ui-tool/tests/terminal-card.client.spec.tsx b/packages/client/ui-tool/tests/terminal-card.client.spec.tsx index 2543f7bfe7..8d57084ab3 100644 --- a/packages/client/ui-tool/tests/terminal-card.client.spec.tsx +++ b/packages/client/ui-tool/tests/terminal-card.client.spec.tsx @@ -12,19 +12,23 @@ import type { import type { SessionListState } from '@deepseek-ai/dsh-api-session-controller/client' import type { SessionId } from '@deepseek-ai/dsh-session/types' import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' +import { en as commonEn } from '@deepseek-ai/dsh-client-locale/src/locales/en.ts' import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts' -import { terminalCardModel, terminalFailed } from '../src/client/tool/models/terminal-card-model.ts' +import { + localizeTerminalCardModel, terminalCardModel, terminalFailed, +} from '../src/client/tool/models/terminal-card-model.ts' import { createChatStore } from '@deepseek-ai/dsh-client-ui-chat/src/client/stores.ts' import { GenericToolCard, type GenericToolCardProps } from '../src/client/tool/toolviews/GenericToolCard.tsx' import { DetailsPanel } from '@deepseek-ai/dsh-client-ui-chat/src/client/details/DetailsPanel.tsx' import { BashRow } from '../src/client/tool/toolviews/bash-sample.tsx' import { renderToolDetails, toolChatSnapshot, useEmptyTrajectory } from './tool-details-render.client.tsx' -import { zh } from '@deepseek-ai/dsh-client-ui-conversation/src/client/locales.ts' +import { en, zh } from '@deepseek-ai/dsh-client-ui-conversation/src/client/locales.ts' import { zh as chatZh } from '@deepseek-ai/dsh-client-ui-chat/src/client/locale.ts' type BashRowProps = Parameters[0] const t: GenericToolCardProps['t'] = makeTranslate(zh, commonZh) +const enT: GenericToolCardProps['t'] = makeTranslate(en, commonEn) const chatT = makeTranslate(chatZh, commonZh) afterEach(cleanup) @@ -65,9 +69,9 @@ const settled = (over?: Partial): ToolResultNode => ({ describe('terminalCardModel', () => { it('derives a running standard-shell card from raw arguments', () => { expect(terminalCardModel(running({ argsRaw: shellArgs({ workdir: '/projects/app' }) }))).toEqual({ - description: 'List files', + copy: { kind: 'shell', command: 'ls -la', description: 'List files' }, card: { - command: 'ls -la', cwd: '/projects/app', output: undefined, + cwd: '/projects/app', output: undefined, exitCode: undefined, signal: undefined, running: true, }, }) @@ -78,9 +82,9 @@ describe('terminalCardModel', () => { call: { name: 'bash', argsRaw: shellArgs({ workdir: '/projects/app' }) }, content: [{ type: 'text', text: 'boom\n[exit code: 2]' }], }))).toEqual({ - description: 'List files', + copy: { kind: 'shell', command: 'ls -la', description: 'List files' }, card: { - command: 'ls -la', cwd: '/projects/app', output: 'boom', + cwd: '/projects/app', output: 'boom', exitCode: 2, signal: undefined, running: false, }, }) @@ -163,10 +167,13 @@ describe('terminalCardModel', () => { const argsRaw = JSON.stringify({ sessionId: 'pty-3', text: 'make' }) const run = running({ name: 'terminal_send', argsRaw }) expect(terminalCardModel(run, '/w/app')).toMatchObject({ - description: 'Terminal pty-3', card: { command: 'make', cwd: '/w/app', running: true }, + copy: { kind: 'terminal-send', text: 'make', sessionId: 'pty-3' }, + card: { cwd: '/w/app', running: true }, }) const done = settled({ call: { name: 'terminal_send', argsRaw }, content: [{ type: 'text', text: 'ok' }] }) - expect(terminalCardModel(done)?.card).toMatchObject({ command: 'make', output: 'ok', running: false }) + expect(localizeTerminalCardModel(terminalCardModel(done)!, enT)).toMatchObject({ + description: 'Terminal pty-3', card: { command: 'make', output: 'ok', running: false }, + }) expect(terminalCardModel(settled({ call: { name: 'terminal_send', argsRaw: JSON.stringify({ sessionId: 'pty-3', text: 'make', run_in_background: true }) }, }))).toBeNull() @@ -176,10 +183,10 @@ describe('terminalCardModel', () => { it('preserves persistent-shell running cards and settled generic output', () => { const persistent = JSON.stringify({ command: 'pwd' }) expect(terminalCardModel(running({ argsRaw: persistent }))).toMatchObject({ - description: undefined, card: { command: 'pwd', running: true }, + copy: { kind: 'shell', command: 'pwd', description: undefined }, card: { running: true }, }) expect(terminalCardModel(running({ name: 'pwsh', argsRaw: persistent }))).toMatchObject({ - description: undefined, card: { command: 'pwd', running: true }, + copy: { kind: 'shell', command: 'pwd', description: undefined }, card: { running: true }, }) expect(terminalCardModel(settled({ call: { name: 'bash', argsRaw: persistent } }))).toBeNull() expect(terminalCardModel(settled({ call: { name: 'pwsh', argsRaw: persistent } }))).toBeNull() @@ -190,8 +197,22 @@ describe('terminalCardModel', () => { call: { name: 'pwsh', argsRaw: ARGS }, content: [{ type: 'text', text: 'failed\n[exit code: 3]' }], }))).toMatchObject({ - description: 'List files', - card: { command: 'ls -la', output: 'failed', exitCode: 3, running: false }, + copy: { kind: 'shell', command: 'ls -la', description: 'List files' }, + card: { output: 'failed', exitCode: 3, running: false }, + }) + }) + + it('keeps terminal_send copy semantic until the render locale is known', () => { + const model = terminalCardModel(running({ + name: 'terminal_send', + argsRaw: JSON.stringify({ sessionId: 'pty-3', text: '' }), + }))! + expect(model.copy).toEqual({ kind: 'terminal-send', text: '', sessionId: 'pty-3' }) + expect(localizeTerminalCardModel(model, t)).toMatchObject({ + description: '终端 pty-3', card: { command: '(发送输入)' }, + }) + expect(localizeTerminalCardModel(model, enT)).toMatchObject({ + description: 'Terminal pty-3', card: { command: '(send input)' }, }) }) @@ -323,6 +344,20 @@ describe('chat row terminal body', () => { expect(runStateOf(view.container)).toBe('ongoing') }) + it.each([ + { locale: 'zh', translate: t, description: '终端 pty-3', command: '(发送输入)' }, + { locale: 'en', translate: enT, description: 'Terminal pty-3', command: '(send input)' }, + ])('renders terminal_send copy through the $locale locale', ({ translate, description, command }) => { + const block = running({ + name: 'terminal_send', + argsRaw: JSON.stringify({ sessionId: 'pty-3', text: '' }), + }) + const view = render() + expect(view.getByText(description)).toBeTruthy() + toggleRow(view) + expect(view.getByText(command)).toBeTruthy() + }) + it('a non-terminal call keeps the args-JSON text body', () => { const view = render( { expect(description.compareDocumentPosition(card!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() }) + it('localizes terminal_send copy in Details', () => { + const argsRaw = JSON.stringify({ sessionId: 'pty-3', text: '' }) + const view = mount(snapshot({ + nodes: [settled({ + call: { name: 'terminal_send', argsRaw }, + content: [{ type: 'text', text: 'ok' }], + })], + }), { ...target, toolName: 'terminal_send' }) + expect(view.getByText('终端 pty-3')).toBeTruthy() + expect(view.getByText('(发送输入)')).toBeTruthy() + }) + it('resolves the prompt cwd against the session workspace', () => { const view = mount(snapshot({ nodes: [settled()] }), target, '/w/app') // No workdir in the call args: the prompt label is the workspace basename. diff --git a/packages/terminal/tool-terminal/src/index.ts b/packages/terminal/tool-terminal/src/index.ts index 0ab37fdc31..b08938d5e9 100644 --- a/packages/terminal/tool-terminal/src/index.ts +++ b/packages/terminal/tool-terminal/src/index.ts @@ -284,8 +284,8 @@ export function apply(ctx: Context, config: Config = {}): void { if (parsed.run_in_background === true) { return { card: 'generic', title: `Send to terminal ${parsed.sessionId as string} in background`, kind: 'execute', rawInput: parsed.text } } - // Keep this visible fallback aligned with dsh-client-ui-tool's - // terminal-card model, which cannot import this Host package. + // Keep these Host-only fallbacks aligned with the conversation locale + // keys `terminal.sendInput` and `terminal.session` used by Web. return { card: 'terminal', title: parsed.text || '(send input)', description: `Terminal ${parsed.sessionId as string}` } }, presentResult(args, result) {