clawt 3.4.6 → 3.5.0

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.
Files changed (44) hide show
  1. package/.claude/settings.local.json +12 -0
  2. package/README.md +0 -4
  3. package/dist/index.js +425 -305
  4. package/dist/postinstall.js +12 -1
  5. package/docs/alias.md +7 -1
  6. package/docs/completion.md +1 -1
  7. package/docs/config.md +4 -3
  8. package/docs/cover-validate.md +4 -3
  9. package/docs/create.md +28 -12
  10. package/docs/home.md +12 -8
  11. package/docs/init.md +16 -9
  12. package/docs/list.md +13 -7
  13. package/docs/merge.md +12 -12
  14. package/docs/remove.md +24 -13
  15. package/docs/reset.md +6 -4
  16. package/docs/resume.md +3 -4
  17. package/docs/status.md +75 -30
  18. package/docs/sync.md +26 -26
  19. package/docs/validate.md +13 -7
  20. package/package.json +1 -1
  21. package/src/commands/tasks.ts +51 -0
  22. package/src/constants/index.ts +3 -0
  23. package/src/constants/interactive-panel.ts +6 -0
  24. package/src/constants/messages/index.ts +4 -2
  25. package/src/constants/messages/interactive-panel.ts +12 -0
  26. package/src/constants/messages/tasks.ts +9 -0
  27. package/src/constants/tasks-template.ts +28 -0
  28. package/src/index.ts +2 -0
  29. package/src/types/command.ts +6 -0
  30. package/src/types/index.ts +1 -1
  31. package/src/utils/formatter.ts +19 -0
  32. package/src/utils/git-branch.ts +116 -0
  33. package/src/utils/git-core.ts +369 -0
  34. package/src/utils/git-worktree.ts +40 -0
  35. package/src/utils/git.ts +3 -521
  36. package/src/utils/index.ts +1 -1
  37. package/src/utils/interactive-panel-render.ts +12 -6
  38. package/src/utils/interactive-panel-state.ts +137 -0
  39. package/src/utils/interactive-panel.ts +44 -188
  40. package/src/utils/keyboard-controller.ts +48 -0
  41. package/src/utils/ui-prompts.ts +240 -0
  42. package/src/utils/worktree-matcher.ts +21 -251
  43. package/tests/unit/commands/tasks.test.ts +153 -0
  44. package/tests/unit/utils/formatter.test.ts +26 -1
package/dist/index.js CHANGED
@@ -513,6 +513,16 @@ var HOME_MESSAGES = {
513
513
  HOME_SWITCH_SUCCESS: (from, to) => `\u2713 \u5DF2\u4ECE ${from} \u5207\u6362\u5230\u4E3B\u5DE5\u4F5C\u5206\u652F ${to}`
514
514
  };
515
515
 
516
+ // src/constants/messages/tasks.ts
517
+ var TASKS_CMD_MESSAGES = {
518
+ /** 任务模板文件已存在 */
519
+ TASK_INIT_FILE_EXISTS: (path2) => `\u6587\u4EF6\u5DF2\u5B58\u5728: ${path2}\uFF0C\u5982\u9700\u8986\u76D6\u8BF7\u5148\u5220\u9664`,
520
+ /** 任务模板生成成功 */
521
+ TASK_INIT_SUCCESS: (path2) => `\u2713 \u4EFB\u52A1\u6A21\u677F\u5DF2\u751F\u6210: ${path2}`,
522
+ /** 任务模板使用提示 */
523
+ TASK_INIT_HINT: (path2) => `\u4F7F\u7528 clawt run -f ${path2} \u6267\u884C\u4EFB\u52A1`
524
+ };
525
+
516
526
  // src/constants/messages/interactive-panel.ts
517
527
  import chalk from "chalk";
518
528
 
@@ -544,6 +554,8 @@ var PANEL_SHORTCUT_KEYS = {
544
554
  };
545
555
  var PANEL_DATE_SEPARATOR_PREFIX = "\u2550\u2550\u2550\u2550";
546
556
  var PANEL_FIXED_ROWS = 5;
557
+ var PANEL_SEPARATOR_MAX_WIDTH = 60;
558
+ var PANEL_DATE_COLOR = "#FF8C00";
547
559
 
548
560
  // src/constants/messages/interactive-panel.ts
549
561
  var SHORTCUT_LABELS = {
@@ -575,6 +587,10 @@ var PANEL_CONFIGURED_BRANCH = (branchName) => chalk.gray(`\u4E3B\u5DE5\u4F5C\u52
575
587
  var PANEL_CONFIGURED_BRANCH_DELETED = (branchName) => chalk.red(`\u2717 \u4E3B\u5DE5\u4F5C\u5206\u652F: ${branchName}\uFF08\u5DF2\u4E0D\u5B58\u5728\uFF09`);
576
588
  var PANEL_CONFIGURED_BRANCH_MISMATCH = (branchName) => chalk.yellow(`\u26A0 \u4E3B\u5DE5\u4F5C\u5206\u652F: ${branchName}\uFF08\u4E0D\u4E00\u81F4\uFF09`);
577
589
  var PANEL_NOT_INITIALIZED = chalk.gray("\u672A\u521D\u59CB\u5316\uFF08\u6267\u884C clawt init \u8BBE\u7F6E\u4E3B\u5DE5\u4F5C\u5206\u652F\uFF09");
590
+ var PANEL_UNKNOWN_DATE = "\u672A\u77E5\u65E5\u671F";
591
+ var PANEL_SYNCED_WITH_MAIN = "\u4E0E\u4E3B\u5206\u652F\u540C\u6B65";
592
+ var PANEL_COMMITS_AHEAD = (count) => `${count} \u4E2A\u672C\u5730\u63D0\u4EA4`;
593
+ var PANEL_COMMITS_BEHIND = (count) => `\u843D\u540E\u4E3B\u5206\u652F ${count} \u4E2A\u63D0\u4EA4`;
578
594
 
579
595
  // src/constants/messages/index.ts
580
596
  var MESSAGES = {
@@ -594,7 +610,8 @@ var MESSAGES = {
594
610
  ...COMPLETION_MESSAGES,
595
611
  ...INIT_MESSAGES,
596
612
  ...COVER_VALIDATE_MESSAGES,
597
- ...HOME_MESSAGES
613
+ ...HOME_MESSAGES,
614
+ ...TASKS_CMD_MESSAGES
598
615
  };
599
616
 
600
617
  // src/constants/exitCodes.ts
@@ -777,6 +794,31 @@ var PRE_CHECK_RESUME = {
777
794
  requireClaudeCode: true
778
795
  };
779
796
 
797
+ // src/constants/tasks-template.ts
798
+ var TASK_TEMPLATE_OUTPUT_DIR = "clawt/tasks";
799
+ var TASK_TEMPLATE_FILENAME_PREFIX = "clawt-tasks";
800
+ var TASK_TEMPLATE_CONTENT = `# Clawt \u4EFB\u52A1\u6587\u4EF6
801
+ #
802
+ # \u4F7F\u7528\u65B9\u6CD5: clawt run -f tasks.md
803
+ # \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
804
+ #
805
+ # \u89C4\u5219:
806
+ # 1. \u6BCF\u4E2A\u4EFB\u52A1\u5757\u7528 <!-- CLAWT-TASKS:START --> \u548C <!-- CLAWT-TASKS:END --> \u5305\u88F9
807
+ # 2. \u5757\u5185 # branch: <\u5206\u652F\u540D> \u58F0\u660E\u5206\u652F\u540D\uFF08\u4F7F\u7528 -b \u53C2\u6570\u65F6\u53EF\u7701\u7565\uFF09
808
+ # 3. \u5757\u5185\u5176\u4F59\u884C\u4E3A\u4EFB\u52A1\u63CF\u8FF0\uFF08\u652F\u6301\u591A\u884C\uFF09
809
+
810
+ <!-- CLAWT-TASKS:START -->
811
+ # branch: feat-example-1
812
+ \u5728\u8FD9\u91CC\u5199\u7B2C\u4E00\u4E2A\u4EFB\u52A1\u7684\u63CF\u8FF0
813
+ <!-- CLAWT-TASKS:END -->
814
+
815
+ <!-- CLAWT-TASKS:START -->
816
+ # branch: feat-example-2
817
+ \u5728\u8FD9\u91CC\u5199\u7B2C\u4E8C\u4E2A\u4EFB\u52A1\u7684\u63CF\u8FF0
818
+ \u652F\u6301\u591A\u884C\u63CF\u8FF0
819
+ <!-- CLAWT-TASKS:END -->
820
+ `;
821
+
780
822
  // src/errors/index.ts
781
823
  var ClawtError = class extends Error {
782
824
  /** 退出码 */
@@ -900,7 +942,7 @@ function parseParallelCommands(commandString) {
900
942
  }
901
943
  function runParallelCommands(commands, options) {
902
944
  const promises = commands.map((command) => {
903
- return new Promise((resolve3) => {
945
+ return new Promise((resolve4) => {
904
946
  logger.debug(`\u5E76\u884C\u542F\u52A8\u547D\u4EE4: ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ""}`);
905
947
  const child = spawn(command, {
906
948
  cwd: options?.cwd,
@@ -908,17 +950,17 @@ function runParallelCommands(commands, options) {
908
950
  shell: true
909
951
  });
910
952
  child.on("error", (err) => {
911
- resolve3({ command, exitCode: 1, error: err.message });
953
+ resolve4({ command, exitCode: 1, error: err.message });
912
954
  });
913
955
  child.on("close", (code) => {
914
- resolve3({ command, exitCode: code ?? 1 });
956
+ resolve4({ command, exitCode: code ?? 1 });
915
957
  });
916
958
  });
917
959
  });
918
960
  return Promise.all(promises);
919
961
  }
920
962
 
921
- // src/utils/git.ts
963
+ // src/utils/git-core.ts
922
964
  import { basename } from "path";
923
965
  import { execSync as execSync2 } from "child_process";
924
966
  function getGitCommonDir(cwd) {
@@ -931,26 +973,6 @@ function getProjectName(cwd) {
931
973
  const topLevel = getGitTopLevel(cwd);
932
974
  return basename(topLevel);
933
975
  }
934
- function checkBranchExists(branchName, cwd) {
935
- try {
936
- execCommand(`git show-ref --verify refs/heads/${branchName}`, { cwd });
937
- return true;
938
- } catch {
939
- return false;
940
- }
941
- }
942
- function createWorktree(branchName, worktreePath, cwd) {
943
- logger.info(`\u521B\u5EFA worktree: ${worktreePath}`);
944
- execCommand(`git worktree add -b ${branchName} "${worktreePath}"`, { cwd });
945
- }
946
- function removeWorktreeByPath(worktreePath, cwd) {
947
- logger.info(`\u79FB\u9664 worktree: ${worktreePath}`);
948
- execCommand(`git worktree remove -f "${worktreePath}"`, { cwd });
949
- }
950
- function deleteBranch(branchName, cwd) {
951
- logger.info(`\u5220\u9664\u5206\u652F: ${branchName}`);
952
- execCommand(`git branch -D ${branchName}`, { cwd });
953
- }
954
976
  function getStatusPorcelain(cwd) {
955
977
  return execCommand("git status --porcelain", { cwd });
956
978
  }
@@ -988,32 +1010,6 @@ function gitStashPush(message, cwd) {
988
1010
  function gitRestoreStaged(cwd) {
989
1011
  execCommand("git restore --staged .", { cwd });
990
1012
  }
991
- function gitWorktreeList(cwd) {
992
- return execCommand("git worktree list", { cwd });
993
- }
994
- function gitWorktreePrune(cwd) {
995
- execCommand("git worktree prune", { cwd });
996
- }
997
- function hasLocalCommits(branchName, cwd) {
998
- try {
999
- const output = execCommand(`git log HEAD..${branchName} --oneline`, { cwd });
1000
- return output.trim() !== "";
1001
- } catch {
1002
- return false;
1003
- }
1004
- }
1005
- function getCommitCountAhead(branchName, cwd) {
1006
- const output = execCommand(`git rev-list --count HEAD..${branchName}`, { cwd });
1007
- return parseInt(output, 10) || 0;
1008
- }
1009
- function getCommitCountBehind(branchName, cwd) {
1010
- try {
1011
- const output = execCommand(`git rev-list --count ${branchName}..HEAD`, { cwd });
1012
- return parseInt(output, 10) || 0;
1013
- } catch {
1014
- return 0;
1015
- }
1016
- }
1017
1013
  function parseShortStat(output) {
1018
1014
  let insertions = 0;
1019
1015
  let deletions = 0;
@@ -1034,9 +1030,6 @@ function getDiffStat(worktreePath) {
1034
1030
  function gitApplyCachedFromStdin(patchContent, cwd) {
1035
1031
  execCommandWithInput("git", ["apply", "--cached"], { input: patchContent, cwd });
1036
1032
  }
1037
- function getCurrentBranch(cwd) {
1038
- return execCommand("git rev-parse --abbrev-ref HEAD", { cwd });
1039
- }
1040
1033
  function getHeadCommitHash(cwd) {
1041
1034
  return execCommand("git rev-parse HEAD", { cwd });
1042
1035
  }
@@ -1092,6 +1085,43 @@ function gitApplyCachedCheck(patchContent, cwd) {
1092
1085
  return false;
1093
1086
  }
1094
1087
  }
1088
+
1089
+ // src/utils/git-branch.ts
1090
+ function checkBranchExists(branchName, cwd) {
1091
+ try {
1092
+ execCommand(`git show-ref --verify refs/heads/${branchName}`, { cwd });
1093
+ return true;
1094
+ } catch {
1095
+ return false;
1096
+ }
1097
+ }
1098
+ function deleteBranch(branchName, cwd) {
1099
+ logger.info(`\u5220\u9664\u5206\u652F: ${branchName}`);
1100
+ execCommand(`git branch -D ${branchName}`, { cwd });
1101
+ }
1102
+ function hasLocalCommits(branchName, cwd) {
1103
+ try {
1104
+ const output = execCommand(`git log HEAD..${branchName} --oneline`, { cwd });
1105
+ return output.trim() !== "";
1106
+ } catch {
1107
+ return false;
1108
+ }
1109
+ }
1110
+ function getCommitCountAhead(branchName, cwd) {
1111
+ const output = execCommand(`git rev-list --count HEAD..${branchName}`, { cwd });
1112
+ return parseInt(output, 10) || 0;
1113
+ }
1114
+ function getCommitCountBehind(branchName, cwd) {
1115
+ try {
1116
+ const output = execCommand(`git rev-list --count ${branchName}..HEAD`, { cwd });
1117
+ return parseInt(output, 10) || 0;
1118
+ } catch {
1119
+ return 0;
1120
+ }
1121
+ }
1122
+ function getCurrentBranch(cwd) {
1123
+ return execCommand("git rev-parse --abbrev-ref HEAD", { cwd });
1124
+ }
1095
1125
  function gitCheckout(branchName, cwd) {
1096
1126
  execCommand(`git checkout ${branchName}`, { cwd });
1097
1127
  }
@@ -1099,6 +1129,22 @@ function createBranch(branchName, cwd) {
1099
1129
  execCommand(`git branch ${branchName}`, { cwd });
1100
1130
  }
1101
1131
 
1132
+ // src/utils/git-worktree.ts
1133
+ function createWorktree(branchName, worktreePath, cwd) {
1134
+ logger.info(`\u521B\u5EFA worktree: ${worktreePath}`);
1135
+ execCommand(`git worktree add -b ${branchName} "${worktreePath}"`, { cwd });
1136
+ }
1137
+ function removeWorktreeByPath(worktreePath, cwd) {
1138
+ logger.info(`\u79FB\u9664 worktree: ${worktreePath}`);
1139
+ execCommand(`git worktree remove -f "${worktreePath}"`, { cwd });
1140
+ }
1141
+ function gitWorktreeList(cwd) {
1142
+ return execCommand("git worktree list", { cwd });
1143
+ }
1144
+ function gitWorktreePrune(cwd) {
1145
+ execCommand("git worktree prune", { cwd });
1146
+ }
1147
+
1102
1148
  // src/utils/formatter.ts
1103
1149
  import chalk4 from "chalk";
1104
1150
  import { createInterface } from "readline";
@@ -1124,14 +1170,14 @@ function printDoubleSeparator() {
1124
1170
  console.log(MESSAGES.DOUBLE_SEPARATOR);
1125
1171
  }
1126
1172
  function confirmAction(question) {
1127
- return new Promise((resolve3) => {
1173
+ return new Promise((resolve4) => {
1128
1174
  const rl = createInterface({
1129
1175
  input: process.stdin,
1130
1176
  output: process.stdout
1131
1177
  });
1132
1178
  rl.question(`${question} (y/N) `, (answer) => {
1133
1179
  rl.close();
1134
- resolve3(answer.toLowerCase() === "y");
1180
+ resolve4(answer.toLowerCase() === "y");
1135
1181
  });
1136
1182
  });
1137
1183
  }
@@ -1222,6 +1268,19 @@ function formatLocalISOString(date) {
1222
1268
  const minutes = String(absMinutes % 60).padStart(2, "0");
1223
1269
  return `${iso}${sign}${hours}:${minutes}`;
1224
1270
  }
1271
+ function generateTaskFilename(prefix) {
1272
+ const now = /* @__PURE__ */ new Date();
1273
+ const pad = (n) => String(n).padStart(2, "0");
1274
+ const timestamp = [
1275
+ now.getFullYear(),
1276
+ pad(now.getMonth() + 1),
1277
+ pad(now.getDate()),
1278
+ pad(now.getHours()),
1279
+ pad(now.getMinutes()),
1280
+ pad(now.getSeconds())
1281
+ ].join("-");
1282
+ return `${prefix}-${timestamp}.md`;
1283
+ }
1225
1284
 
1226
1285
  // src/utils/branch.ts
1227
1286
  function sanitizeBranchName(branchName) {
@@ -1865,15 +1924,10 @@ function removeProjectSnapshots(projectName) {
1865
1924
  }
1866
1925
 
1867
1926
  // src/utils/worktree-matcher.ts
1868
- import Enquirer3 from "enquirer";
1869
1927
  import { statSync as statSync3 } from "fs";
1870
- function findExactMatch(worktrees, branchName) {
1871
- return worktrees.find((wt) => wt.branch === branchName);
1872
- }
1873
- function findFuzzyMatches(worktrees, keyword) {
1874
- const lowerKeyword = keyword.toLowerCase();
1875
- return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerKeyword));
1876
- }
1928
+
1929
+ // src/utils/ui-prompts.ts
1930
+ import Enquirer3 from "enquirer";
1877
1931
  async function promptSelectBranch(worktrees, message) {
1878
1932
  const selectedBranch = await new Enquirer3.Select({
1879
1933
  message,
@@ -1923,6 +1977,77 @@ async function promptMultiSelectBranches(worktrees, message) {
1923
1977
  }).run();
1924
1978
  return worktrees.filter((wt) => selectedBranches.includes(wt.branch));
1925
1979
  }
1980
+ async function promptGroupedMultiSelectBranches(worktrees, message) {
1981
+ const groups = groupWorktreesByDate(worktrees);
1982
+ const choices = buildGroupedChoices(groups);
1983
+ const groupMembershipMap = buildGroupMembershipMap(groups);
1984
+ const groupSelectAllNames = new Set(groupMembershipMap.keys());
1985
+ const allBranchNames = new Set(worktrees.map((wt) => wt.branch));
1986
+ const MultiSelect = Enquirer3.MultiSelect;
1987
+ class MultiSelectWithGroupSelectAll extends MultiSelect {
1988
+ space() {
1989
+ if (!this.focused) return;
1990
+ const focusedName = this.focused.name;
1991
+ if (focusedName === SELECT_ALL_NAME) {
1992
+ const willEnable = !this.focused.enabled;
1993
+ for (const ch of this.choices) {
1994
+ ch.enabled = willEnable;
1995
+ }
1996
+ return this.render();
1997
+ }
1998
+ if (groupSelectAllNames.has(focusedName)) {
1999
+ const willEnable = !this.focused.enabled;
2000
+ const memberNames = groupMembershipMap.get(focusedName);
2001
+ this.focused.enabled = willEnable;
2002
+ for (const ch of this.choices) {
2003
+ if (memberNames.includes(ch.name)) {
2004
+ ch.enabled = willEnable;
2005
+ }
2006
+ }
2007
+ syncGlobalSelectAll(this.choices);
2008
+ return this.render();
2009
+ }
2010
+ this.toggle(this.focused);
2011
+ syncGroupSelectAll(this.choices, focusedName);
2012
+ syncGlobalSelectAll(this.choices);
2013
+ return this.render();
2014
+ }
2015
+ }
2016
+ function syncGlobalSelectAll(choiceList) {
2017
+ const selectAllChoice = choiceList.find((ch) => ch.name === SELECT_ALL_NAME);
2018
+ if (!selectAllChoice) return;
2019
+ const branchItems = choiceList.filter((ch) => allBranchNames.has(ch.name));
2020
+ selectAllChoice.enabled = branchItems.length > 0 && branchItems.every((ch) => ch.enabled);
2021
+ }
2022
+ function syncGroupSelectAll(choiceList, branchName) {
2023
+ for (const [groupName, memberNames] of groupMembershipMap) {
2024
+ if (!memberNames.includes(branchName)) continue;
2025
+ const groupChoice = choiceList.find((ch) => ch.name === groupName);
2026
+ if (!groupChoice) continue;
2027
+ const memberChoices = choiceList.filter((ch) => memberNames.includes(ch.name));
2028
+ groupChoice.enabled = memberChoices.length > 0 && memberChoices.every((ch) => ch.enabled);
2029
+ break;
2030
+ }
2031
+ }
2032
+ const selectedBranches = await new MultiSelectWithGroupSelectAll({
2033
+ message,
2034
+ choices,
2035
+ // 使用空心圆/实心圆作为选中指示符
2036
+ symbols: {
2037
+ indicator: { on: "\u25CF", off: "\u25CB" }
2038
+ }
2039
+ }).run();
2040
+ return worktrees.filter((wt) => selectedBranches.includes(wt.branch));
2041
+ }
2042
+
2043
+ // src/utils/worktree-matcher.ts
2044
+ function findExactMatch(worktrees, branchName) {
2045
+ return worktrees.find((wt) => wt.branch === branchName);
2046
+ }
2047
+ function findFuzzyMatches(worktrees, keyword) {
2048
+ const lowerKeyword = keyword.toLowerCase();
2049
+ return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerKeyword));
2050
+ }
1926
2051
  async function resolveTargetWorktrees(worktrees, messages, branchName) {
1927
2052
  if (worktrees.length === 0) {
1928
2053
  throw new ClawtError(messages.noWorktrees);
@@ -2050,68 +2175,6 @@ function buildGroupMembershipMap(groups) {
2050
2175
  }
2051
2176
  return map;
2052
2177
  }
2053
- async function promptGroupedMultiSelectBranches(worktrees, message) {
2054
- const groups = groupWorktreesByDate(worktrees);
2055
- const choices = buildGroupedChoices(groups);
2056
- const groupMembershipMap = buildGroupMembershipMap(groups);
2057
- const groupSelectAllNames = new Set(groupMembershipMap.keys());
2058
- const allBranchNames = new Set(worktrees.map((wt) => wt.branch));
2059
- const MultiSelect = Enquirer3.MultiSelect;
2060
- class MultiSelectWithGroupSelectAll extends MultiSelect {
2061
- space() {
2062
- if (!this.focused) return;
2063
- const focusedName = this.focused.name;
2064
- if (focusedName === SELECT_ALL_NAME) {
2065
- const willEnable = !this.focused.enabled;
2066
- for (const ch of this.choices) {
2067
- ch.enabled = willEnable;
2068
- }
2069
- return this.render();
2070
- }
2071
- if (groupSelectAllNames.has(focusedName)) {
2072
- const willEnable = !this.focused.enabled;
2073
- const memberNames = groupMembershipMap.get(focusedName);
2074
- this.focused.enabled = willEnable;
2075
- for (const ch of this.choices) {
2076
- if (memberNames.includes(ch.name)) {
2077
- ch.enabled = willEnable;
2078
- }
2079
- }
2080
- syncGlobalSelectAll(this.choices);
2081
- return this.render();
2082
- }
2083
- this.toggle(this.focused);
2084
- syncGroupSelectAll(this.choices, focusedName);
2085
- syncGlobalSelectAll(this.choices);
2086
- return this.render();
2087
- }
2088
- }
2089
- function syncGlobalSelectAll(choiceList) {
2090
- const selectAllChoice = choiceList.find((ch) => ch.name === SELECT_ALL_NAME);
2091
- if (!selectAllChoice) return;
2092
- const branchItems = choiceList.filter((ch) => allBranchNames.has(ch.name));
2093
- selectAllChoice.enabled = branchItems.length > 0 && branchItems.every((ch) => ch.enabled);
2094
- }
2095
- function syncGroupSelectAll(choiceList, branchName) {
2096
- for (const [groupName, memberNames] of groupMembershipMap) {
2097
- if (!memberNames.includes(branchName)) continue;
2098
- const groupChoice = choiceList.find((ch) => ch.name === groupName);
2099
- if (!groupChoice) continue;
2100
- const memberChoices = choiceList.filter((ch) => memberNames.includes(ch.name));
2101
- groupChoice.enabled = memberChoices.length > 0 && memberChoices.every((ch) => ch.enabled);
2102
- break;
2103
- }
2104
- }
2105
- const selectedBranches = await new MultiSelectWithGroupSelectAll({
2106
- message,
2107
- choices,
2108
- // 使用空心圆/实心圆作为选中指示符
2109
- symbols: {
2110
- indicator: { on: "\u25CF", off: "\u25CB" }
2111
- }
2112
- }).run();
2113
- return worktrees.filter((wt) => selectedBranches.includes(wt.branch));
2114
- }
2115
2178
 
2116
2179
  // src/utils/progress-render.ts
2117
2180
  import chalk5 from "chalk";
@@ -2586,7 +2649,7 @@ function executeClaudeTask(worktree, task, onActivity) {
2586
2649
  stdio: ["ignore", "pipe", "pipe"]
2587
2650
  }
2588
2651
  );
2589
- const promise = new Promise((resolve3) => {
2652
+ const promise = new Promise((resolve4) => {
2590
2653
  let stderr = "";
2591
2654
  let finalResult = null;
2592
2655
  const lineBuffer = createLineBuffer();
@@ -2622,7 +2685,7 @@ function executeClaudeTask(worktree, task, onActivity) {
2622
2685
  if (finalResult) {
2623
2686
  success = !finalResult.is_error;
2624
2687
  }
2625
- resolve3({
2688
+ resolve4({
2626
2689
  task,
2627
2690
  branch: worktree.branch,
2628
2691
  worktreePath: worktree.path,
@@ -2632,7 +2695,7 @@ function executeClaudeTask(worktree, task, onActivity) {
2632
2695
  });
2633
2696
  });
2634
2697
  child.on("error", (err) => {
2635
- resolve3({
2698
+ resolve4({
2636
2699
  task,
2637
2700
  branch: worktree.branch,
2638
2701
  worktreePath: worktree.path,
@@ -2691,7 +2754,7 @@ async function executeWithConcurrency(worktrees, tasks, concurrency, renderer, s
2691
2754
  const results = new Array(total);
2692
2755
  let nextIndex = 0;
2693
2756
  let completedCount = 0;
2694
- return new Promise((resolve3) => {
2757
+ return new Promise((resolve4) => {
2695
2758
  function launchNext() {
2696
2759
  if (nextIndex >= total || isInterrupted()) return;
2697
2760
  const index = nextIndex;
@@ -2712,7 +2775,7 @@ async function executeWithConcurrency(worktrees, tasks, concurrency, renderer, s
2712
2775
  }
2713
2776
  launchNext();
2714
2777
  if (completedCount === total) {
2715
- resolve3(results);
2778
+ resolve4(results);
2716
2779
  }
2717
2780
  });
2718
2781
  }
@@ -2767,11 +2830,11 @@ async function executeBatchTasks(worktrees, tasks, concurrency) {
2767
2830
  printWarning(MESSAGES.INTERRUPTED);
2768
2831
  killAllChildProcesses(childProcesses);
2769
2832
  await Promise.allSettled(childProcesses.map(
2770
- (cp) => new Promise((resolve3) => {
2833
+ (cp) => new Promise((resolve4) => {
2771
2834
  if (cp.exitCode !== null) {
2772
- resolve3();
2835
+ resolve4();
2773
2836
  } else {
2774
- cp.on("close", () => resolve3());
2837
+ cp.on("close", () => resolve4());
2775
2838
  }
2776
2839
  })
2777
2840
  ));
@@ -3013,7 +3076,7 @@ function isNewerVersion(latest, current) {
3013
3076
  return false;
3014
3077
  }
3015
3078
  function fetchLatestVersion() {
3016
- return new Promise((resolve3) => {
3079
+ return new Promise((resolve4) => {
3017
3080
  const req = request(NPM_REGISTRY_URL, { timeout: NPM_REGISTRY_TIMEOUT_MS }, (res) => {
3018
3081
  let data = "";
3019
3082
  res.on("data", (chunk) => {
@@ -3022,16 +3085,16 @@ function fetchLatestVersion() {
3022
3085
  res.on("end", () => {
3023
3086
  try {
3024
3087
  const parsed = JSON.parse(data);
3025
- resolve3(parsed.version ?? null);
3088
+ resolve4(parsed.version ?? null);
3026
3089
  } catch {
3027
- resolve3(null);
3090
+ resolve4(null);
3028
3091
  }
3029
3092
  });
3030
3093
  });
3031
- req.on("error", () => resolve3(null));
3094
+ req.on("error", () => resolve4(null));
3032
3095
  req.on("timeout", () => {
3033
3096
  req.destroy();
3034
- resolve3(null);
3097
+ resolve4(null);
3035
3098
  });
3036
3099
  req.end();
3037
3100
  });
@@ -3252,7 +3315,7 @@ import { createInterface as createInterface2 } from "readline";
3252
3315
  import chalk9 from "chalk";
3253
3316
  import stringWidth3 from "string-width";
3254
3317
  function buildSeparatorWithHint(cols, hint) {
3255
- const maxWidth = Math.min(cols, 60);
3318
+ const maxWidth = Math.min(cols, PANEL_SEPARATOR_MAX_WIDTH);
3256
3319
  if (!hint) {
3257
3320
  return chalk9.gray("\u2500".repeat(maxWidth));
3258
3321
  }
@@ -3337,10 +3400,10 @@ function buildGroupedWorktreeLines(worktrees, selectedIndex) {
3337
3400
  function renderDateSeparator(dateKey) {
3338
3401
  const leftPad = " ";
3339
3402
  if (dateKey === UNKNOWN_DATE_GROUP) {
3340
- return `${leftPad}${chalk9.gray(PANEL_DATE_SEPARATOR_PREFIX)} ${chalk9.bold.hex("#FF8C00")("\u672A\u77E5\u65E5\u671F")} ${chalk9.gray(PANEL_DATE_SEPARATOR_PREFIX)}`;
3403
+ return `${leftPad}${chalk9.gray(PANEL_DATE_SEPARATOR_PREFIX)} ${chalk9.bold.hex(PANEL_DATE_COLOR)(PANEL_UNKNOWN_DATE)} ${chalk9.gray(PANEL_DATE_SEPARATOR_PREFIX)}`;
3341
3404
  }
3342
3405
  const relativeDate = formatRelativeDate(dateKey);
3343
- return `${leftPad}${chalk9.gray(PANEL_DATE_SEPARATOR_PREFIX)} ${chalk9.bold.hex("#FF8C00")(dateKey)}${chalk9.hex("#FF8C00")(`\uFF08${relativeDate}\uFF09`)} ${chalk9.gray(PANEL_DATE_SEPARATOR_PREFIX)}`;
3406
+ return `${leftPad}${chalk9.gray(PANEL_DATE_SEPARATOR_PREFIX)} ${chalk9.bold.hex(PANEL_DATE_COLOR)(dateKey)}${chalk9.hex(PANEL_DATE_COLOR)(`\uFF08${relativeDate}\uFF09`)} ${chalk9.gray(PANEL_DATE_SEPARATOR_PREFIX)}`;
3344
3407
  }
3345
3408
  function renderWorktreeBlock(wt, isSelected) {
3346
3409
  const lines = [];
@@ -3353,12 +3416,12 @@ function renderWorktreeBlock(wt, isSelected) {
3353
3416
  lines.push(`${indent}${chalk9.green(`+${wt.insertions}`)} ${chalk9.red(`-${wt.deletions}`)}`);
3354
3417
  }
3355
3418
  if (wt.commitsAhead > 0) {
3356
- lines.push(`${indent}${chalk9.yellow(`${wt.commitsAhead} \u4E2A\u672C\u5730\u63D0\u4EA4`)}`);
3419
+ lines.push(`${indent}${chalk9.yellow(PANEL_COMMITS_AHEAD(wt.commitsAhead))}`);
3357
3420
  }
3358
3421
  if (wt.commitsBehind > 0) {
3359
- lines.push(`${indent}${chalk9.yellow(`\u843D\u540E\u4E3B\u5206\u652F ${wt.commitsBehind} \u4E2A\u63D0\u4EA4`)}`);
3422
+ lines.push(`${indent}${chalk9.yellow(PANEL_COMMITS_BEHIND(wt.commitsBehind))}`);
3360
3423
  } else {
3361
- lines.push(`${indent}${chalk9.green("\u4E0E\u4E3B\u5206\u652F\u540C\u6B65")}`);
3424
+ lines.push(`${indent}${chalk9.green(PANEL_SYNCED_WITH_MAIN)}`);
3362
3425
  }
3363
3426
  if (wt.createdAt) {
3364
3427
  const relativeTime = formatRelativeTime(wt.createdAt);
@@ -3412,16 +3475,166 @@ function calculateVisibleRows(terminalRows) {
3412
3475
  return Math.max(terminalRows - fixedRows, 3);
3413
3476
  }
3414
3477
 
3415
- // src/utils/interactive-panel.ts
3416
- var InteractivePanel = class {
3478
+ // src/utils/keyboard-controller.ts
3479
+ var KeyboardController = class {
3480
+ /** stdin 数据处理器引用(用于清理) */
3481
+ stdinDataHandler = null;
3482
+ /** 按键回调函数 */
3483
+ onKeypress;
3484
+ /**
3485
+ * 创建键盘事件控制器
3486
+ * @param {(data: Buffer) => void} onKeypress - 按键回调函数
3487
+ */
3488
+ constructor(onKeypress) {
3489
+ this.onKeypress = onKeypress;
3490
+ }
3491
+ /**
3492
+ * 启动键盘监听
3493
+ * 将 stdin 设为 raw 模式以捕获每个按键
3494
+ */
3495
+ start() {
3496
+ if (process.stdin.isTTY) {
3497
+ process.stdin.setRawMode(true);
3498
+ }
3499
+ process.stdin.resume();
3500
+ this.stdinDataHandler = (data) => {
3501
+ this.onKeypress(data);
3502
+ };
3503
+ process.stdin.on("data", this.stdinDataHandler);
3504
+ }
3505
+ /**
3506
+ * 停止键盘监听,恢复 stdin 状态
3507
+ */
3508
+ stop() {
3509
+ if (this.stdinDataHandler) {
3510
+ process.stdin.removeListener("data", this.stdinDataHandler);
3511
+ this.stdinDataHandler = null;
3512
+ }
3513
+ if (process.stdin.isTTY) {
3514
+ process.stdin.setRawMode(false);
3515
+ }
3516
+ process.stdin.pause();
3517
+ }
3518
+ };
3519
+
3520
+ // src/utils/interactive-panel-state.ts
3521
+ var PanelStateManager = class {
3417
3522
  /** 当前状态数据 */
3418
- statusResult;
3523
+ statusResult = null;
3419
3524
  /** 当前选中的显示位置索引(对应 displayOrder 数组的下标) */
3420
- selectedDisplayIndex;
3525
+ selectedDisplayIndex = 0;
3421
3526
  /** 显示顺序到原始索引的映射(按日期分组后的排列顺序) */
3422
- displayOrder;
3527
+ displayOrder = [];
3423
3528
  /** 滚动偏移(基于行数) */
3424
- scrollOffset;
3529
+ scrollOffset = 0;
3530
+ /**
3531
+ * 更新状态数据
3532
+ * @param {StatusResult} newStatus - 新的状态数据
3533
+ * @param {string} [previousBranch] - 刷新前选中的分支名
3534
+ */
3535
+ updateData(newStatus, previousBranch) {
3536
+ this.statusResult = newStatus;
3537
+ this.displayOrder = buildDisplayOrder(this.statusResult.worktrees);
3538
+ if (previousBranch && this.displayOrder.length > 0) {
3539
+ const newDisplayIndex = this.displayOrder.findIndex(
3540
+ (origIdx) => this.statusResult.worktrees[origIdx]?.branch === previousBranch
3541
+ );
3542
+ if (newDisplayIndex >= 0) {
3543
+ this.selectedDisplayIndex = newDisplayIndex;
3544
+ } else {
3545
+ this.selectedDisplayIndex = Math.min(this.selectedDisplayIndex, Math.max(0, this.displayOrder.length - 1));
3546
+ }
3547
+ } else {
3548
+ this.selectedDisplayIndex = 0;
3549
+ }
3550
+ }
3551
+ /** 获取当前状态数据 */
3552
+ getStatusResult() {
3553
+ return this.statusResult;
3554
+ }
3555
+ /** 获取当前选中的原始索引 */
3556
+ getSelectedOriginalIndex() {
3557
+ return this.displayOrder[this.selectedDisplayIndex] ?? -1;
3558
+ }
3559
+ /** 获取当前滚动偏移 */
3560
+ getScrollOffset() {
3561
+ return this.scrollOffset;
3562
+ }
3563
+ /**
3564
+ * 向上导航
3565
+ * @returns {boolean} 是否发生变化
3566
+ */
3567
+ navigateUp() {
3568
+ if (!this.statusResult || this.displayOrder.length === 0) return false;
3569
+ if (this.selectedDisplayIndex > 0) {
3570
+ this.selectedDisplayIndex--;
3571
+ this.adjustScrollForSelection();
3572
+ return true;
3573
+ }
3574
+ return false;
3575
+ }
3576
+ /**
3577
+ * 向下导航
3578
+ * @returns {boolean} 是否发生变化
3579
+ */
3580
+ navigateDown() {
3581
+ if (!this.statusResult || this.displayOrder.length === 0) return false;
3582
+ if (this.selectedDisplayIndex < this.displayOrder.length - 1) {
3583
+ this.selectedDisplayIndex++;
3584
+ this.adjustScrollForSelection();
3585
+ return true;
3586
+ }
3587
+ return false;
3588
+ }
3589
+ /**
3590
+ * 获取当前选中的分支名
3591
+ * @returns {string | null} 分支名
3592
+ */
3593
+ getSelectedBranch() {
3594
+ const originalIndex = this.getSelectedOriginalIndex();
3595
+ if (originalIndex === -1 || !this.statusResult) return null;
3596
+ return this.statusResult.worktrees[originalIndex]?.branch || null;
3597
+ }
3598
+ /**
3599
+ * 调整滚动位置以确保选中项在可见区域内
3600
+ */
3601
+ adjustScrollForSelection() {
3602
+ if (!this.statusResult || this.displayOrder.length === 0) return;
3603
+ const originalIndex = this.getSelectedOriginalIndex();
3604
+ const rows = process.stdout.rows || 24;
3605
+ const visibleRows = calculateVisibleRows(rows);
3606
+ const panelLines = buildGroupedWorktreeLines(this.statusResult.worktrees, originalIndex);
3607
+ let firstLine = -1;
3608
+ let lastLine = -1;
3609
+ for (let i = 0; i < panelLines.length; i++) {
3610
+ if (panelLines[i].worktreeIndex === originalIndex) {
3611
+ if (firstLine === -1) firstLine = i;
3612
+ lastLine = i;
3613
+ }
3614
+ }
3615
+ if (firstLine === -1) return;
3616
+ let groupStart = firstLine;
3617
+ while (groupStart > 0 && panelLines[groupStart - 1].type === "separator") {
3618
+ groupStart--;
3619
+ }
3620
+ if (groupStart < this.scrollOffset) {
3621
+ this.scrollOffset = groupStart;
3622
+ }
3623
+ if (lastLine >= this.scrollOffset + visibleRows) {
3624
+ this.scrollOffset = lastLine - visibleRows + 1;
3625
+ }
3626
+ if (this.scrollOffset > groupStart) {
3627
+ this.scrollOffset = groupStart;
3628
+ }
3629
+ }
3630
+ };
3631
+
3632
+ // src/utils/interactive-panel.ts
3633
+ var InteractivePanel = class {
3634
+ /** 状态管理器 */
3635
+ stateManager;
3636
+ /** 键盘控制器 */
3637
+ keyboardController;
3425
3638
  /** 数据刷新定时器引用 */
3426
3639
  refreshTimer;
3427
3640
  /** 倒计时定时器引用 */
@@ -3436,8 +3649,6 @@ var InteractivePanel = class {
3436
3649
  resizeHandler;
3437
3650
  /** exit 兜底处理器 */
3438
3651
  exitHandler;
3439
- /** stdin 数据处理器引用(用于清理) */
3440
- stdinDataHandler;
3441
3652
  /** 操作锁(防止操作期间响应按键) */
3442
3653
  isOperating;
3443
3654
  /** Promise resolve 函数(stop 时调用以完成 start 返回的 Promise) */
@@ -3449,10 +3660,8 @@ var InteractivePanel = class {
3449
3660
  * @param {() => StatusResult} collectStatusFn - 数据收集函数
3450
3661
  */
3451
3662
  constructor(collectStatusFn) {
3452
- this.statusResult = null;
3453
- this.selectedDisplayIndex = 0;
3454
- this.displayOrder = [];
3455
- this.scrollOffset = 0;
3663
+ this.stateManager = new PanelStateManager();
3664
+ this.keyboardController = new KeyboardController(this.handleKeypress.bind(this));
3456
3665
  this.refreshTimer = null;
3457
3666
  this.countdownTimer = null;
3458
3667
  this.refreshCountdown = PANEL_REFRESH_INTERVAL_MS / 1e3;
@@ -3460,7 +3669,6 @@ var InteractivePanel = class {
3460
3669
  this.isTTY = !!process.stdout.isTTY;
3461
3670
  this.resizeHandler = null;
3462
3671
  this.exitHandler = null;
3463
- this.stdinDataHandler = null;
3464
3672
  this.isOperating = false;
3465
3673
  this.resolveStart = null;
3466
3674
  this.collectStatusFn = collectStatusFn;
@@ -3475,12 +3683,11 @@ var InteractivePanel = class {
3475
3683
  console.log(PANEL_NOT_TTY);
3476
3684
  return Promise.resolve();
3477
3685
  }
3478
- return new Promise((resolve3) => {
3479
- this.resolveStart = resolve3;
3480
- this.statusResult = this.collectStatusFn();
3481
- this.displayOrder = buildDisplayOrder(this.statusResult.worktrees);
3686
+ return new Promise((resolve4) => {
3687
+ this.resolveStart = resolve4;
3688
+ this.stateManager.updateData(this.collectStatusFn());
3482
3689
  this.initTerminal();
3483
- this.startKeyboardListener();
3690
+ this.keyboardController.start();
3484
3691
  this.startAutoRefresh();
3485
3692
  this.render();
3486
3693
  });
@@ -3492,7 +3699,7 @@ var InteractivePanel = class {
3492
3699
  if (this.stopped) return;
3493
3700
  this.stopped = true;
3494
3701
  this.clearTimers();
3495
- this.stopKeyboardListener();
3702
+ this.keyboardController.stop();
3496
3703
  this.restoreTerminal();
3497
3704
  if (this.resizeHandler) {
3498
3705
  process.stdout.removeListener("resize", this.resizeHandler);
@@ -3516,6 +3723,7 @@ var InteractivePanel = class {
3516
3723
  process.stdout.write(LINE_WRAP_DISABLE);
3517
3724
  this.resizeHandler = () => {
3518
3725
  if (!this.stopped && !this.isOperating) {
3726
+ this.stateManager.adjustScrollForSelection();
3519
3727
  this.render();
3520
3728
  }
3521
3729
  };
@@ -3535,33 +3743,6 @@ var InteractivePanel = class {
3535
3743
  process.stdout.write(CURSOR_SHOW);
3536
3744
  process.stdout.write(ALT_SCREEN_LEAVE);
3537
3745
  }
3538
- /**
3539
- * 启动键盘监听
3540
- * 将 stdin 设为 raw 模式以捕获每个按键
3541
- */
3542
- startKeyboardListener() {
3543
- if (process.stdin.isTTY) {
3544
- process.stdin.setRawMode(true);
3545
- }
3546
- process.stdin.resume();
3547
- this.stdinDataHandler = (data) => {
3548
- this.handleKeypress(data);
3549
- };
3550
- process.stdin.on("data", this.stdinDataHandler);
3551
- }
3552
- /**
3553
- * 停止键盘监听,恢复 stdin 状态
3554
- */
3555
- stopKeyboardListener() {
3556
- if (this.stdinDataHandler) {
3557
- process.stdin.removeListener("data", this.stdinDataHandler);
3558
- this.stdinDataHandler = null;
3559
- }
3560
- if (process.stdin.isTTY) {
3561
- process.stdin.setRawMode(false);
3562
- }
3563
- process.stdin.pause();
3564
- }
3565
3746
  /**
3566
3747
  * 处理键盘输入
3567
3748
  * @param {Buffer} data - 按键数据
@@ -3574,11 +3755,15 @@ var InteractivePanel = class {
3574
3755
  return;
3575
3756
  }
3576
3757
  if (str === KEY_ARROW_UP) {
3577
- this.navigateUp();
3758
+ if (this.stateManager.navigateUp()) {
3759
+ this.render();
3760
+ }
3578
3761
  return;
3579
3762
  }
3580
3763
  if (str === KEY_ARROW_DOWN) {
3581
- this.navigateDown();
3764
+ if (this.stateManager.navigateDown()) {
3765
+ this.render();
3766
+ }
3582
3767
  return;
3583
3768
  }
3584
3769
  const key = str.toLowerCase();
@@ -3615,67 +3800,6 @@ var InteractivePanel = class {
3615
3800
  return;
3616
3801
  }
3617
3802
  }
3618
- /**
3619
- * 向上导航,选中显示顺序中的上一个 worktree
3620
- */
3621
- navigateUp() {
3622
- if (!this.statusResult || this.displayOrder.length === 0) return;
3623
- if (this.selectedDisplayIndex > 0) {
3624
- this.selectedDisplayIndex--;
3625
- this.adjustScrollForSelection();
3626
- this.render();
3627
- }
3628
- }
3629
- /**
3630
- * 向下导航,选中显示顺序中的下一个 worktree
3631
- */
3632
- navigateDown() {
3633
- if (!this.statusResult || this.displayOrder.length === 0) return;
3634
- if (this.selectedDisplayIndex < this.displayOrder.length - 1) {
3635
- this.selectedDisplayIndex++;
3636
- this.adjustScrollForSelection();
3637
- this.render();
3638
- }
3639
- }
3640
- /**
3641
- * 获取当前选中的原始 worktree 索引
3642
- * @returns {number} 原始 worktrees 数组中的索引
3643
- */
3644
- getSelectedOriginalIndex() {
3645
- return this.displayOrder[this.selectedDisplayIndex];
3646
- }
3647
- /**
3648
- * 调整滚动偏移以确保选中项在可见区域内
3649
- */
3650
- adjustScrollForSelection() {
3651
- if (!this.statusResult || this.displayOrder.length === 0) return;
3652
- const originalIndex = this.getSelectedOriginalIndex();
3653
- const rows = process.stdout.rows || 24;
3654
- const visibleRows = calculateVisibleRows(rows);
3655
- const panelLines = buildGroupedWorktreeLines(this.statusResult.worktrees, originalIndex);
3656
- let firstLine = -1;
3657
- let lastLine = -1;
3658
- for (let i = 0; i < panelLines.length; i++) {
3659
- if (panelLines[i].worktreeIndex === originalIndex) {
3660
- if (firstLine === -1) firstLine = i;
3661
- lastLine = i;
3662
- }
3663
- }
3664
- if (firstLine === -1) return;
3665
- let groupStart = firstLine;
3666
- while (groupStart > 0 && panelLines[groupStart - 1].type === "separator") {
3667
- groupStart--;
3668
- }
3669
- if (groupStart < this.scrollOffset) {
3670
- this.scrollOffset = groupStart;
3671
- }
3672
- if (lastLine >= this.scrollOffset + visibleRows) {
3673
- this.scrollOffset = lastLine - visibleRows + 1;
3674
- }
3675
- if (this.scrollOffset > groupStart) {
3676
- this.scrollOffset = groupStart;
3677
- }
3678
- }
3679
3803
  /**
3680
3804
  * 启动自动刷新:数据刷新定时器 + 倒计时定时器
3681
3805
  */
@@ -3711,45 +3835,25 @@ var InteractivePanel = class {
3711
3835
  */
3712
3836
  refreshData() {
3713
3837
  if (this.stopped || this.isOperating) return;
3714
- const originalIndex = this.displayOrder[this.selectedDisplayIndex];
3715
- const previousBranch = this.statusResult?.worktrees[originalIndex]?.branch;
3716
- this.statusResult = this.collectStatusFn();
3717
- this.displayOrder = buildDisplayOrder(this.statusResult.worktrees);
3718
- this.restoreSelection(previousBranch);
3838
+ const previousBranch = this.stateManager.getSelectedBranch();
3839
+ this.stateManager.updateData(this.collectStatusFn(), previousBranch || void 0);
3840
+ this.stateManager.adjustScrollForSelection();
3719
3841
  this.refreshCountdown = PANEL_REFRESH_INTERVAL_MS / 1e3;
3720
3842
  this.render();
3721
3843
  }
3722
- /**
3723
- * 按分支名恢复选中位置(基于显示顺序)
3724
- * @param {string | undefined} previousBranch - 之前选中的分支名
3725
- */
3726
- restoreSelection(previousBranch) {
3727
- if (!this.statusResult || !previousBranch || this.displayOrder.length === 0) {
3728
- this.selectedDisplayIndex = 0;
3729
- return;
3730
- }
3731
- const newDisplayIndex = this.displayOrder.findIndex(
3732
- (origIdx) => this.statusResult.worktrees[origIdx]?.branch === previousBranch
3733
- );
3734
- if (newDisplayIndex >= 0) {
3735
- this.selectedDisplayIndex = newDisplayIndex;
3736
- } else {
3737
- this.selectedDisplayIndex = Math.min(this.selectedDisplayIndex, Math.max(0, this.displayOrder.length - 1));
3738
- }
3739
- this.adjustScrollForSelection();
3740
- }
3741
3844
  /**
3742
3845
  * 渲染一帧面板内容
3743
3846
  * 使用同步输出防止闪烁
3744
3847
  */
3745
3848
  render() {
3746
- if (this.stopped || this.isOperating || !this.statusResult) return;
3849
+ const statusResult = this.stateManager.getStatusResult();
3850
+ if (this.stopped || this.isOperating || !statusResult) return;
3747
3851
  const cols = process.stdout.columns || DEFAULT_TERMINAL_COLUMNS;
3748
3852
  const rows = process.stdout.rows || 24;
3749
3853
  const frameLines = buildPanelFrame(
3750
- this.statusResult,
3751
- this.getSelectedOriginalIndex(),
3752
- this.scrollOffset,
3854
+ statusResult,
3855
+ this.stateManager.getSelectedOriginalIndex(),
3856
+ this.stateManager.getScrollOffset(),
3753
3857
  rows,
3754
3858
  cols,
3755
3859
  this.refreshCountdown
@@ -3768,63 +3872,56 @@ var InteractivePanel = class {
3768
3872
  * @param {() => void} action - 要执行的操作
3769
3873
  */
3770
3874
  async executeOperation(action) {
3771
- if (!this.statusResult || this.displayOrder.length === 0) return;
3875
+ const statusResult = this.stateManager.getStatusResult();
3876
+ if (!statusResult || this.stateManager.getSelectedOriginalIndex() === -1) return;
3772
3877
  this.isOperating = true;
3773
3878
  this.clearTimers();
3774
3879
  this.restoreTerminal();
3775
- this.stopKeyboardListener();
3880
+ this.keyboardController.stop();
3776
3881
  action();
3777
3882
  console.log(PANEL_PRESS_ENTER_TO_RETURN);
3778
3883
  await this.waitForEnter();
3779
3884
  this.initTerminal();
3780
- this.startKeyboardListener();
3885
+ this.keyboardController.start();
3781
3886
  this.isOperating = false;
3782
3887
  this.refreshData();
3783
3888
  this.startAutoRefresh();
3784
3889
  this.render();
3785
3890
  }
3786
- /**
3787
- * 获取当前选中的分支名
3788
- * @returns {string} 当前选中的分支名
3789
- */
3790
- getSelectedBranch() {
3791
- const originalIndex = this.getSelectedOriginalIndex();
3792
- return this.statusResult.worktrees[originalIndex].branch;
3793
- }
3794
3891
  /**
3795
3892
  * 执行验证操作
3796
3893
  */
3797
3894
  handleValidate() {
3798
- const branch = this.getSelectedBranch();
3799
- runCommandInherited(`clawt validate -b ${branch}`);
3895
+ const branch = this.stateManager.getSelectedBranch();
3896
+ if (branch) runCommandInherited(`clawt validate -b ${branch}`);
3800
3897
  }
3801
3898
  /**
3802
3899
  * 执行合并操作
3803
3900
  */
3804
3901
  handleMerge() {
3805
- const branch = this.getSelectedBranch();
3806
- runCommandInherited(`clawt merge -b ${branch}`);
3902
+ const branch = this.stateManager.getSelectedBranch();
3903
+ if (branch) runCommandInherited(`clawt merge -b ${branch}`);
3807
3904
  }
3808
3905
  /**
3809
3906
  * 执行删除操作
3810
3907
  */
3811
3908
  handleDelete() {
3812
- const branch = this.getSelectedBranch();
3813
- runCommandInherited(`clawt remove -b ${branch}`);
3909
+ const branch = this.stateManager.getSelectedBranch();
3910
+ if (branch) runCommandInherited(`clawt remove -b ${branch}`);
3814
3911
  }
3815
3912
  /**
3816
3913
  * 执行恢复操作
3817
3914
  */
3818
3915
  handleResume() {
3819
- const branch = this.getSelectedBranch();
3820
- runCommandInherited(`clawt resume -b ${branch}`);
3916
+ const branch = this.stateManager.getSelectedBranch();
3917
+ if (branch) runCommandInherited(`clawt resume -b ${branch}`);
3821
3918
  }
3822
3919
  /**
3823
3920
  * 执行同步操作
3824
3921
  */
3825
3922
  handleSync() {
3826
- const branch = this.getSelectedBranch();
3827
- runCommandInherited(`clawt sync -b ${branch}`);
3923
+ const branch = this.stateManager.getSelectedBranch();
3924
+ if (branch) runCommandInherited(`clawt sync -b ${branch}`);
3828
3925
  }
3829
3926
  /**
3830
3927
  * 执行覆盖操作
@@ -3838,14 +3935,14 @@ var InteractivePanel = class {
3838
3935
  * @returns {Promise<void>} 用户按回车时 resolve
3839
3936
  */
3840
3937
  waitForEnter() {
3841
- return new Promise((resolve3) => {
3938
+ return new Promise((resolve4) => {
3842
3939
  const rl = createInterface2({
3843
3940
  input: process.stdin,
3844
3941
  output: process.stdout
3845
3942
  });
3846
3943
  rl.once("line", () => {
3847
3944
  rl.close();
3848
- resolve3();
3945
+ resolve4();
3849
3946
  });
3850
3947
  });
3851
3948
  }
@@ -5386,6 +5483,28 @@ async function handleHome() {
5386
5483
  printSuccess(MESSAGES.HOME_SWITCH_SUCCESS(currentBranch, mainBranch));
5387
5484
  }
5388
5485
 
5486
+ // src/commands/tasks.ts
5487
+ import { resolve as resolve3, dirname as dirname2, join as join10 } from "path";
5488
+ import { existsSync as existsSync13, writeFileSync as writeFileSync6 } from "fs";
5489
+ function registerTasksCommand(program2) {
5490
+ const taskCmd = program2.command("tasks").description("\u4EFB\u52A1\u6587\u4EF6\u7BA1\u7406");
5491
+ taskCmd.command("init").description("\u751F\u6210\u4EFB\u52A1\u6A21\u677F\u6587\u4EF6").argument("[path]", "\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84").action(async (path2) => {
5492
+ const filePath = path2 ?? join10(TASK_TEMPLATE_OUTPUT_DIR, generateTaskFilename(TASK_TEMPLATE_FILENAME_PREFIX));
5493
+ await handleTasksInit(filePath);
5494
+ });
5495
+ }
5496
+ async function handleTasksInit(filePath) {
5497
+ const absolutePath = resolve3(filePath);
5498
+ logger.info(`tasks init \u547D\u4EE4\u6267\u884C\uFF0C\u76EE\u6807\u6587\u4EF6: ${absolutePath}`);
5499
+ if (existsSync13(absolutePath)) {
5500
+ throw new ClawtError(MESSAGES.TASK_INIT_FILE_EXISTS(filePath));
5501
+ }
5502
+ ensureDir(dirname2(absolutePath));
5503
+ writeFileSync6(absolutePath, TASK_TEMPLATE_CONTENT, "utf-8");
5504
+ printSuccess(MESSAGES.TASK_INIT_SUCCESS(filePath));
5505
+ printHint(MESSAGES.TASK_INIT_HINT(filePath));
5506
+ }
5507
+
5389
5508
  // src/index.ts
5390
5509
  var require2 = createRequire(import.meta.url);
5391
5510
  var { version } = require2("../package.json");
@@ -5414,6 +5533,7 @@ registerProjectsCommand(program);
5414
5533
  registerCompletionCommand(program);
5415
5534
  registerInitCommand(program);
5416
5535
  registerHomeCommand(program);
5536
+ registerTasksCommand(program);
5417
5537
  var config = loadConfig();
5418
5538
  applyAliases(program, config.aliases);
5419
5539
  process.on("uncaughtException", (error) => {