@akiojin/gwt 4.11.6 → 4.12.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 (172) hide show
  1. package/bin/gwt.js +1 -1
  2. package/dist/claude.d.ts +1 -0
  3. package/dist/claude.d.ts.map +1 -1
  4. package/dist/claude.js +50 -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 +247 -49
  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 +19 -11
  17. package/dist/cli/ui/components/solid/WizardController.js.map +1 -1
  18. package/dist/cli/ui/components/solid/WizardSteps.d.ts.map +1 -1
  19. package/dist/cli/ui/components/solid/WizardSteps.js +26 -69
  20. package/dist/cli/ui/components/solid/WizardSteps.js.map +1 -1
  21. package/dist/cli/ui/core/theme.d.ts +9 -0
  22. package/dist/cli/ui/core/theme.d.ts.map +1 -1
  23. package/dist/cli/ui/core/theme.js +21 -0
  24. package/dist/cli/ui/core/theme.js.map +1 -1
  25. package/dist/cli/ui/screens/solid/BranchListScreen.d.ts +9 -2
  26. package/dist/cli/ui/screens/solid/BranchListScreen.d.ts.map +1 -1
  27. package/dist/cli/ui/screens/solid/BranchListScreen.js +101 -28
  28. package/dist/cli/ui/screens/solid/BranchListScreen.js.map +1 -1
  29. package/dist/cli/ui/screens/solid/ConfirmScreen.d.ts +2 -1
  30. package/dist/cli/ui/screens/solid/ConfirmScreen.d.ts.map +1 -1
  31. package/dist/cli/ui/screens/solid/ConfirmScreen.js +11 -3
  32. package/dist/cli/ui/screens/solid/ConfirmScreen.js.map +1 -1
  33. package/dist/cli/ui/screens/solid/EnvironmentScreen.d.ts.map +1 -1
  34. package/dist/cli/ui/screens/solid/EnvironmentScreen.js +9 -10
  35. package/dist/cli/ui/screens/solid/EnvironmentScreen.js.map +1 -1
  36. package/dist/cli/ui/screens/solid/LogScreen.d.ts +7 -1
  37. package/dist/cli/ui/screens/solid/LogScreen.d.ts.map +1 -1
  38. package/dist/cli/ui/screens/solid/LogScreen.js +254 -16
  39. package/dist/cli/ui/screens/solid/LogScreen.js.map +1 -1
  40. package/dist/cli/ui/screens/solid/ProfileEnvScreen.d.ts.map +1 -1
  41. package/dist/cli/ui/screens/solid/ProfileEnvScreen.js +8 -5
  42. package/dist/cli/ui/screens/solid/ProfileEnvScreen.js.map +1 -1
  43. package/dist/cli/ui/screens/solid/SelectorScreen.d.ts.map +1 -1
  44. package/dist/cli/ui/screens/solid/SelectorScreen.js +12 -4
  45. package/dist/cli/ui/screens/solid/SelectorScreen.js.map +1 -1
  46. package/dist/cli/ui/types.d.ts +1 -0
  47. package/dist/cli/ui/types.d.ts.map +1 -1
  48. package/dist/cli/ui/utils/branchFormatter.d.ts +1 -0
  49. package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
  50. package/dist/cli/ui/utils/branchFormatter.js +29 -7
  51. package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
  52. package/dist/cli/ui/utils/continueSession.d.ts +14 -0
  53. package/dist/cli/ui/utils/continueSession.d.ts.map +1 -1
  54. package/dist/cli/ui/utils/continueSession.js +61 -3
  55. package/dist/cli/ui/utils/continueSession.js.map +1 -1
  56. package/dist/cli/ui/utils/versionCache.d.ts +37 -0
  57. package/dist/cli/ui/utils/versionCache.d.ts.map +1 -0
  58. package/dist/cli/ui/utils/versionCache.js +70 -0
  59. package/dist/cli/ui/utils/versionCache.js.map +1 -0
  60. package/dist/cli/ui/utils/versionFetcher.d.ts +41 -0
  61. package/dist/cli/ui/utils/versionFetcher.d.ts.map +1 -0
  62. package/dist/cli/ui/utils/versionFetcher.js +89 -0
  63. package/dist/cli/ui/utils/versionFetcher.js.map +1 -0
  64. package/dist/codex.d.ts +1 -0
  65. package/dist/codex.d.ts.map +1 -1
  66. package/dist/codex.js +48 -19
  67. package/dist/codex.js.map +1 -1
  68. package/dist/config/index.d.ts.map +1 -1
  69. package/dist/config/index.js +10 -1
  70. package/dist/config/index.js.map +1 -1
  71. package/dist/gemini.d.ts +1 -0
  72. package/dist/gemini.d.ts.map +1 -1
  73. package/dist/gemini.js +36 -3
  74. package/dist/gemini.js.map +1 -1
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +32 -2
  77. package/dist/index.js.map +1 -1
  78. package/dist/launcher.d.ts.map +1 -1
  79. package/dist/launcher.js +43 -8
  80. package/dist/launcher.js.map +1 -1
  81. package/dist/logging/agentOutput.d.ts +21 -0
  82. package/dist/logging/agentOutput.d.ts.map +1 -0
  83. package/dist/logging/agentOutput.js +164 -0
  84. package/dist/logging/agentOutput.js.map +1 -0
  85. package/dist/logging/formatter.d.ts.map +1 -1
  86. package/dist/logging/formatter.js +18 -4
  87. package/dist/logging/formatter.js.map +1 -1
  88. package/dist/logging/logger.d.ts.map +1 -1
  89. package/dist/logging/logger.js +2 -0
  90. package/dist/logging/logger.js.map +1 -1
  91. package/dist/logging/reader.d.ts +21 -0
  92. package/dist/logging/reader.d.ts.map +1 -1
  93. package/dist/logging/reader.js +79 -0
  94. package/dist/logging/reader.js.map +1 -1
  95. package/dist/opentui/index.solid.js +2306 -653
  96. package/dist/services/dependency-installer.js +2 -2
  97. package/dist/services/dependency-installer.js.map +1 -1
  98. package/dist/utils/session/common.d.ts +8 -0
  99. package/dist/utils/session/common.d.ts.map +1 -1
  100. package/dist/utils/session/common.js +22 -0
  101. package/dist/utils/session/common.js.map +1 -1
  102. package/dist/utils/session/parsers/claude.d.ts +10 -4
  103. package/dist/utils/session/parsers/claude.d.ts.map +1 -1
  104. package/dist/utils/session/parsers/claude.js +64 -18
  105. package/dist/utils/session/parsers/claude.js.map +1 -1
  106. package/dist/utils/session/parsers/codex.d.ts.map +1 -1
  107. package/dist/utils/session/parsers/codex.js +48 -28
  108. package/dist/utils/session/parsers/codex.js.map +1 -1
  109. package/dist/utils/session/parsers/gemini.d.ts.map +1 -1
  110. package/dist/utils/session/parsers/gemini.js +43 -6
  111. package/dist/utils/session/parsers/gemini.js.map +1 -1
  112. package/dist/utils/session/parsers/opencode.d.ts.map +1 -1
  113. package/dist/utils/session/parsers/opencode.js +43 -6
  114. package/dist/utils/session/parsers/opencode.js.map +1 -1
  115. package/dist/utils/session/types.d.ts +7 -0
  116. package/dist/utils/session/types.d.ts.map +1 -1
  117. package/dist/web/client/src/components/ui/alert.d.ts +1 -1
  118. package/dist/worktree.d.ts +4 -1
  119. package/dist/worktree.d.ts.map +1 -1
  120. package/dist/worktree.js +21 -15
  121. package/dist/worktree.js.map +1 -1
  122. package/package.json +2 -1
  123. package/src/claude.ts +64 -28
  124. package/src/cli/ui/App.solid.tsx +324 -51
  125. package/src/cli/ui/__tests__/solid/AppSolid.cleanup.test.tsx +830 -1
  126. package/src/cli/ui/__tests__/solid/BranchListScreen.test.tsx +105 -5
  127. package/src/cli/ui/__tests__/solid/ConfirmScreen.test.tsx +77 -0
  128. package/src/cli/ui/__tests__/solid/LogScreen.test.tsx +351 -0
  129. package/src/cli/ui/__tests__/solid/components/QuickStartStep.test.tsx +73 -2
  130. package/src/cli/ui/__tests__/solid/components/WizardSteps.test.tsx +4 -1
  131. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +72 -45
  132. package/src/cli/ui/components/solid/QuickStartStep.tsx +35 -23
  133. package/src/cli/ui/components/solid/SearchInput.tsx +1 -1
  134. package/src/cli/ui/components/solid/SelectInput.tsx +4 -0
  135. package/src/cli/ui/components/solid/WizardController.tsx +20 -11
  136. package/src/cli/ui/components/solid/WizardSteps.tsx +29 -86
  137. package/src/cli/ui/core/theme.ts +32 -0
  138. package/src/cli/ui/hooks/solid/useAsyncOperation.ts +8 -6
  139. package/src/cli/ui/hooks/solid/useGitOperations.ts +6 -5
  140. package/src/cli/ui/screens/solid/BranchListScreen.tsx +135 -32
  141. package/src/cli/ui/screens/solid/ConfirmScreen.tsx +20 -8
  142. package/src/cli/ui/screens/solid/EnvironmentScreen.tsx +22 -20
  143. package/src/cli/ui/screens/solid/LogScreen.tsx +364 -35
  144. package/src/cli/ui/screens/solid/ProfileEnvScreen.tsx +19 -15
  145. package/src/cli/ui/screens/solid/SelectorScreen.tsx +25 -14
  146. package/src/cli/ui/screens/solid/SettingsScreen.tsx +5 -3
  147. package/src/cli/ui/types.ts +1 -0
  148. package/src/cli/ui/utils/__tests__/branchFormatter.test.ts +53 -6
  149. package/src/cli/ui/utils/branchFormatter.ts +35 -7
  150. package/src/cli/ui/utils/continueSession.ts +90 -3
  151. package/src/cli/ui/utils/versionCache.ts +93 -0
  152. package/src/cli/ui/utils/versionFetcher.ts +120 -0
  153. package/src/codex.ts +62 -20
  154. package/src/config/__tests__/saveSession.test.ts +2 -2
  155. package/src/config/index.ts +11 -1
  156. package/src/gemini.ts +50 -4
  157. package/src/index.test.ts +16 -10
  158. package/src/index.ts +38 -1
  159. package/src/launcher.ts +49 -8
  160. package/src/logging/agentOutput.ts +216 -0
  161. package/src/logging/formatter.ts +23 -4
  162. package/src/logging/logger.ts +2 -0
  163. package/src/logging/reader.ts +117 -0
  164. package/src/services/__tests__/BatchMergeService.test.ts +34 -14
  165. package/src/services/dependency-installer.ts +2 -2
  166. package/src/utils/session/common.ts +28 -0
  167. package/src/utils/session/parsers/claude.ts +79 -29
  168. package/src/utils/session/parsers/codex.ts +50 -26
  169. package/src/utils/session/parsers/gemini.ts +46 -5
  170. package/src/utils/session/parsers/opencode.ts +46 -5
  171. package/src/utils/session/types.ts +4 -0
  172. package/src/worktree.ts +28 -15
@@ -9,6 +9,25 @@ export interface LogFileInfo {
9
9
  mtimeMs: number;
10
10
  }
11
11
 
12
+ export type LogTargetReason =
13
+ | "worktree"
14
+ | "worktree-inaccessible"
15
+ | "current-working-directory"
16
+ | "working-directory"
17
+ | "no-worktree";
18
+
19
+ export interface LogTargetBranch {
20
+ name: string;
21
+ isCurrent?: boolean;
22
+ worktree?: { path: string; isAccessible?: boolean } | undefined;
23
+ }
24
+
25
+ export interface LogTargetResolution {
26
+ logDir: string | null;
27
+ sourcePath: string | null;
28
+ reason: LogTargetReason;
29
+ }
30
+
12
31
  const LOG_FILENAME_PATTERN = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
13
32
 
14
33
  export function resolveLogDir(cwd: string = process.cwd()): string {
@@ -16,6 +35,46 @@ export function resolveLogDir(cwd: string = process.cwd()): string {
16
35
  return path.join(os.homedir(), ".gwt", "logs", cwdBase);
17
36
  }
18
37
 
38
+ export function resolveLogTarget(
39
+ branch: LogTargetBranch | null,
40
+ workingDirectory: string = process.cwd(),
41
+ ): LogTargetResolution {
42
+ if (!branch) {
43
+ return {
44
+ logDir: resolveLogDir(workingDirectory),
45
+ sourcePath: workingDirectory,
46
+ reason: "working-directory",
47
+ };
48
+ }
49
+
50
+ const worktreePath = branch.worktree?.path;
51
+ if (worktreePath) {
52
+ const accessible = branch.worktree?.isAccessible !== false;
53
+ if (accessible) {
54
+ return {
55
+ logDir: resolveLogDir(worktreePath),
56
+ sourcePath: worktreePath,
57
+ reason: "worktree",
58
+ };
59
+ }
60
+ return {
61
+ logDir: null,
62
+ sourcePath: worktreePath,
63
+ reason: "worktree-inaccessible",
64
+ };
65
+ }
66
+
67
+ if (branch.isCurrent) {
68
+ return {
69
+ logDir: resolveLogDir(workingDirectory),
70
+ sourcePath: workingDirectory,
71
+ reason: "current-working-directory",
72
+ };
73
+ }
74
+
75
+ return { logDir: null, sourcePath: null, reason: "no-worktree" };
76
+ }
77
+
19
78
  export function buildLogFilePath(logDir: string, date: string): string {
20
79
  return path.join(logDir, `${date}.jsonl`);
21
80
  }
@@ -66,6 +125,23 @@ export async function listLogFiles(logDir: string): Promise<LogFileInfo[]> {
66
125
  }
67
126
  }
68
127
 
128
+ export async function clearLogFiles(logDir: string): Promise<number> {
129
+ const files = await listLogFiles(logDir);
130
+ let cleared = 0;
131
+ for (const file of files) {
132
+ try {
133
+ await fs.truncate(file.path, 0);
134
+ cleared += 1;
135
+ } catch (error) {
136
+ const err = error as NodeJS.ErrnoException;
137
+ if (err.code !== "ENOENT") {
138
+ throw error;
139
+ }
140
+ }
141
+ }
142
+ return cleared;
143
+ }
144
+
69
145
  export async function listRecentLogFiles(
70
146
  logDir: string,
71
147
  days = 7,
@@ -74,3 +150,44 @@ export async function listRecentLogFiles(
74
150
  const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
75
151
  return files.filter((file) => file.mtimeMs >= cutoff);
76
152
  }
153
+
154
+ export interface LogReadResult {
155
+ date: string;
156
+ lines: string[];
157
+ }
158
+
159
+ export async function readLogLinesForDate(
160
+ logDir: string,
161
+ preferredDate: string,
162
+ ): Promise<LogReadResult | null> {
163
+ const files = await listLogFiles(logDir);
164
+ if (files.length === 0) {
165
+ return null;
166
+ }
167
+
168
+ const ordered: LogFileInfo[] = [];
169
+ const preferred = files.find((file) => file.date === preferredDate);
170
+ if (preferred) {
171
+ ordered.push(preferred);
172
+ }
173
+ for (const file of files) {
174
+ if (preferred && file.date === preferred.date) {
175
+ continue;
176
+ }
177
+ ordered.push(file);
178
+ }
179
+
180
+ for (const file of ordered) {
181
+ const lines = await readLogFileLines(file.path);
182
+ if (lines.length > 0) {
183
+ return { date: file.date, lines };
184
+ }
185
+ }
186
+
187
+ const fallback = files[0];
188
+ if (!fallback) {
189
+ return { date: preferredDate, lines: [] };
190
+ }
191
+ const fallbackDate = preferred?.date ?? fallback.date;
192
+ return { date: fallbackDate, lines: [] };
193
+ }
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test";
2
2
  import { BatchMergeService } from "../BatchMergeService";
3
- import type { BatchMergeConfig } from "../../ui/types";
3
+ import type { BatchMergeConfig, BatchMergeProgress } from "../../cli/ui/types";
4
4
 
5
5
  // Mock git module
6
6
  mock.module("../../git", () => ({
@@ -235,7 +235,9 @@ describe("BatchMergeService", () => {
235
235
  (
236
236
  worktree.generateWorktreePath as ReturnType<typeof mock>
237
237
  ).mockResolvedValue("/repo/.worktrees/feature-b");
238
- (worktree.createWorktree as ReturnType<typeof mock>).mockResolvedValue();
238
+ (worktree.createWorktree as ReturnType<typeof mock>).mockResolvedValue(
239
+ undefined,
240
+ );
239
241
 
240
242
  const worktreePath = await service.ensureWorktree("feature/b");
241
243
 
@@ -271,7 +273,9 @@ describe("BatchMergeService", () => {
271
273
  });
272
274
 
273
275
  it("should successfully merge without conflicts", async () => {
274
- (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue();
276
+ (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
277
+ undefined,
278
+ );
275
279
  (git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(
276
280
  false,
277
281
  );
@@ -293,7 +297,7 @@ describe("BatchMergeService", () => {
293
297
  new Error("Merge conflict"),
294
298
  );
295
299
  (git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(true);
296
- (git.abortMerge as ReturnType<typeof mock>).mockResolvedValue();
300
+ (git.abortMerge as ReturnType<typeof mock>).mockResolvedValue(undefined);
297
301
 
298
302
  const status = await service.mergeBranch("feature/a", "main", config);
299
303
 
@@ -339,11 +343,13 @@ describe("BatchMergeService", () => {
339
343
  });
340
344
 
341
345
  it("should rollback with resetToHead after successful dry-run merge", async () => {
342
- (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue();
346
+ (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
347
+ undefined,
348
+ );
343
349
  (git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(
344
350
  false,
345
351
  );
346
- (git.resetToHead as ReturnType<typeof mock>).mockResolvedValue();
352
+ (git.resetToHead as ReturnType<typeof mock>).mockResolvedValue(undefined);
347
353
 
348
354
  const status = await service.mergeBranch(
349
355
  "feature/a",
@@ -368,7 +374,7 @@ describe("BatchMergeService", () => {
368
374
  new Error("CONFLICT (content)"),
369
375
  );
370
376
  (git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(true);
371
- (git.abortMerge as ReturnType<typeof mock>).mockResolvedValue();
377
+ (git.abortMerge as ReturnType<typeof mock>).mockResolvedValue(undefined);
372
378
 
373
379
  const status = await service.mergeBranch(
374
380
  "feature/a",
@@ -405,14 +411,18 @@ describe("BatchMergeService", () => {
405
411
  });
406
412
 
407
413
  it("should push successfully after merge when autoPush is enabled", async () => {
408
- (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue();
414
+ (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
415
+ undefined,
416
+ );
409
417
  (git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(
410
418
  false,
411
419
  );
412
420
  (git.getCurrentBranchName as ReturnType<typeof mock>).mockResolvedValue(
413
421
  "feature/a",
414
422
  );
415
- (git.pushBranchToRemote as ReturnType<typeof mock>).mockResolvedValue();
423
+ (git.pushBranchToRemote as ReturnType<typeof mock>).mockResolvedValue(
424
+ undefined,
425
+ );
416
426
 
417
427
  const status = await service.mergeBranch(
418
428
  "feature/a",
@@ -431,7 +441,9 @@ describe("BatchMergeService", () => {
431
441
  });
432
442
 
433
443
  it("should handle push failure without failing merge", async () => {
434
- (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue();
444
+ (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
445
+ undefined,
446
+ );
435
447
  (git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(
436
448
  false,
437
449
  );
@@ -455,7 +467,9 @@ describe("BatchMergeService", () => {
455
467
 
456
468
  it("should not push when autoPush is false", async () => {
457
469
  const noPushConfig = { ...autoPushConfig, autoPush: false };
458
- (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue();
470
+ (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
471
+ undefined,
472
+ );
459
473
  (git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(
460
474
  false,
461
475
  );
@@ -482,7 +496,9 @@ describe("BatchMergeService", () => {
482
496
  autoPush: false,
483
497
  };
484
498
 
485
- (git.fetchAllRemotes as ReturnType<typeof mock>).mockResolvedValue();
499
+ (git.fetchAllRemotes as ReturnType<typeof mock>).mockResolvedValue(
500
+ undefined,
501
+ );
486
502
  (git.getRepositoryRoot as ReturnType<typeof mock>).mockResolvedValue(
487
503
  "/repo",
488
504
  );
@@ -492,8 +508,12 @@ describe("BatchMergeService", () => {
492
508
  (worktree.generateWorktreePath as ReturnType<typeof mock>)
493
509
  .mockResolvedValueOnce("/repo/.worktrees/feature-a")
494
510
  .mockResolvedValueOnce("/repo/.worktrees/feature-b");
495
- (worktree.createWorktree as ReturnType<typeof mock>).mockResolvedValue();
496
- (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue();
511
+ (worktree.createWorktree as ReturnType<typeof mock>).mockResolvedValue(
512
+ undefined,
513
+ );
514
+ (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
515
+ undefined,
516
+ );
497
517
  (git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(
498
518
  false,
499
519
  );
@@ -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;
@@ -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,65 @@ 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
+ // Priority 2: Fallback to reading file content if filename lacks UUID
123
+ // (already handled via info above)
100
124
  }
101
125
 
102
- return fallbackMissingCwd;
126
+ return null;
103
127
  }
104
128
 
105
129
  /**
@@ -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)