Files
base-path-demo/TERMINAL_HANDOFF.md
2026-06-08 16:34:13 +08:00

7.9 KiB
Raw Blame History

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.rs
  • server/src/terminal/ws.rs
  • server/src/main.rs

前端

  • app/src/terminal/core.rs
  • app/src/terminal/component.rs
  • app/src/terminal/scrollback.rs
  • app/src/terminal/protocol.rs
  • app/src/pages/terminal.rs
  • style/tailwind.css

3. 当前前端结构

当前前端已经从“组件里直接推导滚动”改成三层:

  1. TerminalCore

    • 基于 vt100::Parser
    • 负责终端字节流解析
    • 负责从 parser 生成 TerminalSnapshot
    • 暴露 snapshot_live()snapshot_for_top_row()
  2. ScrollbackModel

    • 位于 app/src/terminal/scrollback.rs
    • 负责管理:
      • live/history 状态
      • top_row
      • ScrollMetrics
      • scrollTop <-> top_row 映射
      • spacer 渲染计划
  3. TerminalPanel

    • 负责 DOM 事件和 WebSocket
    • 把滚动、输入、resize 转换为 model 更新
    • 再根据 model 请求 TerminalCore 产出 snapshot

这是一次结构性重构不再依赖前面几轮“scrollback_offset / scrollTop / spacer”互相修补的逻辑。

4. 已确认有效的部分

这些能力在当前实现里基本是成立的:

  • WebSocket 通信链路稳定
  • shell 启动和 PTY 读写稳定
  • 普通命令行输入输出稳定
  • btm 这种全屏 TUI 在 live 模式下效果已经明显优于最初版本
  • 宽度和高度测量相比初版更合理
  • 终端样式、ANSI 颜色和 cursor 基本可用

5. 当前未解决问题

用户最新反馈:滚动相关问题仍然存在。

典型症状:

  1. 滚动到高处后,再滚回底部,视图仍可能错位。
  2. 认为已经回到底部,但看不到当前 prompt 或新输出。
  3. 某些情况下顶部会出现大块空白。
  4. 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_rows
  • viewport_rows
  • top_row

但这仍然是假设“每一逻辑行高度固定、所有历史行都可用同一高度表达”。

对于普通命令输出通常够用,但当用户期待系统终端级别的一致性时,仍可能暴露问题。

7. 建议的下一步方向

不建议继续围绕当前 spacer 方案做大量微调。

推荐方向 A显式历史行缓冲

最推荐。

思路:

  1. 在前端维护一份显式的“历史行缓冲”。
  2. 每次收到 PTY 输出时,不只保留当前 snapshot还要把可确认滚出屏幕的行纳入历史缓冲。
  3. 浏览器滚动条直接绑定:
    • history_rows
    • visible_rows
  4. 当前视图渲染为:
    • 历史区真实行
    • 当前 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 screen
  • ViewMode 表示 live/history

然后滚动行为只是在这份显式数据上切片不再通过“scrollback offset + spacer”来推导。

推荐方向 B虚拟列表化

如果历史很多,后续要避免一次性渲染全部历史行。

那就基于显式历史行缓冲再做:

  • 虚拟滚动窗口
  • 仅渲染当前可见行切片
  • 上下填充高度由真实行数计算

这个方向适合在方向 A 稳定后再做。

不推荐继续投入的方向

除非只是临时验证,否则不建议继续在下面这些点上做大量时间投入:

  • 单纯继续调 scrollTop -> topRow 的换算公式
  • 继续加更多“忽略下一次 scroll 事件”之类的回流保护
  • 继续依赖 spacer 修正 live/history 错位

这些都只能减轻问题,不太可能从结构上彻底解决。

8. 一个更稳的实施顺序

建议下一位接手的人按这个顺序推进:

  1. 先保留当前 PTY / WS / 输入输出链路,不动后端。
  2. 在前端新增显式历史缓冲层,不急着删除现有 ScrollbackModel
  3. 把当前渲染改成:
    • history_rows + screen_rows
    • 不再通过 snapshot.scrollback_offset 去制造大块 spacer
  4. 先让滚动逻辑只在显式数据上成立。
  5. 确认:
    • 滚到顶部没有空白
    • 滚回底部一定能恢复 prompt
    • history 模式继续输入一定回 live
  6. 再考虑是否保留 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. 当前验证命令

建议继续用下面这些命令回归:

  • pwd
  • ls
  • cat <long-file>
  • btm
  • top / htop(如果环境可用)
  • 连续回车制造长历史

重点检查:

  • 滚到顶部是否有空白
  • 滚回底部是否恢复 prompt
  • history 模式输入后是否回到 live
  • resize 后历史视图是否稳定