@akiojin/gwt 4.11.6 → 4.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 (172) hide show
  1. package/bin/gwt.js +1 -1
  2. package/dist/claude.d.ts +1 -0
  3. package/dist/claude.d.ts.map +1 -1
  4. package/dist/claude.js +50 -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 +247 -49
  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 +19 -11
  17. package/dist/cli/ui/components/solid/WizardController.js.map +1 -1
  18. package/dist/cli/ui/components/solid/WizardSteps.d.ts.map +1 -1
  19. package/dist/cli/ui/components/solid/WizardSteps.js +26 -69
  20. package/dist/cli/ui/components/solid/WizardSteps.js.map +1 -1
  21. package/dist/cli/ui/core/theme.d.ts +9 -0
  22. package/dist/cli/ui/core/theme.d.ts.map +1 -1
  23. package/dist/cli/ui/core/theme.js +21 -0
  24. package/dist/cli/ui/core/theme.js.map +1 -1
  25. package/dist/cli/ui/screens/solid/BranchListScreen.d.ts +9 -2
  26. package/dist/cli/ui/screens/solid/BranchListScreen.d.ts.map +1 -1
  27. package/dist/cli/ui/screens/solid/BranchListScreen.js +101 -28
  28. package/dist/cli/ui/screens/solid/BranchListScreen.js.map +1 -1
  29. package/dist/cli/ui/screens/solid/ConfirmScreen.d.ts +2 -1
  30. package/dist/cli/ui/screens/solid/ConfirmScreen.d.ts.map +1 -1
  31. package/dist/cli/ui/screens/solid/ConfirmScreen.js +11 -3
  32. package/dist/cli/ui/screens/solid/ConfirmScreen.js.map +1 -1
  33. package/dist/cli/ui/screens/solid/EnvironmentScreen.d.ts.map +1 -1
  34. package/dist/cli/ui/screens/solid/EnvironmentScreen.js +9 -10
  35. package/dist/cli/ui/screens/solid/EnvironmentScreen.js.map +1 -1
  36. package/dist/cli/ui/screens/solid/LogScreen.d.ts +7 -1
  37. package/dist/cli/ui/screens/solid/LogScreen.d.ts.map +1 -1
  38. package/dist/cli/ui/screens/solid/LogScreen.js +254 -16
  39. package/dist/cli/ui/screens/solid/LogScreen.js.map +1 -1
  40. package/dist/cli/ui/screens/solid/ProfileEnvScreen.d.ts.map +1 -1
  41. package/dist/cli/ui/screens/solid/ProfileEnvScreen.js +8 -5
  42. package/dist/cli/ui/screens/solid/ProfileEnvScreen.js.map +1 -1
  43. package/dist/cli/ui/screens/solid/SelectorScreen.d.ts.map +1 -1
  44. package/dist/cli/ui/screens/solid/SelectorScreen.js +12 -4
  45. package/dist/cli/ui/screens/solid/SelectorScreen.js.map +1 -1
  46. package/dist/cli/ui/types.d.ts +1 -0
  47. package/dist/cli/ui/types.d.ts.map +1 -1
  48. package/dist/cli/ui/utils/branchFormatter.d.ts +1 -0
  49. package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
  50. package/dist/cli/ui/utils/branchFormatter.js +29 -7
  51. package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
  52. package/dist/cli/ui/utils/continueSession.d.ts +14 -0
  53. package/dist/cli/ui/utils/continueSession.d.ts.map +1 -1
  54. package/dist/cli/ui/utils/continueSession.js +61 -3
  55. package/dist/cli/ui/utils/continueSession.js.map +1 -1
  56. package/dist/cli/ui/utils/versionCache.d.ts +37 -0
  57. package/dist/cli/ui/utils/versionCache.d.ts.map +1 -0
  58. package/dist/cli/ui/utils/versionCache.js +70 -0
  59. package/dist/cli/ui/utils/versionCache.js.map +1 -0
  60. package/dist/cli/ui/utils/versionFetcher.d.ts +41 -0
  61. package/dist/cli/ui/utils/versionFetcher.d.ts.map +1 -0
  62. package/dist/cli/ui/utils/versionFetcher.js +89 -0
  63. package/dist/cli/ui/utils/versionFetcher.js.map +1 -0
  64. package/dist/codex.d.ts +1 -0
  65. package/dist/codex.d.ts.map +1 -1
  66. package/dist/codex.js +48 -19
  67. package/dist/codex.js.map +1 -1
  68. package/dist/config/index.d.ts.map +1 -1
  69. package/dist/config/index.js +10 -1
  70. package/dist/config/index.js.map +1 -1
  71. package/dist/gemini.d.ts +1 -0
  72. package/dist/gemini.d.ts.map +1 -1
  73. package/dist/gemini.js +36 -3
  74. package/dist/gemini.js.map +1 -1
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +32 -2
  77. package/dist/index.js.map +1 -1
  78. package/dist/launcher.d.ts.map +1 -1
  79. package/dist/launcher.js +43 -8
  80. package/dist/launcher.js.map +1 -1
  81. package/dist/logging/agentOutput.d.ts +21 -0
  82. package/dist/logging/agentOutput.d.ts.map +1 -0
  83. package/dist/logging/agentOutput.js +164 -0
  84. package/dist/logging/agentOutput.js.map +1 -0
  85. package/dist/logging/formatter.d.ts.map +1 -1
  86. package/dist/logging/formatter.js +18 -4
  87. package/dist/logging/formatter.js.map +1 -1
  88. package/dist/logging/logger.d.ts.map +1 -1
  89. package/dist/logging/logger.js +2 -0
  90. package/dist/logging/logger.js.map +1 -1
  91. package/dist/logging/reader.d.ts +21 -0
  92. package/dist/logging/reader.d.ts.map +1 -1
  93. package/dist/logging/reader.js +79 -0
  94. package/dist/logging/reader.js.map +1 -1
  95. package/dist/opentui/index.solid.js +2306 -653
  96. package/dist/services/dependency-installer.js +2 -2
  97. package/dist/services/dependency-installer.js.map +1 -1
  98. package/dist/utils/session/common.d.ts +8 -0
  99. package/dist/utils/session/common.d.ts.map +1 -1
  100. package/dist/utils/session/common.js +22 -0
  101. package/dist/utils/session/common.js.map +1 -1
  102. package/dist/utils/session/parsers/claude.d.ts +10 -4
  103. package/dist/utils/session/parsers/claude.d.ts.map +1 -1
  104. package/dist/utils/session/parsers/claude.js +64 -18
  105. package/dist/utils/session/parsers/claude.js.map +1 -1
  106. package/dist/utils/session/parsers/codex.d.ts.map +1 -1
  107. package/dist/utils/session/parsers/codex.js +48 -28
  108. package/dist/utils/session/parsers/codex.js.map +1 -1
  109. package/dist/utils/session/parsers/gemini.d.ts.map +1 -1
  110. package/dist/utils/session/parsers/gemini.js +43 -6
  111. package/dist/utils/session/parsers/gemini.js.map +1 -1
  112. package/dist/utils/session/parsers/opencode.d.ts.map +1 -1
  113. package/dist/utils/session/parsers/opencode.js +43 -6
  114. package/dist/utils/session/parsers/opencode.js.map +1 -1
  115. package/dist/utils/session/types.d.ts +7 -0
  116. package/dist/utils/session/types.d.ts.map +1 -1
  117. package/dist/web/client/src/components/ui/alert.d.ts +1 -1
  118. package/dist/worktree.d.ts +4 -1
  119. package/dist/worktree.d.ts.map +1 -1
  120. package/dist/worktree.js +21 -15
  121. package/dist/worktree.js.map +1 -1
  122. package/package.json +2 -1
  123. package/src/claude.ts +64 -28
  124. package/src/cli/ui/App.solid.tsx +324 -51
  125. package/src/cli/ui/__tests__/solid/AppSolid.cleanup.test.tsx +830 -1
  126. package/src/cli/ui/__tests__/solid/BranchListScreen.test.tsx +105 -5
  127. package/src/cli/ui/__tests__/solid/ConfirmScreen.test.tsx +77 -0
  128. package/src/cli/ui/__tests__/solid/LogScreen.test.tsx +351 -0
  129. package/src/cli/ui/__tests__/solid/components/QuickStartStep.test.tsx +73 -2
  130. package/src/cli/ui/__tests__/solid/components/WizardSteps.test.tsx +4 -1
  131. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +72 -45
  132. package/src/cli/ui/components/solid/QuickStartStep.tsx +35 -23
  133. package/src/cli/ui/components/solid/SearchInput.tsx +1 -1
  134. package/src/cli/ui/components/solid/SelectInput.tsx +4 -0
  135. package/src/cli/ui/components/solid/WizardController.tsx +20 -11
  136. package/src/cli/ui/components/solid/WizardSteps.tsx +29 -86
  137. package/src/cli/ui/core/theme.ts +32 -0
  138. package/src/cli/ui/hooks/solid/useAsyncOperation.ts +8 -6
  139. package/src/cli/ui/hooks/solid/useGitOperations.ts +6 -5
  140. package/src/cli/ui/screens/solid/BranchListScreen.tsx +135 -32
  141. package/src/cli/ui/screens/solid/ConfirmScreen.tsx +20 -8
  142. package/src/cli/ui/screens/solid/EnvironmentScreen.tsx +22 -20
  143. package/src/cli/ui/screens/solid/LogScreen.tsx +364 -35
  144. package/src/cli/ui/screens/solid/ProfileEnvScreen.tsx +19 -15
  145. package/src/cli/ui/screens/solid/SelectorScreen.tsx +25 -14
  146. package/src/cli/ui/screens/solid/SettingsScreen.tsx +5 -3
  147. package/src/cli/ui/types.ts +1 -0
  148. package/src/cli/ui/utils/__tests__/branchFormatter.test.ts +53 -6
  149. package/src/cli/ui/utils/branchFormatter.ts +35 -7
  150. package/src/cli/ui/utils/continueSession.ts +90 -3
  151. package/src/cli/ui/utils/versionCache.ts +93 -0
  152. package/src/cli/ui/utils/versionFetcher.ts +120 -0
  153. package/src/codex.ts +62 -20
  154. package/src/config/__tests__/saveSession.test.ts +2 -2
  155. package/src/config/index.ts +11 -1
  156. package/src/gemini.ts +50 -4
  157. package/src/index.test.ts +16 -10
  158. package/src/index.ts +38 -1
  159. package/src/launcher.ts +49 -8
  160. package/src/logging/agentOutput.ts +216 -0
  161. package/src/logging/formatter.ts +23 -4
  162. package/src/logging/logger.ts +2 -0
  163. package/src/logging/reader.ts +117 -0
  164. package/src/services/__tests__/BatchMergeService.test.ts +34 -14
  165. package/src/services/dependency-installer.ts +2 -2
  166. package/src/utils/session/common.ts +28 -0
  167. package/src/utils/session/parsers/claude.ts +79 -29
  168. package/src/utils/session/parsers/codex.ts +50 -26
  169. package/src/utils/session/parsers/gemini.ts +46 -5
  170. package/src/utils/session/parsers/opencode.ts +46 -5
  171. package/src/utils/session/types.ts +4 -0
  172. package/src/worktree.ts +28 -15
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) {
@@ -734,10 +746,29 @@ export async function handleAIToolWorkflow(
734
746
  resumeSessionId ??
735
747
  null;
736
748
 
749
+ let resolvedWorktrees: { path: string; branch: string }[] | null = null;
750
+ if (branch) {
751
+ try {
752
+ const allWorktrees = await listAllWorktrees();
753
+ resolvedWorktrees = allWorktrees
754
+ .filter((entry) => entry?.path && entry?.branch)
755
+ .map((entry) => ({ path: entry.path, branch: entry.branch }));
756
+ } catch {
757
+ resolvedWorktrees = null;
758
+ }
759
+ }
760
+ const worktreeLookupOptions =
761
+ resolvedWorktrees && resolvedWorktrees.length > 0
762
+ ? { worktrees: resolvedWorktrees }
763
+ : {};
764
+
737
765
  if (!finalSessionId && tool === "claude-code") {
738
766
  try {
739
767
  finalSessionId =
740
- (await findLatestClaudeSessionId(worktreePath)) ?? null;
768
+ (await findLatestClaudeSessionId(worktreePath, {
769
+ branch,
770
+ ...worktreeLookupOptions,
771
+ })) ?? null;
741
772
  } catch {
742
773
  finalSessionId = null;
743
774
  }
@@ -752,6 +783,8 @@ export async function handleAIToolWorkflow(
752
783
  preferClosestTo: finishedAt,
753
784
  windowMs: 60 * 60 * 1000,
754
785
  cwd: worktreePath,
786
+ branch,
787
+ ...worktreeLookupOptions,
755
788
  });
756
789
  if (latest) {
757
790
  finalSessionId = latest.id;
@@ -766,6 +799,8 @@ export async function handleAIToolWorkflow(
766
799
  until: finishedAt + 60_000,
767
800
  preferClosestTo: finishedAt,
768
801
  windowMs: 60 * 60 * 1000,
802
+ branch,
803
+ ...worktreeLookupOptions,
769
804
  });
770
805
  if (latestClaude) {
771
806
  finalSessionId = latestClaude.id;
@@ -781,6 +816,8 @@ export async function handleAIToolWorkflow(
781
816
  preferClosestTo: finishedAt,
782
817
  windowMs: 60 * 60 * 1000,
783
818
  cwd: worktreePath,
819
+ branch,
820
+ ...worktreeLookupOptions,
784
821
  });
785
822
  if (latestGemini) {
786
823
  finalSessionId = latestGemini.id;
package/src/launcher.ts CHANGED
@@ -9,7 +9,14 @@ import { execa } from "execa";
9
9
  import chalk from "chalk";
10
10
  import type { CodingAgent, CodingAgentLaunchOptions } from "./types/tools.js";
11
11
  import { createLogger } from "./logging/logger.js";
12
- import { resolveVersionSuffix } from "./utils/npmRegistry.js";
12
+ import {
13
+ runAgentWithPty,
14
+ shouldCaptureAgentOutput,
15
+ } from "./logging/agentOutput.js";
16
+ import {
17
+ parsePackageCommand,
18
+ resolveVersionSuffix,
19
+ } from "./utils/npmRegistry.js";
13
20
  import { writeTerminalLine } from "./utils/terminal.js";
14
21
 
15
22
  const logger = createLogger({ category: "launcher" });
@@ -125,14 +132,32 @@ export async function launchCodingAgent(
125
132
  ...(options.sharedEnv ?? {}),
126
133
  ...(agent.env ?? {}),
127
134
  };
135
+ const workingDir = options.cwd ?? process.cwd();
136
+ const captureOutput = shouldCaptureAgentOutput(env);
128
137
 
129
138
  // execa共通オプション(cwdがundefinedの場合は含めない)
130
139
  const execaOptions = {
131
140
  stdio: "inherit" as const,
132
- ...(options.cwd ? { cwd: options.cwd } : {}),
141
+ ...(workingDir ? { cwd: workingDir } : {}),
133
142
  env,
134
143
  };
135
144
 
145
+ const runWithCapture = async (command: string, commandArgs: string[]) => {
146
+ const result = await runAgentWithPty({
147
+ command,
148
+ args: commandArgs,
149
+ cwd: workingDir,
150
+ env,
151
+ agentId: agent.id,
152
+ });
153
+ if (result.signal !== null && result.signal !== undefined) {
154
+ throw new Error(`Coding agent terminated by signal ${result.signal}`);
155
+ }
156
+ if (result.exitCode !== null && result.exitCode !== 0) {
157
+ throw new Error(`Coding agent exited with code ${result.exitCode}`);
158
+ }
159
+ };
160
+
136
161
  logger.info(
137
162
  {
138
163
  agentId: agent.id,
@@ -146,7 +171,11 @@ export async function launchCodingAgent(
146
171
  switch (agent.type) {
147
172
  case "path": {
148
173
  // 絶対パスで直接実行
149
- await execa(agent.command, args, execaOptions);
174
+ if (captureOutput) {
175
+ await runWithCapture(agent.command, args);
176
+ } else {
177
+ await execa(agent.command, args, execaOptions);
178
+ }
150
179
  logger.info({ agentId: agent.id }, "Coding agent completed (path)");
151
180
  break;
152
181
  }
@@ -154,9 +183,12 @@ export async function launchCodingAgent(
154
183
  case "bunx": {
155
184
  // bunx経由でパッケージ実行
156
185
  // バージョン指定がある場合はパッケージ名に付与
157
- const selectedVersion = options.version ?? "installed";
158
- const versionSuffix = resolveVersionSuffix(options.version);
159
- const packageWithVersion = `${agent.command}${versionSuffix}`;
186
+ const { packageName, version: embeddedVersion } = parsePackageCommand(
187
+ agent.command,
188
+ );
189
+ const selectedVersion = options.version ?? embeddedVersion ?? "latest";
190
+ const versionSuffix = resolveVersionSuffix(selectedVersion);
191
+ const packageWithVersion = `${packageName}${versionSuffix}`;
160
192
 
161
193
  // FR-072: Log version information
162
194
  if (selectedVersion === "installed") {
@@ -167,7 +199,12 @@ export async function launchCodingAgent(
167
199
  writeTerminalLine(chalk.cyan(` 🔄 Using bunx ${packageWithVersion}`));
168
200
 
169
201
  // bunx [package@version] [args...]
170
- await execa("bunx", [packageWithVersion, ...args], execaOptions);
202
+ const bunxCommand = captureOutput ? await resolveCommand("bunx") : "bunx";
203
+ if (captureOutput) {
204
+ await runWithCapture(bunxCommand, [packageWithVersion, ...args]);
205
+ } else {
206
+ await execa("bunx", [packageWithVersion, ...args], execaOptions);
207
+ }
171
208
  logger.info(
172
209
  { agentId: agent.id, version: selectedVersion },
173
210
  "Coding agent completed (bunx)",
@@ -178,7 +215,11 @@ export async function launchCodingAgent(
178
215
  case "command": {
179
216
  // PATH解決 → 実行
180
217
  const resolvedPath = await resolveCommand(agent.command);
181
- await execa(resolvedPath, args, execaOptions);
218
+ if (captureOutput) {
219
+ await runWithCapture(resolvedPath, args);
220
+ } else {
221
+ await execa(resolvedPath, args, execaOptions);
222
+ }
182
223
  logger.info({ agentId: agent.id }, "Coding agent completed (command)");
183
224
  break;
184
225
  }
@@ -0,0 +1,216 @@
1
+ import * as pty from "node-pty";
2
+ import type { IPty } from "node-pty";
3
+ import { createLogger } from "./logger.js";
4
+ import { resolveLogDir } from "./reader.js";
5
+ import { getTerminalStreams } from "../utils/terminal.js";
6
+
7
+ export const CAPTURE_AGENT_OUTPUT_ENV = "GWT_CAPTURE_AGENT_OUTPUT";
8
+
9
+ export function shouldCaptureAgentOutput(
10
+ env: NodeJS.ProcessEnv = process.env,
11
+ ): boolean {
12
+ const raw = env[CAPTURE_AGENT_OUTPUT_ENV];
13
+ if (raw === undefined) {
14
+ // Default to false to avoid PTY stdin/stdout conflicts with OpenTUI.
15
+ // Set GWT_CAPTURE_AGENT_OUTPUT=true to enable agent output logging.
16
+ return false;
17
+ }
18
+ const normalized = String(raw).trim().toLowerCase();
19
+ if (!normalized) {
20
+ return false;
21
+ }
22
+ return normalized === "true" || normalized === "1";
23
+ }
24
+
25
+ // eslint-disable-next-line no-control-regex
26
+ const ANSI_PATTERN = new RegExp("\\u001b\\[[0-9;]*[A-Za-z]", "g");
27
+
28
+ export function stripAnsi(value: string): string {
29
+ return value.replace(ANSI_PATTERN, "");
30
+ }
31
+
32
+ export interface AgentOutputLineBuffer {
33
+ push: (chunk: string) => void;
34
+ flush: () => void;
35
+ }
36
+
37
+ export function createAgentOutputLineBuffer(
38
+ onLine: (line: string) => void,
39
+ ): AgentOutputLineBuffer {
40
+ let buffer = "";
41
+
42
+ const push = (chunk: string) => {
43
+ const normalized = chunk.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
44
+ buffer += normalized;
45
+ while (true) {
46
+ const index = buffer.indexOf("\n");
47
+ if (index === -1) {
48
+ break;
49
+ }
50
+ const line = buffer.slice(0, index);
51
+ buffer = buffer.slice(index + 1);
52
+ onLine(line);
53
+ }
54
+ };
55
+
56
+ const flush = () => {
57
+ if (!buffer) {
58
+ return;
59
+ }
60
+ const line = buffer;
61
+ buffer = "";
62
+ onLine(line);
63
+ };
64
+
65
+ return { push, flush };
66
+ }
67
+
68
+ export interface AgentOutputCaptureOptions {
69
+ command: string;
70
+ args: string[];
71
+ cwd: string;
72
+ env: NodeJS.ProcessEnv;
73
+ agentId: string;
74
+ }
75
+
76
+ export interface AgentOutputCaptureResult {
77
+ exitCode: number | null;
78
+ signal?: number | null;
79
+ }
80
+
81
+ const normalizeEnv = (env: NodeJS.ProcessEnv): Record<string, string> =>
82
+ Object.fromEntries(
83
+ Object.entries(env).filter(
84
+ (entry): entry is [string, string] => typeof entry[1] === "string",
85
+ ),
86
+ );
87
+
88
+ const getTerminalSize = (terminal: ReturnType<typeof getTerminalStreams>) => {
89
+ const stdout = terminal.stdout as
90
+ | (NodeJS.WriteStream & { columns?: number; rows?: number })
91
+ | undefined;
92
+ const cols = stdout?.columns ?? process.stdout.columns ?? 80;
93
+ const rows = stdout?.rows ?? process.stdout.rows ?? 24;
94
+ return { cols, rows };
95
+ };
96
+
97
+ export async function runAgentWithPty(
98
+ options: AgentOutputCaptureOptions,
99
+ ): Promise<AgentOutputCaptureResult> {
100
+ const terminal = getTerminalStreams();
101
+ const { cols, rows } = getTerminalSize(terminal);
102
+ const logDir = resolveLogDir(options.cwd);
103
+ const stdoutLogger = createLogger({
104
+ category: "agent.stdout",
105
+ logDir,
106
+ base: { agentId: options.agentId },
107
+ });
108
+ const stderrLogger = createLogger({
109
+ category: "agent.stderr",
110
+ logDir,
111
+ base: { agentId: options.agentId },
112
+ });
113
+
114
+ const normalizedEnv = normalizeEnv(options.env);
115
+ const ptyProcess: IPty = pty.spawn(options.command, options.args, {
116
+ name: process.env.TERM ?? "xterm-256color",
117
+ cols,
118
+ rows,
119
+ cwd: options.cwd,
120
+ env: normalizedEnv,
121
+ });
122
+
123
+ const lineBuffer = createAgentOutputLineBuffer((line) => {
124
+ const cleaned = stripAnsi(line).trimEnd();
125
+ if (!cleaned) {
126
+ return;
127
+ }
128
+ stdoutLogger.info(cleaned);
129
+ });
130
+
131
+ const stdout = terminal.stdout;
132
+ const writeToTerminal =
133
+ stdout && typeof stdout.write === "function"
134
+ ? stdout.write.bind(stdout)
135
+ : null;
136
+
137
+ ptyProcess.onData((data) => {
138
+ if (writeToTerminal) {
139
+ try {
140
+ writeToTerminal(data);
141
+ } catch {
142
+ // Ignore terminal write errors.
143
+ }
144
+ }
145
+ lineBuffer.push(data);
146
+ });
147
+
148
+ const stdin = terminal.stdin;
149
+ const handleInput = (chunk: Buffer | string) => {
150
+ const data = typeof chunk === "string" ? chunk : chunk.toString("utf8");
151
+ ptyProcess.write(data);
152
+ };
153
+
154
+ const stdinWasRaw =
155
+ stdin &&
156
+ typeof (stdin as NodeJS.ReadStream & { isRaw?: boolean }).isRaw ===
157
+ "boolean"
158
+ ? (stdin as NodeJS.ReadStream & { isRaw?: boolean }).isRaw
159
+ : undefined;
160
+
161
+ if (stdin && typeof stdin.on === "function") {
162
+ if (stdin.isTTY && typeof stdin.setRawMode === "function") {
163
+ try {
164
+ stdin.setRawMode(true);
165
+ } catch {
166
+ // Ignore raw mode errors.
167
+ }
168
+ }
169
+ if (typeof stdin.resume === "function") {
170
+ stdin.resume();
171
+ }
172
+ stdin.on("data", handleInput);
173
+ }
174
+
175
+ const handleResize = () => {
176
+ const next = getTerminalSize(terminal);
177
+ try {
178
+ ptyProcess.resize(next.cols, next.rows);
179
+ } catch {
180
+ // Ignore resize errors.
181
+ }
182
+ };
183
+ if (process.stdout && typeof process.stdout.on === "function") {
184
+ process.stdout.on("resize", handleResize);
185
+ }
186
+
187
+ return await new Promise<AgentOutputCaptureResult>((resolve) => {
188
+ ptyProcess.onExit(({ exitCode, signal }) => {
189
+ lineBuffer.flush();
190
+ if (stdin && typeof stdin.off === "function") {
191
+ stdin.off("data", handleInput);
192
+ }
193
+ if (stdin && typeof stdin.pause === "function") {
194
+ stdin.pause();
195
+ }
196
+ if (stdin && stdin.isTTY && typeof stdin.setRawMode === "function") {
197
+ try {
198
+ stdin.setRawMode(Boolean(stdinWasRaw));
199
+ } catch {
200
+ // Ignore raw mode restore errors.
201
+ }
202
+ }
203
+ if (process.stdout && typeof process.stdout.off === "function") {
204
+ process.stdout.off("resize", handleResize);
205
+ }
206
+ if (exitCode !== null && exitCode !== 0) {
207
+ stderrLogger.error(
208
+ { exitCode, signal },
209
+ "Agent exited with non-zero code",
210
+ );
211
+ }
212
+ const normalizedSignal = signal ?? null;
213
+ resolve({ exitCode, signal: normalizedSignal });
214
+ });
215
+ });
216
+ }
@@ -19,17 +19,36 @@ const LEVEL_LABELS: Record<number, string> = {
19
19
  60: "FATAL",
20
20
  };
21
21
 
22
+ const LOCAL_TIME_FORMATTER = new Intl.DateTimeFormat(undefined, {
23
+ hour: "2-digit",
24
+ minute: "2-digit",
25
+ second: "2-digit",
26
+ hour12: false,
27
+ });
28
+
29
+ const formatLocalTimeParts = (date: Date): string => {
30
+ const parts = LOCAL_TIME_FORMATTER.formatToParts(date);
31
+ const get = (type: Intl.DateTimeFormatPartTypes) =>
32
+ parts.find((part) => part.type === type)?.value;
33
+ const hour = get("hour");
34
+ const minute = get("minute");
35
+ const second = get("second");
36
+
37
+ if (!hour || !minute || !second) {
38
+ return LOCAL_TIME_FORMATTER.format(date);
39
+ }
40
+
41
+ return `${hour}:${minute}:${second}`;
42
+ };
43
+
22
44
  const formatTimeLabel = (
23
45
  value: unknown,
24
46
  ): { label: string; timestamp: number | null } => {
25
47
  if (typeof value === "string" || typeof value === "number") {
26
48
  const date = new Date(value);
27
49
  if (!Number.isNaN(date.getTime())) {
28
- const hours = String(date.getHours()).padStart(2, "0");
29
- const minutes = String(date.getMinutes()).padStart(2, "0");
30
- const seconds = String(date.getSeconds()).padStart(2, "0");
31
50
  return {
32
- label: `${hours}:${minutes}:${seconds}`,
51
+ label: formatLocalTimeParts(date),
33
52
  timestamp: date.getTime(),
34
53
  };
35
54
  }
@@ -59,6 +59,7 @@ export function createLogger(config: LoggerConfig = {}): Logger {
59
59
  const destinationStream = pino.destination({
60
60
  dest: destination,
61
61
  sync: true,
62
+ append: true,
62
63
  });
63
64
  return pino(options, destinationStream);
64
65
  }
@@ -89,6 +90,7 @@ export function createLogger(config: LoggerConfig = {}): Logger {
89
90
  const destinationStream = pino.destination({
90
91
  dest: destination,
91
92
  sync: false,
93
+ append: true,
92
94
  });
93
95
  return pino(options, destinationStream);
94
96
  }