T3 Code 源码拆解
客户端

14 · Web 控制面

React、TanStack Router、Effect Atom、Zustand 与 Ghostty 终端如何围绕共享运行时协作。

Web 是主要控制面,不是状态权威

apps/web 使用 React 19、TanStack Router、Effect Atom、Zustand、Lexical 和 Ghostty/WASM 终端。组件数量与体积都很大, 但架构上始终要守住一条线:Server 投影决定 domain truth,Web store 负责同步缓存、派生与交互状态。

启动与路由模式

main.tsx 根据宿主选择 history:

  • 普通浏览器使用 browser history;
  • Electron renderer 使用 hash history,避免自定义 scheme/本地文件刷新时路由交给错误的 HTTP server。

router.ts 与生成的 route tree 组合页面。 核心 URL 显式包含 environmentId 和 threadId,例如 _chat.$environmentId.$threadId.tsx,这让深链可以定位远端环境, 而不是默认“当前连接只有一个”。

Provider 树与共享运行时

Web 启动大致组合:主题/错误边界 → Clerk(云访问时)→ Effect runtime/Atom registry → connection runtime → Router。 apps/web/src/connection/runtime.ts 将浏览器存储、WebSocket/HTTP、online 事件等 platform layer 注入 client-runtime。

Web 状态分层
layout / selection / draft TanStack Router Route Screens Chat / Sidebar / Review / Terminal Effect Atoms - server facts Zustand / React local UI state client-runtime commands + snapshots Effect RPC / HTTP T3 Server

Effect Atom 适合和 Effect query/runtime 组合的远端事实;Zustand/React state 更适合面板宽度、临时选择、未提交 composer 等局部交互。 选择多种状态工具不是问题,问题在于同一个事实是否有两个所有者。

Thread shell 与 detail

侧边栏需要大量 thread 的轻量字段,聊天页只需要当前少数 thread 的完整消息/活动。把所有 detail 塞进全局列表会让启动和每次事件都很重。

T3 Code 区分:

  • thread shell:标题、状态、project、排序/置顶/稍后等列表字段;
  • thread detail:messages、activities、turns、approvals、checkpoint 等深内容。

client-runtime/state/shell.ts 与 threadDetail.ts 分别装载和 reduce;Web 路由按需获取 detail,并做 retention, 离开页面后不必永远把所有历史留在内存。

ChatView 为何巨大

聊天视图同时处理:

  • 交错的 user/assistant messages 与 activities;
  • streaming segment 和自动滚动锚点;
  • approval/user input;
  • proposed plan 与 implement action;
  • composer、附件、路径搜索、模型和权限模式;
  • subagent/collaboration 展示;
  • checkpoint/diff/revert;
  • terminal/preview/right panel;
  • mobile/desktop 响应式差异。

巨型组件是维护风险,但它也揭示了 UI 的真实耦合:滚动、流状态、composer 高度和活动展开会相互影响。合理重构应沿稳定状态所有权切片, 不能只是把 JSX 搬进 30 个文件后仍共享一堆隐式 refs。

流式渲染与稳定身份

Projector/ingestion 已合并 token,Web 仍要避免每个 delta 重排整棵消息树。消息、segment、activity 使用稳定 ID;组件按 ID 订阅 atom 派生, 列表使用稳定 key。滚动策略要区分:

  • 用户仍在底部:新 delta 自动跟随;
  • 用户向上阅读:保留锚点并显示“跳到最新”;
  • 图片/代码块异步改变高度:基于锚点修正,而非每次 scrollTo(bottom)。

这些属于展示逻辑,不应反向修改 Server 的 message lifecycle。

Lexical Composer

Lexical 支持富文本输入、路径 mention/搜索、附件与结构化内容。发送前必须转换成 contracts 接受的稳定 payload, 不能把编辑器内部 node JSON 当长期协议。草稿是客户端状态;成功接受后再清空,网络失败时保留并用同一 command ID 重试。

浏览器内终端

Web 使用自定义 Ghostty terminal WASM surface,而 PTY 在 Server。典型路径:

keyboard → terminal input RPC → Server PTY → byte stream → client buffer → WASM render

终端流与 orchestration event stream 分开,因为字节吞吐、背压和重放语义不同。客户端维护 buffer replay,连接恢复时请求合适基线, 不把每个 terminal byte 写进领域事件日志。

Preview 与桌面增强

普通 Web 可以显示可达 preview URL;Electron renderer 还能通过 IPC 使用 webview、元素拾取、画中画和浏览器自动化宿主。 Web 组件通过 capability/platform adapter 检测能力,不应直接假设 window.electron 始终存在。

这种模式允许同一 UI bundle 在浏览器与桌面运行,同时把高权限操作限制在 preload 暴露的窄 IPC。

Cloud auth 与本地 auth

云连接使用 Clerk 辅助身份流程,最终执行环境仍有自己的授权/session/ticket。authBootstrap、connect/pair routes 和 connection runtime 协调两层,UI 需要区分“未登录 T3 Connect”和“已登录但目标环境未配对”。

性能风险清单

  • Sidebar 数千 thread 的排序/分组应 memoize 并使用 shell pagination;
  • Chat detail 不应被任何无关 thread shell event 整体重算;
  • stream reducer 保持结构共享,避免复制整个环境 state;
  • 终端/preview 高频事件与 React domain state 隔离;
  • 大 diff、markdown 高亮和搜索可按需加载;
  • 路由离开后释放 detail subscription/retention lease。

源码锚点

可迁移经验

  • URL 显式携带 environment identity,多后端产品才有可靠深链。
  • 列表 shell 与当前对象 detail 分层,是大规模实时 UI 的基本模型。
  • 服务端事实与局部交互状态可使用不同工具,但所有权必须唯一。
  • 高频终端/预览流不要强行进入通用 React/领域事件管线。
  • 跨浏览器/Electron 复用 UI 时,通过 capability adapter 暴露平台能力,而非到处读全局变量。

On this page