clawt 3.1.0 → 3.1.2

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/README.md CHANGED
@@ -113,7 +113,7 @@ clawt resume -b <branch> # 指定分支
113
113
  clawt resume # 交互式多选(按创建日期分组)
114
114
  ```
115
115
 
116
- 不传 `-b` 时,分支列表按创建日期分组显示,支持全局全选和按组全选。选 1 个在当前终端恢复,选多个自动在独立终端 Tab 中批量恢复(仅 macOS)。
116
+ 不传 `-b` 时,分支列表按创建日期分组显示,支持全局全选和按组全选。选 1 个默认在新终端 Tab 中恢复(设置 `resumeInPlace: true` 可改为在当前终端就地恢复),选多个自动在独立终端 Tab 中批量恢复(仅 macOS)。
117
117
 
118
118
  如果目标 worktree 存在历史会话,会自动继续上次对话(`--continue`)。
119
119
 
@@ -290,6 +290,7 @@ clawt alias remove l
290
290
  | `confirmDestructiveOps` | `true` | 破坏性操作前确认 |
291
291
  | `maxConcurrency` | `0` | run 命令最大并发数,`0` 为不限制 |
292
292
  | `terminalApp` | `"auto"` | 批量 resume 使用的终端:`auto` / `iterm2` / `terminal` |
293
+ | `resumeInPlace` | `false` | resume 单选时在当前终端就地恢复,`false` 则在新 Tab 中打开 |
293
294
  | `aliases` | `{}` | 命令别名映射(如 `{"l": "list", "r": "run"}`) |
294
295
  | `autoUpdate` | `true` | 自动检查新版本(每 24 小时检查一次 npm registry) |
295
296
 
package/dist/index.js CHANGED
@@ -580,6 +580,10 @@ var CONFIG_DEFINITIONS = {
580
580
  description: "\u6279\u91CF resume \u4F7F\u7528\u7684\u7EC8\u7AEF\u5E94\u7528\uFF1Aauto\uFF08\u81EA\u52A8\u68C0\u6D4B\uFF09\u3001iterm2\u3001terminal\uFF08macOS\uFF09",
581
581
  allowedValues: VALID_TERMINAL_APPS
582
582
  },
583
+ resumeInPlace: {
584
+ defaultValue: false,
585
+ description: "resume \u5355\u9009\u65F6\u662F\u5426\u5728\u5F53\u524D\u7EC8\u7AEF\u5C31\u5730\u6253\u5F00\uFF0Cfalse \u5219\u901A\u8FC7 terminalApp \u5728\u65B0 Tab \u4E2D\u6253\u5F00"
586
+ },
583
587
  aliases: {
584
588
  defaultValue: {},
585
589
  description: "\u547D\u4EE4\u522B\u540D\u6620\u5C04"
@@ -3631,6 +3635,7 @@ function registerRemoveCommand(program2) {
3631
3635
  }
3632
3636
  async function handleRemove(options) {
3633
3637
  validateMainWorktree();
3638
+ requireProjectConfig();
3634
3639
  const projectName = getProjectName();
3635
3640
  logger.info(`remove \u547D\u4EE4\u6267\u884C\uFF0C\u9879\u76EE: ${projectName}`);
3636
3641
  const allWorktrees = getProjectWorktrees();
@@ -3660,7 +3665,6 @@ async function handleRemove(options) {
3660
3665
  const failures = [];
3661
3666
  for (const wt of worktreesToRemove) {
3662
3667
  try {
3663
- await ensureOnMainWorkBranch();
3664
3668
  removeWorktreeByPath(wt.path);
3665
3669
  if (shouldDeleteBranch) {
3666
3670
  deleteBranch(wt.branch);
@@ -3805,7 +3809,13 @@ async function handleResume(options) {
3805
3809
  return;
3806
3810
  }
3807
3811
  if (targetWorktrees.length === 1) {
3808
- launchInteractiveClaude(targetWorktrees[0], { autoContinue: true });
3812
+ const inPlace = getConfigValue("resumeInPlace");
3813
+ if (inPlace) {
3814
+ launchInteractiveClaude(targetWorktrees[0], { autoContinue: true });
3815
+ } else {
3816
+ const hasPreviousSession = hasClaudeSessionHistory(targetWorktrees[0].path);
3817
+ launchInteractiveClaudeInNewTerminal(targetWorktrees[0], hasPreviousSession);
3818
+ }
3809
3819
  } else {
3810
3820
  await handleBatchResume(targetWorktrees);
3811
3821
  }
@@ -3893,7 +3903,6 @@ async function executeSyncForBranch(targetWorktreePath, branch) {
3893
3903
  async function handleSync(options) {
3894
3904
  validateMainWorktree();
3895
3905
  requireProjectConfig();
3896
- await ensureOnMainWorkBranch();
3897
3906
  logger.info(`sync \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch ?? "(\u672A\u6307\u5B9A)"}`);
3898
3907
  const worktrees = getProjectWorktrees();
3899
3908
  const worktree = await resolveTargetWorktree(worktrees, SYNC_RESOLVE_MESSAGES, options.branch);
@@ -525,6 +525,10 @@ var CONFIG_DEFINITIONS = {
525
525
  description: "\u6279\u91CF resume \u4F7F\u7528\u7684\u7EC8\u7AEF\u5E94\u7528\uFF1Aauto\uFF08\u81EA\u52A8\u68C0\u6D4B\uFF09\u3001iterm2\u3001terminal\uFF08macOS\uFF09",
526
526
  allowedValues: VALID_TERMINAL_APPS
527
527
  },
528
+ resumeInPlace: {
529
+ defaultValue: false,
530
+ description: "resume \u5355\u9009\u65F6\u662F\u5426\u5728\u5F53\u524D\u7EC8\u7AEF\u5C31\u5730\u6253\u5F00\uFF0Cfalse \u5219\u901A\u8FC7 terminalApp \u5728\u65B0 Tab \u4E2D\u6253\u5F00"
531
+ },
528
532
  aliases: {
529
533
  defaultValue: {},
530
534
  description: "\u547D\u4EE4\u522B\u540D\u6620\u5C04"
@@ -22,6 +22,7 @@
22
22
  "confirmDestructiveOps": true,
23
23
  "maxConcurrency": 0,
24
24
  "terminalApp": "auto",
25
+ "resumeInPlace": false,
25
26
  "aliases": {},
26
27
  "autoUpdate": true
27
28
  }
@@ -37,6 +38,7 @@
37
38
  | `confirmDestructiveOps` | `boolean` | `true` | 执行破坏性操作(reset、validate --clean)前是否提示确认 |
38
39
  | `maxConcurrency` | `number` | `0` | run 命令默认最大并发数,`0` 表示不限制 |
39
40
  | `terminalApp` | `string` | `"auto"` | 批量 resume 使用的终端应用:`auto`(自动检测)、`iterm2`、`terminal`(macOS) |
41
+ | `resumeInPlace` | `boolean` | `false` | resume 单选时是否在当前终端就地打开,`false` 则通过 `terminalApp` 在新 Tab 中打开 |
40
42
  | `aliases` | `Record<string, string>` | `{}` | 命令别名映射,键为别名,值为目标内置命令名 |
41
43
  | `autoUpdate` | `boolean` | `true` | 是否启用自动更新检查(每 24 小时通过 npm registry 检查一次新版本) |
42
44
 
package/docs/resume.md CHANGED
@@ -38,8 +38,10 @@ clawt resume
38
38
  3. **无匹配** → 报错退出,并列出所有可用分支名
39
39
  4. **根据选中数量自动分发**:
40
40
  - **用户未选择任何分支** → 直接退出
41
- - **选中 1 个** → 在当前终端恢复(同原有行为),通过 `launchInteractiveClaude()` 启动(使用 `spawnSync` + `inherit stdio`)
42
- - **选中多个**进入批量恢复流程(见下文)
41
+ - **选中 1 个** → 根据全局配置项 `resumeInPlace` 决定打开方式:
42
+ - `resumeInPlace: true` 在当前终端就地恢复,通过 `launchInteractiveClaude()` 启动(使用 `spawnSync` + `inherit stdio`)
43
+ - `resumeInPlace: false`(默认) → 通过 `launchInteractiveClaudeInNewTerminal()` 在新终端 Tab 中恢复,终端类型由 `terminalApp` 配置控制
44
+ - **选中多个** → 进入批量恢复流程(见下文),始终在新终端 Tab 中打开,不受 `resumeInPlace` 影响
43
45
 
44
46
  **批量恢复流程:**
45
47
 
package/docs/spec.md CHANGED
@@ -328,7 +328,7 @@ export const PROJECTS_CONFIG_DIR = join(CLAWT_HOME, 'projects');
328
328
  ✗ 该项目尚未初始化,请先执行 clawt init -b<branchName>设置主工作分支
329
329
  ```
330
330
  其他命令(list、resume、config、status、alias、projects、completion)不受影响,无需添加该校验。
331
- > **实现细节**:`ensureOnMainWorkBranch()` 内部已通过 `getMainWorkBranch()` → `requireProjectConfig()` 完成了项目配置校验,因此调用了 `ensureOnMainWorkBranch` 的命令(create、run、validate、sync、remove、merge)**无需再显式调用 `requireProjectConfig()`**,避免重复校验。仅 reset 命令因不调用 `ensureOnMainWorkBranch`,需要自行调用 `requireProjectConfig()`。
331
+ > **实现细节**:`ensureOnMainWorkBranch()` 内部已通过 `getMainWorkBranch()` → `requireProjectConfig()` 完成了项目配置校验,因此调用了 `ensureOnMainWorkBranch` 的命令(create、run、validate、merge)**无需再显式调用 `requireProjectConfig()`**,避免重复校验。sync remove 命令因不依赖主 worktree 的分支状态而不调用 `ensureOnMainWorkBranch`,需自行显式调用 `requireProjectConfig()`。reset 命令同理,也需自行调用 `requireProjectConfig()`。
332
332
  3. **主分支名统一从项目级配置获取**:所有需要获取主分支名的场景(sync 中合并主分支、merge 中计算 merge-base、切回主分支等),统一使用项目级配置中的 `clawtMainWorkBranch`,不再通过 `getCurrentBranch(mainWorktreePath)` 动态获取。因为在新架构下,主 worktree 可能处于验证分支上,`getCurrentBranch` 会返回验证分支名而非真正的主工作分支名。
333
333
  4. **测试文件全量更新**:本次重构涉及的所有命令(init、create、run、validate、sync、remove、merge、reset),其对应的测试文件必须同步更新,确保覆盖新增的验证分支逻辑、项目级配置逻辑和变更后的流程。
334
334
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "3.1.0",
3
+ "version": "3.1.2",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,7 +23,7 @@ import {
23
23
  resolveTargetWorktrees,
24
24
  getValidateBranchName,
25
25
  deleteValidateBranch,
26
- ensureOnMainWorkBranch,
26
+ requireProjectConfig,
27
27
  } from '../utils/index.js';
28
28
  import type { WorktreeMultiResolveMessages } from '../utils/index.js';
29
29
 
@@ -56,6 +56,7 @@ export function registerRemoveCommand(program: Command): void {
56
56
  */
57
57
  async function handleRemove(options: RemoveOptions): Promise<void> {
58
58
  validateMainWorktree();
59
+ requireProjectConfig();
59
60
 
60
61
  const projectName = getProjectName();
61
62
  logger.info(`remove 命令执行,项目: ${projectName}`);
@@ -98,8 +99,6 @@ async function handleRemove(options: RemoveOptions): Promise<void> {
98
99
  const failures: Array<{ path: string; error: string }> = [];
99
100
  for (const wt of worktreesToRemove) {
100
101
  try {
101
- // 确保当前在主工作分支上
102
- await ensureOnMainWorkBranch();
103
102
  removeWorktreeByPath(wt.path);
104
103
  if (shouldDeleteBranch) {
105
104
  deleteBranch(wt.branch);
@@ -15,6 +15,7 @@ import {
15
15
  printInfo,
16
16
  printSuccess,
17
17
  confirmAction,
18
+ getConfigValue,
18
19
  } from '../utils/index.js';
19
20
  import type { WorktreeMultiResolveMessages } from '../utils/index.js';
20
21
 
@@ -66,10 +67,18 @@ async function handleResume(options: ResumeOptions): Promise<void> {
66
67
  }
67
68
 
68
69
  if (targetWorktrees.length === 1) {
69
- // 选中 1 个 → 当前终端恢复(resume 自动续接历史会话)
70
- launchInteractiveClaude(targetWorktrees[0], { autoContinue: true });
70
+ // 选中 1 个 → 根据 resumeInPlace 配置决定打开方式
71
+ const inPlace = getConfigValue('resumeInPlace');
72
+ if (inPlace) {
73
+ // 就地在当前终端恢复
74
+ launchInteractiveClaude(targetWorktrees[0], { autoContinue: true });
75
+ } else {
76
+ // 默认通过 terminalApp 在新 Tab 中恢复
77
+ const hasPreviousSession = hasClaudeSessionHistory(targetWorktrees[0].path);
78
+ launchInteractiveClaudeInNewTerminal(targetWorktrees[0], hasPreviousSession);
79
+ }
71
80
  } else {
72
- // 选中多个 → 逐个在新终端 Tab 中启动
81
+ // 选中多个 → 逐个在新终端 Tab 中启动(不受 resumeInPlace 影响)
73
82
  await handleBatchResume(targetWorktrees);
74
83
  }
75
84
  }
@@ -23,7 +23,6 @@ import {
23
23
  getMainWorkBranch,
24
24
  rebuildValidateBranch,
25
25
  getValidateBranchName,
26
- ensureOnMainWorkBranch,
27
26
  } from '../utils/index.js';
28
27
  import type { WorktreeResolveMessages } from '../utils/index.js';
29
28
 
@@ -140,7 +139,6 @@ export async function executeSyncForBranch(targetWorktreePath: string, branch: s
140
139
  async function handleSync(options: SyncOptions): Promise<void> {
141
140
  validateMainWorktree();
142
141
  requireProjectConfig();
143
- await ensureOnMainWorkBranch();
144
142
 
145
143
  logger.info(`sync 命令执行,分支: ${options.branch ?? '(未指定)'}`);
146
144
 
@@ -35,6 +35,10 @@ export const CONFIG_DEFINITIONS: ConfigDefinitions = {
35
35
  description: '批量 resume 使用的终端应用:auto(自动检测)、iterm2、terminal(macOS)',
36
36
  allowedValues: VALID_TERMINAL_APPS,
37
37
  },
38
+ resumeInPlace: {
39
+ defaultValue: false,
40
+ description: 'resume 单选时是否在当前终端就地打开,false 则通过 terminalApp 在新 Tab 中打开',
41
+ },
38
42
  aliases: {
39
43
  defaultValue: {} as Record<string, string>,
40
44
  description: '命令别名映射',
@@ -17,3 +17,4 @@ export const VALID_TERMINAL_APPS: readonly string[] = ['auto', 'iterm2', 'termin
17
17
 
18
18
  /** iTerm2 应用路径,用于 auto 模式检测是否已安装 */
19
19
  export const ITERM2_APP_PATH = '/Applications/iTerm.app';
20
+
@@ -12,6 +12,8 @@ export interface ClawtConfig {
12
12
  maxConcurrency: number;
13
13
  /** 批量 resume 使用的终端应用:'auto'(自动检测)、'iterm2'、'terminal'(macOS) */
14
14
  terminalApp: string;
15
+ /** resume 单选时是否在当前终端就地打开,false 则通过 terminalApp 在新 Tab 中打开 */
16
+ resumeInPlace: boolean;
15
17
  /** 命令别名映射,键为别名,值为目标内置命令名 */
16
18
  aliases: Record<string, string>;
17
19
  /** 是否启用自动更新检查 */
@@ -49,7 +49,6 @@ vi.mock('../../../src/utils/index.js', () => ({
49
49
  requireProjectConfig: vi.fn().mockReturnValue({ clawtMainWorkBranch: 'main' }),
50
50
  getValidateBranchName: vi.fn((name: string) => `clawt-validate-${name}`),
51
51
  deleteValidateBranch: vi.fn(),
52
- ensureOnMainWorkBranch: vi.fn(),
53
52
  }));
54
53
 
55
54
  import { registerRemoveCommand } from '../../../src/commands/remove.js';
@@ -11,6 +11,8 @@ vi.mock('../../../src/constants/index.js', () => ({
11
11
  RESUME_SELECT_BRANCH: '选择要恢复的分支',
12
12
  RESUME_MULTIPLE_MATCHES: (keyword: string) => `找到多个匹配 "${keyword}" 的分支`,
13
13
  RESUME_NO_MATCH: (keyword: string, branches: string[]) => `未找到匹配 "${keyword}" 的分支`,
14
+ RESUME_ALL_CONFIRM: (count: number) => `确认恢复 ${count} 个分支?`,
15
+ RESUME_ALL_SUCCESS: (count: number) => `已恢复 ${count} 个分支`,
14
16
  },
15
17
  }));
16
18
 
@@ -19,8 +21,14 @@ vi.mock('../../../src/utils/index.js', () => ({
19
21
  validateClaudeCodeInstalled: vi.fn(),
20
22
  getProjectWorktrees: vi.fn(),
21
23
  launchInteractiveClaude: vi.fn(),
24
+ launchInteractiveClaudeInNewTerminal: vi.fn(),
25
+ hasClaudeSessionHistory: vi.fn(),
22
26
  resolveTargetWorktrees: vi.fn(),
23
27
  promptGroupedMultiSelectBranches: vi.fn(),
28
+ printInfo: vi.fn(),
29
+ printSuccess: vi.fn(),
30
+ confirmAction: vi.fn(),
31
+ getConfigValue: vi.fn(),
24
32
  }));
25
33
 
26
34
  import { registerResumeCommand } from '../../../src/commands/resume.js';
@@ -29,24 +37,36 @@ import {
29
37
  validateClaudeCodeInstalled,
30
38
  getProjectWorktrees,
31
39
  launchInteractiveClaude,
40
+ launchInteractiveClaudeInNewTerminal,
41
+ hasClaudeSessionHistory,
32
42
  resolveTargetWorktrees,
33
43
  promptGroupedMultiSelectBranches,
44
+ confirmAction,
45
+ getConfigValue,
34
46
  } from '../../../src/utils/index.js';
35
47
 
36
48
  const mockedValidateMainWorktree = vi.mocked(validateMainWorktree);
37
49
  const mockedValidateClaudeCodeInstalled = vi.mocked(validateClaudeCodeInstalled);
38
50
  const mockedGetProjectWorktrees = vi.mocked(getProjectWorktrees);
39
51
  const mockedLaunchInteractiveClaude = vi.mocked(launchInteractiveClaude);
52
+ const mockedLaunchInteractiveClaudeInNewTerminal = vi.mocked(launchInteractiveClaudeInNewTerminal);
53
+ const mockedHasClaudeSessionHistory = vi.mocked(hasClaudeSessionHistory);
40
54
  const mockedResolveTargetWorktrees = vi.mocked(resolveTargetWorktrees);
41
55
  const mockedPromptGroupedMultiSelectBranches = vi.mocked(promptGroupedMultiSelectBranches);
56
+ const mockedConfirmAction = vi.mocked(confirmAction);
57
+ const mockedGetConfigValue = vi.mocked(getConfigValue);
42
58
 
43
59
  beforeEach(() => {
44
60
  mockedValidateMainWorktree.mockReset();
45
61
  mockedValidateClaudeCodeInstalled.mockReset();
46
62
  mockedGetProjectWorktrees.mockReset();
47
63
  mockedLaunchInteractiveClaude.mockReset();
64
+ mockedLaunchInteractiveClaudeInNewTerminal.mockReset();
65
+ mockedHasClaudeSessionHistory.mockReset();
48
66
  mockedResolveTargetWorktrees.mockReset();
49
67
  mockedPromptGroupedMultiSelectBranches.mockReset();
68
+ mockedConfirmAction.mockReset();
69
+ mockedGetConfigValue.mockReset();
50
70
  });
51
71
 
52
72
  describe('registerResumeCommand', () => {
@@ -63,6 +83,7 @@ describe('handleResume', () => {
63
83
  const worktree = { path: '/path/feature', branch: 'feature' };
64
84
  mockedGetProjectWorktrees.mockReturnValue([worktree]);
65
85
  mockedResolveTargetWorktrees.mockResolvedValue([worktree]);
86
+ mockedGetConfigValue.mockReturnValue(true);
66
87
 
67
88
  const program = new Command();
68
89
  program.exitOverride();
@@ -83,6 +104,7 @@ describe('handleResume', () => {
83
104
  ];
84
105
  mockedGetProjectWorktrees.mockReturnValue(worktrees);
85
106
  mockedPromptGroupedMultiSelectBranches.mockResolvedValue([worktrees[0]]);
107
+ mockedGetConfigValue.mockReturnValue(true);
86
108
 
87
109
  const program = new Command();
88
110
  program.exitOverride();
@@ -100,6 +122,7 @@ describe('handleResume', () => {
100
122
  const worktree = { path: '/path/feature', branch: 'feature' };
101
123
  mockedGetProjectWorktrees.mockReturnValue([worktree]);
102
124
  mockedResolveTargetWorktrees.mockResolvedValue([worktree]);
125
+ mockedGetConfigValue.mockReturnValue(true);
103
126
 
104
127
  const program = new Command();
105
128
  program.exitOverride();
@@ -110,3 +133,93 @@ describe('handleResume', () => {
110
133
  expect(mockedPromptGroupedMultiSelectBranches).not.toHaveBeenCalled();
111
134
  });
112
135
  });
136
+
137
+ describe('handleResume — resumeInPlace 配置', () => {
138
+ it('resumeInPlace 为 true 时,单选在当前终端就地恢复', async () => {
139
+ const worktree = { path: '/path/feature', branch: 'feature' };
140
+ mockedGetProjectWorktrees.mockReturnValue([worktree]);
141
+ mockedResolveTargetWorktrees.mockResolvedValue([worktree]);
142
+ mockedGetConfigValue.mockReturnValue(true);
143
+
144
+ const program = new Command();
145
+ program.exitOverride();
146
+ registerResumeCommand(program);
147
+ await program.parseAsync(['resume', '-b', 'feature'], { from: 'user' });
148
+
149
+ expect(mockedGetConfigValue).toHaveBeenCalledWith('resumeInPlace');
150
+ expect(mockedLaunchInteractiveClaude).toHaveBeenCalledWith(worktree, { autoContinue: true });
151
+ expect(mockedLaunchInteractiveClaudeInNewTerminal).not.toHaveBeenCalled();
152
+ });
153
+
154
+ it('resumeInPlace 为 false 时,单选在新终端 Tab 中恢复', async () => {
155
+ const worktree = { path: '/path/feature', branch: 'feature' };
156
+ mockedGetProjectWorktrees.mockReturnValue([worktree]);
157
+ mockedResolveTargetWorktrees.mockResolvedValue([worktree]);
158
+ mockedGetConfigValue.mockReturnValue(false);
159
+ mockedHasClaudeSessionHistory.mockReturnValue(true);
160
+
161
+ const program = new Command();
162
+ program.exitOverride();
163
+ registerResumeCommand(program);
164
+ await program.parseAsync(['resume', '-b', 'feature'], { from: 'user' });
165
+
166
+ expect(mockedGetConfigValue).toHaveBeenCalledWith('resumeInPlace');
167
+ expect(mockedHasClaudeSessionHistory).toHaveBeenCalledWith(worktree.path);
168
+ expect(mockedLaunchInteractiveClaudeInNewTerminal).toHaveBeenCalledWith(worktree, true);
169
+ expect(mockedLaunchInteractiveClaude).not.toHaveBeenCalled();
170
+ });
171
+
172
+ it('resumeInPlace 为 false 且无历史会话时,传 false 给新终端启动', async () => {
173
+ const worktree = { path: '/path/feature', branch: 'feature' };
174
+ mockedGetProjectWorktrees.mockReturnValue([worktree]);
175
+ mockedResolveTargetWorktrees.mockResolvedValue([worktree]);
176
+ mockedGetConfigValue.mockReturnValue(false);
177
+ mockedHasClaudeSessionHistory.mockReturnValue(false);
178
+
179
+ const program = new Command();
180
+ program.exitOverride();
181
+ registerResumeCommand(program);
182
+ await program.parseAsync(['resume', '-b', 'feature'], { from: 'user' });
183
+
184
+ expect(mockedLaunchInteractiveClaudeInNewTerminal).toHaveBeenCalledWith(worktree, false);
185
+ });
186
+
187
+ it('多选时不受 resumeInPlace 影响,始终在新 Tab 中打开', async () => {
188
+ const worktrees = [
189
+ { path: '/path/feature-a', branch: 'feature-a' },
190
+ { path: '/path/feature-b', branch: 'feature-b' },
191
+ ];
192
+ mockedGetProjectWorktrees.mockReturnValue(worktrees);
193
+ mockedPromptGroupedMultiSelectBranches.mockResolvedValue(worktrees);
194
+ mockedConfirmAction.mockResolvedValue(true);
195
+ mockedHasClaudeSessionHistory.mockReturnValue(false);
196
+ mockedGetConfigValue.mockReturnValue(true);
197
+
198
+ const program = new Command();
199
+ program.exitOverride();
200
+ registerResumeCommand(program);
201
+ await program.parseAsync(['resume'], { from: 'user' });
202
+
203
+ // 多选走 handleBatchResume,不读取 resumeInPlace
204
+ expect(mockedLaunchInteractiveClaude).not.toHaveBeenCalled();
205
+ expect(mockedLaunchInteractiveClaudeInNewTerminal).toHaveBeenCalledTimes(2);
206
+ });
207
+
208
+ it('用户未选择任何分支时直接退出', async () => {
209
+ const worktrees = [
210
+ { path: '/path/feature-a', branch: 'feature-a' },
211
+ { path: '/path/feature-b', branch: 'feature-b' },
212
+ ];
213
+ mockedGetProjectWorktrees.mockReturnValue(worktrees);
214
+ mockedPromptGroupedMultiSelectBranches.mockResolvedValue([]);
215
+
216
+ const program = new Command();
217
+ program.exitOverride();
218
+ registerResumeCommand(program);
219
+ await program.parseAsync(['resume'], { from: 'user' });
220
+
221
+ expect(mockedLaunchInteractiveClaude).not.toHaveBeenCalled();
222
+ expect(mockedLaunchInteractiveClaudeInNewTerminal).not.toHaveBeenCalled();
223
+ expect(mockedGetConfigValue).not.toHaveBeenCalled();
224
+ });
225
+ });
@@ -51,7 +51,6 @@ vi.mock('../../../src/utils/index.js', () => ({
51
51
  getMainWorkBranch: vi.fn().mockReturnValue('main'),
52
52
  rebuildValidateBranch: vi.fn(),
53
53
  getValidateBranchName: vi.fn((name: string) => `clawt-validate-${name}`),
54
- ensureOnMainWorkBranch: vi.fn(),
55
54
  }));
56
55
 
57
56
  import { registerSyncCommand } from '../../../src/commands/sync.js';
@@ -33,6 +33,7 @@ describe('DEFAULT_CONFIG', () => {
33
33
  expect(DEFAULT_CONFIG.confirmDestructiveOps).toBe(true);
34
34
  expect(DEFAULT_CONFIG.maxConcurrency).toBe(0);
35
35
  expect(DEFAULT_CONFIG.aliases).toEqual({});
36
+ expect(DEFAULT_CONFIG.resumeInPlace).toBe(false);
36
37
  });
37
38
  });
38
39