fix(cmd-j): surface cooperative palette failures

This commit is contained in:
Jinwoo-H
2026-08-27 04:27:21 -04:00
parent 2847905e02
commit a3cfa96e70
3 changed files with 97 additions and 23 deletions
@@ -9,10 +9,12 @@ import type { Worktree } from '../../../../shared/worktree/types'
const EMPTY_DOCUMENTS: ReadonlyMap<string, PaletteDocument> = new Map()
const DOCUMENT_BUILD_TIME_SLICE_MS = 16
type CompletedBuild = {
request: WorktreePaletteDocumentBuildRequest
documents: ReadonlyMap<string, PaletteDocument>
}
type CompletedBuild =
| {
request: WorktreePaletteDocumentBuildRequest
documents: ReadonlyMap<string, PaletteDocument>
}
| { 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 }
}
@@ -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 ? (
<output data-error>{this.state.error.message}</output>
) : (
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(
<ProbeErrorBoundary>
<DocumentProbe worktrees={[makeWorktree(1)]} />
</ProbeErrorBoundary>
)
)
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(
<ProbeErrorBoundary>
<SearchProbe query="needle" />
</ProbeErrorBoundary>
)
)
expect(container.querySelector('[data-error]')?.textContent).toBe('search failed')
consoleError.mockRestore()
})
})
@@ -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 }
}