feat(host,client): compose directory picking through slots — dual-face -native, no wire advertisement

ui-workspace's two trigger surfaces each declare a single-kind directory-flow
hole (conversation.hero.workspace.directoryFlow / sidebar.workspaces.directoryFlow,
same owner contract) and keep only the trigger and the adoption: the Open-local-
folder entry renders while the surface's hole is occupied, and the occupant
reports one picked path per open through the hole's owner conversation
(open/busy/onPicked/onCancel/onError).

directory-picker-native becomes dual-face: its browser half fills both holes
with a renderless occupant driving host.pickDirectory, so the cordis.yml row
that mounts the backend also composes the client interaction — a mismatch is
impossible and a second flow package fails at client load.

With composition wiring both sides, the host.describe.directoryPicker
advertisement and the client's kind branching lose their last consumer:
the field, WorkspacesService.directoryPickerKind(), the DirectoryPickerKind
wire type, and the picker's per-open describe read are deleted. The connection
fixture now serves a deterministic pickDirectory path so the keyless snapshot
drives the full pick-then-adopt flow. ui-workspace's hand-rolled declaration
deferral is replaced by the deferRegistration helper it duplicated.
This commit is contained in:
creatixchu
2026-07-28 21:51:01 +08:00
parent 51402ac7af
commit 85ca8be104
59 changed files with 614 additions and 385 deletions

View File

@@ -192,17 +192,14 @@ describe('host.listDirectory / host.createDirectory', () => {
})
})
it('refuses the browse RPCs under a native composition and advertises the kind in describe', async () => {
it('refuses the browse RPCs under a native composition', async () => {
const { api } = await harness()
expect((await api.host.describe(request({}))).result).toMatchObject({ ok: true, value: { directoryPicker: 'native' } })
expect((await api.host.listDirectory(request({}))).result).toMatchObject({
ok: false, error: { code: 'directory-picker-unavailable', details: { capability: 'native' } },
})
expect((await api.host.createDirectory(request({ path: '/x', name: 'y' }))).result).toMatchObject({
ok: false, error: { code: 'directory-picker-unavailable', details: { capability: 'native' } },
})
const browse = await harness(undefined, BROWSE_STUB)
expect((await browse.api.host.describe(request({}))).result).toMatchObject({ ok: true, value: { directoryPicker: 'browse' } })
})
})

View File

@@ -48,7 +48,7 @@ function scriptedApi(overrides: {
...overrides.sessions,
},
host: {
describe: r => ok(r, { version: '0-test', cwd: '/t', attachedSessions: 0, directoryPicker: 'browse' as const }),
describe: r => ok(r, { version: '0-test', cwd: '/t', attachedSessions: 0 }),
pickDirectory: r => ok(r, { path: null }),
listDirectory: r => ok(r, { path: '/t', home: '/t', crumbs: [], entries: [] }),
createDirectory: r => ok(r, { path: '/t/new' }),

View File

@@ -76,7 +76,7 @@ function fakeApi(overrides: Partial<{ muxFrames: MuxFrame[]; hostFrames: HostFra
},
host: {
async describe(request) {
return { rpcId: request.rpcId, result: { ok: true, value: { version: 'v', cwd: '/w', attachedSessions: 0, directoryPicker: 'native' as const } } }
return { rpcId: request.rpcId, result: { ok: true, value: { version: 'v', cwd: '/w', attachedSessions: 0 } } }
},
async pickDirectory(request) {
return { rpcId: request.rpcId, result: { ok: true, value: { path: null } } }

View File

@@ -218,12 +218,9 @@ describe('sessions domain schemas', () => {
describe('host domain schemas', () => {
it('validates describe request/value', () => {
expect(hostDescribeRequestSchema.parse({})).toEqual({})
const value = hostDescribeValueSchema.parse({ version: '1', cwd: '/x', provider: 'p', model: 'm', attachedSessions: 2, directoryPicker: 'native' })
const value = hostDescribeValueSchema.parse({ version: '1', cwd: '/x', provider: 'p', model: 'm', attachedSessions: 2 })
expect(value.attachedSessions).toBe(2)
expect(hostDescribeValueSchema.parse({ version: '1', cwd: '/x', attachedSessions: 0, directoryPicker: 'browse' }).provider).toBeUndefined()
// A kind beyond the two with methods survives the wire (merge-added
// capabilities advertise; the client hides the affordance).
expect(hostDescribeValueSchema.parse({ version: '1', cwd: '/x', attachedSessions: 0, directoryPicker: 'other' }).directoryPicker).toBe('other')
expect(hostDescribeValueSchema.parse({ version: '1', cwd: '/x', attachedSessions: 0 }).provider).toBeUndefined()
})
it('validates the browse listing/creation payloads', () => {