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 (
-
+ }
+ 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. */}
-
+ }
+ 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 (
-