clawt 3.4.5 → 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 (45) hide show
  1. package/.claude/settings.local.json +12 -0
  2. package/README.md +0 -4
  3. package/dist/index.js +430 -306
  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/init.ts +6 -2
  22. package/src/commands/tasks.ts +51 -0
  23. package/src/constants/index.ts +3 -0
  24. package/src/constants/interactive-panel.ts +6 -0
  25. package/src/constants/messages/index.ts +4 -2
  26. package/src/constants/messages/interactive-panel.ts +12 -0
  27. package/src/constants/messages/tasks.ts +9 -0
  28. package/src/constants/tasks-template.ts +28 -0
  29. package/src/index.ts +2 -0
  30. package/src/types/command.ts +6 -0
  31. package/src/types/index.ts +1 -1
  32. package/src/utils/formatter.ts +19 -0
  33. package/src/utils/git-branch.ts +116 -0
  34. package/src/utils/git-core.ts +369 -0
  35. package/src/utils/git-worktree.ts +40 -0
  36. package/src/utils/git.ts +3 -521
  37. package/src/utils/index.ts +1 -1
  38. package/src/utils/interactive-panel-render.ts +12 -6
  39. package/src/utils/interactive-panel-state.ts +137 -0
  40. package/src/utils/interactive-panel.ts +44 -188
  41. package/src/utils/keyboard-controller.ts +48 -0
  42. package/src/utils/ui-prompts.ts +240 -0
  43. package/src/utils/worktree-matcher.ts +21 -251
  44. package/tests/unit/commands/tasks.test.ts +153 -0
  45. package/tests/unit/utils/formatter.test.ts +26 -1
@@ -0,0 +1,240 @@
1
+ import Enquirer from 'enquirer';
2
+ import {
3
+ SELECT_ALL_NAME,
4
+ SELECT_ALL_LABEL,
5
+ } from '../constants/index.js';
6
+ import type { WorktreeInfo } from '../types/index.js';
7
+ import { groupWorktreesByDate, buildGroupedChoices, buildGroupMembershipMap } from './worktree-matcher.js';
8
+
9
+ /** enquirer MultiSelect 选项条目的运行时结构 */
10
+ export interface MultiSelectChoice {
11
+ name: string;
12
+ message: string;
13
+ enabled: boolean;
14
+ }
15
+
16
+ /**
17
+ * enquirer MultiSelect 实例的运行时接口
18
+ * enquirer 类型声明未导出 MultiSelect,手动声明以消除 TypeScript 类型错误
19
+ */
20
+ export interface MultiSelectInstance {
21
+ focused: MultiSelectChoice | undefined;
22
+ choices: MultiSelectChoice[];
23
+ render(): void;
24
+ toggle(choice: MultiSelectChoice): void;
25
+ run(): Promise<string[]>;
26
+ }
27
+
28
+ /** enquirer MultiSelect 分隔线条目结构 */
29
+ export interface MultiSelectSeparator {
30
+ role: 'separator';
31
+ message: string;
32
+ }
33
+
34
+ /** enquirer MultiSelect choices 数组的条目类型 */
35
+ export type GroupedChoice = { name: string; message: string } | MultiSelectSeparator;
36
+
37
+ /**
38
+ * 通过交互式列表让用户从 worktree 列表中选择一个分支
39
+ * @param {WorktreeInfo[]} worktrees - 可供选择的 worktree 列表
40
+ * @param {string} message - 选择提示信息
41
+ * @returns {Promise<WorktreeInfo>} 用户选择的 worktree
42
+ */
43
+ export async function promptSelectBranch(worktrees: WorktreeInfo[], message: string): Promise<WorktreeInfo> {
44
+ // @ts-expect-error enquirer 类型声明未导出 Select 类,但运行时存在
45
+ const selectedBranch: string = await new Enquirer.Select({
46
+ message,
47
+ choices: worktrees.map((wt) => ({
48
+ name: wt.branch,
49
+ message: wt.branch,
50
+ })),
51
+ }).run();
52
+
53
+ return worktrees.find((wt) => wt.branch === selectedBranch)!;
54
+ }
55
+
56
+ /**
57
+ * 通过交互式多选列表让用户从 worktree 列表中选择多个分支
58
+ * 顶部提供「全选」选项,点击可切换全选/全不选
59
+ * 用户可通过空格键选择/取消,回车键确认
60
+ * @param {WorktreeInfo[]} worktrees - 可供选择的 worktree 列表
61
+ * @param {string} message - 选择提示信息
62
+ * @returns {Promise<WorktreeInfo[]>} 用户选择的 worktree 列表
63
+ */
64
+ export async function promptMultiSelectBranches(worktrees: WorktreeInfo[], message: string): Promise<WorktreeInfo[]> {
65
+ // 构建 choices 列表,顶部插入全选选项
66
+ const branchChoices = worktrees.map((wt) => ({
67
+ name: wt.branch,
68
+ message: wt.branch,
69
+ }));
70
+
71
+ const choices = [
72
+ { name: SELECT_ALL_NAME, message: SELECT_ALL_LABEL },
73
+ ...branchChoices,
74
+ ];
75
+
76
+ // @ts-expect-error enquirer 类型声明未导出 MultiSelect 类,但运行时存在
77
+ const MultiSelect: new (options: Record<string, unknown>) => MultiSelectInstance = Enquirer.MultiSelect;
78
+
79
+ /**
80
+ * 扩展 MultiSelect,覆写 space() 方法实现全选 toggle
81
+ * 当焦点在「全选」选项上按空格时,切换所有分支选项的选中状态
82
+ */
83
+ class MultiSelectWithSelectAll extends MultiSelect {
84
+ space(this: MultiSelectInstance) {
85
+ if (!this.focused) return;
86
+
87
+ if (this.focused.name === SELECT_ALL_NAME) {
88
+ // 切换全选:如果全选项当前未选中则全选,否则全不选
89
+ const willEnable = !this.focused.enabled;
90
+ for (const ch of this.choices) {
91
+ ch.enabled = willEnable;
92
+ }
93
+ return this.render();
94
+ }
95
+
96
+ // 非全选选项:执行默认的 toggle 行为
97
+ this.toggle(this.focused);
98
+
99
+ // 同步全选选项状态:所有分支选项都选中时自动勾选全选,否则取消
100
+ const selectAllChoice = this.choices.find((ch) => ch.name === SELECT_ALL_NAME);
101
+ const branchItems = this.choices.filter((ch) => ch.name !== SELECT_ALL_NAME);
102
+ if (selectAllChoice) {
103
+ selectAllChoice.enabled = branchItems.every((ch) => ch.enabled);
104
+ }
105
+
106
+ return this.render();
107
+ }
108
+ }
109
+
110
+ const selectedBranches: string[] = await new MultiSelectWithSelectAll({
111
+ message,
112
+ choices,
113
+ // 使用空心圆/实心圆作为选中指示符
114
+ symbols: {
115
+ indicator: { on: '●', off: '○' },
116
+ },
117
+ }).run();
118
+
119
+ // 过滤掉全选选项,只返回实际的 worktree
120
+ return worktrees.filter((wt) => selectedBranches.includes(wt.branch));
121
+ }
122
+
123
+ /**
124
+ * 通过交互式多选列表(按日期分组)让用户选择多个分支
125
+ * 提供三级联动:全局全选、组级全选、单个分支
126
+ * @param {WorktreeInfo[]} worktrees - 可供选择的 worktree 列表
127
+ * @param {string} message - 选择提示信息
128
+ * @returns {Promise<WorktreeInfo[]>} 用户选择的 worktree 列表
129
+ */
130
+ export async function promptGroupedMultiSelectBranches(
131
+ worktrees: WorktreeInfo[],
132
+ message: string,
133
+ ): Promise<WorktreeInfo[]> {
134
+ const groups = groupWorktreesByDate(worktrees);
135
+ const choices = buildGroupedChoices(groups);
136
+ const groupMembershipMap = buildGroupMembershipMap(groups);
137
+
138
+ // 收集所有组全选的 name,用于判断某个 choice 是否为组全选项
139
+ const groupSelectAllNames = new Set(groupMembershipMap.keys());
140
+
141
+ // 收集所有实际分支的 name
142
+ const allBranchNames = new Set(worktrees.map((wt) => wt.branch));
143
+
144
+ // @ts-expect-error enquirer 类型声明未导出 MultiSelect 类,但运行时存在
145
+ const MultiSelect: new (options: Record<string, unknown>) => MultiSelectInstance = Enquirer.MultiSelect;
146
+
147
+ /**
148
+ * 扩展 MultiSelect,实现三级联动的 space() 覆写
149
+ * - 全局全选:切换所有 choices(含组全选)
150
+ * - 组级全选:切换该组内所有分支,同步全局全选状态
151
+ * - 普通分支:toggle 该分支,同步所属组全选和全局全选状态
152
+ */
153
+ class MultiSelectWithGroupSelectAll extends MultiSelect {
154
+ space(this: MultiSelectInstance) {
155
+ if (!this.focused) return;
156
+
157
+ const focusedName = this.focused.name;
158
+
159
+ if (focusedName === SELECT_ALL_NAME) {
160
+ // 全局全选:切换所有 choices
161
+ const willEnable = !this.focused.enabled;
162
+ for (const ch of this.choices) {
163
+ ch.enabled = willEnable;
164
+ }
165
+ return this.render();
166
+ }
167
+
168
+ if (groupSelectAllNames.has(focusedName)) {
169
+ // 组级全选:切换该组内所有分支
170
+ const willEnable = !this.focused.enabled;
171
+ const memberNames = groupMembershipMap.get(focusedName)!;
172
+ // 切换组全选自身
173
+ this.focused.enabled = willEnable;
174
+ // 切换该组的所有分支
175
+ for (const ch of this.choices) {
176
+ if (memberNames.includes(ch.name)) {
177
+ ch.enabled = willEnable;
178
+ }
179
+ }
180
+ // 同步全局全选状态:检查所有实际分支是否全选
181
+ syncGlobalSelectAll(this.choices);
182
+ return this.render();
183
+ }
184
+
185
+ // 普通分支:toggle 该分支
186
+ this.toggle(this.focused);
187
+
188
+ // 同步所属组全选状态
189
+ syncGroupSelectAll(this.choices, focusedName);
190
+ // 同步全局全选状态
191
+ syncGlobalSelectAll(this.choices);
192
+
193
+ return this.render();
194
+ }
195
+ }
196
+
197
+ /**
198
+ * 同步全局全选状态
199
+ * 根据所有实际分支的选中状态更新全局全选项
200
+ * @param {MultiSelectChoice[]} choiceList - choices 列表
201
+ */
202
+ function syncGlobalSelectAll(choiceList: MultiSelectChoice[]): void {
203
+ const selectAllChoice = choiceList.find((ch) => ch.name === SELECT_ALL_NAME);
204
+ if (!selectAllChoice) return;
205
+
206
+ const branchItems = choiceList.filter((ch) => allBranchNames.has(ch.name));
207
+ selectAllChoice.enabled = branchItems.length > 0 && branchItems.every((ch) => ch.enabled);
208
+ }
209
+
210
+ /**
211
+ * 同步指定分支所属组的全选状态
212
+ * 根据该组内所有分支的选中状态更新组全选项
213
+ * @param {MultiSelectChoice[]} choiceList - choices 列表
214
+ * @param {string} branchName - 刚被 toggle 的分支名
215
+ */
216
+ function syncGroupSelectAll(choiceList: MultiSelectChoice[], branchName: string): void {
217
+ for (const [groupName, memberNames] of groupMembershipMap) {
218
+ if (!memberNames.includes(branchName)) continue;
219
+
220
+ const groupChoice = choiceList.find((ch) => ch.name === groupName);
221
+ if (!groupChoice) continue;
222
+
223
+ const memberChoices = choiceList.filter((ch) => memberNames.includes(ch.name));
224
+ groupChoice.enabled = memberChoices.length > 0 && memberChoices.every((ch) => ch.enabled);
225
+ break;
226
+ }
227
+ }
228
+
229
+ const selectedBranches: string[] = await new MultiSelectWithGroupSelectAll({
230
+ message,
231
+ choices,
232
+ // 使用空心圆/实心圆作为选中指示符
233
+ symbols: {
234
+ indicator: { on: '●', off: '○' },
235
+ },
236
+ }).run();
237
+
238
+ // 过滤掉全选项和组全选项,只返回实际选中的 worktree
239
+ return worktrees.filter((wt) => selectedBranches.includes(wt.branch));
240
+ }
@@ -1,35 +1,17 @@
1
- import Enquirer from 'enquirer';
2
1
  import { statSync } from 'node:fs';
3
2
  import { ClawtError } from '../errors/index.js';
4
3
  import {
5
- SELECT_ALL_NAME,
6
- SELECT_ALL_LABEL,
7
- GROUP_SELECT_ALL_PREFIX,
8
- GROUP_SELECT_ALL_LABEL,
9
- GROUP_SEPARATOR_LABEL,
10
4
  UNKNOWN_DATE_GROUP,
11
5
  UNKNOWN_DATE_SEPARATOR_LABEL,
6
+ GROUP_SEPARATOR_LABEL,
7
+ GROUP_SELECT_ALL_PREFIX,
8
+ GROUP_SELECT_ALL_LABEL,
9
+ SELECT_ALL_NAME,
10
+ SELECT_ALL_LABEL
12
11
  } from '../constants/index.js';
13
12
  import type { WorktreeInfo } from '../types/index.js';
14
-
15
- /** enquirer MultiSelect 选项条目的运行时结构 */
16
- interface MultiSelectChoice {
17
- name: string;
18
- message: string;
19
- enabled: boolean;
20
- }
21
-
22
- /**
23
- * enquirer MultiSelect 实例的运行时接口
24
- * enquirer 类型声明未导出 MultiSelect,手动声明以消除 TypeScript 类型错误
25
- */
26
- interface MultiSelectInstance {
27
- focused: MultiSelectChoice | undefined;
28
- choices: MultiSelectChoice[];
29
- render(): void;
30
- toggle(choice: MultiSelectChoice): void;
31
- run(): Promise<string[]>;
32
- }
13
+ import { promptSelectBranch, promptMultiSelectBranches } from './ui-prompts.js';
14
+ import type { GroupedChoice } from './ui-prompts.js';
33
15
 
34
16
  /**
35
17
  * 分支解析时使用的消息文案配置
@@ -46,27 +28,6 @@ export interface WorktreeResolveMessages {
46
28
  noMatch: (keyword: string, branches: string[]) => string;
47
29
  }
48
30
 
49
- /**
50
- * 在 worktree 列表中精确匹配分支名
51
- * @param {WorktreeInfo[]} worktrees - worktree 列表
52
- * @param {string} branchName - 目标分支名
53
- * @returns {WorktreeInfo | undefined} 匹配的 worktree,未找到返回 undefined
54
- */
55
- export function findExactMatch(worktrees: WorktreeInfo[], branchName: string): WorktreeInfo | undefined {
56
- return worktrees.find((wt) => wt.branch === branchName);
57
- }
58
-
59
- /**
60
- * 在 worktree 列表中进行模糊匹配(子串匹配,大小写不敏感)
61
- * @param {WorktreeInfo[]} worktrees - worktree 列表
62
- * @param {string} keyword - 匹配关键词
63
- * @returns {WorktreeInfo[]} 匹配到的 worktree 列表
64
- */
65
- export function findFuzzyMatches(worktrees: WorktreeInfo[], keyword: string): WorktreeInfo[] {
66
- const lowerKeyword = keyword.toLowerCase();
67
- return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerKeyword));
68
- }
69
-
70
31
  /**
71
32
  * 多选场景下的分支解析消息文案配置
72
33
  * 与 WorktreeResolveMessages 类似,但用于需要多选的命令(如 remove)
@@ -83,89 +44,24 @@ export interface WorktreeMultiResolveMessages {
83
44
  }
84
45
 
85
46
  /**
86
- * 通过交互式列表让用户从 worktree 列表中选择一个分支
87
- * @param {WorktreeInfo[]} worktrees - 可供选择的 worktree 列表
88
- * @param {string} message - 选择提示信息
89
- * @returns {Promise<WorktreeInfo>} 用户选择的 worktree
47
+ * worktree 列表中精确匹配分支名
48
+ * @param {WorktreeInfo[]} worktrees - worktree 列表
49
+ * @param {string} branchName - 目标分支名
50
+ * @returns {WorktreeInfo | undefined} 匹配的 worktree,未找到返回 undefined
90
51
  */
91
- export async function promptSelectBranch(worktrees: WorktreeInfo[], message: string): Promise<WorktreeInfo> {
92
- // @ts-expect-error enquirer 类型声明未导出 Select 类,但运行时存在
93
- const selectedBranch: string = await new Enquirer.Select({
94
- message,
95
- choices: worktrees.map((wt) => ({
96
- name: wt.branch,
97
- message: wt.branch,
98
- })),
99
- }).run();
100
-
101
- return worktrees.find((wt) => wt.branch === selectedBranch)!;
52
+ export function findExactMatch(worktrees: WorktreeInfo[], branchName: string): WorktreeInfo | undefined {
53
+ return worktrees.find((wt) => wt.branch === branchName);
102
54
  }
103
55
 
104
56
  /**
105
- * 通过交互式多选列表让用户从 worktree 列表中选择多个分支
106
- * 顶部提供「全选」选项,点击可切换全选/全不选
107
- * 用户可通过空格键选择/取消,回车键确认
108
- * @param {WorktreeInfo[]} worktrees - 可供选择的 worktree 列表
109
- * @param {string} message - 选择提示信息
110
- * @returns {Promise<WorktreeInfo[]>} 用户选择的 worktree 列表
57
+ * worktree 列表中进行模糊匹配(子串匹配,大小写不敏感)
58
+ * @param {WorktreeInfo[]} worktrees - worktree 列表
59
+ * @param {string} keyword - 匹配关键词
60
+ * @returns {WorktreeInfo[]} 匹配到的 worktree 列表
111
61
  */
112
- export async function promptMultiSelectBranches(worktrees: WorktreeInfo[], message: string): Promise<WorktreeInfo[]> {
113
- // 构建 choices 列表,顶部插入全选选项
114
- const branchChoices = worktrees.map((wt) => ({
115
- name: wt.branch,
116
- message: wt.branch,
117
- }));
118
-
119
- const choices = [
120
- { name: SELECT_ALL_NAME, message: SELECT_ALL_LABEL },
121
- ...branchChoices,
122
- ];
123
-
124
- // @ts-expect-error enquirer 类型声明未导出 MultiSelect 类,但运行时存在
125
- const MultiSelect: new (options: Record<string, unknown>) => MultiSelectInstance = Enquirer.MultiSelect;
126
-
127
- /**
128
- * 扩展 MultiSelect,覆写 space() 方法实现全选 toggle
129
- * 当焦点在「全选」选项上按空格时,切换所有分支选项的选中状态
130
- */
131
- class MultiSelectWithSelectAll extends MultiSelect {
132
- space(this: MultiSelectInstance) {
133
- if (!this.focused) return;
134
-
135
- if (this.focused.name === SELECT_ALL_NAME) {
136
- // 切换全选:如果全选项当前未选中则全选,否则全不选
137
- const willEnable = !this.focused.enabled;
138
- for (const ch of this.choices) {
139
- ch.enabled = willEnable;
140
- }
141
- return this.render();
142
- }
143
-
144
- // 非全选选项:执行默认的 toggle 行为
145
- this.toggle(this.focused);
146
-
147
- // 同步全选选项状态:所有分支选项都选中时自动勾选全选,否则取消
148
- const selectAllChoice = this.choices.find((ch) => ch.name === SELECT_ALL_NAME);
149
- const branchItems = this.choices.filter((ch) => ch.name !== SELECT_ALL_NAME);
150
- if (selectAllChoice) {
151
- selectAllChoice.enabled = branchItems.every((ch) => ch.enabled);
152
- }
153
-
154
- return this.render();
155
- }
156
- }
157
-
158
- const selectedBranches: string[] = await new MultiSelectWithSelectAll({
159
- message,
160
- choices,
161
- // 使用空心圆/实心圆作为选中指示符
162
- symbols: {
163
- indicator: { on: '●', off: '○' },
164
- },
165
- }).run();
166
-
167
- // 过滤掉全选选项,只返回实际的 worktree
168
- return worktrees.filter((wt) => selectedBranches.includes(wt.branch));
62
+ export function findFuzzyMatches(worktrees: WorktreeInfo[], keyword: string): WorktreeInfo[] {
63
+ const lowerKeyword = keyword.toLowerCase();
64
+ return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerKeyword));
169
65
  }
170
66
 
171
67
  /**
@@ -274,15 +170,6 @@ export async function resolveTargetWorktree(
274
170
  throw new ClawtError(messages.noMatch(branchName, allBranches));
275
171
  }
276
172
 
277
- /** enquirer MultiSelect 分隔线条目结构 */
278
- interface MultiSelectSeparator {
279
- role: 'separator';
280
- message: string;
281
- }
282
-
283
- /** enquirer MultiSelect choices 数组的条目类型 */
284
- type GroupedChoice = { name: string; message: string } | MultiSelectSeparator;
285
-
286
173
  /**
287
174
  * 将 Date 对象格式化为本地时区的 YYYY-MM-DD 字符串
288
175
  * @param {Date} date - 日期对象
@@ -428,121 +315,4 @@ export function buildGroupMembershipMap(groups: Map<string, WorktreeInfo[]>): Ma
428
315
  return map;
429
316
  }
430
317
 
431
- /**
432
- * 通过交互式多选列表(按日期分组)让用户选择多个分支
433
- * 提供三级联动:全局全选、组级全选、单个分支
434
- * @param {WorktreeInfo[]} worktrees - 可供选择的 worktree 列表
435
- * @param {string} message - 选择提示信息
436
- * @returns {Promise<WorktreeInfo[]>} 用户选择的 worktree 列表
437
- */
438
- export async function promptGroupedMultiSelectBranches(
439
- worktrees: WorktreeInfo[],
440
- message: string,
441
- ): Promise<WorktreeInfo[]> {
442
- const groups = groupWorktreesByDate(worktrees);
443
- const choices = buildGroupedChoices(groups);
444
- const groupMembershipMap = buildGroupMembershipMap(groups);
445
-
446
- // 收集所有组全选的 name,用于判断某个 choice 是否为组全选项
447
- const groupSelectAllNames = new Set(groupMembershipMap.keys());
448
-
449
- // 收集所有实际分支的 name
450
- const allBranchNames = new Set(worktrees.map((wt) => wt.branch));
451
-
452
- // @ts-expect-error enquirer 类型声明未导出 MultiSelect 类,但运行时存在
453
- const MultiSelect: new (options: Record<string, unknown>) => MultiSelectInstance = Enquirer.MultiSelect;
454
-
455
- /**
456
- * 扩展 MultiSelect,实现三级联动的 space() 覆写
457
- * - 全局全选:切换所有 choices(含组全选)
458
- * - 组级全选:切换该组内所有分支,同步全局全选状态
459
- * - 普通分支:toggle 该分支,同步所属组全选和全局全选状态
460
- */
461
- class MultiSelectWithGroupSelectAll extends MultiSelect {
462
- space(this: MultiSelectInstance) {
463
- if (!this.focused) return;
464
-
465
- const focusedName = this.focused.name;
466
-
467
- if (focusedName === SELECT_ALL_NAME) {
468
- // 全局全选:切换所有 choices
469
- const willEnable = !this.focused.enabled;
470
- for (const ch of this.choices) {
471
- ch.enabled = willEnable;
472
- }
473
- return this.render();
474
- }
475
-
476
- if (groupSelectAllNames.has(focusedName)) {
477
- // 组级全选:切换该组内所有分支
478
- const willEnable = !this.focused.enabled;
479
- const memberNames = groupMembershipMap.get(focusedName)!;
480
- // 切换组全选自身
481
- this.focused.enabled = willEnable;
482
- // 切换该组的所有分支
483
- for (const ch of this.choices) {
484
- if (memberNames.includes(ch.name)) {
485
- ch.enabled = willEnable;
486
- }
487
- }
488
- // 同步全局全选状态:检查所有实际分支是否全选
489
- syncGlobalSelectAll(this.choices);
490
- return this.render();
491
- }
492
-
493
- // 普通分支:toggle 该分支
494
- this.toggle(this.focused);
495
-
496
- // 同步所属组全选状态
497
- syncGroupSelectAll(this.choices, focusedName);
498
- // 同步全局全选状态
499
- syncGlobalSelectAll(this.choices);
500
-
501
- return this.render();
502
- }
503
- }
504
-
505
- /**
506
- * 同步全局全选状态
507
- * 根据所有实际分支的选中状态更新全局全选项
508
- * @param {MultiSelectChoice[]} choiceList - choices 列表
509
- */
510
- function syncGlobalSelectAll(choiceList: MultiSelectChoice[]): void {
511
- const selectAllChoice = choiceList.find((ch) => ch.name === SELECT_ALL_NAME);
512
- if (!selectAllChoice) return;
513
-
514
- const branchItems = choiceList.filter((ch) => allBranchNames.has(ch.name));
515
- selectAllChoice.enabled = branchItems.length > 0 && branchItems.every((ch) => ch.enabled);
516
- }
517
-
518
- /**
519
- * 同步指定分支所属组的全选状态
520
- * 根据该组内所有分支的选中状态更新组全选项
521
- * @param {MultiSelectChoice[]} choiceList - choices 列表
522
- * @param {string} branchName - 刚被 toggle 的分支名
523
- */
524
- function syncGroupSelectAll(choiceList: MultiSelectChoice[], branchName: string): void {
525
- for (const [groupName, memberNames] of groupMembershipMap) {
526
- if (!memberNames.includes(branchName)) continue;
527
-
528
- const groupChoice = choiceList.find((ch) => ch.name === groupName);
529
- if (!groupChoice) continue;
530
-
531
- const memberChoices = choiceList.filter((ch) => memberNames.includes(ch.name));
532
- groupChoice.enabled = memberChoices.length > 0 && memberChoices.every((ch) => ch.enabled);
533
- break;
534
- }
535
- }
536
-
537
- const selectedBranches: string[] = await new MultiSelectWithGroupSelectAll({
538
- message,
539
- choices,
540
- // 使用空心圆/实心圆作为选中指示符
541
- symbols: {
542
- indicator: { on: '●', off: '○' },
543
- },
544
- }).run();
545
-
546
- // 过滤掉全选项和组全选项,只返回实际选中的 worktree
547
- return worktrees.filter((wt) => selectedBranches.includes(wt.branch));
548
- }
318
+ export { promptGroupedMultiSelectBranches } from './ui-prompts.js';