Ver código fonte

fix(client): localize terminal send presentation

imccyu 1 mês atrás
pai
commit
1dd6bf1973

+ 2 - 2
.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

+ 2 - 0
.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.

+ 2 - 0
.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、光标重放、宽字符、行数上限、展开、复制与辅助技术文本完全不变。

+ 6 - 0
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<string, string>
 
 /** 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<ConversationKey, string>

+ 6 - 3
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<ToolDetailsProps, 'block' | 'cwd' | 'useHostDescription' | 't'>) {
   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 ? (

+ 7 - 3
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 = () => {

+ 59 - 28
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<TerminalBlockProps, 'command' | 'cwd' | 'output' | 'exitCode' | 'signal' | 'running'>
+  card: Pick<TerminalBlockProps, 'cwd' | 'output' | 'exitCode' | 'signal' | 'running'>
   /**
-   * 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<TerminalBlockProps, 'command' | 'cwd' | 'output' | 'exitCode' | 'signal' | 'running'>
+  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<string, unknown>): 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<string, unknown>): 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<string, unknown>): 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,

+ 1 - 1
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.

+ 6 - 3
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)

+ 59 - 12
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<typeof BashRow>[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>): 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(<GenericToolCard {...ownerProps(block)} toolName="terminal_send" t={translate} />)
+    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(<GenericToolCard {...ownerProps(settled({
       call: { name: 'bash', argsRaw: shellArgs({ run_in_background: true }) },
@@ -527,6 +562,18 @@ describe('DetailsPanel Output section', () => {
     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.

+ 2 - 2
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) {