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

282 lines
7.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 去“伪装总高度”,而是真正拥有一份历史数据模型。
更具体地说,应当抽成类似:
```text
terminal-core
parser
screen
scrollback_store
viewport_state
terminal-leptos
renderer
dom scroll adapter
input adapter
```
建议引入一个新的模型,例如:
```rust
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 后历史视图是否稳定