@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.
- package/dist/claude.d.ts +4 -1
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +51 -7
- package/dist/claude.js.map +1 -1
- package/dist/cli/ui/components/App.d.ts +7 -0
- package/dist/cli/ui/components/App.d.ts.map +1 -1
- package/dist/cli/ui/components/App.js +307 -18
- package/dist/cli/ui/components/App.js.map +1 -1
- package/dist/cli/ui/components/screens/BranchQuickStartScreen.d.ts +21 -0
- package/dist/cli/ui/components/screens/BranchQuickStartScreen.d.ts.map +1 -0
- package/dist/cli/ui/components/screens/BranchQuickStartScreen.js +145 -0
- package/dist/cli/ui/components/screens/BranchQuickStartScreen.js.map +1 -0
- package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.d.ts +2 -1
- package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.js +4 -2
- package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/ModelSelectorScreen.js +1 -1
- package/dist/cli/ui/components/screens/SessionSelectorScreen.d.ts +10 -2
- package/dist/cli/ui/components/screens/SessionSelectorScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/SessionSelectorScreen.js +18 -7
- package/dist/cli/ui/components/screens/SessionSelectorScreen.js.map +1 -1
- package/dist/cli/ui/types.d.ts +1 -1
- package/dist/cli/ui/types.d.ts.map +1 -1
- package/dist/cli/ui/utils/continueSession.d.ts +18 -0
- package/dist/cli/ui/utils/continueSession.d.ts.map +1 -0
- package/dist/cli/ui/utils/continueSession.js +67 -0
- package/dist/cli/ui/utils/continueSession.js.map +1 -0
- package/dist/codex.d.ts +4 -1
- package/dist/codex.d.ts.map +1 -1
- package/dist/codex.js +70 -5
- package/dist/codex.js.map +1 -1
- package/dist/config/index.d.ts +9 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +11 -2
- package/dist/config/index.js.map +1 -1
- package/dist/gemini.d.ts +4 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +146 -32
- package/dist/gemini.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +118 -8
- package/dist/index.js.map +1 -1
- package/dist/qwen.d.ts +4 -1
- package/dist/qwen.d.ts.map +1 -1
- package/dist/qwen.js +45 -4
- package/dist/qwen.js.map +1 -1
- package/dist/utils/session.d.ts +82 -0
- package/dist/utils/session.d.ts.map +1 -0
- package/dist/utils/session.js +579 -0
- package/dist/utils/session.js.map +1 -0
- package/package.json +1 -1
- package/src/claude.ts +69 -8
- package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +12 -2
- package/src/cli/ui/__tests__/components/screens/BranchQuickStartScreen.test.tsx +142 -0
- package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +14 -0
- package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +29 -10
- package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +4 -1
- package/src/cli/ui/components/App.tsx +403 -23
- package/src/cli/ui/components/screens/BranchQuickStartScreen.tsx +237 -0
- package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +5 -1
- package/src/cli/ui/components/screens/ModelSelectorScreen.tsx +1 -1
- package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +34 -6
- package/src/cli/ui/types.ts +1 -0
- package/src/cli/ui/utils/continueSession.ts +106 -0
- package/src/codex.ts +91 -6
- package/src/config/index.ts +22 -2
- package/src/gemini.ts +179 -41
- package/src/index.ts +144 -16
- package/src/qwen.ts +56 -5
- 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<
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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:
|
|
169
|
+
stdout: "pipe",
|
|
98
170
|
stderr: childStdio.stderr,
|
|
99
171
|
env: baseEnv,
|
|
100
172
|
} as any);
|
|
101
|
-
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
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.
|
|
108
|
-
" 💡 Recommended: Install Gemini CLI globally for faster startup",
|
|
109
|
-
),
|
|
187
|
+
chalk.green(" ✨ Using locally installed gemini command"),
|
|
110
188
|
);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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"], {
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|