diff --git a/apps/web/tests/scaffold.ts b/apps/web/tests/scaffold.ts index ea39e2d29b..249ff619cb 100644 --- a/apps/web/tests/scaffold.ts +++ b/apps/web/tests/scaffold.ts @@ -496,7 +496,7 @@ export async function seedSession(scaffold: WebScaffold, fixtureText: string, id * volatility collapse to stable tokens. */ function normalizeAria(snapshot: string, workspaceCwd: string): string { - // The header breadcrumb renders the workspace's basename, not the full + // The session heading renders the workspace's basename, not the full // path, so both spellings must collapse to the token. const base = workspaceCwd.split('/').pop()! return snapshot diff --git a/apps/web/tests/snapshots/code-mode-round/ui.expected.md b/apps/web/tests/snapshots/code-mode-round/ui.expected.md index 183bd366a0..9b539fc432 100644 --- a/apps/web/tests/snapshots/code-mode-round/ui.expected.md +++ b/apps/web/tests/snapshots/code-mode-round/ui.expected.md @@ -1,6 +1,5 @@ - banner: - - navigation "Session hierarchy": - - 'button "Using ONE run_code program: run" [disabled]' + - 'heading "Using ONE run_code program: run" [level=1]' - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md b/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md index 636b1e6d28..6445b0fd8b 100644 --- a/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md +++ b/apps/web/tests/snapshots/cordis-tool-round/ui.expected.md @@ -1,6 +1,5 @@ - banner: - - navigation "Session hierarchy": - - button "Use only Cordis tools. First" [disabled] + - heading "Use only Cordis tools. First" [level=1] - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md b/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md index 3f7fa52b2e..32b0d9de50 100644 --- a/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md +++ b/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md @@ -1,6 +1,5 @@ - banner: - - navigation "Session hierarchy": - - button "Use the bash tool to" [disabled] + - heading "Use the bash tool to" [level=1] - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md b/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md index f58e5b77f2..35921f50ce 100644 --- a/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md +++ b/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md @@ -1,6 +1,5 @@ - banner: - - navigation "Session hierarchy": - - button "Reply with the single word" [disabled] + - heading "Reply with the single word" [level=1] - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/snapshots/live-interactions/cancel.expected.md b/apps/web/tests/snapshots/live-interactions/cancel.expected.md index 3333237798..2354cc2b66 100644 --- a/apps/web/tests/snapshots/live-interactions/cancel.expected.md +++ b/apps/web/tests/snapshots/live-interactions/cancel.expected.md @@ -1,6 +1,5 @@ - banner: - - navigation "Session hierarchy": - - button "Reply with a one-sentence description" [disabled] + - heading "Reply with a one-sentence description" [level=1] - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/snapshots/live-interactions/error-auth.expected.md b/apps/web/tests/snapshots/live-interactions/error-auth.expected.md index e6a93f2463..05efb17f29 100644 --- a/apps/web/tests/snapshots/live-interactions/error-auth.expected.md +++ b/apps/web/tests/snapshots/live-interactions/error-auth.expected.md @@ -1,6 +1,5 @@ - banner: - - navigation "Session hierarchy": - - button "Reply with a one-sentence description" [disabled] + - heading "Reply with a one-sentence description" [level=1] - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/snapshots/live-interactions/loading.expected.md b/apps/web/tests/snapshots/live-interactions/loading.expected.md index b442b4345a..ecabb944c2 100644 --- a/apps/web/tests/snapshots/live-interactions/loading.expected.md +++ b/apps/web/tests/snapshots/live-interactions/loading.expected.md @@ -1,6 +1,5 @@ - banner: - - navigation "Session hierarchy": - - button "Reply with a one-sentence description" [disabled] + - heading "Reply with a one-sentence description" [level=1] - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/snapshots/live-interactions/retry.expected.md b/apps/web/tests/snapshots/live-interactions/retry.expected.md index f34dddd7dd..4b57ca7a98 100644 --- a/apps/web/tests/snapshots/live-interactions/retry.expected.md +++ b/apps/web/tests/snapshots/live-interactions/retry.expected.md @@ -1,6 +1,5 @@ - banner: - - navigation "Session hierarchy": - - button "Reply with a one-sentence description" [disabled] + - heading "Reply with a one-sentence description" [level=1] - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/snapshots/message-actions/ui.expected.md b/apps/web/tests/snapshots/message-actions/ui.expected.md index 613e9a3605..257318c27f 100644 --- a/apps/web/tests/snapshots/message-actions/ui.expected.md +++ b/apps/web/tests/snapshots/message-actions/ui.expected.md @@ -1,6 +1,5 @@ - banner: - - navigation "Session hierarchy": - - button "Use the read tool twice" [disabled] + - heading "Use the read tool twice" [level=1] - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/snapshots/plan-review/approved.expected.md b/apps/web/tests/snapshots/plan-review/approved.expected.md index c971a6b2e4..c7f5c6d664 100644 --- a/apps/web/tests/snapshots/plan-review/approved.expected.md +++ b/apps/web/tests/snapshots/plan-review/approved.expected.md @@ -1,6 +1,5 @@ - banner: - - navigation "Session hierarchy": - - 'button "Plan a small change: add" [disabled]' + - 'heading "Plan a small change: add" [level=1]' - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/snapshots/question-composer/answered.expected.md b/apps/web/tests/snapshots/question-composer/answered.expected.md index fec84d06db..d916f82f4b 100644 --- a/apps/web/tests/snapshots/question-composer/answered.expected.md +++ b/apps/web/tests/snapshots/question-composer/answered.expected.md @@ -1,6 +1,5 @@ - banner: - - navigation "Session hierarchy": - - button "Use the ask_user_question tool to" [disabled] + - heading "Use the ask_user_question tool to" [level=1] - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/snapshots/queue-actions/collapsed.expected.md b/apps/web/tests/snapshots/queue-actions/collapsed.expected.md index 829f21a70f..052df599d7 100644 --- a/apps/web/tests/snapshots/queue-actions/collapsed.expected.md +++ b/apps/web/tests/snapshots/queue-actions/collapsed.expected.md @@ -1,6 +1,5 @@ - banner: - - navigation "Session hierarchy": - - button "Reply with a one-sentence description" [disabled] + - heading "Reply with a one-sentence description" [level=1] - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/snapshots/queue-actions/editing.expected.md b/apps/web/tests/snapshots/queue-actions/editing.expected.md index 169dde2c51..98b234ec69 100644 --- a/apps/web/tests/snapshots/queue-actions/editing.expected.md +++ b/apps/web/tests/snapshots/queue-actions/editing.expected.md @@ -1,6 +1,5 @@ - banner: - - navigation "Session hierarchy": - - button "Reply with a one-sentence description" [disabled] + - heading "Reply with a one-sentence description" [level=1] - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/snapshots/queue-actions/ui.expected.md b/apps/web/tests/snapshots/queue-actions/ui.expected.md index e2c91f7584..664aaa7041 100644 --- a/apps/web/tests/snapshots/queue-actions/ui.expected.md +++ b/apps/web/tests/snapshots/queue-actions/ui.expected.md @@ -1,6 +1,5 @@ - banner: - - navigation "Session hierarchy": - - button "Reply with a one-sentence description" [disabled] + - heading "Reply with a one-sentence description" [level=1] - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/snapshots/seeded-history/command-row.expected.md b/apps/web/tests/snapshots/seeded-history/command-row.expected.md index 0173726c38..ceeb45dd8a 100644 --- a/apps/web/tests/snapshots/seeded-history/command-row.expected.md +++ b/apps/web/tests/snapshots/seeded-history/command-row.expected.md @@ -1,6 +1,5 @@ - banner: - - navigation "Session hierarchy": - - button "Use the read tool twice" [disabled] + - heading "Use the read tool twice" [level=1] - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/snapshots/seeded-history/ui.expected.md b/apps/web/tests/snapshots/seeded-history/ui.expected.md index 0062b6cfab..3cee5a52de 100644 --- a/apps/web/tests/snapshots/seeded-history/ui.expected.md +++ b/apps/web/tests/snapshots/seeded-history/ui.expected.md @@ -1,6 +1,5 @@ - banner: - - navigation "Session hierarchy": - - button "Use the read tool twice" [disabled] + - heading "Use the read tool twice" [level=1] - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/snapshots/steering/mid-steer.expected.md b/apps/web/tests/snapshots/steering/mid-steer.expected.md index 27b40ef442..671e39a351 100644 --- a/apps/web/tests/snapshots/steering/mid-steer.expected.md +++ b/apps/web/tests/snapshots/steering/mid-steer.expected.md @@ -1,6 +1,5 @@ - banner: - - navigation "Session hierarchy": - - button "Use the ask_user_question tool to" [disabled] + - heading "Use the ask_user_question tool to" [level=1] - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/snapshots/steering/settled.expected.md b/apps/web/tests/snapshots/steering/settled.expected.md index 2ab2f5970d..ba04efc20f 100644 --- a/apps/web/tests/snapshots/steering/settled.expected.md +++ b/apps/web/tests/snapshots/steering/settled.expected.md @@ -1,6 +1,5 @@ - banner: - - navigation "Session hierarchy": - - button "Use the ask_user_question tool to" [disabled] + - heading "Use the ask_user_question tool to" [level=1] - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/apps/web/tests/snapshots/web-search-round/ui.expected.md b/apps/web/tests/snapshots/web-search-round/ui.expected.md index 37d53a0df6..b8eb6a1156 100644 --- a/apps/web/tests/snapshots/web-search-round/ui.expected.md +++ b/apps/web/tests/snapshots/web-search-round/ui.expected.md @@ -1,6 +1,5 @@ - banner: - - navigation "Session hierarchy": - - button "Use web_search to search exactly" [disabled] + - heading "Use web_search to search exactly" [level=1] - tablist: - tab "Chat" [selected] - tab "Trajectory" diff --git a/packages/client/ui-conversation/README.i18n.yaml b/packages/client/ui-conversation/README.i18n.yaml index f8ac552846..a000429d50 100644 --- a/packages/client/ui-conversation/README.i18n.yaml +++ b/packages/client/ui-conversation/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-conversation/README.md -README.md: 68115f8f9b9225d1f6b0e9cc93394d042c2befaa -README.zh.md: 65a3b334ff316a37dfb8506396eae8c17cccb40d +README.md: abbc20aba27fbd8af911d810ad7976d29cb8ad4f +README.zh.md: 600b06140c342bca984787c42eca66f601904a65 diff --git a/packages/client/ui-conversation/README.md b/packages/client/ui-conversation/README.md index 68115f8f9b..abbc20aba2 100644 --- a/packages/client/ui-conversation/README.md +++ b/packages/client/ui-conversation/README.md @@ -6,7 +6,7 @@ Conversation domain: skeleton (header/tabs/composer/empty state), chat view (gro Compaction renders as one collapsed row at the checkpoint's flow position without replacing the transcript above it. The disclosure renders the checkpoint's `compact/summary` provenance; when that event is outside the loaded window, the row remains visible but non-expandable. The framed checkpoint payload is model-facing and never renders. -The resident conversation shell survives no-session and session transitions. Without a current session it renders a disabled input bar; its root-scoped `conversation.hero.workspace` slot hosts the Workspace picker. Selecting a Workspace connects or reuses its Host-owned blank session and opens that session without replacing the shell. Blank sessions render the same composer body as active sessions, while the InputHub carries drafts across Workspace switches and mirrors them into the session store. In the active phase the session header occupies the top as ordinary column chrome; beneath it a scrollport (`data-conversation-scroll`) holds the flowing views and the sticky composer stack (stats dock + input docks + bar). Wheel over the textarea chains: the capped draft scrolls locally until its edge, then forwards to that host. +The resident conversation shell survives no-session and session transitions. Without a current session it renders a disabled input bar; its root-scoped `conversation.hero.workspace` slot hosts the Workspace picker. Selecting a Workspace connects or reuses its Host-owned blank session and opens that session without replacing the shell. Blank sessions render the same composer body as active sessions, while the InputHub carries drafts across Workspace switches and mirrors them into the session store. In the active phase the session header shows only the current session title and view tabs as ordinary column chrome; fork lineage remains session data and is not projected into the header. Beneath it a scrollport (`data-conversation-scroll`) holds the flowing views and the sticky composer stack (stats dock + input docks + bar). Wheel over the textarea chains: the capped draft scrolls locally until its edge, then forwards to that host. The view ring IS a slot: the conversation registration declares the `'conversation.view'` list slot (session scope) in its `children` table, ConversationRoot renders the active entry through its renderSlot share (`only: `), and view tabs project from the ring ledger's registration options (`id`/`order`/`label`). The chat view is this package's own ring entry; other plugins (ui-trajectory) contribute tabs through plain `ctx.slots.register` — the former package-local view registry (`registerView`/`ViewEntry`/`ConversationViewMap` and the chrome attachment table) is retired, with per-view chrome dissolved into the view components themselves. diff --git a/packages/client/ui-conversation/README.zh.md b/packages/client/ui-conversation/README.zh.md index 65a3b334ff..600b06140c 100644 --- a/packages/client/ui-conversation/README.zh.md +++ b/packages/client/ui-conversation/README.zh.md @@ -6,7 +6,7 @@ 压缩(compaction)在检查点自身的消息流位置渲染为一行折叠标记,不替换其上方的 transcript(文本记录)。展开内容来自检查点溯源的 `compact/summary`;该事件位于已加载窗口之外时,标记仍然可见但不可展开。面向模型的带框检查点载荷绝不渲染。 -常驻会话壳会跨无会话与会话状态切换而保留。没有当前会话时,它会渲染禁用输入栏;其根作用域的 `conversation.hero.workspace` slot 承载 Workspace 选择器。选择 Workspace 会连接或复用由 Host 拥有的空白会话,并在不替换会话壳的情况下打开该会话。空白会话与活跃会话渲染相同的输入区主体;InputHub 则在 Workspace 切换间携带草稿,并将草稿镜像到会话 store。活跃阶段会话标题栏以普通列 chrome 占据顶部;其下滚动容器(`data-conversation-scroll`)承载流动排版的各视图与 sticky 编辑器栈(统计 dock+输入区 dock+输入栏)。textarea 上的滚轮会链式处理:限高草稿先在本地滚动,到达边缘后再转交给该宿主。 +常驻会话壳会跨无会话与会话状态切换而保留。没有当前会话时,它会渲染禁用输入栏;其根作用域的 `conversation.hero.workspace` slot 承载 Workspace 选择器。选择 Workspace 会连接或复用由 Host 拥有的空白会话,并在不替换会话壳的情况下打开该会话。空白会话与活跃会话渲染相同的输入区主体;InputHub 则在 Workspace 切换间携带草稿,并将草稿镜像到会话 store。活跃阶段,会话标题栏作为普通列 chrome,仅显示当前会话标题和视图标签;fork 谱系仍保留为会话数据,不投影到标题栏。其下滚动容器(`data-conversation-scroll`)承载流动排版的各视图与 sticky 编辑器栈(统计 dock+输入区 dock+输入栏)。textarea 上的滚轮会链式处理:限高草稿先在本地滚动,到达边缘后再转交给该宿主。 视图环本身就是 slot:会话注册声明 `'conversation.view'` 列表 slot(Session scope),并将其列在 `children` 表中;ConversationRoot 通过 renderSlot share 渲染活跃配置项(`only: `);视图标签页从环账本的注册选项(`id`/`order`/`label`)投影而来。聊天视图是该包(package)自身的环配置项;其他插件(ui-trajectory)通过普通的 `ctx.slots.register` 贡献标签页。先前包内的视图注册表(`registerView`/`ViewEntry`/`ConversationViewMap` 及 chrome 附加表)已退役,逐视图 chrome 则被拆入视图组件自身。 diff --git a/packages/client/ui-conversation/src/client/apply.ts b/packages/client/ui-conversation/src/client/apply.ts index 489ad34e3f..c67431e409 100644 --- a/packages/client/ui-conversation/src/client/apply.ts +++ b/packages/client/ui-conversation/src/client/apply.ts @@ -165,7 +165,6 @@ 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' } }, store: chatStore, inject: (sessionId: SessionId, _actions: BoundActions): ConversationSessionInjected => ({ @@ -175,7 +174,6 @@ export function apply(ctx: Context): void { version: () => slots.getVersion('conversation.view'), }, bindDraftMirror: write => inputHub.shell(sessionId).bindMirror(write), - open: (id) => { sessions.open(id) }, }), }, ConversationSession) diff --git a/packages/client/ui-conversation/src/client/contract/slots.ts b/packages/client/ui-conversation/src/client/contract/slots.ts index b909b6e979..555a6b8e4f 100644 --- a/packages/client/ui-conversation/src/client/contract/slots.ts +++ b/packages/client/ui-conversation/src/client/contract/slots.ts @@ -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, SessionId, ToolCallBlock, WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client' +import type { CommandNode, ConversationSnapshot, ObservableSnapshot, PendingInteraction, PendingWait, 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' @@ -244,8 +244,6 @@ 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 } /** @@ -355,7 +353,6 @@ export type ConversationSessionSlotProps = & PropsRenderSlots<'conversation.view'> & PropsStore & ConversationSessionInjected - & PropsLocale<'conversation'> /** The pending approval carrier the owner dispatches into the composer chain. */ export type ApprovalWait = PendingWait<'approval'> diff --git a/packages/client/ui-conversation/src/client/locales.ts b/packages/client/ui-conversation/src/client/locales.ts index 76560ca9e4..1bda57660d 100644 --- a/packages/client/ui-conversation/src/client/locales.ts +++ b/packages/client/ui-conversation/src/client/locales.ts @@ -30,7 +30,6 @@ export const zh = { 'access.confirm.enable': '启用 Full access', 'hero.headline': '开始构建吧', 'hero.chooseWorkspace': '选择工作区', - 'session.hierarchy': '会话层级', 'details.title': '详情', 'details.close': '关闭详情', 'details.empty': '点击消息流中的工具行查看详情', @@ -130,7 +129,6 @@ 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', diff --git a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css index 38e9e03d3c..1f32d844a6 100644 --- a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css +++ b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css @@ -1,4 +1,4 @@ -/* Conversation column skeleton: header (breadcrumb row + tabs) over the view +/* Conversation column skeleton: header (session title + 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. */ @@ -23,59 +23,24 @@ display: none; } -.crumbRow { +.titleRow { display: flex; align-items: center; - justify-content: space-between; min-height: 32px; } -.crumbs { - display: flex; - align-items: center; - gap: 4px; +.sessionTitle { min-width: 0; + max-width: 100%; overflow: hidden; - 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; -} - -.crumb { - max-width: 220px; - overflow: hidden; + margin: 0; 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; + text-overflow: ellipsis; + white-space: nowrap; } /* figma Tab_Group 34:11441: 35px strip, gap 36, pad-left 8, tabs bottom-aligned. */ diff --git a/packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx b/packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx index 677f33f46a..6b696e9e9d 100644 --- a/packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/ConversationSession.tsx @@ -2,35 +2,21 @@ 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, open, wrapActiveBody, t, + renderSlot, views, bindDraftMirror, wrapActiveBody, }: 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 ancestry = useSessions(s => deriveAncestry(s, sessionId), shallowEqual) + const title = useSessions(s => s.byId[sessionId]?.displayTitle ?? sessionId) const composerPhase = useSession(s => s.composerPhase) const blank = useSession(s => s.blank) const inputState = useInput(s => s) @@ -69,26 +55,8 @@ export function ConversationSession({ > {!hideChrome && ( <> -
- +
+

{title}

{tabs.length > 1 && (
diff --git a/packages/client/ui-conversation/tests/apply-inject.spec.tsx b/packages/client/ui-conversation/tests/apply-inject.spec.tsx index 2b170048f0..cfbb0fdcbe 100644 --- a/packages/client/ui-conversation/tests/apply-inject.spec.tsx +++ b/packages/client/ui-conversation/tests/apply-inject.spec.tsx @@ -228,12 +228,9 @@ describe('conversation slot inject surface', () => { await b.runtime.dispose() }) - it('routes navigation and workspace switching through the runtime owners, carrying the draft', async () => { + it('routes workspace switching through the runtime owner, carrying the draft', async () => { const b = await bench() - const { injected } = b.conversationSurface(ROOT) const resident = b.residentSurface(ROOT) - injected.open(ROOT) - expect(b.runtime.sessions.calls).toContainEqual({ method: 'open', args: [ROOT] }) // Same-session connect (the picked workspace resolves to this session): // no draft movement, plain re-open. b.runtime.workspaces.stub('connectWorkspace', () => Promise.resolve(ROOT)) @@ -241,7 +238,7 @@ describe('conversation slot inject surface', () => { actions.setDraft('carry me') void resident.selectWorkspace('workspace-1' as never) await vi.waitFor(() => { - expect(b.runtime.sessions.calls.filter(c => c.method === 'open')).toHaveLength(2) + expect(b.runtime.sessions.calls.filter(c => c.method === 'open')).toHaveLength(1) }) expect(b.runtime.workspaces.calls).toContainEqual({ method: 'connectWorkspace', args: ['workspace-1'] }) expect(state.getSnapshot().draft).toBe('carry me') diff --git a/packages/client/ui-conversation/tests/assembly-surfaces.spec.tsx b/packages/client/ui-conversation/tests/assembly-surfaces.spec.tsx index 3ddaef873e..8e66dd7541 100644 --- a/packages/client/ui-conversation/tests/assembly-surfaces.spec.tsx +++ b/packages/client/ui-conversation/tests/assembly-surfaces.spec.tsx @@ -20,7 +20,7 @@ * suite only proves the assembled wiring. */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { cleanup, fireEvent, waitFor, within } from '@testing-library/react' +import { cleanup, fireEvent, waitFor } from '@testing-library/react' import { LocaleService } from '@deepseek-ai/dsh-client-locale/client' import type { ISession, SessionId, TodoItem, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client' import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots' @@ -256,16 +256,14 @@ describe('prompt rejection through the assembled composer', () => { }) describe('title projection across assembled surfaces', () => { - it('one summary update re-labels the breadcrumb and document.title consumers together', async () => { + it('one summary update re-labels the current-session heading', async () => { const runtime = await bench([]) const view = runtime.renderRoot() - // The strict session header breadcrumb reads useSessions ancestry. - const crumb = within(view.container.querySelector('[aria-label="会话层级"]') as HTMLElement) - expect(crumb.getByText('S')).toBeTruthy() + expect(view.getByRole('heading', { name: 'S', level: 1 })).toBeTruthy() await runtime.sessions.updateSummary(SID, { displayTitle: '修订标题', title: '修订标题' }) - await waitFor(() => { expect(crumb.getByText('修订标题')).toBeTruthy() }) - expect(crumb.queryByText('S')).toBeNull() + await waitFor(() => { expect(view.getByRole('heading', { name: '修订标题', level: 1 })).toBeTruthy() }) + expect(view.queryByRole('heading', { name: 'S', level: 1 })).toBeNull() await runtime.dispose() }) }) diff --git a/packages/client/ui-conversation/tests/skeleton.spec.tsx b/packages/client/ui-conversation/tests/skeleton.spec.tsx index 0470618ada..35afd759f1 100644 --- a/packages/client/ui-conversation/tests/skeleton.spec.tsx +++ b/packages/client/ui-conversation/tests/skeleton.spec.tsx @@ -111,7 +111,6 @@ 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 }) => { @@ -140,8 +139,6 @@ function mount( version: () => 1, }} bindDraftMirror={write => wiring.bindMirror(write)} - open={open} - t={t} {...owner} /> ) @@ -203,7 +200,7 @@ function mount( } const view = render() return { - view, chat, sink, open, retargetWorkspace, session, slotCalls, + view, chat, sink, retargetWorkspace, session, slotCalls, pickerOwner: () => pickerOwner, rerender: () => { view.rerender() }, } @@ -218,8 +215,8 @@ describe('ConversationRoot resident composer', () => { expect(b.chat.store.getSnapshot().draft).toBe('ordinary revised') fireEvent.keyDown(box, { key: 'Enter' }) expect(b.sink).toHaveBeenCalledWith('ordinary revised') - fireEvent.click(b.view.getByRole('button', { name: 'Root' })) - expect(b.open).toHaveBeenCalledWith(sid('root')) + expect(b.view.getByRole('heading', { name: 'Child', level: 1 })).toBeTruthy() + expect(b.view.queryByText('Root')).toBeNull() }) it('active phase: fixed header outside the scrollport; sticky composer seat inside it', () => { diff --git a/packages/client/ui-trajectory/tests/views.spec.tsx b/packages/client/ui-trajectory/tests/views.spec.tsx index 4459d0c0e8..39504fe66a 100644 --- a/packages/client/ui-trajectory/tests/views.spec.tsx +++ b/packages/client/ui-trajectory/tests/views.spec.tsx @@ -23,7 +23,6 @@ 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' @@ -36,11 +35,6 @@ 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)[key] ?? key afterEach(cleanup) // The chat store persists under its declared key; clear so one case's active // view cannot rehydrate into the next. @@ -219,7 +213,6 @@ function mount(slots: SlotsService, nodes: ConversationSnapshot['nodes'] = NODES return render( children(SID)} useSession={useSession} useSessions={emptySessions()} @@ -236,7 +229,6 @@ 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()} />, ) }