@akiojin/gwt 4.11.6 → 4.12.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.
Files changed (199) hide show
  1. package/bin/gwt.js +36 -10
  2. package/dist/claude.d.ts +1 -0
  3. package/dist/claude.d.ts.map +1 -1
  4. package/dist/claude.js +81 -24
  5. package/dist/claude.js.map +1 -1
  6. package/dist/cli/ui/App.solid.d.ts.map +1 -1
  7. package/dist/cli/ui/App.solid.js +280 -50
  8. package/dist/cli/ui/App.solid.js.map +1 -1
  9. package/dist/cli/ui/components/solid/QuickStartStep.d.ts.map +1 -1
  10. package/dist/cli/ui/components/solid/QuickStartStep.js +35 -22
  11. package/dist/cli/ui/components/solid/QuickStartStep.js.map +1 -1
  12. package/dist/cli/ui/components/solid/SelectInput.d.ts.map +1 -1
  13. package/dist/cli/ui/components/solid/SelectInput.js +2 -1
  14. package/dist/cli/ui/components/solid/SelectInput.js.map +1 -1
  15. package/dist/cli/ui/components/solid/WizardController.d.ts.map +1 -1
  16. package/dist/cli/ui/components/solid/WizardController.js +67 -13
  17. package/dist/cli/ui/components/solid/WizardController.js.map +1 -1
  18. package/dist/cli/ui/components/solid/WizardSteps.d.ts +5 -0
  19. package/dist/cli/ui/components/solid/WizardSteps.d.ts.map +1 -1
  20. package/dist/cli/ui/components/solid/WizardSteps.js +50 -70
  21. package/dist/cli/ui/components/solid/WizardSteps.js.map +1 -1
  22. package/dist/cli/ui/core/theme.d.ts +9 -0
  23. package/dist/cli/ui/core/theme.d.ts.map +1 -1
  24. package/dist/cli/ui/core/theme.js +21 -0
  25. package/dist/cli/ui/core/theme.js.map +1 -1
  26. package/dist/cli/ui/screens/solid/BranchListScreen.d.ts +9 -2
  27. package/dist/cli/ui/screens/solid/BranchListScreen.d.ts.map +1 -1
  28. package/dist/cli/ui/screens/solid/BranchListScreen.js +101 -28
  29. package/dist/cli/ui/screens/solid/BranchListScreen.js.map +1 -1
  30. package/dist/cli/ui/screens/solid/ConfirmScreen.d.ts +2 -1
  31. package/dist/cli/ui/screens/solid/ConfirmScreen.d.ts.map +1 -1
  32. package/dist/cli/ui/screens/solid/ConfirmScreen.js +11 -3
  33. package/dist/cli/ui/screens/solid/ConfirmScreen.js.map +1 -1
  34. package/dist/cli/ui/screens/solid/EnvironmentScreen.d.ts.map +1 -1
  35. package/dist/cli/ui/screens/solid/EnvironmentScreen.js +9 -10
  36. package/dist/cli/ui/screens/solid/EnvironmentScreen.js.map +1 -1
  37. package/dist/cli/ui/screens/solid/LogScreen.d.ts +7 -1
  38. package/dist/cli/ui/screens/solid/LogScreen.d.ts.map +1 -1
  39. package/dist/cli/ui/screens/solid/LogScreen.js +254 -16
  40. package/dist/cli/ui/screens/solid/LogScreen.js.map +1 -1
  41. package/dist/cli/ui/screens/solid/ProfileEnvScreen.d.ts.map +1 -1
  42. package/dist/cli/ui/screens/solid/ProfileEnvScreen.js +8 -5
  43. package/dist/cli/ui/screens/solid/ProfileEnvScreen.js.map +1 -1
  44. package/dist/cli/ui/screens/solid/SelectorScreen.d.ts.map +1 -1
  45. package/dist/cli/ui/screens/solid/SelectorScreen.js +12 -4
  46. package/dist/cli/ui/screens/solid/SelectorScreen.js.map +1 -1
  47. package/dist/cli/ui/types.d.ts +1 -0
  48. package/dist/cli/ui/types.d.ts.map +1 -1
  49. package/dist/cli/ui/utils/branchFormatter.d.ts +1 -0
  50. package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
  51. package/dist/cli/ui/utils/branchFormatter.js +29 -7
  52. package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
  53. package/dist/cli/ui/utils/continueSession.d.ts +14 -0
  54. package/dist/cli/ui/utils/continueSession.d.ts.map +1 -1
  55. package/dist/cli/ui/utils/continueSession.js +61 -3
  56. package/dist/cli/ui/utils/continueSession.js.map +1 -1
  57. package/dist/cli/ui/utils/installedVersionCache.d.ts +33 -0
  58. package/dist/cli/ui/utils/installedVersionCache.d.ts.map +1 -0
  59. package/dist/cli/ui/utils/installedVersionCache.js +59 -0
  60. package/dist/cli/ui/utils/installedVersionCache.js.map +1 -0
  61. package/dist/cli/ui/utils/modelOptions.d.ts.map +1 -1
  62. package/dist/cli/ui/utils/modelOptions.js +16 -0
  63. package/dist/cli/ui/utils/modelOptions.js.map +1 -1
  64. package/dist/cli/ui/utils/versionCache.d.ts +37 -0
  65. package/dist/cli/ui/utils/versionCache.d.ts.map +1 -0
  66. package/dist/cli/ui/utils/versionCache.js +70 -0
  67. package/dist/cli/ui/utils/versionCache.js.map +1 -0
  68. package/dist/cli/ui/utils/versionFetcher.d.ts +41 -0
  69. package/dist/cli/ui/utils/versionFetcher.d.ts.map +1 -0
  70. package/dist/cli/ui/utils/versionFetcher.js +89 -0
  71. package/dist/cli/ui/utils/versionFetcher.js.map +1 -0
  72. package/dist/codex.d.ts +1 -0
  73. package/dist/codex.d.ts.map +1 -1
  74. package/dist/codex.js +95 -25
  75. package/dist/codex.js.map +1 -1
  76. package/dist/config/index.d.ts.map +1 -1
  77. package/dist/config/index.js +10 -1
  78. package/dist/config/index.js.map +1 -1
  79. package/dist/gemini.d.ts +1 -0
  80. package/dist/gemini.d.ts.map +1 -1
  81. package/dist/gemini.js +36 -3
  82. package/dist/gemini.js.map +1 -1
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +35 -2
  85. package/dist/index.js.map +1 -1
  86. package/dist/launcher.d.ts.map +1 -1
  87. package/dist/launcher.js +43 -8
  88. package/dist/launcher.js.map +1 -1
  89. package/dist/logging/agentOutput.d.ts +21 -0
  90. package/dist/logging/agentOutput.d.ts.map +1 -0
  91. package/dist/logging/agentOutput.js +164 -0
  92. package/dist/logging/agentOutput.js.map +1 -0
  93. package/dist/logging/formatter.d.ts.map +1 -1
  94. package/dist/logging/formatter.js +18 -4
  95. package/dist/logging/formatter.js.map +1 -1
  96. package/dist/logging/logger.d.ts.map +1 -1
  97. package/dist/logging/logger.js +2 -0
  98. package/dist/logging/logger.js.map +1 -1
  99. package/dist/logging/reader.d.ts +22 -0
  100. package/dist/logging/reader.d.ts.map +1 -1
  101. package/dist/logging/reader.js +116 -1
  102. package/dist/logging/reader.js.map +1 -1
  103. package/dist/opentui/index.solid.js +2575 -888
  104. package/dist/services/codingAgentResolver.d.ts.map +1 -1
  105. package/dist/services/codingAgentResolver.js +8 -6
  106. package/dist/services/codingAgentResolver.js.map +1 -1
  107. package/dist/services/dependency-installer.js +2 -2
  108. package/dist/services/dependency-installer.js.map +1 -1
  109. package/dist/shared/codingAgentConstants.d.ts +3 -0
  110. package/dist/shared/codingAgentConstants.d.ts.map +1 -1
  111. package/dist/shared/codingAgentConstants.js +66 -0
  112. package/dist/shared/codingAgentConstants.js.map +1 -1
  113. package/dist/utils/bun-runtime.d.ts +12 -0
  114. package/dist/utils/bun-runtime.d.ts.map +1 -0
  115. package/dist/utils/bun-runtime.js +13 -0
  116. package/dist/utils/bun-runtime.js.map +1 -0
  117. package/dist/utils/session/common.d.ts +8 -0
  118. package/dist/utils/session/common.d.ts.map +1 -1
  119. package/dist/utils/session/common.js +22 -0
  120. package/dist/utils/session/common.js.map +1 -1
  121. package/dist/utils/session/parsers/claude.d.ts +10 -4
  122. package/dist/utils/session/parsers/claude.d.ts.map +1 -1
  123. package/dist/utils/session/parsers/claude.js +64 -18
  124. package/dist/utils/session/parsers/claude.js.map +1 -1
  125. package/dist/utils/session/parsers/codex.d.ts.map +1 -1
  126. package/dist/utils/session/parsers/codex.js +47 -28
  127. package/dist/utils/session/parsers/codex.js.map +1 -1
  128. package/dist/utils/session/parsers/gemini.d.ts.map +1 -1
  129. package/dist/utils/session/parsers/gemini.js +43 -6
  130. package/dist/utils/session/parsers/gemini.js.map +1 -1
  131. package/dist/utils/session/parsers/opencode.d.ts.map +1 -1
  132. package/dist/utils/session/parsers/opencode.js +43 -6
  133. package/dist/utils/session/parsers/opencode.js.map +1 -1
  134. package/dist/utils/session/types.d.ts +7 -0
  135. package/dist/utils/session/types.d.ts.map +1 -1
  136. package/dist/web/client/src/components/ui/alert.d.ts +1 -1
  137. package/dist/worktree.d.ts +4 -1
  138. package/dist/worktree.d.ts.map +1 -1
  139. package/dist/worktree.js +21 -15
  140. package/dist/worktree.js.map +1 -1
  141. package/package.json +2 -1
  142. package/src/claude.ts +99 -28
  143. package/src/cli/ui/App.solid.tsx +373 -51
  144. package/src/cli/ui/__tests__/solid/AppSolid.cleanup.test.tsx +921 -1
  145. package/src/cli/ui/__tests__/solid/BranchListScreen.test.tsx +105 -5
  146. package/src/cli/ui/__tests__/solid/ConfirmScreen.test.tsx +77 -0
  147. package/src/cli/ui/__tests__/solid/LogScreen.test.tsx +351 -0
  148. package/src/cli/ui/__tests__/solid/components/QuickStartStep.test.tsx +73 -2
  149. package/src/cli/ui/__tests__/solid/components/WizardController.test.tsx +71 -0
  150. package/src/cli/ui/__tests__/solid/components/WizardSteps.test.tsx +95 -2
  151. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +72 -45
  152. package/src/cli/ui/components/solid/QuickStartStep.tsx +35 -23
  153. package/src/cli/ui/components/solid/SearchInput.tsx +1 -1
  154. package/src/cli/ui/components/solid/SelectInput.tsx +4 -0
  155. package/src/cli/ui/components/solid/WizardController.tsx +85 -12
  156. package/src/cli/ui/components/solid/WizardSteps.tsx +78 -90
  157. package/src/cli/ui/core/theme.ts +32 -0
  158. package/src/cli/ui/hooks/solid/useAsyncOperation.ts +8 -6
  159. package/src/cli/ui/hooks/solid/useGitOperations.ts +6 -5
  160. package/src/cli/ui/screens/solid/BranchListScreen.tsx +135 -32
  161. package/src/cli/ui/screens/solid/ConfirmScreen.tsx +20 -8
  162. package/src/cli/ui/screens/solid/EnvironmentScreen.tsx +22 -20
  163. package/src/cli/ui/screens/solid/LogScreen.tsx +364 -35
  164. package/src/cli/ui/screens/solid/ProfileEnvScreen.tsx +19 -15
  165. package/src/cli/ui/screens/solid/SelectorScreen.tsx +25 -14
  166. package/src/cli/ui/screens/solid/SettingsScreen.tsx +5 -3
  167. package/src/cli/ui/types.ts +1 -0
  168. package/src/cli/ui/utils/__tests__/branchFormatter.test.ts +53 -6
  169. package/src/cli/ui/utils/__tests__/installedVersionCache.test.ts +46 -0
  170. package/src/cli/ui/utils/branchFormatter.ts +35 -7
  171. package/src/cli/ui/utils/continueSession.ts +90 -3
  172. package/src/cli/ui/utils/installedVersionCache.ts +84 -0
  173. package/src/cli/ui/utils/modelOptions.test.ts +6 -0
  174. package/src/cli/ui/utils/modelOptions.ts +16 -0
  175. package/src/cli/ui/utils/versionCache.ts +93 -0
  176. package/src/cli/ui/utils/versionFetcher.ts +120 -0
  177. package/src/codex.ts +124 -26
  178. package/src/config/__tests__/saveSession.test.ts +2 -2
  179. package/src/config/index.ts +11 -1
  180. package/src/gemini.ts +50 -4
  181. package/src/index.test.ts +16 -10
  182. package/src/index.ts +41 -1
  183. package/src/launcher.ts +49 -8
  184. package/src/logging/agentOutput.ts +216 -0
  185. package/src/logging/formatter.ts +23 -4
  186. package/src/logging/logger.ts +2 -0
  187. package/src/logging/reader.ts +165 -1
  188. package/src/services/__tests__/BatchMergeService.test.ts +34 -14
  189. package/src/services/codingAgentResolver.ts +12 -5
  190. package/src/services/dependency-installer.ts +2 -2
  191. package/src/shared/codingAgentConstants.ts +73 -0
  192. package/src/utils/bun-runtime.ts +29 -0
  193. package/src/utils/session/common.ts +28 -0
  194. package/src/utils/session/parsers/claude.ts +79 -29
  195. package/src/utils/session/parsers/codex.ts +49 -26
  196. package/src/utils/session/parsers/gemini.ts +46 -5
  197. package/src/utils/session/parsers/opencode.ts +46 -5
  198. package/src/utils/session/types.ts +4 -0
  199. package/src/worktree.ts +28 -15
@@ -5,10 +5,13 @@ import { CLAUDE_CODE_TOOL } from "../config/builtin-coding-agents.js";
5
5
  import {
6
6
  CODEX_DEFAULT_ARGS,
7
7
  CLAUDE_PERMISSION_SKIP_ARGS,
8
+ shouldEnableCodexSkillsFlag,
9
+ withCodexSkillsFlag,
8
10
  } from "../shared/codingAgentConstants.js";
9
11
  import { prepareCodingAgentExecution } from "./codingAgentCommandResolver.js";
10
12
  import type { CodingAgentLaunchOptions } from "../types/tools.js";
11
13
  import { createLogger } from "../logging/logger.js";
14
+ import { findCommand } from "../utils/command.js";
12
15
 
13
16
  const logger = createLogger({ category: "resolver" });
14
17
 
@@ -267,17 +270,21 @@ export function buildCodexArgs(options: CodexCommandOptions = {}): string[] {
267
270
  export async function resolveCodexCommand(
268
271
  options: CodexCommandOptions = {},
269
272
  ): Promise<ResolvedCommand> {
270
- const args = buildCodexArgs(options);
273
+ const codexLookup = await findCommand("codex");
274
+ const baseArgs = buildCodexArgs(options);
275
+ const args = withCodexSkillsFlag(
276
+ baseArgs,
277
+ shouldEnableCodexSkillsFlag(codexLookup.version),
278
+ );
271
279
 
272
280
  // フルパスを取得(node-ptyはシェルを経由しないため必要)
273
- const codexPath = await resolveCommandPath("codex");
274
- if (codexPath) {
281
+ if (codexLookup.source === "installed" && codexLookup.path) {
275
282
  logger.info(
276
- { command: codexPath, usesFallback: false },
283
+ { command: codexLookup.path, usesFallback: false },
277
284
  "Codex command resolved",
278
285
  );
279
286
  return {
280
- command: codexPath,
287
+ command: codexLookup.path,
281
288
  args,
282
289
  usesFallback: false,
283
290
  };
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import fs from "node:fs/promises";
2
+ import { access } from "node:fs/promises";
3
3
  import { execa } from "execa";
4
4
 
5
5
  export type PackageManager = "bun" | "pnpm" | "npm";
@@ -72,7 +72,7 @@ const INSTALL_CANDIDATES: PackageManagerCandidate[] = [
72
72
 
73
73
  async function fileExists(targetPath: string): Promise<boolean> {
74
74
  try {
75
- await fs.access(targetPath);
75
+ await access(targetPath);
76
76
  return true;
77
77
  } catch (error) {
78
78
  const code = (error as NodeJS.ErrnoException)?.code;
@@ -28,3 +28,76 @@ export const CODEX_DEFAULT_ARGS = [
28
28
  "-c",
29
29
  "shell_environment_policy.experimental_use_profile=true",
30
30
  ] as const;
31
+
32
+ export const CODEX_SKILLS_FLAG_DEPRECATED_FROM = "0.80.0";
33
+
34
+ type ParsedVersion = {
35
+ major: number;
36
+ minor: number;
37
+ patch: number;
38
+ prerelease: string | null;
39
+ };
40
+
41
+ const MODEL_FLAG_PREFIX = "--model=";
42
+
43
+ function normalizeVersion(value?: string | null): string | null {
44
+ if (!value) return null;
45
+ const trimmed = value.trim();
46
+ if (!trimmed) return null;
47
+ return trimmed.replace(/^v/i, "");
48
+ }
49
+
50
+ function parseVersion(value?: string | null): ParsedVersion | null {
51
+ const normalized = normalizeVersion(value);
52
+ if (!normalized) return null;
53
+ const match = normalized.match(
54
+ /^(\d+)\.(\d+)(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?$/,
55
+ );
56
+ if (!match) return null;
57
+ const major = Number(match[1]);
58
+ const minor = Number(match[2]);
59
+ const patch = Number(match[3] ?? "0");
60
+ if (![major, minor, patch].every(Number.isFinite)) return null;
61
+ return {
62
+ major,
63
+ minor,
64
+ patch,
65
+ prerelease: match[4] ?? null,
66
+ };
67
+ }
68
+
69
+ function compareVersions(a: ParsedVersion, b: ParsedVersion): number {
70
+ if (a.major !== b.major) return a.major - b.major;
71
+ if (a.minor !== b.minor) return a.minor - b.minor;
72
+ if (a.patch !== b.patch) return a.patch - b.patch;
73
+ if (a.prerelease && !b.prerelease) return -1;
74
+ if (!a.prerelease && b.prerelease) return 1;
75
+ if (a.prerelease && b.prerelease) {
76
+ return a.prerelease.localeCompare(b.prerelease);
77
+ }
78
+ return 0;
79
+ }
80
+
81
+ export function shouldEnableCodexSkillsFlag(version?: string | null): boolean {
82
+ const parsed = parseVersion(version);
83
+ if (!parsed) return false;
84
+ const threshold = parseVersion(CODEX_SKILLS_FLAG_DEPRECATED_FROM);
85
+ if (!threshold) return false;
86
+ return compareVersions(parsed, threshold) < 0;
87
+ }
88
+
89
+ export function withCodexSkillsFlag(
90
+ args: readonly string[],
91
+ enable: boolean,
92
+ ): string[] {
93
+ if (!enable) return Array.from(args);
94
+ const alreadyEnabled = args.some(
95
+ (arg, index) => arg === "--enable" && args[index + 1] === "skills",
96
+ );
97
+ if (alreadyEnabled) return Array.from(args);
98
+ const next = Array.from(args);
99
+ const modelIndex = next.findIndex((arg) => arg.startsWith(MODEL_FLAG_PREFIX));
100
+ const insertIndex = modelIndex === -1 ? next.length : modelIndex;
101
+ next.splice(insertIndex, 0, "--enable", "skills");
102
+ return next;
103
+ }
@@ -0,0 +1,29 @@
1
+ export interface BunReexecInput {
2
+ hasBunGlobal: boolean;
3
+ bunExecPath?: string | null;
4
+ argv: string[];
5
+ scriptPath: string;
6
+ }
7
+
8
+ export interface BunReexecCommand {
9
+ command: string;
10
+ args: string[];
11
+ }
12
+
13
+ export function buildBunReexecCommand(
14
+ input: BunReexecInput,
15
+ ): BunReexecCommand | null {
16
+ if (input.hasBunGlobal) {
17
+ return null;
18
+ }
19
+
20
+ const scriptPath = input.scriptPath?.trim();
21
+ if (!scriptPath) {
22
+ return null;
23
+ }
24
+
25
+ const bunCommand = input.bunExecPath?.trim() || "bun";
26
+ const args = [scriptPath, ...(input.argv?.slice(2) ?? [])];
27
+
28
+ return { command: bunCommand, args };
29
+ }
@@ -444,3 +444,31 @@ export function matchesCwd(
444
444
  isPathPrefix(normalizedSession, normalizedTarget)
445
445
  );
446
446
  }
447
+
448
+ /**
449
+ * Resolves a branch name for a session cwd using worktree paths.
450
+ * Picks the worktree with the longest matching path prefix.
451
+ */
452
+ export function resolveBranchFromCwd(
453
+ sessionCwd: string | null,
454
+ worktrees: { path: string; branch: string }[],
455
+ ): string | null {
456
+ if (!sessionCwd) return null;
457
+ const normalizedSession = normalizePath(sessionCwd);
458
+ let bestMatch: { branch: string; path: string } | null = null;
459
+
460
+ for (const worktree of worktrees) {
461
+ if (!worktree?.path || !worktree.branch) continue;
462
+ const normalizedPath = normalizePath(worktree.path);
463
+ if (
464
+ normalizedSession === normalizedPath ||
465
+ isPathPrefix(normalizedPath, normalizedSession)
466
+ ) {
467
+ if (!bestMatch || normalizedPath.length > bestMatch.path.length) {
468
+ bestMatch = { branch: worktree.branch, path: normalizedPath };
469
+ }
470
+ }
471
+ }
472
+
473
+ return bestMatch?.branch ?? null;
474
+ }
@@ -16,6 +16,7 @@ import {
16
16
  readFileContent,
17
17
  checkFileStat,
18
18
  } from "../common.js";
19
+ import { listAllWorktrees } from "../../../worktree.js";
19
20
 
20
21
  /**
21
22
  * Encodes a project path for Claude's directory structure.
@@ -75,36 +76,72 @@ function getClaudeRootCandidates(): string[] {
75
76
  * 3. ~/.claude/history.jsonl (global history fallback)
76
77
  *
77
78
  * @param cwd - The working directory to find sessions for
78
- * @param options - Search options (since, until, preferClosestTo, windowMs)
79
+ * @param options - Search options (since, until, preferClosestTo, windowMs, branch/worktrees)
79
80
  * @returns Session info with ID and modification time, or null if not found
80
81
  */
81
82
  export async function findLatestClaudeSession(
82
83
  cwd: string,
83
- options: Omit<SessionSearchOptions, "cwd"> = {},
84
+ options: SessionSearchOptions = {},
84
85
  ): Promise<ClaudeSessionInfo | null> {
85
86
  const rootCandidates = getClaudeRootCandidates();
86
- const encodedPaths = generateClaudeProjectPathCandidates(cwd);
87
-
88
- for (const claudeRoot of rootCandidates) {
89
- for (const encoded of encodedPaths) {
90
- const projectDir = path.join(claudeRoot, "projects", encoded);
91
- const sessionsDir = path.join(projectDir, "sessions");
92
-
93
- // 1) Look under sessions/ (official location)
94
- const session = await findNewestSessionIdFromDir(
95
- sessionsDir,
96
- false,
97
- options,
98
- );
99
- if (session) return session;
87
+ const branchFilter =
88
+ typeof options.branch === "string" && options.branch.trim().length > 0
89
+ ? options.branch.trim()
90
+ : null;
100
91
 
101
- // 2) Look directly under project dir and subdirs
102
- const rootSession = await findNewestSessionIdFromDir(
103
- projectDir,
104
- true,
105
- options,
106
- );
107
- if (rootSession) return rootSession;
92
+ let cwdCandidates: string[] = [];
93
+ if (branchFilter) {
94
+ let worktrees: { path: string; branch: string }[] = [];
95
+ if (Array.isArray(options.worktrees) && options.worktrees.length > 0) {
96
+ worktrees = options.worktrees
97
+ .filter((entry) => entry?.path && entry?.branch)
98
+ .map((entry) => ({ path: entry.path, branch: entry.branch }));
99
+ } else {
100
+ try {
101
+ const allWorktrees = await listAllWorktrees();
102
+ worktrees = allWorktrees
103
+ .filter((entry) => entry?.path && entry?.branch)
104
+ .map((entry) => ({ path: entry.path, branch: entry.branch }));
105
+ } catch {
106
+ worktrees = [];
107
+ }
108
+ }
109
+ const matches = worktrees
110
+ .filter((entry) => entry.branch === branchFilter)
111
+ .map((entry) => entry.path);
112
+ if (!matches.length) return null;
113
+ cwdCandidates = matches;
114
+ } else {
115
+ const baseCwd = options.cwd ?? cwd;
116
+ if (!baseCwd) return null;
117
+ cwdCandidates = [baseCwd];
118
+ }
119
+
120
+ const uniqueCwds = Array.from(new Set(cwdCandidates));
121
+
122
+ for (const candidateCwd of uniqueCwds) {
123
+ const encodedPaths = generateClaudeProjectPathCandidates(candidateCwd);
124
+ for (const claudeRoot of rootCandidates) {
125
+ for (const encoded of encodedPaths) {
126
+ const projectDir = path.join(claudeRoot, "projects", encoded);
127
+ const sessionsDir = path.join(projectDir, "sessions");
128
+
129
+ // 1) Look under sessions/ (official location)
130
+ const session = await findNewestSessionIdFromDir(
131
+ sessionsDir,
132
+ false,
133
+ options,
134
+ );
135
+ if (session) return session;
136
+
137
+ // 2) Look directly under project dir and subdirs
138
+ const rootSession = await findNewestSessionIdFromDir(
139
+ projectDir,
140
+ true,
141
+ options,
142
+ );
143
+ if (rootSession) return rootSession;
144
+ }
108
145
  }
109
146
  }
110
147
 
@@ -124,7 +161,10 @@ export async function findLatestClaudeSession(
124
161
  typeof parsed.project === "string" ? parsed.project : null;
125
162
  const sessionId =
126
163
  typeof parsed.sessionId === "string" ? parsed.sessionId : null;
127
- if (project && sessionId && matchesCwd(project, cwd)) {
164
+ const matchesProject = uniqueCwds.some((candidate) =>
165
+ matchesCwd(project, candidate),
166
+ );
167
+ if (project && sessionId && matchesProject) {
128
168
  return { id: sessionId, mtime: historyStat.mtimeMs };
129
169
  }
130
170
  } catch {
@@ -141,14 +181,14 @@ export async function findLatestClaudeSession(
141
181
  /**
142
182
  * Finds the latest Claude session ID for a given working directory.
143
183
  * @param cwd - The working directory to find sessions for
144
- * @param options - Search options (since, until, preferClosestTo, windowMs)
184
+ * @param options - Search options (since, until, preferClosestTo, windowMs, branch/worktrees)
145
185
  * @returns Session ID string or null if not found
146
186
  */
147
187
  export async function findLatestClaudeSessionId(
148
188
  cwd: string,
149
- options: Omit<SessionSearchOptions, "cwd"> = {},
189
+ options: SessionSearchOptions = {},
150
190
  ): Promise<string | null> {
151
- const found = await findLatestClaudeSession(cwd, options);
191
+ const found = await findLatestClaudeSession(options.cwd ?? cwd, options);
152
192
  return found?.id ?? null;
153
193
  }
154
194
 
@@ -167,6 +207,9 @@ export async function waitForClaudeSessionId(
167
207
  until?: number;
168
208
  preferClosestTo?: number;
169
209
  windowMs?: number;
210
+ branch?: string | null;
211
+ worktrees?: { path: string; branch: string }[] | null;
212
+ cwd?: string | null;
170
213
  } = {},
171
214
  ): Promise<string | null> {
172
215
  const timeoutMs = options.timeoutMs ?? 120_000;
@@ -174,15 +217,22 @@ export async function waitForClaudeSessionId(
174
217
  const deadline = Date.now() + timeoutMs;
175
218
 
176
219
  // Build search options once outside the loop
177
- const searchOptions: Omit<SessionSearchOptions, "cwd"> = {};
220
+ const searchOptions: SessionSearchOptions = {};
178
221
  if (options.since !== undefined) searchOptions.since = options.since;
179
222
  if (options.until !== undefined) searchOptions.until = options.until;
180
223
  if (options.preferClosestTo !== undefined)
181
224
  searchOptions.preferClosestTo = options.preferClosestTo;
182
225
  if (options.windowMs !== undefined) searchOptions.windowMs = options.windowMs;
226
+ if (options.branch !== undefined) searchOptions.branch = options.branch;
227
+ if (options.worktrees !== undefined)
228
+ searchOptions.worktrees = options.worktrees;
229
+ if (options.cwd !== undefined) searchOptions.cwd = options.cwd;
183
230
 
184
231
  while (Date.now() < deadline) {
185
- const found = await findLatestClaudeSession(cwd, searchOptions);
232
+ const found = await findLatestClaudeSession(
233
+ options.cwd ?? cwd,
234
+ searchOptions,
235
+ );
186
236
  if (found?.id) return found.id;
187
237
  await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
188
238
  }
@@ -14,8 +14,10 @@ import {
14
14
  UUID_REGEX,
15
15
  collectFilesIterative,
16
16
  matchesCwd,
17
+ resolveBranchFromCwd,
17
18
  readSessionInfoFromFile,
18
19
  } from "../common.js";
20
+ import { listAllWorktrees } from "../../../worktree.js";
19
21
 
20
22
  /**
21
23
  * Finds the latest Codex session with optional time filtering and cwd matching.
@@ -63,43 +65,64 @@ export async function findLatestCodexSession(
63
65
  return b.mtime - a.mtime;
64
66
  });
65
67
 
66
- let fallbackMissingCwd: CodexSessionInfo | null = null;
68
+ const branchFilter =
69
+ typeof options.branch === "string" && options.branch.trim().length > 0
70
+ ? options.branch.trim()
71
+ : null;
72
+ const shouldCheckBranch = Boolean(branchFilter);
73
+ const shouldCheckCwd = Boolean(options.cwd) && !shouldCheckBranch;
74
+
75
+ let worktrees: { path: string; branch: string }[] = [];
76
+ if (shouldCheckBranch) {
77
+ if (Array.isArray(options.worktrees) && options.worktrees.length > 0) {
78
+ worktrees = options.worktrees
79
+ .filter((entry) => entry?.path && entry?.branch)
80
+ .map((entry) => ({ path: entry.path, branch: entry.branch }));
81
+ } else {
82
+ try {
83
+ const allWorktrees = await listAllWorktrees();
84
+ worktrees = allWorktrees
85
+ .filter((entry) => entry?.path && entry?.branch)
86
+ .map((entry) => ({ path: entry.path, branch: entry.branch }));
87
+ } catch {
88
+ worktrees = [];
89
+ }
90
+ }
91
+ if (!worktrees.length) return null;
92
+ }
67
93
 
68
94
  for (const file of ordered) {
69
95
  // Priority 1: Extract session ID from filename (most reliable for Codex)
70
96
  const filenameMatch = path.basename(file.fullPath).match(UUID_REGEX);
71
- if (filenameMatch) {
72
- const sessionId = filenameMatch[0];
73
- // If cwd filtering is needed, read file content to check cwd
74
- if (options.cwd) {
75
- const info = await readSessionInfoFromFile(file.fullPath);
76
- if (matchesCwd(info.cwd, options.cwd)) {
77
- return { id: sessionId, mtime: file.mtime };
78
- }
79
- if (!info.cwd && !fallbackMissingCwd) {
80
- fallbackMissingCwd = { id: sessionId, mtime: file.mtime };
81
- }
82
- continue; // cwd doesn't match, try next file
97
+ const idFromName = filenameMatch?.[0] ?? null;
98
+ const needsInfo = shouldCheckBranch || shouldCheckCwd || !idFromName;
99
+ const info = needsInfo
100
+ ? await readSessionInfoFromFile(file.fullPath)
101
+ : null;
102
+ const sessionCwd = info?.cwd ?? null;
103
+
104
+ if (shouldCheckBranch) {
105
+ const resolvedBranch = resolveBranchFromCwd(sessionCwd, worktrees);
106
+ if (resolvedBranch !== branchFilter) {
107
+ continue;
83
108
  }
84
- return { id: sessionId, mtime: file.mtime };
85
109
  }
86
110
 
87
- // Priority 2: Fallback to reading file content if filename lacks UUID
88
- const info = await readSessionInfoFromFile(file.fullPath);
89
- if (!info.id) continue;
90
- if (options.cwd) {
91
- if (matchesCwd(info.cwd, options.cwd)) {
92
- return { id: info.id, mtime: file.mtime };
111
+ if (shouldCheckCwd && options.cwd) {
112
+ if (!matchesCwd(sessionCwd, options.cwd)) {
113
+ continue;
93
114
  }
94
- if (!info.cwd && !fallbackMissingCwd) {
95
- fallbackMissingCwd = { id: info.id, mtime: file.mtime };
96
- }
97
- continue;
98
115
  }
99
- return { id: info.id, mtime: file.mtime };
116
+
117
+ const sessionId = idFromName ?? info?.id ?? null;
118
+ if (sessionId) {
119
+ return { id: sessionId, mtime: file.mtime };
120
+ }
121
+
122
+ // (already handled via info above)
100
123
  }
101
124
 
102
- return fallbackMissingCwd;
125
+ return null;
103
126
  }
104
127
 
105
128
  /**
@@ -12,8 +12,10 @@ import type { GeminiSessionInfo, SessionSearchOptions } from "../types.js";
12
12
  import {
13
13
  collectFilesIterative,
14
14
  matchesCwd,
15
+ resolveBranchFromCwd,
15
16
  readSessionInfoFromFile,
16
17
  } from "../common.js";
18
+ import { listAllWorktrees } from "../../../worktree.js";
17
19
 
18
20
  /**
19
21
  * Finds the latest Gemini session with optional time filtering and cwd matching.
@@ -57,15 +59,50 @@ export async function findLatestGeminiSession(
57
59
  return b.mtime - a.mtime;
58
60
  });
59
61
 
62
+ const branchFilter =
63
+ typeof options.branch === "string" && options.branch.trim().length > 0
64
+ ? options.branch.trim()
65
+ : null;
66
+ const shouldCheckBranch = Boolean(branchFilter);
67
+ const shouldCheckCwd = Boolean(options.cwd) && !shouldCheckBranch;
68
+
69
+ let worktrees: { path: string; branch: string }[] = [];
70
+ if (shouldCheckBranch) {
71
+ if (Array.isArray(options.worktrees) && options.worktrees.length > 0) {
72
+ worktrees = options.worktrees
73
+ .filter((entry) => entry?.path && entry?.branch)
74
+ .map((entry) => ({ path: entry.path, branch: entry.branch }));
75
+ } else {
76
+ try {
77
+ const allWorktrees = await listAllWorktrees();
78
+ worktrees = allWorktrees
79
+ .filter((entry) => entry?.path && entry?.branch)
80
+ .map((entry) => ({ path: entry.path, branch: entry.branch }));
81
+ } catch {
82
+ worktrees = [];
83
+ }
84
+ }
85
+ if (!worktrees.length) return null;
86
+ }
87
+
60
88
  for (const file of pool) {
61
89
  const info = await readSessionInfoFromFile(file.fullPath);
62
90
  if (!info.id) continue;
63
- if (options.cwd) {
64
- if (matchesCwd(info.cwd, options.cwd)) {
65
- return { id: info.id, mtime: file.mtime };
91
+ const sessionCwd = info.cwd ?? null;
92
+
93
+ if (shouldCheckBranch) {
94
+ const resolvedBranch = resolveBranchFromCwd(sessionCwd, worktrees);
95
+ if (resolvedBranch !== branchFilter) {
96
+ continue;
66
97
  }
67
- continue;
68
98
  }
99
+
100
+ if (shouldCheckCwd && options.cwd) {
101
+ if (!matchesCwd(sessionCwd, options.cwd)) {
102
+ continue;
103
+ }
104
+ }
105
+
69
106
  return { id: info.id, mtime: file.mtime };
70
107
  }
71
108
 
@@ -82,7 +119,11 @@ export async function findLatestGeminiSessionId(
82
119
  cwd: string,
83
120
  options: SessionSearchOptions = {},
84
121
  ): Promise<string | null> {
85
- const searchOptions: SessionSearchOptions = { cwd: options.cwd ?? cwd };
122
+ const searchOptions: SessionSearchOptions = {
123
+ cwd: options.cwd ?? cwd,
124
+ branch: options.branch ?? null,
125
+ worktrees: options.worktrees ?? null,
126
+ };
86
127
  if (options.since !== undefined) searchOptions.since = options.since;
87
128
  if (options.until !== undefined) searchOptions.until = options.until;
88
129
  if (options.preferClosestTo !== undefined)
@@ -13,8 +13,10 @@ import type { OpenCodeSessionInfo, SessionSearchOptions } from "../types.js";
13
13
  import {
14
14
  collectFilesIterative,
15
15
  matchesCwd,
16
+ resolveBranchFromCwd,
16
17
  readSessionInfoFromFile,
17
18
  } from "../common.js";
19
+ import { listAllWorktrees } from "../../../worktree.js";
18
20
 
19
21
  /**
20
22
  * Returns the OpenCode session directory
@@ -73,15 +75,50 @@ export async function findLatestOpenCodeSession(
73
75
  return b.mtime - a.mtime;
74
76
  });
75
77
 
78
+ const branchFilter =
79
+ typeof options.branch === "string" && options.branch.trim().length > 0
80
+ ? options.branch.trim()
81
+ : null;
82
+ const shouldCheckBranch = Boolean(branchFilter);
83
+ const shouldCheckCwd = Boolean(options.cwd) && !shouldCheckBranch;
84
+
85
+ let worktrees: { path: string; branch: string }[] = [];
86
+ if (shouldCheckBranch) {
87
+ if (Array.isArray(options.worktrees) && options.worktrees.length > 0) {
88
+ worktrees = options.worktrees
89
+ .filter((entry) => entry?.path && entry?.branch)
90
+ .map((entry) => ({ path: entry.path, branch: entry.branch }));
91
+ } else {
92
+ try {
93
+ const allWorktrees = await listAllWorktrees();
94
+ worktrees = allWorktrees
95
+ .filter((entry) => entry?.path && entry?.branch)
96
+ .map((entry) => ({ path: entry.path, branch: entry.branch }));
97
+ } catch {
98
+ worktrees = [];
99
+ }
100
+ }
101
+ if (!worktrees.length) return null;
102
+ }
103
+
76
104
  for (const file of pool) {
77
105
  const info = await readSessionInfoFromFile(file.fullPath);
78
106
  if (!info.id) continue;
79
- if (options.cwd) {
80
- if (matchesCwd(info.cwd, options.cwd)) {
81
- return { id: info.id, mtime: file.mtime };
107
+ const sessionCwd = info.cwd ?? null;
108
+
109
+ if (shouldCheckBranch) {
110
+ const resolvedBranch = resolveBranchFromCwd(sessionCwd, worktrees);
111
+ if (resolvedBranch !== branchFilter) {
112
+ continue;
82
113
  }
83
- continue;
84
114
  }
115
+
116
+ if (shouldCheckCwd && options.cwd) {
117
+ if (!matchesCwd(sessionCwd, options.cwd)) {
118
+ continue;
119
+ }
120
+ }
121
+
85
122
  return { id: info.id, mtime: file.mtime };
86
123
  }
87
124
 
@@ -98,7 +135,11 @@ export async function findLatestOpenCodeSessionId(
98
135
  cwd: string,
99
136
  options: SessionSearchOptions = {},
100
137
  ): Promise<string | null> {
101
- const searchOptions: SessionSearchOptions = { cwd: options.cwd ?? cwd };
138
+ const searchOptions: SessionSearchOptions = {
139
+ cwd: options.cwd ?? cwd,
140
+ branch: options.branch ?? null,
141
+ worktrees: options.worktrees ?? null,
142
+ };
102
143
  if (options.since !== undefined) searchOptions.since = options.since;
103
144
  if (options.until !== undefined) searchOptions.until = options.until;
104
145
  if (options.preferClosestTo !== undefined)
@@ -16,6 +16,10 @@ export interface SessionSearchOptions {
16
16
  windowMs?: number;
17
17
  /** Working directory filter */
18
18
  cwd?: string | null;
19
+ /** Branch name filter (resolved from session cwd via worktree paths) */
20
+ branch?: string | null;
21
+ /** Optional worktree list to avoid shelling out for branch mapping */
22
+ worktrees?: { path: string; branch: string }[] | null;
19
23
  }
20
24
 
21
25
  /**