@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.
- 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 +119 -48
- 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/prompt.d.ts +6 -0
- package/dist/utils/prompt.d.ts.map +1 -0
- package/dist/utils/prompt.js +57 -0
- package/dist/utils/prompt.js.map +1 -0
- 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 +2 -2
- 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 +145 -61
- package/src/qwen.ts +56 -5
- package/src/utils/__tests__/prompt.test.ts +89 -0
- package/src/utils/prompt.ts +74 -0
- 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,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
|
|
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
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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) {
|