# 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 | 点项目卡片**主操作** → 新开 tab：cwd=仓库路径、自动运行 `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[]; // 该项目下当前运行中的 session（1: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 一个项目 ↔ 多个 session（1: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` | 是否计算 dirty（0=关，省 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 + 缓存（TDD，parse 纯函数单测） |
| **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**两块新代码。
