mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge remote-tracking branch 'origin/feat/directory-picker' into feat/workspace-directory-browser
This commit is contained in:
@@ -35,6 +35,15 @@ export function NativeDirectoryFlow(props: DirectoryFlowOwnerProps & NativeFlowI
|
||||
// latest handlers, not the ones captured when the chooser opened.
|
||||
const outcome = useRef(props)
|
||||
outcome.current = props
|
||||
// Unmount (HMR replacing the occupant) discards settlements wholesale: the
|
||||
// dead instance must neither adopt a path nor drive the owner's error
|
||||
// surface. The wire carries no per-request abort, so the host-side chooser
|
||||
// survives until answered — its answer just lands nowhere; the replacement
|
||||
// instance re-arms under the owner's still-open request. An injected-face
|
||||
// identity change alone (re-registration) keeps the pending settlement:
|
||||
// the chooser on the host display is still the same dialog.
|
||||
const alive = useRef(true)
|
||||
useEffect(() => () => { alive.current = false }, [])
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
armed.current = false
|
||||
@@ -43,8 +52,14 @@ export function NativeDirectoryFlow(props: DirectoryFlowOwnerProps & NativeFlowI
|
||||
if (armed.current) return
|
||||
armed.current = true
|
||||
pick().then(
|
||||
(path) => { if (path === null) outcome.current.onCancel(); else outcome.current.onPicked(path) },
|
||||
(reason: unknown) => { outcome.current.onError(reason instanceof Error ? reason.message : String(reason)) },
|
||||
(path) => {
|
||||
if (!alive.current) return
|
||||
if (path === null) outcome.current.onCancel(); else outcome.current.onPicked(path)
|
||||
},
|
||||
(reason: unknown) => {
|
||||
if (!alive.current) return
|
||||
outcome.current.onError(reason instanceof Error ? reason.message : String(reason))
|
||||
},
|
||||
)
|
||||
}, [open, pick])
|
||||
return null
|
||||
|
||||
@@ -118,6 +118,30 @@ describe('directory-picker-native client half', () => {
|
||||
expect(first.onPicked).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('discards a settlement that lands after the flow unmounted', async () => {
|
||||
let resolve!: (path: string | null) => void
|
||||
const pick = vi.fn(() => new Promise<string | null>((settle) => { resolve = settle }))
|
||||
const props = owner()
|
||||
const view = render(<NativeDirectoryFlow {...props} pick={pick} />)
|
||||
expect(pick).toHaveBeenCalledOnce()
|
||||
view.unmount()
|
||||
// The dead instance must neither adopt nor error; the owner's callbacks
|
||||
// stay untouched by the orphaned chooser's answer.
|
||||
await act(async () => { resolve('/tmp/late') })
|
||||
expect(props.onPicked).not.toHaveBeenCalled()
|
||||
expect(props.onCancel).not.toHaveBeenCalled()
|
||||
expect(props.onError).not.toHaveBeenCalled()
|
||||
|
||||
// The failure arm is discarded the same way.
|
||||
let reject!: (reason: unknown) => void
|
||||
const failing = vi.fn(() => new Promise<string | null>((_settle, rejectPick) => { reject = rejectPick }))
|
||||
const late = owner()
|
||||
const failingView = render(<NativeDirectoryFlow {...late} pick={failing} />)
|
||||
failingView.unmount()
|
||||
await act(async () => { reject(new Error('too late')) })
|
||||
expect(late.onError).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports null as cancellation and re-arms after the owner withdraws open', async () => {
|
||||
const pick = vi.fn(async () => null as string | null)
|
||||
const props = owner()
|
||||
|
||||
Reference in New Issue
Block a user