Cell - Ta Web
ta serve 自带的 Web 客户端:一个 Cell 前端组件(@celljs/react),状态全部来自 @celljs/ta-client,因此和桌面端、移动端共用同一套会话状态机。
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) 重新绑定首页。