clawt 2.11.0 → 2.11.1

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.
@@ -35,7 +35,9 @@
35
35
  - remove 批量操作时收集错误继续处理,最后汇总报告
36
36
  - 文档中文风格,技术术语保留英文(worktree, merge, branch, SIGINT 等)
37
37
  - cleanupWorktrees 是 merge 和 run 共用的公共清理函数(在 src/utils/worktree.ts)
38
- - `launchInteractiveClaude` 是 run(交互式模式)和 resume 共用的公共函数(在 src/utils/claude.ts
38
+ - `launchInteractiveClaude` 是 run(交互式模式)和 resume 共用的公共函数(在 src/utils/claude.ts),启动前自动检测会话历史并追加 `--continue`
39
+ - `hasClaudeSessionHistory` 检测 `~/.claude/projects/<encoded-path>/` 下是否有 `.jsonl` 文件(在 src/utils/claude.ts)
40
+ - `CLAUDE_PROJECTS_DIR` 常量(`~/.claude/projects/`)定义在 `src/constants/paths.ts`
39
41
  - killAllChildProcesses 是 run 专用的子进程终止函数(在 src/utils/shell.ts)
40
42
  - validate 快照管理函数在 `src/utils/validate-snapshot.ts`,被 validate、merge、remove 和 status 四个命令使用
41
43
  - `confirmDestructiveAction` 在 `src/utils/formatter.ts`,被 reset、validate --clean 和 config reset 使用
package/README.md CHANGED
@@ -83,6 +83,8 @@ clawt resume -b <branch> # 指定分支
83
83
  clawt resume # 交互式选择
84
84
  ```
85
85
 
86
+ 如果目标 worktree 存在历史会话,会自动继续上次对话(`--continue`)。
87
+
86
88
  ### `clawt create` — 仅创建 worktree(不执行任务)
87
89
 
88
90
  ```bash
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ var CONFIG_PATH = join(CLAWT_HOME, "config.json");
14
14
  var LOGS_DIR = join(CLAWT_HOME, "logs");
15
15
  var WORKTREES_DIR = join(CLAWT_HOME, "worktrees");
16
16
  var VALIDATE_SNAPSHOTS_DIR = join(CLAWT_HOME, "validate-snapshots");
17
+ var CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
17
18
 
18
19
  // src/constants/branch.ts
19
20
  var INVALID_BRANCH_CHARS = /[\/\\.\s~:*?[\]^]+/g;
@@ -904,7 +905,21 @@ import Enquirer from "enquirer";
904
905
 
905
906
  // src/utils/claude.ts
906
907
  import { spawnSync } from "child_process";
907
- function launchInteractiveClaude(worktree) {
908
+ import { existsSync as existsSync5, readdirSync as readdirSync3 } from "fs";
909
+ import { join as join3 } from "path";
910
+ function encodeClaudeProjectPath(absolutePath) {
911
+ return absolutePath.replace(/[^a-zA-Z0-9]/g, "-");
912
+ }
913
+ function hasClaudeSessionHistory(worktreePath) {
914
+ const encodedName = encodeClaudeProjectPath(worktreePath);
915
+ const projectDir = join3(CLAUDE_PROJECTS_DIR, encodedName);
916
+ if (!existsSync5(projectDir)) {
917
+ return false;
918
+ }
919
+ const entries = readdirSync3(projectDir);
920
+ return entries.some((entry) => entry.endsWith(".jsonl"));
921
+ }
922
+ function launchInteractiveClaude(worktree, options = {}) {
908
923
  const commandStr = getConfigValue("claudeCodeCommand");
909
924
  const parts = commandStr.split(/\s+/).filter(Boolean);
910
925
  const cmd = parts[0];
@@ -913,10 +928,17 @@ function launchInteractiveClaude(worktree) {
913
928
  "--append-system-prompt",
914
929
  APPEND_SYSTEM_PROMPT
915
930
  ];
931
+ const hasPreviousSession = options.autoContinue === true && hasClaudeSessionHistory(worktree.path);
932
+ if (hasPreviousSession) {
933
+ args.push("--continue");
934
+ }
916
935
  printInfo(`\u6B63\u5728 worktree \u4E2D\u542F\u52A8 Claude Code \u4EA4\u4E92\u5F0F\u754C\u9762...`);
917
936
  printInfo(` \u5206\u652F: ${worktree.branch}`);
918
937
  printInfo(` \u8DEF\u5F84: ${worktree.path}`);
919
938
  printInfo(` \u6307\u4EE4: ${commandStr}`);
939
+ if (options.autoContinue) {
940
+ printInfo(` \u6A21\u5F0F: ${hasPreviousSession ? "\u7EE7\u7EED\u4E0A\u6B21\u5BF9\u8BDD" : "\u65B0\u5BF9\u8BDD"}`);
941
+ }
920
942
  printInfo("");
921
943
  const result = spawnSync(cmd, args, {
922
944
  cwd: worktree.path,
@@ -931,29 +953,29 @@ function launchInteractiveClaude(worktree) {
931
953
  }
932
954
 
933
955
  // src/utils/validate-snapshot.ts
934
- import { join as join3 } from "path";
935
- import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, readdirSync as readdirSync3, rmdirSync as rmdirSync2 } from "fs";
956
+ import { join as join4 } from "path";
957
+ import { existsSync as existsSync6, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, readdirSync as readdirSync4, rmdirSync as rmdirSync2 } from "fs";
936
958
  function getSnapshotPath(projectName, branchName) {
937
- return join3(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
959
+ return join4(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
938
960
  }
939
961
  function getSnapshotHeadPath(projectName, branchName) {
940
- return join3(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
962
+ return join4(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
941
963
  }
942
964
  function hasSnapshot(projectName, branchName) {
943
- return existsSync5(getSnapshotPath(projectName, branchName));
965
+ return existsSync6(getSnapshotPath(projectName, branchName));
944
966
  }
945
967
  function readSnapshot(projectName, branchName) {
946
968
  const snapshotPath = getSnapshotPath(projectName, branchName);
947
969
  const headPath = getSnapshotHeadPath(projectName, branchName);
948
970
  logger.debug(`\u8BFB\u53D6 validate \u5FEB\u7167: ${snapshotPath}`);
949
- const treeHash = existsSync5(snapshotPath) ? readFileSync2(snapshotPath, "utf-8").trim() : "";
950
- const headCommitHash = existsSync5(headPath) ? readFileSync2(headPath, "utf-8").trim() : "";
971
+ const treeHash = existsSync6(snapshotPath) ? readFileSync2(snapshotPath, "utf-8").trim() : "";
972
+ const headCommitHash = existsSync6(headPath) ? readFileSync2(headPath, "utf-8").trim() : "";
951
973
  return { treeHash, headCommitHash };
952
974
  }
953
975
  function writeSnapshot(projectName, branchName, treeHash, headCommitHash) {
954
976
  const snapshotPath = getSnapshotPath(projectName, branchName);
955
977
  const headPath = getSnapshotHeadPath(projectName, branchName);
956
- const snapshotDir = join3(VALIDATE_SNAPSHOTS_DIR, projectName);
978
+ const snapshotDir = join4(VALIDATE_SNAPSHOTS_DIR, projectName);
957
979
  ensureDir(snapshotDir);
958
980
  writeFileSync2(snapshotPath, treeHash, "utf-8");
959
981
  writeFileSync2(headPath, headCommitHash, "utf-8");
@@ -962,31 +984,31 @@ function writeSnapshot(projectName, branchName, treeHash, headCommitHash) {
962
984
  function removeSnapshot(projectName, branchName) {
963
985
  const snapshotPath = getSnapshotPath(projectName, branchName);
964
986
  const headPath = getSnapshotHeadPath(projectName, branchName);
965
- if (existsSync5(snapshotPath)) {
987
+ if (existsSync6(snapshotPath)) {
966
988
  unlinkSync(snapshotPath);
967
989
  logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${snapshotPath}`);
968
990
  }
969
- if (existsSync5(headPath)) {
991
+ if (existsSync6(headPath)) {
970
992
  unlinkSync(headPath);
971
993
  logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${headPath}`);
972
994
  }
973
995
  }
974
996
  function getProjectSnapshotBranches(projectName) {
975
- const projectDir = join3(VALIDATE_SNAPSHOTS_DIR, projectName);
976
- if (!existsSync5(projectDir)) {
997
+ const projectDir = join4(VALIDATE_SNAPSHOTS_DIR, projectName);
998
+ if (!existsSync6(projectDir)) {
977
999
  return [];
978
1000
  }
979
- const files = readdirSync3(projectDir);
1001
+ const files = readdirSync4(projectDir);
980
1002
  return files.filter((f) => f.endsWith(".tree")).map((f) => f.replace(/\.tree$/, ""));
981
1003
  }
982
1004
  function removeProjectSnapshots(projectName) {
983
- const projectDir = join3(VALIDATE_SNAPSHOTS_DIR, projectName);
984
- if (!existsSync5(projectDir)) {
1005
+ const projectDir = join4(VALIDATE_SNAPSHOTS_DIR, projectName);
1006
+ if (!existsSync6(projectDir)) {
985
1007
  return;
986
1008
  }
987
- const files = readdirSync3(projectDir);
1009
+ const files = readdirSync4(projectDir);
988
1010
  for (const file of files) {
989
- unlinkSync(join3(projectDir, file));
1011
+ unlinkSync(join4(projectDir, file));
990
1012
  }
991
1013
  try {
992
1014
  rmdirSync2(projectDir);
@@ -1283,7 +1305,7 @@ var ProgressRenderer = class {
1283
1305
 
1284
1306
  // src/utils/task-file.ts
1285
1307
  import { resolve } from "path";
1286
- import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
1308
+ import { existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
1287
1309
  var TASK_BLOCK_REGEX = /<!-- CLAWT-TASKS:START -->([\s\S]*?)<!-- CLAWT-TASKS:END -->/g;
1288
1310
  var BRANCH_LINE_REGEX = /^#\s*branch:\s*(.+)$/;
1289
1311
  function parseTaskFile(content, options) {
@@ -1321,7 +1343,7 @@ function parseTaskFile(content, options) {
1321
1343
  }
1322
1344
  function loadTaskFile(filePath, options) {
1323
1345
  const absolutePath = resolve(filePath);
1324
- if (!existsSync6(absolutePath)) {
1346
+ if (!existsSync7(absolutePath)) {
1325
1347
  throw new ClawtError(MESSAGES.TASK_FILE_NOT_FOUND(absolutePath));
1326
1348
  }
1327
1349
  const content = readFileSync3(absolutePath, "utf-8");
@@ -1764,7 +1786,7 @@ async function handleResume(options) {
1764
1786
  logger.info(`resume \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch ?? "(\u672A\u6307\u5B9A)"}`);
1765
1787
  const worktrees = getProjectWorktrees();
1766
1788
  const worktree = await resolveTargetWorktree(worktrees, RESUME_RESOLVE_MESSAGES, options.branch);
1767
- launchInteractiveClaude(worktree);
1789
+ launchInteractiveClaude(worktree, { autoContinue: true });
1768
1790
  }
1769
1791
 
1770
1792
  // src/commands/validate.ts
@@ -9,6 +9,7 @@ var CONFIG_PATH = join(CLAWT_HOME, "config.json");
9
9
  var LOGS_DIR = join(CLAWT_HOME, "logs");
10
10
  var WORKTREES_DIR = join(CLAWT_HOME, "worktrees");
11
11
  var VALIDATE_SNAPSHOTS_DIR = join(CLAWT_HOME, "validate-snapshots");
12
+ var CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
12
13
 
13
14
  // src/constants/messages/common.ts
14
15
  var COMMON_MESSAGES = {
package/docs/spec.md CHANGED
@@ -1088,6 +1088,8 @@ clawt resume
1088
1088
 
1089
1089
  启动命令通过配置项 `claudeCodeCommand`(默认值 `claude`)指定,与 `clawt run` 不传 `--tasks` 时的交互式界面行为一致。
1090
1090
 
1091
+ **会话自动续接:** 启动前会自动检测该 worktree 是否存在 Claude Code 历史会话(通过检查 `~/.claude/projects/<encoded-path>/` 下是否有 `.jsonl` 文件判断),如果存在则自动追加 `--continue` 参数继续上次对话,否则打开新对话。启动信息中会显示当前模式("继续上次对话"或"新对话")。路径编码规则:将绝对路径中所有非字母数字字符替换为 `-`(与 Claude Code 源码的编码逻辑一致)。
1092
+
1091
1093
  ---
1092
1094
 
1093
1095
  ### 5.12 将主分支代码同步到目标 Worktree
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "2.11.0",
3
+ "version": "2.11.1",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -48,6 +48,6 @@ async function handleResume(options: ResumeOptions): Promise<void> {
48
48
  const worktrees = getProjectWorktrees();
49
49
  const worktree = await resolveTargetWorktree(worktrees, RESUME_RESOLVE_MESSAGES, options.branch);
50
50
 
51
- // 启动 Claude Code 交互式界面
52
- launchInteractiveClaude(worktree);
51
+ // 启动 Claude Code 交互式界面(resume 自动续接历史会话)
52
+ launchInteractiveClaude(worktree, { autoContinue: true });
53
53
  }
@@ -1,4 +1,4 @@
1
- export { CLAWT_HOME, CONFIG_PATH, LOGS_DIR, WORKTREES_DIR, VALIDATE_SNAPSHOTS_DIR } from './paths.js';
1
+ export { CLAWT_HOME, CONFIG_PATH, LOGS_DIR, WORKTREES_DIR, VALIDATE_SNAPSHOTS_DIR, CLAUDE_PROJECTS_DIR } from './paths.js';
2
2
  export { INVALID_BRANCH_CHARS } from './branch.js';
3
3
  export { MESSAGES } from './messages/index.js';
4
4
  export { EXIT_CODES } from './exitCodes.js';
@@ -15,3 +15,6 @@ export const WORKTREES_DIR = join(CLAWT_HOME, 'worktrees');
15
15
 
16
16
  /** validate 快照目录 ~/.clawt/validate-snapshots/ */
17
17
  export const VALIDATE_SNAPSHOTS_DIR = join(CLAWT_HOME, 'validate-snapshots');
18
+
19
+ /** Claude Code 项目会话目录 ~/.claude/projects/ */
20
+ export const CLAUDE_PROJECTS_DIR = join(homedir(), '.claude', 'projects');
@@ -1,16 +1,55 @@
1
1
  import { spawnSync } from 'node:child_process';
2
+ import { existsSync, readdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
2
4
  import { ClawtError } from '../errors/index.js';
3
- import { APPEND_SYSTEM_PROMPT } from '../constants/index.js';
5
+ import { APPEND_SYSTEM_PROMPT, CLAUDE_PROJECTS_DIR } from '../constants/index.js';
4
6
  import { getConfigValue } from './config.js';
5
7
  import { printInfo, printWarning } from './formatter.js';
6
8
  import type { WorktreeInfo } from '../types/index.js';
7
9
 
10
+ /**
11
+ * 将路径编码为 Claude Code 项目目录名
12
+ * 规则:将所有非字母数字字符替换为 -(与 Claude Code 源码中的编码逻辑一致)
13
+ * @param {string} absolutePath - 绝对路径
14
+ * @returns {string} 编码后的目录名
15
+ */
16
+ function encodeClaudeProjectPath(absolutePath: string): string {
17
+ return absolutePath.replace(/[^a-zA-Z0-9]/g, '-');
18
+ }
19
+
20
+ /**
21
+ * 检测指定 worktree 路径是否存在 Claude Code 会话历史
22
+ * 通过检查 ~/.claude/projects/<encoded-path>/ 下是否有 .jsonl 文件来判断
23
+ * @param {string} worktreePath - worktree 的绝对路径
24
+ * @returns {boolean} 是否存在会话历史
25
+ */
26
+ export function hasClaudeSessionHistory(worktreePath: string): boolean {
27
+ const encodedName = encodeClaudeProjectPath(worktreePath);
28
+ const projectDir = join(CLAUDE_PROJECTS_DIR, encodedName);
29
+
30
+ if (!existsSync(projectDir)) {
31
+ return false;
32
+ }
33
+
34
+ const entries = readdirSync(projectDir);
35
+ return entries.some((entry) => entry.endsWith('.jsonl'));
36
+ }
37
+
8
38
  /**
9
39
  * 在指定 worktree 中启动 Claude Code CLI 交互式界面
10
40
  * 使用 spawnSync + inherit stdio,让用户直接与 Claude Code 交互
11
41
  * @param {WorktreeInfo} worktree - worktree 信息
12
42
  */
13
- export function launchInteractiveClaude(worktree: WorktreeInfo): void {
43
+ /**
44
+ * @typedef {Object} LaunchClaudeOptions
45
+ * @property {boolean} [autoContinue] - 是否自动检测历史会话并续接
46
+ */
47
+ interface LaunchClaudeOptions {
48
+ /** 是否自动检测历史会话并追加 --continue 参数 */
49
+ autoContinue?: boolean;
50
+ }
51
+
52
+ export function launchInteractiveClaude(worktree: WorktreeInfo, options: LaunchClaudeOptions = {}): void {
14
53
  const commandStr = getConfigValue('claudeCodeCommand');
15
54
  const parts = commandStr.split(/\s+/).filter(Boolean);
16
55
  const cmd = parts[0];
@@ -20,10 +59,19 @@ export function launchInteractiveClaude(worktree: WorktreeInfo): void {
20
59
  APPEND_SYSTEM_PROMPT,
21
60
  ];
22
61
 
62
+ // 仅在启用 autoContinue 时检测历史会话并追加 --continue
63
+ const hasPreviousSession = options.autoContinue === true && hasClaudeSessionHistory(worktree.path);
64
+ if (hasPreviousSession) {
65
+ args.push('--continue');
66
+ }
67
+
23
68
  printInfo(`正在 worktree 中启动 Claude Code 交互式界面...`);
24
69
  printInfo(` 分支: ${worktree.branch}`);
25
70
  printInfo(` 路径: ${worktree.path}`);
26
71
  printInfo(` 指令: ${commandStr}`);
72
+ if (options.autoContinue) {
73
+ printInfo(` 模式: ${hasPreviousSession ? '继续上次对话' : '新对话'}`);
74
+ }
27
75
  printInfo('');
28
76
 
29
77
  const result = spawnSync(cmd, args, {
@@ -52,7 +52,7 @@ export { loadConfig, writeDefaultConfig, getConfigValue, ensureClawtDirs } from
52
52
  export { printSuccess, printError, printWarning, printInfo, printSeparator, printDoubleSeparator, confirmAction, confirmDestructiveAction, formatWorktreeStatus, isWorktreeIdle, formatDuration } from './formatter.js';
53
53
  export { ensureDir, removeEmptyDir } from './fs.js';
54
54
  export { multilineInput } from './prompt.js';
55
- export { launchInteractiveClaude } from './claude.js';
55
+ export { launchInteractiveClaude, hasClaudeSessionHistory } from './claude.js';
56
56
  export { getSnapshotPath, hasSnapshot, readSnapshotTreeHash, readSnapshot, writeSnapshot, removeSnapshot, removeProjectSnapshots, getProjectSnapshotBranches } from './validate-snapshot.js';
57
57
  export { findExactMatch, findFuzzyMatches, promptSelectBranch, promptMultiSelectBranches, resolveTargetWorktree, resolveTargetWorktrees } from './worktree-matcher.js';
58
58
  export type { WorktreeResolveMessages, WorktreeMultiResolveMessages } from './worktree-matcher.js';
@@ -68,7 +68,7 @@ describe('handleResume', () => {
68
68
  expect(mockedValidateMainWorktree).toHaveBeenCalled();
69
69
  expect(mockedValidateClaudeCodeInstalled).toHaveBeenCalled();
70
70
  expect(mockedResolveTargetWorktree).toHaveBeenCalled();
71
- expect(mockedLaunchInteractiveClaude).toHaveBeenCalledWith(worktree);
71
+ expect(mockedLaunchInteractiveClaude).toHaveBeenCalledWith(worktree, { autoContinue: true });
72
72
  });
73
73
 
74
74
  it('不传 -b 时也能调用 resolveTargetWorktree', async () => {
@@ -10,6 +10,12 @@ vi.mock('node:child_process', () => ({
10
10
  spawnSync: vi.fn(),
11
11
  }));
12
12
 
13
+ // mock node:fs
14
+ vi.mock('node:fs', () => ({
15
+ existsSync: vi.fn(),
16
+ readdirSync: vi.fn(),
17
+ }));
18
+
13
19
  // mock config
14
20
  vi.mock('../../../src/utils/config.js', () => ({
15
21
  getConfigValue: vi.fn(),
@@ -22,7 +28,8 @@ vi.mock('../../../src/utils/formatter.js', () => ({
22
28
  }));
23
29
 
24
30
  import { spawnSync } from 'node:child_process';
25
- import { launchInteractiveClaude } from '../../../src/utils/claude.js';
31
+ import { existsSync, readdirSync } from 'node:fs';
32
+ import { launchInteractiveClaude, hasClaudeSessionHistory } from '../../../src/utils/claude.js';
26
33
  import { getConfigValue } from '../../../src/utils/config.js';
27
34
  import { printInfo, printWarning } from '../../../src/utils/formatter.js';
28
35
  import { ClawtError } from '../../../src/errors/index.js';
@@ -32,6 +39,45 @@ const mockedSpawnSync = vi.mocked(spawnSync);
32
39
  const mockedGetConfigValue = vi.mocked(getConfigValue);
33
40
  const mockedPrintInfo = vi.mocked(printInfo);
34
41
  const mockedPrintWarning = vi.mocked(printWarning);
42
+ const mockedExistsSync = vi.mocked(existsSync);
43
+ const mockedReaddirSync = vi.mocked(readdirSync);
44
+
45
+ describe('hasClaudeSessionHistory', () => {
46
+ it('项目目录不存在时返回 false', () => {
47
+ mockedExistsSync.mockReturnValue(false);
48
+
49
+ expect(hasClaudeSessionHistory('/Users/test/project')).toBe(false);
50
+ expect(mockedExistsSync).toHaveBeenCalled();
51
+ });
52
+
53
+ it('项目目录存在但无 .jsonl 文件时返回 false', () => {
54
+ mockedExistsSync.mockReturnValue(true);
55
+ mockedReaddirSync.mockReturnValue(['memory', 'CLAUDE.md'] as unknown as ReturnType<typeof readdirSync>);
56
+
57
+ expect(hasClaudeSessionHistory('/Users/test/project')).toBe(false);
58
+ });
59
+
60
+ it('项目目录存在且有 .jsonl 文件时返回 true', () => {
61
+ mockedExistsSync.mockReturnValue(true);
62
+ mockedReaddirSync.mockReturnValue([
63
+ 'abc-123.jsonl',
64
+ 'memory',
65
+ ] as unknown as ReturnType<typeof readdirSync>);
66
+
67
+ expect(hasClaudeSessionHistory('/Users/test/project')).toBe(true);
68
+ });
69
+
70
+ it('路径编码规则正确(非字母数字字符替换为 -)', () => {
71
+ mockedExistsSync.mockReturnValue(false);
72
+
73
+ hasClaudeSessionHistory('/Users/qihoo/.clawt/worktrees/clawt/resume');
74
+
75
+ // 验证 existsSync 被调用时路径包含编码后的目录名
76
+ // /Users/qihoo/.clawt/worktrees/clawt/resume → -Users-qihoo--clawt-worktrees-clawt-resume
77
+ const calledPath = mockedExistsSync.mock.calls[0][0] as string;
78
+ expect(calledPath).toContain('-Users-qihoo--clawt-worktrees-clawt-resume');
79
+ });
80
+ });
35
81
 
36
82
  describe('launchInteractiveClaude', () => {
37
83
  const worktree = createWorktreeInfo({
@@ -41,6 +87,7 @@ describe('launchInteractiveClaude', () => {
41
87
 
42
88
  it('正常启动 Claude Code(退出码为 0)', () => {
43
89
  mockedGetConfigValue.mockReturnValue('claude');
90
+ mockedExistsSync.mockReturnValue(false);
44
91
  mockedSpawnSync.mockReturnValue({
45
92
  status: 0,
46
93
  error: undefined,
@@ -66,6 +113,7 @@ describe('launchInteractiveClaude', () => {
66
113
 
67
114
  it('输出分支和路径信息', () => {
68
115
  mockedGetConfigValue.mockReturnValue('claude');
116
+ mockedExistsSync.mockReturnValue(false);
69
117
  mockedSpawnSync.mockReturnValue({
70
118
  status: 0,
71
119
  error: undefined,
@@ -84,6 +132,7 @@ describe('launchInteractiveClaude', () => {
84
132
 
85
133
  it('支持带参数的命令(如 npx claude)', () => {
86
134
  mockedGetConfigValue.mockReturnValue('npx claude');
135
+ mockedExistsSync.mockReturnValue(false);
87
136
  mockedSpawnSync.mockReturnValue({
88
137
  status: 0,
89
138
  error: undefined,
@@ -105,6 +154,7 @@ describe('launchInteractiveClaude', () => {
105
154
 
106
155
  it('spawnSync 返回 error 时抛出 ClawtError', () => {
107
156
  mockedGetConfigValue.mockReturnValue('claude');
157
+ mockedExistsSync.mockReturnValue(false);
108
158
  mockedSpawnSync.mockReturnValue({
109
159
  status: null,
110
160
  error: new Error('命令未找到'),
@@ -121,6 +171,7 @@ describe('launchInteractiveClaude', () => {
121
171
 
122
172
  it('非零退出码时调用 printWarning', () => {
123
173
  mockedGetConfigValue.mockReturnValue('claude');
174
+ mockedExistsSync.mockReturnValue(false);
124
175
  mockedSpawnSync.mockReturnValue({
125
176
  status: 1,
126
177
  error: undefined,
@@ -138,6 +189,7 @@ describe('launchInteractiveClaude', () => {
138
189
 
139
190
  it('退出码为 null 时不调用 printWarning', () => {
140
191
  mockedGetConfigValue.mockReturnValue('claude');
192
+ mockedExistsSync.mockReturnValue(false);
141
193
  mockedSpawnSync.mockReturnValue({
142
194
  status: null,
143
195
  error: undefined,
@@ -155,6 +207,7 @@ describe('launchInteractiveClaude', () => {
155
207
 
156
208
  it('退出码为 0 时不调用 printWarning', () => {
157
209
  mockedGetConfigValue.mockReturnValue('claude');
210
+ mockedExistsSync.mockReturnValue(false);
158
211
  mockedSpawnSync.mockReturnValue({
159
212
  status: 0,
160
213
  error: undefined,
@@ -169,4 +222,65 @@ describe('launchInteractiveClaude', () => {
169
222
 
170
223
  expect(mockedPrintWarning).not.toHaveBeenCalled();
171
224
  });
225
+
226
+ it('autoContinue 启用且有会话历史时追加 --continue 参数', () => {
227
+ mockedGetConfigValue.mockReturnValue('claude');
228
+ mockedExistsSync.mockReturnValue(true);
229
+ mockedReaddirSync.mockReturnValue(['session-abc.jsonl'] as unknown as ReturnType<typeof readdirSync>);
230
+ mockedSpawnSync.mockReturnValue({
231
+ status: 0,
232
+ error: undefined,
233
+ stdout: '',
234
+ stderr: '',
235
+ pid: 1234,
236
+ output: [],
237
+ signal: null,
238
+ });
239
+
240
+ launchInteractiveClaude(worktree, { autoContinue: true });
241
+
242
+ const callArgs = mockedSpawnSync.mock.calls[0][1] as string[];
243
+ expect(callArgs).toContain('--continue');
244
+ expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('继续上次对话'));
245
+ });
246
+
247
+ it('autoContinue 启用但无会话历史时不追加 --continue 参数', () => {
248
+ mockedGetConfigValue.mockReturnValue('claude');
249
+ mockedExistsSync.mockReturnValue(false);
250
+ mockedSpawnSync.mockReturnValue({
251
+ status: 0,
252
+ error: undefined,
253
+ stdout: '',
254
+ stderr: '',
255
+ pid: 1234,
256
+ output: [],
257
+ signal: null,
258
+ });
259
+
260
+ launchInteractiveClaude(worktree, { autoContinue: true });
261
+
262
+ const callArgs = mockedSpawnSync.mock.calls[0][1] as string[];
263
+ expect(callArgs).not.toContain('--continue');
264
+ expect(mockedPrintInfo).toHaveBeenCalledWith(expect.stringContaining('新对话'));
265
+ });
266
+
267
+ it('不传 autoContinue 时即使有会话历史也不追加 --continue', () => {
268
+ mockedGetConfigValue.mockReturnValue('claude');
269
+ mockedExistsSync.mockReturnValue(true);
270
+ mockedReaddirSync.mockReturnValue(['session-abc.jsonl'] as unknown as ReturnType<typeof readdirSync>);
271
+ mockedSpawnSync.mockReturnValue({
272
+ status: 0,
273
+ error: undefined,
274
+ stdout: '',
275
+ stderr: '',
276
+ pid: 1234,
277
+ output: [],
278
+ signal: null,
279
+ });
280
+
281
+ launchInteractiveClaude(worktree);
282
+
283
+ const callArgs = mockedSpawnSync.mock.calls[0][1] as string[];
284
+ expect(callArgs).not.toContain('--continue');
285
+ });
172
286
  });