clawt 2.16.3 → 2.16.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -68,9 +68,9 @@ var RUN_MESSAGES = {
68
68
  /** 非 TTY 环境降级输出:任务启动 */
69
69
  PROGRESS_TASK_STARTED: (index, total, branch, path) => `[${index}/${total}] ${branch} \u542F\u52A8 ${path}`,
70
70
  /** 非 TTY 环境降级输出:任务完成 */
71
- PROGRESS_TASK_DONE: (index, total, branch, duration, cost, path) => `[${index}/${total}] ${branch} \u2713 \u5B8C\u6210 ${duration} ${cost} ${path}`,
71
+ PROGRESS_TASK_DONE: (index, total, branch, duration, cost, detail) => `[${index}/${total}] ${branch} \u2713 \u5B8C\u6210 ${duration} ${cost} ${detail}`,
72
72
  /** 非 TTY 环境降级输出:任务失败 */
73
- PROGRESS_TASK_FAILED: (index, total, branch, duration, path) => `[${index}/${total}] ${branch} \u2717 \u5931\u8D25 ${duration} ${path}`,
73
+ PROGRESS_TASK_FAILED: (index, total, branch, duration, detail) => `[${index}/${total}] ${branch} \u2717 \u5931\u8D25 ${duration} ${detail}`,
74
74
  /** 并发限制提示 */
75
75
  CONCURRENCY_INFO: (concurrency, total) => `\u5E76\u53D1\u9650\u5236: ${concurrency}\uFF0C\u5171 ${total} \u4E2A\u4EFB\u52A1`,
76
76
  /** 并发数无效提示 */
@@ -203,7 +203,16 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
203
203
  /** 并行执行中单个命令失败 */
204
204
  VALIDATE_PARALLEL_CMD_FAILED: (command, exitCode) => ` \u2717 ${command}\uFF08\u9000\u51FA\u7801: ${exitCode}\uFF09`,
205
205
  /** 并行执行中单个命令启动失败 */
206
- VALIDATE_PARALLEL_CMD_ERROR: (command, errorMessage) => ` \u2717 ${command}\uFF08\u9519\u8BEF: ${errorMessage}\uFF09`
206
+ VALIDATE_PARALLEL_CMD_ERROR: (command, errorMessage) => ` \u2717 ${command}\uFF08\u9519\u8BEF: ${errorMessage}\uFF09`,
207
+ /** patch apply 失败后询问用户是否执行 sync */
208
+ VALIDATE_CONFIRM_AUTO_SYNC: (branch) => `\u662F\u5426\u7ACB\u5373\u6267\u884C sync \u540C\u6B65\u4E3B\u5206\u652F\u5230 ${branch}\uFF1F`,
209
+ /** 自动 sync 开始提示 */
210
+ VALIDATE_AUTO_SYNC_START: (branch) => `\u6B63\u5728\u81EA\u52A8\u540C\u6B65\u4E3B\u5206\u652F\u5230 ${branch} ...`,
211
+ /** 自动 sync 存在冲突,无法重试 */
212
+ VALIDATE_AUTO_SYNC_CONFLICT: (worktreePath) => `\u540C\u6B65\u5B58\u5728\u51B2\u7A81\uFF0C\u8BF7\u8FDB\u5165\u76EE\u6807 worktree \u624B\u52A8\u89E3\u51B3\u51B2\u7A81\u540E\u91CD\u8BD5
213
+ cd ${worktreePath}`,
214
+ /** 用户拒绝自动 sync */
215
+ VALIDATE_AUTO_SYNC_DECLINED: (branch) => `\u8BF7\u624B\u52A8\u6267\u884C clawt sync -b ${branch} \u540C\u6B65\u4E3B\u5206\u652F\u540E\u91CD\u8BD5`
207
216
  };
208
217
 
209
218
  // src/constants/messages/sync.ts
package/docs/spec.md CHANGED
@@ -46,6 +46,7 @@
46
46
  | CLI 框架 | Commander.js |
47
47
  | 日志库 | winston (按日期滚动文件) |
48
48
  | 交互式 | enquirer (选项选择/确认对话) |
49
+ | 终端宽度 | string-width (ANSI 安全的字符宽度计算) |
49
50
  | 测试 | Vitest + @vitest/coverage-v8 |
50
51
  | 构建 | tsup / tsc |
51
52
  | 分发 | pnpm 全局安装 (`pnpm add -g clawt`) |
@@ -338,8 +339,9 @@ clawt run -b <branchName>
338
339
  4. 通过公共函数 `executeBatchTasks`(`src/utils/task-executor.ts`)启动批量任务执行,该函数负责进度面板渲染、SIGINT 中断处理、并发控制和汇总输出。对每个 worktree 并行启动 Claude Code CLI:
339
340
  ```bash
340
341
  cd ~/.clawt/worktrees/<project>/<branchName>-<i>
341
- claude -p "<tasks[i]>" --output-format json --permission-mode bypassPermissions
342
+ claude -p "<tasks[i]>" --output-format stream-json --verbose --permission-mode bypassPermissions
342
343
  ```
344
+ 使用 `stream-json` 格式可实时获取 Claude Code 的流式事件(工具调用、文本输出、最终结果),用于在进度面板中显示每个任务的实时活动描述和结果预览。流式事件解析由 `src/utils/stream-parser.ts` 负责。
343
345
  5. 进入**事件监听通知**阶段(见 [5.3](#53-任务完成通知机制))
344
346
  6. **中断处理(Ctrl+C / SIGINT)**
345
347
  - 监听 `SIGINT` 信号,用户按下 Ctrl+C 时触发
@@ -405,7 +407,7 @@ clawt run -b <branchName>
405
407
 
406
408
  **机制说明:**
407
409
 
408
- Claude Code CLI 以 `--output-format json` 运行时,退出后会在 stdout 输出如下 JSON
410
+ Claude Code CLI 以 `--output-format stream-json --verbose` 运行时,stdout 会持续输出 JSON 行(每行一个事件),包括 `system`、`assistant`(含 `tool_use` 和 `text`)、`user`(含 `tool_result`)等类型。任务结束时输出 `type: "result"` 事件:
409
411
 
410
412
  ```json
411
413
  {
@@ -423,12 +425,22 @@ Claude Code CLI 以 `--output-format json` 运行时,退出后会在 stdout
423
425
  }
424
426
  ```
425
427
 
428
+ **流式事件解析(`src/utils/stream-parser.ts`):**
429
+
430
+ 由于 stdout 的 `data` 事件可能在行中间切割,使用 `createLineBuffer()` 行缓冲器拼接完整行后,通过 `parseStreamLine()` 解析为 `StreamEvent` 对象,再由 `parseStreamEvent()` 提取活动信息(`ParsedActivity`):
431
+
432
+ - **`tool_use` 类型**:提取工具名和文件路径/命令参数,格式如 `Read index.ts`、`Bash ls -la`
433
+ - **`text` 类型**:提取文本片段,格式如 `思考中: 让我分析一下`
434
+ - **`result` 类型**:构造 `ClaudeCodeResult` 对象,提取耗时、费用、结果文本等
435
+
436
+ 活动描述文本最大长度为 `ACTIVITY_TEXT_MAX_LENGTH`(30 字符),超出后截断并追加省略号。结果预览文本最大长度为 `RESULT_PREVIEW_MAX_LENGTH`(40 字符)。
437
+
426
438
  **事件监听与通知流程:**
427
439
 
428
440
  1. 为每个 Claude Code 子进程维护状态(运行中 / 已完成 / 已失败)
429
441
  2. 监听每个子进程的 `close` 事件(基于 Node.js `ChildProcess` 的事件驱动机制)
430
- 3. 当某个子进程触发 `close` 事件时,解析其 stdout 输出的 JSON
431
- 4. 在主 worktree 的 clawt 终端实时输出通知(进度面板每个任务行末尾显示 worktree 路径,终端可点击跳转):
442
+ 3. 当某个子进程触发 `close` 事件时,解析流式输出中最后的 `result` 事件
443
+ 4. 在主 worktree 的 clawt 终端实时输出通知。TTY 环境下使用进度面板,进度面板每个任务行第二列显示 worktree 路径(终端可点击跳转),运行中显示实时活动描述,完成/失败后显示结果预览:
432
444
 
433
445
  ```
434
446
  ✓ [完成] worktree: ~/.clawt/worktrees/main-project/feature-scheme-1
@@ -452,6 +464,39 @@ Claude Code CLI 以 `--output-format json` 运行时,退出后会在 stdout
452
464
  ════════════════════════════════════════
453
465
  ```
454
466
 
467
+ #### 进度面板渲染机制
468
+
469
+ 进度面板由 `ProgressRenderer`(`src/utils/progress.ts`)负责渲染,渲染函数拆分到 `src/utils/progress-render.ts`。
470
+
471
+ **TTY 模式渲染策略(备选屏幕缓冲区):**
472
+
473
+ - **进入备选屏幕**:`start()` 时通过 `ALT_SCREEN_ENTER`(`\x1B[?1049h`)进入终端备选屏幕缓冲区,隔离进度面板与主屏幕内容
474
+ - **禁用行换行**:通过 `LINE_WRAP_DISABLE`(`\x1B[?7l`)防止超长行自动折行,配合按终端宽度截断保证每行只占一行
475
+ - **每帧渲染**:使用 `CLEAR_SCREEN` + `CURSOR_HOME` 清屏后完全重绘,无需计算 `CURSOR_UP` 回退量,不受终端 reflow 影响
476
+ - **防闪烁**:每帧渲染使用 Synchronized Output(`SYNC_OUTPUT_START` / `SYNC_OUTPUT_END`),终端缓冲全部输出后一次性刷新
477
+ - **行宽截断**:通过 `truncateToTerminalWidth()`(`src/utils/progress-render.ts`)将含 ANSI 转义码的字符串截断到终端可见列数,使用 `string-width` 库正确计算中文/emoji 宽度
478
+ - **终端 resize 响应**:监听 `process.stdout` 的 `resize` 事件,窗口宽度变化时立即触发重绘
479
+ - **退出时恢复**:`stop()` 时恢复行换行、显示光标、退出备选屏幕,然后在主屏幕上重新输出最终面板状态(备选屏幕内容不保留)
480
+ - **异常退出兜底**:注册 `process.on('exit')` 处理器,确保即使异常退出也能恢复终端状态
481
+
482
+ **任务行格式:**
483
+
484
+ ```
485
+ [1/3] /path/to/worktree ⠹ 运行中 1m23s Read index.ts
486
+ [2/3] /path/to/worktree ✓ 完成 2m05s $0.08 任务已成功完成
487
+ [3/3] /path/to/worktree ● 排队中
488
+ ```
489
+
490
+ - 第二列为 worktree 路径(`path.padEnd(maxPathWidth)` 对齐)
491
+ - 运行中状态:末尾显示实时活动描述文本(如工具名+文件名、思考中+文本片段)
492
+ - 完成/失败状态:末尾显示结果预览文本(从 `ClaudeCodeResult.result` 提取,最大 40 字符)
493
+
494
+ **非 TTY 降级模式:**
495
+
496
+ - 启动时输出 `[1/3] branch 启动 path`
497
+ - 完成时输出 `[1/3] branch ✓ 完成 duration cost detail`(`detail` 优先使用结果预览,无则回退到路径)
498
+ - 失败时输出 `[1/3] branch ✗ 失败 duration detail`
499
+
455
500
  ---
456
501
 
457
502
  ### 5.4 在主 Worktree 验证其他分支
@@ -576,7 +621,26 @@ git restore --staged .
576
621
  ```
577
622
 
578
623
  > 此步骤结束后,目标 worktree 的代码保持原样,主 worktree 工作目录包含目标分支的全量变更。
579
- > 如果 patch apply 失败(目标分支与主分支差异过大),会提示用户先执行 `clawt sync -b <branchName>` 同步主分支后重试。
624
+ > 如果 patch apply 失败(目标分支与主分支差异过大),`migrateChangesViaPatch` 返回 `{ success: false }`,进入自动 sync 交互流程(见下文 [patch apply 失败后的自动 sync 流程](#patch-apply-失败后的自动-sync-流程))。
625
+
626
+ ##### patch apply 失败后的自动 sync 流程
627
+
628
+ 当 patch apply 失败时,validate 不再直接退出,而是通过 `handlePatchApplyFailure()` 函数进入交互流程:
629
+
630
+ 1. **询问用户**:提示 `是否立即执行 sync 同步主分支到 <branchName>?`
631
+ 2. **用户拒绝** → 输出提示 `请手动执行 clawt sync -b <branchName> 同步主分支后重试`,退出
632
+ 3. **用户确认** → 调用 `executeSyncForBranch(targetWorktreePath, branchName)` 自动执行 sync
633
+ - **sync 成功** → validate 流程结束(用户需重新执行 validate)
634
+ - **sync 存在冲突** → 输出提示 `同步存在冲突,请进入目标 worktree 手动解决冲突后重试`,退出
635
+
636
+ > `executeSyncForBranch` 为 sync 命令抽取的核心操作函数(见 [5.12](#512-将主分支代码同步到目标-worktree)),供 validate 等命令复用。
637
+
638
+ **实现要点:**
639
+
640
+ - `migrateChangesViaPatch()` 返回类型从 `void` 改为 `{ success: boolean }`,patch apply 失败时返回 `{ success: false }` 而非抛出异常
641
+ - `handleFirstValidate()` 和 `handleIncrementalValidate()` 从同步函数改为 `async` 函数,以支持交互式确认
642
+ - `handlePatchApplyFailure()` 为新增的异步函数(`src/commands/validate.ts`),负责 patch 失败后的交互逻辑
643
+ - 消息常量:`MESSAGES.VALIDATE_CONFIRM_AUTO_SYNC`、`MESSAGES.VALIDATE_AUTO_SYNC_START`、`MESSAGES.VALIDATE_AUTO_SYNC_CONFLICT`、`MESSAGES.VALIDATE_AUTO_SYNC_DECLINED`(`src/constants/messages/validate.ts`)
580
644
 
581
645
  ##### 步骤 4:保存快照为 git tree 对象
582
646
 
@@ -715,7 +779,7 @@ clawt validate -b feature-scheme-1 -r "pnpm test & pnpm build"
715
779
 
716
780
  ##### 步骤 3:从目标分支获取最新全量变更
717
781
 
718
- 通过 patch 方式从目标分支获取最新全量变更(流程同首次 validate 的步骤 3)。
782
+ 通过 patch 方式从目标分支获取最新全量变更(流程同首次 validate 的步骤 3)。如果 patch apply 失败,同样进入自动 sync 交互流程(见首次 validate 的 [patch apply 失败后的自动 sync 流程](#patch-apply-失败后的自动-sync-流程)),validate 流程提前结束。
719
783
 
720
784
  ##### 步骤 4:保存最新快照为 git tree 对象
721
785
 
@@ -1356,16 +1420,36 @@ clawt sync
1356
1420
  - 唯一匹配 → 直接使用
1357
1421
  - 多个匹配 → 通过交互式列表让用户从匹配结果中选择
1358
1422
  3. **无匹配** → 报错退出,并列出所有可用分支名
1359
- 3. **获取主分支名**:通过 `git rev-parse --abbrev-ref HEAD` 获取主 worktree 当前分支名(不硬编码 main/master)
1360
- 4. **自动保存未提交变更**:检查目标 worktree 是否有未提交修改
1423
+ 3. 调用 `executeSyncForBranch(targetWorktreePath, branch)` 执行核心同步逻辑
1424
+
1425
+ #### `executeSyncForBranch` — sync 核心操作函数
1426
+
1427
+ `executeSyncForBranch(targetWorktreePath: string, branch: string): SyncResult` 是从 `handleSync` 中抽取的核心同步逻辑,不包含 worktree 解析交互,供 validate 等命令复用。
1428
+
1429
+ **接口定义:**
1430
+
1431
+ ```typescript
1432
+ /** sync 核心操作的执行结果 */
1433
+ export interface SyncResult {
1434
+ /** 是否同步成功 */
1435
+ success: boolean;
1436
+ /** 是否存在合并冲突 */
1437
+ hasConflict: boolean;
1438
+ }
1439
+ ```
1440
+
1441
+ **执行流程:**
1442
+
1443
+ 1. **获取主分支名**:通过 `git rev-parse --abbrev-ref HEAD` 获取主 worktree 当前分支名(不硬编码 main/master)
1444
+ 2. **自动保存未提交变更**:检查目标 worktree 是否有未提交修改
1361
1445
  - 有修改 → 自动执行 `git add . && git commit -m "<AUTO_SAVE_COMMIT_MESSAGE>"` 保存变更(commit message 由常量 `AUTO_SAVE_COMMIT_MESSAGE` 定义,值为 `chore: auto-save before sync`,同时用于 merge 命令的 squash 检测)
1362
1446
  - 无修改 → 跳过
1363
- 5. **在目标 worktree 中合并主分支**:
1447
+ 3. **在目标 worktree 中合并主分支**:
1364
1448
  ```bash
1365
1449
  cd ~/.clawt/worktrees/<project>/<branchName>
1366
1450
  git merge <mainBranch>
1367
1451
  ```
1368
- 6. **冲突处理**:
1452
+ 4. **冲突处理**:
1369
1453
  - **有冲突** → 输出警告,提示用户进入目标 worktree 手动解决:
1370
1454
  ```
1371
1455
  合并存在冲突,请进入目标 worktree 手动解决:
@@ -1373,9 +1457,10 @@ clawt sync
1373
1457
  解决冲突后执行 git add . && git merge --continue
1374
1458
  clawt validate -b <branch> 验证变更
1375
1459
  ```
1460
+ - 返回 `{ success: false, hasConflict: true }`
1376
1461
  - **无冲突** → 继续
1377
- 7. **清除 validate 快照**:合并成功后,如果该分支存在 validate 快照(`.tree` 和 `.head` 文件),自动删除(代码基础已变化,旧快照无效)
1378
- 8. **输出成功提示**:
1462
+ 5. **清除 validate 快照**:合并成功后,如果该分支存在 validate 快照(`.tree` 和 `.head` 文件),自动删除(代码基础已变化,旧快照无效)
1463
+ 6. **输出成功提示**并返回 `{ success: true, hasConflict: false }`:
1379
1464
  ```
1380
1465
  ✓ 已将 <mainBranch> 的最新代码同步到 <branchName>
1381
1466
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "2.16.3",
3
+ "version": "2.16.5",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -19,6 +19,7 @@
19
19
  "chalk": "^5.4.1",
20
20
  "commander": "^13.1.0",
21
21
  "enquirer": "^2.4.1",
22
+ "string-width": "^8.2.0",
22
23
  "winston": "^3.17.0",
23
24
  "winston-daily-rotate-file": "^5.0.0"
24
25
  },
@@ -77,21 +77,22 @@ function mergeMainBranch(worktreePath: string, mainBranch: string): boolean {
77
77
  }
78
78
  }
79
79
 
80
+ /** sync 核心操作的执行结果 */
81
+ export interface SyncResult {
82
+ /** 是否同步成功 */
83
+ success: boolean;
84
+ /** 是否存在合并冲突 */
85
+ hasConflict: boolean;
86
+ }
87
+
80
88
  /**
81
- * 执行 sync 命令的核心逻辑
82
- * 将主分支最新代码同步到目标 worktree
83
- * @param {SyncOptions} options - 命令选项
89
+ * 执行 sync 核心操作(检查未提交→自动保存→merge 主分支→清除快照)
90
+ * 不包含 worktree 解析交互,供 validate 等命令复用
91
+ * @param {string} targetWorktreePath - 目标 worktree 路径
92
+ * @param {string} branch - 分支名
93
+ * @returns {SyncResult} sync 执行结果
84
94
  */
85
- async function handleSync(options: SyncOptions): Promise<void> {
86
- validateMainWorktree();
87
-
88
- logger.info(`sync 命令执行,分支: ${options.branch ?? '(未指定)'}`);
89
-
90
- // 解析目标 worktree(精确匹配 / 模糊匹配 / 交互选择)
91
- const worktrees = getProjectWorktrees();
92
- const worktree = await resolveTargetWorktree(worktrees, SYNC_RESOLVE_MESSAGES, options.branch);
93
- const { path: targetWorktreePath, branch } = worktree;
94
-
95
+ export function executeSyncForBranch(targetWorktreePath: string, branch: string): SyncResult {
95
96
  // 获取主分支名(不硬编码 main/master)
96
97
  const mainWorktreePath = getGitTopLevel();
97
98
  const mainBranch = getCurrentBranch(mainWorktreePath);
@@ -107,7 +108,7 @@ async function handleSync(options: SyncOptions): Promise<void> {
107
108
 
108
109
  if (hasConflict) {
109
110
  printWarning(MESSAGES.SYNC_CONFLICT(targetWorktreePath));
110
- return;
111
+ return { success: false, hasConflict: true };
111
112
  }
112
113
 
113
114
  // 合并成功后清除该分支的 validate 快照(代码基础已变化,旧快照无效)
@@ -118,4 +119,23 @@ async function handleSync(options: SyncOptions): Promise<void> {
118
119
  }
119
120
 
120
121
  printSuccess(MESSAGES.SYNC_SUCCESS(branch, mainBranch));
122
+ return { success: true, hasConflict: false };
123
+ }
124
+
125
+ /**
126
+ * 执行 sync 命令的核心逻辑
127
+ * 将主分支最新代码同步到目标 worktree
128
+ * @param {SyncOptions} options - 命令选项
129
+ */
130
+ async function handleSync(options: SyncOptions): Promise<void> {
131
+ validateMainWorktree();
132
+
133
+ logger.info(`sync 命令执行,分支: ${options.branch ?? '(未指定)'}`);
134
+
135
+ // 解析目标 worktree(精确匹配 / 模糊匹配 / 交互选择)
136
+ const worktrees = getProjectWorktrees();
137
+ const worktree = await resolveTargetWorktree(worktrees, SYNC_RESOLVE_MESSAGES, options.branch);
138
+ const { path: targetWorktreePath, branch } = worktree;
139
+
140
+ executeSyncForBranch(targetWorktreePath, branch);
121
141
  }
@@ -4,6 +4,7 @@ import { logger } from '../logger/index.js';
4
4
  import { ClawtError } from '../errors/index.js';
5
5
  import { MESSAGES } from '../constants/index.js';
6
6
  import type { ValidateOptions } from '../types/index.js';
7
+ import { executeSyncForBranch } from './sync.js';
7
8
  import {
8
9
  validateMainWorktree,
9
10
  getProjectName,
@@ -33,6 +34,7 @@ import {
33
34
  writeSnapshot,
34
35
  removeSnapshot,
35
36
  confirmDestructiveAction,
37
+ confirmAction,
36
38
  printSuccess,
37
39
  printError,
38
40
  printWarning,
@@ -121,8 +123,9 @@ async function handleDirtyMainWorktree(mainWorktreePath: string): Promise<void>
121
123
  * @param {string} mainWorktreePath - 主 worktree 路径
122
124
  * @param {string} branchName - 分支名
123
125
  * @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
126
+ * @returns {{ success: boolean }} patch 迁移结果
124
127
  */
125
- function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: string, branchName: string, hasUncommitted: boolean): void {
128
+ function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: string, branchName: string, hasUncommitted: boolean): { success: boolean } {
126
129
  let didTempCommit = false;
127
130
 
128
131
  try {
@@ -143,9 +146,11 @@ function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: st
143
146
  } catch (error) {
144
147
  logger.warn(`patch apply 失败: ${error}`);
145
148
  printWarning(MESSAGES.VALIDATE_PATCH_APPLY_FAILED(branchName));
146
- throw error;
149
+ return { success: false };
147
150
  }
148
151
  }
152
+
153
+ return { success: true };
149
154
  } finally {
150
155
  // 确保临时 commit 一定会被撤销,恢复目标 worktree 原状
151
156
  // 每个操作独立 try-catch,避免前一个失败导致后续操作不执行
@@ -164,6 +169,31 @@ function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: st
164
169
  }
165
170
  }
166
171
 
172
+ /**
173
+ * patch apply 失败后的交互处理:询问用户是否自动执行 sync
174
+ * @param {string} targetWorktreePath - 目标 worktree 路径
175
+ * @param {string} branchName - 分支名
176
+ */
177
+ async function handlePatchApplyFailure(targetWorktreePath: string, branchName: string): Promise<void> {
178
+ // 询问用户是否自动执行 sync
179
+ const confirmed = await confirmAction(MESSAGES.VALIDATE_CONFIRM_AUTO_SYNC(branchName));
180
+
181
+ if (!confirmed) {
182
+ // 用户拒绝自动 sync
183
+ printWarning(MESSAGES.VALIDATE_AUTO_SYNC_DECLINED(branchName));
184
+ return;
185
+ }
186
+
187
+ // 用户确认,执行 sync
188
+ printInfo(MESSAGES.VALIDATE_AUTO_SYNC_START(branchName));
189
+ const syncResult = executeSyncForBranch(targetWorktreePath, branchName);
190
+
191
+ if (syncResult.hasConflict) {
192
+ // sync 存在冲突,提示用户手动解决
193
+ printWarning(MESSAGES.VALIDATE_AUTO_SYNC_CONFLICT(targetWorktreePath));
194
+ }
195
+ }
196
+
167
197
  /**
168
198
  * 保存当前主 worktree 工作目录变更为 git tree 对象快照
169
199
  * 操作序列:git add . → git write-tree → git restore --staged .
@@ -231,9 +261,15 @@ async function handleValidateClean(options: ValidateOptions): Promise<void> {
231
261
  * @param {string} branchName - 分支名
232
262
  * @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
233
263
  */
234
- function handleFirstValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): void {
264
+ async function handleFirstValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): Promise<void> {
235
265
  // 通过 patch 迁移目标分支全量变更到主 worktree
236
- migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
266
+ const result = migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
267
+
268
+ if (!result.success) {
269
+ // patch 失败,询问用户是否自动 sync
270
+ await handlePatchApplyFailure(targetWorktreePath, branchName);
271
+ return;
272
+ }
237
273
 
238
274
  // 保存快照为 git tree 对象
239
275
  saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
@@ -250,7 +286,7 @@ function handleFirstValidate(targetWorktreePath: string, mainWorktreePath: strin
250
286
  * @param {string} branchName - 分支名
251
287
  * @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
252
288
  */
253
- function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): void {
289
+ async function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): Promise<void> {
254
290
  // 步骤 1:读取旧快照(tree hash + 当时的 HEAD commit hash)
255
291
  const { treeHash: oldTreeHash, headCommitHash: oldHeadCommitHash } = readSnapshot(projectName, branchName);
256
292
 
@@ -262,7 +298,13 @@ function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath:
262
298
  }
263
299
 
264
300
  // 步骤 3:通过 patch 从目标分支获取最新全量变更
265
- migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
301
+ const result = migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
302
+
303
+ if (!result.success) {
304
+ // patch 失败,询问用户是否自动 sync
305
+ await handlePatchApplyFailure(targetWorktreePath, branchName);
306
+ return;
307
+ }
266
308
 
267
309
  // 步骤 4:保存最新快照为 git tree 对象(同时记录当前 HEAD)
268
310
  saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
@@ -441,14 +483,14 @@ async function handleValidate(options: ValidateOptions): Promise<void> {
441
483
  if (!isWorkingDirClean(mainWorktreePath)) {
442
484
  await handleDirtyMainWorktree(mainWorktreePath);
443
485
  }
444
- handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
486
+ await handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
445
487
  } else {
446
488
  // 首次模式:先确保主 worktree 干净
447
489
  if (!isWorkingDirClean(mainWorktreePath)) {
448
490
  await handleDirtyMainWorktree(mainWorktreePath);
449
491
  }
450
492
 
451
- handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
493
+ await handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
452
494
  }
453
495
 
454
496
  // validate 成功后执行用户指定的命令
@@ -10,11 +10,21 @@ export { DEBUG_LOG_PREFIX, DEBUG_TIMESTAMP_FORMAT } from './logger.js';
10
10
  export {
11
11
  SPINNER_FRAMES,
12
12
  SPINNER_INTERVAL_MS,
13
- CURSOR_UP,
14
- CLEAR_LINE,
15
13
  CURSOR_HIDE,
16
14
  CURSOR_SHOW,
17
15
  TASK_STATUS_ICONS,
18
16
  TASK_STATUS_LABELS,
17
+ ACTIVITY_TEXT_MAX_LENGTH,
18
+ TEXT_ACTIVITY_PREFIX,
19
+ RESULT_PREVIEW_MAX_LENGTH,
20
+ DEFAULT_TERMINAL_COLUMNS,
21
+ LINE_WRAP_DISABLE,
22
+ LINE_WRAP_ENABLE,
23
+ SYNC_OUTPUT_START,
24
+ SYNC_OUTPUT_END,
25
+ ALT_SCREEN_ENTER,
26
+ ALT_SCREEN_LEAVE,
27
+ CLEAR_SCREEN,
28
+ CURSOR_HOME,
19
29
  } from './progress.js';
20
30
  export { SELECT_ALL_NAME, SELECT_ALL_LABEL } from './prompt.js';
@@ -17,11 +17,11 @@ export const RUN_MESSAGES = {
17
17
  PROGRESS_TASK_STARTED: (index: number, total: number, branch: string, path: string) =>
18
18
  `[${index}/${total}] ${branch} 启动 ${path}`,
19
19
  /** 非 TTY 环境降级输出:任务完成 */
20
- PROGRESS_TASK_DONE: (index: number, total: number, branch: string, duration: string, cost: string, path: string) =>
21
- `[${index}/${total}] ${branch} ✓ 完成 ${duration} ${cost} ${path}`,
20
+ PROGRESS_TASK_DONE: (index: number, total: number, branch: string, duration: string, cost: string, detail: string) =>
21
+ `[${index}/${total}] ${branch} ✓ 完成 ${duration} ${cost} ${detail}`,
22
22
  /** 非 TTY 环境降级输出:任务失败 */
23
- PROGRESS_TASK_FAILED: (index: number, total: number, branch: string, duration: string, path: string) =>
24
- `[${index}/${total}] ${branch} ✗ 失败 ${duration} ${path}`,
23
+ PROGRESS_TASK_FAILED: (index: number, total: number, branch: string, duration: string, detail: string) =>
24
+ `[${index}/${total}] ${branch} ✗ 失败 ${duration} ${detail}`,
25
25
  /** 并发限制提示 */
26
26
  CONCURRENCY_INFO: (concurrency: number, total: number) =>
27
27
  `并发限制: ${concurrency},共 ${total} 个任务`,
@@ -53,4 +53,16 @@ export const VALIDATE_MESSAGES = {
53
53
  /** 并行执行中单个命令启动失败 */
54
54
  VALIDATE_PARALLEL_CMD_ERROR: (command: string, errorMessage: string) =>
55
55
  ` ✗ ${command}(错误: ${errorMessage})`,
56
+ /** patch apply 失败后询问用户是否执行 sync */
57
+ VALIDATE_CONFIRM_AUTO_SYNC: (branch: string) =>
58
+ `是否立即执行 sync 同步主分支到 ${branch}?`,
59
+ /** 自动 sync 开始提示 */
60
+ VALIDATE_AUTO_SYNC_START: (branch: string) =>
61
+ `正在自动同步主分支到 ${branch} ...`,
62
+ /** 自动 sync 存在冲突,无法重试 */
63
+ VALIDATE_AUTO_SYNC_CONFLICT: (worktreePath: string) =>
64
+ `同步存在冲突,请进入目标 worktree 手动解决冲突后重试\n cd ${worktreePath}`,
65
+ /** 用户拒绝自动 sync */
66
+ VALIDATE_AUTO_SYNC_DECLINED: (branch: string) =>
67
+ `请手动执行 clawt sync -b ${branch} 同步主分支后重试`,
56
68
  } as const;
@@ -4,12 +4,6 @@ export const SPINNER_FRAMES: readonly string[] = ['⠋', '⠙', '⠹', '⠸', '
4
4
  /** spinner 刷新间隔(毫秒) */
5
5
  export const SPINNER_INTERVAL_MS = 100;
6
6
 
7
- /** ANSI 转义:光标上移 n 行 */
8
- export const CURSOR_UP = (n: number): string => `\x1B[${n}A`;
9
-
10
- /** ANSI 转义:清除从光标到行尾 */
11
- export const CLEAR_LINE = '\x1B[0K';
12
-
13
7
  /** ANSI 转义:隐藏光标 */
14
8
  export const CURSOR_HIDE = '\x1B[?25l';
15
9
 
@@ -37,3 +31,39 @@ export const TASK_STATUS_LABELS = {
37
31
  /** 失败 */
38
32
  FAILED: '失败',
39
33
  } as const;
34
+
35
+ /** 活动描述文本的最大字符数 */
36
+ export const ACTIVITY_TEXT_MAX_LENGTH = 30;
37
+
38
+ /** 文本类型活动的前缀 */
39
+ export const TEXT_ACTIVITY_PREFIX = '思考中';
40
+
41
+ /** 结果预览文本的最大字符数 */
42
+ export const RESULT_PREVIEW_MAX_LENGTH = 40;
43
+
44
+ /** 终端宽度的默认值(无法获取时的回退值) */
45
+ export const DEFAULT_TERMINAL_COLUMNS = 80;
46
+
47
+ /** ANSI 转义:禁用终端自动行换行(超出宽度的内容被截断而非折行) */
48
+ export const LINE_WRAP_DISABLE = '\x1B[?7l';
49
+
50
+ /** ANSI 转义:恢复终端自动行换行 */
51
+ export const LINE_WRAP_ENABLE = '\x1B[?7h';
52
+
53
+ /** ANSI 转义:开启同步输出模式(终端缓冲所有输出直到关闭,防止渲染闪烁) */
54
+ export const SYNC_OUTPUT_START = '\x1B[?2026h';
55
+
56
+ /** ANSI 转义:关闭同步输出模式(终端一次性刷新缓冲区内容) */
57
+ export const SYNC_OUTPUT_END = '\x1B[?2026l';
58
+
59
+ /** ANSI 转义:进入备选屏幕缓冲区(同时保存光标位置) */
60
+ export const ALT_SCREEN_ENTER = '\x1B[?1049h';
61
+
62
+ /** ANSI 转义:离开备选屏幕缓冲区(同时恢复光标位置) */
63
+ export const ALT_SCREEN_LEAVE = '\x1B[?1049l';
64
+
65
+ /** ANSI 转义:清除整个屏幕 */
66
+ export const CLEAR_SCREEN = '\x1B[2J';
67
+
68
+ /** ANSI 转义:光标移到屏幕左上角 (1,1) */
69
+ export const CURSOR_HOME = '\x1B[H';
@@ -61,6 +61,8 @@ export type { WorktreeResolveMessages, WorktreeMultiResolveMessages } from './wo
61
61
  export { ProgressRenderer } from './progress.js';
62
62
  export { parseTaskFile, loadTaskFile, parseTasksFromOptions } from './task-file.js';
63
63
  export { executeBatchTasks } from './task-executor.js';
64
+ export { createLineBuffer, parseStreamLine, parseStreamEvent, formatActivityText, truncateText } from './stream-parser.js';
65
+ export type { ParsedActivity, StreamEvent, LineBuffer } from './stream-parser.js';
64
66
  export { detectTerminalApp, openCommandInNewTerminalTab } from './terminal.js';
65
67
  export { truncateTaskDesc, printDryRunPreview } from './dry-run.js';
66
68
  export { applyAliases } from './alias.js';