diff --git a/src/renderer/src/components/cmd-j/use-cooperative-worktree-palette-documents.ts b/src/renderer/src/components/cmd-j/use-cooperative-worktree-palette-documents.ts index e96020d9bf3..cc4da22811b 100644 --- a/src/renderer/src/components/cmd-j/use-cooperative-worktree-palette-documents.ts +++ b/src/renderer/src/components/cmd-j/use-cooperative-worktree-palette-documents.ts @@ -9,10 +9,12 @@ import type { Worktree } from '../../../../shared/worktree/types' const EMPTY_DOCUMENTS: ReadonlyMap = new Map() const DOCUMENT_BUILD_TIME_SLICE_MS = 16 -type CompletedBuild = { - request: WorktreePaletteDocumentBuildRequest - documents: ReadonlyMap -} +type CompletedBuild = + | { + request: WorktreePaletteDocumentBuildRequest + documents: ReadonlyMap + } + | { request: WorktreePaletteDocumentBuildRequest; error: unknown } type WorktreePaletteDocumentBuildRequest = { worktrees: readonly Worktree[] @@ -31,17 +33,28 @@ export function useCooperativeWorktreePaletteDocuments( void buildWorktreePaletteDocumentsCooperatively(request.worktrees, request.sources, { shouldContinue: () => current, timeSliceMs: DOCUMENT_BUILD_TIME_SLICE_MS - }).then((documents) => { - if (current && documents) { - setCompleted({ request, documents }) + }).then( + (documents) => { + if (current && documents) { + setCompleted({ request, documents }) + } + }, + (error: unknown) => { + if (current) { + setCompleted({ request, error }) + } } - }) + ) return () => { current = false } }, [request]) - return completed?.request === request - ? { documents: completed.documents, pending: false } - : { documents: EMPTY_DOCUMENTS, pending: true } + if (completed?.request !== request) { + return { documents: EMPTY_DOCUMENTS, pending: true } + } + if ('error' in completed) { + throw completed.error + } + return { documents: completed.documents, pending: false } } diff --git a/src/renderer/src/components/cmd-j/use-cooperative-worktree-palette-hooks.test.tsx b/src/renderer/src/components/cmd-j/use-cooperative-worktree-palette-hooks.test.tsx index d39fb334230..9864f5a1347 100644 --- a/src/renderer/src/components/cmd-j/use-cooperative-worktree-palette-hooks.test.tsx +++ b/src/renderer/src/components/cmd-j/use-cooperative-worktree-palette-hooks.test.tsx @@ -1,6 +1,6 @@ // @vitest-environment happy-dom -import { act } from 'react' +import { act, Component, type ReactNode } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { PaletteDocument } from '@/lib/palette-match/palette-document' @@ -101,6 +101,22 @@ function SearchProbe({ query }: { query: string }): React.JSX.Element { ) } +class ProbeErrorBoundary extends Component<{ children: ReactNode }, { error: Error | null }> { + state: { error: Error | null } = { error: null } + + static getDerivedStateFromError(error: Error): { error: Error } { + return { error } + } + + render(): ReactNode { + return this.state.error ? ( + {this.state.error.message} + ) : ( + this.props.children + ) + } +} + describe('cooperative worktree palette hooks', () => { let container: HTMLDivElement let root: Root @@ -169,4 +185,36 @@ describe('cooperative worktree palette hooks', () => { expect(container.querySelector('output')?.dataset.pending).toBe('false') expect(container.textContent).toBe('current') }) + + it('surfaces a current document build failure to the palette error boundary', async () => { + cooperativeMocks.buildDocuments.mockRejectedValueOnce(new Error('document build failed')) + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined) + + await act(async () => + root.render( + + + + ) + ) + + expect(container.querySelector('[data-error]')?.textContent).toBe('document build failed') + consoleError.mockRestore() + }) + + it('surfaces a current search failure to the palette error boundary', async () => { + cooperativeMocks.searchDocuments.mockRejectedValueOnce(new Error('search failed')) + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined) + + await act(async () => + root.render( + + + + ) + ) + + expect(container.querySelector('[data-error]')?.textContent).toBe('search failed') + consoleError.mockRestore() + }) }) diff --git a/src/renderer/src/components/cmd-j/use-cooperative-worktree-palette-search.ts b/src/renderer/src/components/cmd-j/use-cooperative-worktree-palette-search.ts index 052922dae50..03257c1cd5b 100644 --- a/src/renderer/src/components/cmd-j/use-cooperative-worktree-palette-search.ts +++ b/src/renderer/src/components/cmd-j/use-cooperative-worktree-palette-search.ts @@ -9,10 +9,12 @@ import { parseCmdJTaskSourceUrl } from '@/lib/worktree-palette-task-url-match' const COOPERATIVE_SEARCH_MIN_WORKTREES = 200 -type CompletedSearch = { - request: WorktreePaletteSearchArgs - results: PaletteSearchResult[] -} +type CompletedSearch = + | { + request: WorktreePaletteSearchArgs + results: PaletteSearchResult[] + } + | { request: WorktreePaletteSearchArgs; error: unknown } type CooperativeWorktreePaletteSearch = { pending: boolean @@ -63,11 +65,18 @@ export function useCooperativeWorktreePaletteSearch( let current = true void searchWorktreeDocumentsCooperatively(request, { shouldContinue: () => current - }).then((results) => { - if (current && results) { - setCompleted({ request, results }) + }).then( + (results) => { + if (current && results) { + setCompleted({ request, results }) + } + }, + (error: unknown) => { + if (current) { + setCompleted({ request, error }) + } } - }) + ) return () => { current = false } @@ -76,7 +85,11 @@ export function useCooperativeWorktreePaletteSearch( if (immediateResults) { return { pending: false, results: immediateResults } } - return completed?.request === request - ? { pending: false, results: completed.results } - : { pending: true, results: [] } + if (completed?.request !== request) { + return { pending: true, results: [] } + } + if ('error' in completed) { + throw completed.error + } + return { pending: false, results: completed.results } }