Files
CodePilot/docs/handover/git-terminal-layout.md
T
7418andClaude Opus 4.6 0eabed8c27 feat: Git integration, terminal, unified layout — full Phase 1-5 implementation
**Layout refactor (Phase 1):**
- UnifiedTopBar replacing Header.tsx — chat title editing, project folder,
  commit/push split button, Git branch+dirty badge, terminal/file-tree toggles
- PanelZone renders FileTree, GitPanel, PreviewPanel side-by-side on the right
- Independent panel toggle states in PanelContext
- ResizeHandle unified across all panels
- Removed old RightPanel, Header, DocPreview components

**Git backend (Phase 2):**
- src/lib/git/service.ts — execFile-based Git operations with timeout + injection safety
- API routes: status, branches, checkout, log, commit, push, commit-detail, worktrees, derive
- Worktree derive creates DB session inheriting source session settings

**Git frontend (Phase 3):**
- GitPanel with 4 collapsible sections: Status, Branches, History, Worktrees
- CommitDialog with commit vs commit-and-push modes
- GitBranchSelector with inline error, worktree-occupied branches disabled
- GitWorktreeSection with switch-to/derive actions, current/dirty indicators
- useGitStatus polled in AppShell, derived into context (no redundant setState)

**Terminal (Phase 4):**
- electron/terminal-manager.ts — child_process.spawn fallback (PTY upgrade documented)
- IPC handlers + TerminalDrawer/TerminalInstance components

**Polish (Phase 5):**
- Push inline feedback with 4s auto-dismiss
- Branch detection fix, commit diff exit-code handling
- Removed redundant git state: currentBranch/gitDirtyCount now derived, not setState
- useTerminal isElectron uses lazy initializer instead of effect
- i18n keys (en + zh), user tutorial docs, handover document

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 00:38:00 +08:00

198 lines
9.4 KiB
Markdown
Raw 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.
# Git 集成 + 终端 + 统一布局重构
> 完成时间:2026-03-13 | 涉及 ~50 个文件 | 5 阶段交付
## 概述
将 CodePilot 从纯聊天界面升级为轻量桌面聊天工作区:新增独立 Git 功能面板、交互式终端抽屉、统一顶栏,并将布局从左右面板模型重构为四层纵向架构。
## 布局架构
```
┌───────────────────────────────────────────────────────────────┐
│ UnifiedTopBar (固定顶栏, Electron drag, h-12) │
├───────────────────────────────────────────┬───────────────────┤
│ │ PanelZone (右侧) │
│ Chat Main Area │ Preview | Git | │
│ (flex-1, overflow-hidden) │ FileTree │
│ │ (各面板独立开关) │
├───────────────────────────────────────────┴───────────────────┤
│ TerminalDrawer (底部抽屉, 默认隐藏, 可 resize) │
└───────────────────────────────────────────────────────────────┘
```
关键布局决策:
- PanelZone 在聊天区**右侧**(非上方),因为上方展开会压缩聊天区高度,体验不佳
- 面板固定顺序:Preview | Git | FileTree(从左到右)
- 每个面板独立开关,可同时打开多个
- 所有面板支持拖动调整宽度,统一使用 `ResizeHandle` 组件
## 状态模型
### PanelContext (`src/hooks/usePanel.ts`)
废弃旧的 `panelOpen` / `panelContent` 单面板模型,改为独立布尔状态:
```
fileTreeOpen / setFileTreeOpen — 文件树面板
gitPanelOpen / setGitPanelOpen — Git 面板
previewOpen / setPreviewOpen — 文件预览面板
terminalOpen / setTerminalOpen — 终端抽屉
currentBranch / setCurrentBranch — 当前分支名(供顶栏显示)
gitDirtyCount / setGitDirtyCount — 已跟踪变更数(不含 untracked)
```
`currentBranch` 和 `gitDirtyCount` 由 `AppShell` 中的 `useGitStatus` hook 驱动,不依赖 Git 面板是否打开。
### 跨组件通信
使用 CustomEvent 进行松耦合通信:
| 事件名 | 触发方 | 监听方 | 用途 |
|--------|--------|--------|------|
| `git-refresh` | CommitDialog, GitPanelContainer refresh 按钮 | useGitStatus, GitHistorySection | 提交/推送后刷新 Git 状态 |
| `session-updated` | UnifiedTopBar 标题编辑 | ChatListPanel | 会话标题变更同步到列表 |
| `attach-file-to-chat` | FileTreePanel | MessageInput | 文件树中添加文件到聊天 |
## 统一顶栏 (`UnifiedTopBar.tsx`)
```
[聊天标题 ✏️] / [项目文件夹名] [提交全部 ▾] [Git main · N] [终端] [文件树]
```
- 左侧:聊天标题(可编辑,铅笔图标常显)+ 斜杠 + 项目文件夹名(可点击打开)
- 右侧按钮顺序:提交全部(带下拉含推送选项)→ Git(含分支名+脏文件数)→ 终端 → 文件树
- 按钮图标默认 `text-muted-foreground`,hover 变 `text-foreground`,激活态用 `variant="secondary"`
- 提交按钮用 `border border-border` 描边,文字和图标用正常前景色
- 仅在 `/chat/[id]` 路由显示操作按钮,`/chat` 空页面不显示
- Electron 拖拽区:整个顶栏 `WebkitAppRegion: drag`,交互区域设为 `no-drag`
## Git 后端 (`src/lib/git/service.ts`)
所有函数接收 `cwd` 参数,通过 `execFile` 执行(非 `exec`,防 shell 注入):
| 函数 | 超时 | 说明 |
|------|------|------|
| `isGitRepo` | 5s | `rev-parse --is-inside-work-tree` |
| `getRepoRoot` | 5s | `rev-parse --show-toplevel` |
| `getStatus` | 10s | porcelain v2 解析,分离 tracked/untracked |
| `getBranches` | 10s | `-a` 列出所有分支,**用 `git branch`(不带 -a)获取本地分支列表来区分 local/remote** |
| `checkout` | 15s | 检查 dirty 状态后切换 |
| `getLog` | 10s | 自定义分隔符格式化输出 |
| `commit` | 30s | `add -A` → `diff --cached --quiet` 检查 → `commit` |
| `push` | 30s | 自动 `set-upstream` 如果需要 |
| `getWorktrees` | 10s | porcelain 解析 + **逐个 worktree 检查 dirty 状态** |
| `deriveWorktree` | 30s | `worktree add -b` |
### 已修复的问题
1. **分支 `/` 误判**:`feat/xxx` 这类本地分支名曾被 `includes('/')` 误标为 remote。修复方案:先用 `git branch`(无 `-a`)获取本地分支名集合,只有不在本地集合中的才标 `isRemote`。
2. **Commit diff 检测**:`git diff --cached --quiet` exit 1 = 有变更,`runGit` 会 reject。原始逻辑正确地 catch 并继续;如有真实错误(如索引损坏),后续 `git commit` 会给出正确报错。
3. **Worktree dirty 状态**:不再硬编码 `false`,改为对每个非 bare worktree 执行 `git status --porcelain --untracked-files=no`。
## Git 前端面板
### 组件层次
```
panels/GitPanel.tsx (GitPanelContainer)
├── ResizeHandle (left side, 280-600px)
├── Header: 标题 + 刷新按钮 + 关闭按钮
└── git/GitPanel.tsx (业务容器, 4 个可折叠区块)
├── GitStatusSection — 分支 + upstream + ahead/behind + 变更文件列表
├── GitBranchSelector — 分支切换(dirty 时禁用,worktree 占用时标注)
├── GitHistorySection — 提交历史(订阅 git-refresh 事件)
└── GitWorktreeSection — 工作树列表 + 切换 + 派生
```
### 变更文件列表
- 已跟踪变更(M/A/D/R)排前面,untracked 文件单独一组
- 顶栏脏文件计数只统计已跟踪变更,不含 untracked
- 无变更时显示"所有更改都已提交"
### 提交流程
1. 顶栏"提交全部"按钮 → 打开 `CommitDialog`
2. Dialog 提供两个 radio 选项:仅提交 / 提交并推送
3. 提交成功后触发 `git-refresh` 事件,所有 Git 相关组件刷新
4. 推送通过 dropdown 也可以单独触发
### Worktree 操作
- 列表显示 current 标识(高亮 + badge)和 dirty 指示器
- "切换到工作树"按钮:先查 `/api/chat/sessions/by-cwd` 找现有 session,没有则 POST 创建新 session,然后 `router.push`
- "派生新工作树":创建 git worktree + 创建新 DB session(继承源 session 的 model/mode/provider 等),自动跳转
### 分支切换错误处理
`handleCheckout` 在 `res.ok === false` 时抛异常 → `GitBranchSelector` catch 后设置 `localError`,分支列表保持展开不收起。
## 终端 (`electron/terminal-manager.ts`)
### 当前实现(Phase 4 v1)
使用 `child_process.spawn` + `stdio: 'pipe'`,**不是真正的 PTY**。已知限制:
- `resize()` 是 no-op
- 全屏程序(vim, htop)无法正常渲染
- readline 行编辑受限
### 升级到 node-pty 的步骤
1. `npm install node-pty`
2. `scripts/after-pack.js`:rebuild 列表添加 `node-pty`
3. `electron-builder.yml`:asarUnpack 添加 `"**/node-pty/**"`
4. `scripts/build-electron.mjs`:esbuild externals 添加 `node-pty`
5. `terminal-manager.ts`:`spawn` → `pty.spawn`,实现 `resize()`
### 前端组件
- `TerminalDrawer.tsx`:底部抽屉,可 resize 高度
- `TerminalInstance.tsx`:连接 Electron IPC,管理生命周期
- 快捷键 `Ctrl+`` / `Cmd+`` 切换终端
## Resize Handle 统一
所有可拖动宽度的面板统一使用 `ResizeHandle` 组件(`src/components/layout/ResizeHandle.tsx`):
| 使用方 | side | 宽度范围 | 默认宽度 |
|--------|------|----------|----------|
| ChatListPanel | right | — | — |
| PreviewPanel | left | 320-800px | 480px |
| GitPanelContainer | left | 280-600px | 360px |
| FileTreePanel | left | 220-500px | 280px |
右侧面板的 handle 在 left side,拖动方向:左拖 = 变宽(`w - delta`)。
## API 路由
| 路由 | 方法 | 用途 |
|------|------|------|
| `/api/git/status` | GET | Git 状态(branch, dirty, changedFiles) |
| `/api/git/branches` | GET | 分支列表 |
| `/api/git/checkout` | POST | 切换分支 |
| `/api/git/log` | GET | 提交历史 |
| `/api/git/commit` | POST | 提交(add -A + commit) |
| `/api/git/push` | POST | 推送 |
| `/api/git/commit-detail/[sha]` | GET | 提交详情 + diff |
| `/api/git/worktrees` | GET | 工作树列表 |
| `/api/git/worktrees/derive` | POST | 派生工作树 + 创建 session |
| `/api/chat/sessions/by-cwd` | GET | 按 working_directory 查找 session |
## 已废弃的文件
- `src/components/layout/RightPanel.tsx` — 由 PanelZone + panels/ 替代
- `src/components/layout/Header.tsx` — 由 UnifiedTopBar 替代
- `src/components/layout/DocPreview.tsx` — 由 panels/PreviewPanel.tsx 替代
## 已知技术债务
1. **终端不是 PTY**:见上文升级步骤,需要 native module 编译支持
2. **Git 面板无 staging UI**:当前 commit 直接 `git add -A`,无法选择性 stage/unstage
3. **Worktree 无删除功能**:只有列表和派生,没有 `git worktree remove`
4. **Git 轮询间隔固定 10s**:未做 WebSocket 或 file watcher 优化
## i18n 键
新增键前缀:`topBar.*`(~8 键)、`git.*`(~40 键)、`terminal.*`(~5 键)。分布在 `src/i18n/en.ts` 和 `zh.ts` 中。