Przeglądaj źródła

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).
Chinesezjc 2 miesięcy temu
rodzic
commit
6608ede1a0

+ 11 - 0
packages/client/ui-conversation/src/client/toolviews/search-row.module.css

@@ -93,3 +93,14 @@
   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);
+}

+ 25 - 0
packages/client/ui-conversation/src/client/toolviews/search-row.tsx

@@ -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
  * 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 search = searchCardModel(block)
   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 (
     <div className={css.card}>
       <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 && (
         <SearchBlock {...search.card} maxLines={CHAT_SEARCH_MAX_LINES} className={css.search} />
       )}
+      {failure !== null && <div className={css.failure}>{failure}</div>}
     </div>
   )
 }

+ 19 - 0
packages/client/ui-conversation/tests/search-card.spec.tsx

@@ -184,6 +184,25 @@ describe('SearchRow keyed card', () => {
     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', () => {
     const view = render(<SearchRow {...rowProps(settledGrep({
       resultView: resultMatches({ title: '3 matches in 2 files' }),