From 01d450f819116beaaf2ffc9ca93f31c8b50c5e5b Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Fri, 31 Jul 2026 16:13:10 +0800 Subject: [PATCH] refactor(web-cards): route read/search/web/diff rows through collapsed ToolRow The read, search, web, and file-mutation toolviews each hand-drew their own summary-row chrome and kept their card (ReadBlock/SearchBlock/WebBlock/ DiffBlock) resident below the summary. Fold all four into the shared ToolRow: ToolRow now takes read/search/web/diff card material alongside terminal, and renders it in its collapsed-by-default expanded body, so every tool row shares one expand interaction and starts collapsed. The GenericToolCard fallback and the DetailsPanel Output section keep rendering the same cards (the panel at full height); the per-row .module.css files are deleted as ToolRow owns the chrome. --- .../client/chat/GenericToolCard.module.css | 17 --- .../toolviews/file-mutation-row.module.css | 130 ---------------- .../client/toolviews/file-mutation-row.tsx | 134 +++++----------- .../src/client/toolviews/read-row.module.css | 119 --------------- .../src/client/toolviews/read-row.tsx | 109 +++++-------- .../client/toolviews/search-row.module.css | 117 -------------- .../src/client/toolviews/search-row.tsx | 144 ++++++------------ .../src/client/toolviews/web-row.module.css | 95 ------------ .../src/client/toolviews/web-row.tsx | 96 +++++------- 9 files changed, 163 insertions(+), 798 deletions(-) delete mode 100644 packages/client/ui-conversation/src/client/chat/GenericToolCard.module.css delete mode 100644 packages/client/ui-conversation/src/client/toolviews/file-mutation-row.module.css delete mode 100644 packages/client/ui-conversation/src/client/toolviews/read-row.module.css delete mode 100644 packages/client/ui-conversation/src/client/toolviews/search-row.module.css delete mode 100644 packages/client/ui-conversation/src/client/toolviews/web-row.module.css 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) }, }