@akiojin/gwt 2.11.1 → 2.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 (76) hide show
  1. package/dist/claude.d.ts +4 -1
  2. package/dist/claude.d.ts.map +1 -1
  3. package/dist/claude.js +51 -7
  4. package/dist/claude.js.map +1 -1
  5. package/dist/cli/ui/components/App.d.ts +7 -0
  6. package/dist/cli/ui/components/App.d.ts.map +1 -1
  7. package/dist/cli/ui/components/App.js +307 -18
  8. package/dist/cli/ui/components/App.js.map +1 -1
  9. package/dist/cli/ui/components/screens/BranchQuickStartScreen.d.ts +21 -0
  10. package/dist/cli/ui/components/screens/BranchQuickStartScreen.d.ts.map +1 -0
  11. package/dist/cli/ui/components/screens/BranchQuickStartScreen.js +145 -0
  12. package/dist/cli/ui/components/screens/BranchQuickStartScreen.js.map +1 -0
  13. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.d.ts +2 -1
  14. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.d.ts.map +1 -1
  15. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.js +4 -2
  16. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.js.map +1 -1
  17. package/dist/cli/ui/components/screens/ModelSelectorScreen.js +1 -1
  18. package/dist/cli/ui/components/screens/SessionSelectorScreen.d.ts +10 -2
  19. package/dist/cli/ui/components/screens/SessionSelectorScreen.d.ts.map +1 -1
  20. package/dist/cli/ui/components/screens/SessionSelectorScreen.js +18 -7
  21. package/dist/cli/ui/components/screens/SessionSelectorScreen.js.map +1 -1
  22. package/dist/cli/ui/types.d.ts +1 -1
  23. package/dist/cli/ui/types.d.ts.map +1 -1
  24. package/dist/cli/ui/utils/continueSession.d.ts +18 -0
  25. package/dist/cli/ui/utils/continueSession.d.ts.map +1 -0
  26. package/dist/cli/ui/utils/continueSession.js +67 -0
  27. package/dist/cli/ui/utils/continueSession.js.map +1 -0
  28. package/dist/codex.d.ts +4 -1
  29. package/dist/codex.d.ts.map +1 -1
  30. package/dist/codex.js +70 -5
  31. package/dist/codex.js.map +1 -1
  32. package/dist/config/index.d.ts +9 -1
  33. package/dist/config/index.d.ts.map +1 -1
  34. package/dist/config/index.js +11 -2
  35. package/dist/config/index.js.map +1 -1
  36. package/dist/gemini.d.ts +4 -1
  37. package/dist/gemini.d.ts.map +1 -1
  38. package/dist/gemini.js +146 -32
  39. package/dist/gemini.js.map +1 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +119 -48
  42. package/dist/index.js.map +1 -1
  43. package/dist/qwen.d.ts +4 -1
  44. package/dist/qwen.d.ts.map +1 -1
  45. package/dist/qwen.js +45 -4
  46. package/dist/qwen.js.map +1 -1
  47. package/dist/utils/prompt.d.ts +6 -0
  48. package/dist/utils/prompt.d.ts.map +1 -0
  49. package/dist/utils/prompt.js +57 -0
  50. package/dist/utils/prompt.js.map +1 -0
  51. package/dist/utils/session.d.ts +82 -0
  52. package/dist/utils/session.d.ts.map +1 -0
  53. package/dist/utils/session.js +579 -0
  54. package/dist/utils/session.js.map +1 -0
  55. package/package.json +2 -2
  56. package/src/claude.ts +69 -8
  57. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +12 -2
  58. package/src/cli/ui/__tests__/components/screens/BranchQuickStartScreen.test.tsx +142 -0
  59. package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +14 -0
  60. package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +29 -10
  61. package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +4 -1
  62. package/src/cli/ui/components/App.tsx +403 -23
  63. package/src/cli/ui/components/screens/BranchQuickStartScreen.tsx +237 -0
  64. package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +5 -1
  65. package/src/cli/ui/components/screens/ModelSelectorScreen.tsx +1 -1
  66. package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +34 -6
  67. package/src/cli/ui/types.ts +1 -0
  68. package/src/cli/ui/utils/continueSession.ts +106 -0
  69. package/src/codex.ts +91 -6
  70. package/src/config/index.ts +22 -2
  71. package/src/gemini.ts +179 -41
  72. package/src/index.ts +145 -61
  73. package/src/qwen.ts +56 -5
  74. package/src/utils/__tests__/prompt.test.ts +89 -0
  75. package/src/utils/prompt.ts +74 -0
  76. package/src/utils/session.ts +704 -0
package/src/gemini.ts CHANGED
@@ -2,6 +2,7 @@ import { execa } from "execa";
2
2
  import chalk from "chalk";
3
3
  import { existsSync } from "fs";
4
4
  import { createChildStdio, getTerminalStreams } from "./utils/terminal.js";
5
+ import { findLatestGeminiSessionId } from "./utils/session.js";
5
6
 
6
7
  const GEMINI_CLI_PACKAGE = "@google/gemini-cli@latest";
7
8
 
@@ -23,8 +24,9 @@ export async function launchGeminiCLI(
23
24
  extraArgs?: string[];
24
25
  envOverrides?: Record<string, string>;
25
26
  model?: string;
27
+ sessionId?: string | null;
26
28
  } = {},
27
- ): Promise<void> {
29
+ ): Promise<{ sessionId?: string | null }> {
28
30
  const terminal = getTerminalStreams();
29
31
 
30
32
  try {
@@ -44,14 +46,68 @@ export async function launchGeminiCLI(
44
46
  }
45
47
 
46
48
  // Handle execution mode
49
+ const resumeSessionId =
50
+ options.sessionId && options.sessionId.trim().length > 0
51
+ ? options.sessionId.trim()
52
+ : null;
53
+
54
+ const buildArgs = (useResumeId: boolean) => {
55
+ const a: string[] = [];
56
+ if (options.model) {
57
+ a.push("--model", options.model);
58
+ }
59
+
60
+ switch (options.mode) {
61
+ case "continue":
62
+ if (useResumeId && resumeSessionId) {
63
+ a.push("--resume", resumeSessionId);
64
+ } else {
65
+ a.push("--resume");
66
+ }
67
+ break;
68
+ case "resume":
69
+ if (useResumeId && resumeSessionId) {
70
+ a.push("--resume", resumeSessionId);
71
+ } else {
72
+ a.push("--resume");
73
+ }
74
+ break;
75
+ case "normal":
76
+ default:
77
+ break;
78
+ }
79
+
80
+ if (options.skipPermissions) {
81
+ a.push("-y");
82
+ }
83
+ if (options.extraArgs && options.extraArgs.length > 0) {
84
+ a.push(...options.extraArgs);
85
+ }
86
+ return a;
87
+ };
88
+
89
+ const argsPrimary = buildArgs(true);
90
+ const argsFallback = buildArgs(false);
91
+
92
+ // Log selected mode/ID
47
93
  switch (options.mode) {
48
94
  case "continue":
49
- args.push("-r", "latest");
50
- console.log(chalk.cyan(" ⏭️ Continuing most recent session"));
95
+ if (resumeSessionId) {
96
+ console.log(
97
+ chalk.cyan(
98
+ ` ⏭️ Continuing specific session: ${resumeSessionId}`,
99
+ ),
100
+ );
101
+ } else {
102
+ console.log(chalk.cyan(" ⏭️ Continuing most recent session"));
103
+ }
51
104
  break;
52
105
  case "resume":
53
- args.push("-r", "latest");
54
- console.log(chalk.cyan(" 🔄 Resuming session"));
106
+ if (resumeSessionId) {
107
+ console.log(chalk.cyan(` 🔄 Resuming session: ${resumeSessionId}`));
108
+ } else {
109
+ console.log(chalk.cyan(" 🔄 Resuming session (latest)"));
110
+ }
55
111
  break;
56
112
  case "normal":
57
113
  default:
@@ -61,17 +117,10 @@ export async function launchGeminiCLI(
61
117
 
62
118
  // Handle skip permissions (YOLO mode)
63
119
  if (options.skipPermissions) {
64
- args.push("-y");
65
120
  console.log(
66
121
  chalk.yellow(" ⚠️ Auto-approving all actions (YOLO mode)"),
67
122
  );
68
123
  }
69
-
70
- // Append any pass-through arguments after our flags
71
- if (options.extraArgs && options.extraArgs.length > 0) {
72
- args.push(...options.extraArgs);
73
- }
74
-
75
124
  terminal.exitRawMode();
76
125
 
77
126
  const baseEnv = {
@@ -84,46 +133,130 @@ export async function launchGeminiCLI(
84
133
  // Auto-detect locally installed gemini command
85
134
  const hasLocalGemini = await isGeminiCommandAvailable();
86
135
 
87
- try {
88
- if (hasLocalGemini) {
89
- // Use locally installed gemini command
90
- console.log(
91
- chalk.green(" Using locally installed gemini command"),
92
- );
93
- await execa("gemini", args, {
136
+ // Capture session ID from Gemini's exit summary
137
+ let capturedSessionId: string | null = null;
138
+ const extractSessionId = (output: string | undefined) => {
139
+ if (!output) return;
140
+ // Gemini outputs "Session ID: <uuid>" in exit summary
141
+ // UUID may be split across lines due to terminal width
142
+ // First, find "Session ID:" and extract following hex characters
143
+ const sessionIdIndex = output.indexOf("Session ID:");
144
+ if (sessionIdIndex === -1) return;
145
+
146
+ // Extract text after "Session ID:" until we have enough hex chars for UUID
147
+ const afterLabel = output.slice(sessionIdIndex + "Session ID:".length);
148
+ // Remove all non-hex characters except dash, then extract UUID pattern
149
+ const hexOnly = afterLabel.replace(/[^0-9a-fA-F-]/g, "");
150
+ // UUID format: 8-4-4-4-12 = 32 hex chars + 4 dashes
151
+ const uuidMatch = hexOnly.match(
152
+ /^([0-9a-f]{8})-?([0-9a-f]{4})-?([0-9a-f]{4})-?([0-9a-f]{4})-?([0-9a-f]{12})/i,
153
+ );
154
+ if (uuidMatch) {
155
+ capturedSessionId = `${uuidMatch[1]}-${uuidMatch[2]}-${uuidMatch[3]}-${uuidMatch[4]}-${uuidMatch[5]}`;
156
+ }
157
+ };
158
+
159
+ const runGemini = async (runArgs: string[]): Promise<string | undefined> => {
160
+ // Capture stdout while passing through to terminal
161
+ // Store chunks to extract session ID after process exits
162
+ const outputChunks: string[] = [];
163
+
164
+ const runWithCapture = async (cmd: string, args: string[]) => {
165
+ const child = execa(cmd, args, {
94
166
  cwd: worktreePath,
95
167
  shell: true,
96
168
  stdin: childStdio.stdin,
97
- stdout: childStdio.stdout,
169
+ stdout: "pipe",
98
170
  stderr: childStdio.stderr,
99
171
  env: baseEnv,
100
172
  } as any);
101
- } else {
102
- // Fallback to bunx
103
- console.log(
104
- chalk.cyan(" 🔄 Falling back to bunx @google/gemini-cli@latest"),
105
- );
173
+
174
+ // Pass stdout through to terminal while capturing
175
+ child.stdout?.on("data", (chunk: Buffer) => {
176
+ const text = chunk.toString("utf8");
177
+ outputChunks.push(text);
178
+ terminal.stdout.write(chunk);
179
+ });
180
+
181
+ await child;
182
+ return outputChunks.join("");
183
+ };
184
+
185
+ if (hasLocalGemini) {
106
186
  console.log(
107
- chalk.yellow(
108
- " 💡 Recommended: Install Gemini CLI globally for faster startup",
109
- ),
187
+ chalk.green(" ✨ Using locally installed gemini command"),
110
188
  );
111
- console.log(chalk.yellow(" npm install -g @google/gemini-cli"));
112
- console.log("");
113
- // Wait 2 seconds to let user read the message
114
- await new Promise((resolve) => setTimeout(resolve, 2000));
115
- await execa("bunx", [GEMINI_CLI_PACKAGE, ...args], {
116
- cwd: worktreePath,
117
- shell: true,
118
- stdin: childStdio.stdin,
119
- stdout: childStdio.stdout,
120
- stderr: childStdio.stderr,
121
- env: baseEnv,
122
- } as any);
189
+ return await runWithCapture("gemini", runArgs);
190
+ }
191
+ console.log(
192
+ chalk.cyan(" 🔄 Falling back to bunx @google/gemini-cli@latest"),
193
+ );
194
+ console.log(
195
+ chalk.yellow(
196
+ " 💡 Recommended: Install Gemini CLI globally for faster startup",
197
+ ),
198
+ );
199
+ console.log(chalk.yellow(" npm install -g @google/gemini-cli"));
200
+ console.log("");
201
+ await new Promise((resolve) => setTimeout(resolve, 2000));
202
+ return await runWithCapture("bunx", [GEMINI_CLI_PACKAGE, ...runArgs]);
203
+ };
204
+
205
+ let output: string | undefined;
206
+ try {
207
+ // Try with explicit session ID first (if any), then fallback to --resume (latest) once
208
+ try {
209
+ output = await runGemini(argsPrimary);
210
+ } catch (err) {
211
+ const shouldRetry =
212
+ (options.mode === "resume" || options.mode === "continue") &&
213
+ resumeSessionId;
214
+ if (shouldRetry) {
215
+ console.log(
216
+ chalk.yellow(
217
+ ` ⚠️ Failed to resume session ${resumeSessionId}. Retrying with latest session...`,
218
+ ),
219
+ );
220
+ output = await runGemini(argsFallback);
221
+ } else {
222
+ throw err;
223
+ }
123
224
  }
124
225
  } finally {
125
226
  childStdio.cleanup();
126
227
  }
228
+
229
+ // Extract session ID from Gemini's exit summary output
230
+ extractSessionId(output);
231
+
232
+ // Fallback to file-based detection if stdout capture failed
233
+ if (!capturedSessionId) {
234
+ try {
235
+ capturedSessionId =
236
+ (await findLatestGeminiSessionId(worktreePath, {
237
+ cwd: worktreePath,
238
+ })) ??
239
+ resumeSessionId ??
240
+ null;
241
+ } catch {
242
+ capturedSessionId = resumeSessionId ?? null;
243
+ }
244
+ }
245
+
246
+ if (capturedSessionId) {
247
+ console.log(chalk.cyan(`\n 🆔 Session ID: ${capturedSessionId}`));
248
+ console.log(
249
+ chalk.gray(` Resume command: gemini --resume ${capturedSessionId}`),
250
+ );
251
+ } else {
252
+ console.log(
253
+ chalk.yellow(
254
+ "\n ℹ️ Could not determine Gemini session ID automatically.",
255
+ ),
256
+ );
257
+ }
258
+
259
+ return capturedSessionId ? { sessionId: capturedSessionId } : {};
127
260
  } catch (error: any) {
128
261
  const hasLocalGemini = await isGeminiCommandAvailable();
129
262
  let errorMessage: string;
@@ -181,7 +314,12 @@ export async function launchGeminiCLI(
181
314
  async function isGeminiCommandAvailable(): Promise<boolean> {
182
315
  try {
183
316
  const command = process.platform === "win32" ? "where" : "which";
184
- await execa(command, ["gemini"], { shell: true });
317
+ await execa(command, ["gemini"], {
318
+ shell: true,
319
+ stdin: "ignore",
320
+ stdout: "ignore",
321
+ stderr: "ignore",
322
+ });
185
323
  return true;
186
324
  } catch {
187
325
  // gemini command not found in PATH
package/src/index.ts CHANGED
@@ -35,14 +35,21 @@ import {
35
35
  } from "./utils/terminal.js";
36
36
  import { getToolById, getSharedEnvironment } from "./config/tools.js";
37
37
  import { launchCustomAITool } from "./launcher.js";
38
- import { saveSession } from "./config/index.js";
38
+ import { saveSession, loadSession } from "./config/index.js";
39
+ import {
40
+ findLatestCodexSession,
41
+ findLatestClaudeSession,
42
+ findLatestGeminiSession,
43
+ } from "./utils/session.js";
39
44
  import { getPackageVersion } from "./utils.js";
40
- import readline from "node:readline";
45
+ import { findLatestClaudeSessionId } from "./utils/session.js";
46
+ import { resolveContinueSessionId } from "./cli/ui/utils/continueSession.js";
41
47
  import {
42
48
  installDependenciesForWorktree,
43
49
  DependencyInstallError,
44
50
  type DependencyInstallResult,
45
51
  } from "./services/dependency-installer.js";
52
+ import { waitForEnter } from "./utils/prompt.js";
46
53
 
47
54
  const ERROR_PROMPT = chalk.yellow(
48
55
  "Review the error details, then press Enter to continue.",
@@ -185,50 +192,6 @@ async function runDependencyInstallStep<T extends DependencyInstallResult>(
185
192
  }
186
193
  }
187
194
 
188
- async function waitForEnter(promptMessage: string): Promise<void> {
189
- if (!process.stdin.isTTY) {
190
- // For non-interactive environments, resolve immediately.
191
- return;
192
- }
193
-
194
- // Ensure stdin is resumed and not in raw mode before using readline.
195
- // This is crucial for environments where stdin might be paused or in raw mode
196
- // by other libraries (like Ink.js).
197
- if (typeof process.stdin.resume === "function") {
198
- process.stdin.resume();
199
- }
200
- if (process.stdin.isRaw) {
201
- process.stdin.setRawMode(false);
202
- }
203
-
204
- await new Promise<void>((resolve) => {
205
- const rl = readline.createInterface({
206
- input: process.stdin,
207
- output: process.stdout,
208
- });
209
-
210
- // Handle Ctrl+C to gracefully exit.
211
- rl.on("SIGINT", () => {
212
- rl.close();
213
- // Restore stdin to a paused state before exiting.
214
- if (typeof process.stdin.pause === "function") {
215
- process.stdin.pause();
216
- }
217
- process.exit(0);
218
- });
219
-
220
- rl.question(`${promptMessage}\n`, () => {
221
- rl.close();
222
- // Pause stdin again to allow other parts of the application
223
- // to take control if needed.
224
- if (typeof process.stdin.pause === "function") {
225
- process.stdin.pause();
226
- }
227
- resolve();
228
- });
229
- });
230
- }
231
-
232
195
  function showHelp(): void {
233
196
  console.log(`
234
197
  Worktree Manager
@@ -273,6 +236,7 @@ async function mainInkUI(): Promise<SelectionResult | undefined> {
273
236
 
274
237
  let selectionResult: SelectionResult | undefined;
275
238
 
239
+ // Resume stdin to ensure it's ready for Ink.js
276
240
  if (typeof terminal.stdin.resume === "function") {
277
241
  terminal.stdin.resume();
278
242
  }
@@ -324,6 +288,7 @@ export async function handleAIToolWorkflow(
324
288
  skipPermissions,
325
289
  model,
326
290
  inferenceLevel,
291
+ sessionId: selectedSessionId,
327
292
  } = selectionResult;
328
293
 
329
294
  const branchLabel = displayName ?? branch;
@@ -564,26 +529,61 @@ export async function handleAIToolWorkflow(
564
529
 
565
530
  // Save selection immediately so "last tool" is reflected even if the tool
566
531
  // is interrupted or killed mid-run (e.g., Ctrl+C).
567
- await saveSession({
568
- lastWorktreePath: worktreePath,
569
- lastBranch: branch,
570
- lastUsedTool: tool,
571
- toolLabel: toolConfig.displayName ?? tool,
572
- mode,
573
- model: model ?? null,
574
- timestamp: Date.now(),
575
- repositoryRoot: repoRoot,
576
- });
532
+ await saveSession(
533
+ {
534
+ lastWorktreePath: worktreePath,
535
+ lastBranch: branch,
536
+ lastUsedTool: tool,
537
+ toolLabel: toolConfig.displayName ?? tool,
538
+ mode,
539
+ model: model ?? null,
540
+ reasoningLevel: inferenceLevel ?? null,
541
+ skipPermissions: skipPermissions ?? null,
542
+ timestamp: Date.now(),
543
+ repositoryRoot: repoRoot,
544
+ },
545
+ { skipHistory: true },
546
+ );
547
+
548
+ // Lookup saved session ID for continue/resume
549
+ let resumeSessionId: string | null =
550
+ selectedSessionId && selectedSessionId.length > 0
551
+ ? selectedSessionId
552
+ : null;
553
+ if (mode === "continue" || mode === "resume") {
554
+ const existingSession = await loadSession(repoRoot);
555
+ const history = existingSession?.history ?? [];
556
+
557
+ resumeSessionId =
558
+ resumeSessionId ??
559
+ (await resolveContinueSessionId({
560
+ history,
561
+ sessionData: existingSession,
562
+ branch,
563
+ toolId: tool,
564
+ repoRoot,
565
+ }));
566
+
567
+ if (!resumeSessionId) {
568
+ printWarning(
569
+ "No saved session ID found for this branch/tool. Falling back to tool default.",
570
+ );
571
+ }
572
+ }
573
+
574
+ const launchStartedAt = Date.now();
577
575
 
578
576
  // Launch selected AI tool
579
577
  // Builtin tools use their dedicated launch functions
580
578
  // Custom tools use the generic launchCustomAITool function
579
+ let launchResult: { sessionId?: string | null } | void;
581
580
  if (tool === "claude-code") {
582
581
  const launchOptions: {
583
582
  mode?: "normal" | "continue" | "resume";
584
583
  skipPermissions?: boolean;
585
584
  envOverrides?: Record<string, string>;
586
585
  model?: string;
586
+ sessionId?: string | null;
587
587
  } = {
588
588
  mode:
589
589
  mode === "resume"
@@ -593,11 +593,12 @@ export async function handleAIToolWorkflow(
593
593
  : "normal",
594
594
  skipPermissions,
595
595
  envOverrides: sharedEnv,
596
+ sessionId: resumeSessionId,
596
597
  };
597
598
  if (model) {
598
599
  launchOptions.model = model;
599
600
  }
600
- await launchClaudeCode(worktreePath, launchOptions);
601
+ launchResult = await launchClaudeCode(worktreePath, launchOptions);
601
602
  } else if (tool === "codex-cli") {
602
603
  const launchOptions: {
603
604
  mode?: "normal" | "continue" | "resume";
@@ -605,6 +606,7 @@ export async function handleAIToolWorkflow(
605
606
  envOverrides?: Record<string, string>;
606
607
  model?: string;
607
608
  reasoningEffort?: CodexReasoningEffort;
609
+ sessionId?: string | null;
608
610
  } = {
609
611
  mode:
610
612
  mode === "resume"
@@ -614,6 +616,7 @@ export async function handleAIToolWorkflow(
614
616
  : "normal",
615
617
  bypassApprovals: skipPermissions,
616
618
  envOverrides: sharedEnv,
619
+ sessionId: resumeSessionId,
617
620
  };
618
621
  if (model) {
619
622
  launchOptions.model = model;
@@ -621,13 +624,14 @@ export async function handleAIToolWorkflow(
621
624
  if (inferenceLevel) {
622
625
  launchOptions.reasoningEffort = inferenceLevel as CodexReasoningEffort;
623
626
  }
624
- await launchCodexCLI(worktreePath, launchOptions);
627
+ launchResult = await launchCodexCLI(worktreePath, launchOptions);
625
628
  } else if (tool === "gemini-cli") {
626
629
  const launchOptions: {
627
630
  mode?: "normal" | "continue" | "resume";
628
631
  skipPermissions?: boolean;
629
632
  envOverrides?: Record<string, string>;
630
633
  model?: string;
634
+ sessionId?: string | null;
631
635
  } = {
632
636
  mode:
633
637
  mode === "resume"
@@ -637,17 +641,19 @@ export async function handleAIToolWorkflow(
637
641
  : "normal",
638
642
  skipPermissions,
639
643
  envOverrides: sharedEnv,
644
+ sessionId: resumeSessionId,
640
645
  };
641
646
  if (model) {
642
647
  launchOptions.model = model;
643
648
  }
644
- await launchGeminiCLI(worktreePath, launchOptions);
649
+ launchResult = await launchGeminiCLI(worktreePath, launchOptions);
645
650
  } else if (tool === "qwen-cli") {
646
651
  const launchOptions: {
647
652
  mode?: "normal" | "continue" | "resume";
648
653
  skipPermissions?: boolean;
649
654
  envOverrides?: Record<string, string>;
650
655
  model?: string;
656
+ sessionId?: string | null;
651
657
  } = {
652
658
  mode:
653
659
  mode === "resume"
@@ -657,15 +663,16 @@ export async function handleAIToolWorkflow(
657
663
  : "normal",
658
664
  skipPermissions,
659
665
  envOverrides: sharedEnv,
666
+ sessionId: resumeSessionId,
660
667
  };
661
668
  if (model) {
662
669
  launchOptions.model = model;
663
670
  }
664
- await launchQwenCLI(worktreePath, launchOptions);
671
+ launchResult = await launchQwenCLI(worktreePath, launchOptions);
665
672
  } else {
666
673
  // Custom tool
667
674
  printInfo(`Launching custom tool: ${toolConfig.displayName}`);
668
- await launchCustomAITool(toolConfig, {
675
+ launchResult = await launchCustomAITool(toolConfig, {
669
676
  mode:
670
677
  mode === "resume"
671
678
  ? "resume"
@@ -678,6 +685,83 @@ export async function handleAIToolWorkflow(
678
685
  });
679
686
  }
680
687
 
688
+ // Persist session with captured session ID (if any)
689
+ let finalSessionId =
690
+ (launchResult as { sessionId?: string | null } | undefined)?.sessionId ??
691
+ resumeSessionId ??
692
+ null;
693
+
694
+ if (!finalSessionId && tool === "claude-code") {
695
+ try {
696
+ finalSessionId = (await findLatestClaudeSessionId(worktreePath)) ?? null;
697
+ } catch {
698
+ finalSessionId = null;
699
+ }
700
+ }
701
+ const finishedAt = Date.now();
702
+
703
+ if (tool === "codex-cli") {
704
+ try {
705
+ const latest = await findLatestCodexSession({
706
+ since: launchStartedAt - 60_000,
707
+ until: finishedAt + 60_000,
708
+ preferClosestTo: finishedAt,
709
+ windowMs: 60 * 60 * 1000,
710
+ cwd: worktreePath,
711
+ });
712
+ if (latest) {
713
+ finalSessionId = latest.id;
714
+ }
715
+ } catch {
716
+ // ignore fallback failure
717
+ }
718
+ } else if (tool === "claude-code") {
719
+ try {
720
+ const latestClaude = await findLatestClaudeSession(worktreePath, {
721
+ since: launchStartedAt - 60_000,
722
+ until: finishedAt + 60_000,
723
+ preferClosestTo: finishedAt,
724
+ windowMs: 60 * 60 * 1000,
725
+ });
726
+ if (latestClaude) {
727
+ finalSessionId = latestClaude.id;
728
+ }
729
+ } catch {
730
+ // ignore
731
+ }
732
+ } else if (tool === "gemini-cli") {
733
+ try {
734
+ const latestGemini = await findLatestGeminiSession(worktreePath, {
735
+ since: launchStartedAt - 60_000,
736
+ until: finishedAt + 60_000,
737
+ preferClosestTo: finishedAt,
738
+ windowMs: 60 * 60 * 1000,
739
+ cwd: worktreePath,
740
+ });
741
+ if (latestGemini) {
742
+ finalSessionId = latestGemini.id;
743
+ }
744
+ } catch {
745
+ // ignore
746
+ }
747
+ }
748
+
749
+ await saveSession({
750
+ lastWorktreePath: worktreePath,
751
+ lastBranch: branch,
752
+ lastUsedTool: tool,
753
+ toolLabel: toolConfig.displayName ?? tool,
754
+ mode,
755
+ model: model ?? null,
756
+ reasoningLevel: inferenceLevel ?? null,
757
+ skipPermissions: skipPermissions ?? null,
758
+ timestamp: Date.now(),
759
+ repositoryRoot: repoRoot,
760
+ lastSessionId: finalSessionId,
761
+ });
762
+
763
+ // Small buffer before returning to branch list to avoid abrupt screen swap
764
+ await new Promise((resolve) => setTimeout(resolve, 3000));
681
765
  printInfo("Session completed successfully. Returning to main menu...");
682
766
  return;
683
767
  } catch (error) {