@akiojin/gwt 2.11.0 → 2.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 (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/branchFormatter.d.ts.map +1 -1
  25. package/dist/cli/ui/utils/branchFormatter.js +0 -13
  26. package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
  27. package/dist/cli/ui/utils/continueSession.d.ts +18 -0
  28. package/dist/cli/ui/utils/continueSession.d.ts.map +1 -0
  29. package/dist/cli/ui/utils/continueSession.js +67 -0
  30. package/dist/cli/ui/utils/continueSession.js.map +1 -0
  31. package/dist/codex.d.ts +4 -1
  32. package/dist/codex.d.ts.map +1 -1
  33. package/dist/codex.js +70 -5
  34. package/dist/codex.js.map +1 -1
  35. package/dist/config/index.d.ts +9 -1
  36. package/dist/config/index.d.ts.map +1 -1
  37. package/dist/config/index.js +11 -2
  38. package/dist/config/index.js.map +1 -1
  39. package/dist/gemini.d.ts +4 -1
  40. package/dist/gemini.d.ts.map +1 -1
  41. package/dist/gemini.js +146 -32
  42. package/dist/gemini.js.map +1 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +118 -8
  45. package/dist/index.js.map +1 -1
  46. package/dist/qwen.d.ts +4 -1
  47. package/dist/qwen.d.ts.map +1 -1
  48. package/dist/qwen.js +45 -4
  49. package/dist/qwen.js.map +1 -1
  50. package/dist/utils/session.d.ts +82 -0
  51. package/dist/utils/session.d.ts.map +1 -0
  52. package/dist/utils/session.js +579 -0
  53. package/dist/utils/session.js.map +1 -0
  54. package/package.json +1 -1
  55. package/src/claude.ts +69 -8
  56. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +12 -2
  57. package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +2 -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/__tests__/utils/branchFormatter.test.ts +0 -1
  63. package/src/cli/ui/components/App.tsx +403 -23
  64. package/src/cli/ui/components/screens/BranchQuickStartScreen.tsx +237 -0
  65. package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +5 -1
  66. package/src/cli/ui/components/screens/ModelSelectorScreen.tsx +1 -1
  67. package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +34 -6
  68. package/src/cli/ui/types.ts +1 -0
  69. package/src/cli/ui/utils/branchFormatter.ts +0 -13
  70. package/src/cli/ui/utils/continueSession.ts +106 -0
  71. package/src/codex.ts +91 -6
  72. package/src/config/index.ts +22 -2
  73. package/src/gemini.ts +179 -41
  74. package/src/index.ts +144 -16
  75. package/src/qwen.ts +56 -5
  76. package/src/utils/session.ts +704 -0
@@ -14,6 +14,9 @@ export interface SessionData {
14
14
  lastWorktreePath: string | null;
15
15
  lastBranch: string | null;
16
16
  lastUsedTool?: string;
17
+ lastSessionId?: string | null;
18
+ reasoningLevel?: string | null;
19
+ skipPermissions?: boolean | null;
17
20
  timestamp: number;
18
21
  repositoryRoot: string;
19
22
  mode?: "normal" | "continue" | "resume";
@@ -27,8 +30,11 @@ export interface ToolSessionEntry {
27
30
  worktreePath: string | null;
28
31
  toolId: string;
29
32
  toolLabel: string;
33
+ sessionId?: string | null;
30
34
  mode?: "normal" | "continue" | "resume" | null;
31
35
  model?: string | null;
36
+ reasoningLevel?: string | null;
37
+ skipPermissions?: boolean | null;
32
38
  timestamp: number;
33
39
  }
34
40
 
@@ -110,7 +116,10 @@ function getSessionFilePath(repositoryRoot: string): string {
110
116
  return path.join(sessionDir, `${repoName}_${repoHash}.json`);
111
117
  }
112
118
 
113
- export async function saveSession(sessionData: SessionData): Promise<void> {
119
+ export async function saveSession(
120
+ sessionData: SessionData,
121
+ options: { skipHistory?: boolean } = {},
122
+ ): Promise<void> {
114
123
  try {
115
124
  const sessionPath = getSessionFilePath(sessionData.repositoryRoot);
116
125
  const sessionDir = path.dirname(sessionPath);
@@ -131,15 +140,22 @@ export async function saveSession(sessionData: SessionData): Promise<void> {
131
140
  }
132
141
 
133
142
  // 新しい履歴エントリを追加(branch/worktree/toolが揃っている場合のみ)
134
- if (sessionData.lastBranch && sessionData.lastWorktreePath) {
143
+ if (
144
+ !options.skipHistory &&
145
+ sessionData.lastBranch &&
146
+ sessionData.lastWorktreePath
147
+ ) {
135
148
  const entry: ToolSessionEntry = {
136
149
  branch: sessionData.lastBranch,
137
150
  worktreePath: sessionData.lastWorktreePath,
138
151
  toolId: sessionData.lastUsedTool ?? "unknown",
139
152
  toolLabel:
140
153
  sessionData.toolLabel ?? sessionData.lastUsedTool ?? "Custom",
154
+ sessionId: sessionData.lastSessionId ?? null,
141
155
  mode: sessionData.mode ?? null,
142
156
  model: sessionData.model ?? null,
157
+ reasoningLevel: sessionData.reasoningLevel ?? null,
158
+ skipPermissions: sessionData.skipPermissions ?? false,
143
159
  timestamp: sessionData.timestamp,
144
160
  };
145
161
  existingHistory = [...existingHistory, entry].slice(-100); // keep latest 100
@@ -148,6 +164,9 @@ export async function saveSession(sessionData: SessionData): Promise<void> {
148
164
  const payload: SessionData = {
149
165
  ...sessionData,
150
166
  history: existingHistory,
167
+ lastSessionId: sessionData.lastSessionId ?? null,
168
+ reasoningLevel: sessionData.reasoningLevel ?? null,
169
+ skipPermissions: sessionData.skipPermissions ?? false,
151
170
  };
152
171
 
153
172
  await writeFile(sessionPath, JSON.stringify(payload, null, 2), "utf-8");
@@ -264,6 +283,7 @@ export async function getLastToolUsageMap(
264
283
  toolLabel: parsed.toolLabel ?? parsed.lastUsedTool ?? "Custom",
265
284
  mode: parsed.mode ?? null,
266
285
  model: parsed.model ?? null,
286
+ reasoningLevel: parsed.reasoningLevel ?? null,
267
287
  timestamp: parsed.timestamp ?? Date.now(),
268
288
  });
269
289
  }
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,9 +35,16 @@ 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";
45
+ import { findLatestClaudeSessionId } from "./utils/session.js";
40
46
  import readline from "node:readline";
47
+ import { resolveContinueSessionId } from "./cli/ui/utils/continueSession.js";
41
48
  import {
42
49
  installDependenciesForWorktree,
43
50
  DependencyInstallError,
@@ -273,6 +280,7 @@ async function mainInkUI(): Promise<SelectionResult | undefined> {
273
280
 
274
281
  let selectionResult: SelectionResult | undefined;
275
282
 
283
+ // Resume stdin to ensure it's ready for Ink.js
276
284
  if (typeof terminal.stdin.resume === "function") {
277
285
  terminal.stdin.resume();
278
286
  }
@@ -324,6 +332,7 @@ export async function handleAIToolWorkflow(
324
332
  skipPermissions,
325
333
  model,
326
334
  inferenceLevel,
335
+ sessionId: selectedSessionId,
327
336
  } = selectionResult;
328
337
 
329
338
  const branchLabel = displayName ?? branch;
@@ -564,26 +573,61 @@ export async function handleAIToolWorkflow(
564
573
 
565
574
  // Save selection immediately so "last tool" is reflected even if the tool
566
575
  // 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
- });
576
+ await saveSession(
577
+ {
578
+ lastWorktreePath: worktreePath,
579
+ lastBranch: branch,
580
+ lastUsedTool: tool,
581
+ toolLabel: toolConfig.displayName ?? tool,
582
+ mode,
583
+ model: model ?? null,
584
+ reasoningLevel: inferenceLevel ?? null,
585
+ skipPermissions: skipPermissions ?? null,
586
+ timestamp: Date.now(),
587
+ repositoryRoot: repoRoot,
588
+ },
589
+ { skipHistory: true },
590
+ );
591
+
592
+ // Lookup saved session ID for continue/resume
593
+ let resumeSessionId: string | null =
594
+ selectedSessionId && selectedSessionId.length > 0
595
+ ? selectedSessionId
596
+ : null;
597
+ if (mode === "continue" || mode === "resume") {
598
+ const existingSession = await loadSession(repoRoot);
599
+ const history = existingSession?.history ?? [];
600
+
601
+ resumeSessionId =
602
+ resumeSessionId ??
603
+ (await resolveContinueSessionId({
604
+ history,
605
+ sessionData: existingSession,
606
+ branch,
607
+ toolId: tool,
608
+ repoRoot,
609
+ }));
610
+
611
+ if (!resumeSessionId) {
612
+ printWarning(
613
+ "No saved session ID found for this branch/tool. Falling back to tool default.",
614
+ );
615
+ }
616
+ }
617
+
618
+ const launchStartedAt = Date.now();
577
619
 
578
620
  // Launch selected AI tool
579
621
  // Builtin tools use their dedicated launch functions
580
622
  // Custom tools use the generic launchCustomAITool function
623
+ let launchResult: { sessionId?: string | null } | void;
581
624
  if (tool === "claude-code") {
582
625
  const launchOptions: {
583
626
  mode?: "normal" | "continue" | "resume";
584
627
  skipPermissions?: boolean;
585
628
  envOverrides?: Record<string, string>;
586
629
  model?: string;
630
+ sessionId?: string | null;
587
631
  } = {
588
632
  mode:
589
633
  mode === "resume"
@@ -593,11 +637,12 @@ export async function handleAIToolWorkflow(
593
637
  : "normal",
594
638
  skipPermissions,
595
639
  envOverrides: sharedEnv,
640
+ sessionId: resumeSessionId,
596
641
  };
597
642
  if (model) {
598
643
  launchOptions.model = model;
599
644
  }
600
- await launchClaudeCode(worktreePath, launchOptions);
645
+ launchResult = await launchClaudeCode(worktreePath, launchOptions);
601
646
  } else if (tool === "codex-cli") {
602
647
  const launchOptions: {
603
648
  mode?: "normal" | "continue" | "resume";
@@ -605,6 +650,7 @@ export async function handleAIToolWorkflow(
605
650
  envOverrides?: Record<string, string>;
606
651
  model?: string;
607
652
  reasoningEffort?: CodexReasoningEffort;
653
+ sessionId?: string | null;
608
654
  } = {
609
655
  mode:
610
656
  mode === "resume"
@@ -614,6 +660,7 @@ export async function handleAIToolWorkflow(
614
660
  : "normal",
615
661
  bypassApprovals: skipPermissions,
616
662
  envOverrides: sharedEnv,
663
+ sessionId: resumeSessionId,
617
664
  };
618
665
  if (model) {
619
666
  launchOptions.model = model;
@@ -621,13 +668,14 @@ export async function handleAIToolWorkflow(
621
668
  if (inferenceLevel) {
622
669
  launchOptions.reasoningEffort = inferenceLevel as CodexReasoningEffort;
623
670
  }
624
- await launchCodexCLI(worktreePath, launchOptions);
671
+ launchResult = await launchCodexCLI(worktreePath, launchOptions);
625
672
  } else if (tool === "gemini-cli") {
626
673
  const launchOptions: {
627
674
  mode?: "normal" | "continue" | "resume";
628
675
  skipPermissions?: boolean;
629
676
  envOverrides?: Record<string, string>;
630
677
  model?: string;
678
+ sessionId?: string | null;
631
679
  } = {
632
680
  mode:
633
681
  mode === "resume"
@@ -637,17 +685,19 @@ export async function handleAIToolWorkflow(
637
685
  : "normal",
638
686
  skipPermissions,
639
687
  envOverrides: sharedEnv,
688
+ sessionId: resumeSessionId,
640
689
  };
641
690
  if (model) {
642
691
  launchOptions.model = model;
643
692
  }
644
- await launchGeminiCLI(worktreePath, launchOptions);
693
+ launchResult = await launchGeminiCLI(worktreePath, launchOptions);
645
694
  } else if (tool === "qwen-cli") {
646
695
  const launchOptions: {
647
696
  mode?: "normal" | "continue" | "resume";
648
697
  skipPermissions?: boolean;
649
698
  envOverrides?: Record<string, string>;
650
699
  model?: string;
700
+ sessionId?: string | null;
651
701
  } = {
652
702
  mode:
653
703
  mode === "resume"
@@ -657,15 +707,16 @@ export async function handleAIToolWorkflow(
657
707
  : "normal",
658
708
  skipPermissions,
659
709
  envOverrides: sharedEnv,
710
+ sessionId: resumeSessionId,
660
711
  };
661
712
  if (model) {
662
713
  launchOptions.model = model;
663
714
  }
664
- await launchQwenCLI(worktreePath, launchOptions);
715
+ launchResult = await launchQwenCLI(worktreePath, launchOptions);
665
716
  } else {
666
717
  // Custom tool
667
718
  printInfo(`Launching custom tool: ${toolConfig.displayName}`);
668
- await launchCustomAITool(toolConfig, {
719
+ launchResult = await launchCustomAITool(toolConfig, {
669
720
  mode:
670
721
  mode === "resume"
671
722
  ? "resume"
@@ -678,6 +729,83 @@ export async function handleAIToolWorkflow(
678
729
  });
679
730
  }
680
731
 
732
+ // Persist session with captured session ID (if any)
733
+ let finalSessionId =
734
+ (launchResult as { sessionId?: string | null } | undefined)?.sessionId ??
735
+ resumeSessionId ??
736
+ null;
737
+
738
+ if (!finalSessionId && tool === "claude-code") {
739
+ try {
740
+ finalSessionId = (await findLatestClaudeSessionId(worktreePath)) ?? null;
741
+ } catch {
742
+ finalSessionId = null;
743
+ }
744
+ }
745
+ const finishedAt = Date.now();
746
+
747
+ if (tool === "codex-cli") {
748
+ try {
749
+ const latest = await findLatestCodexSession({
750
+ since: launchStartedAt - 60_000,
751
+ until: finishedAt + 60_000,
752
+ preferClosestTo: finishedAt,
753
+ windowMs: 60 * 60 * 1000,
754
+ cwd: worktreePath,
755
+ });
756
+ if (latest) {
757
+ finalSessionId = latest.id;
758
+ }
759
+ } catch {
760
+ // ignore fallback failure
761
+ }
762
+ } else if (tool === "claude-code") {
763
+ try {
764
+ const latestClaude = await findLatestClaudeSession(worktreePath, {
765
+ since: launchStartedAt - 60_000,
766
+ until: finishedAt + 60_000,
767
+ preferClosestTo: finishedAt,
768
+ windowMs: 60 * 60 * 1000,
769
+ });
770
+ if (latestClaude) {
771
+ finalSessionId = latestClaude.id;
772
+ }
773
+ } catch {
774
+ // ignore
775
+ }
776
+ } else if (tool === "gemini-cli") {
777
+ try {
778
+ const latestGemini = await findLatestGeminiSession(worktreePath, {
779
+ since: launchStartedAt - 60_000,
780
+ until: finishedAt + 60_000,
781
+ preferClosestTo: finishedAt,
782
+ windowMs: 60 * 60 * 1000,
783
+ cwd: worktreePath,
784
+ });
785
+ if (latestGemini) {
786
+ finalSessionId = latestGemini.id;
787
+ }
788
+ } catch {
789
+ // ignore
790
+ }
791
+ }
792
+
793
+ await saveSession({
794
+ lastWorktreePath: worktreePath,
795
+ lastBranch: branch,
796
+ lastUsedTool: tool,
797
+ toolLabel: toolConfig.displayName ?? tool,
798
+ mode,
799
+ model: model ?? null,
800
+ reasoningLevel: inferenceLevel ?? null,
801
+ skipPermissions: skipPermissions ?? null,
802
+ timestamp: Date.now(),
803
+ repositoryRoot: repoRoot,
804
+ lastSessionId: finalSessionId,
805
+ });
806
+
807
+ // Small buffer before returning to branch list to avoid abrupt screen swap
808
+ await new Promise((resolve) => setTimeout(resolve, 3000));
681
809
  printInfo("Session completed successfully. Returning to main menu...");
682
810
  return;
683
811
  } catch (error) {