Files
web-terminal/docs/FEATURE_PROJECT_MANAGER.md
Yaojia Wang dc5d073374 chore(v0.6): checkpoint foundation — Amber theme, P1 types, P3 config, design docs
Prior-session prep for the v0.6 Project Manager, checkpointed on the
v0.6-projects branch as a clean base for the parallel builders:

- P1 (src/types.ts): ProjectInfo + ProjectSessionRef contracts; Config
  gains projectRoots/projectScanDepth/projectScanTtlMs/projectDirtyCheck.
- P3 (src/config.ts): parseBool + parseProjectRoots helpers; parse the
  4 PROJECT_* env vars with defaults ([homeDir]/4/10000/true).
- UI: public/style.css indigo -> Amber (#e3a64a) accent theme.
- Docs: FEATURE_PROJECT_MANAGER.md (1:N session design), THEME.md,
  docs/mockups/ (final-amber.png is the locked visual).

Typecheck (backend + web) green. No behavior change to runtime code yet;
P2/P4/P5/P6 implement the feature on top of this.
2026-06-30 10:05:40 +02:00

16 KiB
Raw Permalink Blame History

Feature: Project Manager 项目工作台 (v0.6)

状态: 提案 / Draft探索完成待评审后进入 PLAN 目标: 首页除了"会话选择器"外,新增一个 项目面板展示主机上的所有项目git 仓库),点一下某个项目 → 应用自动 spawn 一个 Claude Code sessioncwd = 该仓库目录、自动运行 claude),并把 tab 标签标注为 repo 名关键结论: 终端机制几乎零改动——按目录 spawn (attach.cwd)、自动输入命令 (initialInput)、自定义标签 (customTitle) 全部已存在。新代码只有两块:git 仓库发现 + GET /projects 端点Projects 面板 UI


1. 用户故事 (User Stories)

  • US1(核心):作为开发者,我打开 web-terminal 首页,看到我所有项目的卡片墙;点 web-terminal 卡片,立刻开出一个新 tab标签写着 web-terminal,里面已经 cd 到该目录并启动了 claude,我直接开始派活。
  • US2:每个项目卡片显示它的当前分支、是否有未提交改动 (dirty)、以及是否已有正在运行的 session(避免重复开)。
  • US3:如果某项目已经有一个跑着的 session点卡片直接进入那个 session(而不是再开一个)。
  • US4:我能搜索/过滤项目(项目多时),把常用项目置顶/收藏
  • US5:我能在卡片上选择"新开 Claude session"还是"只开 shell(不自动跑 claude",或"resume 该项目最近一次 Claude 会话"。

2. 竞品参考 (Prior Art)

调研了主流的"项目选择器 → 启动 agent 会话"产品,提炼可借鉴点:

产品 项目发现方式 卡片展示的元数据 启动交互 可借鉴 UX
CloudCLI / claudecodeui (siteboon) 自动扫描 ~/.claude/projects/ 配置目录 项目名、路径、历史 session 列表 点项目 → 列出其 sessions → 选一个 resume / 新建 侧边栏树形项目→会话二级结构;与本地 Claude 配置同步
官方 Claude Code ~/.claude/projects/ 按 cwd 路径 keyed cwd、历史会话 claude 在 cwd 启动;claude --resume <id> "项目 = 工作目录"这一极简心智模型
Conductor (Melty Labs, Mac) 每个 workspace = 一个 git worktree 看板:每个 agent 在干嘛 / 卡在哪 把 ticket 拉进 workspace 当任务描述 看板式 dashboard + "拉一个 ticket 进来就开干"
Crystal / Nimbalyst (stravu) 多个并行 session 跑在 git worktree 并行会话、方案对比 一个项目开多个并行 session 比较产出 同一项目并行多 session、对比方案
AgentsRoom 手机端远程 终端输出、待回答提示 手机监控 + 回答 prompt 移动端"监控 + 审批"闭环(本项目 v0.3 已有类似)
tmuxinator / sesh 扫描配置/目录列出 project 项目名 一键 attach "每项目一会话" "每项目一会话"的命名与复用

结论 / 我们要偷的点:

  1. 项目 = 工作目录(官方心智模型)——发现逻辑围绕"目录是不是 git 仓库"。
  2. 二级结构CloudCLI项目 → 该项目下的历史/活动 session点项目可"新建"或"进入已有"。
  3. 看板式状态Conductor卡片直接显示该项目有没有 running session、在 working/waiting/idle 哪个状态(本项目 v0.3 hook 状态可复用)。
  4. git 元数据(分支 + dirty让卡片"活"起来。
  5. 本项目的差异化护城河自托管、不经云、多设备镜像同看、缩略图会话墙——Project 面板应与这些无缝融合(项目卡片可直接复用实时缩略图)。

3. 功能需求 (Functional Requirements)

ID 需求 优先级
FR1 提供 GET /projects,返回主机上发现的项目列表(名称、绝对路径、是否 git、分支、dirty、最近活跃时间 P0
FR2 项目发现:扫描 1~N 个配置的根目录(默认 ~,可 env 覆盖)下的 git 仓库;并合并 ~/.claude/projects 里出现过的 cwd最近用过的项目 P0
FR3 首页提供 Projects 视图(与现有 Sessions 选择器并列/可切换),渲染项目卡片网格 P0
FR4 点项目卡片主操作 → 新开 tabcwd=仓库路径、自动运行 claude、tab 标签=repo 名 P0
FR5 一个项目可对应多个 session1:N。卡片展示该项目下所有运行中的 session(每个含名称/Claude 状态/缩略图),可分别进入;同时永远提供" 新开"。详见 §4.6 P0
FR6 卡片显示 当前分支 + dirty 标记 P1
FR7 项目搜索/过滤框;收藏/置顶localStorage 持久化) P1
FR8 「+ 新开」菜单:新开 claude / 只开 shell / claude --resume <最近会话> P2
FR9 性能:发现逻辑有深度上限、跳过 node_modules/.git 内部、结果缓存,避免扫盘卡顿 P0

4. 设计与架构 (Design)

4.1 数据流

首页 Projects 视图
   │  GET /projects
   ▼
src/http/projects.ts  ──扫描配置根目录找 .git──►  ProjectInfo[]
   │                  ──合并 ~/.claude/projects 的 cwd──►
   │                  ──对每个 repo 读 branch / dirty──►
   ▼
卡片网格 (public/projects.ts)
   │  点卡片
   ▼
TabApp.openProject(repoPath, repoName)
   │  = addEntry(null, repoName, repoPath, 'claude')   ← 全部已存在的能力
   ▼
TerminalSession: attach{cwd: repoPath} → spawn PTY(cwd) → 700ms 后自动输入 "claude\r"
   │
   ▼
tab 标签 = customTitle(repoName)最高优先级shell 之后发的 OSC 标题不会覆盖)

核心洞察:从"点项目"到"开出带名字、跑着 claude 的 session"这条链路,终端侧零改动——直接调 TabApp.addEntry(null, repoName, repoPath, 'claude') 即可(见集成点表)。

4.2 既有集成点(无需改动,直接复用)

能力 现状 钩子位置
按目录 spawn PTY 已有 (M6) attach.cwdmanager.handleAttach(…, cwd)createSession(…, cwd)spawn({cwd}) (src/session/session.ts:93)
自动运行 claude 已有 (O2, 客户端驱动) TerminalSessionOpts.initialInputattached 后 700ms 自动输入 (public/terminal-session.ts:253-261)
标签=repo 名 已有 addEntry(null, repoName, repoPath, 'claude')customTitle 优先级最高 (public/tabs.ts:107,143)
启动器视图骨架 可复用 mountLauncher / .mg-grid / makePreviewCard (public/launcher.ts, public/preview-grid.ts)
历史 cwd 扫描模板 可复用 listSessions()~/.claude/projects/*.jsonl,提取 cwd (src/http/history.ts:80)

4.3 新增后端:src/http/projects.ts(模仿 history.ts

// 在 src/types.ts 增加共享契约(协调点,不要本地重声明)
export interface ProjectInfo {
  name: string;        // repo 目录名
  path: string;        // 绝对路径
  isGit: boolean;
  branch?: string;     // 当前分支git 仓库才有)
  dirty?: boolean;     // 有无未提交改动
  lastActiveMs?: number; // 来自 ~/.claude/projects 的最近 mtime用于排序
  sessions: ProjectSessionRef[]; // 该项目下当前运行中的 session1:N可能为空
}

// 一个项目下的某个运行中 session 的引用(由 /projects 把 live-sessions 按 cwd 归并而来)
export interface ProjectSessionRef {
  id: string;          // sessionId
  title?: string;      // tab 标题 / 自动名(如 'claude', 'shell', 'test'
  claudeStatus?: 'working' | 'waiting' | 'idle'; // 复用 v0.3 hook 状态
  startedMs: number;
  clients: number;     // 当前挂着几个 WS 客户端(多设备镜像)
}
// src/http/projects.ts 纯异步、best-effort、parse 逻辑可单测)
export async function listProjects(cfg): Promise<ProjectInfo[]>
//  1. roots = cfg.projectRoots默认 [os.homedir()]
//  2. 广度优先扫描,遇到 .git 即记为 repo 并停止下钻该子树
//     - 深度上限 cfg.projectScanDepth默认 4
//     - 跳过 node_modules / .git / 隐藏目录 / 符号链接环
//  3. 合并 history.ts 收集到的 cwd最近用过但不在扫描根下的项目
//  4. 对每个 repo读分支 = 解析 .git/HEAD轻量免 spawn git
//     dirty = `git status --porcelain` 截断输出(可选、可惰性/并发限流)
//  5. 去重(by path)、按 lastActiveMs 倒序、限量返回

分支读取:优先 读取 .git/HEADref: refs/heads/<branch>)避免为每个 repo spawn git扫描快。dirty 状态较贵(要 git status),可惰性(卡片展开/hover 时再查)或并发限流(一次最多 N 个)以免大量 repo 卡顿。

4.4 新增端点:GET /projects

只读发现端点,/sessions/live-sessions 同类——不需要 Origin/CSRF 守卫(那是给状态变更路由的)。放在 src/server.ts:151 附近:

app.get('/projects', async (_req, res) => res.json(await listProjects(cfg)));

4.5 前端:public/projects.ts + TabApp.openProject

  • mountProjects(host, { onOpenProject, onEnterSession }),与 mountLauncher 并列挂载(tabs.ts:59 旁)。
  • 首页加 Sessions ↔ Projects 切换(两个 tab/段控),复用 .mg-gridel() 卡片工具。
  • 项目卡片repo 名 + 分支 chip + dirty 圆点 + session 计数徽标 + 该项目所有 running session 列表(见 §4.6)。
  • openProject(新开)与 openSession(进入既有,已存在于 tabs.ts:129
// public/tabs.ts 新增(与 newTabForResume 几乎同构)
openProject(repoPath: string, repoName: string, cmd = 'claude') {
  // 同名多开时加序号后缀,避免标签全叫 'web-terminal'
  const n = this.countOpenWithTitlePrefix(repoName);
  const label = n === 0 ? repoName : `${repoName} #${n + 1}`;
  this.addEntry(null, label, repoPath, cmd); // customTitle, cwd, initialInput
  this.rebuild(); // + activate 新 tab
}

4.6 一个项目 ↔ 多个 session1:N核心修正

一个项目常会同时存在多个 session一个 claude + 一个跑测试/git 的 shell、或并行开几个 claude 比方案Conductor/Crystal 模式)、或同一会话被多设备镜像。因此卡片不能假设"0 或 1 个会话"。

归并逻辑(后端 listProjects:把 manager.list()GET /live-sessions,每个含 cwdclients,见 session.ts:116)按 cwd 前缀归属到项目,填进 ProjectInfo.sessions[]Claude 状态来自 v0.3 hook 侧信道(按 sessionId

匹配口径:以 session 的spawn cwd 为锚(稳定,不随用户 cd 漂移)。cwd === project.pathcwdproject.path 之下都算该项目。git worktree(同 repo、不同路径v0.6 先按各自路径当独立项目呈现;"按 repo 把多个 worktree 收拢成一组"列入未来扩展§9

卡片交互(按 session 数自适应)

该项目 running session 数 卡片主体 主操作
0 " start claude here" 占位 Open ▸ claudeopenProject
1 1 个 session 行(状态点 + 名称 + 缩略图) 点行 = 进入;底部 New
N N 个 session 行(各带 ⚙//✓ 状态、镜像端数、可单独进入/Kill 每行点击进入对应 session底部 New claude ▾
  • 每个 session 行:● 状态点 + 标题claude / shell / #2…)+ 👥×2(镜像设备数)+ 点击 → onEnterSession(id) = TabApp.openSession(id)(已有,会聚焦既有 tab 或挂载该 session不重复开
  • 行尾 = Kill 该 session复用 DELETE /live-sessions/:id,带 Origin 守卫)。
  • 「+ New」永远在因为"已有会话"不代表"不想再开一个"。 展开 = 新 claude / 只开 shell / claude --resume
  • 同名多 tabopenProject 给第 2+ 个加 #n 后缀(如 web-terminal #2),避免标签无法区分。

5. 安全考量 (Security)

  • GET /projects 只读,符合现有威胁模型(工具已对 LAN 交出整个 shell暴露目录名不额外升级风险——与 /sessions 一致,不加 Origin 守卫,但也不能写
  • 路径越界:发现的路径仅在服务端本地枚举;前端传回的 cwd 走既有 attach.cwd 校验(必须绝对路径,protocol.ts:142),不引入新攻击面。
  • 扫描成本即 DoS 面:深度上限 + 跳过大目录 + 结果缓存(如 10s TTL避免被频繁刷 /projects 拖垮磁盘。
  • dirty 检查用 git status 会 spawn 子进程——限流 + 超时,防止 repo 多时 fork 爆炸。

6. 配置 (新增 env沿用"无硬编码"原则)

Env 默认 说明
PROJECT_ROOTS ~home 目录) 逗号分隔的扫描根目录
PROJECT_SCAN_DEPTH 4 扫描最大深度
PROJECT_SCAN_TTL 10000 (ms) /projects 结果缓存时长
PROJECT_DIRTY_CHECK 1 是否计算 dirty0=关,省 git 调用)

7. 任务拆解 (v0.6 — 按 PLAN/Owns 模型)

沿用 v0.4/v0.5 的做法:作为 feature 条目并入,定义 disjoint Owns: 便于并行派给 module-builder

任务 Owns独占写 依赖 说明
P1 类型契约 src/types.ts(加 ProjectInfo,改 Config 加 project* 字段) 协调点,先落地,解锁并行
P2 后端发现 src/http/projects.ts + test/projects.test.ts P1 git 扫描 + HEAD/dirty + 合并 history cwd + 缓存TDDparse 纯函数单测)
P3 配置 src/config.ts(加 projectRoots/scanDepth/... P1 env 解析 + 默认值
P4 端点接线 src/server.ts(加 GET /projects 一行 + import P2,P3 /sessions 同位
P5 前端面板 public/projects.ts + 样式 P1 卡片网格、搜索、收藏、切换视图
P6 Tab 接线 public/tabs.tsopenProject + 挂载 mountProjects + running-session 匹配) P5 复用 addEntry/openSession

P2/P3/P5 同波次、文件 disjoint可并行P4/P6 收尾接线。建议 builder 用 isolation: worktree


8. 验收标准 (Acceptance)

  • A1GET /projects 在含多个 git 仓库的目录树下返回正确列表(名称/路径/分支/dirty扫描有深度上限、跳过 node_modules10s 内缓存命中。
  • A2:首页能切到 Projects 视图,渲染卡片网格;分支与 dirty 显示正确。
  • A3:点一个无运行会话的项目 → 新 tab 标签=repo 名、cwd 正确、约 0.7s 后自动出现 claude 启动界面。
  • A4:点一个已有运行会话的项目 → 进入既有 session不重复开。
  • A5:搜索过滤、收藏置顶生效并持久化。
  • A6:后端测试覆盖发现/解析逻辑≥80%,纯函数单测 + 临时目录集成)。

9. 未来扩展 / 暂不做 (Out of Scope for v0.6)

  • git worktree 看板Conductor/Crystal 式:一个项目并行多 worktree 多 agent——强大但属另一个大特性v0.6 先做"一项目一会话"。
  • 项目内文件树/编辑器/Git ExplorerCloudCLI 有)——与本项目 byte-shuttle 哲学冲突,明确不做。
  • 从 ticket/issue 拉任务进项目Conductor 式)——后续可作为 v0.7 与 hook 系统结合。
  • 多 agent 中立Cursor/Codex/Gemini——目前 hooks 与 claude 命令是 Claude 专用;如需中立化,initialInput 可改为可配置命令。

附:一句话总结

这是一个**"发现源 + 启动器视图"特性。开 session 的终端机制(按目录 spawn、自动跑 claude、标签=repo 名)今天就能做到,只差仓库发现(GET /projects项目面板 UI**两块新代码。