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

243 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Feature: Project Manager 项目工作台 (v0.6)
> **状态**: 提案 / Draft探索完成待评审后进入 PLAN
> **目标**: 首页除了"会话选择器"外,新增一个 **项目面板**展示主机上的所有项目git 仓库),点一下某个项目 → 应用自动 spawn 一个 **Claude Code session**cwd = 该仓库目录、自动运行 `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 | **一个项目可对应多个 session**1: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.cwd``manager.handleAttach(…, cwd)``createSession(…, cwd)``spawn({cwd})` (`src/session/session.ts:93`) |
| 自动运行 `claude` | ✅ 已有 (O2, 客户端驱动) | `TerminalSessionOpts.initialInput``attached` 后 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`
```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 客户端(多设备镜像)
}
```
```ts
// 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/HEAD``ref: 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` 附近:
```ts
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-grid``el()` 卡片工具。
- 项目卡片repo 名 + 分支 chip + dirty 圆点 + **session 计数徽标 + 该项目所有 running session 列表**(见 §4.6)。
- `openProject`(新开)与 `openSession`(进入既有,已存在于 `tabs.ts:129`
```ts
// 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`,每个含 `cwd``clients`,见 `session.ts:116`)按 **cwd 前缀归属到项目**,填进 `ProjectInfo.sessions[]`Claude 状态来自 v0.3 hook 侧信道(按 sessionId
> **匹配口径**:以 session 的**spawn cwd** 为锚(稳定,不随用户 `cd` 漂移)。`cwd === project.path` 或 `cwd` 在 `project.path` 之下都算该项目。**git worktree**(同 repo、不同路径v0.6 先按各自路径当**独立项目**呈现;"按 repo 把多个 worktree 收拢成一组"列入未来扩展§9
**卡片交互(按 session 数自适应)**
| 该项目 running session 数 | 卡片主体 | 主操作 |
|---|---|---|
| **0** | " start claude here" 占位 | **Open ▸ claude**`openProject` |
| **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`
- **同名多 tab**`openProject` 给第 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.ts``openProject` + 挂载 `mountProjects` + running-session 匹配) | P5 | 复用 `addEntry`/`openSession` |
> P2/P3/P5 同波次、文件 disjoint可并行P4/P6 收尾接线。建议 builder 用 `isolation: worktree`。
---
## 8. 验收标准 (Acceptance)
- **A1**`GET /projects` 在含多个 git 仓库的目录树下返回正确列表(名称/路径/分支/dirty扫描有深度上限、跳过 `node_modules`10s 内缓存命中。
- **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 Explorer**CloudCLI 有)——与本项目 byte-shuttle 哲学冲突,明确不做。
- 从 ticket/issue **拉任务进项目**Conductor 式)——后续可作为 v0.7 与 hook 系统结合。
- 多 agent 中立Cursor/Codex/Gemini——目前 hooks 与 `claude` 命令是 Claude 专用;如需中立化,`initialInput` 可改为可配置命令。
---
## 附:一句话总结
> 这是一个**"发现源 + 启动器视图"**特性。开 session 的终端机制(按目录 spawn、自动跑 claude、标签=repo 名)**今天就能做到**,只差**仓库发现(`GET /projects`**和**项目面板 UI**两块新代码。