refactor(ui-conversation): extract shared toolview row-status helpers
SearchRow (this PR) and FileMutationRow (landed on master) independently carry byte-identical rowStateStatus + rowResultText helpers, which the duplication gate flags once both are present. Extract both into contract/toolview-status.ts and consume them from both rows, deleting the clone rather than nudging it under the threshold.
This commit is contained in:
@@ -0,0 +1,45 @@
|
|||||||
|
// Shared toolview-row helpers for the keyed rows whose card is resident below a
|
||||||
|
// summary (SearchRow, FileMutationRow): the visually hidden run-state label and
|
||||||
|
// the flattened settled-result text for the fallback arm a card cannot render.
|
||||||
|
// Both are pure functions of a frozen call slice — no chat-domain imports — so a
|
||||||
|
// row stays a thin ToolRowProps consumer.
|
||||||
|
|
||||||
|
import type { ToolRowProps } from './slots.ts'
|
||||||
|
import type { ToolRowState } from './tool-call-model.ts'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visually hidden run-state label for a row's leading `StateDot` (which is
|
||||||
|
* `aria-hidden`), so assistive technology still announces the state. Returns
|
||||||
|
* null for the settled-ok state, which needs no spoken label.
|
||||||
|
* @param state - the row's run state.
|
||||||
|
* @returns the label, or null when none is needed.
|
||||||
|
*/
|
||||||
|
export function rowStateStatus(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 fallback
|
||||||
|
* arm a keyed row shows when its card cannot render the result — an errored call
|
||||||
|
* (the tool emits no result view on error) or a settled call with no card view
|
||||||
|
* (a nested `run_code` sub-dispatch, a legacy generic result). The keyed row owns
|
||||||
|
* the render slot, so without this the model-facing text would have nowhere to
|
||||||
|
* go. Falls back to the error name/code when the result carries no text block.
|
||||||
|
* @param block - the frozen call slice.
|
||||||
|
* @returns the result text, or null for a running call or an empty result.
|
||||||
|
*/
|
||||||
|
export function rowResultText(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
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import { DiffBlock, IconEditOutline16, StateDot } from '@deepseek-ai/dsh-client-
|
|||||||
import type { ToolRowProps } from '../contract/slots.ts'
|
import type { ToolRowProps } from '../contract/slots.ts'
|
||||||
import { CHAT_DIFF_MAX_LINES, diffCardModel } from '../contract/diff-card-model.ts'
|
import { CHAT_DIFF_MAX_LINES, diffCardModel } from '../contract/diff-card-model.ts'
|
||||||
import { toolRowModel, type ToolRowState } from '../contract/tool-call-model.ts'
|
import { toolRowModel, type ToolRowState } from '../contract/tool-call-model.ts'
|
||||||
|
import { rowResultText, rowStateStatus } from '../contract/toolview-status.ts'
|
||||||
import css from './file-mutation-row.module.css'
|
import css from './file-mutation-row.module.css'
|
||||||
|
|
||||||
function leadingFor(state: ToolRowState) {
|
function leadingFor(state: ToolRowState) {
|
||||||
@@ -29,37 +30,6 @@ function leadingFor(state: ToolRowState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* File-mutation row: icon + {Edit,Write} · {path} in the shared ToolRow chrome,
|
* 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
|
* with the applied diff resident below it. The summary is a path link (a file
|
||||||
@@ -70,11 +40,11 @@ function errorText(block: ToolRowProps['block']): string | null {
|
|||||||
export function FileMutationRow({ toolName, block, cwd, openFile }: ToolRowProps) {
|
export function FileMutationRow({ toolName, block, cwd, openFile }: ToolRowProps) {
|
||||||
const model = toolRowModel(toolName, block, cwd)
|
const model = toolRowModel(toolName, block, cwd)
|
||||||
const diff = diffCardModel(block)
|
const diff = diffCardModel(block)
|
||||||
const status = stateStatus(model.state)
|
const status = rowStateStatus(model.state)
|
||||||
const filePath = model.filePath
|
const filePath = model.filePath
|
||||||
// An errored mutation has no diff card (presentResult returns undefined on
|
// 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.
|
// isError); surface its result text so the failure is more than a red dot.
|
||||||
const failure = diff === null && model.state === 'error' ? errorText(block) : null
|
const failure = diff === null && model.state === 'error' ? rowResultText(block) : null
|
||||||
return (
|
return (
|
||||||
<div className={css.card}>
|
<div className={css.card}>
|
||||||
<div className={css.root} data-variant={model.variant} data-state={model.state}>
|
<div className={css.root} data-variant={model.variant} data-state={model.state}>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { IconSearchOutline16, SearchBlock, StateDot } from '@deepseek-ai/dsh-cli
|
|||||||
import type { ToolRowProps } from '../contract/slots.ts'
|
import type { ToolRowProps } from '../contract/slots.ts'
|
||||||
import { CHAT_SEARCH_MAX_LINES, searchCardModel } from '../contract/search-card-model.ts'
|
import { CHAT_SEARCH_MAX_LINES, searchCardModel } from '../contract/search-card-model.ts'
|
||||||
import { toolRowModel, type ToolRowState } from '../contract/tool-call-model.ts'
|
import { toolRowModel, type ToolRowState } from '../contract/tool-call-model.ts'
|
||||||
|
import { rowResultText, rowStateStatus } from '../contract/toolview-status.ts'
|
||||||
import css from './search-row.module.css'
|
import css from './search-row.module.css'
|
||||||
|
|
||||||
/** Leading-slot glyph substitution: the search icon yields to the terminal
|
/** Leading-slot glyph substitution: the search icon yields to the terminal
|
||||||
@@ -30,40 +31,6 @@ function leadingFor(state: ToolRowState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search row: icon + Search · {summary} in the shared ToolRow chrome, with the
|
* 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 —
|
* completed search's card resident below it, and — when the result was capped —
|
||||||
@@ -75,15 +42,15 @@ function errorText(block: ToolRowProps['block']): string | null {
|
|||||||
export function SearchRow({ toolName, block }: ToolRowProps) {
|
export function SearchRow({ toolName, block }: ToolRowProps) {
|
||||||
const model = toolRowModel(toolName, block)
|
const model = toolRowModel(toolName, block)
|
||||||
const search = searchCardModel(block)
|
const search = searchCardModel(block)
|
||||||
const status = stateStatus(model.state)
|
const status = rowStateStatus(model.state)
|
||||||
// A settled call with no search card — an errored search (grep/glob emit no
|
// 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
|
// 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
|
// 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.
|
// 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
|
// A running call ('kind' absent) has no result to flatten; rowResultText
|
||||||
// null for it, so the arm stays closed until settle.
|
// returns null for it, so the arm stays closed until settle.
|
||||||
const settled = 'kind' in block
|
const settled = 'kind' in block
|
||||||
const fallback = search === null && settled ? errorText(block) : null
|
const fallback = search === null && settled ? rowResultText(block) : null
|
||||||
return (
|
return (
|
||||||
<div className={css.card}>
|
<div className={css.card}>
|
||||||
<div className={css.root} data-variant="search" data-tool={toolName} data-state={model.state}>
|
<div className={css.root} data-variant="search" data-tool={toolName} data-state={model.state}>
|
||||||
|
|||||||
Reference in New Issue
Block a user