@akiojin/gwt 4.11.6 → 4.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/bin/gwt.js +36 -10
  2. package/dist/claude.d.ts +1 -0
  3. package/dist/claude.d.ts.map +1 -1
  4. package/dist/claude.js +81 -24
  5. package/dist/claude.js.map +1 -1
  6. package/dist/cli/ui/App.solid.d.ts.map +1 -1
  7. package/dist/cli/ui/App.solid.js +280 -50
  8. package/dist/cli/ui/App.solid.js.map +1 -1
  9. package/dist/cli/ui/components/solid/QuickStartStep.d.ts.map +1 -1
  10. package/dist/cli/ui/components/solid/QuickStartStep.js +35 -22
  11. package/dist/cli/ui/components/solid/QuickStartStep.js.map +1 -1
  12. package/dist/cli/ui/components/solid/SelectInput.d.ts.map +1 -1
  13. package/dist/cli/ui/components/solid/SelectInput.js +2 -1
  14. package/dist/cli/ui/components/solid/SelectInput.js.map +1 -1
  15. package/dist/cli/ui/components/solid/WizardController.d.ts.map +1 -1
  16. package/dist/cli/ui/components/solid/WizardController.js +67 -13
  17. package/dist/cli/ui/components/solid/WizardController.js.map +1 -1
  18. package/dist/cli/ui/components/solid/WizardSteps.d.ts +5 -0
  19. package/dist/cli/ui/components/solid/WizardSteps.d.ts.map +1 -1
  20. package/dist/cli/ui/components/solid/WizardSteps.js +50 -70
  21. package/dist/cli/ui/components/solid/WizardSteps.js.map +1 -1
  22. package/dist/cli/ui/core/theme.d.ts +9 -0
  23. package/dist/cli/ui/core/theme.d.ts.map +1 -1
  24. package/dist/cli/ui/core/theme.js +21 -0
  25. package/dist/cli/ui/core/theme.js.map +1 -1
  26. package/dist/cli/ui/screens/solid/BranchListScreen.d.ts +9 -2
  27. package/dist/cli/ui/screens/solid/BranchListScreen.d.ts.map +1 -1
  28. package/dist/cli/ui/screens/solid/BranchListScreen.js +101 -28
  29. package/dist/cli/ui/screens/solid/BranchListScreen.js.map +1 -1
  30. package/dist/cli/ui/screens/solid/ConfirmScreen.d.ts +2 -1
  31. package/dist/cli/ui/screens/solid/ConfirmScreen.d.ts.map +1 -1
  32. package/dist/cli/ui/screens/solid/ConfirmScreen.js +11 -3
  33. package/dist/cli/ui/screens/solid/ConfirmScreen.js.map +1 -1
  34. package/dist/cli/ui/screens/solid/EnvironmentScreen.d.ts.map +1 -1
  35. package/dist/cli/ui/screens/solid/EnvironmentScreen.js +9 -10
  36. package/dist/cli/ui/screens/solid/EnvironmentScreen.js.map +1 -1
  37. package/dist/cli/ui/screens/solid/LogScreen.d.ts +7 -1
  38. package/dist/cli/ui/screens/solid/LogScreen.d.ts.map +1 -1
  39. package/dist/cli/ui/screens/solid/LogScreen.js +254 -16
  40. package/dist/cli/ui/screens/solid/LogScreen.js.map +1 -1
  41. package/dist/cli/ui/screens/solid/ProfileEnvScreen.d.ts.map +1 -1
  42. package/dist/cli/ui/screens/solid/ProfileEnvScreen.js +8 -5
  43. package/dist/cli/ui/screens/solid/ProfileEnvScreen.js.map +1 -1
  44. package/dist/cli/ui/screens/solid/SelectorScreen.d.ts.map +1 -1
  45. package/dist/cli/ui/screens/solid/SelectorScreen.js +12 -4
  46. package/dist/cli/ui/screens/solid/SelectorScreen.js.map +1 -1
  47. package/dist/cli/ui/types.d.ts +1 -0
  48. package/dist/cli/ui/types.d.ts.map +1 -1
  49. package/dist/cli/ui/utils/branchFormatter.d.ts +1 -0
  50. package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
  51. package/dist/cli/ui/utils/branchFormatter.js +29 -7
  52. package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
  53. package/dist/cli/ui/utils/continueSession.d.ts +14 -0
  54. package/dist/cli/ui/utils/continueSession.d.ts.map +1 -1
  55. package/dist/cli/ui/utils/continueSession.js +61 -3
  56. package/dist/cli/ui/utils/continueSession.js.map +1 -1
  57. package/dist/cli/ui/utils/installedVersionCache.d.ts +33 -0
  58. package/dist/cli/ui/utils/installedVersionCache.d.ts.map +1 -0
  59. package/dist/cli/ui/utils/installedVersionCache.js +59 -0
  60. package/dist/cli/ui/utils/installedVersionCache.js.map +1 -0
  61. package/dist/cli/ui/utils/modelOptions.d.ts.map +1 -1
  62. package/dist/cli/ui/utils/modelOptions.js +16 -0
  63. package/dist/cli/ui/utils/modelOptions.js.map +1 -1
  64. package/dist/cli/ui/utils/versionCache.d.ts +37 -0
  65. package/dist/cli/ui/utils/versionCache.d.ts.map +1 -0
  66. package/dist/cli/ui/utils/versionCache.js +70 -0
  67. package/dist/cli/ui/utils/versionCache.js.map +1 -0
  68. package/dist/cli/ui/utils/versionFetcher.d.ts +41 -0
  69. package/dist/cli/ui/utils/versionFetcher.d.ts.map +1 -0
  70. package/dist/cli/ui/utils/versionFetcher.js +89 -0
  71. package/dist/cli/ui/utils/versionFetcher.js.map +1 -0
  72. package/dist/codex.d.ts +1 -0
  73. package/dist/codex.d.ts.map +1 -1
  74. package/dist/codex.js +95 -25
  75. package/dist/codex.js.map +1 -1
  76. package/dist/config/index.d.ts.map +1 -1
  77. package/dist/config/index.js +10 -1
  78. package/dist/config/index.js.map +1 -1
  79. package/dist/gemini.d.ts +1 -0
  80. package/dist/gemini.d.ts.map +1 -1
  81. package/dist/gemini.js +36 -3
  82. package/dist/gemini.js.map +1 -1
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +35 -2
  85. package/dist/index.js.map +1 -1
  86. package/dist/launcher.d.ts.map +1 -1
  87. package/dist/launcher.js +43 -8
  88. package/dist/launcher.js.map +1 -1
  89. package/dist/logging/agentOutput.d.ts +21 -0
  90. package/dist/logging/agentOutput.d.ts.map +1 -0
  91. package/dist/logging/agentOutput.js +164 -0
  92. package/dist/logging/agentOutput.js.map +1 -0
  93. package/dist/logging/formatter.d.ts.map +1 -1
  94. package/dist/logging/formatter.js +18 -4
  95. package/dist/logging/formatter.js.map +1 -1
  96. package/dist/logging/logger.d.ts.map +1 -1
  97. package/dist/logging/logger.js +2 -0
  98. package/dist/logging/logger.js.map +1 -1
  99. package/dist/logging/reader.d.ts +22 -0
  100. package/dist/logging/reader.d.ts.map +1 -1
  101. package/dist/logging/reader.js +116 -1
  102. package/dist/logging/reader.js.map +1 -1
  103. package/dist/opentui/index.solid.js +2575 -888
  104. package/dist/services/codingAgentResolver.d.ts.map +1 -1
  105. package/dist/services/codingAgentResolver.js +8 -6
  106. package/dist/services/codingAgentResolver.js.map +1 -1
  107. package/dist/services/dependency-installer.js +2 -2
  108. package/dist/services/dependency-installer.js.map +1 -1
  109. package/dist/shared/codingAgentConstants.d.ts +3 -0
  110. package/dist/shared/codingAgentConstants.d.ts.map +1 -1
  111. package/dist/shared/codingAgentConstants.js +66 -0
  112. package/dist/shared/codingAgentConstants.js.map +1 -1
  113. package/dist/utils/bun-runtime.d.ts +12 -0
  114. package/dist/utils/bun-runtime.d.ts.map +1 -0
  115. package/dist/utils/bun-runtime.js +13 -0
  116. package/dist/utils/bun-runtime.js.map +1 -0
  117. package/dist/utils/session/common.d.ts +8 -0
  118. package/dist/utils/session/common.d.ts.map +1 -1
  119. package/dist/utils/session/common.js +22 -0
  120. package/dist/utils/session/common.js.map +1 -1
  121. package/dist/utils/session/parsers/claude.d.ts +10 -4
  122. package/dist/utils/session/parsers/claude.d.ts.map +1 -1
  123. package/dist/utils/session/parsers/claude.js +64 -18
  124. package/dist/utils/session/parsers/claude.js.map +1 -1
  125. package/dist/utils/session/parsers/codex.d.ts.map +1 -1
  126. package/dist/utils/session/parsers/codex.js +47 -28
  127. package/dist/utils/session/parsers/codex.js.map +1 -1
  128. package/dist/utils/session/parsers/gemini.d.ts.map +1 -1
  129. package/dist/utils/session/parsers/gemini.js +43 -6
  130. package/dist/utils/session/parsers/gemini.js.map +1 -1
  131. package/dist/utils/session/parsers/opencode.d.ts.map +1 -1
  132. package/dist/utils/session/parsers/opencode.js +43 -6
  133. package/dist/utils/session/parsers/opencode.js.map +1 -1
  134. package/dist/utils/session/types.d.ts +7 -0
  135. package/dist/utils/session/types.d.ts.map +1 -1
  136. package/dist/web/client/src/components/ui/alert.d.ts +1 -1
  137. package/dist/worktree.d.ts +4 -1
  138. package/dist/worktree.d.ts.map +1 -1
  139. package/dist/worktree.js +21 -15
  140. package/dist/worktree.js.map +1 -1
  141. package/package.json +2 -1
  142. package/src/claude.ts +99 -28
  143. package/src/cli/ui/App.solid.tsx +373 -51
  144. package/src/cli/ui/__tests__/solid/AppSolid.cleanup.test.tsx +921 -1
  145. package/src/cli/ui/__tests__/solid/BranchListScreen.test.tsx +105 -5
  146. package/src/cli/ui/__tests__/solid/ConfirmScreen.test.tsx +77 -0
  147. package/src/cli/ui/__tests__/solid/LogScreen.test.tsx +351 -0
  148. package/src/cli/ui/__tests__/solid/components/QuickStartStep.test.tsx +73 -2
  149. package/src/cli/ui/__tests__/solid/components/WizardController.test.tsx +71 -0
  150. package/src/cli/ui/__tests__/solid/components/WizardSteps.test.tsx +95 -2
  151. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +72 -45
  152. package/src/cli/ui/components/solid/QuickStartStep.tsx +35 -23
  153. package/src/cli/ui/components/solid/SearchInput.tsx +1 -1
  154. package/src/cli/ui/components/solid/SelectInput.tsx +4 -0
  155. package/src/cli/ui/components/solid/WizardController.tsx +85 -12
  156. package/src/cli/ui/components/solid/WizardSteps.tsx +78 -90
  157. package/src/cli/ui/core/theme.ts +32 -0
  158. package/src/cli/ui/hooks/solid/useAsyncOperation.ts +8 -6
  159. package/src/cli/ui/hooks/solid/useGitOperations.ts +6 -5
  160. package/src/cli/ui/screens/solid/BranchListScreen.tsx +135 -32
  161. package/src/cli/ui/screens/solid/ConfirmScreen.tsx +20 -8
  162. package/src/cli/ui/screens/solid/EnvironmentScreen.tsx +22 -20
  163. package/src/cli/ui/screens/solid/LogScreen.tsx +364 -35
  164. package/src/cli/ui/screens/solid/ProfileEnvScreen.tsx +19 -15
  165. package/src/cli/ui/screens/solid/SelectorScreen.tsx +25 -14
  166. package/src/cli/ui/screens/solid/SettingsScreen.tsx +5 -3
  167. package/src/cli/ui/types.ts +1 -0
  168. package/src/cli/ui/utils/__tests__/branchFormatter.test.ts +53 -6
  169. package/src/cli/ui/utils/__tests__/installedVersionCache.test.ts +46 -0
  170. package/src/cli/ui/utils/branchFormatter.ts +35 -7
  171. package/src/cli/ui/utils/continueSession.ts +90 -3
  172. package/src/cli/ui/utils/installedVersionCache.ts +84 -0
  173. package/src/cli/ui/utils/modelOptions.test.ts +6 -0
  174. package/src/cli/ui/utils/modelOptions.ts +16 -0
  175. package/src/cli/ui/utils/versionCache.ts +93 -0
  176. package/src/cli/ui/utils/versionFetcher.ts +120 -0
  177. package/src/codex.ts +124 -26
  178. package/src/config/__tests__/saveSession.test.ts +2 -2
  179. package/src/config/index.ts +11 -1
  180. package/src/gemini.ts +50 -4
  181. package/src/index.test.ts +16 -10
  182. package/src/index.ts +41 -1
  183. package/src/launcher.ts +49 -8
  184. package/src/logging/agentOutput.ts +216 -0
  185. package/src/logging/formatter.ts +23 -4
  186. package/src/logging/logger.ts +2 -0
  187. package/src/logging/reader.ts +165 -1
  188. package/src/services/__tests__/BatchMergeService.test.ts +34 -14
  189. package/src/services/codingAgentResolver.ts +12 -5
  190. package/src/services/dependency-installer.ts +2 -2
  191. package/src/shared/codingAgentConstants.ts +73 -0
  192. package/src/utils/bun-runtime.ts +29 -0
  193. package/src/utils/session/common.ts +28 -0
  194. package/src/utils/session/parsers/claude.ts +79 -29
  195. package/src/utils/session/parsers/codex.ts +49 -26
  196. package/src/utils/session/parsers/gemini.ts +46 -5
  197. package/src/utils/session/parsers/opencode.ts +46 -5
  198. package/src/utils/session/types.ts +4 -0
  199. package/src/worktree.ts +28 -15
package/src/codex.ts CHANGED
@@ -13,8 +13,18 @@ import {
13
13
  waitForCodexSessionId,
14
14
  } from "./utils/session.js";
15
15
  import { findCommand } from "./utils/command.js";
16
+ import {
17
+ runAgentWithPty,
18
+ shouldCaptureAgentOutput,
19
+ } from "./logging/agentOutput.js";
20
+ import { createLogger } from "./logging/logger.js";
21
+ import {
22
+ shouldEnableCodexSkillsFlag,
23
+ withCodexSkillsFlag,
24
+ } from "./shared/codingAgentConstants.js";
16
25
 
17
26
  const CODEX_CLI_PACKAGE = "@openai/codex";
27
+ const logger = createLogger({ category: "codex" });
18
28
 
19
29
  /**
20
30
  * Reasoning effort levels supported by Codex CLI.
@@ -43,8 +53,6 @@ export const buildDefaultCodexArgs = (
43
53
  ): string[] => [
44
54
  "--enable",
45
55
  "web_search_request",
46
- "--enable",
47
- "skills",
48
56
  `--model=${model}`,
49
57
  "--sandbox",
50
58
  "workspace-write",
@@ -96,6 +104,7 @@ export async function launchCodexCLI(
96
104
  model?: string;
97
105
  reasoningEffort?: CodexReasoningEffort;
98
106
  sessionId?: string | null;
107
+ branch?: string | null;
99
108
  version?: string | null;
100
109
  } = {},
101
110
  ): Promise<{ sessionId?: string | null }> {
@@ -166,7 +175,18 @@ export async function launchCodexCLI(
166
175
  args.push(...options.extraArgs);
167
176
  }
168
177
 
169
- const codexArgs = buildDefaultCodexArgs(model, reasoningEffort);
178
+ const requestedVersion = options.version ?? "latest";
179
+ const codexLookup = await findCommand("codex");
180
+ const skillsFlagVersion =
181
+ requestedVersion === "installed"
182
+ ? (codexLookup.version ?? null)
183
+ : requestedVersion === "latest"
184
+ ? null
185
+ : requestedVersion;
186
+ const codexArgs = withCodexSkillsFlag(
187
+ buildDefaultCodexArgs(model, reasoningEffort),
188
+ shouldEnableCodexSkillsFlag(skillsFlagVersion),
189
+ );
170
190
 
171
191
  args.push(...codexArgs);
172
192
 
@@ -175,8 +195,6 @@ export async function launchCodexCLI(
175
195
  terminal.exitRawMode();
176
196
  resetTerminalModes(terminal.stdout);
177
197
 
178
- const childStdio = createChildStdio();
179
-
180
198
  const env = Object.fromEntries(
181
199
  Object.entries({
182
200
  ...process.env,
@@ -185,26 +203,116 @@ export async function launchCodexCLI(
185
203
  (entry): entry is [string, string] => typeof entry[1] === "string",
186
204
  ),
187
205
  );
206
+ const captureOutput = shouldCaptureAgentOutput(env);
207
+ const childStdio = captureOutput ? null : createChildStdio();
188
208
 
189
- // Auto-detect locally installed codex command
190
- const codexLookup = await findCommand("codex");
209
+ // codexLookup is used to decide local vs bunx execution
191
210
 
192
211
  const execChild = async (child: Promise<unknown>) => {
193
212
  try {
194
- await child;
213
+ const result = (await child) as {
214
+ exitCode?: number | null;
215
+ signal?: string | null;
216
+ };
217
+ return {
218
+ exitCode: result.exitCode ?? 0,
219
+ signal: result.signal ?? null,
220
+ };
195
221
  } catch (execError: unknown) {
196
222
  // Treat SIGINT/SIGTERM as normal exit (user pressed Ctrl+C)
197
223
  const signal = (execError as { signal?: unknown })?.signal;
224
+ const exitCode =
225
+ (execError as { exitCode?: number | null })?.exitCode ?? null;
198
226
  if (signal === "SIGINT" || signal === "SIGTERM") {
227
+ return { exitCode, signal };
228
+ }
229
+ throw execError;
230
+ }
231
+ };
232
+ // Treat SIGINT (2), SIGTERM (15) as normal exit signals (user interrupts)
233
+ const isNormalExitSignal = (signal?: number | null) =>
234
+ signal === 2 || signal === 15;
235
+ const runCommand = async (command: string, commandArgs: string[]) => {
236
+ const runStartedAt = Date.now();
237
+ if (captureOutput) {
238
+ const result = await runAgentWithPty({
239
+ command,
240
+ args: commandArgs,
241
+ cwd: worktreePath,
242
+ env,
243
+ agentId: "codex-cli",
244
+ });
245
+ const durationMs = Date.now() - runStartedAt;
246
+ const exitCode = result.exitCode ?? null;
247
+ const signal = result.signal ?? null;
248
+ const signalIsNormal = isNormalExitSignal(signal);
249
+ const hasError =
250
+ (!signalIsNormal && signal !== null && signal !== undefined) ||
251
+ (exitCode !== null && exitCode !== 0);
252
+ if (hasError) {
253
+ logger.error({ exitCode, signal, durationMs }, "Codex CLI exited");
254
+ } else {
255
+ logger.info({ exitCode, signal, durationMs }, "Codex CLI exited");
256
+ }
257
+ if (signalIsNormal) {
199
258
  return;
200
259
  }
260
+ if (signal !== null && signal !== undefined) {
261
+ throw new Error(`Codex CLI terminated by signal ${signal}`);
262
+ }
263
+ if (exitCode !== null && exitCode !== 0) {
264
+ throw new Error(
265
+ `Codex CLI exited with code ${exitCode ?? "unknown"}`,
266
+ );
267
+ }
268
+ return;
269
+ }
270
+
271
+ if (!childStdio) {
272
+ return;
273
+ }
274
+
275
+ try {
276
+ const child = execa(command, commandArgs, {
277
+ cwd: worktreePath,
278
+ stdin: childStdio.stdin,
279
+ stdout: childStdio.stdout,
280
+ stderr: childStdio.stderr,
281
+ env,
282
+ });
283
+ const result = await execChild(child);
284
+ const durationMs = Date.now() - runStartedAt;
285
+ logger.info(
286
+ {
287
+ exitCode: result.exitCode ?? null,
288
+ signal: result.signal,
289
+ durationMs,
290
+ },
291
+ "Codex CLI exited",
292
+ );
293
+ } catch (execError: unknown) {
294
+ const durationMs = Date.now() - runStartedAt;
295
+ const exitCode =
296
+ (execError as { exitCode?: number | null })?.exitCode ?? null;
297
+ const signal =
298
+ (execError as { signal?: string | null })?.signal ?? null;
299
+ logger.error({ exitCode, signal, durationMs }, "Codex CLI failed");
201
300
  throw execError;
202
301
  }
203
302
  };
204
303
 
205
304
  // Determine execution strategy based on version selection
206
305
  // FR-063b: "installed" option only appears when local command exists
207
- const selectedVersion = options.version ?? "installed";
306
+ let selectedVersion = requestedVersion;
307
+
308
+ if (requestedVersion === "installed" && !codexLookup.path) {
309
+ writeTerminalLine(
310
+ chalk.yellow(
311
+ " ⚠️ Installed codex command not found. Falling back to latest.",
312
+ ),
313
+ );
314
+ selectedVersion = "latest";
315
+ }
208
316
 
209
317
  // Log version information (FR-072)
210
318
  if (selectedVersion === "installed") {
@@ -220,30 +328,16 @@ export async function launchCodexCLI(
220
328
  writeTerminalLine(
221
329
  chalk.green(" ✨ Using locally installed codex command"),
222
330
  );
223
- const child = execa(codexLookup.path, args, {
224
- cwd: worktreePath,
225
- stdin: childStdio.stdin,
226
- stdout: childStdio.stdout,
227
- stderr: childStdio.stderr,
228
- env,
229
- });
230
- await execChild(child);
331
+ await runCommand(codexLookup.path, args);
231
332
  } else {
232
333
  // FR-067, FR-068: Use bunx with version suffix for latest/specific versions
233
334
  const packageWithVersion = `${CODEX_CLI_PACKAGE}@${selectedVersion}`;
234
335
  writeTerminalLine(chalk.cyan(` 🔄 Using bunx ${packageWithVersion}`));
235
336
 
236
- const child = execa("bunx", [packageWithVersion, ...args], {
237
- cwd: worktreePath,
238
- stdin: childStdio.stdin,
239
- stdout: childStdio.stdout,
240
- stderr: childStdio.stderr,
241
- env,
242
- });
243
- await execChild(child);
337
+ await runCommand("bunx", [packageWithVersion, ...args]);
244
338
  }
245
339
  } finally {
246
- childStdio.cleanup();
340
+ childStdio?.cleanup();
247
341
  }
248
342
 
249
343
  // File-based session detection only - no stdout capture
@@ -257,6 +351,10 @@ export async function launchCodexCLI(
257
351
  preferClosestTo: finishedAt,
258
352
  windowMs: 10 * 60 * 1000,
259
353
  cwd: worktreePath,
354
+ branch: options.branch ?? null,
355
+ worktrees: options.branch
356
+ ? [{ path: worktreePath, branch: options.branch }]
357
+ : null,
260
358
  });
261
359
  const detectedSessionId = latest?.id ?? null;
262
360
  // When we explicitly resumed a specific session, keep that ID as the source of truth.
@@ -53,7 +53,7 @@ describe("saveSession", () => {
53
53
  expect(loaded?.history?.[0]?.toolVersion).toBe("2.1.1");
54
54
  });
55
55
 
56
- it("should save null toolVersion when not provided", async () => {
56
+ it("should save latest toolVersion when not provided", async () => {
57
57
  // Arrange
58
58
  const sessionData = {
59
59
  lastWorktreePath: testRepoRoot,
@@ -73,7 +73,7 @@ describe("saveSession", () => {
73
73
  // Assert
74
74
  expect(loaded).not.toBeNull();
75
75
  expect(loaded?.history).toHaveLength(1);
76
- expect(loaded?.history?.[0]?.toolVersion).toBeNull();
76
+ expect(loaded?.history?.[0]?.toolVersion).toBe("latest");
77
77
  });
78
78
 
79
79
  it("should preserve toolVersion across multiple saves", async () => {
@@ -48,6 +48,14 @@ const DEFAULT_CONFIG: AppConfig = {
48
48
  worktreeNamingPattern: "{repo}-{branch}",
49
49
  };
50
50
 
51
+ function normalizeToolVersion(version?: string | null): string {
52
+ if (!version) {
53
+ return "latest";
54
+ }
55
+ const trimmed = version.trim();
56
+ return trimmed.length > 0 ? trimmed : "latest";
57
+ }
58
+
51
59
  /**
52
60
  * 設定ファイルを読み込む
53
61
  */
@@ -121,6 +129,7 @@ export async function saveSession(
121
129
  try {
122
130
  const sessionPath = getSessionFilePath(sessionData.repositoryRoot);
123
131
  const sessionDir = path.dirname(sessionPath);
132
+ const resolvedToolVersion = normalizeToolVersion(sessionData.toolVersion);
124
133
 
125
134
  // ディレクトリを作成
126
135
  await mkdir(sessionDir, { recursive: true });
@@ -154,7 +163,7 @@ export async function saveSession(
154
163
  model: sessionData.model ?? null,
155
164
  reasoningLevel: sessionData.reasoningLevel ?? null,
156
165
  skipPermissions: sessionData.skipPermissions ?? false,
157
- toolVersion: sessionData.toolVersion ?? null,
166
+ toolVersion: resolvedToolVersion,
158
167
  timestamp: sessionData.timestamp,
159
168
  };
160
169
  existingHistory = [...existingHistory, entry].slice(-100); // keep latest 100
@@ -166,6 +175,7 @@ export async function saveSession(
166
175
  lastSessionId: sessionData.lastSessionId ?? null,
167
176
  reasoningLevel: sessionData.reasoningLevel ?? null,
168
177
  skipPermissions: sessionData.skipPermissions ?? false,
178
+ toolVersion: resolvedToolVersion,
169
179
  };
170
180
 
171
181
  await writeFile(sessionPath, JSON.stringify(payload, null, 2), "utf-8");
package/src/gemini.ts CHANGED
@@ -9,6 +9,10 @@ import {
9
9
  } from "./utils/terminal.js";
10
10
  import { findCommand } from "./utils/command.js";
11
11
  import { findLatestGeminiSessionId } from "./utils/session.js";
12
+ import {
13
+ runAgentWithPty,
14
+ shouldCaptureAgentOutput,
15
+ } from "./logging/agentOutput.js";
12
16
 
13
17
  const GEMINI_CLI_PACKAGE = "@google/gemini-cli";
14
18
 
@@ -45,6 +49,7 @@ export async function launchGeminiCLI(
45
49
  envOverrides?: Record<string, string>;
46
50
  model?: string;
47
51
  sessionId?: string | null;
52
+ branch?: string | null;
48
53
  version?: string | null;
49
54
  } = {},
50
55
  ): Promise<{ sessionId?: string | null }> {
@@ -154,8 +159,8 @@ export async function launchGeminiCLI(
154
159
  (entry): entry is [string, string] => typeof entry[1] === "string",
155
160
  ),
156
161
  );
157
-
158
- const childStdio = createChildStdio();
162
+ const captureOutput = shouldCaptureAgentOutput(baseEnv);
163
+ const childStdio = captureOutput ? null : createChildStdio();
159
164
 
160
165
  // Auto-detect locally installed gemini command
161
166
  const geminiLookup = await findCommand("gemini");
@@ -166,7 +171,17 @@ export async function launchGeminiCLI(
166
171
 
167
172
  // Determine execution strategy based on version selection
168
173
  // FR-063b: "installed" option only appears when local command exists
169
- const selectedVersion = options.version ?? "installed";
174
+ const requestedVersion = options.version ?? "latest";
175
+ let selectedVersion = requestedVersion;
176
+
177
+ if (requestedVersion === "installed" && !geminiLookup.path) {
178
+ writeTerminalLine(
179
+ chalk.yellow(
180
+ " ⚠️ Installed gemini command not found. Falling back to latest.",
181
+ ),
182
+ );
183
+ selectedVersion = "latest";
184
+ }
170
185
 
171
186
  // Log version information (FR-072)
172
187
  if (selectedVersion === "installed") {
@@ -188,8 +203,35 @@ export async function launchGeminiCLI(
188
203
  throw execError;
189
204
  }
190
205
  };
206
+ // Treat SIGHUP (1), SIGINT (2), SIGTERM (15) as normal exit signals
207
+ // SIGHUP can occur when the PTY closes, SIGINT/SIGTERM are user interrupts
208
+ const isNormalExitSignal = (signal?: number | null) =>
209
+ signal === 1 || signal === 2 || signal === 15;
191
210
 
192
211
  const run = async (cmd: string, args: string[]) => {
212
+ if (captureOutput) {
213
+ const result = await runAgentWithPty({
214
+ command: cmd,
215
+ args,
216
+ cwd: worktreePath,
217
+ env: baseEnv,
218
+ agentId: "gemini-cli",
219
+ });
220
+ if (isNormalExitSignal(result.signal)) {
221
+ return;
222
+ }
223
+ if (result.exitCode !== null && result.exitCode !== 0) {
224
+ throw new Error(
225
+ `Gemini CLI exited with code ${result.exitCode ?? "unknown"}`,
226
+ );
227
+ }
228
+ return;
229
+ }
230
+
231
+ if (!childStdio) {
232
+ return;
233
+ }
234
+
193
235
  const child = execa(cmd, args, {
194
236
  cwd: worktreePath,
195
237
  stdin: childStdio.stdin,
@@ -237,7 +279,7 @@ export async function launchGeminiCLI(
237
279
  }
238
280
  }
239
281
  } finally {
240
- childStdio.cleanup();
282
+ childStdio?.cleanup();
241
283
  }
242
284
 
243
285
  const explicitResumeSucceeded = usedExplicitSessionId && !fellBackToLatest;
@@ -253,6 +295,10 @@ export async function launchGeminiCLI(
253
295
  capturedSessionId =
254
296
  (await findLatestGeminiSessionId(worktreePath, {
255
297
  cwd: worktreePath,
298
+ branch: options.branch ?? null,
299
+ worktrees: options.branch
300
+ ? [{ path: worktreePath, branch: options.branch }]
301
+ : null,
256
302
  })) ?? null;
257
303
  } catch {
258
304
  capturedSessionId = null;
package/src/index.test.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { describe, it, expect, spyOn, beforeEach, afterEach } from "bun:test";
2
+ import { getPackageVersion } from "./utils";
2
3
  import * as utils from "./utils";
3
4
 
4
5
  // showVersion関数のテスト(TDD Green phase)
@@ -28,7 +29,6 @@ describe("showVersion via CLI args", () => {
28
29
  consoleLogSpy.mockRestore();
29
30
  consoleErrorSpy.mockRestore();
30
31
  processExitSpy.mockRestore();
31
-
32
32
  // process.argvを復元
33
33
  process.argv = originalArgv;
34
34
  });
@@ -37,32 +37,34 @@ describe("showVersion via CLI args", () => {
37
37
  // Arrange: CLIフラグを設定
38
38
  process.argv = ["node", "index.js", "--version"];
39
39
 
40
- // getPackageVersion()をモック
41
- const mockVersion = "2.6.1";
42
- spyOn(utils, "getPackageVersion").mockResolvedValue(mockVersion);
40
+ const expectedVersion = await getPackageVersion();
41
+ if (!expectedVersion) {
42
+ throw new Error("Failed to resolve package version for test.");
43
+ }
43
44
 
44
45
  // Act: main()を呼び出す
45
46
  const { main } = await import("./index");
46
47
  await main();
47
48
 
48
49
  // Assert: 標準出力にバージョンが表示されることを期待
49
- expect(consoleLogSpy).toHaveBeenCalledWith(mockVersion);
50
+ expect(consoleLogSpy).toHaveBeenCalledWith(expectedVersion);
50
51
  }, 30000);
51
52
 
52
53
  it("正常系: -vフラグでバージョンを表示する", async () => {
53
54
  // Arrange: CLIフラグを設定
54
55
  process.argv = ["node", "index.js", "-v"];
55
56
 
56
- // getPackageVersion()をモック
57
- const mockVersion = "1.12.3";
58
- spyOn(utils, "getPackageVersion").mockResolvedValue(mockVersion);
57
+ const expectedVersion = await getPackageVersion();
58
+ if (!expectedVersion) {
59
+ throw new Error("Failed to resolve package version for test.");
60
+ }
59
61
 
60
62
  // Act: main()を呼び出す
61
63
  const { main } = await import("./index");
62
64
  await main();
63
65
 
64
66
  // Assert: 標準出力にバージョンが表示されることを期待
65
- expect(consoleLogSpy).toHaveBeenCalledWith(mockVersion);
67
+ expect(consoleLogSpy).toHaveBeenCalledWith(expectedVersion);
66
68
  }, 30000);
67
69
 
68
70
  // Note: This test is skipped due to module caching issues in CI environment
@@ -72,7 +74,10 @@ describe("showVersion via CLI args", () => {
72
74
  process.argv = ["node", "index.js", "--version"];
73
75
 
74
76
  // getPackageVersion()をモックしてnullを返す
75
- spyOn(utils, "getPackageVersion").mockResolvedValue(null);
77
+ const getPackageVersionSpy = spyOn(
78
+ utils,
79
+ "getPackageVersion",
80
+ ).mockResolvedValue(null);
76
81
 
77
82
  // Act: main()を呼び出す
78
83
  const { main } = await import("./index");
@@ -83,5 +88,6 @@ describe("showVersion via CLI args", () => {
83
88
  expect.stringContaining("Error"),
84
89
  );
85
90
  expect(processExitSpy).toHaveBeenCalledWith(1);
91
+ getPackageVersionSpy.mockRestore();
86
92
  });
87
93
  });
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  isProtectedBranchName,
28
28
  switchToProtectedBranch,
29
29
  resolveWorktreePathForBranch,
30
+ listAllWorktrees,
30
31
  repairWorktreePath,
31
32
  } from "./worktree.js";
32
33
  import {
@@ -192,6 +193,11 @@ function performTerminalCleanup(): void {
192
193
  * Returns SelectionResult if user made selections, undefined if user quit
193
194
  */
194
195
  async function mainSolidUI(): Promise<SelectionResult | undefined> {
196
+ if (!("Bun" in globalThis)) {
197
+ throw new Error(
198
+ "OpenTUI requires the Bun runtime. Run with Bun (e.g. bunx @akiojin/gwt@latest).",
199
+ );
200
+ }
195
201
  const { renderSolidApp } = await import("./opentui/index.solid.js");
196
202
  const terminal = getTerminalStreams();
197
203
 
@@ -629,6 +635,7 @@ export async function handleAIToolWorkflow(
629
635
  model?: string;
630
636
  sessionId?: string | null;
631
637
  chrome?: boolean;
638
+ branch?: string | null;
632
639
  version?: string | null;
633
640
  } = {
634
641
  mode:
@@ -641,6 +648,7 @@ export async function handleAIToolWorkflow(
641
648
  envOverrides: sharedEnv,
642
649
  sessionId: resumeSessionId,
643
650
  chrome: true,
651
+ branch,
644
652
  version: toolVersion ?? null,
645
653
  };
646
654
  if (normalizedModel) {
@@ -655,6 +663,7 @@ export async function handleAIToolWorkflow(
655
663
  model?: string;
656
664
  reasoningEffort?: CodexReasoningEffort;
657
665
  sessionId?: string | null;
666
+ branch?: string | null;
658
667
  version?: string | null;
659
668
  } = {
660
669
  mode:
@@ -666,6 +675,7 @@ export async function handleAIToolWorkflow(
666
675
  bypassApprovals: skipPermissions,
667
676
  envOverrides: sharedEnv,
668
677
  sessionId: resumeSessionId,
678
+ branch,
669
679
  version: toolVersion ?? null,
670
680
  };
671
681
  if (normalizedModel) {
@@ -683,6 +693,7 @@ export async function handleAIToolWorkflow(
683
693
  envOverrides?: Record<string, string>;
684
694
  model?: string;
685
695
  sessionId?: string | null;
696
+ branch?: string | null;
686
697
  version?: string | null;
687
698
  } = {
688
699
  mode:
@@ -694,6 +705,7 @@ export async function handleAIToolWorkflow(
694
705
  skipPermissions,
695
706
  envOverrides: sharedEnv,
696
707
  sessionId: resumeSessionId,
708
+ branch,
697
709
  version: toolVersion ?? null,
698
710
  };
699
711
  if (normalizedModel) {
@@ -715,6 +727,9 @@ export async function handleAIToolWorkflow(
715
727
  cwd: worktreePath,
716
728
  sharedEnv,
717
729
  };
730
+ if (tool === "opencode" && normalizedModel) {
731
+ customLaunchOptions.extraArgs = ["-m", normalizedModel];
732
+ }
718
733
  if (toolVersion) {
719
734
  customLaunchOptions.version = toolVersion;
720
735
  }
@@ -734,10 +749,29 @@ export async function handleAIToolWorkflow(
734
749
  resumeSessionId ??
735
750
  null;
736
751
 
752
+ let resolvedWorktrees: { path: string; branch: string }[] | null = null;
753
+ if (branch) {
754
+ try {
755
+ const allWorktrees = await listAllWorktrees();
756
+ resolvedWorktrees = allWorktrees
757
+ .filter((entry) => entry?.path && entry?.branch)
758
+ .map((entry) => ({ path: entry.path, branch: entry.branch }));
759
+ } catch {
760
+ resolvedWorktrees = null;
761
+ }
762
+ }
763
+ const worktreeLookupOptions =
764
+ resolvedWorktrees && resolvedWorktrees.length > 0
765
+ ? { worktrees: resolvedWorktrees }
766
+ : {};
767
+
737
768
  if (!finalSessionId && tool === "claude-code") {
738
769
  try {
739
770
  finalSessionId =
740
- (await findLatestClaudeSessionId(worktreePath)) ?? null;
771
+ (await findLatestClaudeSessionId(worktreePath, {
772
+ branch,
773
+ ...worktreeLookupOptions,
774
+ })) ?? null;
741
775
  } catch {
742
776
  finalSessionId = null;
743
777
  }
@@ -752,6 +786,8 @@ export async function handleAIToolWorkflow(
752
786
  preferClosestTo: finishedAt,
753
787
  windowMs: 60 * 60 * 1000,
754
788
  cwd: worktreePath,
789
+ branch,
790
+ ...worktreeLookupOptions,
755
791
  });
756
792
  if (latest) {
757
793
  finalSessionId = latest.id;
@@ -766,6 +802,8 @@ export async function handleAIToolWorkflow(
766
802
  until: finishedAt + 60_000,
767
803
  preferClosestTo: finishedAt,
768
804
  windowMs: 60 * 60 * 1000,
805
+ branch,
806
+ ...worktreeLookupOptions,
769
807
  });
770
808
  if (latestClaude) {
771
809
  finalSessionId = latestClaude.id;
@@ -781,6 +819,8 @@ export async function handleAIToolWorkflow(
781
819
  preferClosestTo: finishedAt,
782
820
  windowMs: 60 * 60 * 1000,
783
821
  cwd: worktreePath,
822
+ branch,
823
+ ...worktreeLookupOptions,
784
824
  });
785
825
  if (latestGemini) {
786
826
  finalSessionId = latestGemini.id;