Skip to content

Cell - Ta Web ​

ta serve 自带的 Web 客户端:一个 Cell 前端组件(@celljs/react),状态全部来自 @celljs/ta-client,因此和桌面端、移动端共用同一套会话状态机。

bash
ta serve --web     # 启动并在浏览器里打开,已带上 token

安装为应用(PWA) ​

Web 客户端可以装成桌面或手机上的应用:Chrome / Edge 地址栏的安装图标或账号菜单里的"安装为应用",iOS Safari 的"添加到主屏幕"。装好后在单独的窗口里打开,图标是 Cell 的小恐龙。

  • 文件在 src/browser/public/(manifest.webmanifest、sw.js、icons/),构建时放在站点根路径;清单与 Service Worker 只在安全上下文生效:本机(127.0.0.1、localhost)或 HTTPS,用 --host 经普通 HTTP 从别的机器访问时只是网页。
  • 应用记住的是地址(含端口)和 token,所以 ta serve 默认沿用上次的端口和 token(见 @celljs/ta)。
  • Service Worker 不缓存 /api 下的任何东西:页面走网络,失败时用上次的页面;/assets/ 下带哈希的脚本先读缓存,页面换了新构建就清掉旧脚本。ta serve 没在运行时应用照样打开,提示"Ta 服务没有在运行",启动后自动连上。界面文案把 ta serve 称为"Ta 服务",只在需要用户去终端执行时写出命令。

界面 ​

主框架是 IDE 式布局:最左侧是导航栏(logo 兼作侧栏开关、Web 扩展的页面,底部是齿轮和账号菜单),旁边是侧栏(可拖宽、可收起;顶部一排图标标签在 Web 扩展的侧栏视图和对话列表之间切换,标签右边是当前视图的搜索框和 +)(服务器上所有打开的会话,可新建、在指定目录新建、关闭),中间是当前对话,右侧是可收起的面板,放 Web 扩展的卡片。窄屏时导航栏和对话列表变成抽屉,面板浮在对话上方。

一开始,界面上只有对话。你告诉 Ta 想让这个页面做什么、显示什么,Ta 就把它做成一个 pi 包——对话旁的卡片、单独的页面,或者 Ta 的新命令——做完立刻出现在页面上。

界面上新增的一切都来自包里的 Web 扩展,和 pi 的扩展同一套机制、同一套叫法:

  • 包就是 pi 的包。package.json 的 pi 里,extensions 是给 Ta(终端和服务端)的扩展,web 是给这个页面的扩展;没有声明时取 web/ 目录下的模块。包放在 ~/.ta/agent/extensions/<名字>/(项目的 .ta/extensions/)即可生效,也可以用 ta install 安装。
  • Web 扩展是一个不带 import 的 ES 模块,默认导出一个函数,页面用 pi 调用它;返回的函数在卸载时调用。pi 的接口照 pi 的 ExtensionAPI 命名:ui.setWidget(默认在输入框上方,和 pi 一样;placement: 'panel' 放进右侧工作台的卡片标签)、registerView(导航栏里的页面,或侧栏顶部的一个标签;location: 'panel' 是右侧工作台的工具)、registerMessageRenderer(显示 pi 扩展用 sendMessage 发出的自定义消息)、registerTool(页面打开的对话里由浏览器执行的工具)、registerOpener(打开回复里的链接:文件及其行、目录、网页,按注册顺序询问,返回 true 表示已打开)、on(当前对话的事件)、sendUserMessage(发到当前对话;/命令 会执行 pi 扩展的命令)、switchSession(把 ta serve 里另一个打开的会话显示为当前对话)、callHost(调用本包 pi.host 的方法,经 callPackageHost)、reloadExtensions(扩展自己安装、导入或移除了包之后,像回复结束时一样重新加载 Web 扩展和当前对话的 pi 扩展)、ui.showView / ui.useView(显示、查看任意扩展的视图)、ui.Composer / ui.Markdown(和对话一样的输入框与 Markdown)、ui.Dialog(盖住整页的弹窗)、ui.openEditor 的 preview(单击打开的预览标签,标题斜体,下一个预览会替换它,双击或 ui.pinEditor 固定)、ui.setEditorDirty(标签显示未保存圆点,关闭前确认)、ui.useActiveEditor(工作台当前标签)、ui.DiffEditor(Monaco 差异编辑器,并排或内联,可编辑右侧)、ui.confirm 的 confirmLabel / danger、视图的 head(自己的顶栏)、ui.input / ui.editor(向用户要一行或多行文字,取消时为 undefined)、页面的 menu(侧栏列出该页面时悬浮显示的 … 菜单)与 pages(侧栏列出的页面,导航栏不再单列)、ui.notify(页面右下角的通知)、ui.confirm(继续前先确认,返回用户是否同意)、ui.theme、ui.language、ui.supportsWebview(能否用 <webview> 显示网站,Ta 桌面版里可以,其他地方用 iframe)、ui.debugWebview(Ta 桌面版:向这种 webview 里的网页发 Chrome DevTools Protocol 命令,只限网页本身)、ui.desktop.browserData(Ta 桌面版,macOS 和 Windows:用户在应用的原生对话框里同意后,把其浏览器某个个人资料的 Cookie 导入内置浏览器;也可清除内置浏览器数据),以及页面的 React 和 ta serve 的服务:server(会话,pi 的 RPC 命令)、workspace(目录里的文件与命令)、terminals(终端)、packages(包)(类型见 WebExtensionAPI)。
  • 用自然语言做包:内置扩展 @celljs/ta-extensions 带技能 ta-package(终端里的 Ta 也有),并在导航栏加了"扩展"页(一句话做扩展、导出导入),写明了包的格式、Web 扩展接口和验证步骤。当前对话每次回复结束,页面重新读取 Web 扩展:新的加载、改过的重新加载、删掉的卸载;工具有变化时,用同一个会话文件重新打开这段对话(只限本页打开的对话),包有改动时让会话重新加载 pi 扩展。页面重新显示时也会检查一次(包也可能在终端里改)。
  • 稳定:每个扩展注册的东西归它自己,卸载时一并撤掉;一个扩展加载失败或渲染出错,只在它自己的位置显示错误,不影响页面和其他扩展。

设置 → 更多 → 扩展(齿轮菜单的"扩展")列出安装的扩展(每个是一个带 Web 扩展的包,可移除、重新加载);内置扩展列出随 Ta 提供的包和 Web 扩展,可以在这里开关:

  • 我的分身:一个可以用语音或文字交代事情的分身。主对话是一个普通的 Ta 会话,加了角色说明(appendSystemPrompt)和五个客户端工具(start_task 等);每件事作为一个新会话在后台执行,结束后以 <task_update> 回报。形象就是 Cell logo 里的恐龙(DinoAvatar,与 assets/logo.svg 同一路径),只用眼神和小标记表示待命、在听、思考、干活、说话、需要你确认、完成、睡着。

对话与输入框 ​

  • 对话流:一轮里连续的思考、工具调用和中途说明折成一行“已执行 N 步”:进行中展开、只显示最近几步,结束后收起(有失败的步骤时保持展开并标出),最后的正文是回复。布局是纯函数 ProcessUtil(util/process.ts);每条用稳定的 key 并按条 memo,流式输出时只重绘变化的那条。
  • 输入框:Enter 发送、Shift+Enter 换行,输入法合成期间的回车不发送;Ta 工作时 Enter 引导、Alt+Enter 排队追加、Esc 停止,发送与停止是同一个按钮。/ 列出命令,@ 按文件名查找并插入 @路径(服务端 findFiles,不支持时菜单里说明);图片可粘贴、拖入或选择(PNG/JPEG/GIF/WebP,单张 5 MB、每条 8 张以内);排队的消息显示在输入框上方,可撤回到输入框(clearQueue)。逻辑在 ComposerUtil(util/composer.ts)。
  • 头像:TaAvatar 就是 Cell logo,mark 是 1~2 个字符的角标,用来区分智能体(Web 扩展里是 pi.ui.Avatar);每轮回复开头放一个小头像(一轮一次):工作中用眼神表示思考、执行或输出,最后一轮完成时眯眼带勾,出错时带提醒角标;空对话页是大号头像,会眨眼、不时看向输入框。减少动效时全部静止。
  • 状态与提示:分四级(info / success / warning / error),每级有固定的图标和颜色,正文始终用正文色。按归属分三种:全局的连接状态条(主区顶部的胶囊,断线超过 1 秒才出现,显示原因、倒计时和“立即重试”,恢复后显示“已重新连接”2 秒);通知(TaNotices,右下角、手机在顶部,success 3 秒、info 4 秒、warning 6 秒、error 8 秒,有按钮的多留 4 秒,悬停或聚焦时暂停计时,同一条重复出现时合并计数,最多 4 条);就地提示(Callout,留在出事的位置,如一轮回复失败、扩展视图出错、token 被拒绝)。操作失败(发送、切换模型、打开对话等)统一用 notices.error:服务端带 hint 的错误把建议放第二行,断线导致的失败合并成一条“没有完成:ta serve 未连接”。需要用户确认的操作用 notices.confirm,不可撤销的操作按钮为红色、默认焦点在“取消”。
  • 链接:回复里的链接都可以点(TaLinks,解析在 LinkUtil)。网页链接、正文里的裸 URL 和 <URL> 默认在右侧工作台的浏览器里打开,Cmd/Ctrl + 点击(或中键)在系统浏览器里打开;文件路径([说明](https://github.com/cellbang/cell/blob/main/ta-packages/ta-web/src/a.ts#L10-L20)、src/a.ts:10:5、file:// 地址,以及写成行内代码的 src/a.ts:12、package.json)按对话目录解析,在工作台打开并定位、选中对应行列(写法同 VS Code:#L10、#L10C5-L12C3、:10:5、:10-20);目录(docs/)在资源管理器里切到该目录。打开动作由 Web 扩展提供(registerOpener,内置的 @celljs/ta-workspace 负责文件、目录和内置浏览器)。安全上照 VS Code 的做法:只认 http(s)、mailto 和路径,javascript:、data:、command: 等其他协议一律当普通文本;渲染结果是 React 元素而不是 HTML;路径先经服务端确认存在,读写仍受执行环境约束;在系统浏览器打开前会弹窗显示完整地址并突出域名(外观相近的域名按浏览器的 punycode 显示),可复制、打开或信任该域名,本机地址(localhost、127.0.0.1、ta serve 所在主机)和 设置 → 链接 → 受信任的域名(github.com、*.github.com、localhost:3000、*)里的不再询问;新窗口一律 noopener,noreferrer。内置浏览器里,与 Ta 页面同源的地址不给 allow-same-origin,碰不到页面和 token。
  • 账号菜单:导航栏底部的圆形头像(AccountMenu),是 ta serve 所在电脑的用户名首字母(TaServerInfo.user),断开时右下角出现黄点;菜单里是用户名与电脑名(host)、Ta 服务的连接状态与版本、安装为应用(浏览器支持时)和退出登录。

设置 ​

头像上方的齿轮(ManageMenu)是两层菜单:设置(⌘/Ctrl+,)、扩展、键盘快捷方式;模型账号…、打开工作目录…、在手机上使用…;主题和语言在右侧展开第二层,最后是关于 Ta。设置是主区域的一个视图(SettingsView,打开时占用对话和工作台的位置,Esc 或 × 关闭),左侧是两层目录(界面 / Ta / 更多,下面是各分组,滚动时跟随高亮),右侧是分组卡片,顶部搜索按每一行的名称和说明过滤:

  • 界面:主题、减少动效、界面语言、朗读回复;存在服务器上的用户数据里(TaSettings),换浏览器、换设备都一样。
  • Ta:Ta 本身的设置,读写 pi 的 settings.json(服务端 getSettings / updateSettings,可改的键见 ta-protocol 的 TA_SETTINGS):默认模型、思考强度、传输方式、引导与排队消息的送达方式、自动压缩与重试、代码模式、技能命令、Shell 与命令前缀、图片。顶部切换用户(agent 目录,对所有项目生效)和项目(当前对话目录下的 .ta/settings.json,覆盖用户设置);没设置的项显示继承来的值("默认(…)"或"沿用用户设置(…)"),在当前层设置过的项左侧有蓝条,悬停出现重置。项目未被信任时只读并说明原因。
  • 更多:扩展、内置扩展、键盘快捷方式、关于(版本、工作目录、模型账号、手机访问、退出登录)。

登录模型服务、工作目录与手机 ​

  • 模型账号:和终端里 pi 的 /login 一样,可以用账号(OAuth,Claude、ChatGPT 这类订阅)或 API Key 登录模型服务。登录在 ta serve 上进行(TaAuthServer):要打开的登录页、设备码、要填的 Key 只发给发起登录的页面;凭据存在 ta serve 所在电脑的 auth.json,不回到浏览器。没有可用的模型时(CELL_TA_NOT_SIGNED_IN),页面直接打开登录页,而不是显示 pi 关于 /login 的提示;桌面应用第一次启动时也一样。
  • 工作目录:没指定目录时,新对话和分身在 ta serve 的工作目录里打开。"打开工作目录…"(TaServer.setWorkingDirectory)对所有客户端生效,跨重启保留;已经打开的对话不动。
  • 在手机上使用:经 Relay 配对一台设备(@celljs/ta-relay),配对链接显示成二维码(util/qr-code.ts 自己画,没有依赖),只显示一次;下面列出已配对的设备和 ta serve 是否连着 Relay,可以取消配对。默认 Relay 是 https://relay.cellbang.com。

结构 ​

TaWorkbench(TaWorkbenchImpl)一条连接和服务器上打开的会话;token 取自 URL 片段或 localStorage;连不上时先探测服务器:有回应是 token 不对(回到登录页),没回应是 ta serve 没在运行(保留 token,提示后自动重试)
TaPwa(TaPwaImpl)把页面做成可安装的应用:链接清单和图标、注册 Service Worker、在账号菜单里提供"安装为应用"(浏览器给出安装提示时)
TaWebExtensions(TaWebExtensionsImpl)加载、重新加载、卸载 Web 扩展,汇总它们注册的页面、卡片、消息渲染器和工具
BuiltinWebExtension内置 Web 扩展的扩展点:AssistantWebExtension(分身)
TaChat(TaChatImpl)当前对话:新建、切换、关闭;打开的会话带上 Web 扩展的工具,回复结束后重新加载扩展;记住上次的会话文件和新对话目录(用户级)
TaAssistant(TaAssistantImpl)分身:主对话、任务与派发任务的客户端工具;会话文件与任务列表是用户级状态,换浏览器还是同一个分身
TaState(TaStateImpl)界面状态按级别保存,见下文“界面状态”
TaSettings(TaSettingsImpl)主题、语言、朗读、减少动效、打开的内置扩展;语言经 @celljs/widget 的 LocaleManager 切换
TaNotices(TaNoticesImpl)页面的通知与确认框:Ta 自身和 Web 扩展共用
TaAppPage@View 注册的首页(AppView)

服务端(@celljs/ta-host)为此加了 getSettings / updateSettings(ta 的设置),以及三个方法:listWebExtensions、readWebExtension、removeWebExtension,按 pi 加载包的方式(packages 设置与 extensions/ 目录,项目的只在受信任时)找到 Web 扩展;以及与 pi /reload 相同的 reload。

界面状态 ​

界面上要在刷新后留下来的东西,按“谁应该再看到它”分级,由 TaState 统一保存(组件里用 usePersisted(spec),spec 写明键、级别、初始值和读回时的校验):

级别存在哪里用于
tab本标签页的 sessionStorage当前页面、侧栏标签:两个标签页可以各看各的
device本浏览器的 localStorage各栏宽度与收起、右侧工作台是否打开:跟着屏幕走
user服务器上 @celljs/ta-web 这个包的数据(getExtensionData),浏览器另存一份设置、上次的对话、新对话目录、分身的会话与任务、工作台打开的工具:换浏览器、换设备都一样

user 级先用浏览器里的副本(首屏不闪),再读服务器,服务器的值不同就更新;改动约 300 ms 后合并写入,页面隐藏或关闭时立即写,断线期间的改动在重连后补写,其他浏览器的改动经 onExtensionData 推送过来。读回时本页已改过的值不会被覆盖,浏览器之间以最后写入为准。旧版本只存在浏览器里的值第一次读到时会搬到服务器。

属于某个目录的状态(资源管理器展开的文件夹)由显示它的 Web 扩展用 pi.storage.directory(cwd) 保存;服务器按目录(连同执行环境,ta serve --exec 时远程机器上的同名目录是另一个)分文件存放。打开的文件由 @celljs/ta-workspace 自己记在 pi.storage 里,加载时用 openEditor(editor, { preserveFocus: true }) 在后台重新打开,不会弹出工作台。

token 是登录凭据,不算界面状态,仍由 TaWorkbench 存在 localStorage(安装成应用后在自己的窗口里也能登录)。

要加一个内置 Web 扩展,写一个 @Component(BuiltinWebExtension);要替换整个界面,用 @View({ index: true, component: MyView }, true) 重新绑定首页。