feat(web): add workspace-aware session flow

This commit is contained in:
imccyu
2026-07-25 16:04:48 +08:00
parent 755e2a8c51
commit 9eb9c70a8a
170 changed files with 7573 additions and 3006 deletions

View File

@@ -164,7 +164,7 @@ class SlotErrorBoundary extends Component<
/**
* Standard-kit synthesis shared by both scope branches: the global
* useSessions hook, the session pair, the store pair when declared, the
* useSessions/useWorkspaces hooks, the session pair, the store pair when declared, the
* renderSlot binding when children are declared, and the SessionProvider
* seat when the children declare a session-scope slot. Hosts hand out BARE
* observable sources (hooks never cross the host contract); every hook is
@@ -174,7 +174,10 @@ class SlotErrorBoundary extends Component<
function standardKit(host: SlotRendererHost, entry: StoredEntry, cell: SessionCell | undefined): {
kit: InjectedProps; actions: object | undefined
} {
const kit: InjectedProps = { useSessions: observableHook(host.sessions.list) }
const kit: InjectedProps = {
useSessions: observableHook(host.sessions.list),
useWorkspaces: observableHook(host.workspaces.list),
}
if (cell !== undefined) {
kit['useSession'] = observableHook(cell.session)
kit['sessionId'] = cell.sessionId

View File

@@ -38,6 +38,9 @@ function hostOver(core: SlotCore): SlotRendererHost {
current: { getSnapshot: () => undefined, subscribe: () => () => {} },
cell: () => undefined,
},
workspaces: {
list: { getSnapshot: () => ({}), subscribe: () => () => {} },
},
}
}

View File

@@ -81,6 +81,7 @@ function makeHost() {
const live = new Set<StoredEntry>()
const storeCache = new Map<StoredEntry, Map<string, StoreInstanceLike>>()
const list = observable<{ ids: string[] }>({ ids: [] })
const workspaces = observable<{ ids: string[] }>({ ids: [] })
const current = observable<string | undefined>(undefined)
const cells = new Map<string, SessionCell>()
@@ -122,10 +123,12 @@ function makeHost() {
current,
cell: (id) => cells.get(id),
},
workspaces: { list: workspaces },
}
return {
host,
list,
workspaces,
current,
declare: (key: string, spec: DeclaredSpec) => { specs.set(key, spec); bump(key) },
add: (key: string, partial: Omit<StoredEntry, 'options'> & { options?: StoredEntry['options'] }) => {
@@ -483,6 +486,19 @@ describe('standard-kit synthesis', () => {
expect(view.container.textContent).toBe('2')
})
it('delivers a live useWorkspaces hook to every slot component', () => {
const h = makeHost()
h.declare('k.single', SINGLE_ROOT)
h.add('k.single', {
component: ({ useWorkspaces }: { useWorkspaces: <S>(sel: (s: { ids: string[] }) => S) => S }) =>
<b>{useWorkspaces((s) => s.ids.length)}</b>,
})
const { view } = mountRoot(h, { 'k.single': SINGLE_ROOT }, (renderSlot) => renderSlot('k.single', {}))
expect(view.container.textContent).toBe('0')
act(() => { h.workspaces.set({ ids: ['w1'] }) })
expect(view.container.textContent).toBe('1')
})
it('delivers the session pair (bound useSession + sessionId) under SessionProvider', () => {
const h = makeHost()
h.declare('k.session', SINGLE_SESSION)
@@ -710,6 +726,7 @@ describe('inject: execution point, parameter derivation, cache granularity', ()
(renderSlot) => renderSlot('k.single', { owner: 'owner', shared: 'owner' }))
const props = seen.at(-1)!
expect(typeof props['useSessions']).toBe('function') // kit always present
expect(typeof props['useWorkspaces']).toBe('function')
expect(props['fromInject']).toBe('inject')
expect(props['owner']).toBe('owner')
expect(props['shared']).toBe('owner') // owner overrides inject

View File

@@ -52,6 +52,7 @@ function makeHost(bodies: { root: (rp: (key: string, owner: object) => React.Rea
current,
cell: (id) => cells.get(id),
},
workspaces: { list: observable<unknown>({ items: [] }) },
}
return {
host,

View File

@@ -42,6 +42,9 @@ function makeHost() {
current: { getSnapshot: () => undefined, subscribe: () => () => {} },
cell: () => undefined,
},
workspaces: {
list: { getSnapshot: () => ({}), subscribe: () => () => {} },
},
}
return {
host,