112 lines
5.5 KiB
TypeScript
112 lines
5.5 KiB
TypeScript
/**
|
|
* Workspace plugin, browser half. Two registrations: WorkspaceBrowser fills
|
|
* the sidebar shell's `sidebar.workspaces` hole (the whole browsing region),
|
|
* and WorkspacePicker fills the conversation hero's picker hole
|
|
* (`conversation.hero.workspace` — both hero forms). Both read real Host
|
|
* Workspaces through the global useWorkspaces hook, and each declares its
|
|
* own `single` directory-flow child hole for the composed picker package's
|
|
* client half (see the contract module doc). Export discipline:
|
|
* packages/client/AGENTS.md.
|
|
*/
|
|
import { deferRegistration } from '@deepseek-ai/dsh-client-ui-slots'
|
|
import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots'
|
|
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
import type { WorkspaceBrowserInjected, WorkspacePickerInjected } from './contract/slots.ts'
|
|
import { createWorkspaceViewStore } from './stores.ts'
|
|
import { WorkspaceBrowser } from './WorkspaceBrowser.tsx'
|
|
import { WorkspacePicker } from './WorkspacePicker.tsx'
|
|
|
|
export type {
|
|
DirectoryFlowOwnerProps, DirectoryFlowSlotName, DirectoryPickingHooks, DirectoryPickingInjected,
|
|
WorkspaceBrowserInjected, WorkspaceBrowserProps, WorkspacePickerInjected, WorkspacePickerProps,
|
|
} from './contract/slots.ts'
|
|
|
|
/**
|
|
* Required services (cordis fiber inject). The target slots are declared by
|
|
* the ui-sidebar / ui-conversation applies, whose activation order relative
|
|
* to this one is NOT constrained: dshClient.inject edges are informational
|
|
* (loading/prefetch metadata, never apply sequencing) and neither owner
|
|
* provides a waitable service. apply therefore registers via
|
|
* declaration-aware deferral instead of assuming order.
|
|
*/
|
|
export const inject = ['slots', 'sessions', 'workspaces']
|
|
|
|
/**
|
|
* Register the browser and picker once their slot declarations are on the
|
|
* ledger. Inject factories return plain callbacks; data reads use the
|
|
* framework's global hooks.
|
|
* @param ctx - client root context.
|
|
*/
|
|
export function apply(ctx: ClientContext): void {
|
|
// Stable per-surface occupancy sources (the renderer's hook cache keys by
|
|
// source identity): true while the surface's directory-flow hole is filled.
|
|
const flowSource = (hole: 'sidebar.workspaces.directoryFlow' | 'conversation.hero.workspace.directoryFlow'): HostObservable<boolean> => ({
|
|
getSnapshot: () => ctx.slots.entries(hole).length > 0,
|
|
subscribe: listener => ctx.slots.subscribe(hole, listener),
|
|
})
|
|
const browserFlowSource = flowSource('sidebar.workspaces.directoryFlow')
|
|
const pickerFlowSource = flowSource('conversation.hero.workspace.directoryFlow')
|
|
const browserInjected = (): WorkspaceBrowserInjected => ({
|
|
// Explicit group actions keep their target; unscoped New Session rides
|
|
// the runtime's shared action (recent-Workspace projection inside).
|
|
startSession: (workspaceId) => { ctx.workspaces.startSession(workspaceId) },
|
|
open: (sessionId) => { ctx.sessions.open(sessionId) },
|
|
renameSession: async (sessionId, title) => {
|
|
// Row → session-face hop: rename is a per-session verb (ISession), not
|
|
// a list-service verb; the binding resolves any listed session.
|
|
const session = ctx.sessions.binding(sessionId)?.session
|
|
if (session === undefined) throw new Error(`unknown session "${sessionId}"`)
|
|
const result = await session.rename(title)
|
|
if (!result.ok) throw new Error(result.error.message)
|
|
},
|
|
forkSession: (sessionId) => {
|
|
ctx.sessions.fork({ sessionId, increaseTitle: true })
|
|
.then((childId) => { ctx.sessions.open(childId) })
|
|
.catch(() => {
|
|
// Fork or child-rename failure keeps the current selection.
|
|
})
|
|
},
|
|
renameWorkspace: async (workspaceId, title) => { await ctx.workspaces.rename(workspaceId, title) },
|
|
deleteWorkspace: async (workspaceId) => { await ctx.workspaces.delete(workspaceId) },
|
|
insertSessionBefore: async (workspaceId, sessionId, beforeSessionId) => {
|
|
await ctx.workspaces.insertSessionBefore(workspaceId, sessionId, beforeSessionId)
|
|
},
|
|
createWorkspace: input => ctx.workspaces.create(input),
|
|
hooks: { directoryFlow: browserFlowSource },
|
|
})
|
|
const pickerInjected = (): WorkspacePickerInjected => ({
|
|
createWorkspace: input => ctx.workspaces.create(input),
|
|
hooks: { directoryFlow: pickerFlowSource },
|
|
})
|
|
// Declaration-aware registration (deferRegistration): each owner's
|
|
// declaring apply may activate after this one, and a register into an
|
|
// undeclared slot throws; the deferral also re-registers after an HMR
|
|
// collapse re-declares the slot. Each registration declares its own
|
|
// directory-flow child hole in the same call (declaration = render
|
|
// authorization, one table).
|
|
ctx.effect(() => {
|
|
const deferred = [
|
|
deferRegistration(ctx.slots, 'sidebar.workspaces', WorkspaceBrowser, () =>
|
|
ctx.slots.register(
|
|
{
|
|
name: 'sidebar.workspaces',
|
|
children: { 'sidebar.workspaces.directoryFlow': { kind: 'single', scope: 'root' } },
|
|
store: createWorkspaceViewStore(),
|
|
inject: browserInjected,
|
|
},
|
|
WorkspaceBrowser,
|
|
)),
|
|
deferRegistration(ctx.slots, 'conversation.hero.workspace', WorkspacePicker, () =>
|
|
ctx.slots.register(
|
|
{
|
|
name: 'conversation.hero.workspace',
|
|
children: { 'conversation.hero.workspace.directoryFlow': { kind: 'single', scope: 'root' } },
|
|
inject: pickerInjected,
|
|
},
|
|
WorkspacePicker,
|
|
)),
|
|
]
|
|
return () => { for (const entry of deferred) entry.dispose() }
|
|
}, 'ui-workspace: browser + picker registrations')
|
|
}
|