Issue:GAL 视窗对话框只显示「(编写代码中……)」,正文永远不渲染
标题
GAL 视窗对话框只显示「(编写代码中……)」——nodes/partial 从 useSession 读取,应为 useChat
环境
- gal-view
0.3.3,commit d002fe0(github:Ayase34/gal-view#main)
- 宿主:npm 安装的
@deepseek-ai/dsh,Web GUI(dsh web,http://127.0.0.1:3080)
- 平台:Windows,Node v24.16.0
- 安装方式:
dsh plugin --profile web add github:Ayase34/gal-view#main
复现
- 装好插件、重启 web,打开会话页的「GAL视窗」标签。
- 在视窗内的输入框发一句话,等模型跑完。
现象
对话框从头到尾只显示一句状态行「(编写代码中……)」,正文始终为空:
- 用户说的话不显示;
- 模型的回复定稿后也不显示(不是"流式期间不渲染"那么简单,是彻底不渲染);
- 历史面板(透明按钮「历史」)同样永远显示"还没有对话记录";
- 「思考中」也从不出现,无论模型是在思考还是在调工具,状态恒为「编写代码中」。
预期:按 README —— 对话正文渲染进「台词」元素,打字机逐字显示,超出容量可点击翻页。
根因
.dsh-plugin/client/GalView.jsx(0.3.3 约 L157–L169)从会话 hook 里取 chat 投影的字段:
export function GalView({ useSession, inputActions, useScene, useHistory, useAssets, useFonts, useStore, actions, api }) {
...
const nodes = useSession(s => s.nodes) // ← 恒为 undefined
const partial = useSession(s => s.partial) // ← 恒为 undefined
const running = useSession(s => s.running) // ← 这个是对的
const blank = useSession(s => s.blank)
const runningCalls = useSession(s => s.runningCalls) // ← 恒为 undefined
const pending = useSession(s => s.pending) // ← 恒为 undefined
const promptError = useSession(s => s.promptError)
但 conversation.view 槽位的 useSession 只暴露控制器快照 SessionSnapshot,字段集是固定且公开的:
export interface SessionSnapshot {
readonly sessionId: SessionId
readonly queue: readonly QueuedMessage[]
readonly pendingSubmissions: readonly PendingSubmission[]
readonly running: boolean
readonly subagent: { ... } | null
readonly removed: boolean
readonly openState: OpenState
readonly openError: RemoteFailure | null
readonly hasMore: boolean
readonly loadingOlder: boolean
readonly promptError: PromptError | null
readonly blank: boolean
readonly lastAgentError: string | null
readonly promptAttempted: boolean
readonly awaitingFirstTurn: boolean
}
没有 nodes / partial / runningCalls。
已定稿的对话节点与流式 partial 属于 chat 投影,通过同槽位的另一个标准 hook useChat 提供,且它的 legacy 切片正好就是插件想要的形状:
// dsh-client-ui-chat:LegacySliceBuilder.snapshot()
{ nodes, turnTimings, turnEnds, partial, runningCalls }
两条链路可以对照证实:
dsh-client-ui-session 的 BUILTIN_SOURCE:hooks: { session: binding.session } → 这就是 useSession
dsh-client-ui-chat 的 ctx.uiSession.provide({ hooks: ['chat'], resolve: binding => ({ hooks: { chat: chatSource(binding) } }) }) → 这就是 useChat
两者的标准 props 名称与来源(dsh-client-ui-renderer 的 standardKit / standardProps):
conversation.view standardProps:
... useChat: UseChat, useConversation: UseConversation, useInput, inputActions,
useSession: SessionSnapshotSelector, sessionId, useProjection, useTrajectory
顺带一提,官方自带的 ChatView 正是这么用的:
function ChatView({ useSession, useChat, ... }) {
const order = useChat((s) => s.order)
const running = useSession((s) => s.running) // 控制器字段走 useSession
因果链(为什么恰好卡在「编写代码中」)
nodes === undefined → nodesToLines(undefined) → [] → lines 恒为空
lastLine === null、pendingPlayer === null
showStatusPage = running && (liveText !== '' || statusHold || pendingPlayer === null) → 只要 running 就恒为真
currentLine 被换成 { key: 'live', kind: 'assistant', text: '' },正文为空
partial === undefined → deriveStatus 里 hasText 为假、partialStatus(undefined) 返回 null → 兜底 '编写代码中'
- 于是整轮对话期间,对话框 = 空正文 + 一句「(编写代码中…)」,历史面板恒为空
如果 partial 能读到,工具调用阶段仍会显示「编写代码中」(这是设计意图),但正文会正常渲染 —— 所以问题不是文案,而是数据源接错。
为什么现有测试没抓到
scripts/smoke.mjs 的夹具是照着假设造宿主,而不是照着宿主契约:
const sessionState = {
current: {
sessionId: 'session-1',
nodes: [ ... ], // ← 把 chat 投影塞进了 session
partial: null,
running: false,
blank: false,
},
}
...
useSession: bindHook(sessionSource),
useChat 从头到尾没传,组件里也不读。测试和实现共享同一个错误假设,所以 100% 绿。
建议修复
从 useChat 取投影数据,useSession 只留控制器字段:
export function GalView({ useSession, useChat, inputActions, useScene, useHistory, useAssets, useFonts, useStore, actions, api }) {
...
// 会话节点与流式 partial 属于 chat 投影(标准 hook useChat)——
// useSession 只提供 running/blank/promptError 等控制器字段。
const chatFallback = useSession(s => s)
const chat = useChat !== null && useChat !== undefined
? useChat(s => s.legacy ?? s)
: chatFallback // 兼容把两者合一的旧夹具
const nodes = chat.nodes
const partial = chat.partial
const runningCalls = chat.runningCalls
const running = useSession(s => s.running)
const blank = useSession(s => s.blank)
const promptError = useSession(s => s.promptError)
const pending = useSession(s => s.pending) // 注意:见下
pending(待批准/待回答)也不在 SessionSnapshot 上,它走的是另一个标准 hook useSessionPendingInteraction。如果 deriveStatus 的「等待回应」要生效,这里需要一并改成:
const pendingInteraction = useSessionPendingInteraction(s => s)
const pending = pendingInteraction === undefined || pendingInteraction === null ? [] : [pendingInteraction]
(我按上面这版打了补丁,本地已验证正文能正常渲染;pending 那处我没有实测,仅从契约推断。)
验证方式(无需 Playwright)
宿主契约可以直接对照宿主产物核实,不依赖 gal-view 自己的夹具:
# useSession 的真实字段集
grep -r "interface SessionSnapshot" node_modules/@deepseek-ai/dsh-client-ui-chat/lib/client.js
# useChat 的 legacy 切片
grep -nE "nodes: this\.finalized|partial: this\.partial|runningCalls: this\.runningCalls" \
node_modules/@deepseek-ai/dsh-client-ui-chat/lib/client.js
# conversation.view 是否声明了 useChat
grep -n '"useChat: UseChat"' node_modules/@deepseek-ai/dsh-cordis-client-runner/lib/client.js
另外我在本地用真实构建产物做了一次渲染核对(假 React + 假宿主,把节点放进 chat 投影):
| 产物 |
打字机目标 |
| 修复前 |
[""] —— 对话框无正文 |
| 修复后 |
["<chat 投影里的定稿文本>"] |
建议同时修测试
scripts/smoke.mjs 的夹具应拆成两个源,让 useChat 成为必需项,否则这个 bug 回归时测试依然全绿:
// session 源只有控制器字段
sessionState.current = { sessionId, running, blank, promptError }
// chat 源承载投影
chatState.current = { nodes, partial, runningCalls }
...
useSession: bindHook(sessionSource),
useChat: bindHook(chatSource),
并补一条断言:非空 nodes 时对话框正文必须出现该文本、且不得出现「(编写代码中…)」。
Issue:GAL 视窗对话框只显示「(编写代码中……)」,正文永远不渲染
标题
GAL 视窗对话框只显示「(编写代码中……)」——
nodes/partial从useSession读取,应为useChat环境
0.3.3,commitd002fe0(github:Ayase34/gal-view#main)@deepseek-ai/dsh,Web GUI(dsh web,http://127.0.0.1:3080)dsh plugin --profile web add github:Ayase34/gal-view#main复现
现象
对话框从头到尾只显示一句状态行「(编写代码中……)」,正文始终为空:
预期:按 README —— 对话正文渲染进「台词」元素,打字机逐字显示,超出容量可点击翻页。
根因
.dsh-plugin/client/GalView.jsx(0.3.3 约 L157–L169)从会话 hook 里取 chat 投影的字段:但
conversation.view槽位的useSession只暴露控制器快照SessionSnapshot,字段集是固定且公开的:没有
nodes/partial/runningCalls。已定稿的对话节点与流式 partial 属于 chat 投影,通过同槽位的另一个标准 hook
useChat提供,且它的legacy切片正好就是插件想要的形状:两条链路可以对照证实:
dsh-client-ui-session的BUILTIN_SOURCE:hooks: { session: binding.session }→ 这就是useSessiondsh-client-ui-chat的ctx.uiSession.provide({ hooks: ['chat'], resolve: binding => ({ hooks: { chat: chatSource(binding) } }) })→ 这就是useChat两者的标准 props 名称与来源(
dsh-client-ui-renderer的standardKit/standardProps):顺带一提,官方自带的 ChatView 正是这么用的:
因果链(为什么恰好卡在「编写代码中」)
nodes === undefined→nodesToLines(undefined)→[]→lines恒为空lastLine === null、pendingPlayer === nullshowStatusPage = running && (liveText !== '' || statusHold || pendingPlayer === null)→ 只要 running 就恒为真currentLine被换成{ key: 'live', kind: 'assistant', text: '' },正文为空partial === undefined→deriveStatus里hasText为假、partialStatus(undefined)返回null→ 兜底'编写代码中'如果
partial能读到,工具调用阶段仍会显示「编写代码中」(这是设计意图),但正文会正常渲染 —— 所以问题不是文案,而是数据源接错。为什么现有测试没抓到
scripts/smoke.mjs的夹具是照着假设造宿主,而不是照着宿主契约:useChat从头到尾没传,组件里也不读。测试和实现共享同一个错误假设,所以 100% 绿。建议修复
从
useChat取投影数据,useSession只留控制器字段:pending(待批准/待回答)也不在SessionSnapshot上,它走的是另一个标准 hookuseSessionPendingInteraction。如果deriveStatus的「等待回应」要生效,这里需要一并改成:(我按上面这版打了补丁,本地已验证正文能正常渲染;
pending那处我没有实测,仅从契约推断。)验证方式(无需 Playwright)
宿主契约可以直接对照宿主产物核实,不依赖 gal-view 自己的夹具:
另外我在本地用真实构建产物做了一次渲染核对(假 React + 假宿主,把节点放进 chat 投影):
[""]—— 对话框无正文["<chat 投影里的定稿文本>"]建议同时修测试
scripts/smoke.mjs的夹具应拆成两个源,让useChat成为必需项,否则这个 bug 回归时测试依然全绿:并补一条断言:非空
nodes时对话框正文必须出现该文本、且不得出现「(编写代码中…)」。