refactor(client): isolate trajectory history
This commit is contained in:
@@ -83,8 +83,8 @@ export function apply(ctx: Context): void {
|
||||
},
|
||||
}), 'ui-conversation: input standard-kit provider')
|
||||
|
||||
// Resident current-session-optional shell. It constructs the stable
|
||||
// composer frame; the strict session child decides whether Chat mounts it.
|
||||
// Resident current-session-optional shell. It owns the stable Hero/composer
|
||||
// frame while strict session slots fill only their session-bound regions.
|
||||
slots.register({
|
||||
name: 'conversation',
|
||||
children: {
|
||||
@@ -114,8 +114,8 @@ export function apply(ctx: Context): void {
|
||||
}),
|
||||
}, ConversationRoot)
|
||||
|
||||
// The strict session subtree owns the per-session store and view content;
|
||||
// the resident parent supplies the composer node through owner props.
|
||||
// The strict session subtree owns only per-session store and view content;
|
||||
// the resident parent keeps Hero and composer layout identity stable.
|
||||
slots.register({
|
||||
name: 'conversation.session',
|
||||
children: { 'conversation.view': { kind: 'list', scope: 'session' } },
|
||||
|
||||
@@ -19,7 +19,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
'conversation.session': { kind: 'single'; scope: 'session'; owner: ConversationSessionOwnerProps }
|
||||
/**
|
||||
* The conversation view ring: one list entry per view tab (chat here;
|
||||
* Trajectory from ui-trajectory), rendered one-at-a-time by
|
||||
* trajectory/waterfall from ui-trajectory), rendered one-at-a-time by
|
||||
* ConversationRoot via `only: <active id>`. Declared by this package's
|
||||
* 'conversation' entry (declaring is claiming). Session scope: views read
|
||||
* the conversation snapshot through the standard kit.
|
||||
@@ -117,8 +117,6 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
||||
|
||||
/** Owner share of the strict session content seat. */
|
||||
export interface ConversationSessionOwnerProps {
|
||||
/** Composer chain assembled by the resident parent; only Chat mounts it. */
|
||||
composer: ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,7 +141,7 @@ export interface ConvViewOwnerProps {}
|
||||
|
||||
/**
|
||||
* Owner share of a per-view toolview slot: the call material the rendering
|
||||
* view supplies per row. Uniform across views — the Trajectory
|
||||
* view supplies per row. Uniform across views — the trajectory/waterfall
|
||||
* toolview slots (same kind/scope/owner, names fixed by the slot-naming
|
||||
* discipline) land with their own row render sites; today only the chat slot
|
||||
* is declared (RendersCheck rejects a declaration nobody renders).
|
||||
|
||||
@@ -118,11 +118,6 @@ export function ConversationRoot({
|
||||
{inputBar}
|
||||
</div>
|
||||
)
|
||||
const composer = renderSlotChain(
|
||||
'conversation.composer',
|
||||
{ interactions: pending },
|
||||
{ fallback: composerBar, overlay: true },
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={css.root} data-phase={settling ? 'settling' : hero ? 'hero' : 'active'}>
|
||||
@@ -130,9 +125,12 @@ export function ConversationRoot({
|
||||
renders no chrome while blank but owns the draft-persistence mirror
|
||||
bind — unmounting it in the hero would lose pre-first-send text on
|
||||
a refresh or scope rebuild. */}
|
||||
{sessionId !== undefined
|
||||
? renderSlot('conversation.session', { composer })
|
||||
: composer}
|
||||
{sessionId !== undefined && renderSlot('conversation.session', {})}
|
||||
{renderSlotChain(
|
||||
'conversation.composer',
|
||||
{ interactions: pending },
|
||||
{ fallback: composerBar, overlay: true },
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Strict per-session conversation content: header, view ring, and chat store bindings. */
|
||||
|
||||
import { Fragment, useEffect, useSyncExternalStore } from 'react'
|
||||
import { useEffect, useSyncExternalStore } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { shallowEqual } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { SessionId, SessionListState, SessionSummary } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
@@ -24,7 +24,7 @@ function deriveAncestry(list: SessionListState, id: SessionId): readonly Session
|
||||
|
||||
export function ConversationSession({
|
||||
sessionId, useSession, useSessions, useInput, inputActions, useStore, actions,
|
||||
renderSlot, views, bindDraftMirror, open, composer,
|
||||
renderSlot, views, bindDraftMirror, open,
|
||||
}: ConversationSessionProps) {
|
||||
useSyncExternalStore(views.subscribe, views.version)
|
||||
const tabs = views.list()
|
||||
@@ -32,7 +32,6 @@ export function ConversationSession({
|
||||
const active = tabs.find(view => view.id === activeId) ?? tabs[0]
|
||||
const ancestry = useSessions(s => deriveAncestry(s, sessionId), shallowEqual)
|
||||
const composerPhase = useSession(s => s.composerPhase)
|
||||
const hasPending = useSession(s => s.pending.length > 0)
|
||||
const blank = useSession(s => s.blank)
|
||||
const inputState = useInput(s => s)
|
||||
const storedDraft = useStore(s => s.draft)
|
||||
@@ -45,11 +44,11 @@ export function ConversationSession({
|
||||
// the machine mirror, not this seed effect.
|
||||
}, [inputActions])
|
||||
|
||||
const blankHero = blank && composerPhase === 'blank'
|
||||
if (blank && composerPhase === 'blank') return null
|
||||
|
||||
return (
|
||||
<>
|
||||
{!blankHero && <header className={css.header}>
|
||||
<header className={css.header}>
|
||||
<div className={css.crumbRow}>
|
||||
<nav className={css.crumbs} aria-label="Session hierarchy">
|
||||
{ancestry.map((summary, index) => {
|
||||
@@ -87,13 +86,10 @@ export function ConversationSession({
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</header>}
|
||||
{!blankHero && <div className={css.viewArea}>
|
||||
</header>
|
||||
<div className={css.viewArea}>
|
||||
{active !== undefined && renderSlot('conversation.view', {}, { only: active.id })}
|
||||
</div>}
|
||||
{(blankHero || active?.id === 'chat' || hasPending) && (
|
||||
<Fragment key="composer">{composer}</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ function sessionFakeFor() {
|
||||
return {
|
||||
open: vi.fn(() => Promise.resolve()),
|
||||
loadOlder: vi.fn<ISession['loadOlder']>(() => Promise.resolve()),
|
||||
loadAllHistory: vi.fn<ISession['loadAllHistory']>(() => Promise.resolve()),
|
||||
prompt: vi.fn<ISession['prompt']>(() => Promise.resolve({ ok: true, value: { accepted: true } })),
|
||||
cancel: vi.fn<ISession['cancel']>(() => Promise.resolve({ ok: true, value: { accepted: true } })),
|
||||
} satisfies SessionBehaviorOverrides
|
||||
|
||||
@@ -59,7 +59,6 @@ async function bench(nodes: ToolResultNode[]) {
|
||||
snapshot: { nodes },
|
||||
session: {
|
||||
loadOlder: vi.fn<ISession['loadOlder']>(),
|
||||
loadAllHistory: vi.fn<ISession['loadAllHistory']>(),
|
||||
prompt: vi.fn<ISession['prompt']>(async () => ({ ok: true, value: { accepted: true } })),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -18,7 +18,7 @@ import { ConversationSession } from '../src/client/skeleton/ConversationSession.
|
||||
import { InputBar } from '../src/client/skeleton/InputBar.tsx'
|
||||
import type { InputBarProps } from '../src/client/skeleton/InputBar.tsx'
|
||||
import type {
|
||||
ComposerBarOwnerProps, ConversationSessionOwnerProps,
|
||||
ComposerBarOwnerProps,
|
||||
} from '../src/client/contract/slots.ts'
|
||||
|
||||
/** Machine-backed wiring over a sink spy. */
|
||||
@@ -88,10 +88,8 @@ function mount(
|
||||
slotCalls.push(key)
|
||||
if (key === 'conversation.hero.workspace') { pickerOwner = owner; return null }
|
||||
if (key === 'conversation.session') {
|
||||
const sessionOwner = owner as ConversationSessionOwnerProps
|
||||
return (
|
||||
<ConversationSession
|
||||
composer={sessionOwner.composer}
|
||||
sessionId={SID}
|
||||
SessionProvider={({ children }) => children(SID)}
|
||||
useSession={useSession}
|
||||
|
||||
Reference in New Issue
Block a user