树状的 AI 对话 — 把"线性聊天"撕开成可分叉、可回跳、可聚焦阅读的思维树,
同时和你本机的 Claude Code / Codex CLI 会话双向打通。
介绍 · 核心特性 · CLI 打通 · Quickstart · 上下文模式 · 技术架构 · 快捷键
- 安装依赖:
make setup - 启动服务:
make dev - 访问地址:
http://localhost:3000
前置依赖和进阶说明见下方 Quickstart。
Project 模式默认的线性 thread 视图 —— 连续阅读 + 行内分叉「↳ N 个分支」+「在 CLI 继续」+ 右下角树缩略图导航
| 树状画布 — 思维树概览 | 聚焦阅读 — 锚点回跳 | 参考材料 — 飞书 / YouTube / 网页通吃 |
|---|---|---|
![]() |
![]() |
![]() |
更多截图
行内分叉:线性视图里真分叉折成「↳ N 个分支」,点开就地切到那条 lineage
选区分叉:在任意回复里选一段 → ⌘K 长出子节点
笔记本:选中 → 📌 进抽屉,点笔记跳回原节点 + 闪烁定位
Wide tree:55+ 节点的真实使用形态,深度学习 / 个股研究都能撑住
线性聊天有两个老问题:
- 想顺手追问一句细节,整个上下文就跑偏了。 你被迫在「问完再回主线」和「假装没看见」之间二选一。
- 长会话尾段越聊越钝。 Claude/GPT 的实际有效上下文比标称小得多,一窗到底等于反复让模型在长 prompt 里做摘要。
Trellis 的处理方式:每个回答都是一个节点,选中文字 → ⌘K 即可在那一句旁边长出分叉子节点,原会话不动。每个分支独立流式生成、独立持久化、独立可回跳,画布上呈现整棵树。
而且它不是另起炉灶——Trellis 把请求转交给你本机的 claude / codex CLI 子进程,并且能把 CLI 已有的会话 attach 进来双向同步:CLI 里聊的、trellis 里聊的,落的是同一份 transcript,两边都看得到、都能续。
适合:长篇研究式问答(个股深度研究、技术学习、文档导读)、需要保留多条思路的探索、需要随时回到某个引用处对照的阅读,以及——把你在终端里跑的 Claude Code 会话搬进一个能浏览/搜索/分叉的工作台。
- 线性 thread 主视图(Project 默认):像 ChatGPT 一样把当前 lineage 纵向铺开连续阅读;真分叉折成行内「↳ N 个分支」可展开切换;右下角 SVG 树缩略图导航全貌,点节点即跳
- 树状画布:ReactFlow + Dagre 自动布局的全局思维树,一键在「线性 / 画布」之间切换
- 三层视图:Layer 1 全局画布 / Layer 2 单节点聚焦 / Layer 3 全屏阅读
- 子树折叠:卡片右下角
▶ N/▼ N角标一键收起后代;折叠状态按 session 持久化 - 大纲侧边栏:左侧树形 outline,缩进按「分叉深度」而非轮数——线性长聊保持平铺不跑出面板,只有真分叉才缩进
- 缩放分级渲染(LoD)、Fit View(
F)、切 active 平滑 pan
- ⌘K 分叉:任意回复里选一段 → 浮 popover → 输入追问,新节点带 anchor 出生
- 锚点高亮 + 回跳:父节点正文那段变黄
<mark>,点击跳到子节点;子节点全屏顶部常驻「从『…』分叉」横幅,B一键回父节点引用处 + emerald 闪烁定位 - 编辑即重问:全屏问题区铅笔 → 改问法 = 新建 sibling 分支,原问答无损保留,天然多版本对比
- 跨 markdown 结构:代码块 / 表格 / 链接 / 加粗 / 列表都能正确包裹(React 渲染后直接 wrap textNode,不改 markdown 源)
详见 下方专章。这是 Trellis 区别于一般 AI 客户端的核心。
- Attach 本机 CLI 会话:按 provider 浏览
~/.claude/projects或$CODEX_HOME/sessions里的已有会话,手选 attach 进 Trellis(不按目录批量灌) - 真双向实时同步:CLI 侧聊新一轮 → watcher ≤1s 自动导入 trellis;trellis 侧续聊 → resume 写回同一份 jsonl + 身份对账。两边落同一 transcript
- 分支对齐:CLI 的 rewind /
/branch↔ trellis 分叉双向对齐(一棵 trellis 树 = 一组 CLI session 按 message uuid 求并集)。trellis 从任意历史节点分叉 → 构造前缀 jsonl,在 CLI 侧也成为独立可--resume的会话 - 「在 CLI 继续」:Project 会话每个回答下一键复制
claude --resume <id>或codex resume <id>命令,回终端无缝接着聊
- 多种来源:粘贴文本 / URL 抓取(飞书 / YouTube / GitHub / 普通网页通吃)
- 抓取策略不 hard-code:URL 内容由 Claude/Codex CLI 自己挑工具(飞书走 feishu-cli、YouTube 走字幕、普通网页走 web-fetch)
- 抓取进度 ring + 失败降级手动粘贴 + ⟳ 重新拉取
- 参考节点同样能选区分叉发问,体验和 QA 节点一致
session 创建时锁定 mode + workspace + model(per-session 持久化,切走再回来不会变),顶栏只读 badge 显示。详见 下方专章。
| 模式 | 类比 | cwd | Tools / Skills / MCP | 跨节点记忆 | 适合 |
|---|---|---|---|---|---|
| Chat | GPT 网页客户端 | 无 | Claude:WebSearch + WebFetch;Codex:内置 cached web search + read-only;⚡ 增强模式 = scratch 目录 + 全工具 |
无 | 默认日常问答、补认知、查信息;临时跑个命令 / skill 开增强 |
| Project | Claude Projects / 长期项目 | session 绑定 | 全开 | 有(沿祖先链,分支间隔离) | 在仓库里干活、跨节点延续记忆,cache 命中通常 70%+ |
Provider 可切 Claude(Sonnet / Opus / Haiku)/ Codex(OpenAI)/ Mock(确定性假回复,看 UI 用),两档语义对齐。
- 工具调用可视化:解析 stream-json 的
tool_use/tool_result,在节点折叠区展示每次 Bash / Read / Write / WebFetch 的入参与输出 - 交互式工具表单(Claude):模型发起
AskUserQuestion/ExitPlanMode时渲染成可填表单(单选/多选/批准计划),就地作答回灌 - 本地文件预览:回答里生成或提到的文件 / HTML 直接在 trellis 内预览(HTML 走 opaque-origin sandbox iframe,图片/PDF/markdown 各自渲染);行内像路径的代码也可点开。安全围栏 = session 实际碰过的目录范围,realpath 归一防逃逸
- ⌘P 跨 session 全文搜:FTS5 trigram 分词(中英混排都能子串匹配),覆盖问题 / 回答 / 参考 / 笔记,按 mode facet 过滤
- 命令面板:首屏输入框
/前缀触发 ——/new/clear(🧹 开新话题/清空上下文)/archive/switch/model等 session 元操作,纯 Trellis 命令本地执行不发 LLM - Skill 入口:Claude 输入
/<skill-name>;Codex 输入$<skill-name>,并按.agents/skills、~/.agents/skills、$CODEX_HOME/skills等作用域发现(纯 Chat 点选自动开启增强模式) - Agent / System Prompt 可配:5 个内置 Agent + 自定义人设,Claude 与 Codex 都可用;per-session 锁定,也支持
@slug单轮调用
- Session tabs:tmux 式常驻 tab 条 +
⌘1-9快切 + live 状态点(streaming / done / error) - 归档:软隐藏不删 jsonl/节点,可恢复
- Header context 徽章:当前上下文压力实时显示,≥50% 变可点 popover 提示开新话题
- 重开恢复浏览位置:切回 session 回到上次离开的节点 + 视图层
- 粘贴 / 拖拽 / 选文件,两档模式通吃;Project 模式连续追问可跨节点引用同一张图
- 内容寻址存储:blob 落
~/.trellis/blobs/<sha256>.<ext>自动去重,不进 SQLite - 重试自动带回原图;单图 ≤ 10MB,单节点 ≤ 6 张;PNG / JPEG / WebP / GIF
- 阅读时选中 + 📌 → 当前 session 笔记抽屉(桌面右侧 320px / 移动端底部 sheet)
- 点笔记卡 → 跳回源节点 + scroll-to + emerald 闪烁;per-session,删 session 自动清理
- Enter 发送(可一键切回 ⌘Enter);流式中
Esc或卡片 ⏹ 中止 - 保留半截响应:中止后部分内容落库(
aborted灰色态),原 prompt 回填便于编辑重试 - In-place retry:失败/中止节点原地重试不新增节点
- Durable streams:spawn 与 HTTP 解耦,断线/切 tab 不杀生成,回来自动续上
- 节点编号
#N+ 未读小圆点(停留 1s+ 自动标已读)+J/K跳未读 + Done Toast - 四桶 Token 计量:input / output / cacheRead / cacheCreation;⚡ emerald 强调 cache 复用。context 占用按主 agent 当前窗口实际值算(非跨工具迭代累计,避免虚高数倍)
| 键 | 行为 | 作用域 |
|---|---|---|
⌘K |
选区分叉 popover | 文本选中时 |
⌘D |
选区入笔记本 | 文本选中时 |
⌘P |
跨 session 全文搜索 | 全局 |
⌘↩ / Enter |
发送(默认 Enter,可切 ⌘Enter) | textarea |
Esc |
关 popover / 中止流 / 关弹窗 | 全局 |
J / K |
下/上一个未读节点(环绕) | 全局 |
Alt + 方向 |
节点导航:上=父 / 下=首子 / 左右=兄弟 | 全局 |
⌘1-9 |
切到第 N 个 session tab | 全局 |
F |
Fit canvas to viewport | 画布 |
B |
回父节点锚点引用处 | 全屏阅读且有父节点时 |
- JSON 导出(完整树 + token + 元数据)/ Markdown 导出(按深度生成层级标题,飞书友好)
- 本地落地:SQLite
~/.trellis/data.db(WAL,启动自迁移),全部 session / 节点 / 引用 / 笔记 / 节点位置持久化 - session 管理:picker 切换 / 改名 / 归档 / 删除(带确认)
- 明暗主题一键切(localStorage 持久化 + 预水合防闪白)
- iOS Safari 选区双保险(polled + selectionchange),移动端专属 📌 按钮
- 响应式:outline / 笔记 / 缩略图在移动端转抽屉或底部 sheet;触屏支持画布缩放拖动
- 不是 SaaS。SQLite 落本地(
~/.trellis/data.db),单人单机。 - 不是直接调 API。Trellis 把请求转交给本机
claude/codexCLI 子进程——订阅 / 余额 / 模型权限完全跟 CLI 走,Trellis 不存任何 API key。 - 不是为多人协作设计的。没有用户系统、没有同步;公网暴露请自带一层认证(仓库内置一个可选的 cookie 登录闸,见
proxy.ts)。
- Bun v1.1+
- 至少装一个 LLM CLI(Trellis 不直接打 API,是 spawn 本机 CLI):
- Claude Code CLI:
npm i -g @anthropic-ai/claude-code→claude可用并已登录 - Codex CLI(可选):
codex login完成登录
- Claude Code CLI:
- Web 终端功能依赖宿主机安装 ttyd 与 tmux。macOS:
brew install ttyd tmux;Debian/Ubuntu:apt install ttyd tmux - 完全不装 CLI 也能启动——provider picker 选
Mock,返回固定假回复,仅用于看 UI
Trellis 的 LLM/CLI 运行时层是普通 npm 依赖(@smokingmouse/agent + @smokingmouse/llm,源码在 sm-toolkit),bun install 一步装完,无需额外 clone/build 任何仓库。
git clone https://github.com/SmokingMouse/trellis.git
cd trellis
make setup # = bun install + 前置检查
make dev只想看当前环境缺什么、不装任何东西:make check。
若未安装 ttyd,Trellis 仍可启动,但 Web 终端不可用;启动/部署日志与终端面板会提示 ttyd 缺失和对应平台的安装命令。Trellis 不会自动执行安装命令。
唯一的运行时注意点:必须用 bun --bun run dev(make dev/make build/make start 已经这么做了)——bun run 不会把 Next/Turbopack 内部起的 worker 进程纳入 bun 运行时,导致 bun:sqlite(lib/server/sqlite.ts 用)解析不到。
打开 http://localhost:3000,第一次输入问题即创建 session。想 attach 已有 CLI 会话:左侧 sidebar →「Attach CLI 会话」。
原生 claude / codex 走 CLI 登录态,零配置。想接 Anthropic 兼容的第三方端点(deepseek / kimi / ark …):
mkdir -p ~/.config/sm
cp node_modules/@smokingmouse/llm/endpoints.example.yaml ~/.config/sm/endpoints.yaml
# 编辑它:填你自己的 provider / 模型清单,API key 只写环境变量名,key 本体放 env搜索顺序 $SM_ENDPOINTS_PATH → ~/.config/sm/endpoints.yaml → ~/.claude/global/endpoints.yaml(legacy)。改完重启即出现在模型 picker。
要改 sm-toolkit 本体时:clone 到 ~/sdk,make link-sdk 把两个包软链过来(bun install 会冲掉软链,重跑即可);make unlink-sdk 回到 registry 版本。
单机随手跑一把:
make build && make start # 固定 -p 3088;等价 bun --bun run build && bun --bun run start -- -p 3088数据落 ~/.trellis/data.db(SQLite WAL,自动迁移)。卸载只需删掉这个目录。
如果 Trellis 是常驻服务(launchd / systemd),不要在它正在运行的那个目录里 make build 再重启:next build 会就地改掉运行中的 .next,build 失败就把服务打成半死,build 成功却忘了重启则是「内存里旧模块 + 磁盘上新文件」混跑(页面能开但交互全挂)。而且没有回滚路径。
改用 make deploy:
make deploy # 部署当前 HEAD
make deploy REF=v1.2.0 # 部署指定 ref
make rollback # 切回上一个 release
make deploy-status # 看 current / previous 与上次部署状态它做的事:把目标 commit 导出到 ~/.trellis/releases/<sha>/ 里装依赖并 build(全程不碰正在跑的服务)→ 用真数据库的一致性快照起一个临时实例,验证它真的能启动、能出页面、能读现网数据 → 备份数据库 → 原子换 ~/.trellis/current 软链 + 重启 → 验活;验活不过自动回滚到上一个 release。
任何一步失败都不会动到正在跑的版本。实测切换窗口 ≈ 0.2s,且期间返回的是维护页而不是连接被拒——网关进程先占住端口、再拉起 Next,Next 起不来时它不会跟着自杀,而是退避重启并出 503 维护页(未登录只显示「正在更新」,版本号与日志要登录后才看得到)。GET /__gate/health 给出网关与 Next 的真实状态。
首次启用需要把常驻服务的工作目录指向软链,一次性:
make deploy # 先建出第一个 release
make install-service # 把常驻服务的工作目录改成 ~/.trellis/current(会先备份原定义文件)之后仓库目录就只是开发用的 checkout 了,在里面 build 不再影响线上。应急回滚不依赖仓库:~/.trellis/bin/rollback.sh。
macOS / Linux 都支持:重启走 launchd(launchctl kickstart -k)还是 systemd user unit(systemctl --user restart)按平台自动判定,unit 名默认取 label 的最后一段(com.smokingmouse.trellis → trellis.service,用 TRELLIS_DEPLOY_UNIT 覆盖)。make deploy-status 会把认到的那套连同它当前的工作目录一起打出来。
两种情况
make deploy会拒绝执行,都可以FORCE=1强行继续:① 有会话正在生成(切换会中断它们,会列出是哪些);② 常驻服务的工作目录不是~/.trellis/current(这种状态下重启只是让服务在原目录里重来一遍,跟部署的那个 sha 没关系)。
部署产物(release / 数据库 / 配置)全部落在 ~/.trellis/,代码里没有写死的机器路径;真正跟机器走的是宿主机状态:bun、已登录的 CLI、常驻服务定义、以及不进 git 的 .env.local。按顺序补齐:
-
装依赖:bun(必须,运行时用了
bun:sqlite/Bun.serve,Node 跑不了)→ claude CLI 并claude login→ 可选 codex / ttyd / tmux。make check随时报缺什么。 -
clone +
make setup。 -
建运行期配置
~/.trellis/shared/.env.local(shared/下的文件会被软链进每个 release,不随 release 清理丢失):TRELLIS_AUTH_PASS=<登录密码> TRELLIS_AUTH_TOKEN=<随机长串,cookie 会话令牌> TRELLIS_REPO_DIR=<trellis 仓库 checkout 的绝对路径> # 可选:agent-server 主页收编(TRELLIS_AS_ADOPT),配置说明见下 TRELLIS_AS=off
前两个任一缺失 = 认证闸静默关闭。机器只在内网/Tailscale 里可以不配;挂公网隧道必须配——Trellis 能 spawn CLI 在宿主机执行任意代码,这道闸是唯一的门。
TRELLIS_REPO_DIR给应用内「检查更新/更新到最新」用(release 目录不是 git 仓库,部署脚本要回到 checkout 里跑)。 -
装常驻服务定义(模板见下),
WorkingDirectory先指向仓库 checkout,并在 checkout 里make build一次让它有东西可跑,然后加载服务。 -
首次部署:
make deploy FORCE=1——此时服务工作目录还不是~/.trellis/current(正是下一步要改的),preflight 会拦,首次明知故犯一次。 -
make install-service:把服务定义的工作目录改到~/.trellis/current(自动备份原文件并重载)。之后make deploy不再需要 FORCE,仓库目录退化为纯开发 checkout。 -
验证:
make deploy-status;curl http://127.0.0.1:3088/__gate/health应给出next=ready且auth=on。
主页收编与 project 切流共用启用判定:TRELLIS_AS=off 优先级最高,即使设置了 socket 也关闭;否则 TRELLIS_AS=on 或设置 TRELLIS_AS_SOCKET 即启用。另设 TRELLIS_AS_PROJECT=on 才给新 project 会话打 thread 标记。只关 PROJECT 会停止新会话打标,已绑定会话继续使用 daemon;关 AS 则 project 新请求带 notice 回退兼容模式,保留输入与祖先历史,外部会话保留历史并拒绝发送。pane 绑定仍由 herdr-bridge 接线。
先独立启动 agent-server daemon,再在开发用 .env.local 或部署用 ~/.trellis/shared/.env.local 配置下表变量并重启 Trellis。可参考仓库的 .env.example。设置 TRELLIS_AS_ADOPT=on 后,在主页查看并继续外部会话,审批与系统日志也在主页呈现。
| 变量 | 含义与默认值 |
|---|---|
TRELLIS_AS |
默认不启用;on 启用,off 是最高优先级硬关闸,即使已配置 socket 也禁用。未设置时,非空 TRELLIS_AS_SOCKET 会自动启用。 |
TRELLIS_AS_PROJECT |
on 时为新 project 会话绑定 daemon thread,默认关闭。已绑定会话由 TRELLIS_AS 控制。 |
TRELLIS_AS_ADOPT |
默认 off;设为 on 自动收编 daemon 上至少有一个 turn、尚未绑定且非 closed 的外部线程。TRELLIS_AS=off 覆盖它。 |
TRELLIS_AS_PROJECT_ID |
可选的精确 projects.id,只允许该项目所属工作区的新会话绑定 AS;未知归属保持 legacy。未设置时保留全部新 project 会话可切流的行为;停流请关闭 PROJECT 或 AS,不要只删此筛选值。 |
TRELLIS_AS_SOCKET |
daemon Unix socket 的绝对路径。未设置时遵循 agent-server 路径规则:AGENT_SERVER_SOCKET_PATH 优先,其次绝对 XDG_RUNTIME_DIR 下的 sm-toolkit/agent-server.sock,其次绝对 XDG_STATE_HOME 下的同一路径,最后为 $HOME/.sm-toolkit/agent-server.sock。 |
TRELLIS_AS_TOKEN_PATH |
已运行 daemon 的 token 文件绝对路径,不是 token 内容。默认绝对 XDG_STATE_HOME 下的 sm-toolkit/agent-server/token,否则 $HOME/.agent-server/token。自定义 socket 不会自动改变 token 路径,两个配置需指向同一个 daemon。 |
连接失败不会阻断 Trellis 启动;收编扫描器按下述策略退避。主页权限与系统日志通过节点 SSE 订阅 daemon 快照与通知,保留断线续传、15 秒保活与慢消费者保护。主页与接口沿用 Trellis 现有鉴权闸;这些变量仅在服务端读取,不要使用 NEXT_PUBLIC_ 前缀。
实时审批依赖 daemon 的 pendingRequests 能力:首次 attach/断线重连用快照初始化,随后订阅 thread/pendingRequests;project 的可操作表单来自 server request。静默时不重复推送历史;旧 daemon 应升级后使用实时审批。
project 从最新空闲节点续聊复用 thread;早期节点续聊、显式 fork、重试,或原 thread 已被其他客户端推进时,需要新 thread。daemon 声明 midThreadFork 时,以 as_turns.last_item_id 为边界发送 thread/fork {fromItemId},新历史只含所选节点之前的完整前缀。缺少该能力才把所选祖先历史播种到新 thread;有能力但边界无效时直接报错,不静默退回播种。无 daemon 映射的兼容历史仍需播种。
客户端随仓库 vendor,来源固定在 vendor/agent-server/VENDORED_FROM;刷新用 SM_TOOLKIT_DIR=/path/to/sm-toolkit sh scripts/vendor-agent-server.sh 后执行 bun install。复核命令:bunx tsc --noEmit、bun test、scripts/mobile-verify/mobile-as-project.sh、scripts/mobile-verify/mobile-as-adopt.sh;移动验收只用 mock 引擎与脚本锁定的隔离端口。
外部会话收编由 TRELLIS_AS_ADOPT=on 单独开启,不依赖 PROJECT 开关或 PROJECT_ID 灰度范围。启动发现线程并读取首次快照,健康时每 1.5 秒用一次 thread/list 发现新线程(超过协议每页 10000 条才分页);已订阅线程由通知标记变化,只有新增、摘要变化或收到通知的线程按已存 sinceSeq 增量 attach,无变化时零 attach。故障按 2–30 秒指数退避,重连从已投影 cursor 补齐断线变化;关闭/删除线程清理观察缓存。关闭 ADOPT 不启动扫描器,不向 daemon 发出收编请求。零 turn 线程持续观察但不建会话,首个 turn 出现后才收编;扫描快照及 turn 通知都可识别首轮。
每轮 thread/list 底噪随历史累计线程数(含 closed)线性增长,按二审实测约 160 条线程后单轮超过 100 KB,已记 backlog。
非 closed、未绑定的线程按 realpath 后 cwd 的最长包含真实 workspace 根归属;真实根必须有 .git 文件/目录,或是 Herdr 已知的 repo/worktree。非 git 目录项目与系统 home/scratch 根均不参与,cwd 精确等于系统根也不复用其归属。非 git 目录项目如「投研」不参与归属,其下外部线程进入「外部会话」。没有真实匹配时自动创建唯一的系统项目「外部会话」(cluster_key=trellis:external)。若 cwd 已被其他非真实 workspace 占用,则使用外部项目的独立归组 workspace,实际执行目录仍保存在 sessions.workspace_path;不移动已有 workspace 或其中的会话。会话及节点携带 origin=external、backend,标题保留 meta/fjContext 中的标题或契约号。
主页提问复用原 thread,审批竞答、中断和分叉沿用 project 路径;正在运行的轮次不能被静默覆盖。外部线程关闭后,主页保留历史并显示已结束,拒绝新提问。Trellis 不会调用 thread/close 收编线程;删除会话只解绑,as_adoptions 留永久墓碑,即使外部再有新 turn 或 Trellis 重启也不会重新收编。关闭 ADOPT 不删除已收编历史;需要回退时设 TRELLIS_AS_ADOPT=off 并重启。整体关闭可设 TRELLIS_AS=off,收编会话保持历史并拒绝发送,不会另起兼容引擎。
as/1 的 attach 提供 items、不提供历史 Turn 记录;回填文本和工具项保持原投影格式,历史状态从 items 与线程状态恢复,不虚构历史 usage。新 turn 的生命周期通知用于完成状态和 usage。验证脚本 scripts/mobile-verify/mobile-as-adopt.sh 使用隔离 fixture daemon、数据库副本、3479/3480 与共用互斥锁,清理所有自建进程并输出手机截图。
launchd(macOS):存为 ~/Library/LaunchAgents/com.smokingmouse.trellis.plist(label 可用 TRELLIS_DEPLOY_LABEL 换),launchctl bootstrap gui/$(id -u) <plist路径> 加载。把 /Users/YOU 全部换成真实家目录——plist 不展开 ~:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.smokingmouse.trellis</string>
<key>ProgramArguments</key>
<array>
<string>/Users/YOU/.bun/bin/bun</string>
<string>--bun</string><string>run</string><string>start</string>
<string>--</string><string>-p</string><string>3088</string>
</array>
<!-- install-service 只改这一行;首次先指向仓库 checkout -->
<key>WorkingDirectory</key>
<string>/Users/YOU/path/to/trellis</string>
<key>EnvironmentVariables</key>
<dict>
<key>HOME</key>
<string>/Users/YOU</string>
<!-- launchd 不继承 shell 环境。spawn claude/git/ttyd 全靠这条 PATH:
必须含 claude CLI 所在 bin(npm -g 的目录,nvm 下形如
~/.nvm/versions/node/vXX/bin)和 Homebrew bin。 -->
<key>PATH</key>
<string>/Users/YOU/.bun/bin:/Users/YOU/.nvm/versions/node/vXX.X.X/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
<key>NODE_ENV</key>
<string>production</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/YOU/Library/Logs/trellis.log</string>
<key>StandardErrorPath</key>
<string>/Users/YOU/Library/Logs/trellis.log</string>
</dict>
</plist>macOS 已知雷:claude CLI 的凭证默认存 keychain,launchd 会话读 keychain 可能读到失效凭证——症状是终端里
claude一切正常、Trellis spawn 出的却报未登录。修法:删掉 keychain 里的 Claude Code 凭证条目让 CLI 回退到~/.claude/.credentials.json文件存储,重新claude login。
systemd user unit(Linux):存为 ~/.config/systemd/user/trellis.service,systemctl --user daemon-reload && systemctl --user enable --now trellis。ssh 登出后要继续跑:loginctl enable-linger $USER(缺它连 systemctl --user 都不可用,部署 preflight 会报 XDG_RUNTIME_DIR 相关错误):
[Unit]
Description=trellis
After=network.target
[Service]
# install-service 只改这一行;首次先指向仓库 checkout
WorkingDirectory=%h/path/to/trellis
ExecStart=%h/.bun/bin/bun --bun run start -- -p 3088
# systemd 不继承 shell 环境。PATH 必须含 claude CLI 所在 bin。
Environment=PATH=%h/.bun/bin:%h/.local/bin:/usr/local/bin:/usr/bin:/bin
Environment=NODE_ENV=production
Restart=always
RestartSec=2
[Install]
WantedBy=default.targetTrellis 把每个 Project 会话当成真实 CLI thread:Claude transcript 位于 ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl,Codex rollout 位于 $CODEX_HOME/sessions/YYYY/MM/DD/*.jsonl。在此之上做了三件事:
- 发现 + attach:浏览本机 CLI 会话清单(排除 trellis 自己 spawn 的,防回环),手选哪些 attach。attach 的会话 origin 标
cli-import,跑在 Project 模式 - CLI → trellis:watcher(
instrumentation.ts启动)监听 attached jsonl 所在目录,文件一变(CLI 侧聊了新轮 / rewind //branch)→ debounce 后增量重导 → SSE 推前端无刷新 reload - Trellis → CLI:在 attached 会话续聊 = 对应 provider 的 resume 写回同一 transcript;done 后身份对账(删临时流式节点,让 canonical CLI 节点接管),两个方向收敛到同一份 transcript
- 物理约束:同一会话别在 CLI 和 trellis 同时各聊一轮(抢 append);串行无碍
CLI 的单条会话是线性的,Trellis 的是树。统一模型:一棵 Trellis 树 = 一组 CLI thread(root + 各 fork)的 lineage 并集。
- CLI 里 Claude
/branch/--fork-session或 Codexfork产生的新 transcript → Trellis 自动按共享前缀归并到同一棵树 - Trellis 里从 tip 续 = 线性 append 同 transcript;从任意历史节点分叉 = 构造 root→X 的前缀 transcript 并换新 session/thread id,再由对应 CLI resume,因此分支在终端也能独立继续
/clear(CLI 抹掉上下文、开新 session id)→ 那是另一份不共享 uuid 的 jsonl,trellis 不会自动并进当前树(它是「不相关的新对话」),而是作为一条独立可 attach 的会话出现。等价物 = trellis 的「🧹 新话题」(同树里加一个全新上下文的根)/compact(同 session 摘要压缩、不换 jsonl)→ 在原 jsonl 追加一个type:"system"边界节点,对话继续。trellis 读全量 jsonl,边界前后所有原始轮都完整显示(compact 只压缩「模型那侧的窗口」,不动存储)——解析器专门桥接这个 system 边界节点,保证父链不在 compact 处断裂
session 创建时锁定 mode + workspace,整棵树共用。两档差别在 prompt 怎么组装、CLI 走什么权限、是否绑定 cwd、跨节点要不要共享记忆,trade-off 是「成本 / 权限范围 / 上下文连续性」。
历史注:2026-07-16 砍掉了中间档 Workspace(一次性 CLI、每轮无状态)——实际用量为零,它的两个场景已被别人吃掉:临时跑命令/skill → Chat 增强模式;在仓库干活 → Project。存量 workspace 会话(如有)migrate 时自动归入 Project。
- Prompt:Trellis 自己组装"祖先链 → 当前问题"(深度可调 + anchor excerpt),用一个简短 system prompt
- 执行:claude 仅开
WebSearch + WebFetch,cwd~,不读 CLAUDE.md / 不加载 skills / MCP / Bash - 每条分支真正独立——claude 看到的只是 trellis 给的折叠历史,无外部副作用
- claude 家族默认走 B-fork(
--fork-session,历史活在 fork 出的 CLI session 里,不往 prompt 折叠) - ⚡ 增强模式(可随时开关,点选 skill 自动开):spawn 到
~/.trellis/chat-scratchscratch 目录 + 权限全开——能跑 skill / Bash / 文件工具,但不绑定你的仓库
- Prompt:只发当前问题给 CLI,让它 resume 持久 thread 自己找历史
- 执行:一条岔一个 CLI lineage——顺着聊 = resume 线性 append 同一份 transcript;真分叉 = 在分叉点构造前缀 transcript 成新 session/thread;cwd 绑定 workspace_path
- 沿祖先链共享 CLI 历史(分支间互相隔离),线性续聊通常能复用大量 cache
- 「🧹 新话题」= 同树里另起一个 root(全新 fresh context,不 resume 旧记忆,对应 CLI
/clear) - 存量会话(per-lineage 上线前建的)保持旧的全树共享行为不迁移;新建 project session 自动 per-lineage
Chat→ sandboxread-only,隔离 AGENTS.md / 环境 skills / plugins / MCP,保留 Codex 内置 web search(CLI 默认 cached);增强模式 = scratch 目录 + 全工具Project→$CODEX_HOME/config.toml+ MCP/tools/web 全开,一条分支一个可独立 resume 的 rollout lineage,cwd = workspace_path- Agent 人设、模型、静态 sandbox 权限、隔离与挂载 skill 生效;Codex exec 当前没有接入 Trellis 的逐项审批回调,工具白/黑名单也无法强制
flowchart LR
subgraph 浏览器["浏览器 (Next.js App Router)"]
UI["React + Zustand<br/>线性 thread / ReactFlow 画布"]
DOM["DOM mark injector<br/>(textNode wrap)"]
end
subgraph 服务端["Next.js Route Handlers (Node runtime)"]
Chat["/api/chat · SSE 推流"]
Refs["/api/references"]
Sync["/api/cli-sync<br/>discover / attach / events"]
Sess["/api/sessions · /api/search"]
end
subgraph CLI["CLI 子进程"]
Claude["claude (stream-json)"]
Codex["codex exec (jsonl)"]
Mock["mock (确定性)"]
end
DB[("~/.trellis/data.db<br/>SQLite WAL")]
JSONL[("~/.claude/projects · $CODEX_HOME/sessions<br/>CLI transcripts")]
UI -- "POST /api/chat" --> Chat
Chat -- "SSE delta" --> UI
Chat -- "spawn" --> Claude
Chat -- "spawn" --> Codex
Chat -- "spawn" --> Mock
Claude -- "stdout JSONL" --> Chat
Refs -- "spawn URL fetch" --> Claude
Watcher["cli-sync watcher<br/>(instrumentation boot)"]
JSONL -- "fs.watch 增量" --> Watcher
Watcher -- "import + SSE" --> UI
Claude -- "resume 写回" --> JSONL
Chat <--> DB
Sync <--> DB
Sess <--> DB
Watcher <--> DB
- 前端:Next.js 16 App Router,单页,所有"导航"都是 Zustand 状态。线性 thread + ReactFlow/Dagre 画布双视图;stream-bus 把 SSE delta 直喂 DOM,绕开 React 重渲染热路径
- 服务端:Route Handlers(
runtime: nodejs),spawnclaude -p … --output-format stream-json/codex exec --json,按行解析 JSONL 转 SSE - CLI 同步:
instrumentation.ts启动单例 watcher;Claude/Codex provider parser(jsonl → Q/A 树)+cli-import-db.ts(union upsert)+cli-fork.ts(前缀 transcript / lineage 解析) - 存储:
~/.trellis/data.db(better-sqlite3 WAL),schema 在lib/server/sqlite.ts启动自迁移 - mark 注入:选区/笔记/锚点高亮是 React 渲染之后对 textNode wrap
<mark>(lib/dom-mark-injector.ts),不改 markdown 源
更详细的架构决策见 progress/ 下各 spec(CLI 同步、分支对齐 P1/P2、线性视图等)。
app/ Next.js App Router 页面 + API routes(含 api/cli-sync)
components/ React UI(LinearThreadView / Canvas / NodeFullView / Outline / …)
hooks/ useUnreadNavigation / useCliSyncEvents / useReconnectStreams / …
lib/
collapsed.ts 折叠/祖先链纯函数
layout.ts Dagre 布局 + LoD 阈值
llm/ provider 抽象(claude / codex / mock)
server/
cli-import*.ts CLI jsonl 解析 + DB union 导入
cli-fork.ts 前缀 jsonl 构造 / lineage 解析 / 在 CLI 继续
cli-sync-*.ts watcher + SSE 事件
repo.ts / sqlite.ts DB 访问 + schema 迁移
run-bus.ts spawn 所有权 + durable streams
stores/ Zustand session store(视图 / 折叠 / 流式控制)
progress/ 开发 dashboard + session log + 各 feature spec
instrumentation.ts Next 启动钩子:拉起 CLI 同步 watcher
Alpha,自用为主,已在生产环境跑了一段时间。还存在的粗糙边角:
- 移动端(≤ 640px)只做到能用,iOS Safari 上选区分叉偶有抖动
- 没有用户/权限/同步——多人用法请加 reverse proxy 或用内置 cookie 闸
- 分支对齐 P2 的「从历史节点分叉」已端到端验证,但树内分叉的 fork 文件会在 CLI 项目目录里累积(可接受,Claude 自己
/branch也这样)
欢迎提 Issue / PR,但请理解定位是个人工具,不会为了泛用化把模型 / 路由抽象到框架级别。
MIT(见 LICENSE)。底层依赖的 Claude Code / Codex CLI 各有自己的服务条款,自行确认。


