clawt 3.9.6 → 3.9.8

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.
package/dist/index.js CHANGED
@@ -574,8 +574,10 @@ var TASKS_CMD_MESSAGES = {
574
574
  TASK_INIT_FILE_EXISTS: (path2) => `\u6587\u4EF6\u5DF2\u5B58\u5728: ${path2}\uFF0C\u5982\u9700\u8986\u76D6\u8BF7\u5148\u5220\u9664`,
575
575
  /** 任务模板生成成功 */
576
576
  TASK_INIT_SUCCESS: (path2) => `\u2713 \u4EFB\u52A1\u6A21\u677F\u5DF2\u751F\u6210: ${path2}`,
577
- /** 任务模板使用提示 */
578
- TASK_INIT_HINT: (path2) => `\u4F7F\u7528 clawt run -f ${path2} \u6267\u884C\u4EFB\u52A1`
577
+ /** 任务模板使用提示(分行列出 run 和 resume 两种用法) */
578
+ TASK_INIT_HINT: (path2) => `\u6267\u884C\u4EFB\u52A1:
579
+ clawt run -f ${path2} # \u521B\u5EFA worktree \u5E76\u6267\u884C\uFF08\u5206\u652F\u540D\u9700\u4E0D\u5B58\u5728\uFF09
580
+ clawt resume -f ${path2} # \u5728\u5DF2\u6709 worktree \u4E2D\u8FFD\u95EE\uFF08\u5206\u652F\u540D\u9700\u5DF2\u5B58\u5728\uFF09`
579
581
  };
580
582
 
581
583
  // src/constants/messages/post-create.ts
@@ -780,6 +782,10 @@ var PROJECT_CONFIG_DEFINITIONS = {
780
782
  postCreate: {
781
783
  defaultValue: void 0,
782
784
  description: "worktree \u521B\u5EFA\u540E\u81EA\u52A8\u6267\u884C\u7684\u547D\u4EE4\uFF0C\u7528\u4E8E\u5B89\u88C5\u4F9D\u8D56\u7B49\u521D\u59CB\u5316\u64CD\u4F5C"
785
+ },
786
+ claudeCodeCommand: {
787
+ defaultValue: void 0,
788
+ description: "Claude Code CLI \u542F\u52A8\u6307\u4EE4\uFF08\u672A\u8BBE\u7F6E\u65F6\u56DE\u9000\u5230\u5168\u5C40\u914D\u7F6E\uFF09"
783
789
  }
784
790
  };
785
791
  function deriveDefaultConfig2(definitions) {
@@ -912,7 +918,7 @@ var TASK_TEMPLATE_CONTENT = `# Clawt \u4EFB\u52A1\u6587\u4EF6
912
918
  # \u683C\u5F0F\u8BF4\u660E: \u6807\u7B7E\u5916\u7684\u6587\u672C\u4F1A\u88AB\u5FFD\u7565\uFF0C\u6BCF\u4E2A\u4EFB\u52A1\u7528 START/END \u6807\u7B7E\u5305\u88F9
913
919
  #
914
920
  # \u89C4\u5219:
915
- # 1. \u6BCF\u4E2A\u4EFB\u52A1\u5757\u7528 <!-- CLAWT-TASKS:START --> \u548C <!-- CLAWT-TASKS:END --> \u5305\u88F9
921
+ # 1. \u6BCF\u4E2A\u4EFB\u52A1\u5757\u7528 <START> \u548C <END> \u6807\u7B7E\u5305\u88F9\uFF08\u5B9E\u9645\u6807\u7B7E\u89C1\u4E0B\u65B9\u793A\u4F8B\uFF09
916
922
  # 2. \u5757\u5185 # branch: <\u5206\u652F\u540D> \u58F0\u660E\u5206\u652F\u540D\uFF08\u4F7F\u7528 -b \u53C2\u6570\u65F6\u53EF\u7701\u7565\uFF09
917
923
  # 3. \u5757\u5185\u5176\u4F59\u884C\u4E3A\u4EFB\u52A1\u63CF\u8FF0\uFF08\u652F\u6301\u591A\u884C\uFF09
918
924
 
@@ -1001,7 +1007,8 @@ function enableConsoleTransport() {
1001
1007
  }
1002
1008
 
1003
1009
  // src/utils/shell.ts
1004
- import { execSync as execSync2, execFileSync, spawn, spawnSync } from "child_process";
1010
+ import { exec, execSync as execSync2, execFileSync, spawn, spawnSync } from "child_process";
1011
+ import { promisify } from "util";
1005
1012
 
1006
1013
  // src/utils/git-lock.ts
1007
1014
  import { join as join2, isAbsolute } from "path";
@@ -1067,6 +1074,7 @@ function waitForGitIndexLockRetrySync() {
1067
1074
  }
1068
1075
 
1069
1076
  // src/utils/shell.ts
1077
+ var execPromise = promisify(exec);
1070
1078
  function getEnvWithoutNestedSessionFlag() {
1071
1079
  const { CLAUDECODE: _, ...env } = process.env;
1072
1080
  return { ...env, CLAUDE_CODE_ENTRYPOINT: CLAUDE_CODE_ENTRYPOINT_VALUE };
@@ -1095,6 +1103,15 @@ function execCommand(command, options) {
1095
1103
  }
1096
1104
  }
1097
1105
  }
1106
+ async function execCommandAsync(command, options) {
1107
+ logger.debug(`\u6267\u884C\u5F02\u6B65\u547D\u4EE4: ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ""}`);
1108
+ const { stdout } = await execPromise(command, {
1109
+ cwd: options?.cwd,
1110
+ encoding: "utf-8",
1111
+ maxBuffer: EXEC_MAX_BUFFER
1112
+ });
1113
+ return stdout.trim();
1114
+ }
1098
1115
  function spawnProcess(command, args, options) {
1099
1116
  logger.debug(`\u542F\u52A8\u5B50\u8FDB\u7A0B: ${command} ${args.join(" ")}${options?.cwd ? ` (cwd: ${options.cwd})` : ""}`);
1100
1117
  return spawn(command, args, {
@@ -1262,6 +1279,9 @@ function getProjectName(cwd) {
1262
1279
  function getStatusPorcelain(cwd) {
1263
1280
  return execCommand("git status --porcelain", { cwd });
1264
1281
  }
1282
+ async function getStatusPorcelainAsync(cwd) {
1283
+ return execCommandAsync("git status --porcelain", { cwd });
1284
+ }
1265
1285
  function isWorkingDirClean(cwd) {
1266
1286
  return getStatusPorcelain(cwd) === "";
1267
1287
  }
@@ -1316,6 +1336,10 @@ function getDiffStat(worktreePath) {
1316
1336
  const output = execCommand("git diff --shortstat HEAD", { cwd: worktreePath });
1317
1337
  return parseShortStat(output);
1318
1338
  }
1339
+ async function getDiffStatAsync(worktreePath) {
1340
+ const output = await execCommandAsync("git diff --shortstat HEAD", { cwd: worktreePath });
1341
+ return parseShortStat(output);
1342
+ }
1319
1343
  function gitApplyCachedFromStdin(patchContent, cwd) {
1320
1344
  execCommandWithInput("git", ["apply", "--cached"], { input: patchContent, cwd });
1321
1345
  }
@@ -1423,12 +1447,16 @@ function getCommitCountAhead(branchName, cwd) {
1423
1447
  const output = execCommand(`git rev-list --count HEAD..${branchName}`, { cwd });
1424
1448
  return parseInt(output, 10) || 0;
1425
1449
  }
1426
- function getCommitCountBehind(branchName, cwd) {
1450
+ function parseDivergenceOutput(output) {
1451
+ const [leftStr, rightStr] = output.trim().split(/\s+/);
1452
+ return { ahead: parseInt(rightStr, 10) || 0, behind: parseInt(leftStr, 10) || 0 };
1453
+ }
1454
+ async function getCommitDivergenceAsync(branchName, cwd) {
1427
1455
  try {
1428
- const output = execCommand(`git rev-list --count ${branchName}..HEAD`, { cwd });
1429
- return parseInt(output, 10) || 0;
1456
+ const output = await execCommandAsync(`git rev-list --left-right --count HEAD...${branchName}`, { cwd });
1457
+ return parseDivergenceOutput(output);
1430
1458
  } catch {
1431
- return 0;
1459
+ return { ahead: 0, behind: 0 };
1432
1460
  }
1433
1461
  }
1434
1462
  function getCurrentBranch(cwd) {
@@ -1638,7 +1666,7 @@ function validateBranchesNotExist(branchNames) {
1638
1666
  }
1639
1667
 
1640
1668
  // src/utils/project-config.ts
1641
- import { existsSync as existsSync3, readFileSync, writeFileSync } from "fs";
1669
+ import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
1642
1670
  import { join as join4 } from "path";
1643
1671
 
1644
1672
  // src/utils/fs.ts
@@ -1680,6 +1708,101 @@ function calculateDirSize(dirPath) {
1680
1708
  return totalSize;
1681
1709
  }
1682
1710
 
1711
+ // src/utils/config.ts
1712
+ import { existsSync as existsSync3, readFileSync, writeFileSync } from "fs";
1713
+ function loadConfig() {
1714
+ if (!existsSync3(CONFIG_PATH)) {
1715
+ return { ...DEFAULT_CONFIG };
1716
+ }
1717
+ try {
1718
+ const raw = readFileSync(CONFIG_PATH, "utf-8");
1719
+ return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
1720
+ } catch {
1721
+ logger.warn(MESSAGES.CONFIG_CORRUPTED);
1722
+ writeDefaultConfig();
1723
+ return { ...DEFAULT_CONFIG };
1724
+ }
1725
+ }
1726
+ function writeConfig(config2) {
1727
+ writeFileSync(CONFIG_PATH, JSON.stringify(config2, null, 2), "utf-8");
1728
+ }
1729
+ function writeDefaultConfig() {
1730
+ writeConfig(DEFAULT_CONFIG);
1731
+ }
1732
+ function saveConfig(config2) {
1733
+ writeFileSync(CONFIG_PATH, JSON.stringify(config2, null, 2), "utf-8");
1734
+ }
1735
+ function getConfigValue(key) {
1736
+ const config2 = loadConfig();
1737
+ return config2[key];
1738
+ }
1739
+ function ensureClawtDirs() {
1740
+ ensureDir(CLAWT_HOME);
1741
+ ensureDir(LOGS_DIR);
1742
+ ensureDir(WORKTREES_DIR);
1743
+ ensureDir(PROJECTS_CONFIG_DIR);
1744
+ }
1745
+ function parseConcurrency(optionValue, configValue) {
1746
+ if (optionValue === void 0) {
1747
+ return configValue;
1748
+ }
1749
+ const parsed = parseInt(optionValue, 10);
1750
+ if (Number.isNaN(parsed) || parsed < 0) {
1751
+ throw new ClawtError(MESSAGES.CONCURRENCY_INVALID);
1752
+ }
1753
+ return parsed;
1754
+ }
1755
+
1756
+ // src/utils/json.ts
1757
+ function primitiveToString(value) {
1758
+ if (value === void 0) {
1759
+ return "undefined";
1760
+ }
1761
+ if (value === null) {
1762
+ return "null";
1763
+ }
1764
+ if (typeof value === "symbol") {
1765
+ return value.toString();
1766
+ }
1767
+ if (typeof value === "function") {
1768
+ return `[Function: ${value.name || "anonymous"}]`;
1769
+ }
1770
+ return String(value);
1771
+ }
1772
+ function safeStringify(value, indent = 2) {
1773
+ if (value === null || typeof value !== "object") {
1774
+ return primitiveToString(value);
1775
+ }
1776
+ try {
1777
+ const seen = /* @__PURE__ */ new WeakSet();
1778
+ return JSON.stringify(
1779
+ value,
1780
+ (_key, val) => {
1781
+ if (typeof val === "bigint") {
1782
+ return val.toString();
1783
+ }
1784
+ if (typeof val === "undefined" || typeof val === "function" || typeof val === "symbol") {
1785
+ return primitiveToString(val);
1786
+ }
1787
+ if (typeof val === "object" && val !== null) {
1788
+ if (seen.has(val)) {
1789
+ return "[Circular]";
1790
+ }
1791
+ seen.add(val);
1792
+ }
1793
+ return val;
1794
+ },
1795
+ indent
1796
+ );
1797
+ } catch {
1798
+ try {
1799
+ return JSON.stringify(String(value), null, indent);
1800
+ } catch {
1801
+ return "[Unserializable]";
1802
+ }
1803
+ }
1804
+ }
1805
+
1683
1806
  // src/utils/project-config.ts
1684
1807
  function getProjectConfigPath(projectName) {
1685
1808
  return join4(PROJECTS_CONFIG_DIR, projectName, "config.json");
@@ -1687,11 +1810,11 @@ function getProjectConfigPath(projectName) {
1687
1810
  function loadProjectConfig() {
1688
1811
  const projectName = getProjectName();
1689
1812
  const configPath = getProjectConfigPath(projectName);
1690
- if (!existsSync3(configPath)) {
1813
+ if (!existsSync4(configPath)) {
1691
1814
  return null;
1692
1815
  }
1693
1816
  try {
1694
- const raw = readFileSync(configPath, "utf-8");
1817
+ const raw = readFileSync2(configPath, "utf-8");
1695
1818
  return JSON.parse(raw);
1696
1819
  } catch (error) {
1697
1820
  logger.warn(`\u9879\u76EE\u914D\u7F6E\u6587\u4EF6\u89E3\u6790\u5931\u8D25: ${error}`);
@@ -1703,7 +1826,7 @@ function saveProjectConfig(config2) {
1703
1826
  const configPath = getProjectConfigPath(projectName);
1704
1827
  const projectDir = join4(PROJECTS_CONFIG_DIR, projectName);
1705
1828
  ensureDir(projectDir);
1706
- writeFileSync(configPath, JSON.stringify(config2, null, 2), "utf-8");
1829
+ writeFileSync2(configPath, safeStringify({ ...config2 }, 2), "utf-8");
1707
1830
  logger.info(`\u9879\u76EE\u914D\u7F6E\u5DF2\u4FDD\u5B58: ${configPath}`);
1708
1831
  }
1709
1832
  function requireProjectConfig() {
@@ -1731,6 +1854,24 @@ function getValidateRunCommand() {
1731
1854
  const config2 = loadProjectConfig();
1732
1855
  return config2?.validateRunCommand || void 0;
1733
1856
  }
1857
+ function resolveClaudeCodeCommand() {
1858
+ const projectConfig = loadProjectConfig();
1859
+ if (projectConfig?.claudeCodeCommand) {
1860
+ return projectConfig.claudeCodeCommand;
1861
+ }
1862
+ return getConfigValue("claudeCodeCommand");
1863
+ }
1864
+ function normalizeProjectConfig(config2, key, value) {
1865
+ if (value === "") {
1866
+ const def = PROJECT_CONFIG_DEFINITIONS[key];
1867
+ if (def?.defaultValue === void 0) {
1868
+ const normalized = { ...config2 };
1869
+ delete normalized[key];
1870
+ return normalized;
1871
+ }
1872
+ }
1873
+ return config2;
1874
+ }
1734
1875
 
1735
1876
  // src/utils/validate-branch.ts
1736
1877
  import Enquirer from "enquirer";
@@ -1897,7 +2038,7 @@ async function runPreChecks(options) {
1897
2038
 
1898
2039
  // src/utils/worktree.ts
1899
2040
  import { join as join5 } from "path";
1900
- import { existsSync as existsSync4, readdirSync as readdirSync2 } from "fs";
2041
+ import { existsSync as existsSync5, readdirSync as readdirSync2 } from "fs";
1901
2042
  function getProjectWorktreeDir() {
1902
2043
  const projectName = getProjectName();
1903
2044
  return join5(WORKTREES_DIR, projectName);
@@ -1934,7 +2075,7 @@ function createWorktreesByBranches(branchNames) {
1934
2075
  }
1935
2076
  function getProjectWorktrees() {
1936
2077
  const projectDir = getProjectWorktreeDir();
1937
- if (!existsSync4(projectDir)) {
2078
+ if (!existsSync5(projectDir)) {
1938
2079
  return [];
1939
2080
  }
1940
2081
  const worktreeListOutput = gitWorktreeList();
@@ -1984,51 +2125,6 @@ function getWorktreeStatus(worktree) {
1984
2125
  }
1985
2126
  }
1986
2127
 
1987
- // src/utils/config.ts
1988
- import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
1989
- function loadConfig() {
1990
- if (!existsSync5(CONFIG_PATH)) {
1991
- return { ...DEFAULT_CONFIG };
1992
- }
1993
- try {
1994
- const raw = readFileSync2(CONFIG_PATH, "utf-8");
1995
- return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
1996
- } catch {
1997
- logger.warn(MESSAGES.CONFIG_CORRUPTED);
1998
- writeDefaultConfig();
1999
- return { ...DEFAULT_CONFIG };
2000
- }
2001
- }
2002
- function writeConfig(config2) {
2003
- writeFileSync2(CONFIG_PATH, JSON.stringify(config2, null, 2), "utf-8");
2004
- }
2005
- function writeDefaultConfig() {
2006
- writeConfig(DEFAULT_CONFIG);
2007
- }
2008
- function saveConfig(config2) {
2009
- writeFileSync2(CONFIG_PATH, JSON.stringify(config2, null, 2), "utf-8");
2010
- }
2011
- function getConfigValue(key) {
2012
- const config2 = loadConfig();
2013
- return config2[key];
2014
- }
2015
- function ensureClawtDirs() {
2016
- ensureDir(CLAWT_HOME);
2017
- ensureDir(LOGS_DIR);
2018
- ensureDir(WORKTREES_DIR);
2019
- ensureDir(PROJECTS_CONFIG_DIR);
2020
- }
2021
- function parseConcurrency(optionValue, configValue) {
2022
- if (optionValue === void 0) {
2023
- return configValue;
2024
- }
2025
- const parsed = parseInt(optionValue, 10);
2026
- if (Number.isNaN(parsed) || parsed < 0) {
2027
- throw new ClawtError(MESSAGES.CONCURRENCY_INVALID);
2028
- }
2029
- return parsed;
2030
- }
2031
-
2032
2128
  // src/utils/prompt.ts
2033
2129
  import Enquirer2 from "enquirer";
2034
2130
  async function promptCommitMessage(promptMessage, nonInteractiveErrorMessage) {
@@ -2200,7 +2296,7 @@ function hasClaudeSessionHistory(worktreePath) {
2200
2296
  return entries.some((entry) => entry.endsWith(".jsonl"));
2201
2297
  }
2202
2298
  function launchInteractiveClaude(worktree, options = {}) {
2203
- const commandStr = getConfigValue("claudeCodeCommand");
2299
+ const commandStr = resolveClaudeCodeCommand();
2204
2300
  const parts = commandStr.split(/\s+/).filter(Boolean);
2205
2301
  const cmd = parts[0];
2206
2302
  const args = [
@@ -2235,7 +2331,7 @@ function escapeShellSingleQuote(str) {
2235
2331
  return str.replace(/'/g, "'\\''");
2236
2332
  }
2237
2333
  function buildClaudeCommand(worktree, hasPreviousSession) {
2238
- const commandStr = getConfigValue("claudeCodeCommand");
2334
+ const commandStr = resolveClaudeCodeCommand();
2239
2335
  const systemPrompt = APPEND_SYSTEM_PROMPT;
2240
2336
  const escapedPath = escapeShellSingleQuote(worktree.path);
2241
2337
  const escapedPrompt = escapeShellSingleQuote(systemPrompt);
@@ -3562,56 +3658,6 @@ async function checkForUpdates(currentVersion) {
3562
3658
  }
3563
3659
  }
3564
3660
 
3565
- // src/utils/json.ts
3566
- function primitiveToString(value) {
3567
- if (value === void 0) {
3568
- return "undefined";
3569
- }
3570
- if (value === null) {
3571
- return "null";
3572
- }
3573
- if (typeof value === "symbol") {
3574
- return value.toString();
3575
- }
3576
- if (typeof value === "function") {
3577
- return `[Function: ${value.name || "anonymous"}]`;
3578
- }
3579
- return String(value);
3580
- }
3581
- function safeStringify(value, indent = 2) {
3582
- if (value === null || typeof value !== "object") {
3583
- return primitiveToString(value);
3584
- }
3585
- try {
3586
- const seen = /* @__PURE__ */ new WeakSet();
3587
- return JSON.stringify(
3588
- value,
3589
- (_key, val) => {
3590
- if (typeof val === "bigint") {
3591
- return val.toString();
3592
- }
3593
- if (typeof val === "undefined" || typeof val === "function" || typeof val === "symbol") {
3594
- return primitiveToString(val);
3595
- }
3596
- if (typeof val === "object" && val !== null) {
3597
- if (seen.has(val)) {
3598
- return "[Circular]";
3599
- }
3600
- seen.add(val);
3601
- }
3602
- return val;
3603
- },
3604
- indent
3605
- );
3606
- } catch {
3607
- try {
3608
- return JSON.stringify(String(value), null, indent);
3609
- } catch {
3610
- return "[Unserializable]";
3611
- }
3612
- }
3613
- }
3614
-
3615
3661
  // src/utils/validate-runner.ts
3616
3662
  function handleErrorClipboard(clipboardContent) {
3617
3663
  const success = copyToClipboard(clipboardContent);
@@ -3795,7 +3841,7 @@ function buildSeparatorWithHint(cols, hint) {
3795
3841
  const rightLen = remaining - leftLen;
3796
3842
  return `${chalk9.gray("\u2500".repeat(leftLen))} ${hint} ${chalk9.gray("\u2500".repeat(rightLen))}`;
3797
3843
  }
3798
- function buildPanelFrame(statusResult, selectedIndex, scrollOffset, rows, cols, countdown) {
3844
+ function buildPanelFrame(statusResult, selectedIndex, scrollOffset, rows, cols, countdown, cachedPanelLines) {
3799
3845
  const lines = [];
3800
3846
  lines.push(PANEL_TITLE(statusResult.main.projectName));
3801
3847
  lines.push(renderConfiguredBranchLine(statusResult.main));
@@ -3806,7 +3852,7 @@ function buildPanelFrame(statusResult, selectedIndex, scrollOffset, rows, cols,
3806
3852
  lines.push(PANEL_NO_WORKTREES);
3807
3853
  lines.push(buildSeparatorWithHint(cols, ""));
3808
3854
  } else {
3809
- const panelLines = buildGroupedWorktreeLines(statusResult.worktrees, selectedIndex);
3855
+ const panelLines = cachedPanelLines ?? buildGroupedWorktreeLines(statusResult.worktrees, selectedIndex);
3810
3856
  const hasOverflowUp = scrollOffset > 0;
3811
3857
  const hasOverflowDown = scrollOffset + visibleRows < panelLines.length;
3812
3858
  lines.push(buildSeparatorWithHint(cols, hasOverflowUp ? PANEL_OVERFLOW_UP_HINT : ""));
@@ -4002,8 +4048,11 @@ var PanelStateManager = class {
4002
4048
  displayOrder = [];
4003
4049
  /** 滚动偏移(基于行数) */
4004
4050
  scrollOffset = 0;
4051
+ /** 缓存的面板行列表,在 updateData 和导航时更新 */
4052
+ cachedPanelLines = [];
4005
4053
  /**
4006
4054
  * 更新状态数据
4055
+ * 一次性计算 displayOrder 和 cachedPanelLines,后续 adjustScrollForSelection 和 render 复用缓存
4007
4056
  * @param {StatusResult} newStatus - 新的状态数据
4008
4057
  * @param {string} [previousBranch] - 刷新前选中的分支名
4009
4058
  */
@@ -4022,6 +4071,7 @@ var PanelStateManager = class {
4022
4071
  } else {
4023
4072
  this.selectedDisplayIndex = 0;
4024
4073
  }
4074
+ this.rebuildCachedPanelLines();
4025
4075
  }
4026
4076
  /** 获取当前状态数据 */
4027
4077
  getStatusResult() {
@@ -4035,6 +4085,13 @@ var PanelStateManager = class {
4035
4085
  getScrollOffset() {
4036
4086
  return this.scrollOffset;
4037
4087
  }
4088
+ /**
4089
+ * 获取缓存的面板行列表
4090
+ * @returns {PanelLine[]} 缓存的面板行列表
4091
+ */
4092
+ getCachedPanelLines() {
4093
+ return this.cachedPanelLines;
4094
+ }
4038
4095
  /**
4039
4096
  * 向上导航
4040
4097
  * @returns {boolean} 是否发生变化
@@ -4043,6 +4100,7 @@ var PanelStateManager = class {
4043
4100
  if (!this.statusResult || this.displayOrder.length === 0) return false;
4044
4101
  if (this.selectedDisplayIndex > 0) {
4045
4102
  this.selectedDisplayIndex--;
4103
+ this.rebuildCachedPanelLines();
4046
4104
  this.adjustScrollForSelection();
4047
4105
  return true;
4048
4106
  }
@@ -4056,6 +4114,7 @@ var PanelStateManager = class {
4056
4114
  if (!this.statusResult || this.displayOrder.length === 0) return false;
4057
4115
  if (this.selectedDisplayIndex < this.displayOrder.length - 1) {
4058
4116
  this.selectedDisplayIndex++;
4117
+ this.rebuildCachedPanelLines();
4059
4118
  this.adjustScrollForSelection();
4060
4119
  return true;
4061
4120
  }
@@ -4072,13 +4131,14 @@ var PanelStateManager = class {
4072
4131
  }
4073
4132
  /**
4074
4133
  * 调整滚动位置以确保选中项在可见区域内
4134
+ * 复用 cachedPanelLines,不再重新调用 buildGroupedWorktreeLines
4075
4135
  */
4076
4136
  adjustScrollForSelection() {
4077
4137
  if (!this.statusResult || this.displayOrder.length === 0) return;
4078
4138
  const originalIndex = this.getSelectedOriginalIndex();
4079
4139
  const rows = process.stdout.rows || 24;
4080
4140
  const visibleRows = calculateVisibleRows(rows);
4081
- const panelLines = buildGroupedWorktreeLines(this.statusResult.worktrees, originalIndex);
4141
+ const panelLines = this.cachedPanelLines;
4082
4142
  let firstLine = -1;
4083
4143
  let lastLine = -1;
4084
4144
  for (let i = 0; i < panelLines.length; i++) {
@@ -4102,6 +4162,18 @@ var PanelStateManager = class {
4102
4162
  this.scrollOffset = groupStart;
4103
4163
  }
4104
4164
  }
4165
+ /**
4166
+ * 重建缓存的 panelLines
4167
+ * 在数据更新或导航变化时调用
4168
+ */
4169
+ rebuildCachedPanelLines() {
4170
+ if (!this.statusResult) {
4171
+ this.cachedPanelLines = [];
4172
+ return;
4173
+ }
4174
+ const originalIndex = this.getSelectedOriginalIndex();
4175
+ this.cachedPanelLines = buildGroupedWorktreeLines(this.statusResult.worktrees, originalIndex);
4176
+ }
4105
4177
  };
4106
4178
 
4107
4179
  // src/utils/interactive-panel.ts
@@ -4126,13 +4198,17 @@ var InteractivePanel = class {
4126
4198
  exitHandler;
4127
4199
  /** 操作锁(防止操作期间响应按键) */
4128
4200
  isOperating;
4201
+ /** 刷新锁(防止异步刷新期间触发重复刷新) */
4202
+ isRefreshing;
4129
4203
  /** Promise resolve 函数(stop 时调用以完成 start 返回的 Promise) */
4130
4204
  resolveStart;
4131
- /** 数据收集函数引用 */
4205
+ /** 数据收集函数引用(异步,支持并行收集 worktree 数据) */
4132
4206
  collectStatusFn;
4207
+ /** 上一帧的总行数,用于 footer-only 渲染时定位最后一行 */
4208
+ lastFrameLineCount = 0;
4133
4209
  /**
4134
4210
  * 创建交互式面板
4135
- * @param {() => StatusResult} collectStatusFn - 数据收集函数
4211
+ * @param {() => Promise<StatusResult>} collectStatusFn - 异步数据收集函数
4136
4212
  */
4137
4213
  constructor(collectStatusFn) {
4138
4214
  this.stateManager = new PanelStateManager();
@@ -4145,6 +4221,7 @@ var InteractivePanel = class {
4145
4221
  this.resizeHandler = null;
4146
4222
  this.exitHandler = null;
4147
4223
  this.isOperating = false;
4224
+ this.isRefreshing = false;
4148
4225
  this.resolveStart = null;
4149
4226
  this.collectStatusFn = collectStatusFn;
4150
4227
  }
@@ -4153,14 +4230,14 @@ var InteractivePanel = class {
4153
4230
  * 非 TTY 时打印提示并退出
4154
4231
  * @returns {Promise<void>} 面板关闭时 resolve
4155
4232
  */
4156
- start() {
4233
+ async start() {
4157
4234
  if (!this.isTTY) {
4158
4235
  console.log(PANEL_NOT_TTY);
4159
- return Promise.resolve();
4236
+ return;
4160
4237
  }
4238
+ this.stateManager.updateData(await this.collectStatusFn());
4161
4239
  return new Promise((resolve4) => {
4162
4240
  this.resolveStart = resolve4;
4163
- this.stateManager.updateData(this.collectStatusFn());
4164
4241
  this.initTerminal();
4165
4242
  this.keyboardController.start();
4166
4243
  this.startAutoRefresh();
@@ -4290,7 +4367,7 @@ var InteractivePanel = class {
4290
4367
  if (this.refreshCountdown > 0) {
4291
4368
  this.refreshCountdown--;
4292
4369
  }
4293
- this.render();
4370
+ this.renderFooterOnly();
4294
4371
  }, PANEL_COUNTDOWN_INTERVAL_MS);
4295
4372
  if (this.refreshTimer.unref) this.refreshTimer.unref();
4296
4373
  if (this.countdownTimer.unref) this.countdownTimer.unref();
@@ -4309,19 +4386,25 @@ var InteractivePanel = class {
4309
4386
  }
4310
4387
  }
4311
4388
  /**
4312
- * 刷新数据:记录当前选中分支 → 重新收集 → 恢复选中位置 → 重置倒计时 → 重绘
4389
+ * 刷新数据:记录当前选中分支 → 异步重新收集 → 恢复选中位置 → 重置倒计时 → 重绘
4390
+ * 使用 isRefreshing 锁防止异步刷新期间触发重复刷新
4313
4391
  */
4314
- refreshData() {
4315
- if (this.stopped || this.isOperating) return;
4316
- const previousBranch = this.stateManager.getSelectedBranch();
4317
- this.stateManager.updateData(this.collectStatusFn(), previousBranch || void 0);
4318
- this.stateManager.adjustScrollForSelection();
4319
- this.refreshCountdown = PANEL_REFRESH_INTERVAL_MS / 1e3;
4320
- this.render();
4392
+ async refreshData() {
4393
+ if (this.stopped || this.isOperating || this.isRefreshing) return;
4394
+ this.isRefreshing = true;
4395
+ try {
4396
+ const previousBranch = this.stateManager.getSelectedBranch();
4397
+ this.stateManager.updateData(await this.collectStatusFn(), previousBranch || void 0);
4398
+ this.stateManager.adjustScrollForSelection();
4399
+ this.refreshCountdown = PANEL_REFRESH_INTERVAL_MS / 1e3;
4400
+ this.render();
4401
+ } finally {
4402
+ this.isRefreshing = false;
4403
+ }
4321
4404
  }
4322
4405
  /**
4323
4406
  * 渲染一帧面板内容
4324
- * 使用同步输出防止闪烁
4407
+ * 使用同步输出防止闪烁,复用缓存的 panelLines 避免重复 groupWorktreesByDate 计算
4325
4408
  */
4326
4409
  render() {
4327
4410
  const statusResult = this.stateManager.getStatusResult();
@@ -4334,7 +4417,8 @@ var InteractivePanel = class {
4334
4417
  this.stateManager.getScrollOffset(),
4335
4418
  rows,
4336
4419
  cols,
4337
- this.refreshCountdown
4420
+ this.refreshCountdown,
4421
+ this.stateManager.getCachedPanelLines()
4338
4422
  );
4339
4423
  process.stdout.write(SYNC_OUTPUT_START);
4340
4424
  process.stdout.write(CLEAR_SCREEN);
@@ -4343,6 +4427,22 @@ var InteractivePanel = class {
4343
4427
  const suffix = i < frameLines.length - 1 ? "\n" : "";
4344
4428
  process.stdout.write(`${truncateToTerminalWidth(frameLines[i], cols)}${suffix}`);
4345
4429
  }
4430
+ this.lastFrameLineCount = frameLines.length;
4431
+ process.stdout.write(SYNC_OUTPUT_END);
4432
+ }
4433
+ /**
4434
+ * 仅更新 footer 行(倒计时文本)
4435
+ * 使用 ANSI 光标定位直接覆写最后一行,避免全量重绘
4436
+ */
4437
+ renderFooterOnly() {
4438
+ if (this.stopped || this.isOperating || this.lastFrameLineCount === 0) return;
4439
+ const cols = process.stdout.columns || DEFAULT_TERMINAL_COLUMNS;
4440
+ const footerText = renderFooter(this.refreshCountdown);
4441
+ const truncated = truncateToTerminalWidth(footerText, cols);
4442
+ process.stdout.write(SYNC_OUTPUT_START);
4443
+ process.stdout.write(`\x1B[${this.lastFrameLineCount};1H`);
4444
+ process.stdout.write("\x1B[2K");
4445
+ process.stdout.write(truncated);
4346
4446
  process.stdout.write(SYNC_OUTPUT_END);
4347
4447
  }
4348
4448
  /**
@@ -4363,7 +4463,7 @@ var InteractivePanel = class {
4363
4463
  this.initTerminal();
4364
4464
  this.keyboardController.start();
4365
4465
  this.isOperating = false;
4366
- this.refreshData();
4466
+ await this.refreshData();
4367
4467
  this.startAutoRefresh();
4368
4468
  this.render();
4369
4469
  }
@@ -5507,7 +5607,7 @@ async function handleStatus(options) {
5507
5607
  await panel.start();
5508
5608
  return;
5509
5609
  }
5510
- const statusResult = collectStatus();
5610
+ const statusResult = await collectStatus();
5511
5611
  logger.info(`status \u547D\u4EE4\u6267\u884C\uFF0C\u9879\u76EE: ${statusResult.main.projectName}\uFF0C\u5171 ${statusResult.totalWorktrees} \u4E2A worktree`);
5512
5612
  if (options.json) {
5513
5613
  printStatusAsJson(statusResult);
@@ -5515,7 +5615,7 @@ async function handleStatus(options) {
5515
5615
  }
5516
5616
  printStatusAsText(statusResult);
5517
5617
  }
5518
- function collectStatus() {
5618
+ async function collectStatus() {
5519
5619
  const projectName = getProjectName();
5520
5620
  const currentBranch = getCurrentBranch();
5521
5621
  const isClean = isWorkingDirClean();
@@ -5533,7 +5633,9 @@ function collectStatus() {
5533
5633
  deletions
5534
5634
  };
5535
5635
  const worktrees = getProjectWorktrees();
5536
- const worktreeStatuses = worktrees.map((wt) => collectWorktreeDetailedStatus(wt, projectName));
5636
+ const worktreeStatuses = await Promise.all(
5637
+ worktrees.map((wt) => collectWorktreeDetailedStatusAsync(wt, projectName))
5638
+ );
5537
5639
  const snapshots = collectSnapshots(projectName, worktrees);
5538
5640
  return {
5539
5641
  main: main2,
@@ -5542,49 +5644,61 @@ function collectStatus() {
5542
5644
  totalWorktrees: worktrees.length
5543
5645
  };
5544
5646
  }
5545
- function collectWorktreeDetailedStatus(worktree, projectName) {
5546
- const changeStatus = detectChangeStatus(worktree);
5547
- const { commitsAhead, commitsBehind } = countCommitDivergence(worktree.branch);
5548
- const { insertions, deletions } = countDiffStat(worktree.path);
5647
+ async function collectWorktreeDetailedStatusAsync(worktree, projectName) {
5648
+ const [divergence, porcelain, diffStat] = await Promise.all([
5649
+ countCommitDivergenceAsync(worktree.branch),
5650
+ detectStatusPorcelainAsync(worktree.path),
5651
+ countDiffStatAsync(worktree.path)
5652
+ ]);
5653
+ const changeStatus = detectChangeStatusFromPorcelain(porcelain, divergence.commitsAhead);
5549
5654
  const createdAt = getWorktreeCreatedTime(worktree.path);
5550
5655
  return {
5551
5656
  path: worktree.path,
5552
5657
  branch: worktree.branch,
5553
5658
  changeStatus,
5554
- commitsAhead,
5555
- commitsBehind,
5659
+ commitsAhead: divergence.commitsAhead,
5660
+ commitsBehind: divergence.commitsBehind,
5556
5661
  snapshotTime: resolveSnapshotTime(projectName, worktree.branch),
5557
- insertions,
5558
- deletions,
5662
+ insertions: diffStat.insertions,
5663
+ deletions: diffStat.deletions,
5559
5664
  createdAt
5560
5665
  };
5561
5666
  }
5562
- function detectChangeStatus(worktree) {
5667
+ function detectChangeStatusFromPorcelain(porcelain, commitsAhead) {
5668
+ const hasConflict = porcelain.split("\n").some((line) => /^(UU|AA|DD|DU|UD|AU|UA)/.test(line));
5669
+ if (hasConflict) {
5670
+ return "conflict";
5671
+ }
5672
+ if (porcelain !== "") {
5673
+ return "uncommitted";
5674
+ }
5675
+ if (commitsAhead > 0) {
5676
+ return "committed";
5677
+ }
5678
+ return "clean";
5679
+ }
5680
+ async function detectStatusPorcelainAsync(worktreePath) {
5563
5681
  try {
5564
- if (hasMergeConflict(worktree.path)) {
5565
- return "conflict";
5566
- }
5567
- if (!isWorkingDirClean(worktree.path)) {
5568
- return "uncommitted";
5569
- }
5570
- if (hasLocalCommits(worktree.branch)) {
5571
- return "committed";
5572
- }
5573
- return "clean";
5682
+ return await getStatusPorcelainAsync(worktreePath);
5574
5683
  } catch {
5575
- return "clean";
5684
+ return "";
5576
5685
  }
5577
5686
  }
5578
- function countCommitDivergence(branchName) {
5687
+ async function countCommitDivergenceAsync(branchName) {
5579
5688
  try {
5580
- return {
5581
- commitsAhead: getCommitCountAhead(branchName),
5582
- commitsBehind: getCommitCountBehind(branchName)
5583
- };
5689
+ const { ahead, behind } = await getCommitDivergenceAsync(branchName);
5690
+ return { commitsAhead: ahead, commitsBehind: behind };
5584
5691
  } catch {
5585
5692
  return { commitsAhead: 0, commitsBehind: 0 };
5586
5693
  }
5587
5694
  }
5695
+ async function countDiffStatAsync(worktreePath) {
5696
+ try {
5697
+ return await getDiffStatAsync(worktreePath);
5698
+ } catch {
5699
+ return { insertions: 0, deletions: 0 };
5700
+ }
5701
+ }
5588
5702
  function countDiffStat(worktreePath) {
5589
5703
  try {
5590
5704
  return getDiffStat(worktreePath);
@@ -6189,7 +6303,8 @@ async function handleInitShow(options) {
6189
6303
  PROJECT_CONFIG_DEFINITIONS,
6190
6304
  { selectPrompt: MESSAGES.INIT_SELECT_PROMPT }
6191
6305
  );
6192
- const updatedConfig = { ...config2, [key]: newValue };
6306
+ const mergedConfig = { ...config2, [key]: newValue };
6307
+ const updatedConfig = normalizeProjectConfig(mergedConfig, key, newValue);
6193
6308
  saveProjectConfig(updatedConfig);
6194
6309
  printSuccess(MESSAGES.INIT_SET_SUCCESS(key, String(newValue)));
6195
6310
  }