feat(web): add workspace-aware session flow
This commit is contained in:
@@ -6,10 +6,10 @@
|
||||
* renders HERE with live parameters from the concession solve, and the
|
||||
* session pair renders under the SessionProvider standard seat (render-prop
|
||||
* form, injected by the renderer because the children declaration contains
|
||||
* session-scope slots; session slots get sessionId as a framework-standard
|
||||
* prop, so the owner shares stay empty). Pure component: everything arrives
|
||||
* through the four prop shares — zero cordis or framework imports, zero
|
||||
* self-made hooks.
|
||||
* session-scope slots; session data arrives through framework-standard props
|
||||
* and each registrant's inject face). Pure component: everything arrives
|
||||
* through the three framework shares — zero cordis or framework imports,
|
||||
* zero self-made hooks.
|
||||
*/
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
@@ -18,7 +18,7 @@ import { computeColumns } from './columns.ts'
|
||||
import type { createLayoutStore } from './stores.ts'
|
||||
import css from './AppFrame.module.css'
|
||||
|
||||
/** Full composed props: runtime share + child-slot render share + store share (no business face). */
|
||||
/** Full composed props: runtime share + child-slot render share + store share. */
|
||||
export type AppFrameProps =
|
||||
& PropsRuntime<'root'>
|
||||
& PropsRenderSlots<'sidebar' | 'conversation' | 'details' | 'conversation.empty'>
|
||||
@@ -82,8 +82,17 @@ function DragHandle(props: { side: 'sidebar' | 'details'; left: number; onStart:
|
||||
}
|
||||
|
||||
/** The three-column frame (see module doc). SessionProvider arrives as a standard seat (declaring a session-scope child summons it — no framework import). */
|
||||
export function AppFrame({ useStore, actions, renderSlot, SessionProvider }: AppFrameProps) {
|
||||
export function AppFrame({
|
||||
useStore,
|
||||
actions,
|
||||
renderSlot,
|
||||
SessionProvider,
|
||||
useSessions,
|
||||
useWorkspaces,
|
||||
}: AppFrameProps) {
|
||||
const panels = useStore((s) => s)
|
||||
const sessions = useSessions(s => s)
|
||||
const baselinesReady = useWorkspaces(s => s.baselinesReady)
|
||||
const frameRef = useRef<HTMLDivElement | null>(null)
|
||||
const [viewport, setViewport] = useState(() => window.innerWidth)
|
||||
|
||||
@@ -143,24 +152,47 @@ export function AppFrame({ useStore, actions, renderSlot, SessionProvider }: App
|
||||
sidebar keeps the mounted slot at the compact-rail width, and the
|
||||
component sees its rendered state as owner params decided here
|
||||
(collapsed follows the preference, not the resolved width). */}
|
||||
{renderSlot('sidebar', { collapsed: panels.sidebar === 0, width: cols.sidebar })}
|
||||
{renderSlot('sidebar', {
|
||||
collapsed: panels.sidebar === 0,
|
||||
width: cols.sidebar,
|
||||
})}
|
||||
</div>
|
||||
<SessionProvider
|
||||
empty={() => (
|
||||
{!baselinesReady
|
||||
? (
|
||||
<>
|
||||
<CenterColumn>{renderSlot('conversation.empty', {})}</CenterColumn>
|
||||
<CenterColumn>
|
||||
<div role="status">Loading workspaces and sessions…</div>
|
||||
</CenterColumn>
|
||||
<DetailsColumn />
|
||||
</>
|
||||
)}
|
||||
>
|
||||
{() => (
|
||||
<>
|
||||
{/* sessionId is a framework-standard prop on session slots — the owner passes nothing. */}
|
||||
<CenterColumn>{renderSlot('conversation', {})}</CenterColumn>
|
||||
<DetailsColumn>{renderSlot('details', {})}</DetailsColumn>
|
||||
</>
|
||||
)}
|
||||
</SessionProvider>
|
||||
)
|
||||
: sessions.intent !== undefined
|
||||
? (
|
||||
<>
|
||||
<CenterColumn>
|
||||
{renderSlot('conversation.empty', {})}
|
||||
</CenterColumn>
|
||||
<DetailsColumn />
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<SessionProvider
|
||||
empty={() => (
|
||||
<>
|
||||
<CenterColumn><div role="status">Opening session…</div></CenterColumn>
|
||||
<DetailsColumn />
|
||||
</>
|
||||
)}
|
||||
>
|
||||
{() => (
|
||||
<>
|
||||
{/* Session data and actions arrive from standard hooks and the registrant's inject face. */}
|
||||
<CenterColumn>{renderSlot('conversation', {})}</CenterColumn>
|
||||
<DetailsColumn>{renderSlot('details', {})}</DetailsColumn>
|
||||
</>
|
||||
)}
|
||||
</SessionProvider>
|
||||
)}
|
||||
{/* The collapsed rail is fixed-width: no resize handle while closed. */}
|
||||
{panels.sidebar > 0 && <DragHandle side="sidebar" left={cols.sidebar} onStart={onSidebarStart} onDrag={onSidebarDrag} onEnd={onDragEnd} />}
|
||||
{cols.details > 0 && <DragHandle side="details" left={viewport - cols.details} onStart={onDetailsStart} onDrag={onDetailsDrag} onEnd={onDragEnd} />}
|
||||
|
||||
@@ -29,8 +29,8 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
interface SlotMap {
|
||||
// The 'root' entry itself is the runtime's built-in slot (declared
|
||||
// there); these four are the frame's children, declared by the same
|
||||
// register() call that contributes AppFrame. Session slots carry no
|
||||
// owner share: the framework injects sessionId as a standard prop.
|
||||
// register() call that contributes AppFrame. Session owners never pass
|
||||
// sessionId: the framework injects it as a standard prop.
|
||||
'sidebar': { kind: 'single'; scope: 'root'; owner: SidebarOwnerProps }
|
||||
'conversation': { kind: 'single'; scope: 'session'; owner: ConvOwnerProps }
|
||||
'details': { kind: 'single'; scope: 'session'; owner: DetailsOwnerProps }
|
||||
@@ -41,12 +41,8 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
// OwnerShare contracts — the render-side share the slot owner supplies at
|
||||
// renderSlot. Registrants IMPORT these and compose their full component props
|
||||
// through the four-share intersection (PropsRuntime & PropsRenderSlots &
|
||||
// PropsStore & I). Session owner shares stay literally empty: a phantom
|
||||
// `sessionId?: never` would intersect with the framework's mandatory
|
||||
// SessionStandardProps.sessionId and collapse the composed props to never —
|
||||
// the anti-smuggling guard is mutually exclusive with standard injection, so
|
||||
// the standard member's own type is the only guard on standard keys. Phantom
|
||||
// members remain fine on keys the standards never claim (EmptyOwnerProps).
|
||||
// PropsStore & I). Conversation business state and actions arrive through
|
||||
// framework-standard hooks and each registrant's inject face, not owner props.
|
||||
|
||||
/** Sidebar owner share: live column state from the frame's concession solve. */
|
||||
export interface SidebarOwnerProps {
|
||||
@@ -56,13 +52,13 @@ export interface SidebarOwnerProps {
|
||||
width: number
|
||||
}
|
||||
|
||||
/** Conversation owner share: empty — sessionId arrives as a framework-standard prop. */
|
||||
/** Conversation owner share: business state and actions belong to the registrant. */
|
||||
export interface ConvOwnerProps {}
|
||||
|
||||
/** Details owner share: empty — sessionId arrives as a framework-standard prop. */
|
||||
export interface DetailsOwnerProps {}
|
||||
|
||||
/** Empty-state owner share (ui-conversation registers EmptyState here). */
|
||||
/** Empty-state owner share: business state and actions belong to the registrant. */
|
||||
export interface EmptyOwnerProps { children?: never }
|
||||
|
||||
/** Required services (cordis fiber inject — the loader passes the whole export surface as an object plugin). */
|
||||
@@ -89,9 +85,8 @@ export function apply(ctx: ClientContext): void {
|
||||
// Exclusive store: the factory itself — the framework instantiates per
|
||||
// entry and delivers useStore/actions to AppFrame as standard props.
|
||||
store: createLayoutStore,
|
||||
// No business face for the frame (I = {}): the hook's job is the
|
||||
// assembly side effect wiring the entry's bound actions into the
|
||||
// cross-plugin service seam.
|
||||
// The hook's only side effect connects the root store to ctx.layout;
|
||||
// conversation business actions belong to their registrants.
|
||||
inject: (actions: PanelActions) => {
|
||||
layout.attachPanels(actions)
|
||||
return {}
|
||||
|
||||
@@ -13,19 +13,19 @@ import {
|
||||
SIDEBAR_DEFAULT, SIDEBAR_MAX, SIDEBAR_MIN,
|
||||
} from './columns.ts'
|
||||
|
||||
/** Panel width preferences in px (0 = closed) — the layout store's state. */
|
||||
type PanelWidths = { sidebar: number; details: number }
|
||||
/** Layout store state: panel width preferences in px (0 = closed). */
|
||||
type LayoutState = { sidebar: number; details: number }
|
||||
|
||||
/**
|
||||
* Annotation twin of the actions literal below (the export needs a declared
|
||||
* return type); drift fails assignability at the defineStore call.
|
||||
*/
|
||||
type LayoutActions = {
|
||||
setSidebar: (draft: PanelWidths, px: number) => void
|
||||
setDetails: (draft: PanelWidths, px: number) => void
|
||||
toggleSidebar: (draft: PanelWidths) => void
|
||||
openDetails: (draft: PanelWidths) => void
|
||||
closeDetails: (draft: PanelWidths) => void
|
||||
setSidebar: (draft: LayoutState, px: number) => void
|
||||
setDetails: (draft: LayoutState, px: number) => void
|
||||
toggleSidebar: (draft: LayoutState) => void
|
||||
openDetails: (draft: LayoutState) => void
|
||||
closeDetails: (draft: LayoutState) => void
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,9 +36,9 @@ type LayoutActions = {
|
||||
* open/close transitions write 0 / the default explicitly.
|
||||
* @returns the store handle (spec + type + identity + factory in one).
|
||||
*/
|
||||
export function createLayoutStore(): EngineStoreHandle<PanelWidths, LayoutActions> {
|
||||
return defineStore({
|
||||
init: () => ({ sidebar: SIDEBAR_DEFAULT, details: 0 }),
|
||||
export function createLayoutStore(): EngineStoreHandle<LayoutState, LayoutActions> {
|
||||
const handle = defineStore({
|
||||
init: (): LayoutState => ({ sidebar: SIDEBAR_DEFAULT, details: 0 }),
|
||||
persist: 'dsh.layout.panels',
|
||||
actions: {
|
||||
setSidebar: (d, px: number) => { d.sidebar = clampWidth(px, SIDEBAR_MIN, SIDEBAR_MAX) },
|
||||
@@ -48,4 +48,5 @@ export function createLayoutStore(): EngineStoreHandle<PanelWidths, LayoutAction
|
||||
closeDetails: (d) => { d.details = 0 },
|
||||
},
|
||||
})
|
||||
return handle
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user