Skip to content

Commit 1dea83c

Browse files
committed
Merge remote-tracking branch 'origin/develop' into feat/theme-skins-system
# Conflicts: # packages/web/src/app/providers.lifecycle.test.tsx # packages/web/src/app/providers.tsx # packages/web/src/features/terminal-panel/views/shared/xterm-host.tsx
2 parents 4211833 + 4e2535b commit 1dea83c

37 files changed

Lines changed: 3645 additions & 50 deletions
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
# Mobile Terminal Copy Mode — Design
2+
3+
Date: 2026-05-12
4+
Status: Draft
5+
Owner: spencer
6+
7+
## Problem
8+
9+
移动端 terminal 当前以触摸滚动和软键输入为主,无法像普通网页文本那样通过长按进入原生选区,因此用户不能方便地复制终端输出。
10+
11+
仓库现状决定了这不是一个“打开浏览器默认选区”就能解决的问题:
12+
13+
- 移动端 terminal 由 [`XtermHost`](../../../packages/web/src/features/terminal-panel/views/shared/xterm-host.tsx) 承载。
14+
- 该组件在移动端显式接管了 `touchstart/touchmove/touchend/touchcancel`,用于终端滚动与惯性滚动。
15+
- 当前的 terminal 交互目标是“可滚动、可输入”,不是“原生可选中文本”。
16+
17+
用户希望增加一个移动端可用的复制路径,优先满足“长按后选中当前看到的终端输出并复制”。
18+
19+
## Goals
20+
21+
- 为移动端 terminal 增加长按进入复制模式的能力。
22+
- 复制模式中允许使用浏览器原生文本选区,支持多行选中。
23+
- v1 仅覆盖当前可见 viewport 的终端内容。
24+
- 复制模式退出后恢复到原本的 live terminal 交互。
25+
- 不改变桌面端现有 copy-on-select 行为。
26+
27+
## Non-Goals
28+
29+
- 不在 live xterm 上直接实现移动端原生长按选区。
30+
- 不支持跨出当前 viewport 的历史 scrollback 选中。
31+
- 不实现自定义选区手柄、放大镜或自定义复制菜单。
32+
- 不新增服务端专用“移动端复制快照”接口。
33+
- 不改变现有移动端软键条的键位语义。
34+
35+
## User Decisions Captured
36+
37+
- 采用“长按进入 overlay 复制模式”而不是直接改 xterm 原生选区。
38+
- v1 的 overlay 内容来源于前端当前可见 DOM,而不是服务端快照。
39+
- v1 支持多行选中。
40+
- v1 范围只覆盖当前可见 viewport。
41+
42+
## Approaches Considered
43+
44+
### Option A: 直接使用当前前端可见 DOM 生成复制 overlay(推荐)
45+
46+
优点:
47+
48+
- 所见即所得,复制内容与用户此刻看到的终端一致。
49+
- 不依赖现有服务端 snapshot 格式转换。
50+
- 不需要新增协议或服务端接口。
51+
- 与公开可验证的同类 workaround 一致,风险更低。
52+
53+
缺点:
54+
55+
- v1 只能稳定覆盖当前 viewport。
56+
- 需要处理 xterm DOM 的空格、行宽、样式拷贝与定位。
57+
58+
### Option B: 复用服务端 `terminal.snapshot` 生成 overlay
59+
60+
优点:
61+
62+
- 理论上可以脱离前端渲染层,未来更容易扩展到更大范围。
63+
64+
缺点:
65+
66+
- 当前 `terminal.snapshot` 是 headless xterm 的序列化 ANSI/VT 数据,不是可直接选中的文本结构。
67+
- 服务端现有 `renderSnapshotToText()` 只适合摘要用途,会丢失可视布局信息。
68+
- 为 v1 引入服务端转换逻辑,复杂度不划算。
69+
70+
### Option C: 直接在 live xterm 上实现移动端原生选区
71+
72+
优点:
73+
74+
- 交互最接近桌面或原生文本组件。
75+
76+
缺点:
77+
78+
- 与 xterm.js 当前移动端支持现状冲突明显。
79+
- 会直接撞上现有 touch scroll、焦点、软键盘与渲染层限制。
80+
- 超出本次范围。
81+
82+
## Final Choice
83+
84+
采用 Option A。
85+
86+
v1 把复制能力定义为一个独立的“复制模式”:用户长按 terminal 输出区域后,前端读取当前可见 viewport 的 xterm DOM,生成一个位于 terminal 上层的可选中文本 overlay。用户在 overlay 上使用浏览器原生选区完成复制,退出后再回到 live terminal。
87+
88+
## Interaction Model
89+
90+
### 1. 默认状态
91+
92+
在未进入复制模式时,移动端 terminal 保持当前行为:
93+
94+
- 单指纵向拖动:滚动 terminal。
95+
- 点击 terminal:聚焦输入。
96+
- 软键条正常可用。
97+
- 不暴露原生文本选区。
98+
99+
### 2. 进入复制模式
100+
101+
用户在 terminal 输出区域单指长按约 450-500ms 后触发复制模式。
102+
103+
长按识别条件:
104+
105+
- 仅单指触摸有效。
106+
- 触点位移超过小阈值(建议 5-8px)则取消长按。
107+
- 明显纵向滚动手势优先判定为滚动,不进入复制模式。
108+
- 若触点位于当前输入行附近,则不进入复制模式,保留输入/粘贴优先级。
109+
110+
进入复制模式时:
111+
112+
- 触发轻微震动(如果浏览器支持 `navigator.vibrate`)。
113+
- 冻结一份“当前可见 viewport”的文本快照。
114+
- 在 terminal 上方显示全覆盖 overlay。
115+
- overlay 内文本可使用浏览器原生选区。
116+
- live terminal 继续存在于底层,但不响应触摸交互。
117+
118+
### 3. 复制模式中的交互
119+
120+
复制模式内的用户心智是“正在复制这一屏文本”,而不是“仍在操作 live terminal”。
121+
122+
具体行为:
123+
124+
- 用户可原生长按/拖动选择多行文本。
125+
- overlay 自身允许滚动,但滚动范围仅限当前快照容器,不会拉取更多历史 terminal 内容。
126+
- 底部软键条建议隐藏或禁用,避免误触发送输入。
127+
- terminal 输出即使后台继续到达,也不会实时更新 overlay。
128+
129+
### 4. 退出复制模式
130+
131+
主退出方式:
132+
133+
- 点击显式的“完成”按钮。
134+
135+
辅助退出方式:
136+
137+
- 当没有激活选区时,点击 overlay 空白区域退出。
138+
139+
v1 不依赖“复制成功自动退出”作为核心流程,因为浏览器复制菜单与系统选区的完成信号并不总是稳定可观察。若浏览器确实触发了 `copy` 事件,可作为增强项做延迟自动退出,但不是必需条件。
140+
141+
## Overlay Content Source
142+
143+
### Why Not Server Snapshot
144+
145+
当前服务端 `terminal.snapshot` 实现来自 headless xterm 的 `SerializeAddon.serialize()`,返回的是可重建终端状态的序列化数据,不是用于浏览器原生选区的文本结构。
146+
147+
参考:
148+
149+
- [`packages/server/src/terminal/terminal-snapshot-buffer.ts`](../../../packages/server/src/terminal/terminal-snapshot-buffer.ts)
150+
- [`packages/server/src/commands/terminal.ts`](../../../packages/server/src/commands/terminal.ts)
151+
152+
当前服务端把 snapshot 渲染成纯文本的能力也仅用于摘要:
153+
154+
- [`packages/server/src/terminal/snapshot-render.ts`](../../../packages/server/src/terminal/snapshot-render.ts)
155+
156+
该实现会 strip ANSI 并按换行切分,无法保留 overlay 所需的可视布局、空格占位和逐行宽度一致性,因此不适合作为 v1 的复制源。
157+
158+
### DOM-Based Snapshot
159+
160+
v1 以当前前端 xterm 可见 DOM 作为唯一数据源。
161+
162+
推荐来源:
163+
164+
- `terminal.element` 下当前可见的 `.xterm-rows`
165+
166+
生成步骤:
167+
168+
1. 读取当前 `.xterm-rows` 的可见内容。
169+
2. clone 当前行结构。
170+
3. 将原始 span 的 computed style 拷贝到 clone,避免脱离 `.xterm` 容器后颜色丢失。
171+
4. 将普通空格替换为 `NBSP`,保证原生选区时空白可见且可点中。
172+
5. 按当前 terminal `cols` 为每一行补齐尾部空白,避免短行选区断裂。
173+
6. 按当前 viewport 中 `.xterm-rows` 的实际偏移量,把 clone 后的内容对齐到 overlay。
174+
175+
结果是:overlay 展示的是“用户眼前这一屏的静态文本版”,不是一份重新解释的服务端文本。
176+
177+
## Viewport Scope
178+
179+
v1 的复制范围严格限定为“进入复制模式那一刻,terminal 当前可见 viewport 中的内容”。
180+
181+
这意味着:
182+
183+
- 支持多行拖选。
184+
- 支持从当前可见区域上半部分选到下半部分。
185+
- 不支持继续向上滚到更早的 scrollback 后扩展当前选区。
186+
187+
若用户想复制更早输出,操作方式是:
188+
189+
1. 先退出复制模式。
190+
2. 在 live terminal 中滚到目标位置。
191+
3. 再次长按进入复制模式。
192+
193+
## UI Surface
194+
195+
### Overlay Shell
196+
197+
overlay 覆盖在 `.xterm-host` 之上,建议包含:
198+
199+
- 一个顶部轻量工具条。
200+
- 一个主文本区。
201+
202+
工具条内容:
203+
204+
- 左侧标题:`复制模式`
205+
- 右侧主操作:`完成`
206+
207+
可选的辅助提示文案:
208+
209+
- `拖动选择文本`
210+
211+
### Visual Rules
212+
213+
- overlay 背景延续 terminal 深色底,但比 live terminal 稍亮或稍实,以明确“当前是冻结层”。
214+
- overlay 不需要复杂动画,最多做一个轻微淡入。
215+
- 文本区使用与 terminal 一致的字体、字号和行高,避免视觉跳变。
216+
217+
## Input, Scroll, And State Coordination
218+
219+
进入复制模式后:
220+
221+
- 终端输入事件不应再透传给 PTY。
222+
- 终端 touch scroll 逻辑暂停。
223+
- 软键条交互暂停或隐藏。
224+
225+
退出复制模式后:
226+
227+
- 恢复原有 touch scroll。
228+
- 恢复输入与软键条。
229+
- 丢弃 overlay 快照。
230+
- live terminal 继续显示最新内容;复制模式期间积累的输出在退出后自然可见。
231+
232+
本次不要求暂停服务端输出流,也不要求阻塞 terminal 底层渲染。复制模式只阻断用户交互层,不改变现有输出链路。
233+
234+
## Error Handling
235+
236+
若在进入复制模式时无法读取 `.xterm-rows` 或生成 overlay 内容失败:
237+
238+
- 不进入复制模式。
239+
- 保持现有 terminal 交互不变。
240+
- 给出轻量 toast:
241+
- 中文标题:`无法进入复制模式`
242+
- 中文正文:`请重试,或先滚动终端后再长按`
243+
- 英文标题:`Couldn't enter copy mode`
244+
- 英文正文:`Try again, or scroll the terminal and long press again`
245+
246+
复制模式本身不需要“复制成功 toast”。成功应保持静默。
247+
248+
## Testing
249+
250+
- `XtermHost` 长按识别:
251+
- 单指长按进入复制模式。
252+
- 位移超过阈值后取消长按。
253+
- 纵向滚动手势不进入复制模式。
254+
- 输入行附近长按不进入复制模式。
255+
- overlay 内容:
256+
- 基于当前可见 viewport 生成。
257+
- 支持多行文本。
258+
- 行尾空白补齐后可连续选中。
259+
- 样式拷贝后颜色与基础排版保持稳定。
260+
- 状态切换:
261+
- 进入复制模式后暂停软键条交互。
262+
- 退出后恢复 live terminal。
263+
- 复制模式期间新输出不会刷新 overlay。
264+
- 异常路径:
265+
- DOM 取样失败时显示 toast,且不破坏 terminal 可用性。
266+
267+
## Rollout
268+
269+
- 一次合入,无设置开关。
270+
- 范围仅限移动端 viewport。
271+
- 桌面端逻辑不受影响。
272+
273+
## Open Questions
274+
275+
无。

0 commit comments

Comments
 (0)