feat: revert crumbs navigation for subagents

This commit is contained in:
imccyu
2026-08-01 14:30:16 +08:00
committed by Tianyi Cui
parent 431fb4b035
commit 5d56019d22
28 changed files with 139 additions and 34 deletions

View File

@@ -1,5 +1,6 @@
- banner:
- 'heading "Using ONE run_code program: run" [level=1]'
- navigation "Session hierarchy":
- 'button "Using ONE run_code program: run" [disabled]'
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,5 +1,6 @@
- banner:
- heading "Use only Cordis tools. First" [level=1]
- navigation "Session hierarchy":
- button "Use only Cordis tools. First" [disabled]
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,5 +1,6 @@
- banner:
- heading "Use the bash tool to" [level=1]
- navigation "Session hierarchy":
- button "Use the bash tool to" [disabled]
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,5 +1,6 @@
- banner:
- heading "Reply with the single word" [level=1]
- navigation "Session hierarchy":
- button "Reply with the single word" [disabled]
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,5 +1,6 @@
- banner:
- heading "Reply with a one-sentence description" [level=1]
- navigation "Session hierarchy":
- button "Reply with a one-sentence description" [disabled]
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,5 +1,6 @@
- banner:
- heading "Reply with a one-sentence description" [level=1]
- navigation "Session hierarchy":
- button "Reply with a one-sentence description" [disabled]
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,5 +1,6 @@
- banner:
- heading "Reply with a one-sentence description" [level=1]
- navigation "Session hierarchy":
- button "Reply with a one-sentence description" [disabled]
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,5 +1,6 @@
- banner:
- heading "Reply with a one-sentence description" [level=1]
- navigation "Session hierarchy":
- button "Reply with a one-sentence description" [disabled]
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,5 +1,6 @@
- banner:
- heading "Use the read tool twice" [level=1]
- navigation "Session hierarchy":
- button "Use the read tool twice" [disabled]
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,5 +1,6 @@
- banner:
- 'heading "Plan a small change: add" [level=1]'
- navigation "Session hierarchy":
- 'button "Plan a small change: add" [disabled]'
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,5 +1,6 @@
- banner:
- heading "Use the ask_user_question tool to" [level=1]
- navigation "Session hierarchy":
- button "Use the ask_user_question tool to" [disabled]
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,5 +1,6 @@
- banner:
- heading "Reply with a one-sentence description" [level=1]
- navigation "Session hierarchy":
- button "Reply with a one-sentence description" [disabled]
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,5 +1,6 @@
- banner:
- heading "Reply with a one-sentence description" [level=1]
- navigation "Session hierarchy":
- button "Reply with a one-sentence description" [disabled]
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,5 +1,6 @@
- banner:
- heading "Reply with a one-sentence description" [level=1]
- navigation "Session hierarchy":
- button "Reply with a one-sentence description" [disabled]
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,5 +1,6 @@
- banner:
- heading "Reply with a one-sentence description" [level=1]
- navigation "Session hierarchy":
- button "Reply with a one-sentence description" [disabled]
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,5 +1,6 @@
- banner:
- heading "Use the read tool twice" [level=1]
- navigation "Session hierarchy":
- button "Use the read tool twice" [disabled]
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,5 +1,6 @@
- banner:
- heading "Use the read tool twice" [level=1]
- navigation "Session hierarchy":
- button "Use the read tool twice" [disabled]
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,5 +1,6 @@
- banner:
- heading "Use the ask_user_question tool to" [level=1]
- navigation "Session hierarchy":
- button "Use the ask_user_question tool to" [disabled]
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,5 +1,6 @@
- banner:
- heading "Use the ask_user_question tool to" [level=1]
- navigation "Session hierarchy":
- button "Use the ask_user_question tool to" [disabled]
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -1,5 +1,8 @@
- banner:
- heading "event-sourcing researcher" [level=1]
- navigation "Session hierarchy":
- button "Ask a research subagent to"
- text: /
- button "event-sourcing researcher" [disabled]
- button "1 个子代理":
- text: 1 个子代理
- img

View File

@@ -1,5 +1,6 @@
- banner:
- heading "Use web_search to search exactly" [level=1]
- navigation "Session hierarchy":
- button "Use web_search to search exactly" [disabled]
- tablist:
- tab "Chat" [selected]
- tab "Trajectory"

View File

@@ -165,6 +165,7 @@ export function apply(ctx: Context): void {
// the resident parent keeps Hero and composer layout identity stable.
slots.register({
name: 'conversation.session',
locale: NS,
children: {
'conversation.view': { kind: 'list', scope: 'session' },
'conversation.session.header.actions': { kind: 'list', scope: 'session' },
@@ -177,6 +178,7 @@ export function apply(ctx: Context): void {
version: () => slots.getVersion('conversation.view'),
},
bindDraftMirror: write => inputHub.shell(sessionId).bindMirror(write),
open: (id) => { sessions.open(id) },
}),
}, ConversationSession)

View File

@@ -3,7 +3,7 @@ import type { ReactNode, RefObject } from 'react'
import type {
InjectFace, MaybeSnapshotSelectorHook, PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore, SnapshotSelectorHook,
} from '@deepseek-ai/dsh-client-ui-slots'
import type { CommandNode, ConversationSnapshot, ObservableSnapshot, PendingInteraction, PendingWait, ToolCallBlock, WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client'
import type { CommandNode, ConversationSnapshot, ObservableSnapshot, PendingInteraction, PendingWait, SessionId, ToolCallBlock, WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client'
import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
import type { ComposerKeyboard, EditSelection, InputActions, InputNotice, InputState } from '../input/contract.ts'
import type { createChatStore } from '../stores.ts'
@@ -249,6 +249,8 @@ export interface ConversationSessionInjected {
}
/** Bind the input machine's draft persistence mirror to the session store. */
bindDraftMirror: (write: (text: string) => void) => () => void
/** Select a real Session through the runtime navigation owner. */
open: (sessionId: SessionId) => void
}
/**
@@ -360,6 +362,7 @@ export type ConversationSessionSlotProps =
& PropsRenderSlots<'conversation.view' | 'conversation.session.header.actions'>
& PropsStore<ChatStore>
& ConversationSessionInjected
& PropsLocale<'conversation'>
/** The pending approval carrier the owner dispatches into the composer chain. */
export type ApprovalWait = PendingWait<'approval'>

View File

@@ -30,6 +30,7 @@ export const zh = {
'access.confirm.enable': '启用 Full access',
'hero.headline': '开始构建吧',
'hero.chooseWorkspace': '选择工作区',
'session.hierarchy': '会话层级',
'details.title': '详情',
'details.close': '关闭详情',
'details.empty': '点击消息流中的工具行查看详情',
@@ -129,6 +130,7 @@ export const en = {
'access.confirm.enable': 'Enable Full access',
'hero.headline': 'Let\'s start building',
'hero.chooseWorkspace': 'Choose workspace',
'session.hierarchy': 'Session hierarchy',
'details.title': 'Details',
'details.close': 'Close details',
'details.empty': 'Click a tool row in the message flow to view its details',

View File

@@ -1,4 +1,4 @@
/* Conversation column skeleton: header (session title + tabs) over the view
/* Conversation column skeleton: header (breadcrumb row only for subagents not fork + tabs) over the view
area, composer InputBar at the bottom. Column width/squeeze is layout's;
this fills its cell. Figma: Header 39:27730 (83px two-row), tabs 13px with
a 3px active bar. */
@@ -26,23 +26,55 @@
.titleRow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
min-height: 32px;
}
.sessionTitle {
.crumbs {
display: flex;
align-items: center;
gap: 4px;
min-width: 0;
max-width: 100%;
overflow: hidden;
margin: 0;
padding: 4px 8px;
white-space: nowrap;
}
.crumbSeg {
display: inline-flex;
align-items: center;
gap: 4px;
min-width: 0;
}
.crumbSep {
/* figma: "/" separators are 14px caption gray (75:7903), one tint lighter than crumb text. */
color: var(--dsw-alias-label-caption);
font-size: 14px;
line-height: 20px;
font-weight: 500;
color: var(--dsw-alias-label-primary);
}
.crumb {
max-width: 220px;
overflow: hidden;
padding: 4px 8px;
border: none;
border-radius: 12px;
background: transparent;
font-size: 14px;
line-height: 20px;
color: var(--dsw-alias-label-tertiary);
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
}
.crumb:hover:not(:disabled) {
background: var(--dsw-alias-interactive-bg-hover);
}
.crumbCurrent {
font-weight: 500;
color: var(--dsw-alias-label-primary);
cursor: default;
}
.headerActions {

View File

@@ -2,21 +2,35 @@
import { useEffect, useSyncExternalStore, type ReactNode } 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'
import type { ConversationSessionSlotProps } from '../contract/slots.ts'
import css from './ConversationRoot.module.css'
/** Full props composed from the strict session slot contract. */
export type ConversationSessionProps = ConversationSessionSlotProps
function deriveAncestry(list: SessionListState, id: SessionId): readonly SessionSummary[] {
const chain: SessionSummary[] = []
let cursor: SessionId | undefined = id
while (cursor !== undefined) {
const summary: SessionSummary | undefined = list.byId[cursor]
if (summary === undefined || chain.includes(summary)) break
chain.unshift(summary)
cursor = summary.parentId
}
return chain
}
export function ConversationSession({
sessionId, useSession, useSessions, useInput, inputActions, useStore, actions,
renderSlot, views, bindDraftMirror, wrapActiveBody,
renderSlot, views, bindDraftMirror, open, wrapActiveBody, t
}: ConversationSessionProps) {
useSyncExternalStore(views.subscribe, views.version)
const tabs = views.list()
const activeId = useStore(s => s.view) ?? 'chat'
const active = tabs.find(view => view.id === activeId) ?? tabs[0]
const title = useSessions(s => s.byId[sessionId]?.displayTitle ?? sessionId)
const ancestry = useSessions(s => deriveAncestry(s, sessionId), shallowEqual)
const composerPhase = useSession(s => s.composerPhase)
const blank = useSession(s => s.blank)
const inputState = useInput(s => s)
@@ -56,7 +70,25 @@ export function ConversationSession({
{!hideChrome && (
<>
<div className={css.titleRow}>
<h1 className={css.sessionTitle}>{title}</h1>
<nav className={css.crumbs} aria-label={t('session.hierarchy')}>
{ancestry.map((summary, index) => {
const last = index === ancestry.length - 1
return (
<span key={summary.id} className={css.crumbSeg}>
{index > 0 && <span className={css.crumbSep}>/</span>}
<button
type="button"
className={clsx(css.crumb, last && css.crumbCurrent)}
disabled={last}
onClick={() => { open(summary.id) }}
>
{summary.displayTitle}
</button>
</span>
)
})}
{ancestry.length === 0 && <span className={css.crumbCurrent}>{sessionId}</span>}
</nav>
<div className={css.headerActions}>
{renderSlot('conversation.session.header.actions', {})}
</div>

View File

@@ -111,6 +111,7 @@ function mount(
const useInput = bindSnapshotSelector(wiring.state)
const inputActions = wiring.actions
const stop = vi.fn()
const open = vi.fn()
const slotCalls: string[] = []
let pickerOwner: unknown
const renderSlot = ((key: string, owner: object, opts?: { only?: string }) => {
@@ -139,6 +140,8 @@ function mount(
version: () => 1,
}}
bindDraftMirror={write => wiring.bindMirror(write)}
open={open}
t={t}
{...owner}
/>
)

View File

@@ -23,6 +23,7 @@ import type {
import type { ConvViewProps, ViewTab } from '@deepseek-ai/dsh-client-ui-conversation/client'
import { ConversationSession, type ConversationSessionProps } from '@deepseek-ai/dsh-client-ui-conversation/src/client/skeleton/ConversationSession.tsx'
import { createChatStore } from '@deepseek-ai/dsh-client-ui-conversation/src/client/stores.ts'
import { zh as conversationZh } from '@deepseek-ai/dsh-client-ui-conversation/src/client/locales.ts'
import { apply, inject } from '@deepseek-ai/dsh-client-ui-trajectory/client'
import { apply as nodeApply } from '@deepseek-ai/dsh-client-ui-trajectory'
import type { TrajectoryTurnModel } from '../src/client/layout.ts'
@@ -35,6 +36,11 @@ import { deriveTrajectoryTimeline } from '../src/client/timeline.ts'
const SID = 's1' as SessionId
// Stub of the conversation package's standard locale seat (this spec mounts
// its ConversationSession chrome); answers from the zh dictionary and falls
// back to the key like the real chain.
const tConversation: ConversationSessionProps['t'] =
key => (conversationZh as Record<string, string>)[key] ?? key
afterEach(cleanup)
// The chat store persists under its declared key; clear so one case's active
// view cannot rehydrate into the next.
@@ -213,6 +219,7 @@ function mount(slots: SlotsService, nodes: ConversationSnapshot['nodes'] = NODES
return render(
<ConversationSession
sessionId={SID}
t={tConversation}
SessionProvider={({ children }) => children(SID)}
useSession={useSession}
useSessions={emptySessions()}
@@ -229,6 +236,7 @@ function mount(slots: SlotsService, nodes: ConversationSnapshot['nodes'] = NODES
useInput={bindSnapshotSelector(createSnapshotStore({ draft: '', draftRev: 0, phase: 'plain', queue: [] })) as never}
inputActions={{ setDraft: vi.fn(), submit: vi.fn() }}
bindDraftMirror={() => () => {}}
open={vi.fn()}
/>,
)
}