Merge remote-tracking branch 'origin/master' into worktree/web-session-titles
# Conflicts: # .agents/notes/implemented/process/2026-07-20-gui-testing-system.i18n.yaml # packages/client/ui-conversation/tests/apply-inject.spec.tsx # packages/client/ui-conversation/tests/chat-stats-bash-sample.spec.tsx # packages/client/ui-conversation/tests/gate-branch-tails.spec.tsx # packages/client/ui-conversation/tests/selection-survival.spec.ts # packages/client/ui-conversation/tests/skeleton-branches.spec.tsx # packages/client/ui-conversation/tests/skeleton.spec.tsx # packages/client/ui-layout/tests/service.spec.ts # packages/client/ui-sidebar/tests/apply.spec.tsx # packages/client/ui-sidebar/tests/store.spec.ts # packages/client/ui-trajectory/tests/views.spec.tsx # packages/client/web/src/app.tsx # packages/client/web/tests/boot.spec.tsx # packages/host/runtime/README.md # packages/host/runtime/tests/host-runtime.spec.ts
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
import { useSyncExternalStore } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ObservableSnapshot, SnapshotStore } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type { ObservableSnapshot, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { LoaderStatus } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import css from './AppRoot.module.css'
|
||||
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
/**
|
||||
* Real-UI assembly closure. Runs only after loader.settled(): resolves the
|
||||
* layout plugin's export surface from the loader module table (type-only
|
||||
* import keeps the plugin out of the shell bundle), closes SessionProvider and
|
||||
* scopedSlots over the shell's whitelist, and mounts RootBindingProvider so
|
||||
* root-slot inject factories can reach ctx.
|
||||
* Real-UI assembly closure. Runs only after loader.settled(): the whole
|
||||
* layout tree hangs off the built-in 'root' slot (ui-layout registers
|
||||
* AppFrame there and renders the child slots internally) — the shell's
|
||||
* render is the one ctx-level renderSlot call in the program.
|
||||
*/
|
||||
import type { ReactNode } from 'react'
|
||||
import type { Context } from 'cordis'
|
||||
import {
|
||||
createSessionProvider, RootBindingProvider, scopedSlots,
|
||||
} from '@deepseek-ai/dsh-client-web-react'
|
||||
import type { SessionId, SessionsService } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { SessionsService } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { DocumentTitle } from './DocumentTitle.tsx'
|
||||
|
||||
type LayoutExports = typeof import('@deepseek-ai/dsh-client-ui-layout/client')
|
||||
// Type-only: pulls the runtime's SlotMap declaration merge (the 'root' key) into this program.
|
||||
import type {} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
|
||||
/** Assembly inputs: the settled root ctx plus the loader's module-table read surface. */
|
||||
export interface AssemblyDeps {
|
||||
/** Client root context (all plugin services provided). */
|
||||
ctx: Context
|
||||
/** Module-table resolver (the loader's require; missing spec = throw). */
|
||||
/** Module-table resolver (the loader's require; missing spec = throw). Kept in the seam for future shell needs. */
|
||||
requireModule: (spec: string) => unknown
|
||||
}
|
||||
|
||||
@@ -30,67 +27,20 @@ export interface AssemblyDeps {
|
||||
*/
|
||||
export function buildRenderApp(deps: AssemblyDeps): () => ReactNode {
|
||||
const { ctx } = deps
|
||||
const layoutExports = deps.requireModule('@deepseek-ai/dsh-client-ui-layout/client') as LayoutExports
|
||||
const { AppFrame, CenterColumn, DetailsColumn } = layoutExports
|
||||
const layout = ctx.layout
|
||||
// ctx.get: the typed `sessions` Context merge is suspended pending the
|
||||
// client/host declaration-collision arbitration (runtime's merge note).
|
||||
const sessions = ctx.get('sessions') as SessionsService | undefined
|
||||
if (sessions === undefined) throw new Error('shell assembly: sessions service unavailable')
|
||||
|
||||
// Whitelist closure: the four layout-owned top slots, granted to the shell assembler.
|
||||
const slots = scopedSlots(ctx.slots.core, 'sidebar', 'conversation', 'details', 'conversation.empty')
|
||||
|
||||
// Stable references — created once per assembly, never per render.
|
||||
const rootBinding = { ctx }
|
||||
const useCurrent = (): SessionId | undefined => layout.current.useSelector((s) => s.sessionId)
|
||||
const useSidebar = layout.sidebar.useSelector
|
||||
const useDetails = layout.details.useSelector
|
||||
const setSidebarWidth = (px: number): void => { layout.setSidebarWidth(px) }
|
||||
const setDetailsWidth = (px: number): void => { layout.setDetailsWidth(px) }
|
||||
const useSessions = bindSnapshotSelector(sessions.list)
|
||||
const SessionDocumentTitle = (): ReactNode => {
|
||||
const id = useCurrent()
|
||||
const title = sessions.list.useSelector(state => id === undefined ? undefined : state.byId[id]?.title)
|
||||
const title = useSessions((state) => {
|
||||
const id = state.current
|
||||
return id === undefined ? undefined : state.byId[id]?.title
|
||||
})
|
||||
return <DocumentTitle {...title === undefined ? {} : { title }} />
|
||||
}
|
||||
|
||||
const renderBody = (id: SessionId): ReactNode => (
|
||||
<>
|
||||
<CenterColumn>{slots.renderSlot('conversation', { sessionId: id })}</CenterColumn>
|
||||
<DetailsColumn>{slots.renderSlot('details', { sessionId: id })}</DetailsColumn>
|
||||
</>
|
||||
)
|
||||
// No selected session: the conversation.empty root slot carries EmptyState
|
||||
// (ui-conversation registers it); the fallback keeps the grid shape until
|
||||
// that owner lands.
|
||||
const renderEmpty = (): ReactNode => (
|
||||
<>
|
||||
<CenterColumn>{slots.renderSlot('conversation.empty', {}, { fallback: null })}</CenterColumn>
|
||||
<DetailsColumn />
|
||||
</>
|
||||
)
|
||||
|
||||
// Provider deps speak plain string (web-react's inversion: it never imports
|
||||
// runtime); the assembler re-brands at this boundary — ids entering the
|
||||
// provider came from layout.current, which only holds validated SessionIds.
|
||||
const SessionProvider = createSessionProvider({
|
||||
useCurrent,
|
||||
resolveBinding: (id) => sessions.binding(id as SessionId),
|
||||
renderBody: (id) => renderBody(id as SessionId),
|
||||
})
|
||||
|
||||
return () => (
|
||||
<RootBindingProvider value={rootBinding}>
|
||||
<>
|
||||
<SessionDocumentTitle />
|
||||
<AppFrame
|
||||
useSidebar={useSidebar}
|
||||
useDetails={useDetails}
|
||||
setSidebarWidth={setSidebarWidth}
|
||||
setDetailsWidth={setDetailsWidth}
|
||||
sidebar={slots.renderSlot('sidebar', {})}
|
||||
>
|
||||
<SessionProvider renderEmpty={renderEmpty} />
|
||||
</AppFrame>
|
||||
</RootBindingProvider>
|
||||
{ctx.slots.renderSlot('root', {})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
import { Context } from 'cordis'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-web-react'
|
||||
import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { createSlotRenderer } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { createClientLoader, type ClientLoaderOptions } from '@deepseek-ai/dsh-client-runtime/loader'
|
||||
import { AppRoot } from './AppRoot.tsx'
|
||||
import { buildRenderApp } from './app.tsx'
|
||||
@@ -59,7 +60,13 @@ export function bootWebShell(el: HTMLElement, seams?: BootSeams): () => void {
|
||||
|
||||
loader.start()
|
||||
loader.settled().then(
|
||||
() => { settled.flip() },
|
||||
() => {
|
||||
// The renderer install is a shell-boot act, but ctx.slots exists only
|
||||
// once the runtime plugin loaded — so it lands here, after settled and
|
||||
// before the flip that lets renderApp call renderSlot('root').
|
||||
ctx.slots.install(createSlotRenderer())
|
||||
settled.flip()
|
||||
},
|
||||
() => { /* stay on the loading page; failures render from loader.status */ },
|
||||
)
|
||||
return () => { root.unmount() }
|
||||
|
||||
@@ -13,7 +13,6 @@ import * as ReactDomClient from 'react-dom/client'
|
||||
import * as Cordis from 'cordis'
|
||||
import * as UiSlots from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import * as WebReact from '@deepseek-ai/dsh-client-web-react'
|
||||
import * as WebReactStore from '@deepseek-ai/dsh-client-web-react/store'
|
||||
import * as UiPrimitives from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
|
||||
/**
|
||||
@@ -29,7 +28,6 @@ export function seedModules(): Record<string, unknown> {
|
||||
'cordis': Cordis,
|
||||
'@deepseek-ai/dsh-client-ui-slots': UiSlots,
|
||||
'@deepseek-ai/dsh-client-web-react': WebReact,
|
||||
'@deepseek-ai/dsh-client-web-react/store': WebReactStore,
|
||||
'@deepseek-ai/dsh-client-ui-primitives': UiPrimitives,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user