Skip to main content Skills Marketplace Descubra e explore skills de IA criadas pela comunidade.
Instalar com Codex ou Claude Copie este prompt, cole no Codex, Claude ou outro assistente e deixe que ele revise a página da skill e instale para você.
Copiar promptMostrar detalhes do prompt Um comando direto ignora o prompt de revisão. Verifique a origem antes de executá-lo.
npx skills add https://github.com/Jasonzhangf/zterm --skill terminal-buffer-truthO comando permanece em uma só linha. Role horizontalmente para revisá-lo antes de copiar.
Prefere uma cópia local? Baixe os arquivos disponíveis atualmente no SkillsMP.
Baixar Zip Baixando... name terminal-buffer-truth description terminal buffer / render / daemon mirror 真源与门禁
terminal-buffer-truth
Performance repair boundary
resource.renderer_window may hand an explicitly immutable projected snapshot to resource.client_sparse_buffer's render-store projection edge only after renderer-side equality/reuse checks complete. This removes duplicate full-window comparison/copy work; it does not change wire payload, buffer truth, visible-range ownership, or daemon behavior.
Performance trace storage is a bounded debug side channel. Eviction must remain metadata-only and O(1) per record; trace optimization must never trim terminal business payload or alter runtime truth.
适用场景
terminal buffer / render / scroll / input 延迟问题
出现“初次连接慢、输入不刷新、reading 拉不动、回到底部不 follow、带宽异常”
任何想在 server / buffer manager / renderer 之间加补丁、fallback、第二语义的时候
开发前置门禁:先架构,后代码
任何 terminal / session / daemon / buffer / renderer 相关开发、修复、重构,必须先完成架构映射,再读代码和修改代码。
固定顺序:
先读 android/docs/architecture.md
再读 android/docs/audits/2026-07-02-architecture-boundary-remediation.md
同时读 android/docs/decisions/2026-09-05-runtime-memory-truth.md 区分当前实现与目标;参数/初始化/路由目标不从旧经验段落重建。
再读 android/docs/resource-registry.json 与 android/docs/resource-map.md,确认 source/target resource、直接/间接关系、via_resources、禁止直连关系
再读本 skill 与相关 decision / feature registry / function map / mainline call map
再读代码定位 owner 与实现点
写出本次方法如何对应架构后,才允许修改代码
修改前必须明确:
本次问题属于哪个功能块:session lifecycle / daemon truth / client buffer-render / UI projection / persistence truth
本次问题涉及哪些 resource id,关系是 direct / via / observer / binding pending 哪一类
唯一 owner 文件或模块是谁
当前越界处理方式是 物理移除 / 分离下沉 / 显式兼容保留 哪一种
哪些路径允许修改,哪些路径禁止修改
正向测试和反向测试分别锁住什么风险
若涉及 daemon / tmux / mirror / transport 主链,且本机可启动真实 daemon 与 tmux,必须跑真实闭环验证;只跑单测、typecheck、静态 gate 不得宣称完成。默认命令:pnpm --dir android run daemon:mirror:close-loop。
若仓库内 Mac client 可用,还必须跑 Mac 客户端核心连接 gate,证明本地 client transport/runtime 能连;daemon-only probe 不能替代。默认最小命令:pnpm --dir mac test -- --reporter dot 与 pnpm --dir mac run type-check,其中必须覆盖 bridge-transport、local-tmux-transport、terminal-runtime、workbench active target。
禁止事项:
禁止先 grep 到命中点就直接 patch
禁止在未声明的 resource relation 上实现 shortcut;缺资源关系先补 registry/map/gate
禁止在非 owner 层补偿 owner 层问题
禁止用 fallback / 默认值 / catch 后继续成功 来掩盖真源缺失
禁止 UI / App / daemon 任一层替其它层维护第二份状态机
Mux 控制线与业务线
物理 transport 完成 mux-hello 后,wire 上只允许 TerminalMuxServerFrame / TerminalMuxClientFrame。
target 控制事实(例如 sessions、session-activity、target error/pong)只走 mux-target-message;session terminal 业务只走 mux-channel-message / mux-channel-binary。
已协商 mux 的物理 transport 禁止裸发 BridgeServerMessage。唯一物理 sender contract 必须使用 shared protocol 的 wire-frame union,禁止 as unknown as 绕过类型锁。
mux channel open 必须原子。控制事实 publish 失败时,terminal-mux-channel-runtime.ts 必须删除 channel registry、关闭 subscriber、投影显式 mux-channel-closed error,并停止 attach。
正向 gate:legacy/pre-mux 允许 raw control;mux transport 必须 target envelope;list/open/heartbeat 均通过 isTerminalMuxServerFrame。
反向 gate:mux transport 不得出现 raw session-activity 或错误的 mux-channel-message;publish throw 后不得留下 channel、subscriber transport 或继续 attach。
冻结角色边界
tmux truth
-> daemon server
-> client buffer manager
-> client sparse buffer
-> renderer window
-> DOM renderer
-> terminal shell
-> UI shell
四层只允许单向依赖,禁止越层漂移。
物理 owner 分层
client.renderer_window 只拥有 follow / reading / renderBottomIndex / visible range / immutable render snapshot / next-RAF commit。
client.dom_renderer 拥有 TerminalView / VisibleRow / TerminalPreviewRow / mirror-fixed zoom-pan / shared cell render / terminal theme 的 DOM 投影,禁止请求 transport、修改 sparse truth 或自己决定 follow/reading/renderBottomIndex。
client.terminal_shell 拥有 stage shell、shell skin、status、quickbar assembly、copy menu、keyboard lift,只能消费 renderer/DOM projection 并发出 shell intent。
禁止 client.renderer_window 直接承担 DOM 投影,也禁止 client.terminal_shell 持有 sparse/render 真源。
测试闭环阶梯:证明范围必须逐层匹配
terminal / daemon / client / renderer 相关任务完成前,先写清本轮影响到哪一层,再跑到对应层级。低层验证不能冒充高层完成。
L0 静态与架构 gate
证明:类型、文档 map、owner/gate、禁止路径扫描没有明显破坏。
不证明:真实 daemon、真实 tmux、真实 client、真实 UI 能连。
常用 gate:pnpm --dir android run test:feature-registry -- --reporter dot、pnpm --dir android exec tsc -p tsconfig.json --noEmit --pretty false、平台对应 type-check。
L1 owner 单元/红测
证明:修改点 owner 的正向/反向语义被锁住。
不证明:跨层真实连接成立。
要求:状态机、stream 收口、timeout、retry、错误投影、资源清理必须同时有正向与反向测试。
L2 daemon/tmux 真回环
证明:当前 daemon 代码能真实启动,tmux oracle 与 daemon mirror/client replay 一致,server 读写链路可通。
不证明:App/Mac/Web/Android client 入口一定连得上。
默认命令:pnpm --dir android run daemon:mirror:close-loop。
证据最少包含:case summary、tmux oracle、daemon websocket events、replay/strict audit 结果。
L3 本地客户端 transport/runtime gate
证明:仓库内本地 client 的 transport/runtime 能发起连接、完成握手、处理 head/body/input。
不证明:packaged app、真机 UI、IME、窗口生命周期一定正确。
Mac 默认命令:pnpm --dir mac test -- --reporter dot 与 pnpm --dir mac run type-check。
Mac 必须覆盖:bridge-transport、local-tmux-transport、terminal-runtime、workbench active target。
Android/Web 默认至少覆盖:open-tab restore/resume、SessionContext transport、buffer apply、TerminalPage/TerminalView 渲染入口的目标测试。
Session 切换、foreground resume、drawer/preview active 投影、或用户报告“session 名字和内容对不上”时,必须加入 session identity 黑盒门禁:同一测试里给两个 session 写不同 body marker,通过真实 sessionBufferStore -> TerminalPageStageShell -> TerminalView 渲染,断言 header/active id/name 与 DOM body marker 同源;同时模拟旧 session 迟到 publish、pause/resume、layout/IME resize,证明旧 body 不会出现在新 active 名字下。
L4 app shell / UI 行为 gate
证明:页面入口、tab/split/drawer/IME/layout 等 UI shell 行为在测试环境中按语义工作。
不证明:安装包、真实 WebView、真实 Electron packaged app 一定一致。
要求:若问题发生在容器、IME、drawer、pane、renderer 可见窗口,不允许只跑 daemon 或 transport gate。
L5 packaged / device / real app smoke
证明:真实 app 入口、打包产物、设备/桌面运行态按用户路径工作。
Android:每次影响 Android 真机行为且需要 Jason 复测的修复,都必须构建可升级 APK 包并发布到 update channel;默认命令 pnpm --dir android run build:android。汇报必须给出 versionName、versionCode、APK 路径、sha256;有 online ADB 设备时继续安装/启动/真机 smoke,没有设备时明确 L5 缺口。
Mac:packaged .app 或唯一 dev Electron 实例,截图/DOM/进程证据,必要时资源采样。
不证明:未覆盖的其它平台或远端网络路径。
汇报规则
汇报时必须按层级列出已跑 gate,并说明每个 gate 证明什么。
若只跑到 L2,只能说 daemon/tmux 闭环通过,不能说本地客户端连接正常。
若只跑到 L3,只能说 client 核心连接逻辑通过,不能说 packaged app / 真机 UI 已闭环。
若任务本身影响 L4/L5,但无法在线或真实运行验证,必须明确剩余风险和缺口。
0. transport / session 语义冻结
terminal input wire 在没有 daemon/client 协议协商与版本门禁前必须保持 string-only :
禁止把 { data, sentAt } 这类 object envelope 直接放进现有 input.payload,旧 daemon 会把 object 写成 [object Object] 进入 tmux
若需要迟到输入防护,优先在 client transport backpressure 处拒绝入队;新增 wire envelope 必须先有协议版本协商、旧 daemon 红测和升级迁移计划
daemon 收到非 string input payload 必须显式 input_invalid,不得解包执行、不得隐式 stringification
client 侧必须把下面两类动作彻底拆开,禁止再混成一个 switchRuntime 布尔语义:
restore-sync
只恢复本地 runtime shell / active tab
不得 自动打开或恢复 daemon transport
explicit-resume
只在用户显式切 tab / 显式恢复当前 session 时触发
才允许桥接到 resumeActiveSessionTransport / ensureActiveSessionFresh
新冻结:persisted terminal page 的冷启动不属于“纯 restore-sync”;它必须把 restored active tab 直接映射成一次 explicit-resume,否则现场会出现“当前 tab 黑屏不刷,切到别的 tab 再切回来才首连首刷”。
App 层若只是把 tab/runtime 切成 active,不等于 transport 已连通
session.state === connected、terminal page 显示 connected、activeSessionId 命中,都不是 transport freshness 真源
transport freshness 唯一 owner 只能留在 SessionContext -> ensureActiveSessionFresh / buildActiveSessionRefreshPlan
transport closed/error/tmux_session_unavailable 只属于 transport / attach fact ,不得 被 App 直接映射成 open-tab 物理关闭
tmux_session_unavailable 也不得从非 active、非 live session 投影成当前 UI 的错误/重连 banner。抽屉打开、session picker refresh、foreground audit 发现 stale persisted tab 时,只能记录缺失事实并停止该 session 的自动 retry;不能 emit SESSION_STATUS_EVENT(type='error'),不能让缺失的旧 tab 污染当前 active session。
open-tab 物理关闭只能由用户显式 close 触发;远端 tmux session-name audit 只能记录缺失/更新历史 session group,不得删除 open tab、切走 active tab、写 closed tombstone、调用 runtime closeSession
terminal tab chrome 必须以 OPEN_TABS 为唯一真源 materialize;runtime sessions 只补 transport/state。若 persisted open tab 的 runtime shell 缺失,UI 必须保留 closed placeholder;只有用户显式 resume/open 才允许按 persisted tab 重建 runtime shell 和 transport,禁止用 open tabs ∩ runtime sessions 过滤导致“看起来自动关闭 tab”。
OPEN_TABS 的物理身份只能是 sessionId;sessionName + daemon/bridge owner 语义 key 不得用于 normalize/upsert/runtime-merge/close 时合并、替换、删除已打开 tab。semantic key 只允许用于 saved-list import 去重与用户显式 close 后的 tombstone。
0.1 Windows daemon / WezTerm runner 经验
Windows daemon 只能把 WezTerm 当外部 terminal/mux source;runner 必须显式设置 ZTERM_TERMINAL_BACKEND=wezterm,禁止在 Windows 上误走 tmux。
从 SSH 里 Start-Process node server.cjs 只能当 direct smoke,不能当持久服务结论;OpenSSH job 生命周期可能带走子进程。持久运行真源必须是 Windows Scheduled Task ZTermDaemon 或后续明确的服务 owner。
PowerShell 5.1 兼容边界要测试真实机器:$PID/$pid 是只读自动变量,Start-Process 的 stdout/stderr 不能指向同一文件,New-ScheduledTaskSettingsSet 参数集不能按 PowerShell 7 猜。
PowerShell 5.1 写 JSON 配置时默认容易带 BOM,daemon 读取会直接炸 Unexpected token '';Windows runner 必须用 no-BOM UTF-8 写配置,不能依赖 Set-Content -Encoding UTF8 这种默认行为。
Windows Scheduled Task 运行环境不继承交互式 shell 的 PATH;runner 不能假设 wezterm.exe 可直接找到,必须显式探测/固化 ZTERM_WEZTERM_EXE 或安装目录。
Windows direct route 验证必须同时看:本机 127.0.0.1:<port>、本机 Tailscale IP <100.x>:<port>、远端设备到 <100.x>:<port>;前两者成功不等于 Android/Mac 经 Tailscale 可达。
Windows WezTerm closeout 必须同时跑 direct remote/input smoke 和 live daemon protocol smoke。live gate 必须自动比较输入 source marker 与解码后的真实 buffer-sync target,并通过 daemon control 精确关闭本轮 session;只验 CLI、create/connect/input,或通过 SSH 绕过 daemon 清理,都不算闭环。
后端 session 关闭必须经 selected backend 唯一 owner:WezTerm 调 WezTermBackendRuntime#closeSession,tmux 才允许 tmux kill-session。禁止让兼容 wire 名 tmux-kill-session 把实现锁死到 tmux。
1. daemon server
server / daemon 是独立层,只做:
mirror tmux truth
回 buffer-head-request
回 buffer-sync-request
处理 transport 级 input / file / schedule / tmux 基础控制
1.0 daemon 唯一心智
tmux -> daemon mirror writer -> daemon mirror store -> read api -> client
daemon 不关心也不能关心任何客户端逻辑/状态
daemon 只维护自己的 tmux mirror truth
daemon 内部也必须 读写解耦
写侧:tmux -> mirror store
读侧:mirror store -> head/range reply
mirror 写侧默认不得再拆第二语义:
tmux / WezTerm 禁止 history capture + visible capture + concat,只允许
single-capture -> canonicalize -> mirror store
Herdr 是唯一显式例外:官方 pane read --source recent --lines N 是
adapter 的 history tail truth,canonical frame 的可见 tail 是 live
overlay。两者只能在 Herdr adapter 内合并成一个 mirror snapshot;mirror
store 层不得再拆第二语义,不得把 1000 行 history 放进 33ms capture loop。
若 tmux 正在中间刷新,writer 必须执行 结构稳定才发布 :只有连续两次 canonical snapshot 的 absolute window / geometry 稳定,或已与当前 mirror 完整一致,才允许写 mirror;发布内容取第二次最新 snapshot。禁止要求动态 TUI 内容逐字完全一致,否则 top/htop/vim 持续变化会多次重采样并进入 failure backoff,表现为刷新很慢。
不允许基于 内容 overlap / repeated text 推断新的 absolute anchor
mirror absolute window 只能来自 tmux authoritative available window;内容相似性最多用于 debug,不得进入写侧真相
buffer-head-request / buffer-sync-request 只是读当前 mirror
请求不得触发 tmux capture / canonical rebuild / planner
正常 live 主链只允许:
tmux -> daemon mirror truth commit -> daemon push buffer-sync -> client buffer apply -> renderer body repaint
daemon live push 语义必须再冻结:
mirror body unchanged -> push buffer-head/info
mirror body changed -> push buffer-sync diff
diff 只允许基于 daemon mirror 前后版本计算
不得 基于客户端状态算 diff
daemon mirror capture 调度只允许消费 daemon 自己的物理事实:
mirror capture/canonicalize duration
ready body-subscribed subscriber transport count
daemon failure/backoff fact
禁止消费 active tab / foreground / follow / reading / visible range / viewport / pane layout
transport buffered bytes / send error 只由 daemon.buffer_publisher 的 per-subscriber pending/flush 消费,不得拖低 mirror capture cadence
有健康 subscriber 的 ready mirror 必须保持 active capture cadence;不得因为最近 1.5s 没观察到 body change 就退到 idle cadence。idle 只属于无 subscriber、失败/backoff 或 capture cost 过高等 daemon 物理事实;transport backpressure 由 daemon.buffer_publisher per-subscriber 处理,不得降低 mirror capture cadence;否则 TUI/status bar 的下一次原地更新会被 polling 先天限速。
硬规则:
server 不做 follow / reading 策略
server 不做 renderer 策略
server 不做 planner / prefetch / snapshot / fallback
server 不替客户端判断 gap,不替客户端决定该拉哪段
server 不关心 client 本地 buffer 是否为空、是否 gap、是否 follow/reading、是否首屏
server 不允许在 head/range 请求路径里“先同步 tmux 再回复”
每次回复都带当前 head ,避免客户端额外猜
mirror 生命周期也必须独立:
bodySubscribed=false、tab 暂停取数不销毁 mirror;仍存在 physical subscriber 时 transport detach 只影响对应 subscriber
最后一个 physical subscriber detach/close 后,terminal runtime 必须销毁 daemon-owned mirror truth、live timer 与 mirror-scoped input batch,后续显式 attach 再创建新 mirror
该 release 不得 kill tmux;sibling subscriber 不得被清理或受影响
daemon 只允许持有自己的服务端真相:
ws/rtc transport 是 daemon 可持有的物理连接真相
mirror 是 daemon 可持有的 terminal 真相
tmux session / file transfer / schedule 是 daemon 可持有的业务真相
daemon attach/connect 不得 隐式 new-session;远端 tmux session 不存在时只能显式报 tmux_session_unavailable,显式创建唯一走 tmux-create-session。
invalid pane metrics / pane is dead 不是 tmux_session_unavailable;它只表示 mirror capture/pane target 故障,不得 release subscriber / close client runtime
transport 断开只影响该 transport 自身,不允许推导客户端 active/inactive/foreground/background 语义
多客户端只表示“多个 transport/订阅者读同一 mirror”,不是 daemon 维护一份客户端状态机
daemon 不允许保留 client 风格状态机:
不允许 session.state
不允许 mirror.state
不允许 terminalWidthMode / requestedAdaptiveCols
不允许把 resize / terminal-width-mode 做成 daemon 内部状态推进入口
不允许 logical client session
不允许 clientSessionId 成为 daemon 内部长期状态 owner
不允许 readyTransportId
不允许 session transport token / attach-resume state machine
不允许 active tab / foreground / background / pane / viewport / visible range / width mode
若协议兼容期仍接收这些字段:
只能作为一次性 attach 参数或调试透传
不得写入 daemon 长期状态
不得反向驱动 mirror / tmux / transport 生命周期
clientSessionId / sessionTransportToken / session-ticket 的额外冻结:
2. client buffer manager
buffer manager 是独立 worker,不归 daemon、不归 renderer。
它的唯一职责:
自己起 timer
定时先问 head
自己比较 local buffer 和 daemon head
接收 renderer 声明的 visible range
自己决定请求哪段 buffer
head 变了或 gap 补齐了,就通知 renderer 对应 line/range patch
补充冻结:
buffer-head 到达时,buffer manager 只更新 metadata / planner 输入
cursor 变化也只更新 metadata
buffer manager 不得因为 head/cursor-only 更新直接触发正文 repaint
buffer-head 携带 cursor / cursorKeys metadata 时,可以更新本地 metadata truth,但不得调用 scheduleSessionRenderCommit() 或发布 render body;否则旧 body 会在真正 buffer-sync apply 前被重新投影,表现为“先闪旧 buffer,再被新 buffer 覆盖”。
2.1 本地 buffer 真相
目标本地维护一个默认 3000 行 、可配置的 rolling window;最大值由后续配置契约定义
当前 production shared/client 默认与最大仍是 1000 行 ,属于已知迁移差距,不能把目标值冒充当前实现
按绝对行号存储
可以是 sparse,不要求永远连续
历史超出窗口后滑走,但不是单次 payload 来了就把本地历史裁掉
已有 absolute-index 内容不能因为窗口判断错误而被逻辑清空
同 revision 的迟到旧 payload 不得把本地窗口重新锚回更老的位置
它只允许 patch 当前已配置 rolling window 内的 absolute-index truth
不允许因为晚到的 prepend / reading repair 响应,把 follow 中已经稳定的 tail window 拖回去
迟到旧 payload 不得覆盖当前本地 truth:incomingRevision < localRevision 必须显式 drop、记录 debug、请求当前 tail;同 revision payload 若会改写当前本地已有 non-gap absolute-index 行 ,也必须显式 drop。只有同 revision 且命中本地 gap 的 payload 才能作为 gap repair 合并。禁止用“先清空 buffer/DOM 再刷新”掩盖旧 payload 污染。
2.1.1 本地 buffer 不变量
local window invalid 只说明“当前工作窗口理解错了”,不说明已有 buffer truth 作废
anchor mismatch / head mismatch 也一样;它们只影响下一次 request plan,不影响已有 absolute-index 内容的存在性
buffer manager 没有权利 把已有本地 buffer 先 reset 成空窗再重拉
revision reset / reconnect / reanchor 期间若已有本地画面,低 revision 且 startIndex/endIndex 为空、lines=[] 的 payload 只能表示“新 buffer 尚未 ready”;client 必须保留上一帧等待非空或明确范围的 buffer-sync,禁止先发布空 buffer 导致黑屏再刷新
遇到上述 revision-reset 空 payload 且本地已有画面时,client 必须清掉该 session 的 tail-refresh debounce 并立即用 authoritative head 重发 tail sync;禁止只早退等待旧 debounce/下一轮 tick,否则普通连接/恢复会表现为“保留旧画面但刷新很慢”
正确动作只能是:保留已有内容 -> 计算缺口/新窗口 -> 请求 range -> merge -> 通知 renderer
2.2 follow 路径
每次 tick:
先问 head
比较本地尾部与 daemon head
若本地为空、失真,或离 head 超过当前 visible window:
直接请求 当前 visible window
移动本地 sliding window 到最新尾部
中间缺口不补
若离 head 不远:
补充冻结:
visible-window body pull 和 本地 rolling retention 是两个独立真相,禁止再用同一个 cacheLines 语义混写两者;retention 只表示本地保留跨度,不是拉取目标
2.3 reading 路径
reading 不改变 buffer manager 的 head-first 主循环
buffer manager 不持有 follow / reading / renderBottomIndex
它只接收 renderer 声明的 当前 visible range
若当前 visible range 内不连续,buffer manager 才请求 gap;隐藏区域不因已有 WebSocket 连接而主动拉取
gap repair 属于 visible range repair,不属于 renderer state ownership
2.4 禁止事项
renderer 不能直接触发 transport pull
buffer manager 不能替 renderer 改 mode
buffer manager 不能持有 renderer mode / renderBottomIndex / viewport scroll
不能因为本地历史有 gap,就在 follow 下回补整段历史
不能把 snapshot / patch-middle / fallback 再塞回来
不能把 local window invalid / anchor mismatch / head mismatch 实现成“先清空已有本地 buffer 再重拉”
buffer manager 也必须 读写解耦 :
写侧:同步 daemon -> 更新本地 sparse buffer
读侧:renderer 只消费当前本地 buffer
buffer manager 不关心 renderer 如何滚动、如何绘制、如何布局
buffer/head request cadence 必须是 session-aware:以目标 session 的 transport/socket buffered amount 与 runtime metrics 解析快慢通道;禁止在 assembly/request 路径直接调用全局 cadence,避免好网 session 被非目标或默认 33ms 节流拖慢
same-revision merge 也必须遵守“tail 优先稳定”:
若当前本地窗口已经贴着 authoritative tail,且迟到 payload 只覆盖更老的历史、不推进 tail
那么它只能补当前窗口内已有 absolute-index 行,不得 回拖 startIndex/endIndex
大 payload 裁剪也必须遵守“authoritative tail 优先”:
若 incoming buffer-sync span 覆盖当前 authoritative tail,且 span 长度超过当前配置的本地 retention
本地窗口必须裁成对应 tail window,不得因为 incoming startIndex 更老而裁成 head window
现场信号是 incoming.endIndex == nextTailEndIndex 但 nextEndIndex << nextTailEndIndex;这会先发布旧历史窗口,再被下一帧尾部 patch 拉回,表现为旧 buffer 闪屏
红测必须直接复现“大 span 覆盖 tail + lineCount > cacheLines”的日志形状,禁止只测 missingRanges 连续性
近尾大 payload 也必须遵守“当前 tail window 不回拖”:
若当前本地窗口已经贴着 authoritative tail,incoming buffer-sync 是较新 revision、覆盖了当前窗口一部分、但 endIndex < authoritativeTailEndIndex
本地窗口必须保持当前 tail anchor,只 patch overlap 行;不得因 incoming.startIndex < current.startIndex 把窗口重新锚到更老位置
现场信号是 previousEndIndex == tail,随后 nextEndIndex < tail,再被同 revision 追加尾部 patch 拉回;这会在底部持续更新时闪旧 buffer
红测必须复现近尾 span 如当前 [10606,11606) + incoming [10592,11601) + tail 11606,并证明非 tail reading/prepend 窗口仍可移动
sparse buffer-sync 只能建立在连续 revision 基线上:
若 client local revision 跳过 daemon 中间 revision,且 incoming payload 没覆盖完整 [startIndex,endIndex) 窗口,client 不得把该 sparse diff 合并成本地 body truth
正确动作是拒绝这次 sparse body apply,清 tail-refresh debounce,并请求当前 authoritative tail window
3. renderer
renderer 只看三件事:
local render buffer tail:buffer manager 已提交给 renderer 的本地内容底部
renderBottomIndex:当前要显示窗口的底部
visible range:当前要画的 absolute rows
额外门禁:
renderer body repaint 只允许来自 buffer-sync apply
daemon head metadata 只属于 buffer manager / planner 输入;renderer 不订阅 daemon head store,不用 daemon head 推进 follow demand
cursor metadata 可以被 renderer 读取
但 head/cursor metadata 不得作为正文 repaint 触发源
render store 发布也必须单调:
同一 session 已发布 revision=N 的 render body 后,任何 revision<N 的 render snapshot 都必须拒绝发布并记录 session.render-store.revision-regression-drop
render revision reset 只能来自显式 session truth reset(如 deleteSession() / 重建 session),不得由迟到旧 snapshot 隐式覆盖
该门禁只保护 renderer 发布边界;若高 revision payload 自身携带旧内容,仍必须回 daemon mirror / buffer-sync 源头追真源,禁止用清 DOM / 清 buffer 掩盖
3.1 block/shade glyph 真相
U+2580..U+259F(block / shade / quadrant)不能按普通文本 glyph 渲染;这类字符在 TUI/tmux 里经常承担“背景块/色块”语义。
renderer 必须按 fg/bg 生成 fill/gradient/pattern;若直接渲成普通字形,现场会表现成“红绿背景变灰”。
Android/WebView 不应依赖 color-mix(...) 渲染 ░▒▓;应在 JS 侧直接算出最终 RGB,否则现场可能退成灰/透明。
它不关心:
transport
daemon 策略
buffer 拉取策略
输入法
3.0 宽度模式真源
renderer 还必须显式区分两种宽度模式:
adaptive-phone
当前配置真源在 Settings
允许手机适配宽度,但最多只允许改 cols
Android runtime 后续不允许因为 keyboard / IME / safe-area / 容器高度变化继续改 tmux rows
mirror-fixed
daemon mirror / tmux 宽度保持上游真相
client viewport、IME、safe-area、容器宽度、字体缩放,不得 改写 mirror 宽度
renderer 只能:
读取原始列 truth
对当前 viewport 做横向裁切
维护自己的 horizontal render window
cell 宽度真相必须来自客户端实测的像素宽度 ,不能再把 1ch / 2ch 当终端列宽真相
双宽 cell 只能按 2 * measuredCellWidthPx 渲染;浏览器 fallback 字体的 ch 不是 tmux 列宽真相
pinch 缩小只缩放独立 canvas 视觉层;Android WebView 冻结实现是 CSS zoom
加 renderer 在 paint 前重派生可见行数。禁止把 zoom 直接写进 .term-grid,
禁止改写 tmux/daemon rows/cols,禁止等普通 effect 晚一帧同步行数。
缩放态仍使用原生 scrollTop;.term-grid 保持物理行高与普通 padding,
不得用 translateY 模拟纵向滚动。renderer 的 viewportRows 必须按
clientHeight / (physicalRowHeight * visualScale) 向上取整,visible demand
与 repair range 随之扩大;follow/bottom sync 在 visualScale < 1 时不得被抑制。
buffer window、visual row height、viewportRows、scroll range、render frame
与 virtual padding 必须由 shared renderer 的唯一动态 geometry entry 同次派生。
padding 位于 zoom layer 内时保留物理行高,交由 CSS zoom 统一缩放;禁止在
DOM projection 层再次除以 visualScale,禁止 TerminalView 维护第二套几何公式。
若 buffer 行宽大于 viewport:
默认显示左侧窗口
用户横向平移 renderer window 看右侧
不允许 本地重排、换行、回写上游宽度
cursor 也是上游 truth
Android client / renderer 不得自行改 cursor 样式、颜色、位置语义
但 cursor truth 也不得通过改写 buffer cell 来传递
正确做法只能是:daemon 单独回 cursor metadata;renderer 按 metadata 做 overlay / highlight
buffer lines 只承载 tmux 原始 cell truth
3.1 follow
follow 只是在收到 head / buffer 更新后
把 renderBottomIndex 对齐到最新底部
然后从本地内容池取当前窗口来画
若窗口里有 gap,先直接画空白占位
3.2 reading
用户上滚立即进入 reading
reading 时只改自己的 renderBottomIndex
申请的是当前 visible range 的渲染窗口
buffer 更新只会让 renderer 重绘当前窗口,不会自动滚动
即使当前窗口不连续,renderer 也只能把缺口画成 gap / blank marker;不能把已有 absolute-index 内容当成不存在
gap 行的显示规则也冻结为:
先显示空背景占位,不等待补齐
补齐后只替换对应 absolute-index 行
相邻 absolute line number 若不连续,当前行号必须显式高亮(debug 下优先红标)
follow 态若只是因为 live tail refresh / pending follow realign / programmatic scroll 导致 DOM 暂时没贴底,不得自动进入 reading ;进入 reading 只能由用户滚动手势触发
follow 态若只是因为 IME 弹起 / viewport 高度变化 / UI shell relayout 触发 DOM scroll,也不得自动进入 reading
viewport relayout 只允许触发 follow realign
不允许把“布局导致的 scrollTop 变化”误判成“用户回滚”
3.3 reading 退出条件
只允许三种:
重新进入
下滚到底部
用户输入
除此之外,live update / 补 gap / 尾部推进,都不能把用户拉回 follow。
3.4 visible range 声明与局部重刷
renderer 是 visible range 唯一真相
buffer manager 不得自行猜 renderer 当前窗口
renderer 必须把当前 visible range 声明给 buffer manager
buffer manager 补齐 gap 后,renderer 只重刷对应 absolute rows / changed range
禁止 “等整窗补齐后再统一出图”
4. UI shell
UI 只负责容器位置与裁切:
terminal 容器放在哪里
keyboard / IME 弹起后容器怎么上抬
终端可见区域是多少
硬规则:
IME 只移动容器,不改变内容
renderer 只在容器里画,不关心输入法
keyboard / IME 不得回灌成 buffer / render 真相
状态浮窗 与 绝对行号 都属于 UI shell observability,但必须 解耦
状态浮窗 只负责连接/刷新/模式观测
绝对行号 必须由独立显式开关控制,不能再隐式绑定到状态浮窗
Android 顶部 header inset 只允许来自 UI shell 的单一稳定像素真相 ;IME 弹起导致的 visualViewport.offsetTop 不得再被当成第二份 top inset 叠到 header 上
Android connect / reconnect 不得 把 UI 容器测得的 cols/rows 当成 tmux viewport 真相带给 daemon;容器变窄/变矮、IME 弹起、safe-area、前后台恢复,都只能影响 shell 裁切与 renderer 可见窗口
tmux rows 也必须单独冻结:
Android runtime 后续运行期间,keyboard / IME / safe-area / 容器高度变化 不得 继续改 rows
最多只允许初始化阶段确定一次 rows;更稳妥的实现是直接保持上游 tmux rows,不再由 Android 改写
adaptive-phone 若需要适配手机,最多只改 cols
server 的 attach / resize / width-mode reconcile 也不得再写第二份 rows;rows 真相只能来自上游 tmux / mirror baseline
“看不到的地方不画”属于 renderer 绘制窗口 真相,不属于 UI shell / buffer manager / daemon;UI shell 只能改容器位置与可见高度,不能借机改 tmux geometry
QuickBar / 快捷菜单属于 UI shell;整块 shell 区域 都必须吃掉非交互点击,不能让空白点击穿透到底层 terminal/ImeAnchor 把 IME 弹出来
QuickBar 壳布局必须是 三栏 :前两栏保持老样式(左侧固定六键区两行 + 右侧两行滚动快捷区),第三栏恢复工具栏
QuickBar 固定布局还要守住:
左侧固定六键区必须是:状态 / ↑ / 键盘 与 ← / ↓ / →
状态 只替换老附件位;↑ 与 键盘 保持老位置
文件/图片/同步/截图 这四个工具入口要作为 第三栏工具栏 显式可见
工具栏只能有这一份;悬浮菜单里不得再重复渲染 文件/图片/同步/截图
固定六键区宽度必须能完整容纳 状态 / 键盘 文案,不能裁切、顶出或超界
只有 QuickBar 内显式 editor / input / button 等交互控件允许接管焦点;普通 shell 容器点击必须被阻断在 UI shell 层
若 keyboardInsetPx > 0,QuickBar 必须作为整体容器 抬升到键盘上方;同一份 keyboard inset 只能消费一次:terminal stage.bottom = quickBarHeight + keyboardLift,quickbar shell.bottom = keyboardLift,禁止再用 QuickBar 内部 padding/margin 对同一份 inset 二次抬升
QuickBar 的 onMeasuredHeightChange 必须上报自身真实 chrome 高度,不得再扣 keyboardInsetPx;IME lift 已由外层 shell bottom 消费。若测量阶段扣 inset,quickBarHeight 会在键盘弹出时归零,导致 stage 丢失快捷栏预留。
5. 反模式清单
以下一律视为错误实现:
snapshot
stream-mode
planner
viewport prefetch 第二链路
ws close -> daemon 推导客户端状态并修改 mirror/tmux 生命周期
inactive tab -> close session / close transport
reconnect -> new client session semantics
daemon 在 buffer-head-request / buffer-sync-request 路径里触发 tmux capture
daemon 根据 client 状态决定“要不要先刷新一下 mirror 再回复”
daemon 因单个 subscriber detach 就销毁仍有 sibling 的 mirror,或把 mirror release 错误升级成 tmux kill-session
daemon 把 cursor / selection / transient visual state 直接写进 buffer cells
client viewport 变窄就改写 daemon mirror / tmux 宽度
mirror-fixed 下把长行本地重排成手机宽度
mirror-fixed 下在没有 horizontal pan 手势链的情况下把 tab swipe 禁用,造成无横向交互出口
renderer 直接 request buffer
buffer manager 直接改 renderer follow/reading
follow 下因为历史 gap 去回补整段旧历史
local window invalid -> empty local buffer -> full reanchor
anchor mismatch -> clear local truth
head mismatch -> treat local content as lost
初次连接或恢复连接时,两三 K 两三 K 慢慢追历史
任何 fallback / 降级 / 第二语义
6. 必须遵守的开发顺序
先落 docs / AGENTS / skill
-> 再补测试
-> 再改代码
-> 再跑真实回环
顺序错了,视为没按真源做。
其中测试与检查清单真源固定为:
android/docs/daemon-mirror-test-plan.md
android/docs/terminal-test-loop-checklist.md
7. 必跑真回环
tmux truth
-> daemon log
-> renderer declare visible range
-> client buffer manager log
-> renderer commit log
-> Android APK 真实画面
最少覆盖:
初次连接
冷启动进入单个 active tab,等待首屏刷新
进入一个 tab 后切换到另一个 active tab,等待首屏刷新
后台恢复
输入英文 / 数字 / 空格 / 回车
reading 连续上滚
输入退出 reading
daemon 重启恢复
prompt / input row style parity
输入发出后、buffer-sync 前,terminal 可见内容不得本地直接变化
buffer-sync 到达后,renderer 只回显 payload,不得自己再造 prompt/cursor 第二语义
same-session transport retry
inactive tab stops polling but does not close session/transport
same target multi-session stays isolated without shared reconnect fate
foreground resume reuses the original session transport before any fresh reconnect
prompt / input row 必须可比对 char / fg / bg / flags
daemon 若回 cursor,必须是独立 cursor metadata ;lines 不得因 cursor 改变
7.1 必须沉淀成自动回归
上述 case 不能只靠人工重试。
必须把问题收敛成:
可复现的本地 case
-> 可失败的自动测试
-> 修复后稳定转绿
-> 纳入每次编译前回归
只要某个 terminal 线上问题还不能被本地自动 case 复现,就不允许说“根因已收敛”。
最低自动回归覆盖:
server contract:head / range reply 语义
buffer manager:head-first / far jump / reading gap / in-flight closeout
renderer:follow commit / reading hold / input reset follow
daemon mirror close loop:top / vim / input echo
Android 首屏:cold start single tab / switch to another tab 的 first paint
新增门禁精华:
cold-start / foreground resume 的 transport gate 必须优先 active tab;若 hidden tabs 跟着一起 eager reconnect,active tab 的首屏会被排队拖慢。除非已有被验证的 hidden low-frequency 设计,否则 hidden tab 默认只保留 runtime shell,等显式激活再 reconnect。
若现场 buffer-sync 下行长期几百 KB/s 甚至 MB/s,先直接抓 daemon 回包;若仍返回 legacy lines[].cells[] 而不是 compact i/t/w/s,优先查 daemon service staged runtime 没更新 ,尤其是 start/restart 只重启 launchd 但没重建 ~/.wterm/daemon-runtime/server.cjs。
daemon service 管理也必须遵守唯一真源:start/restart 必须重建当前 staged runtime;服务异常必须显式失败,不能 fallback 回 tmux session 掩盖旧 runtime/旧语义。
本地 daemon mirror close-loop 必须使用隔离测试端口 ;禁止复用用户常驻 service 端口(如 3333),否则脚本会误连现场 daemon,出现“自动回归假绿 / 假红”。
daemon mirror close-loop 的 client replay harness 也必须服从 revision reset 真相 :daemon 重启后若 revision 回到更小值,回放时必须先 reset local buffer 再 apply;否则会把回环假红误报成 daemon/client 主链故障。
hidden / non-visible tab 不得继续挂载 renderer 实例;renderer scope 必须严格等于当前 visible pane。否则 header truth 已切换但 body 仍残留旧 session DOM,Android WebView 容易出现“页头/内容对不上、像花屏”的 stale compositing。
renderer scope 回归测试不能只断言 “inactive renderer 还在但 data-active=false”;必须直接断言 hidden renderer 不在 DOM ,否则会把 DOM 覆盖类问题测成假绿。
foreground resume 对 active tab 不能只补一发 buffer-head-request;若 daemon 仅 revision 前进而 latestEndIndex 不变,buffer manager 仍必须带一次性 same-end tail refresh demand,确保 head -> sync -> body repaint 闭环成立。
App foreground resume 的真相只能是:先 probe/resume 当前 active transport,再决定是否 reconnect ;App 不得再按 UI session.state 先分叉,否则会把“label stale but transport alive”误杀成重连。
foreground/background 的 hidden gate 也必须只有一份真源;若 App 已统一消费 visibilitychange / resume / appStateChange,SessionContext active tick 不得再自行读取 document.visibilityState 做第二份停刷判定。
foreground false->true 不能只等 active tick;SessionContext lifecycle 必须把 active session 送入唯一 explicit-resume 入口,和冷启动恢复共用同一 transport owner,否则会重演“杀进程秒连、后台回来卡死”的双路径分裂。
若日志出现 sessionState=reconnecting + ws=null + no pending open intent,优先判定为 stale reconnect bookkeeping ,不是“真的还在连”。foreground/explicit refresh 必须允许重新 reconnect;不能让粗粒度 reconnectInFlight=true 永久挡住恢复。
open-tab restore / runtime sync 允许为 persisted tabs 恢复 local runtime shell :可 做 cold restore / sessionId remap,但 自动打开 daemon session。真正的 daemon open 只能来自显式用户 open/import/resume 动作。
8. 现场判断口径
看到这些现象,优先判对应层:
初次连接慢慢追历史:buffer manager 错
输入发出去几分钟不刷新:buffer manager / renderer 通知链错
reading 一滚就被拉回:renderer mode 错
带宽异常大:仍有 snapshot / 整窗重拉 / payload 误裁
keyboard 影响内容或行数:UI shell 越层
收到 head 以后长期不再拉新 buffer:优先查 buffer manager 的 in-flight pull 是否死锁
pullHz == 0 && renderHz == 0:优先查 active tab 首次激活后是否根本没进入 head-first 主循环
foreground / cold-start 后 active tab 长时间 connecting 且 hidden tabs 同时在连:优先查 active-only transport gate 是否被破坏
Android 若 ImeAnchor 已经产生日志,但 client 侧出现 session.input.queue 且长期无刷新:先判定为 active transport 已死 ,不是 IME 问题;active tab 在 resume / switch / input 这三个动作上,只要发现没有 live ws,就必须立即 reconnect,不能只排队等下一次偶然恢复
用户现场若给的是 ADB device 地址 ,不要误判成 daemon 地址;先从 Android WebView localStorage 真源读取当前 bridgeHost / bridgePort / authToken,再去打 /health、/debug/runtime、WebSocket probe
若怀疑“是 daemon 慢”,必须补一个 independent direct daemon probe :临时 tmux session 上测 connect -> head -> input -> head change;如果 direct probe 是几十毫秒,而现场 session 仍是几十秒,就先把 generic daemon 基线排除,转查现场 session / IME / active transport 链路
若真机现场出现 session.buffer.request 已发出、daemon direct probe 也能直接拿到非空 range,但 APK 仍首屏空白/R=0,优先判定为 client 侧 buffer-sync -> local apply -> renderer commit 断链 ;先补本地结构化证据,不要再回头怪 daemon
若现场出现“本地窗口判断错后直接白屏/大包重拉”,优先判定为 client 侧越权清空已有 absolute-index buffer truth ;这不是 daemon 问题,也不是 buffer 真丢了,而是 client 把“窗口规划错误”实现成了“truth reset”
若 Android 真机出现“未点键盘却前台自动弹 IME”或 IME 在九宫格/QWERTY 间异常切换,优先查 ImeAnchor 的 stale show/focus 状态是否跨前后台遗留;只有显式 keyboard action 才允许 show IME
若现场表现为“语音/CJK commit 已经发生,但要再补一个字符才刷新 ”,优先查两件事:
same-end 新 revision 是否被旧的 in-flight tail-refresh 误判成“已覆盖”;同窗同 range 但 targetHeadRevision 变了,必须允许重发
buffer-head.cursor 是否被 client 丢弃;head 已经带来的 cursor metadata 必须立刻进入本地 truth,不能等下一次 buffer-sync 才纠正高亮/光标
若现场表现为“长输入/语音输入丢字,或语音换行直接执行命令 ”,必须把文本语义与传输完整性分开锁:
Android committed text 的 CR/LF 只在 terminal.keyboard_ime normalization owner 归一成文本分隔空格;显式 Enter 继续走 editor action / hardware key 独立路径,禁止 daemon、transport、renderer 再过滤语音换行
input wire 在协议协商前保持 string-only;native bridge、client WebSocket frame、daemon frame cap、backend write 各自使用独立 UTF-8 byte budget,禁止把对象 envelope 或 renderer/local echo 当补偿
Session preview truth
多终端快捷预览只能是 UI projection:数据链必须保持 tmux -> daemon mirror -> client sparse buffer -> immutable render store -> shared TerminalView -> preview DOM。
禁止 preview 自建 ANSI/cell/cursor parser、截图/文本 cache、transport/reconnect、resize、viewport writeback、width-mode write、tmux geometry write 或 buffer reset。
Preview tile 必须把 TerminalView 作为 read-only shared renderer 使用:active=false、live=true、无 input/resize/viewport callbacks、allowDomFocus=false、mirror-fixed。
Secondary preview 的每行必须是同一 passive row 投影:复用共享 terminalCellStyle()/row view-model,按相同 style run 聚合成少量 span,保留 ANSI fg/bg/flags;禁止退回纯文本 theme.foreground 或另写第二套 color parser。
Preview selection 持久化时必须存完整 open-session identity,并在恢复时至少匹配 sessionId + bridgeHost + bridgePort + sessionName,有 daemonHostId 时也必须匹配;stale target 是失效选择,不是隐式 open/reconnect intent。
Preview 打开才允许把 selected ids 临时加入 body subscription live set;关闭/后台必须回到 baseline。黑盒 gate 必须自动比较 tmux source、daemon/client sparse truth、render store 和 preview DOM,并验证 subscriber lifecycle。
App foreground truth 是唯一的 preview/body-demand 输入;后台态必须把 preview 选中集拉回 baseline,不能继续维持额外 body subscription、视频解码或轮询。任何 offscreen 重媒体流都必须由同一个 foreground gate 停掉,禁止再长出第二套后台 timer/observer 补偿。
Android 原生后台服务只允许作为平台执行支持:Activity 停止且 retained sessions 存在时,可持有通知面与一个进程级 PARTIAL_WAKE_LOCK(保留 WAKE_LOCK 权限)供现有 control-plane / target-transport owner 做低频心跳;必须保持 bounded lifecycle,sessionCount <= 0 / stop 即 release 并 stopForeground(true) / stopSelf()。它禁止拥有 socket、route、session truth,禁止请求忽略电池优化权限,禁止替 transport owner 开/建连接、做 body/video 订阅或后台保活。
Preview tile activation 必须走唯一 page owner:先把目标 session 投进当前 focused session-group slot,再发 active-session switch。只切 active session 不改 viewport projection,会让输入/live 到新 session、真实 shell 仍显示旧 center session;preview 关闭后旧 center 不再 live,表现为“preview 刷新但进入 shell 不刷新”。
Source-to-shell 黑盒 gate 必须覆盖 preview grid -> real TerminalStageShell 替换后继续刷新:选中 session 新 marker 出现在真实 shell DOM,旧 session marker 被排除,物理 socket 不重建,subscribers 恢复 baseline。
Preview tile 长按替换只改 ordered selection:420ms 长按必须有移动阈值,触发或移动后都要抑制 synthetic click;菜单只列当前 open 且未选中的 session,替换保持原 slot 顺序。禁止借替换触发 active switch、socket open owner、buffer reset 或 renderer 写入。
Preview 打开时由 mode owner 捕获 entry { activeSessionId, slotIds, focusSlot }。关闭按钮、右滑退出、Android system Back 都走唯一 cancel owner 并恢复该快照;tile tap activation 必须先清除快照再执行显式 switch。Back listener 只在 preview open 生命周期注册。
2026-06-29 buffer publish short-circuit
当“大面积刷新后空白、滚一下又恢复”时,优先查 commitBuffer() 是否把 live buffer 引用直接塞进 store 并被引用短路。
真源规则:store 必须存不可变快照,commit 只能按内容判等,不能靠对象引用判等;否则上游原地 mutate 会让正文刷新静默失效。
2026-06-29 Windows WezTerm backend boundary
WezTerm mux 可作为 Windows buffer source,但 wezterm cli 输出不是 daemon truth;ZTerm adapter 必须把 get-text --escapes 转成自己拥有的 absolute mirror snapshot 后才允许进入 buffer-sync 链。
WezTerm cursor 不在 get-text --escapes 正文里;Windows backend 必须从 wezterm cli list --format json 读取 cursor_x/cursor_y/cursor_visibility,并作为独立 metadata 进入 mirror snapshot / buffer-head.cursor。
wezterm cli --prefer-mux send-text --pane-id <id> --no-paste 只允许通过 stdin 写真实 terminal input,禁止把用户输入塞进 shell args;已验证 Enter / Backspace / arrow escape / raw TUI / Codex TUI text entry。
WezTerm backend 是显式 backend selection,不是 tmux fallback;Windows 默认 wezterm,非 Windows 默认 tmux,未知 backend 必须显式报错。
已知限制:ETX/Ctrl+C 可送到 raw-mode/TUI,但不能当作 Windows console control event 中断 cmd.exe 子进程;不要宣称完整键盘等价。
2026-08-14 Terminal Source Adapter Boundary
daemon mirror 只消费统一 TerminalSourceAdapter.readSnapshot() 产出的
TerminalSourceMirrorSnapshot;mirror 主链禁止散点执行 tmux/Herdr/WezTerm
source 命令,source 差异全部收口到 adapter。
adapter snapshot 必须携带 daemon-owned absolute range / geometry / cursor
metadata;absolute range 只能来自 adapter/source-side canonicalizer,禁止用
内容 overlap / repeated text 推断 anchor。
未知/不支持 source 必须显式失败;禁止把 unknown source 默认落到 tmux。
adapter 只承载 daemon source 侧事实,禁止把 client viewport/follow/reading/
active tab/foreground 带进 daemon 或 adapter。
adapter snapshot 的 revision 只是 source-side 信息字段;zterm
mirror.revision 由 daemon.mirror_store 的 runtime scheduling owner 唯一
推进,禁止让 source revision 直接变成 zterm revision broadcast,也禁止
daemon.mirror_writer 推进 revision。
validated source capture 与 authoritative mirror snapshot commit write 由
daemon.mirror_writer 唯一执行;resource.mirror_store 仍是 canonical
mirror truth / revision / runtime scheduling 的唯一 owner。adapter 只是把
source readback 归一成 mirror snapshot,不新增第二真源或第二 writer。
2026-08-14 Herdr History + Live Latency Boundary
Herdr history truth 是官方 pane read <pane_id> --source recent --lines <min(terminalCacheLines,1000)> --format ansi --raw 的 adapter-owned tail
snapshot;canonical frame 仍只提供 live visible rows / cursor / geometry /
cursorKeysApp / alternateScreen。
pane get.scroll 的 maxOffsetFromBottom + viewportRows 只作为 source
total-row hint 推进 daemon-owned tail window;禁止把它当成 absolute line
identity,禁止用内容 overlap 推断 index。
per-session sourceEndIndex 只单调增长,bufferStartIndex 必须等于
sourceEndIndex - bufferLines.length,availableStartIndex 同
bufferStartIndex;Herdr 0.8.0 只能提供最近 1000 行,禁止本地拼接或
滚动遍历伪造更早历史。
host 在底部且 frame geometry 与 history geometry 匹配时,才允许用 live
visible rows 替换 merged buffer 尾部;host scrolled 时不得 overlay,cursor
必须显式 null,并立即触发下一次 history 刷新。
history 刷新必须低频(默认 1000ms;attach / host scroll / geometry change
立即),pane read 1000 行禁止进入 33ms loop;live frame 到达通过
onLiveActivity 唤醒 mirror live sync,无 body subscriber 时由现有
scheduleMirrorLiveSync 停止,不产生空转。
pane read 失败或返回空必须显式失败,禁止 frame-only 24 行兜底;官方
1000 行上限写入显式 capability gap herdr-history-limit-1000。
pane get scroll metrics 从每帧同步读取改为低频节流(默认 100ms),
metrics 读取失败不得丢弃 frame。
2026-08-14 Visible Repair Ledger
visible non-gap repair 必须按
sessionId + visibleRange + tailEndIndex + targetRevision 维护 per-session
ledger:pending -> dispatched -> fulfilled,dispatched 超时未收到完整
visible-window authoritative response 可再次 dispatch,但不能无限放大。
repair 未实际写入 wire 时保持 pending;send-fail/no-socket 不清 ledger。
只有覆盖完整 visible range 的 body apply、显式 session cleanup、或
authoritative revision epoch reset 才允许清除/fulfill。
禁止用全局 gapRanges 或 localRevision == daemonHeadRevision 代替行级
freshness;sparse 后续 revision 不得掩盖旧 non-gap visible 行。
fulfilled / superseded 是历史账目,不得抑制新的 repair demand;只有 active
pending / dispatched 且未超过 stale timeout 才在重发窗口内抑制,否则后续
sparse demand 必须创建新 ledger entry 继续收敛。
完整 authoritative body apply 只精确 fulfill 仍 active 的 ledger entry:按
entry 自身 targetRevision 标记 fulfilled,不创建以 response revision 为
key 的合成 entry;payloadRevision >= entry.targetRevision 且 payload
连续覆盖 entry 的 request range 才满足。
红测必须覆盖:repair 丢失后仍收敛、repair 响应不完整仍 pending、
full response 只 clear 一次、unchanged visible rows 不 spam、fulfilled 后
新 sparse demand 不被旧账目永久压住。
2026-08-14 Daemon Input Queue Boundary
daemon.input_queue 唯一 owner 是
src/server/daemon-input-queue-runtime.ts;receive validation、byte cap、
stale/session-required retryable nack、seq dedupe、enqueue/dispose、
backend write ordering 都在该文件加
src/server/terminal-reliable-input-ack.ts。
terminal-message-runtime.ts 只把 input 和 plain-text 帧转给 queue
owner;terminal-control-runtime.ts 只向 queue 暴露
writeBackendInputGroup / chunk budget,不再持有 queue internals。
queue 是唯一 daemon 输入写后端路径;mirror runtime 的 live input 也走同一
enqueue/dispose owner。禁止其他 handler 直接写 tmux/backend input。
必须保持:payload 仍 string-only;stale/session-required 保留 retryable
nack;invalid/oversize 非 retryable;duplicate seq 不得重复写;coalescing/
顺序/chunk sizing/append-enter 边界不变;detach/close/destroy 必须 dispose
queued input。
红测:daemon-input-queue-runtime.test.ts、
terminal-message-runtime.test.ts、
terminal-control-runtime.input-queue.test.ts、mirror/backpressure/
detached-session 回归。
9. Android copy-mode gate
WebView copy-mode 长按有两层 native gate:setOnLongClickListener(v -> true) 只管系统 ActionMode / 工具栏,setLongClickable(false) 才会停掉原生 haptic / selection 拦截,让 JS onTouchStart 的长按计时器真正启动。
copy 菜单不出时,先查 native long-clickability,再查 DOM touch 事件链,最后才看 React 菜单状态。
DOM 层仍负责 preventDefault / stopPropagation,但不能指望它单独压住 Android 原生长按行为。
2026-06-01 多 pane closeout 精华
多 pane UI 对齐必须查“生产入口真源”:Mac 当前真实入口是 ShellWorkspace,不要只测未接入入口组件;packaged smoke 必须用进程路径证明启动的是 mac/out/.../ZTerm.app。
Android 接 shared PaneTabs/PaneStage 时,不得丢旧交互合约:header padding、close aria/touch、relay badge button、横向 touch-scroll 抑制、inactive pane first-frame viewport demand 都要进红测。
2026-06-01 iTerm2-style split correction
多 pane / split 需求不得再按 flat horizontal panes 验收;正确真源是 split tree(leaf pane + row/column split node + ratio)。Mac packaged smoke 也必须验证横/竖嵌套 split 可见和分隔线可拖拽,而不是仅看到多个列。
2026-07-12 terminal layout ownership correction
terminal 内容排版唯一真源是 tmux/mirror capture:mirror.rows/cols/bufferStartIndex/bufferLines/cursor 只能由 tmux capture/readback owner 写入。
client 不做 terminal 内容排版:TerminalView / renderer / UI shell 只消费 mirror rows/cols/buffer truth,负责裁切、显示、输入、viewport demand;不得根据输入次数、IME、WebView 测量、container resize、viewportCols 或 active/focus 状态重排 terminal content。
进入 session 后如果看不到之前 buffer,先查 attach/resume -> tmux geometry/head -> buffer-head -> buffer-sync -> local apply 首包链路;不要在 renderer 层用 CSS wrap、row background、scrollbar、border、默认 theme、synthetic cols/rows 或 forced layout key 去“修排版”。
adaptive-phone 也不是 client 本地排版:正确链路只能是 client 上报 measured cols -> daemon adaptive lease owner 请求 tmux reflow -> tmux capture/readback 更新 mirror truth -> client 固定行高渲染。
反模式:把 viewportCols / widthMode 放进 render geometry revision key、无 tmux truth 时补 80 cols、修改 shared renderer row/cell/theme 背景来掩盖旧 buffer 或空 buffer、用 UI border/scrollbar/IME 变化触发 terminal content geometry refresh。
红测要求:session enter / drawer switch / explicit resume 必须证明 first buffer-head-request 到达当前 active resource,head 后按 tmux availableStartIndex/latestEndIndex/rows 拉 buffer-sync;renderer 测试只证明消费 fixed mirror truth,不证明或制造排版。
2026-08-27 Mac packaged app 启动协议
Unsigned 本地 Mac package 必须先 ad-hoc 重签再启动:codesign --force --deep --sign - <ZTerm.app>;缺少这一步或使用 open -n 启动 unsigned package,会被 Launch Services 在后续启动时重新走 Gatekeeper 并把 bundle 标成“恶意软件/移到废纸篓”。
禁止 open -n unsigned package。正确启动是直接执行 "<ZTerm.app>/Contents/MacOS/ZTerm" --remote-debugging-port=<port> --user-data-dir=<evidence>/user-data --no-sandbox,并把 stdout/stderr、exit code 落到 evidence。
每次打包后必须验证 bundle 存在:test -d mac/out/mac-arm64/ZTerm.app;缺失时先 pnpm --dir mac run package,禁止继续跑 blackbox gate。
spctl --assess rejected 不等于 unsigned internal alpha 启动失败。真实判定必须看进程路径 + CDP /json/version + /json/list 是否返回 ZTerm Mac page target。
启动前必须清理旧 CDP owner:ps -axo pid,ppid,comm,args | egrep 'ZTerm|--remote-debugging-port=<port>',再按明确 PID 关闭;禁止多实例叠在同一个 port 上。
2026-08-18 terminal container shrink boundary
容器缩小或恢复时,先验证 TerminalStageShell -> PaneStage -> .wterm -> ResizeObserver/clientHeight -> renderer viewport demand 的实际尺寸链;shell stage wrapper 必须是可计算高度的 column flex 容器,不能只设置 flex: 1。
TerminalView 是 DOM 高度和 viewportRows 的唯一 owner。TerminalPage / pane 编排层禁止读取 buffer.rows、buffer.endIndex 伪造非 active pane 的 viewport demand;该路径会把 buffer truth 和 renderer window 混成第二语义,并可能在 shrink 后黑屏。
shrink 验收必须同时检查:stage/.wterm clientHeight > 0、renderer 上报的 viewportRows 随高度变化、absolute tail 行连续、没有 data-terminal-line-discontinuous;再恢复原尺寸确认 geometry 和尾行继续收敛。
open-tab-intent 这类 core truth 模块不得出现 fallbackActiveSessionId / fallbackSessionIds 式命名;需要保留 active 或选择关闭后的下一个 active 时,必须写成显式 policy(如 preserveActiveSessionId / nextActiveCandidateSessionIds)并由架构 gate 锁住。
open-tab-persistence 读写失败不得变成空 truth:存储损坏、读取异常、写入异常必须返回显式 failed/invalid 或 { ok:false, error },调用方至少记录结构化 runtime debug。
force relay / use auto / reconnect-with-mode 这类同一 open tab 的 transport-mode rebuild 只能放在 useSessionOpenActions 这类 session-open owner;App.tsx、TerminalPage、header、drawer 只能传 intent,不能自己构造 Host 或执行 closeSession -> createSession -> switchSession 生命周期序列。 现场若出现:
app 说 connected
但 daemon 看不到 subscriber / session-open / head-sync 进展
优先回查这条语义分裂,不要先怀疑 daemon / token / tmux 宽高逻辑
回归最少要同时覆盖:
cold restore 不自动开 transport
explicit tab switch / explicit resume 会触发 transport reopen
App 层不得再长出第二套 foreground refresh / reconnect 语义
daemon 不得长期保存 UI / renderer width policy;adaptive-phone 只能进入唯一 adaptive width lease owner:
adaptive-phone connect/resize 注册当前 physical transport subscriber 的 { cols, heartbeatAt }。
同一 tmux mirror 多个 adaptive lease 必须按 active holders 聚合最窄 cols,并只在 applyAdaptiveTmuxWidth() 内请求 tmux resize-window -x <cols> 让 tmux 自己重排。
持有 lease 的 transport close/detach、切到 mirror-fixed、invalid cols 或 heartbeat 过期后必须清理该 subscriber metadata 并重算;最后一个 lease 消失必须在 releaseAdaptiveTmuxWidth() 恢复/释放 tmux 宽度控制权。
widthMode、terminalWidthMode、requestedAdaptiveCols 不得写入 daemon 业务真相;resize-window / window-size 只允许出现在 adaptive lease owner 的 apply/release 函数里。
daemon 请求 tmux resize 后不得自写 mirror.rows/cols;mirror 内容和尺寸仍只能来自 tmux capture/readback。
好网 fast lane 与弱网 slow lane 必须由红测锁定;性能 trace 只记录 timestamp/duration/bytes/line count/id/kind 这类 metadata,禁止记录真实 terminal payload 内容。
生产性能 trace 必须按 traceId + mirrorRevision + subscriberId 关联独立样本;同 session 的不同 revision 不得被拼成一个伪 latency。完整阶段是 capture -> canonicalize -> mirror commit -> send -> client rx -> buffer apply -> RAF -> render commit,只允许有界 metadata ring 和 p50/p95/p99 summary。 physical subscriber 与 live body subscription 必须分离:
attach transport 不等于永久订阅正文;
daemon 只保存 bodySubscribed 这一物理事实,不保存 active/inactive/visible/foreground 原因;
unsubscribe 只停止 unsolicited buffer-sync,不得 close transport、detach mirror、禁用 input/file/schedule 或 explicit head/range read;
recurring capture 只由 ready 且 bodySubscribed 的 physical subscriber demand 驱动;unsubscribe 必须经同一个 scheduler owner 立即停旧 timer,恢复 demand 后恢复 scheduler,不得由 head/range 请求直接 capture。
RTC/Relay datachannel close 可能不会可靠到达 daemon;daemon 必须按 transport inbound heartbeat sweep stale bound subscriber,并走 detachSubscriberTransportOnly。该路径禁止杀 tmux;若 detach 后 mirror 已无任何 physical subscriber,只允许由 terminal runtime 释放 daemon-owned mirror/buffer/input/timer 资源;仍有 sibling subscriber 时必须保留 mirror。不得在 Android UI/renderer/buffer 层做退出补偿。
subscriber backpressure 禁止直接永久 skip 当前 revision:
每个 subscriber 最多保留一个 bounded pending latest revision 和合并后的 absolute ranges;
pending flush 时必须从当前 mirror store 读取最新权威行,禁止保存历史 serialized payload/cells;
changed span 序列化后超过单消息预算时,必须按 absolute row 连续切成同 revision 多个 buffer-sync,覆盖原 span 的每一行;禁止裁成 live tail,否则超过一屏的大刷新会静默丢掉 source rows;
high/low water 必须迟滞;send error/non-open/旧 generation 不得清 pending 或推进 sent revision;
一个 slow subscriber 不得降低 healthy subscriber cadence;输出停止并 drain 后必须到达 daemon latest revision。
以上 subscriber 发布状态唯一 owner 是 daemon.buffer_publisher(src/server/daemon-buffer-publisher-runtime.ts);terminal-mirror-runtime.ts 只调用 publisher 的 broadcast/flush 接口,不得重放 pending/backpressure/head cache/frame split 语义。
buffer-sync-request 的 range response 必须进入 publisher 的独立 per-subscriber FIFO lane;不得复用 live pending-latest 合并语义,必须保留请求范围和 requestSentAt,并受同一 backpressure/frame-split/send-budget 边界约束。
shared physical transport 的正文发送必须由 physical send boundary 统一预算并按 logical subscriber 轮转;publisher 可以维护 subscriber pending state,但不得让单 channel 连续发送全部分片而独占 shared socket。
mirror capture 允许的性能优化只有同一 writer 内的 authoritative hot-range patch:
range 至少覆盖完整 mutable pane,absolute anchor 只来自 tmux pane identity/history_size/rows/cols/alternate/captured count;
已确认旧 history 只能作为同一 mirror store 内的 retained prefix;
cols/reflow、rows 变化导致未知 prefix、pane/alternate 变化、history shrink/clear、absolute discontinuity、结构不稳定或周期 reconciliation 到期时,必须由同一 owner 做 full reconciliation;
hot patch/full reconciliation 是同一个 validator/committer 的两种 commit mode,不是 fallback 或第二真源。
structured send 与 pre-serialized sendText 必须进入同一 accounting owner,记录 bytes/total/error/buffered-before-after/duration/backpressure transition;禁止为统计重新 stringify terminal payload。
网络 RTT/jitter/stall 只调节 producer 的 head/pull/probe cadence;body 到达后 renderer 只允许 next-RAF commit,禁止再加 network debounce。
弱网性能闭环必须使用主链外透明 TCP byte proxy:只允许延迟、限速、暂停读取、周期 stall、显式断连;禁止解析、重写、压缩或裁剪 WebSocket/terminal payload。代理测试必须证明双向字节完全等价,并用真实 control/session WebSocket 统计 inactive body bytes、slow/healthy cadence 和 final revision。
buffer-head 只允许更新 head metadata / cursor metadata / planner 输入
只有 buffer-sync apply 可以触发正文 body repaint
buffer-head.latestEndIndex 只表示 daemon mirror 的 authoritative tail absolute index(bufferStartIndex + bufferLines.length),不是 row-level body freshness。诊断 input/tail 行旧内容时,禁止用 latestEndIndex 或全局 localRevision == daemonHeadRevision 直接证明可见非 gap 行已新鲜;必须比较 source row / payload row / client sparse row / DOM row。
稀疏 buffer-sync 只覆盖 payload 中的 row/range。若一个可见非 gap 行曾漏刷,后续单行 status/footer sparse patch 可能推进全局 buffer revision 并掩盖该旧行。此类问题必须加“source row cleared once -> client missed non-gap row -> later tiny sparse patch -> DOM still stale”的黑盒 gate,再改 sparse apply / visible-window repaint authority;禁止用 QuickBar、reconnect、header repaint、DOM 清空补偿。
全屏 TUI 刷新必须比较完整 cell truth,而不是只比较文本和 absolute index:同一行文字不变但背景色、flags、style、cursor 相关 cell metadata 变化时也必须 repaint。回归 gate 要包含 fullscreen/status-row 场景,证明同 bufferStartIndex / 同文本 / 新 revision / 新 cell style 会更新 DOM。
terminal body 问题禁止用页面 chrome 补偿:网络 banner、状态栏、debug overlay、quickbar 只能是 UI projection owner。若出现“两个状态栏、一个旧状态”或状态栏挤压导致布局变化,修 terminal-page-shell-ui / TerminalPageDebugOverlay 的单一投影与 fixed overlay,不得改 daemon/buffer/renderer truth 来迎合 chrome。 daemon 不得改写 buffer cells 本身
包括但不限于:cursor paint、reverse 注入、样式补丁、局部重写
若需要传 cursor truth,必须走独立元数据 ,不能写回 lines[].cells[].flags
文件传输这类 session 元数据也一样:远端默认目录必须来自 daemon 读取 tmux 当前 pane_current_path;client 不得拿 process.env.HOME / 本地 env 冒充远端 cwd
clientSessionId 是 client-owned session identity
sessionTransportToken / session-ticket 是 attach-only wire material
它们可以留在协议兼容层
但不得成为 daemon 业务真相
client 侧也不得把 sessionTransportToken 放进长期 transport runtime store;它只能是 handshake 期间的临时 attach 材料 若兼容层仍需要握手关联字段,优先使用:
openRequestId = client-local open intent correlation
sessionTransportToken = daemon one-shot attach proof
禁止再把 clientSessionId 放回 daemon token owner / primary wire correlation 语义
daemon terminal core 的代码组织 也必须收口:
server.ts 只保留 transport/http glue
mirror lifecycle / live sync / attach / input orchestration 必须下沉到独立 terminal core 模块
file list / mkdir / download / upload / remote screenshot / attach-file binary / paste-image binary 也必须下沉到独立 runtime
禁止一边说“daemon 不关心客户端”,一边把 terminal core 业务散落回 server.ts
否则旧行会作为“未覆盖但保留”的本地 truth 永久存在,表现为大面积刷新时旧内容跟着 buffer 上移
buffer-sync 的 in-flight / pull bookkeeping 只是 transport bookkeeping ,不是 buffer truth;active tab 重新进入、resume、reconnect 时不得让旧 bookkeeping 永久挡住新的 head-first 请求链
session transport 的重建真相不能来自“安静时间”:lastServerActivityAt 过旧、缺 pong、缺 head、pong-only traffic 都不得让仍为 WebSocket.OPEN 的长连接过期。active tab 恢复 / 重新进入 / tick 只能在原 transport 上 request-head / ping 观测;只有物理 close/error、send 抛错、daemon 不可达、用户显式关闭、或 tmux/session target 事实变更,才允许重建。
session transport target identity 必须包含 route 语义。bridgeHost:bridgePort:authToken 不足以判定 same target;同 daemon 的 Tailscale/WS、rtc-direct、TURN rtc-relay、Relay endpoint candidates、transportMode、daemon/relay identity 必须影响 target key,Relay lastSeenAt 这类 freshness metadata 不得影响 key。否则会复用错误 OPEN socket,出现有上下行速率但可见 body 不更新。 transport topology 也必须冻结:
daemon 只认 transport connection truth
daemon 不认 clientSessionId、不认 control/session 两级客户端状态机
daemon 不负责客户端 auth 复用策略、session 恢复策略、active transport 选择策略
客户端如果需要长期复用长链接,这是客户端 transport owner 的职责,不得下沉到 daemon 变成客户端状态机
active / inactive 只影响“是否继续取数”,不影响 logical session / transport 身份:
inactive tab 不主动高频拉 head/range
但不是 关闭 session
也不是 关闭 transport
server 普通 closed / websocket detach 只表示 transport failure,必须进入 retryable reconnect owner;terminal 终态关闭只认显式业务真相,例如 tmux_session_killed。禁止把普通 closed 映射成本地 tab/session closed,否则 WebSocket 断开会卡死不重试。
drawer 中 remote-only session 是 daemon catalog truth,不是本地 open-tab truth;关闭必须走 tmux control owner(killTmuxSession 后刷新 catalog),禁止把 remote:* id 交给 local open-tab close。 reconnect / resume / foreground/background / tab active 都是客户端逻辑
daemon 不能持有这些状态机
daemon 只暴露稳定 mirror read/write 接口和基础 transport 接入能力
remote screenshot 也属于 UI shell / session control 闭环:
UI 必须能区分 capturing -> transferring -> preview-ready
capturing / transferring 都必须有显式失败边界 ;不允许无限 spinner
客户端不得 在收到远程截图后直接自动落盘并宣称成功
正确动作是:先预览,再由用户显式 save / discard
QuickBar 工具语义也必须固定:
文件 = 本地文件选择并上传到当前 session
图片 = 本地图片选择并上传到当前 session
同步 = 打开远程文件同步页 / FileTransferSheet
截图 = 远端截图预览流
session schedule 也必须保持 daemon 单真源:
maxRuns=0 表示无限次,默认 3
firedCount / endAt / stop condition 只能由 daemon 维护;client 只编辑和展示
若为了审计 buffer/render 真相新增 debug UI:
只能做 观测 :如绝对行号、当前 follow / reading 模式、拉取/刷新状态
debug UI 不得反向驱动 buffer manager / renderer / daemon 行为
active tab 持久化也属于 app-shell truth:
ACTIVE_SESSION 是最后激活 tab 的唯一持久化真相
每次 tab 激活都必须立即写回 ACTIVE_SESSION
冷启动 / 恢复只允许按 ACTIVE_SESSION 恢复 active tab,ACTIVE_PAGE 只决定页面种类,不得反向覆盖
若 QuickBar 自己的 textarea / sheet 抢到 DOM focus,只允许暂停 terminal ImeAnchor 路由;不得 把 QuickBar overlay / floating composer 使用的 keyboardInsetPx 清零,否则会被输入法盖住
Android terminal 原生输入若走 ImeAnchor,则 ImeAnchor editable / composing / selection 必须是单一真相 ;组合输入期间不得一边让 IME 持有 composing state,一边又由插件自行清空/改写 editable 造成第二语义
ImeAnchor 的 InputConnection 也必须服从这条真相:commitText / finishComposingText 不能跳过 super 直接短路返回;否则 framework editable/selection 不更新,真机会出现 输入法底部预编辑光标错位 / caret 乱飞 mirror-fixed 下,UI shell 若启用横向查看:
自动关闭左右滑切 tab
单指横滑只服务于 renderer horizontal pan
不允许一次手势里同时尝试切 tab 与横向平移
horizontal offset 尚未到 0 时,能真实改变 offset 的横滑必须由 renderer 独占并截断父级 touchmove/touchend
只有 offset 在手势开始前已经为 0,左缘右滑才可进入 drawer owner;只缩热区不能解决子级 pan 与父级 drawer 同时解析同一 touch 序列的问题
mirror-fixed 下若当前客户端并没有独立 horizontal pan 手势链在生效:
禁止把左右滑切 tab 直接禁用成“无交互出口”
此时单指横滑仍归 UI shell swipe surface 所有,继续用于切 tab
adaptive-phone 下若保留左右滑切 tab,该手势也必须属于 UI shell interaction surface :
renderer 不得自己持有 swipe gesture state machine
renderer 不得自己决定 tab navigation 命中
renderer 只允许上报 viewport / scroll / input 真相
tab swipe 的 owner 只能是 shell 包裹层或独立 interaction block
createSession(connect:false)
不得
active re-entry / active tick 若遇到 closed/error/unavailable session,生命周期链只能 skip 或读取当前 live transport;不得 自动 reconnect。foreground resume 不再是独立语义,必须映射成 explicit-resume,由唯一 reconnect/open owner 判定是否重新打开 daemon session。 adaptive-phone 是 daemon adaptive width lease,不是 renderer 后处理:
client 只上报 measured cols 与 transport heartbeat。
daemon 唯一 lease owner 聚合 active adaptive subscribers 的最窄 cols,并请求 tmux resize-window -x 进行真实重排。
客户端退出、transport detach、心跳过期后必须重算;最后 lease 过期/消失必须恢复/释放 tmux width ownership。
红测必须覆盖最窄优先、holder 消失重排、最后 lease 过期释放、非 adaptive 不注册 lease,并反向证明只有 adaptive owner 能动 tmux。
adaptive-phone attach/resize 的 invalid cols 必须在进入任何 throwing geometry normalizer 前被显式拒绝:缺失 / NaN / <=0 cols 返回 adaptive_width_cols_invalid,daemon 进程必须继续存活。测试 helper 禁止用 cols || default 掩盖真实 daemon 的 strict normalizer;invalid-input 红测必须使用 strict normalizer,并最好补真实 WebSocket probe。
mirror-fixed 是 client render crop / pan policy:不得注册 adaptive width lease,不得改变 tmux width。
adaptive-phone 的 tmux side effect 必须单点化:只允许 terminal-mirror-runtime.ts 的 adaptive lease owner 执行 resize-window -x 和 final release 的 set-window-option -u window-size;禁止 daemon-start、renderer、UI、foreground/background、普通 resize/attach 分支散落执行。
daemon 可以向 tmux 发用户/协议请求(如 send-keys、create/kill/rename session),但 adaptive width 不是 tmux 请求。请求结果不能被 daemon 预测写入 mirror truth;mirror.rows/cols/bufferStartIndex/bufferLines/cursor 只能由 tmux 回读 / capture owner 写入。
daemon 存在性检查 / mirror capture / debug probe 只能读取 tmux truth,不得为了客户端显示方便改用户 tmux option。特别禁止 assertTmuxSessionExists、control runtime、capture runtime 执行 set-option ... alternate-screen off 或其它 window/session option 变更;若历史 daemon 留下 alternate-screen off,只能作为一次性现场清理 set-window-option -u -t <target> alternate-screen,代码真源必须物理删除副作用并加 gate。
若 App 首帧就已经持有现存 sessions[],也必须立刻持久化 OPEN_TABS / ACTIVE_SESSION;不能因为“这次不是 restore 分支”就跳过首次回写,否则下次冷启动恢复会拿到陈旧 tab 真相。
若现场是输入区文本对了、但样式和 tmux 不同 ,先不要怀疑 local echo。先用回环证明:terminal 可见内容是否只在 buffer-sync 后变化;若是,再直接比 daemon payload 的 prompt/input row char/fg/bg/flags 。
“输入区 / 光标”专项必须至少有一条红灯门禁 :daemon cursor paint 不得给普通 prompt cell 注入 synthetic reverse style;若这里错,后续任何 IME/renderer 修修补补都会继续假修。
若现场出现 buffer-sync 明明持续收到,但 localRevision/localEndIndex 长时间不前进、client 反复请求同一 3 屏窗口 ,优先查 client 侧 incoming buffer-sync apply 阶段 ;收到即更新本地 buffer truth,不要再叠微任务批处理/延迟 flush 第二语义。
若现场是“有内容但要等补齐才整屏一起跳出来”,优先判 renderer / buffer manager 边界错 ;正确语义是 gap 先空白,补齐后局部重刷
reading-repair / visible-gap repair 的 client 判重与 in-flight cover 必须纳入当前 missingRanges / gap 拓扑语义 ;同一 knownRevision/localWindow/requestWindow 下,只要可见区 gap 变了,就必须允许再次发 repair。否则现场会出现:页面局部空白,手动上下划一下(viewport 改变)后才补刷。
reading-repair / missingRanges 的 daemon 响应必须是 连续 authoritative span ,禁止返回“外层 request window 很大,但 lines 只包含多个非连续 gap 行”的带洞 payload。若一次请求有多个 gap,响应只能返回从第一个 gap 到最后一个 gap 之间的完整行 span,或未来协议显式拆成多个独立连续 buffer-sync;当前 client apply/render 不接受中间带洞窗口,否则会保留旧行并在刷新时闪旧 buffer。
Android / Mac renderer 的 visible-gap repair demand 必须共用 shared buildTerminalViewportDemandWithRepair;平台 view 只传入 local buffer window + gap ranges,不得复制 missingRanges 计算。demand key 必须纳入非空 missingRanges,无 gap 时不发送空数组以保持旧 payload 形状。
Android terminal header 的顶部 inset 必须由 UI shell 提供单一像素真相 ;Header 自己不得再额外叠 env(safe-area-inset-top) 做第二份 safe-area 计算。
terminal 冷启动 / 恢复 tab 时,最后 active tab 真相只能来自 ACTIVE_SESSION ;ACTIVE_PAGE.focusSessionId 只描述页面焦点,不得反向覆盖已恢复的 active session。
foreground resume / tab re-entry 时,若 active session 的 ws.readyState === OPEN,不得仅因后台静默一段时间就直接重连 ;必须先用同一条 ws 做 buffer-head-request / ping / 必要的 session 状态查询。只有 socket 物理 close/error、用户显式 reconnect、或当前 session 已无可用 ws 且属于 explicit-resume/open,才允许新建 ws。
foreground resume / tab re-entry / network online 时,若 active session 仍卡在同一 CONNECTING socket 或 pending open intent,不得因等待预算超时自动 force-replace 。继续等待同一条 pending ws,并把等待状态显式投影到 SessionContext;不能创建第二条 session ws。网络通断事件不是 reconnect 依据,只是触发现有 ws 的协议探测。
reconnectRuntime.connecting / stale reconnect bookkeeping 只是 client 本地编排观测,不是 transport failure truth;foreground resume / tab re-entry / online 不得因为它返回 reconnect 或创建第二条 ws,只能显式显示等待或在 socket 物理 close/error 后进入真正 open/reconnect owner。
force-replace API 不属于 lifecycle/probe/input/foreground/online 恢复路径;禁止把 stale activity、missed pong、pong-only、foreground resume、tab re-entry、online 事件映射成 cleanup old socket -> fresh connect。物理 close/error 进入唯一 reconnect/open owner,不能由 UI 或 buffer 层清理 live socket。
任何 buffer-head-request / buffer-sync-request 若允许调用方显式传 ws,都必须先校验:该 ws 仍是当前 session 的 active transport socket ;旧 superseded socket 只能被物理关闭或忽略,绝不能继续拿来发 head/range 请求污染当前 transport 真相。
transport 生命周期门禁不只覆盖 onopen/onmessage/onerror/onclose;凡是“旧 ws 回调里继续触发 request/head/probe”的路径,也必须有同样的 active-socket gate,否则 stale transport 仍会在写侧继续推进错误状态。
若 tmux capture / mirror canonicalize 链路可能收到 extended-color ANSI(38:2::r:g:b / 48:2::r:g:b / 38:5:n / 48:5:n)的 colon 语法,进入 parser 前必须先规范化到当前唯一支持的 semicolon 语法;否则颜色会退回 default sentinel,现场表现就是红/绿背景丢失或发灰。 transport/session 生命周期若要改,先问自己有没有违反这四条:
是否又把 per-session ws 当成 transport 真相
是否又让 reconnect 走 cleanup old socket -> fresh connect
是否又让 inactive tab 关闭 session / transport
是否又让 daemon 因 ws close / grace timeout 回收 logical session
若真机出现大块灰条/花屏/光标样式乱飞 ,优先查 compact wire 的 default color sentinel 是否和 TerminalCell 真相一致;当前 app/runtime 里的默认前景/背景是 256/256,不能在 compact encode/decode 里偷偷改成 ANSI 15/0。
若真机出现terminal 默认背景和真实终端背景不一致 ,优先查 renderer default bg sentinel:bg=256 必须绘制为 theme.background,不能映射为 transparent。row / cell wrap / gap fill 也必须主动 paint terminal theme background,禁止让外层容器或页面背景替代 terminal 背景。
若 bg=256 -> theme.background 修复后真机仍“无变化”,下一步必须用 WebView DevTools 读取 live localStorage['zterm:bridge-settings'].terminalThemeId、.wterm computed background、最近可见 row/cell computed background。若 active preset 自身的 theme.background 是纯黑,继续改 renderer 是错路;应修 shared theme preset 真源,并用 live DOM 证明 computed background 变化。
若真机输入或刷新时先闪一帧旧 buffer 再被新 buffer 覆盖 ,优先查 render gate 是否在 buffer truth 之后又加了 per-session debounce。正确语义是 buffer-sync apply -> schedule RAF -> RAF 时读取当前 live buffer 一次;render gate 只做 frame coalescing,不能再消费 network/transport cadence,也不能保存会晚发布的旧 scheduled snapshot。
若真机出现光标颜色不对 / 光标像普通反显文本 / 光标样式污染邻格 ,先查 Android client 是否越权改了 cursor 样式 ;renderer 只能回显 payload,不能再造第二套 cursor 视觉语义。
若现场看起来像“正文解析错了”,先把 terminal body 与 IME/editor overlay 分开审;底部灰条/编辑条不属于 daemon buffer truth,不能直接当成 compact wire 正文错误。
compact wire 的正文门禁必须覆盖 ANSI + CJK + reverse + bg span + 中间空格 ;只有 body parity 红灯以后,才允许改 contract/renderer,禁止凭截图先回退 codec。
若 iTerm2 与 ZTerm 出现局部错位/灰块,且混合 ASCII/CJK/色块区域异常,优先查 renderer measureTerminalViewport() 的 glyph probe 是否把单个字符测成接近整屏宽;cell 宽度必须拒绝异常整屏测量并回退到字体估算,禁止在页面层补第二份宽度逻辑。
网络变化后 App 卡死但杀进程恢复时,先查 lifecycle 是否在前台 online 事件恢复 active tab transport;恢复动作只能复用现有 active resume/audit/follow reset 主线,hidden online 不恢复,禁止扫所有 session。
若现场是“点击快捷栏空白区弹出输入法 / 键盘起来后快捷栏被盖住”,先判 UI shell/QuickBar ,不要误把它当成 renderer 或 buffer 问题;必须先补 shell 区域阻断与 keyboard lift 的红灯测试。
tmux send-keys -l 的 argv 可接受不等于 PTY 长输入稳定;必须用 source SHA-256 与 tmux target file SHA-256 自动比较,并把 byte-exact source/target gate 与 mirror recovery gate 分开
长 payload 测试要先单独发送 stty -echo / sink prelude,并等待 ready marker 后再流式发送 body;禁止把关闭 echo 与大 payload 混在同一 burst 后把回显洪泛误判成 input byte loss
若 source/target digest 通过而 mirror recovery 失败,归类为 mirror/read-render 恢复故障,不得报告成输入丢字 若现场表现为“偶发先出现错误帧内容,下一帧又恢复正常 ”,优先审 render truth 边界是否持有 live buffer 的可变引用 :
SessionBufferState -> SessionRenderBufferSnapshot 必须产出 immutable render snapshot
lines / gapRanges / cursor 不得把 live buffer 引用直接交给 renderer/store
render store 读取到的只能是独立 render truth;否则后续 patch/merge 复用 row/object 时,会把已准备绘制的上一帧污染成“短暂错帧”
若 wire payload 带 frameChunkCount > 1,还必须先审 client buffer owner 是否逐 chunk publish:
frame chunk 只允许进入 bounded assembly state,不得直接 commit local buffer 或 schedule renderer
全部 chunk 必须按 absolute row 验证无洞、无重叠、无冲突,完整覆盖 frame window 后一次 apply/commit/render
红测必须在每个 chunk 到达后读取 local/render truth,证明完成前始终保持上一完整帧,完成后直接切到新完整帧;禁止只断言最终结果正确
generatedAt 只作为 frame identity,不得替代 revision/absolute index 做 freshness 排序
same revision 不同 frame identity 混入时,必须拒绝本次 interleave、清除 poisoned incomplete assembly,并把错误与 exact frame repair range 写入独立 resource.client_buffer_frame_assembly truth;repair 未实际写入 wire 时保持 pending,只有 dispatch 成功才改为 dispatched,每个 revision 最多 dispatch 一次
frame assembly ref 是独立于 sparse buffer 的 client 必需资源;生产 caller 漏注入必须在类型/构建期失败,禁止 optional ref 让 chunk 永久 pending,禁止用 same-resource edge 掩盖 assembly 到 sparse apply 的真实资源边
较新 frame 尚未完整时,迟到的较低 revision payload 无论是 chunked 还是 unchunked 都必须拒绝且保留较新 pending frame;禁止 passthrough 清掉 assembly 或发布旧 body
frame repair 禁止把非法 revision 修成 0;same-revision interleave 只修原 pending frame range。无合法 revision 时等 authoritative live head;成功 repair 后用有界 per-session revision ledger 跨后续成功 revision 保留 dispatch 事实,迟到 malformed payload 不得对旧 revision 二次 dispatch;新 frame supersede 旧 pending frame 时清掉旧 repair error,但保留 ledger
frame assembly 必须服从 shared protocol 的 span/chunk/retained-byte/lifetime 硬上限;body ingress 写 map 前拦截容量越界,buffer-head cadence 主动到期并释放 incomplete frame。禁止把“等下一 chunk/等 session close 清理”当生命周期边界
查真实 socket 路径中的 wire normalizer;它必须原样保留 authoritative frame identity。新增 frame 字段后只改 protocol/assembly 而漏改 normalizer,会让所有 chunk 被误判成 passthrough,是“单测绿、真机仍闪”的首查项
tab switch、inactive drop、socket generation cleanup、reconnect 只清 incomplete pending chunks;revision-reset expectation、frame error truth 和 bounded repair ledger 在同一 daemon revision epoch 内跨这些生命周期保留。首次 authoritative lower head 是 epoch 边界,必须在任何 repair dispatch 前清 pending/error/ledger;同一 epoch 重复 lower head 不得再次清 ledger;explicit local session destruction 删除两类 resource
若现场表现为“底部固定灰条/固定行在内容更新时持续上移,旧内容没有被当前位置正确替换 ”,必须先补 renderer 红测再修:构造同一 absolute row index / 同一 viewport bottom 下行内容发生变化的 case,先证明当前 DOM 复用了旧行或错位上移;红测变绿后把该测试纳入 TerminalView.dynamic-refresh / renderer 回归。禁止先凭截图改代码、再补测试。
若 renderer/client gate 过了但真机仍出现 TUI/input 旧行漏刷或上移 ,下一步必须测 daemon capture 主入口而不是只测 helper:captureMirrorAuthoritativeBufferFromTmux() 必须实际调用稳定化主线,覆盖 transient half-frame 不发布;同一 mirror 的 totalAvailableLines 必须以当前 mirror end 为单调下界,避免 alternate-screen 短可见窗口把 absolute tail 拉回 pane height。
若现场表现为“临时错误/短暂不可用导致 tab 自动关闭 ”,必须先补 session lifecycle 红测再修:tmux_session_unavailable / 网络短断 / handshake 临时失败只能进入 retryable error/reconnect,禁止发 SESSION_STATUS_EVENT(type='closed'),也禁止触发 open-tab prune;只有明确 terminal close 语义才允许进入 tab close 链。
若现场要排 Android IME 抬高 / 安装升级 timeout,debug 观测链也必须服从唯一真源:只允许 client snapshot source -> collectClientDebugSnapshot -> active session WS debug-snapshot -> daemon store 这一条链;禁止再开第二条 relay/debug transport 或散落页面内临时上报。
DebugRegistry duplicate registration 是 fail-fast;测试文件只要 import 真实 client-debug-snapshot 并 render 真实 TerminalPage/App,resetClientDebugSnapshotForTests 与 cleanup 就必须放 file scope。把 hooks 放在某个 describe 内会让后续 describe 残留 producer,报 duplicate debug producer: terminal-page,此时先修测试隔离,不能把生产 fail-fast 改成静默替换。
explicit terminal input 不能只依赖 lifecycle/heartbeat 去发现远端回显;input payload 必须同步发出,首个未完成 pendingInputTailRefresh 的 buffer-head 请求必须放到 coalesced microtask,后续 burst input 在 pending 清除前合并,禁止每键强制刷 head 或把 head 请求绑回 key event stack。
Android 物理键盘不能依赖 DOM textarea focus 路径;native ImeAnchor key 必须直接走 shared terminal keyboard resolver 并写入 active session。plain letter 留给 editable/IME 文本路径,Ctrl/Alt 组合键和方向/Esc 等特殊键走硬件 key path;红测必须让 allowDomFocus=false 时 Ctrl+C 仍到达 terminal input。
Android IME 改变 shell geometry 时,IME 只能影响外层容器 / quickbar 位置,不能进入 TerminalView 内容 viewport 计算链;页面层必须冻结 terminal content geometry,禁止用 IME 高度生成 renderer layout refresh / viewport demand,也禁止触发 upstream onResize 改 tmux rows。
大面积刷新若遇到 revision-gap sparse payload,client 必须拒绝合并错误 sparse body 并请求 authoritative tail;同时应 schedule 当前稳定 local buffer 的 render commit,把已确认 truth 重推给 renderer。禁止把 sparse payload 当 fallback 成功,也禁止等待 tail 期间让 renderer 没有任何稳定帧信号。
大面积刷新若是 contiguous sparse tail jump,post-apply visible-gap repair 必须先判断是否已有本地窗口且旧 visible range 是否贴旧 tail:已有窗口且贴旧 tail 代表 follow,应改用新 tail 默认 visible range 查 gap;初始 sparse 首帧或不贴旧 tail 代表 reading/未定,必须保留旧 visible range,禁止吞掉 renderer 后续 reading-gap 请求或把用户历史阅读位置重解释成新 tail。
高 pane 新首帧若 cursor 可见、cursor 在 tail viewport 之外、且 tail window 全部为空白,follow anchor 可以落到 cursorRowIndex + 1,让首屏显示 prompt 而不是空白下 pane;tail window 有真实内容或 cursor 已靠近 tail 时仍必须保持 tail follow。cursor metadata 不能作为 body repaint 源。
Android shell shrink 黑屏先查 client.terminal_shell 的 flex geometry:承载 PaneStage 的 absolute stage wrapper 必须是 display:flex; flex-direction:column; min-height:0,否则 flex:1 不会把稳定高度传给 pane/.wterm,最终表现为 renderer host 高度 0 或过期。Shell memo comparator 必须覆盖所有影响 absolute top/bottom 的 measured inset props;漏掉 terminalChromeTopPx 会让容器位置停在旧值。修复必须留在 shell geometry owner,不能在 buffer manager 伪造 viewport 或加渲染 fallback。
reading 遇到大面积 buffer shift 时,只有上一帧 padding delta 超过 viewport 高度才把 native scroll 映射到当前 effectiveRenderBottomIndex;小 padding 变化不得重锚,避免普通历史读取在刷新时跳动。没有上一帧或 viewport 高度未知时保持原位置。 若现场表现为“能输入但画面停在旧 buffer / session drawer 卡 connecting / 杀 APP 秒连但后台回来不刷 ”,先判 client 读链恢复断裂:
WebSocket 是客户端物理 transport,不是 daemon 持有的永久 per-session 真源;daemon 真源是 tmux mirror
ws.readyState === OPEN 或 input 可写只证明写路径可用,不证明 buffer-head -> buffer-sync -> local apply -> render commit 已恢复
explicit resume / foreground return / active session change 必须 forceHead + markResumeTail,即使本地已有旧 buffer
stale pending transport-open bookkeeping 不是 waitable state;唯一 reconnect owner 清 pending intent/control socket 后 rebuild same target,fresh pending 才等待
禁止在 UI drawer、input runtime、renderer 增加补偿重试;测试锁 session-context-activity-runtime、session-context-lifecycle、SessionContext.ws-refresh 和 architecture boundary gate
若现场表现为“抽屉切 session 先显示连接失败、再点一次又能连上”,先审 terminal.transport_lifecycle 的 retryable reconnect projection:retryable handshake/control attach failure 和 scheduleReconnectRuntime() 的 retryable reconnect start 都只能保持 reconnecting 并继续 retry,禁止 emitSessionStatus(..., 'error') 投给 UI。只有 nonretryable/auth rejected、auto reconnect explicitly blocked、或显式 retry exhausted 才能投 terminal error;不要在 drawer/UI 加二次过滤。
前台恢复 / 自动重连的 reconnectAttempt 是标准流程内部计数,不是用户可见故障事实。只要状态仍是 reconnecting,顶部只能显示中性进度(如 正在重连 / 正在同步控制通道),不得因 attempt 次数、probe timeout、RTC data channel closed 等中间失败显示错误浮窗。只有 offline、typed terminal error、auth/nonretryable failure 才能进入错误 banner。
drawer/tab switch 是显式用户 resume intent,必须一路传到 SessionContext.switchSession(..., { refreshSource:'explicit-resume' })。只在 open-tab 层标 switchRuntime:'explicit-resume' 不够;如果 provider facade 固定转成 active-reentry,同一次用户选择会被拆成两套资源语义。内部 lifecycle active change 才默认 active-reentry。
terminal input 只允许消费当前 SessionTransportResource.socket 并同步写入;它不是 reconnect/open-intent owner。禁止在 input runtime 里调用 reconnectSession、probeOrReconnectStaleSessionTransport、shouldReconnectQueuedActiveInput 或 stale pending-open 补偿,也禁止 input owner 直接 ws.close() / 替换物理 transport。缺 transport / pending-open / backpressure 必须显式 drop/debug,transport close/replace/reconnect 恢复唯一 owner 是 terminal.transport_lifecycle。
active buffer-head 若早于 renderer visible range 到达,buffer owner 必须按 daemon head bounds 直接 bootstrap 当前 tail 的 buffer-sync body;默认拉多屏 tail 并受 availableStartIndex/latestEndIndex 约束,避免最后一屏为空白时把健康 session 误渲成黑屏。非 active / 无 visible demand 不拉正文。禁止把 renderer layout 当首包前置条件,也禁止用 hidden cache window 冒充 visible fetch window。
buffer-head 只能更新 head/cursor metadata;不得触发正文 render commit。正文 repaint 只来自 buffer-sync apply,否则会把旧 body 重新投影成短暂闪屏。
buffer-head 也不得提前清理 pending body request 权威。若 tail-refresh / reading-repair 的 buffer-sync-request 已发出,head 先到、同 revision body 后到是合法顺序;lastSyncRequestAtRef 只能由 body apply/drop 路径消费。否则后到 fresh body 会被误判成 unsolicited same-revision stale overwrite,表现为输入框/可见旧行不刷新。
Buffer timestamp 只许做观测元数据:client request requestedAt、daemon response generatedAt/requestSentAt 可以进 debug 和黑盒 freshness 证据,但不得替代 revision + absolute row index 排序,也不得作为 accept/drop body 的业务真相。 若 Android 升级/安装后仍看到旧 terminal/session 内容,先区分 旧进程残留 和 新包冷启动真相 :
adb install -r / 系统安装器更新包后,前台旧 WebView 进程可能继续显示旧 JS/runtime projection;这不是 OPEN_TABS / TERMINAL_LAYOUT 一定复活
先记录 dumpsys package 的 versionCode/lastUpdateTime、pidof com.zterm.android、dumpsys window focus、UI dump 文本;再 am force-stop 后冷启动对比
如果 force-stop 冷启动后旧内容消失,修复 owner 是 native App update process handoff:系统安装器拉起后旧 App 进程必须退出;禁止清 app_webview / Local Storage / OPEN_TABS 当 workaround
adb install 冷启动只证明新 APK 可运行,不证明 App 内 AppUpdatePlugin.downloadAndInstall() handoff 已闭环;必须单独验证插件路径或明确 L5 缺口
若 Android 滚到历史区后点击键盘无法唤出 IME,先审 input intent -> renderer follow 边界:
terminal click / quickbar keyboard show / blur-to-keyboard 必须先让 renderer 回到底部 follow,再调用 native IME show/focus
IME 显隐决策必须读取 native ImeAnchor.getState()/keyboardState 的 keyboardVisible,不要用本地 requested flag 或 inset 猜
IME 只移动 QuickBar/UI shell,不改变 TerminalView 内容 stage,不触发 upstream resize,不改 tmux rows/cols
真机脚本必须确认 app surface 可见;keyguard/SystemUI 拥有焦点时只能标 L5 阻塞
mirror-fixed 横向滑动只能是 renderer projection:.term-grid 可按 session 记住水平 offset 并做 translateX(-offset);禁止把横滑映射成 daemon resize、tmux width change、adaptive lease 或 buffer/mirror truth 修改。adaptive-phone 不响应横向 pan,它的宽度变化只走 daemon adaptive lease owner。
mirror-fixed 横向手势归属:只有 offset 已为 0 且起点在左缘热区内的右滑可交给 drawer;positive offset 右滑、非左缘右滑、右侧/中间横滑都必须由 TerminalView 消费并 stopPropagation(),即使 offset 已经被 clamp 到 0、视觉上不能继续移动。禁止只 preventDefault() 后让父层 touchend 解析成 drawer previous。