7.9 KiB
Terminal Handoff
本文档用于把 base-path-demo 当前浏览器终端实现的状态、已验证结论、未解决问题和下一步建议整理清楚,方便后续继续开发。
1. 当前目标
项目目标不是简单接一个现成 JS 终端,而是在 base-path-demo 中逐步沉淀一套 Rust 终端能力:
- 后端:
Axum + portable-pty - 前端:
Leptos + Rust/WASM - 长期方向:抽出可复用 Rust 终端组件库
当前已经完成:
- PTY 启动与 shell 会话
- WebSocket 双向通信
- Rust/WASM 终端渲染 MVP
- ANSI 样式分段渲染
- 宽高测量与 PTY resize
- 基础 scrollback/history 浏览能力
当前仍未稳定完成:
- 长历史滚动后的视口一致性
- 从 history 模式返回 live 模式时的可靠定位
- 超长输出下的 scrollback / DOM / parser 三者完全一致
2. 当前实现位置
后端
server/src/terminal/pty_session.rsserver/src/terminal/ws.rsserver/src/main.rs
前端
app/src/terminal/core.rsapp/src/terminal/component.rsapp/src/terminal/scrollback.rsapp/src/terminal/protocol.rsapp/src/pages/terminal.rsstyle/tailwind.css
3. 当前前端结构
当前前端已经从“组件里直接推导滚动”改成三层:
-
TerminalCore- 基于
vt100::Parser - 负责终端字节流解析
- 负责从 parser 生成
TerminalSnapshot - 暴露
snapshot_live()与snapshot_for_top_row()
- 基于
-
ScrollbackModel- 位于
app/src/terminal/scrollback.rs - 负责管理:
live/history状态top_rowScrollMetricsscrollTop <-> top_row映射- spacer 渲染计划
- 位于
-
TerminalPanel- 负责 DOM 事件和 WebSocket
- 把滚动、输入、resize 转换为 model 更新
- 再根据 model 请求
TerminalCore产出 snapshot
这是一次结构性重构,不再依赖前面几轮“scrollback_offset / scrollTop / spacer”互相修补的逻辑。
4. 已确认有效的部分
这些能力在当前实现里基本是成立的:
- WebSocket 通信链路稳定
- shell 启动和 PTY 读写稳定
- 普通命令行输入输出稳定
btm这种全屏 TUI 在 live 模式下效果已经明显优于最初版本- 宽度和高度测量相比初版更合理
- 终端样式、ANSI 颜色和 cursor 基本可用
5. 当前未解决问题
用户最新反馈:滚动相关问题仍然存在。
典型症状:
- 滚动到高处后,再滚回底部,视图仍可能错位。
- 认为已经回到底部,但看不到当前 prompt 或新输出。
- 某些情况下顶部会出现大块空白。
- history 模式和 live 模式切换仍然不完全符合真实终端体验。
6. 为什么当前结构仍可能不稳
虽然已经抽出了 ScrollbackModel,但当前方案本质上仍然依赖一个前提:
用 DOM 滚动条 + spacer 高度,去模拟“完整终端历史缓冲区”。
这条路的优势是实现快,但仍有天然风险:
6.1 vt100 只给出“当前视口”
vt100 当前更适合表达:
- 当前可见 screen
- 当前 scrollback offset
它不是为浏览器里的“无限历史虚拟滚动列表”直接设计的 UI 数据结构。
也就是说,浏览器里看到的“长历史列表”不是 parser 原生给出来的,而是我们通过 spacer + 视口切换模拟出来的。
6.2 DOM 滚动不是终端滚动
浏览器滚动条是像素系统,终端历史是行系统。
即使我们做了:
- 行高量化
- near-bottom 阈值
- top row model
- 程序滚动忽略回流
仍然可能在以下场景出现边界问题:
- resize 后 scrollHeight 改变
- 某些行因字符宽度或字体 fallback 产生高度/宽度偏差
- cursor / prompt / wrapped line 在 parser 里和 DOM 里对应关系不完全一致
6.3 spacer 模型仍然是“UI 侧估计”
现在的 spacer 高度来自:
scrollback_rowsviewport_rowstop_row
但这仍然是假设“每一逻辑行高度固定、所有历史行都可用同一高度表达”。
对于普通命令输出通常够用,但当用户期待系统终端级别的一致性时,仍可能暴露问题。
7. 建议的下一步方向
不建议继续围绕当前 spacer 方案做大量微调。
推荐方向 A:显式历史行缓冲
最推荐。
思路:
- 在前端维护一份显式的“历史行缓冲”。
- 每次收到 PTY 输出时,不只保留当前 snapshot,还要把可确认滚出屏幕的行纳入历史缓冲。
- 浏览器滚动条直接绑定:
history_rowsvisible_rows
- 当前视图渲染为:
- 历史区真实行
- 当前 screen 行
这样浏览器滚动就不再依赖 spacer 去“伪装总高度”,而是真正拥有一份历史数据模型。
更具体地说,应当抽成类似:
terminal-core
parser
screen
scrollback_store
viewport_state
terminal-leptos
renderer
dom scroll adapter
input adapter
建议引入一个新的模型,例如:
pub struct TerminalBufferModel {
pub history_rows: Vec<RenderedRow>,
pub screen_rows: Vec<RenderedRow>,
pub viewport_rows: usize,
pub mode: ViewMode,
}
其中:
history_rows是真实可滚动历史screen_rows是当前 parser screenViewMode表示 live/history
然后滚动行为只是在这份显式数据上切片,不再通过“scrollback offset + spacer”来推导。
推荐方向 B:虚拟列表化
如果历史很多,后续要避免一次性渲染全部历史行。
那就基于显式历史行缓冲再做:
- 虚拟滚动窗口
- 仅渲染当前可见行切片
- 上下填充高度由真实行数计算
这个方向适合在方向 A 稳定后再做。
不推荐继续投入的方向
除非只是临时验证,否则不建议继续在下面这些点上做大量时间投入:
- 单纯继续调
scrollTop -> topRow的换算公式 - 继续加更多“忽略下一次 scroll 事件”之类的回流保护
- 继续依赖 spacer 修正 live/history 错位
这些都只能减轻问题,不太可能从结构上彻底解决。
8. 一个更稳的实施顺序
建议下一位接手的人按这个顺序推进:
- 先保留当前 PTY / WS / 输入输出链路,不动后端。
- 在前端新增显式历史缓冲层,不急着删除现有
ScrollbackModel。 - 把当前渲染改成:
history_rows + screen_rows- 不再通过
snapshot.scrollback_offset去制造大块 spacer
- 先让滚动逻辑只在显式数据上成立。
- 确认:
- 滚到顶部没有空白
- 滚回底部一定能恢复 prompt
- history 模式继续输入一定回 live
- 再考虑是否保留
vt100的 scrollback offset 接口,还是让它只负责 screen,而历史完全交给自有 buffer model。
9. 当前代码是否值得保留
值得保留的部分:
portable-pty后端- Axum WebSocket 协议与会话层
TerminalCore的 ANSI 分段渲染能力- 宽高测量和 PTY resize
ScrollbackModel里关于 live/history 的概念划分
可以重做的部分:
- 终端历史滚动的底层表现方式
- spacer 方案本身
- DOM scroll 与 parser scrollback 的耦合方式
10. 建议的 handoff 结论
当前项目已经证明:
- Rust 后端 PTY 是可行的
- Rust/WASM 终端组件是可行的
btm和普通 shell 已经能跑
但要让它真正成为一个稳定的 Rust 终端组件库,下一步不应继续在“spacer + 当前视口推导”上做小修小补,而应转向:
显式历史缓冲模型 + 真实终端行数据结构 + 可选虚拟滚动
这会更接近真正终端组件库内部的设计,也更利于后续抽成 crate。
11. 当前验证命令
建议继续用下面这些命令回归:
pwdlscat <long-file>btmtop/htop(如果环境可用)- 连续回车制造长历史
重点检查:
- 滚到顶部是否有空白
- 滚回底部是否恢复 prompt
- history 模式输入后是否回到 live
- resize 后历史视图是否稳定