Description 现象
从聊天框打开会话面板时,有概率提示 terminal not found(终端未找到);关闭面板再重新打开,终端又能正常出现。
打开/关闭终端面板时,会话消息区的滚动位置会被重置,滚动条被推到顶部。
复现步骤
在会话中打开会话面板(默认 tab 为终端);
偶发出现 terminal not found;
关闭面板后重新打开,终端恢复;
另外:在已滚动到会话中间/底部时打开或关闭终端面板,消息区滚动位置会丢失并回到顶部附近。
问题一:terminal not found
调用链
打开会话面板时默认 tab 是 terminal,于是同时发生两件事:
ensureContext(contextKey, cwd, true) 异步执行 terminal.list;
TerminalPane 挂载后立刻对 store 里的 activeTerminalId 调用 terminal.reconnect。
后端 TerminalService.getContextSession() 在 sessions 中找不到该 id 时抛出 ErrorCode.terminal.notFound。
根因分析
终端 detach 后进入宽限期,默认 graceMs = 45s,超时后 cleanupSession() 会从 sessions 中删除;
后端/容器重启同样会清空所有 session;
但前端 zustand store 里可能仍保留旧 terminalId(未收到 closed 事件或页面未刷新),此时 terminal.reconnect 命中的是不存在的会话,直接报 terminal not found;
ensureContext() 的处理存在缺口:当服务器返回的终端列表为空、但本地仍有旧 id 时,只把旧 id 标记为 expired,不会自动创建新终端 (其自动创建条件为 terminals.length === 0 && previousIds.length === 0);
关闭再打开面板会重新挂载:此时本地 id 已被清空,条件成立,于是自动创建终端,表现为“关掉再开就好了”;
附带竞态:ensureContext() 的 list 请求是异步的,而 TerminalPane 挂载即 reconnect,reconnect 可能先于 list 完成,从而先撞上失效 id。
相关位置
web/src/components/chat/session-panel.tsx:ensureContext() 调用与终端面板挂载
web/src/components/terminal/terminal-pane.tsx:挂载时 reconnectTerminal()
web/src/stores/terminal-store.ts:ensureContext() / reconnectTerminal() / markTerminalExpired()
src/terminal/terminal.service.ts:getContextSession() 抛 terminal.not_found;startGraceTimer() 到期删除会话
修复方向
ensureContext():服务器列表为空但本地有旧 id 时,标记过期后仍按 autoCreate 创建新终端;
reconnectTerminal() 遇到 not_found 时自动刷新列表并创建新终端自愈,而不是停留在错误态;
SessionPanel 等待 ensureContext 完成(或 context hydrated)后再挂载 TerminalPane,消除并发竞态。
问题二:开关终端面板导致会话滚动被重置到顶部
根因分析
桌面端在 web/src/routes/thread-view.tsx 中使用二选一结构渲染:
面板打开:ResizablePanelGroup 内嵌 ChatTimeline
面板关闭:直接渲染 ChatTimeline
打开/关闭终端会在两个分支间切换,导致 ChatTimeline 被卸载并重新挂载;
TanStack Virtual 的滚动位置与测量状态保存在组件实例内部,卸载即丢失,重新挂载从 0 开始;
重新挂载时虚拟列表先按估算高度渲染,初始 scrollToIndex(last) 在测量完成之前执行,最终位置不准确,表现为滚动条被推到顶部附近;
移动端使用底部 Sheet,ChatTimeline 不卸载,因此不受影响。
修复方向
保持 ChatTimeline 单实例:面板开关只改变容器布局/尺寸,不切换渲染分支;
或在 store 中按 thread 记录并恢复滚动偏移,待测量完成后恢复;
或为虚拟列表增加“测量完成后重新贴底/恢复偏移”的逻辑,并把面板开关纳入触发条件。
环境
Image: ghcr.io/limlll/codex-webui:latest(release codex-0.153.2-2)
会话面板默认终端 tab;桌面端可稳定观察到滚动位置重置问题
Reactions are currently unavailable
You can’t perform that action at this time.
现象
terminal not found(终端未找到);关闭面板再重新打开,终端又能正常出现。复现步骤
terminal not found;问题一:terminal not found
调用链
terminal,于是同时发生两件事:ensureContext(contextKey, cwd, true)异步执行terminal.list;TerminalPane挂载后立刻对 store 里的activeTerminalId调用terminal.reconnect。TerminalService.getContextSession()在sessions中找不到该 id 时抛出ErrorCode.terminal.notFound。根因分析
graceMs = 45s,超时后cleanupSession()会从sessions中删除;terminalId(未收到 closed 事件或页面未刷新),此时terminal.reconnect命中的是不存在的会话,直接报terminal not found;ensureContext()的处理存在缺口:当服务器返回的终端列表为空、但本地仍有旧 id 时,只把旧 id 标记为 expired,不会自动创建新终端(其自动创建条件为terminals.length === 0 && previousIds.length === 0);ensureContext()的 list 请求是异步的,而TerminalPane挂载即 reconnect,reconnect 可能先于 list 完成,从而先撞上失效 id。相关位置
web/src/components/chat/session-panel.tsx:ensureContext()调用与终端面板挂载web/src/components/terminal/terminal-pane.tsx:挂载时reconnectTerminal()web/src/stores/terminal-store.ts:ensureContext()/reconnectTerminal()/markTerminalExpired()src/terminal/terminal.service.ts:getContextSession()抛terminal.not_found;startGraceTimer()到期删除会话修复方向
ensureContext():服务器列表为空但本地有旧 id 时,标记过期后仍按autoCreate创建新终端;reconnectTerminal()遇到not_found时自动刷新列表并创建新终端自愈,而不是停留在错误态;SessionPanel等待ensureContext完成(或 context hydrated)后再挂载TerminalPane,消除并发竞态。问题二:开关终端面板导致会话滚动被重置到顶部
根因分析
web/src/routes/thread-view.tsx中使用二选一结构渲染:ResizablePanelGroup内嵌ChatTimelineChatTimelineChatTimeline被卸载并重新挂载;scrollToIndex(last)在测量完成之前执行,最终位置不准确,表现为滚动条被推到顶部附近;ChatTimeline不卸载,因此不受影响。修复方向
ChatTimeline单实例:面板开关只改变容器布局/尺寸,不切换渲染分支;环境
ghcr.io/limlll/codex-webui:latest(releasecodex-0.153.2-2)