fix(web-search-card): surface result text when an errored search has no card
grep/glob return no presentResult on an error result, so an errored search had no card and the keyed SearchRow showed only a red dot — the model-facing error text (bad pattern, missing path, a nested run_code dispatch with no card) was nowhere on screen. Add an error-text arm mirroring the file-mutation and read rows. Added tests for the text arm and its name/code fallback. The unknown-kind fallback in search-card-model is already guarded (returns null → generic path).
This commit is contained in:
@@ -93,3 +93,14 @@
|
|||||||
clip: rect(0 0 0 0);
|
clip: rect(0 0 0 0);
|
||||||
white-space: nowrap;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,6 +40,27 @@ function stateStatus(state: ToolRowState): string | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A settled result's text, flattened from its content blocks, for the arm that
|
||||||
|
* shows a failure the search card cannot: grep/glob have no `presentResult` on
|
||||||
|
* an error result, so an errored search has no card, and the keyed row is not a
|
||||||
|
* details-panel target. Without this the failure — a bad pattern, a missing
|
||||||
|
* path, a nested run_code dispatch that returned no card — 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
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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. The summary row is not a
|
* completed search's card resident below it. The summary row is not a
|
||||||
@@ -51,6 +72,9 @@ 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 = stateStatus(model.state)
|
||||||
|
// An errored search has no card (grep/glob return no presentResult on error);
|
||||||
|
// surface its result text so the failure is more than a red dot.
|
||||||
|
const failure = search === null && model.state === 'error' ? errorText(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}>
|
||||||
@@ -65,6 +89,7 @@ export function SearchRow({ toolName, block }: ToolRowProps) {
|
|||||||
{search !== null && (
|
{search !== null && (
|
||||||
<SearchBlock {...search.card} maxLines={CHAT_SEARCH_MAX_LINES} className={css.search} />
|
<SearchBlock {...search.card} maxLines={CHAT_SEARCH_MAX_LINES} className={css.search} />
|
||||||
)}
|
)}
|
||||||
|
{failure !== null && <div className={css.failure}>{failure}</div>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,6 +184,25 @@ describe('SearchRow keyed card', () => {
|
|||||||
expect(errorView.container.querySelector('[data-variant="search"]')?.getAttribute('data-state')).toBe('error')
|
expect(errorView.container.querySelector('[data-variant="search"]')?.getAttribute('data-state')).toBe('error')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('surfaces the result text when an errored search has no card', () => {
|
||||||
|
// grep/glob return no presentResult on error → no card; the row shows the
|
||||||
|
// model-facing error text instead of a bare red dot.
|
||||||
|
const view = render(<SearchRow {...rowProps(settledGrep({
|
||||||
|
isError: true, resultView: null,
|
||||||
|
content: [{ type: 'text', text: 'grep: invalid regular expression' }],
|
||||||
|
}), 'grep')} />)
|
||||||
|
expect(searchKindOf(view.container)).toBeNull()
|
||||||
|
expect(view.getByText('grep: invalid regular expression')).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to the error name/code when an errored result has no text block', () => {
|
||||||
|
const view = render(<SearchRow {...rowProps(settledGrep({
|
||||||
|
isError: true, resultView: null, content: [],
|
||||||
|
error: { name: 'ToolError', code: 'timeout' },
|
||||||
|
}), 'grep')} />)
|
||||||
|
expect(view.getByText('ToolError: timeout')).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
it('shows the result view\'s replacement title instead of the args summary', () => {
|
it('shows the result view\'s replacement title instead of the args summary', () => {
|
||||||
const view = render(<SearchRow {...rowProps(settledGrep({
|
const view = render(<SearchRow {...rowProps(settledGrep({
|
||||||
resultView: resultMatches({ title: '3 matches in 2 files' }),
|
resultView: resultMatches({ title: '3 matches in 2 files' }),
|
||||||
|
|||||||
Reference in New Issue
Block a user