fix(client,host): review round — hooks-compartment occupancy, StrictMode re-arm, internal flow module, honest swap comment

- Directory-flow occupancy moves onto the inject face's reserved hooks
  compartment: apply publishes a stable observable per surface and the
  renderer binds useDirectoryFlow — no hand-rolled component subscriptions
  (the client contract's channel for registrant-private reactive facts).
- The native flow's alive guard re-arms in effect setup: StrictMode's
  development replay ran the cleanup once and every later outcome was
  discarded.
- NativeDirectoryFlow moves to a package-internal module; ./client exports
  only the Loader surface, tests import the internal module directly.
- The composition swap comment no longer advertises -browse as a complete
  swap before its dialog lands (stacked follow-up).
This commit is contained in:
creatixchu
2026-07-29 02:50:45 +08:00
parent 7114cc7475
commit a9b8fa2585
11 changed files with 140 additions and 114 deletions

View File

@@ -88,18 +88,18 @@ describe('ui-workspace apply', () => {
const browser = (b.slots.entries('sidebar.workspaces')[0]!.inject as () => WorkspaceBrowserInjected)()
const picker = (b.slots.entries('conversation.hero.workspace')[0]!.inject as () => WorkspacePickerInjected)()
expect(browser.hasDirectoryFlow()).toBe(false)
expect(picker.hasDirectoryFlow()).toBe(false)
// A flow occupant flips exactly its own surface.
expect(browser.hooks.directoryFlow.getSnapshot()).toBe(false)
expect(picker.hooks.directoryFlow.getSnapshot()).toBe(false)
// A flow occupant flips exactly its own surface, and the source notifies.
const notified = vi.fn()
const unsubscribe = browser.subscribeDirectoryFlow(notified)
const unsubscribe = browser.hooks.directoryFlow.subscribe(notified)
const dispose = b.slots.register({ name: 'sidebar.workspaces.directoryFlow' } as never, () => null)
expect(browser.hasDirectoryFlow()).toBe(true)
expect(picker.hasDirectoryFlow()).toBe(false)
expect(browser.hooks.directoryFlow.getSnapshot()).toBe(true)
expect(picker.hooks.directoryFlow.getSnapshot()).toBe(false)
await Promise.resolve()
expect(notified).toHaveBeenCalled()
dispose()
expect(browser.hasDirectoryFlow()).toBe(false)
expect(browser.hooks.directoryFlow.getSnapshot()).toBe(false)
unsubscribe()
})

View File

@@ -59,8 +59,7 @@ function mount(overrides: Partial<WorkspaceBrowserProps> = {}) {
deleteWorkspace: vi.fn(async () => {}),
insertSessionBefore: vi.fn(async () => {}),
createWorkspace: vi.fn(async () => workspace('created', [])),
hasDirectoryFlow: () => true,
subscribeDirectoryFlow: () => () => {},
useDirectoryFlow: bindSnapshotSelector({ getSnapshot: () => true, subscribe: () => () => {} }),
renderSlot: ((_name: string, owner: { open: boolean }) => (owner.open ? <div data-testid="directory-flow" /> : null)) as never,
...overrides,
}

View File

@@ -6,6 +6,7 @@ import type {
} from '@deepseek-ai/dsh-client-runtime/client'
import { WorkspaceCreateError } from '@deepseek-ai/dsh-client-runtime/client'
import type { DirectoryFlowOwnerProps } from '../src/client/contract/slots.ts'
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
import { WorkspacePicker } from '../src/client/WorkspacePicker.tsx'
afterEach(cleanup)
@@ -50,16 +51,19 @@ function flowProbe() {
return { probe, renderSlot }
}
/** Manual occupancy source: flip() drives the uSES subscription like a real registration change. */
/** Manual occupancy source bound like the renderer would: flip() drives the hook like a real registration change. */
function occupancySource(initial = true) {
let occupied = initial
const listeners = new Set<() => void>()
return {
hasDirectoryFlow: () => occupied,
subscribeDirectoryFlow: (listener: () => void) => {
const useDirectoryFlow = bindSnapshotSelector({
getSnapshot: () => occupied,
subscribe: (listener: () => void) => {
listeners.add(listener)
return () => { listeners.delete(listener) }
},
})
return {
useDirectoryFlow,
flip: (next: boolean) => {
occupied = next
for (const listener of [...listeners]) listener()
@@ -85,8 +89,7 @@ function mount(
onPick={onPick}
onClose={onClose}
createWorkspace={createWorkspace}
hasDirectoryFlow={occupancy.hasDirectoryFlow}
subscribeDirectoryFlow={occupancy.subscribeDirectoryFlow}
useDirectoryFlow={occupancy.useDirectoryFlow}
renderSlot={renderSlot}
/>
)
@@ -265,7 +268,7 @@ describe('WorkspacePicker', () => {
<WorkspacePicker
open useSessions={hook(sessions)} useWorkspaces={hook(workspaceState([]))}
onPick={vi.fn()} onClose={vi.fn()} createWorkspace={vi.fn()}
hasDirectoryFlow={() => true} subscribeDirectoryFlow={() => () => {}} renderSlot={renderSlot}
useDirectoryFlow={occupancySource().useDirectoryFlow} renderSlot={renderSlot}
/>,
)
expect(screen.queryByRole('menu')).toBeNull()
@@ -280,7 +283,7 @@ describe('WorkspacePicker', () => {
<WorkspacePicker
open anchorRef={anchor()} useSessions={hook(sessions)} useWorkspaces={hook(state)}
onPick={vi.fn()} onClose={vi.fn()} createWorkspace={vi.fn()}
hasDirectoryFlow={() => true} subscribeDirectoryFlow={() => () => {}} renderSlot={renderSlot}
useDirectoryFlow={occupancySource().useDirectoryFlow} renderSlot={renderSlot}
/>,
)
expect(screen.getByRole('status').textContent).toBe('Loading workspaces…')