@akiojin/gwt 2.11.1 → 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 (70) 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 +118 -8
  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/session.d.ts +82 -0
  48. package/dist/utils/session.d.ts.map +1 -0
  49. package/dist/utils/session.js +579 -0
  50. package/dist/utils/session.js.map +1 -0
  51. package/package.json +1 -1
  52. package/src/claude.ts +69 -8
  53. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +12 -2
  54. package/src/cli/ui/__tests__/components/screens/BranchQuickStartScreen.test.tsx +142 -0
  55. package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +14 -0
  56. package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +29 -10
  57. package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +4 -1
  58. package/src/cli/ui/components/App.tsx +403 -23
  59. package/src/cli/ui/components/screens/BranchQuickStartScreen.tsx +237 -0
  60. package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +5 -1
  61. package/src/cli/ui/components/screens/ModelSelectorScreen.tsx +1 -1
  62. package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +34 -6
  63. package/src/cli/ui/types.ts +1 -0
  64. package/src/cli/ui/utils/continueSession.ts +106 -0
  65. package/src/codex.ts +91 -6
  66. package/src/config/index.ts +22 -2
  67. package/src/gemini.ts +179 -41
  68. package/src/index.ts +144 -16
  69. package/src/qwen.ts +56 -5
  70. 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,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) {
package/src/qwen.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 { findLatestQwenSessionId } from "./utils/session.js";
5
6
 
6
7
  const QWEN_CLI_PACKAGE = "@qwen-code/qwen-code@latest";
7
8
 
@@ -23,8 +24,9 @@ export async function launchQwenCLI(
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 {
@@ -46,18 +48,26 @@ export async function launchQwenCLI(
46
48
  // Handle execution mode
47
49
  // Note: Qwen CLI doesn't have explicit continue/resume CLI options at startup.
48
50
  // Session management is done via /chat commands during interactive sessions.
51
+ const resumeSessionId =
52
+ options.sessionId && options.sessionId.trim().length > 0
53
+ ? options.sessionId.trim()
54
+ : null;
49
55
  switch (options.mode) {
50
56
  case "continue":
51
57
  console.log(
52
58
  chalk.cyan(
53
- " ⏭️ Starting session (use /chat resume in the CLI to continue)",
59
+ resumeSessionId
60
+ ? ` ⏭️ Starting session (then /chat resume ${resumeSessionId})`
61
+ : " ⏭️ Starting session (use /chat resume in the CLI to continue)",
54
62
  ),
55
63
  );
56
64
  break;
57
65
  case "resume":
58
66
  console.log(
59
67
  chalk.cyan(
60
- " 🔄 Starting session (use /chat resume in the CLI to continue)",
68
+ resumeSessionId
69
+ ? ` 🔄 Starting session (then /chat resume ${resumeSessionId})`
70
+ : " 🔄 Starting session (use /chat resume in the CLI to continue)",
61
71
  ),
62
72
  );
63
73
  break;
@@ -80,6 +90,8 @@ export async function launchQwenCLI(
80
90
  args.push(...options.extraArgs);
81
91
  }
82
92
 
93
+ console.log(chalk.gray(` 📋 Args: ${args.join(" ")}`));
94
+
83
95
  terminal.exitRawMode();
84
96
 
85
97
  const baseEnv = {
@@ -93,10 +105,22 @@ export async function launchQwenCLI(
93
105
  const hasLocalQwen = await isQwenCommandAvailable();
94
106
 
95
107
  try {
108
+ const execChild = async (child: any) => {
109
+ try {
110
+ await child;
111
+ } catch (execError: any) {
112
+ // Treat SIGINT/SIGTERM as normal exit (user pressed Ctrl+C)
113
+ if (execError.signal === "SIGINT" || execError.signal === "SIGTERM") {
114
+ return;
115
+ }
116
+ throw execError;
117
+ }
118
+ };
119
+
96
120
  if (hasLocalQwen) {
97
121
  // Use locally installed qwen command
98
122
  console.log(chalk.green(" ✨ Using locally installed qwen command"));
99
- await execa("qwen", args, {
123
+ const child = execa("qwen", args, {
100
124
  cwd: worktreePath,
101
125
  shell: true,
102
126
  stdin: childStdio.stdin,
@@ -104,6 +128,7 @@ export async function launchQwenCLI(
104
128
  stderr: childStdio.stderr,
105
129
  env: baseEnv,
106
130
  } as any);
131
+ await execChild(child);
107
132
  } else {
108
133
  // Fallback to bunx
109
134
  console.log(
@@ -118,7 +143,7 @@ export async function launchQwenCLI(
118
143
  console.log("");
119
144
  // Wait 2 seconds to let user read the message
120
145
  await new Promise((resolve) => setTimeout(resolve, 2000));
121
- await execa("bunx", [QWEN_CLI_PACKAGE, ...args], {
146
+ const child = execa("bunx", [QWEN_CLI_PACKAGE, ...args], {
122
147
  cwd: worktreePath,
123
148
  shell: true,
124
149
  stdin: childStdio.stdin,
@@ -126,10 +151,36 @@ export async function launchQwenCLI(
126
151
  stderr: childStdio.stderr,
127
152
  env: baseEnv,
128
153
  } as any);
154
+ await execChild(child);
129
155
  }
130
156
  } finally {
131
157
  childStdio.cleanup();
132
158
  }
159
+
160
+ let capturedSessionId: string | null = null;
161
+ try {
162
+ capturedSessionId =
163
+ (await findLatestQwenSessionId(worktreePath)) ??
164
+ resumeSessionId ??
165
+ null;
166
+ } catch {
167
+ capturedSessionId = resumeSessionId ?? null;
168
+ }
169
+
170
+ if (capturedSessionId) {
171
+ console.log(chalk.cyan(`\n 🆔 Session tag: ${capturedSessionId}`));
172
+ console.log(
173
+ chalk.gray(` Resume in Qwen CLI: /chat resume ${capturedSessionId}`),
174
+ );
175
+ } else {
176
+ console.log(
177
+ chalk.yellow(
178
+ "\n ℹ️ Could not determine Qwen session tag automatically.",
179
+ ),
180
+ );
181
+ }
182
+
183
+ return capturedSessionId ? { sessionId: capturedSessionId } : {};
133
184
  } catch (error: any) {
134
185
  const hasLocalQwen = await isQwenCommandAvailable();
135
186
  let errorMessage: string;