16 · Expo / React Native 移动端
移动端如何复用连接与领域状态,并用原生模块解决终端、Diff、Markdown 与输入性能。
移动端不是缩小版网页
apps/mobile 使用 Expo、React Native 0.85、React 19.2 与 React Navigation。它与 Web 共享 contracts、client-runtime 和领域语义,
但不共享 DOM UI。原因很实际:导航、键盘、安全存储、后台生命周期、长列表和终端渲染都具有原生约束。
App.tsx 组合 Atom registry、云 auth、connection runtime、
主题与 native navigation。它首先是一台“可能随时休眠、换网络”的远程控制设备。
共享与不共享
| 共享 | 移动端独有 |
|---|---|
| RPC/contracts 与错误 Schema | React Navigation screen stack |
| Environment catalog/registry/supervisor | Secure storage、AppState、network bridge |
| Thread/project/session reducers | Swift/Kotlin 原生渲染模块 |
| 命令、审批、checkpoint 语义 | 键盘、safe area、手势、触觉反馈 |
| Relay discovery/auth | Push/前后台与移动网络体验 |
这是一种“headless 运行时共享”:重用状态机和协议,不强求组件跨平台。
移动连接的额外压力
手机网络在 Wi‑Fi/蜂窝/VPN 间切换,操作系统会冻结 JS,后台 socket 可能被静默回收。移动 platform layer 将 NetInfo/AppState 等信号转成 client-runtime wakeup;supervisor 决定 probe 或重连。
移动特定 probe 窗口更短(本快照约 3 秒),目的是在用户打开 App 时快速识别僵尸连接;但离线仍不消耗 backoff attempt。 App 前台恢复时不能直接创建第二条 socket,必须让 registry/supervisor 串行化环境 lease。
Native navigation 与 environment identity
导航参数需要 environmentId + threadId,和 Web URL 一样显式。Screen mount 获取 detail lease,unmount/blur 释放或降低 retention;
后台通知/深链先解析 environment,再打开 thread。不能用一个全局 currentServer 推断,否则用户切换环境时返回栈会展示错数据。
为什么写原生模块
源码包含 iOS/Android 原生实现,用于 review diff、terminal、markdown、composer 等重负载区域。动机通常是:
- 大 diff/markdown 在 JS 线程解析布局会阻塞手势;
- 终端需要高频字节绘制、字体网格和选择;
- Composer 与系统输入法、自动完成、附件交互耦合;
- 原生列表/文本能降低 bridge 往返。
原生模块的输入应是稳定的领域/view model,而不是直接理解 Provider 原始事件;否则每次协议升级都要同时发 iOS/Android binary。
移动终端
features/terminal/ 将职责拆为:
terminalLaunchContext:从 environment/thread 得到连接上下文;threadTerminalPanelModel:面板状态;terminalBufferReplay:恢复输出基线与增量;NativeTerminalSurface:原生渲染;nativeTerminalModule:JS/native 边界;- preferences/theme/menu:纯产品配置。
Buffer replay 很关键:移动 App 恢复时,不能假设在后台收到所有字节;Server/terminal session 要提供可重放 buffer 或新基线。 终端数据不进入 orchestration event log,因为吞吐和保留语义不同。
审批是移动端的核心价值
手机最有价值的场景往往不是编辑长提示,而是查看进度、批准命令、回答一个问题、interrupt 或接收完成状态。 这要求 approval/pending input 已经由 Server 持久化,移动端只是另一个有 scope 的响应者。
双设备同时操作时,第二个响应应收到“已结算/冲突”的领域结果;UI 刷新 projection,不自行覆盖第一个决定。
认证与安全存储
移动端用 Clerk 处理云身份,并在本地 secure storage 保存环境授权材料。Pairing link 的敏感 token 放 fragment,避免被 HTTP server/log 收集; 兑换后保存 session/DPoP key,并为 WebSocket 取得短期 ticket。
日志和 diagnostics 必须使用 client-runtime 的 safe error/trace,避免把 bearer、ticket、pairing proof 或完整 URL 输出到 Expo log。
后台与任务持续性
Agent 运行在开发机 Server,因此手机进入后台不会中止 turn,这是本地执行边界带来的直接收益。移动端不应尝试在后台维持无限 socket; 恢复时基于 Server 事实追上即可。若需要通知,应由可选 relay/notification 控制面发“有更新”信号,而不是把完整代码/消息放进 push payload。
性能与容量
- thread list 使用 shell/pagination,不装载所有 detail;
- message/activity 长列表要稳定 key、窗口化与原生文本优化;
- 大 diff 按需请求,尊重 Server 大小上限;
- Markdown 高亮/代码块避免在每个 delta 全量解析;
- foreground 恢复合并 wakeup,防止 AppState/NetInfo 同时触发两次重连;
- 原生模块失败必须有纯 React fallback 或明确不可用状态。
源码锚点
可迁移经验
- 跨 Web/Native 最值得共享的是协议与状态机,不是 DOM 组件。
- 后台恢复应“探测—有界补发—快照重建”,不要依赖后台 socket 永久在线。
- 原生优化边界接收稳定 view model,避免理解供应商协议。
- 将长期任务留在 Server,移动端做可丢弃、可重建的控制面。
- 多设备审批由领域幂等和权威 projection 解决,不靠按钮 disabled 的本地状态。