clawt 2.16.4 → 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.
@@ -47,6 +47,18 @@
47
47
  - `confirmDestructiveAction` 在 `src/utils/formatter.ts`,被 reset、validate --clean 和 config reset 使用
48
48
  - sanitizeBranchName 清理后为空串时抛出 BRANCH_NAME_EMPTY 错误
49
49
 
50
+ ## 进度面板与流解析
51
+
52
+ - 进度面板使用备选屏幕缓冲区(Alt Screen)+ Synchronized Output 防闪烁 + 行宽截断
53
+ - `ProgressRenderer`(`src/utils/progress.ts`)负责面板生命周期:start(进入备选屏幕)、render(清屏重绘)、stop(退出备选屏幕+主屏幕输出最终状态)
54
+ - `src/utils/progress-render.ts` 包含纯渲染函数(`renderTaskLine`、`renderSummaryLine`、`truncateToTerminalWidth`)和 `TaskProgress` 类型
55
+ - `src/utils/stream-parser.ts` 负责 Claude Code stream-json 输出的流式解析,包含 `createLineBuffer`、`parseStreamLine`、`parseStreamEvent`、`formatActivityText`、`truncateText` 函数
56
+ - ANSI 转义常量(`ALT_SCREEN_ENTER/LEAVE`、`SYNC_OUTPUT_START/END`、`LINE_WRAP_DISABLE/ENABLE`、`CLEAR_SCREEN`、`CURSOR_HOME` 等)定义在 `src/constants/progress.ts`
57
+ - `ACTIVITY_TEXT_MAX_LENGTH`(30)、`TEXT_ACTIVITY_PREFIX`(思考中)、`RESULT_PREVIEW_MAX_LENGTH`(40)、`DEFAULT_TERMINAL_COLUMNS`(80)定义在 `src/constants/progress.ts`
58
+ - `string-width` 库用于计算含 ANSI 的字符串可见宽度(正确处理中文/emoji)
59
+ - docs/spec.md 中进度面板说明位于 `5.3 任务完成通知机制` 章节下的 `#### 进度面板渲染机制` 子章节
60
+ - run 命令使用 `--output-format stream-json --verbose` 调用 claude CLI(替代旧的 `--output-format json`)
61
+
50
62
  ## 配置项同步检查点
51
63
 
52
64
  配置项变更时需在以下 4 处保持一致:
@@ -70,7 +82,7 @@ run 命令有两种模式(自 claudeCodeCommand 特性后):
70
82
  - 批量任务执行逻辑从 `src/commands/run.ts` 提取到 `src/utils/task-executor.ts`(公共函数 `executeBatchTasks`)
71
83
  - 进度面板渲染逻辑从 `src/utils/progress.ts` 拆分出 `src/utils/progress-render.ts`(纯渲染函数 + TaskProgress 类型)
72
84
  - `formatDuration` 从 `src/utils/progress.ts` 移至 `src/utils/formatter.ts`
73
- - 进度面板每个任务行末尾显示 worktree 路径(终端可点击跳转)
85
+ - 进度面板每个任务行第二列显示 worktree 路径(终端可点击跳转),运行中显示活动描述,完成/失败显示结果预览
74
86
 
75
87
  ## 命令清单(12 个)
76
88
 
package/dist/index.js CHANGED
@@ -36,7 +36,7 @@ var COMMON_MESSAGES = {
36
36
  /** worktree 创建成功 */
37
37
  WORKTREE_CREATED: (count) => `\u2713 \u5DF2\u521B\u5EFA ${count} \u4E2A worktree`,
38
38
  /** worktree 移除成功 */
39
- WORKTREE_REMOVED: (path) => `\u2713 \u5DF2\u79FB\u9664 worktree: ${path}`,
39
+ WORKTREE_REMOVED: (path2) => `\u2713 \u5DF2\u79FB\u9664 worktree: ${path2}`,
40
40
  /** 没有 worktree */
41
41
  NO_WORKTREES: "(\u65E0 worktree)",
42
42
  /** 目标 worktree 不存在 */
@@ -74,17 +74,17 @@ var RUN_MESSAGES = {
74
74
  /** 中断后保留 worktree */
75
75
  INTERRUPT_KEPT: "\u5DF2\u4FDD\u7559 worktree\uFF0C\u53EF\u7A0D\u540E\u4F7F\u7528 clawt remove \u624B\u52A8\u6E05\u7406",
76
76
  /** 非 TTY 环境降级输出:任务启动 */
77
- PROGRESS_TASK_STARTED: (index, total, branch, path) => `[${index}/${total}] ${branch} \u542F\u52A8 ${path}`,
77
+ PROGRESS_TASK_STARTED: (index, total, branch, path2) => `[${index}/${total}] ${branch} \u542F\u52A8 ${path2}`,
78
78
  /** 非 TTY 环境降级输出:任务完成 */
79
- PROGRESS_TASK_DONE: (index, total, branch, duration, cost, path) => `[${index}/${total}] ${branch} \u2713 \u5B8C\u6210 ${duration} ${cost} ${path}`,
79
+ PROGRESS_TASK_DONE: (index, total, branch, duration, cost, detail) => `[${index}/${total}] ${branch} \u2713 \u5B8C\u6210 ${duration} ${cost} ${detail}`,
80
80
  /** 非 TTY 环境降级输出:任务失败 */
81
- PROGRESS_TASK_FAILED: (index, total, branch, duration, path) => `[${index}/${total}] ${branch} \u2717 \u5931\u8D25 ${duration} ${path}`,
81
+ PROGRESS_TASK_FAILED: (index, total, branch, duration, detail) => `[${index}/${total}] ${branch} \u2717 \u5931\u8D25 ${duration} ${detail}`,
82
82
  /** 并发限制提示 */
83
83
  CONCURRENCY_INFO: (concurrency, total) => `\u5E76\u53D1\u9650\u5236: ${concurrency}\uFF0C\u5171 ${total} \u4E2A\u4EFB\u52A1`,
84
84
  /** 并发数无效提示 */
85
85
  CONCURRENCY_INVALID: "\u5E76\u53D1\u6570\u5FC5\u987B\u4E3A\u6B63\u6574\u6570",
86
86
  /** 任务文件不存在 */
87
- TASK_FILE_NOT_FOUND: (path) => `\u4EFB\u52A1\u6587\u4EF6\u4E0D\u5B58\u5728: ${path}`,
87
+ TASK_FILE_NOT_FOUND: (path2) => `\u4EFB\u52A1\u6587\u4EF6\u4E0D\u5B58\u5728: ${path2}`,
88
88
  /** 任务文件中没有解析到有效任务 */
89
89
  TASK_FILE_EMPTY: "\u4EFB\u52A1\u6587\u4EF6\u4E2D\u6CA1\u6709\u89E3\u6790\u5230\u6709\u6548\u4EFB\u52A1",
90
90
  /** 任务文件中某个任务块缺少分支名 */
@@ -96,7 +96,7 @@ var RUN_MESSAGES = {
96
96
  /** --file 和 --tasks 不能同时使用 */
97
97
  FILE_AND_TASKS_CONFLICT: "--file \u548C --tasks \u4E0D\u80FD\u540C\u65F6\u4F7F\u7528",
98
98
  /** 任务文件加载成功 */
99
- TASK_FILE_LOADED: (count, path) => `\u2713 \u4ECE ${path} \u52A0\u8F7D\u4E86 ${count} \u4E2A\u4EFB\u52A1`,
99
+ TASK_FILE_LOADED: (count, path2) => `\u2713 \u4ECE ${path2} \u52A0\u8F7D\u4E86 ${count} \u4E2A\u4EFB\u52A1`,
100
100
  /** 未指定 -b 或 -f */
101
101
  BRANCH_OR_FILE_REQUIRED: "\u8BF7\u6307\u5B9A -b \u5206\u652F\u540D\u6216 -f \u4EFB\u52A1\u6587\u4EF6",
102
102
  /** dry-run 预览标题 */
@@ -462,8 +462,6 @@ var DEBUG_TIMESTAMP_FORMAT = "HH:mm:ss.SSS";
462
462
  // src/constants/progress.ts
463
463
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
464
464
  var SPINNER_INTERVAL_MS = 100;
465
- var CURSOR_UP = (n) => `\x1B[${n}A`;
466
- var CLEAR_LINE = "\x1B[0K";
467
465
  var CURSOR_HIDE = "\x1B[?25l";
468
466
  var CURSOR_SHOW = "\x1B[?25h";
469
467
  var TASK_STATUS_ICONS = {
@@ -484,6 +482,18 @@ var TASK_STATUS_LABELS = {
484
482
  /** 失败 */
485
483
  FAILED: "\u5931\u8D25"
486
484
  };
485
+ var ACTIVITY_TEXT_MAX_LENGTH = 30;
486
+ var TEXT_ACTIVITY_PREFIX = "\u601D\u8003\u4E2D";
487
+ var RESULT_PREVIEW_MAX_LENGTH = 40;
488
+ var DEFAULT_TERMINAL_COLUMNS = 80;
489
+ var LINE_WRAP_DISABLE = "\x1B[?7l";
490
+ var LINE_WRAP_ENABLE = "\x1B[?7h";
491
+ var SYNC_OUTPUT_START = "\x1B[?2026h";
492
+ var SYNC_OUTPUT_END = "\x1B[?2026l";
493
+ var ALT_SCREEN_ENTER = "\x1B[?1049h";
494
+ var ALT_SCREEN_LEAVE = "\x1B[?1049l";
495
+ var CLEAR_SCREEN = "\x1B[2J";
496
+ var CURSOR_HOME = "\x1B[H";
487
497
 
488
498
  // src/constants/prompt.ts
489
499
  var SELECT_ALL_NAME = "__select_all__";
@@ -1434,28 +1444,59 @@ async function resolveTargetWorktree(worktrees, messages, branchName) {
1434
1444
 
1435
1445
  // src/utils/progress-render.ts
1436
1446
  import chalk3 from "chalk";
1437
- function getMaxBranchWidth(tasks) {
1438
- return Math.max(...tasks.map((t) => t.branch.length));
1447
+ import stringWidth from "string-width";
1448
+ var ANSI_RESET = "\x1B[0m";
1449
+ function truncateToTerminalWidth(text, maxWidth) {
1450
+ if (stringWidth(text) <= maxWidth) {
1451
+ return text;
1452
+ }
1453
+ let visibleWidth = 0;
1454
+ let result = "";
1455
+ let i = 0;
1456
+ while (i < text.length) {
1457
+ if (text[i] === "\x1B" && text[i + 1] === "[") {
1458
+ const match = text.slice(i).match(/^\x1B\[[0-9;]*m/);
1459
+ if (match) {
1460
+ result += match[0];
1461
+ i += match[0].length;
1462
+ continue;
1463
+ }
1464
+ }
1465
+ const charWidth = stringWidth(text[i]);
1466
+ if (visibleWidth + charWidth > maxWidth) {
1467
+ break;
1468
+ }
1469
+ result += text[i];
1470
+ visibleWidth += charWidth;
1471
+ i++;
1472
+ }
1473
+ return result + ANSI_RESET;
1439
1474
  }
1440
- function renderTaskLine(task, total, maxBranchWidth, spinnerChar) {
1475
+ function getMaxPathWidth(tasks) {
1476
+ return Math.max(...tasks.map((t) => t.path.length));
1477
+ }
1478
+ function renderTaskLine(task, total, maxPathWidth, spinnerChar) {
1441
1479
  const indexStr = `[${task.index}/${total}]`;
1442
- const branchStr = task.branch.padEnd(maxBranchWidth);
1480
+ const pathStr = task.path.padEnd(maxPathWidth);
1443
1481
  switch (task.status) {
1444
1482
  case "pending": {
1445
- return `${indexStr} ${branchStr} ${chalk3.gray(TASK_STATUS_ICONS.PENDING)} ${chalk3.gray(TASK_STATUS_LABELS.PENDING)} ${chalk3.dim(task.path)}`;
1483
+ return `${indexStr} ${pathStr} ${chalk3.gray(TASK_STATUS_ICONS.PENDING)} ${chalk3.gray(TASK_STATUS_LABELS.PENDING)}`;
1446
1484
  }
1447
1485
  case "running": {
1448
1486
  const elapsed = formatDuration(Date.now() - task.startedAt);
1449
- return `${indexStr} ${branchStr} ${chalk3.cyan(spinnerChar)} ${chalk3.cyan(TASK_STATUS_LABELS.RUNNING)} ${chalk3.gray(elapsed)} ${chalk3.dim(task.path)}`;
1487
+ const detail = task.activity ? ` ${chalk3.dim(task.activity)}` : "";
1488
+ return `${indexStr} ${pathStr} ${chalk3.cyan(spinnerChar)} ${chalk3.cyan(TASK_STATUS_LABELS.RUNNING)} ${chalk3.gray(elapsed)}${detail}`;
1450
1489
  }
1451
1490
  case "done": {
1452
1491
  const duration = task.durationMs != null ? formatDuration(task.durationMs) : "N/A";
1453
1492
  const cost = task.costUsd != null ? `$${task.costUsd.toFixed(2)}` : "";
1454
- return `${indexStr} ${branchStr} ${chalk3.green(TASK_STATUS_ICONS.DONE)} ${chalk3.green(TASK_STATUS_LABELS.DONE)} ${chalk3.gray(duration)} ${chalk3.yellow(cost)} ${chalk3.dim(task.path)}`;
1493
+ const preview = task.resultPreview ? ` ${chalk3.dim(task.resultPreview)}` : "";
1494
+ return `${indexStr} ${pathStr} ${chalk3.green(TASK_STATUS_ICONS.DONE)} ${chalk3.green(TASK_STATUS_LABELS.DONE)} ${chalk3.gray(duration)} ${chalk3.yellow(cost)}${preview}`;
1455
1495
  }
1456
1496
  case "failed": {
1457
1497
  const duration = task.durationMs != null ? formatDuration(task.durationMs) : "N/A";
1458
- return `${indexStr} ${branchStr} ${chalk3.red(TASK_STATUS_ICONS.FAILED)} ${chalk3.red(TASK_STATUS_LABELS.FAILED)} ${chalk3.gray(duration)} ${chalk3.dim(task.path)}`;
1498
+ const preview = task.resultPreview ? ` ${chalk3.dim(task.resultPreview)}` : "";
1499
+ return `${indexStr} ${pathStr} ${chalk3.red(TASK_STATUS_ICONS.FAILED)} ${chalk3.red(TASK_STATUS_LABELS.FAILED)} ${chalk3.gray(duration)}${preview}`;
1459
1500
  }
1460
1501
  }
1461
1502
  }
@@ -1484,10 +1525,12 @@ var ProgressRenderer = class {
1484
1525
  timer;
1485
1526
  /** 是否为 TTY 环境 */
1486
1527
  isTTY;
1487
- /** 已渲染的行数(用于回退光标) */
1488
- renderedLineCount;
1528
+ /** resize 事件处理器引用(用于 stop 时移除监听) */
1529
+ resizeHandler;
1489
1530
  /** 是否已停止 */
1490
1531
  stopped;
1532
+ /** exit 兜底处理器(确保异常退出时终端状态被恢复) */
1533
+ exitHandler;
1491
1534
  /** 是否存在排队任务(启用汇总行渲染) */
1492
1535
  hasPendingTasks;
1493
1536
  /**
@@ -1502,8 +1545,9 @@ var ProgressRenderer = class {
1502
1545
  this.frameIndex = 0;
1503
1546
  this.timer = null;
1504
1547
  this.isTTY = !!process.stdout.isTTY;
1505
- this.renderedLineCount = 0;
1548
+ this.resizeHandler = null;
1506
1549
  this.stopped = false;
1550
+ this.exitHandler = null;
1507
1551
  this.hasPendingTasks = !allRunning;
1508
1552
  this.tasks = branches.map((branch, i) => ({
1509
1553
  index: i + 1,
@@ -1514,7 +1558,9 @@ var ProgressRenderer = class {
1514
1558
  finishedAt: null,
1515
1559
  lastActiveAt: allRunning ? now : 0,
1516
1560
  durationMs: null,
1517
- costUsd: null
1561
+ costUsd: null,
1562
+ activity: null,
1563
+ resultPreview: null
1518
1564
  }));
1519
1565
  }
1520
1566
  /**
@@ -1532,7 +1578,9 @@ var ProgressRenderer = class {
1532
1578
  }
1533
1579
  return;
1534
1580
  }
1581
+ process.stdout.write(ALT_SCREEN_ENTER);
1535
1582
  process.stdout.write(CURSOR_HIDE);
1583
+ process.stdout.write(LINE_WRAP_DISABLE);
1536
1584
  this.render();
1537
1585
  this.timer = setInterval(() => {
1538
1586
  this.frameIndex = (this.frameIndex + 1) % SPINNER_FRAMES.length;
@@ -1541,6 +1589,18 @@ var ProgressRenderer = class {
1541
1589
  if (this.timer.unref) {
1542
1590
  this.timer.unref();
1543
1591
  }
1592
+ this.resizeHandler = () => {
1593
+ if (!this.stopped) {
1594
+ this.render();
1595
+ }
1596
+ };
1597
+ process.stdout.on("resize", this.resizeHandler);
1598
+ this.exitHandler = () => {
1599
+ process.stdout.write(LINE_WRAP_ENABLE);
1600
+ process.stdout.write(CURSOR_SHOW);
1601
+ process.stdout.write(ALT_SCREEN_LEAVE);
1602
+ };
1603
+ process.on("exit", this.exitHandler);
1544
1604
  }
1545
1605
  /**
1546
1606
  * 更新指定任务的最后活动时间戳
@@ -1550,6 +1610,17 @@ var ProgressRenderer = class {
1550
1610
  updateActivity(index) {
1551
1611
  this.tasks[index].lastActiveAt = Date.now();
1552
1612
  }
1613
+ /**
1614
+ * 更新指定任务的活动描述文本
1615
+ * 同时更新 lastActiveAt 时间戳
1616
+ * @param {number} index - 任务索引(从 0 开始)
1617
+ * @param {string} text - 活动描述文本
1618
+ */
1619
+ updateActivityText(index, text) {
1620
+ const task = this.tasks[index];
1621
+ task.activity = text;
1622
+ task.lastActiveAt = Date.now();
1623
+ }
1553
1624
  /**
1554
1625
  * 标记指定任务为运行中状态
1555
1626
  * 将 pending 任务标记为 running 并设置启动时间戳
@@ -1570,32 +1641,36 @@ var ProgressRenderer = class {
1570
1641
  * @param {number} index - 任务索引(从 0 开始)
1571
1642
  * @param {number} durationMs - 耗时(毫秒)
1572
1643
  * @param {number} costUsd - 费用(美元)
1644
+ * @param {string} [resultPreview] - 结果预览文本(可选)
1573
1645
  */
1574
- markDone(index, durationMs, costUsd) {
1646
+ markDone(index, durationMs, costUsd, resultPreview) {
1575
1647
  const task = this.tasks[index];
1576
1648
  task.status = "done";
1577
1649
  task.finishedAt = Date.now();
1578
1650
  task.durationMs = durationMs;
1579
1651
  task.costUsd = costUsd;
1652
+ task.resultPreview = resultPreview ?? null;
1580
1653
  if (!this.isTTY) {
1581
1654
  const duration = formatDuration(durationMs);
1582
1655
  const cost = `$${costUsd.toFixed(2)}`;
1583
- console.log(MESSAGES.PROGRESS_TASK_DONE(task.index, this.total, task.branch, duration, cost, task.path));
1656
+ console.log(MESSAGES.PROGRESS_TASK_DONE(task.index, this.total, task.branch, duration, cost, task.resultPreview ?? task.path));
1584
1657
  }
1585
1658
  }
1586
1659
  /**
1587
1660
  * 标记指定任务为失败状态
1588
1661
  * @param {number} index - 任务索引(从 0 开始)
1589
1662
  * @param {number} durationMs - 耗时(毫秒)
1663
+ * @param {string} [resultPreview] - 结果预览文本(可选)
1590
1664
  */
1591
- markFailed(index, durationMs) {
1665
+ markFailed(index, durationMs, resultPreview) {
1592
1666
  const task = this.tasks[index];
1593
1667
  task.status = "failed";
1594
1668
  task.finishedAt = Date.now();
1595
1669
  task.durationMs = durationMs;
1670
+ task.resultPreview = resultPreview ?? null;
1596
1671
  if (!this.isTTY) {
1597
1672
  const duration = formatDuration(durationMs);
1598
- console.log(MESSAGES.PROGRESS_TASK_FAILED(task.index, this.total, task.branch, duration, task.path));
1673
+ console.log(MESSAGES.PROGRESS_TASK_FAILED(task.index, this.total, task.branch, duration, task.resultPreview ?? task.path));
1599
1674
  }
1600
1675
  }
1601
1676
  /**
@@ -1609,30 +1684,58 @@ var ProgressRenderer = class {
1609
1684
  clearInterval(this.timer);
1610
1685
  this.timer = null;
1611
1686
  }
1687
+ if (this.resizeHandler) {
1688
+ process.stdout.removeListener("resize", this.resizeHandler);
1689
+ this.resizeHandler = null;
1690
+ }
1612
1691
  if (this.isTTY) {
1613
1692
  this.render();
1693
+ process.stdout.write(LINE_WRAP_ENABLE);
1614
1694
  process.stdout.write(CURSOR_SHOW);
1695
+ process.stdout.write(ALT_SCREEN_LEAVE);
1696
+ const cols = process.stdout.columns || DEFAULT_TERMINAL_COLUMNS;
1697
+ const finalLines = this.buildLines(cols);
1698
+ for (const line of finalLines) {
1699
+ process.stdout.write(`${line}
1700
+ `);
1701
+ }
1702
+ }
1703
+ if (this.exitHandler) {
1704
+ process.removeListener("exit", this.exitHandler);
1705
+ this.exitHandler = null;
1615
1706
  }
1616
1707
  }
1617
1708
  /**
1618
- * 执行一次完整的面板渲染
1619
- * 先回退光标到面板起始位置,再逐行输出
1709
+ * 构建当前帧的面板行内容
1710
+ * @param {number} cols - 终端列数
1711
+ * @returns {string[]} 截断后的面板行数组
1620
1712
  */
1621
- render() {
1622
- const maxBranchWidth = getMaxBranchWidth(this.tasks);
1713
+ buildLines(cols) {
1714
+ const maxPathWidth = getMaxPathWidth(this.tasks);
1623
1715
  const spinnerChar = SPINNER_FRAMES[this.frameIndex];
1624
- const lines = this.tasks.map((task) => renderTaskLine(task, this.total, maxBranchWidth, spinnerChar));
1716
+ const lines = this.tasks.map((task) => renderTaskLine(task, this.total, maxPathWidth, spinnerChar));
1625
1717
  if (this.hasPendingTasks) {
1626
1718
  lines.push(renderSummaryLine(this.tasks, this.total));
1627
1719
  }
1628
- if (this.renderedLineCount > 0) {
1629
- process.stdout.write(CURSOR_UP(this.renderedLineCount));
1630
- }
1720
+ return lines.map((line) => truncateToTerminalWidth(line, cols));
1721
+ }
1722
+ /**
1723
+ * 执行一次完整的面板渲染
1724
+ * 在备选屏幕缓冲区中,每次清屏+归位后完全重绘
1725
+ * 无需计算 CURSOR_UP 回退量,不受终端 reflow 影响
1726
+ * 使用 Synchronized Output 防止多行写入时的闪烁
1727
+ */
1728
+ render() {
1729
+ const cols = process.stdout.columns || DEFAULT_TERMINAL_COLUMNS;
1730
+ const lines = this.buildLines(cols);
1731
+ process.stdout.write(SYNC_OUTPUT_START);
1732
+ process.stdout.write(CLEAR_SCREEN);
1733
+ process.stdout.write(CURSOR_HOME);
1631
1734
  for (const line of lines) {
1632
- process.stdout.write(`${line}${CLEAR_LINE}
1735
+ process.stdout.write(`${line}
1633
1736
  `);
1634
1737
  }
1635
- this.renderedLineCount = lines.length;
1738
+ process.stdout.write(SYNC_OUTPUT_END);
1636
1739
  }
1637
1740
  };
1638
1741
 
@@ -1695,11 +1798,114 @@ function loadTaskFile(filePath, options) {
1695
1798
  return entries;
1696
1799
  }
1697
1800
 
1801
+ // src/utils/stream-parser.ts
1802
+ import path from "path";
1803
+ function createLineBuffer() {
1804
+ let buffer = "";
1805
+ return {
1806
+ push(chunk) {
1807
+ buffer += chunk;
1808
+ const lines = buffer.split("\n");
1809
+ buffer = lines.pop() ?? "";
1810
+ return lines;
1811
+ },
1812
+ flush() {
1813
+ const remaining = buffer;
1814
+ buffer = "";
1815
+ return remaining || null;
1816
+ }
1817
+ };
1818
+ }
1819
+ function parseStreamLine(line) {
1820
+ const trimmed = line.trim();
1821
+ if (!trimmed) return null;
1822
+ try {
1823
+ return JSON.parse(trimmed);
1824
+ } catch {
1825
+ return null;
1826
+ }
1827
+ }
1828
+ function truncateText(text, maxLength) {
1829
+ if (text.length <= maxLength) return text;
1830
+ return text.substring(0, maxLength - 1) + "\u2026";
1831
+ }
1832
+ function formatActivityText(kind, toolName, input, text) {
1833
+ let raw = "";
1834
+ if (kind === "tool_use" && toolName) {
1835
+ const filePath = input?.file_path;
1836
+ const command = input?.command;
1837
+ if (filePath) {
1838
+ const basename2 = path.basename(filePath);
1839
+ raw = `${toolName} ${basename2}`;
1840
+ } else if (command) {
1841
+ const cleaned = command.replace(/[\n\r\t]+/g, " ").trim();
1842
+ raw = `${toolName} ${cleaned}`;
1843
+ } else {
1844
+ raw = toolName;
1845
+ }
1846
+ } else if (kind === "text" && text) {
1847
+ const cleaned = text.replace(/[\n\r\t]/g, " ").trim();
1848
+ if (!cleaned) return "";
1849
+ raw = `${TEXT_ACTIVITY_PREFIX}: ${cleaned}`;
1850
+ }
1851
+ if (!raw) return "";
1852
+ return truncateText(raw, ACTIVITY_TEXT_MAX_LENGTH);
1853
+ }
1854
+ function parseStreamEvent(event) {
1855
+ if (event.type === "result") {
1856
+ const result = {
1857
+ type: event.type,
1858
+ subtype: event.subtype ?? "",
1859
+ is_error: event.is_error ?? false,
1860
+ duration_ms: event.duration_ms ?? 0,
1861
+ duration_api_ms: event.duration_api_ms ?? 0,
1862
+ num_turns: event.num_turns ?? 0,
1863
+ result: event.result ?? "",
1864
+ stop_reason: event.stop_reason ?? "",
1865
+ session_id: event.session_id ?? "",
1866
+ total_cost_usd: event.total_cost_usd ?? 0,
1867
+ usage: event.usage ?? {}
1868
+ };
1869
+ return { kind: "result", activityText: "", result };
1870
+ }
1871
+ if (event.type === "assistant") {
1872
+ const content = event.message?.content;
1873
+ if (!content || content.length === 0) return null;
1874
+ let lastToolUse = null;
1875
+ let lastText = null;
1876
+ for (const block of content) {
1877
+ if (block.type === "tool_use") {
1878
+ lastToolUse = block;
1879
+ } else if (block.type === "text") {
1880
+ lastText = block;
1881
+ }
1882
+ }
1883
+ if (lastToolUse) {
1884
+ const activityText = formatActivityText(
1885
+ "tool_use",
1886
+ lastToolUse.name,
1887
+ lastToolUse.input
1888
+ );
1889
+ if (activityText) {
1890
+ return { kind: "tool_use", activityText };
1891
+ }
1892
+ }
1893
+ if (lastText?.text) {
1894
+ const activityText = formatActivityText("text", void 0, void 0, lastText.text);
1895
+ if (activityText) {
1896
+ return { kind: "text", activityText };
1897
+ }
1898
+ }
1899
+ return null;
1900
+ }
1901
+ return null;
1902
+ }
1903
+
1698
1904
  // src/utils/task-executor.ts
1699
- function executeClaudeTask(worktree, task) {
1905
+ function executeClaudeTask(worktree, task, onActivity) {
1700
1906
  const child = spawnProcess(
1701
1907
  "claude",
1702
- ["-p", task, "--output-format", "json", "--permission-mode", "bypassPermissions"],
1908
+ ["-p", task, "--output-format", "stream-json", "--verbose", "--permission-mode", "bypassPermissions"],
1703
1909
  {
1704
1910
  cwd: worktree.path,
1705
1911
  // stdin 必须设置为 'ignore',不能用 'pipe'
@@ -1710,31 +1916,47 @@ function executeClaudeTask(worktree, task) {
1710
1916
  }
1711
1917
  );
1712
1918
  const promise = new Promise((resolve2) => {
1713
- let stdout = "";
1714
1919
  let stderr = "";
1920
+ let finalResult = null;
1921
+ const lineBuffer = createLineBuffer();
1715
1922
  child.stdout?.on("data", (data) => {
1716
- stdout += data.toString();
1923
+ const lines = lineBuffer.push(data.toString());
1924
+ for (const line of lines) {
1925
+ const event = parseStreamLine(line);
1926
+ if (!event) continue;
1927
+ const parsed = parseStreamEvent(event);
1928
+ if (!parsed) continue;
1929
+ if (parsed.kind === "result") {
1930
+ finalResult = parsed.result ?? null;
1931
+ } else if (parsed.activityText && onActivity) {
1932
+ onActivity(parsed.activityText);
1933
+ }
1934
+ }
1717
1935
  });
1718
1936
  child.stderr?.on("data", (data) => {
1719
1937
  stderr += data.toString();
1720
1938
  });
1721
1939
  child.on("close", (code) => {
1722
- let result = null;
1723
- let success = code === 0;
1724
- try {
1725
- if (stdout.trim()) {
1726
- result = JSON.parse(stdout.trim());
1727
- success = !result.is_error;
1940
+ const remaining = lineBuffer.flush();
1941
+ if (remaining) {
1942
+ const event = parseStreamLine(remaining);
1943
+ if (event) {
1944
+ const parsed = parseStreamEvent(event);
1945
+ if (parsed?.kind === "result") {
1946
+ finalResult = parsed.result ?? null;
1947
+ }
1728
1948
  }
1729
- } catch {
1730
- logger.warn(`\u89E3\u6790 Claude Code \u8F93\u51FA\u5931\u8D25: ${stdout.substring(0, 200)}`);
1949
+ }
1950
+ let success = code === 0;
1951
+ if (finalResult) {
1952
+ success = !finalResult.is_error;
1731
1953
  }
1732
1954
  resolve2({
1733
1955
  task,
1734
1956
  branch: worktree.branch,
1735
1957
  worktreePath: worktree.path,
1736
1958
  success,
1737
- result,
1959
+ result: finalResult,
1738
1960
  error: success ? void 0 : stderr || "\u4EFB\u52A1\u6267\u884C\u5931\u8D25"
1739
1961
  });
1740
1962
  });
@@ -1776,16 +1998,20 @@ async function handleInterruptCleanup(worktrees) {
1776
1998
  }
1777
1999
  }
1778
2000
  function updateRendererStatus(renderer, index, result, startTime) {
2001
+ const rawResultText = result.result?.result;
2002
+ const resultPreview = rawResultText ? truncateText(rawResultText.replace(/[\n\r\t]+/g, " ").trim(), RESULT_PREVIEW_MAX_LENGTH) : void 0;
1779
2003
  if (result.success) {
1780
2004
  renderer.markDone(
1781
2005
  index,
1782
2006
  result.result?.duration_ms ?? Date.now() - startTime,
1783
- result.result?.total_cost_usd ?? 0
2007
+ result.result?.total_cost_usd ?? 0,
2008
+ resultPreview
1784
2009
  );
1785
2010
  } else {
1786
2011
  renderer.markFailed(
1787
2012
  index,
1788
- result.result?.duration_ms ?? Date.now() - startTime
2013
+ result.result?.duration_ms ?? Date.now() - startTime,
2014
+ resultPreview
1789
2015
  );
1790
2016
  }
1791
2017
  }
@@ -1803,11 +2029,10 @@ async function executeWithConcurrency(worktrees, tasks, concurrency, renderer, s
1803
2029
  const task = tasks[index];
1804
2030
  logger.info(`\u542F\u52A8\u4EFB\u52A1 ${index + 1}: ${task} (worktree: ${wt.path})`);
1805
2031
  renderer.markRunning(index);
1806
- const handle = executeClaudeTask(wt, task);
1807
- childProcesses.push(handle.child);
1808
- handle.child.stderr?.on("data", () => {
1809
- renderer.updateActivity(index);
2032
+ const handle = executeClaudeTask(wt, task, (activityText) => {
2033
+ renderer.updateActivityText(index, activityText);
1810
2034
  });
2035
+ childProcesses.push(handle.child);
1811
2036
  handle.promise.then((result) => {
1812
2037
  results[index] = result;
1813
2038
  completedCount++;
@@ -1830,11 +2055,10 @@ async function executeAllParallel(worktrees, tasks, renderer, startTime, isInter
1830
2055
  const handles = worktrees.map((wt, index) => {
1831
2056
  const task = tasks[index];
1832
2057
  logger.info(`\u542F\u52A8\u4EFB\u52A1 ${index + 1}: ${task} (worktree: ${wt.path})`);
1833
- const handle = executeClaudeTask(wt, task);
1834
- childProcesses.push(handle.child);
1835
- handle.child.stderr?.on("data", () => {
1836
- renderer.updateActivity(index);
2058
+ const handle = executeClaudeTask(wt, task, (activityText) => {
2059
+ renderer.updateActivityText(index, activityText);
1837
2060
  });
2061
+ childProcesses.push(handle.child);
1838
2062
  return handle;
1839
2063
  });
1840
2064
  const results = await Promise.all(
@@ -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
  /** 并发数无效提示 */