diff --git a/packages/client/ui-conversation/src/client/chat/GenericToolCard.module.css b/packages/client/ui-conversation/src/client/chat/GenericToolCard.module.css deleted file mode 100644 index d21f6d3712..0000000000 --- a/packages/client/ui-conversation/src/client/chat/GenericToolCard.module.css +++ /dev/null @@ -1,17 +0,0 @@ -/* GenericToolCard resident cards: a read-declaring or web-declaring tool - without its own keyed row (e.g. web_fetch) grows a resident card under its - summary row. A column around the ToolRow keeps the row's own 24px height, so - the read card renders identically to the keyed ReadRow and the web card to - the web_search/web_fetch WebRow. */ - -.card { - display: flex; - flex-direction: column; -} - -/* Row indentation matches ToolRow's expanded bodies (16px leading + 6px gap), - and replaces the primitive's standalone vertical margin with the flow's. */ -.read, -.web { - margin: 4px 0 4px 22px; -} diff --git a/packages/client/ui-conversation/src/client/toolviews/file-mutation-row.module.css b/packages/client/ui-conversation/src/client/toolviews/file-mutation-row.module.css deleted file mode 100644 index 3ecf480adf..0000000000 --- a/packages/client/ui-conversation/src/client/toolviews/file-mutation-row.module.css +++ /dev/null @@ -1,130 +0,0 @@ -/* File-mutation toolview: same geometry/tokens as ToolRow (figma - {Edit,Write} · path), plus the diff card the row stacks under its summary - line. Mirrors bash-sample.module.css, whose terminal card this replaces with - a diff card. */ - -/* Summary line over the diff card; the summary row keeps its own 24px height, - so the card is a column around it rather than a change to it. */ -.card { - display: flex; - flex-direction: column; -} - -/* Row indentation matches ToolRow's expanded bodies (16px leading + 6px gap), - and replaces the primitive's standalone vertical margin with the flow's. */ -.diff { - margin: 4px 0 4px 22px; -} - -.root { - position: relative; /* sweep-glare overlay anchor */ - overflow: hidden; - display: flex; - align-items: center; - height: 24px; - min-width: 0; -} - -/* Running sweep glare — same deepsuite ShimmerText pattern as ToolRow. */ -.root[data-state='running']::after { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - width: 300px; - background: linear-gradient( - 90deg, - transparent 0%, - color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, - transparent 100% - ); - animation: dsh-file-mutation-row-sweep 2.6s ease-out infinite; - pointer-events: none; -} - -@keyframes dsh-file-mutation-row-sweep { - 0% { left: -300px; } - 90%, 100% { left: 100%; } -} - -.leading { - flex: none; - width: 16px; - height: 16px; - display: inline-flex; - align-items: center; - justify-content: center; - margin-right: 6px; - color: var(--dsw-alias-label-tertiary); -} - -.title { - flex: none; - font-size: 14px; - line-height: 24px; - color: var(--dsw-alias-label-secondary); -} - -.sep { - flex: none; - width: 2px; - height: 2px; - border-radius: 1px; - margin: 0 8px; - background: var(--dsw-alias-label-caption); -} - -.summary { - flex: 1 1 auto; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 14px; - line-height: 24px; - color: var(--dsw-alias-label-tertiary); -} - -/* File-tool path: same geometry as .summary; hover underline + pointer. */ -.fileLink { - flex: 1 1 auto; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - margin: 0; - padding: 0; - border: none; - background: none; - font: inherit; - text-align: left; - font-size: 14px; - line-height: 24px; - color: var(--dsw-alias-label-tertiary); - cursor: pointer; -} - -.fileLink:hover { - text-decoration: underline; -} - -.visuallyHidden { - position: absolute; - width: 1px; - height: 1px; - overflow: hidden; - clip: rect(0 0 0 0); - white-space: nowrap; -} - -/* The result text for an errored mutation, indented to the card's own column - (the diff card's inset) and in the error tone, since it stands in for the diff - card the failure path does not produce. */ -.failure { - margin: 4px 0 4px 22px; - white-space: pre-wrap; - overflow-wrap: anywhere; - font: var(--dsw-font-xs-13); - color: var(--dsw-alias-state-error-primary); -} diff --git a/packages/client/ui-conversation/src/client/toolviews/file-mutation-row.tsx b/packages/client/ui-conversation/src/client/toolviews/file-mutation-row.tsx index 323a73e77c..227726cf61 100644 --- a/packages/client/ui-conversation/src/client/toolviews/file-mutation-row.tsx +++ b/packages/client/ui-conversation/src/client/toolviews/file-mutation-row.tsx @@ -1,104 +1,54 @@ -// File-mutation toolview registrant: third-party posture over the keyed -// toolview hole (ctx.slots.register + ToolRowProps only — never imports the -// chat domain), registered under both `edit` and `write`. Product chrome -// matches ToolRow (figma: {Edit,Write} · {path}). -// -// A write/edit call declares the diff render intent, so this row renders the -// applied change through DiffBlock resident below its summary line — the same -// posture BashRow gives a terminal card. The row has no expand control and is -// not a details-panel target (tool rows stopped being one), so the diff body -// is resident rather than expand-gated, and the card's own copy and expand -// controls are the row's only interactions. CHAT_DIFF_MAX_LINES caps the body -// against the message flow; the details panel keeps the block's full default. -// The summary stays a path link (the file-tool interaction) that opens through -// the host. +// File-mutation toolview registrant: the keyed toolview hole for the `edit` +// and `write` tools. The row composes the shared ToolRow (chrome, running +// sweep, whole-row expand) and feeds it the applied diff as ToolRow's `diff` +// card material, so the change renders through DiffBlock in the collapsed-by- +// default expanded body — the same unified interaction every other card row +// has. The summary stays a path link (the file-tool interaction) that opens +// through the host; an errored mutation (write/edit return no diff on +// `result.isError`) keeps the model-facing error text on ToolRow's Output +// section, its first line in the collapsed summary. import type { Context } from 'cordis' -import { DiffBlock, IconEditOutline16, StateDot } from '@deepseek-ai/dsh-client-ui-primitives' +import { IconEditOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' +import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' import type { ToolRowProps } from '../contract/slots.ts' -import { CHAT_DIFF_MAX_LINES, diffCardModel } from '../contract/diff-card-model.ts' -import { toolRowModel, type ToolRowState } from '../contract/tool-call-model.ts' -import css from './file-mutation-row.module.css' +import { diffCardModel } from '../contract/diff-card-model.ts' +import { toolRowModel } from '../contract/tool-call-model.ts' +import { ToolRow } from '../chat/ToolRow.tsx' +import { NS } from '../locales.ts' -function leadingFor(state: ToolRowState) { - switch (state) { - case 'error': return - case 'stopped': return - // Running keeps the icon — the row sweep carries the in-flight signal. - default: return - } -} - -/** Visually hidden status — StateDot is aria-hidden; AT needs a text label. */ -function stateStatus(state: ToolRowState): string | null { - switch (state) { - case 'running': return '运行中' - case 'error': return '失败' - case 'stopped': return '已停止' - default: return null - } -} - -/** - * A settled result's text, flattened from its content blocks, for the arm that - * shows a failure the diff card cannot: write/edit return `undefined` from - * `presentResult` on `result.isError`, so an errored mutation has no diff card, - * and the keyed row is not a details-panel target. Without this the failure — - * an `old_string` that did not match, a permission denial — would read as a bare - * red dot with the model-facing error text nowhere on screen. - * @param block - the frozen call slice. - * @returns the result text, or null for a running call or an empty result. - */ -function errorText(block: ToolRowProps['block']): string | null { - if (!('kind' in block)) return null - const parts: string[] = [] - for (const item of block.content) { - if (item.type === 'text') parts.push(item.text) - } - if (parts.length === 0 && block.error !== undefined) parts.push(`${block.error.name}: ${block.error.code}`) - const text = parts.join('\n') - return text === '' ? null : text -} +/** Full row props: the toolview runtime share plus the standard locale seat. */ +type FileMutationRowProps = ToolRowProps & PropsLocale<'conversation'> /** * File-mutation row: icon + {Edit,Write} · {path} in the shared ToolRow chrome, - * with the applied diff resident below it. The summary is a path link (a file - * tool's interaction); the host's `openFile` resolves it against the session - * cwd, so this passes the tool's own path verbatim. The card's copy and expand - * controls are the row's only other actions. + * with the applied diff as the row's collapsed-by-default card body. The + * summary is a path link (a file tool's interaction); the host's `openFile` + * resolves it against the session cwd, so this passes the tool's own path + * verbatim. An errored mutation has no diff card, so ToolRow surfaces the + * model-facing error text through its Output section and its first line in the + * collapsed summary instead. */ -export function FileMutationRow({ toolName, block, cwd, openFile }: ToolRowProps) { +export function FileMutationRow({ toolName, block, cwd, openFile, inspect, t }: FileMutationRowProps) { const model = toolRowModel(toolName, block, cwd) const diff = diffCardModel(block) - const status = stateStatus(model.state) - const filePath = model.filePath - // An errored mutation has no diff card (presentResult returns undefined on - // isError); surface its result text so the failure is more than a red dot. - const failure = diff === null && model.state === 'error' ? errorText(block) : null return ( -
-
- {leadingFor(model.state)} - {status !== null && {status}} - {model.title} - - {filePath !== undefined ? ( - - ) : ( - {model.summary} - )} -
- {diff !== null && ( - - )} - {failure !== null &&
{failure}
} -
+ } + title={model.title} + summary={model.summary} + body={null} + output={model.output} + errorSummary={model.errorSummary} + diff={diff} + state={model.state} + filePath={model.filePath} + onOpenFile={openFile} + inspect={inspect} + /> ) } @@ -117,7 +67,7 @@ export const fileMutationToolview = { * @param ctx - registrant context (disposal rides ctx.effect inside slots.register). */ apply(ctx: Context): void { - ctx.slots.register({ name: 'conversation.chat.toolview', key: 'edit' }, FileMutationRow) - ctx.slots.register({ name: 'conversation.chat.toolview', key: 'write' }, FileMutationRow) + ctx.slots.register({ name: 'conversation.chat.toolview', key: 'edit', locale: NS }, FileMutationRow) + ctx.slots.register({ name: 'conversation.chat.toolview', key: 'write', locale: NS }, FileMutationRow) }, } diff --git a/packages/client/ui-conversation/src/client/toolviews/read-row.module.css b/packages/client/ui-conversation/src/client/toolviews/read-row.module.css deleted file mode 100644 index a03a949451..0000000000 --- a/packages/client/ui-conversation/src/client/toolviews/read-row.module.css +++ /dev/null @@ -1,119 +0,0 @@ -/* Read toolview: same geometry/tokens as ToolRow (figma Read · {path}), plus - the read card the row stacks under its summary line. */ - -/* Summary line over the read card; the summary row keeps its own 24px height, - so the card is a column around it rather than a change to it. */ -.card { - display: flex; - flex-direction: column; -} - -/* Row indentation matches ToolRow's expanded bodies (16px leading + 6px gap), - and replaces the primitive's standalone vertical margin with the flow's. */ -.read { - margin: 4px 0 4px 22px; -} - -.root { - position: relative; /* sweep-glare overlay anchor */ - overflow: hidden; - display: flex; - align-items: center; - height: 24px; - min-width: 0; -} - -/* Running sweep glare — same pattern as BashRow/ToolRow, so a running read row - gives the same executing feedback a running command row does. The leading - read icon stays static (a read has no per-step state to animate); the sweep - is the row-level running signal. */ -.root[data-state='running']::after { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - width: 300px; - background: linear-gradient( - 90deg, - transparent 0%, - color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, - transparent 100% - ); - animation: dsh-read-row-sweep 2.6s ease-out infinite; - pointer-events: none; -} - -@keyframes dsh-read-row-sweep { - 0% { left: -300px; } - 90%, 100% { left: 100%; } -} - -.leading { - flex: none; - width: 16px; - height: 16px; - display: inline-flex; - align-items: center; - justify-content: center; - margin-right: 6px; - color: var(--dsw-alias-label-tertiary); -} - -.title { - flex: none; - font-size: 14px; - line-height: 24px; - color: var(--dsw-alias-label-secondary); -} - -.sep { - flex: none; - width: 2px; - height: 2px; - border-radius: 1px; - margin: 0 8px; - background: var(--dsw-alias-label-caption); -} - -.summary { - flex: 1 1 auto; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 14px; - line-height: 24px; - color: var(--dsw-alias-label-tertiary); -} - -/* File path: same geometry as .summary; hover underline + pointer. */ -.fileLink { - flex: 1 1 auto; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - margin: 0; - padding: 0; - border: none; - background: none; - text-align: left; - font-size: 14px; - line-height: 24px; - color: var(--dsw-alias-label-tertiary); - cursor: pointer; -} - -.fileLink:hover { - text-decoration: underline; -} - -.visuallyHidden { - position: absolute; - width: 1px; - height: 1px; - overflow: hidden; - clip: rect(0 0 0 0); - white-space: nowrap; -} diff --git a/packages/client/ui-conversation/src/client/toolviews/read-row.tsx b/packages/client/ui-conversation/src/client/toolviews/read-row.tsx index 4527f85537..8d3694eeef 100644 --- a/packages/client/ui-conversation/src/client/toolviews/read-row.tsx +++ b/packages/client/ui-conversation/src/client/toolviews/read-row.tsx @@ -1,88 +1,49 @@ -// Read toolview registrant: the keyed toolview hole for the read tool -// (ctx.slots.register + ToolRowProps only — never imports the chat domain). -// Product chrome matches ToolRow (figma: Read · {path}); the summary is the -// file path as an openable link, exactly as the generic read row draws it. -// -// A read RESULT declares the read render intent, so this row renders the file's -// own line-numbered, syntax-highlighted content through ReadBlock resident -// below its summary line — the same posture BashRow gives a terminal card. The -// card is capped at CHAT_READ_MAX_LINES (the chat flow's tighter cap over the -// block's own default of 16) with the block's internal expander keeping a long -// read from taking over the message flow. A running read (no result yet) and a -// non-read result both render the summary row alone. The read intent is +// Read toolview registrant: the keyed toolview hole for the read tool. The row +// composes the shared ToolRow (chrome, running sweep, whole-row expand) and +// feeds it the file's line-numbered, syntax-highlighted content as ToolRow's +// `read` card material, so it renders through ReadBlock in the collapsed-by- +// default expanded body — the same unified interaction every other card row +// has. The summary path is an openable host link. A running read (no result +// yet) and a non-read result render the summary row alone: the read intent is // result-side only, so there is no running-state read card to draw. import type { Context } from 'cordis' -import { IconBrowseOutline16, ReadBlock, StateDot } from '@deepseek-ai/dsh-client-ui-primitives' +import { IconBrowseOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' +import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' import type { ToolRowProps } from '../contract/slots.ts' -import { CHAT_READ_MAX_LINES, readCardModel } from '../contract/read-card-model.ts' -import { toolRowModel, type ToolRowState } from '../contract/tool-call-model.ts' -import css from './read-row.module.css' +import { readCardModel } from '../contract/read-card-model.ts' +import { toolRowModel } from '../contract/tool-call-model.ts' +import { ToolRow } from '../chat/ToolRow.tsx' +import { NS } from '../locales.ts' -/** Leading-slot state substitution: the tool icon yields to the state dot - * (error = red, interrupted = amber). Running keeps the icon. */ -function leadingFor(state: ToolRowState) { - switch (state) { - case 'error': return - case 'stopped': return - default: return - } -} - -/** Visually hidden status — StateDot is aria-hidden; AT needs a text label. */ -function stateStatus(state: ToolRowState): string | null { - switch (state) { - case 'running': return '运行中' - case 'error': return '失败' - case 'stopped': return '已停止' - default: return null - } -} +/** Full row props: the toolview runtime share plus the standard locale seat. */ +type ReadRowProps = ToolRowProps & PropsLocale<'conversation'> /** * Read row: icon + Read · {path} in the shared ToolRow chrome, with the file's - * read card resident below it. The summary path is an openable host link when - * the row names a single file; the card's copy and expand controls plus that - * link are the row's only interactions (tool rows are not details-panel - * targets). + * read card as the row's collapsed-by-default card body. The summary path is an + * openable host link when the row names a single file. */ -export function ReadRow({ toolName, block, sessionId, useSessions, openFile }: ToolRowProps) { - // Session workspace root: the read view's path relativizes against it (a - // workspace-rooted absolute path shows its short form), which the pure - // presenter cannot do. - const cwd = useSessions(list => list.byId[sessionId]?.cwd) +export function ReadRow({ toolName, block, cwd, openFile, inspect, t }: ReadRowProps) { const model = toolRowModel(toolName, block, cwd) const read = readCardModel(block, cwd) - const status = stateStatus(model.state) - const filePath = model.filePath return ( -
- {/* jscpd:ignore-start — the summary-line chrome (leading, status, title, - sep, path-link/summary) is the shared ToolRow row shape every keyed - toolview draws; extracting it into one component is a separate change - tracked for all rows at once, not this read-card PR. */} -
- {leadingFor(model.state)} - {status !== null && {status}} - {model.title} - - {filePath !== undefined ? ( - - ) : ( - {model.summary} - )} -
- {/* jscpd:ignore-end */} - {read !== null && ( - - )} -
+ } + title={model.title} + summary={model.summary} + body={null} + output={model.output} + errorSummary={model.errorSummary} + read={read} + state={model.state} + filePath={model.filePath} + onOpenFile={openFile} + inspect={inspect} + /> ) } @@ -100,6 +61,6 @@ export const readToolview = { * @param ctx - registrant context (disposal rides ctx.effect inside slots.register). */ apply(ctx: Context): void { - ctx.slots.register({ name: 'conversation.chat.toolview', key: 'read' }, ReadRow) + ctx.slots.register({ name: 'conversation.chat.toolview', key: 'read', locale: NS }, ReadRow) }, } diff --git a/packages/client/ui-conversation/src/client/toolviews/search-row.module.css b/packages/client/ui-conversation/src/client/toolviews/search-row.module.css deleted file mode 100644 index 21908bd9e1..0000000000 --- a/packages/client/ui-conversation/src/client/toolviews/search-row.module.css +++ /dev/null @@ -1,117 +0,0 @@ -/* Search toolview: same geometry/tokens as ToolRow and BashRow (figma - Search · summary), plus the search card the row stacks resident under its - summary line. */ - -/* Summary line over the search card; the summary row keeps its own 24px - height, so the card is a column around it rather than a change to it. */ -.card { - display: flex; - flex-direction: column; -} - -/* Row indentation matches ToolRow's expanded bodies (16px leading + 6px gap), - and replaces the primitive's standalone vertical margin with the flow's. */ -.search { - margin: 4px 0 4px 22px; -} - -.root { - position: relative; /* sweep-glare overlay anchor */ - overflow: hidden; - display: flex; - align-items: center; - height: 24px; - min-width: 0; -} - -/* Running sweep glare — same deepsuite ShimmerText pattern as ToolRow / BashRow. */ -.root[data-state='running']::after { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - width: 300px; - background: linear-gradient( - 90deg, - transparent 0%, - color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, - transparent 100% - ); - animation: dsh-search-row-sweep 2.6s ease-out infinite; - pointer-events: none; -} - -@keyframes dsh-search-row-sweep { - 0% { left: -300px; } - 90%, 100% { left: 100%; } -} - -.leading { - flex: none; - width: 16px; - height: 16px; - display: inline-flex; - align-items: center; - justify-content: center; - margin-right: 6px; - color: var(--dsw-alias-label-tertiary); -} - -.title { - flex: none; - font-size: 14px; - line-height: 24px; - color: var(--dsw-alias-label-secondary); -} - -.sep { - flex: none; - width: 2px; - height: 2px; - border-radius: 1px; - margin: 0 8px; - background: var(--dsw-alias-label-caption); -} - -.summary { - flex: 1 1 auto; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 14px; - line-height: 24px; - color: var(--dsw-alias-label-tertiary); -} - -.visuallyHidden { - position: absolute; - width: 1px; - height: 1px; - overflow: hidden; - clip: rect(0 0 0 0); - white-space: nowrap; -} - -/* The result text for an errored search, indented to the card's own column and - in the error tone, standing in for the search card the failure path does not - produce. */ -.failure { - margin: 4px 0 4px 22px; - white-space: pre-wrap; - overflow-wrap: anywhere; - font: var(--dsw-font-xs-13); - color: var(--dsw-alias-state-error-primary); -} - -/* The recovery footer for a capped search: the model-facing result text (its - `Full … stored at …` locator) shown below the card in the muted tone, since - the card holds only the retained rows. Same column indent as the card body. */ -.recovery { - margin: 4px 0 4px 22px; - white-space: pre-wrap; - overflow-wrap: anywhere; - font: var(--dsw-font-xs-13); - color: var(--dsw-alias-label-tertiary); -} diff --git a/packages/client/ui-conversation/src/client/toolviews/search-row.tsx b/packages/client/ui-conversation/src/client/toolviews/search-row.tsx index 8c0181ba78..0fb08ca5e9 100644 --- a/packages/client/ui-conversation/src/client/toolviews/search-row.tsx +++ b/packages/client/ui-conversation/src/client/toolviews/search-row.tsx @@ -1,109 +1,59 @@ -// Search toolview registrant: the keyed toolview hole (ctx.slots.register + -// ToolRowProps only — never imports the chat domain). One SearchRow component -// registered under both `grep` and `glob`, since both tools declare the same -// `card: 'search'` render intent and render as one visual object; the row reads -// the `kind` discriminant off the derived model to draw grouped matches or a -// path list. Product chrome matches ToolRow / BashRow (Search · {summary}). -// -// A search call declares its render intent result-time only, so this row's -// search card is resident below the summary rather than expand-gated: the row -// itself has no expand control, and the card's own copy, per-file collapse, and -// head/tail expand are the row's only interactions. CHAT_SEARCH_MAX_LINES is -// passed as `maxLines` — the chat flow's tighter cap over the block's own -// default of 16 — so a large result stays bounded in the message flow. +// Search toolview registrant: the keyed toolview hole for the `grep` and `glob` +// tools. One SearchRow component registered under both, since both declare the +// same `card: 'search'` render intent and render as one visual object; the +// derived model's `kind` decides the card shape (grouped matches or a path +// list). The row composes the shared ToolRow (chrome, running sweep, whole-row +// expand) and feeds it the completed search as ToolRow's `search` card +// material, so it renders through SearchBlock in the collapsed-by-default +// expanded body — with a capped search's recovery footer below the card. A +// search declares its render intent result-time only, so a running row is the +// summary line alone; a settled call with no search card (an errored search, a +// nested run_code sub-dispatch, a legacy generic result) surfaces its +// model-facing text through ToolRow's Output section instead. import type { Context } from 'cordis' -import { IconSearchOutline16, SearchBlock, StateDot } from '@deepseek-ai/dsh-client-ui-primitives' +import { IconSearchOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' +import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' import type { ToolRowProps } from '../contract/slots.ts' -import { CHAT_SEARCH_MAX_LINES, searchCardModel } from '../contract/search-card-model.ts' -import { toolRowModel, type ToolRowState } from '../contract/tool-call-model.ts' -import css from './search-row.module.css' +import { searchCardModel } from '../contract/search-card-model.ts' +import { toolRowModel } from '../contract/tool-call-model.ts' +import { ToolRow } from '../chat/ToolRow.tsx' +import { NS } from '../locales.ts' -/** Leading-slot glyph substitution: the search icon yields to the terminal - * state semantic (error = red, interrupted = amber). Running keeps the icon — - * the row sweep carries the in-flight signal. */ -function leadingFor(state: ToolRowState) { - switch (state) { - case 'error': return - case 'stopped': return - default: return - } -} - -/** Visually hidden status — StateDot is aria-hidden; assistive technology needs a text label. */ -function stateStatus(state: ToolRowState): string | null { - switch (state) { - case 'running': return '运行中' - case 'error': return '失败' - case 'stopped': return '已停止' - default: return null - } -} - -/** - * A settled result's text, flattened from its content blocks, for the arm that - * shows a result the search card cannot. Two cases reach it: an errored search - * (grep/glob emit no `presentResult` on an error result, so an errored search - * has no card), and a settled call whose result view is not a search card at all - * — a nested `run_code` sub-dispatch (the backend computes no presentationMeta - * for it, so `resultView` is null) or a legacy generic result. In both the keyed - * SearchRow owns the render slot, so without this arm the model-facing text would - * have nowhere to go: an errored search would read as a bare red dot, and a - * successful cardless result would show only its summary with its content lost. - * @param block - the frozen call slice. - * @returns the result text, or null for a running call or an empty result. - */ -function errorText(block: ToolRowProps['block']): string | null { - if (!('kind' in block)) return null - const parts: string[] = [] - for (const item of block.content) { - if (item.type === 'text') parts.push(item.text) - } - if (parts.length === 0 && block.error !== undefined) parts.push(`${block.error.name}: ${block.error.code}`) - const text = parts.join('\n') - return text === '' ? null : text -} +/** Full row props: the toolview runtime share plus the standard locale seat. */ +type SearchRowProps = ToolRowProps & PropsLocale<'conversation'> /** * Search row: icon + Search · {summary} in the shared ToolRow chrome, with the - * completed search's card resident below it, and — when the result was capped — - * the recovery footer below the card. The summary row is not a details-panel - * control, so the card's copy, per-file collapse, and expand controls are the - * row's only interactions. Registered under both `grep` and `glob`; the derived - * model's `kind` decides the card shape. + * completed search's card as the row's collapsed-by-default card body (a capped + * search's recovery footer rides below it, inside ToolRow). Registered under + * both `grep` and `glob`; the derived model's `kind` decides the card shape. A + * settled call with no search card surfaces its model-facing text through + * ToolRow's Output section, since the keyed SearchRow owns this render slot. */ -export function SearchRow({ toolName, block }: ToolRowProps) { +export function SearchRow({ toolName, block, inspect, t }: SearchRowProps) { const model = toolRowModel(toolName, block) const search = searchCardModel(block) - const status = stateStatus(model.state) - // A settled call with no search card — an errored search (grep/glob emit no - // result view on error), a successful nested run_code sub-dispatch, or a - // legacy generic result — has its model-facing text nowhere else to go, since - // the keyed SearchRow owns this render slot. Surface it as the fallback body. - // A running call ('kind' absent) has no result to flatten; errorText returns - // null for it, so the arm stays closed until settle. - const settled = 'kind' in block - const fallback = search === null && settled ? errorText(block) : null return ( -
-
- {leadingFor(model.state)} - {status !== null && {status}} - {model.title} - - {/* The result view's replacement title outranks the args-derived - summary, matching the terminal card's description precedence. */} - {search?.title ?? model.summary} -
- {search !== null && ( - - )} - {/* A capped search drops rows from the card; its recovery locator (the - `Full … stored at …` footer) lives only in the result text, so show it - below the card so the one path to the dropped rows survives. */} - {search?.recovery !== undefined &&
{search.recovery}
} - {fallback !== null &&
{fallback}
} -
+ } + title={model.title} + // The result view's replacement title outranks the args-derived summary, + // matching the terminal card's description precedence. + summary={search?.title ?? model.summary} + body={null} + // A settled call with no search card (errored search, nested run_code + // sub-dispatch, legacy generic result) has its text nowhere else to go; + // ToolRow's Output section carries it, and errorSummary its first line. + output={search === null ? model.output : null} + errorSummary={model.errorSummary} + search={search} + state={model.state} + inspect={inspect} + /> ) } @@ -123,7 +73,7 @@ export const searchToolview = { * @param ctx - registrant context (disposal rides ctx.effect inside slots.register). */ apply(ctx: Context): void { - ctx.slots.register({ name: 'conversation.chat.toolview', key: 'grep' }, SearchRow) - ctx.slots.register({ name: 'conversation.chat.toolview', key: 'glob' }, SearchRow) + ctx.slots.register({ name: 'conversation.chat.toolview', key: 'grep', locale: NS }, SearchRow) + ctx.slots.register({ name: 'conversation.chat.toolview', key: 'glob', locale: NS }, SearchRow) }, } diff --git a/packages/client/ui-conversation/src/client/toolviews/web-row.module.css b/packages/client/ui-conversation/src/client/toolviews/web-row.module.css deleted file mode 100644 index 0b1519e218..0000000000 --- a/packages/client/ui-conversation/src/client/toolviews/web-row.module.css +++ /dev/null @@ -1,95 +0,0 @@ -/* Web toolview: same geometry/tokens as ToolRow (figma icon · summary), plus - the web card the row stacks under its summary line, mirroring the bash row's - resident terminal card. */ - -/* Summary line over the web card; the summary row keeps its own 24px height, - so the card is a column around it rather than a change to it. */ -.card { - display: flex; - flex-direction: column; -} - -/* Row indentation matches ToolRow's expanded bodies (16px leading + 6px gap), - and replaces the primitive's standalone vertical margin with the flow's. */ -.web { - margin: 4px 0 4px 22px; -} - -.root { - position: relative; /* sweep-glare overlay anchor */ - overflow: hidden; - display: flex; - align-items: center; - height: 24px; - min-width: 0; -} - -/* Running sweep glare — same deepsuite ShimmerText pattern as ToolRow. */ -.root[data-state='running']::after { - content: ''; - position: absolute; - top: 0; - bottom: 0; - left: 0; - width: 300px; - background: linear-gradient( - 90deg, - transparent 0%, - color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, - transparent 100% - ); - animation: dsh-web-row-sweep 2.6s ease-out infinite; - pointer-events: none; -} - -@keyframes dsh-web-row-sweep { - 0% { left: -300px; } - 90%, 100% { left: 100%; } -} - -.leading { - flex: none; - width: 16px; - height: 16px; - display: inline-flex; - align-items: center; - justify-content: center; - margin-right: 6px; - color: var(--dsw-alias-label-tertiary); -} - -.title { - flex: none; - font-size: 14px; - line-height: 24px; - color: var(--dsw-alias-label-secondary); -} - -.sep { - flex: none; - width: 2px; - height: 2px; - border-radius: 1px; - margin: 0 8px; - background: var(--dsw-alias-label-caption); -} - -.summary { - flex: 1 1 auto; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 14px; - line-height: 24px; - color: var(--dsw-alias-label-tertiary); -} - -.visuallyHidden { - position: absolute; - width: 1px; - height: 1px; - overflow: hidden; - clip: rect(0 0 0 0); - white-space: nowrap; -} diff --git a/packages/client/ui-conversation/src/client/toolviews/web-row.tsx b/packages/client/ui-conversation/src/client/toolviews/web-row.tsx index b86c523a26..4d8ab5f59f 100644 --- a/packages/client/ui-conversation/src/client/toolviews/web-row.tsx +++ b/packages/client/ui-conversation/src/client/toolviews/web-row.tsx @@ -1,24 +1,25 @@ -// Web toolview registrant: third-party posture over the keyed toolview hole -// (ctx.slots.register + ToolRowProps only — never imports the chat domain). -// Registered under BOTH web_search and web_fetch, since both declare the one -// `web` render intent and render through the one WebBlock family; the row -// discriminates on the toolName only to pick its icon and title. -// -// A web tool declares the `web` render intent at result time, so this row -// renders the completed retrieval through WebBlock resident below its summary, -// the same posture BashRow uses for the terminal card: no expand control on the -// row itself, not a details-panel target, and the block's own expander keeps a -// long source list from taking over the message flow (CHAT_WEB_MAX_SOURCES is -// passed as maxSources — the chat flow's tighter cap over the block's default -// of 16). Until the call settles there is no web card (the tools keep a generic -// pending view), so a running row is the summary line alone. +// Web toolview registrant: the keyed toolview hole for the `web_search` and +// `web_fetch` tools. Registered under BOTH, since both declare the one `web` +// render intent and render through the one WebBlock family; the row +// discriminates on the toolName only to pick its icon and title. The row +// composes the shared ToolRow (chrome, running sweep, whole-row expand) and +// feeds it the completed retrieval as ToolRow's `web` card material, so it +// renders through WebBlock in the collapsed-by-default expanded body — the same +// unified interaction every other card row has. Until the call settles there is +// no web card (the tools keep a generic pending view), so a running row is the +// summary line alone. import type { Context } from 'cordis' -import { IconBrowseOutline16, IconSearchOutline16, StateDot, WebBlock } from '@deepseek-ai/dsh-client-ui-primitives' +import { IconBrowseOutline16, IconSearchOutline16 } from '@deepseek-ai/dsh-client-ui-primitives' +import type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots' import type { ToolRowProps } from '../contract/slots.ts' -import { CHAT_WEB_MAX_SOURCES, webCardModel } from '../contract/web-card-model.ts' -import { toolRowModel, type ToolRowState } from '../contract/tool-call-model.ts' -import css from './web-row.module.css' +import { webCardModel } from '../contract/web-card-model.ts' +import { toolRowModel } from '../contract/tool-call-model.ts' +import { ToolRow } from '../chat/ToolRow.tsx' +import { NS } from '../locales.ts' + +/** Full row props: the toolview runtime share plus the standard locale seat. */ +type WebRowProps = ToolRowProps & PropsLocale<'conversation'> /** web_fetch reads one URL; web_search queries. Titles are figma literals. */ const WEB_TITLES: Record = { @@ -26,49 +27,30 @@ const WEB_TITLES: Record = { web_fetch: 'Fetch', } -/** Leading icon per tool, yielding to the state semantic while failed/stopped. */ -function leadingFor(toolName: string, state: ToolRowState) { - switch (state) { - case 'error': return - case 'stopped': return - // Running keeps the icon — the row sweep carries the in-flight signal. - default: return toolName === 'web_fetch' ? : - } -} - -/** Visually hidden status — StateDot is aria-hidden; AT needs a text label. */ -function stateStatus(state: ToolRowState): string | null { - switch (state) { - case 'running': return '运行中' - case 'error': return '失败' - case 'stopped': return '已停止' - default: return null - } -} - /** * Web row: icon + Search/Fetch · {summary} in the shared ToolRow chrome, with - * the completed retrieval's web card resident below it. The summary row is not - * a details-panel control (tool rows stopped being one), so the card's own - * links and expander are the row's only interactions. + * the completed retrieval's web card as the row's collapsed-by-default card + * body. The row discriminates on `toolName` only to pick its icon and title. */ -export function WebRow({ toolName, block }: ToolRowProps) { +export function WebRow({ toolName, block, inspect, t }: WebRowProps) { const model = toolRowModel(toolName, block) const web = webCardModel(block) - const status = stateStatus(model.state) + const icon = toolName === 'web_fetch' ? : return ( -
-
- {leadingFor(toolName, model.state)} - {status !== null && {status}} - {WEB_TITLES[toolName] ?? model.title} - - {model.summary} -
- {web !== null && ( - - )} -
+ ) } @@ -86,7 +68,7 @@ export const webToolview = { * @param ctx - registrant context (disposal rides ctx.effect inside slots.register). */ apply(ctx: Context): void { - ctx.slots.register({ name: 'conversation.chat.toolview', key: 'web_search' }, WebRow) - ctx.slots.register({ name: 'conversation.chat.toolview', key: 'web_fetch' }, WebRow) + ctx.slots.register({ name: 'conversation.chat.toolview', key: 'web_search', locale: NS }, WebRow) + ctx.slots.register({ name: 'conversation.chat.toolview', key: 'web_fetch', locale: NS }, WebRow) }, }