@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/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
  }
@@ -9,6 +9,26 @@ export interface LogFileInfo {
9
9
  mtimeMs: number;
10
10
  }
11
11
 
12
+ export type LogTargetReason =
13
+ | "worktree"
14
+ | "worktree-inaccessible"
15
+ | "current-working-directory"
16
+ | "working-directory"
17
+ | "working-directory-fallback"
18
+ | "no-worktree";
19
+
20
+ export interface LogTargetBranch {
21
+ name: string;
22
+ isCurrent?: boolean;
23
+ worktree?: { path: string; isAccessible?: boolean } | undefined;
24
+ }
25
+
26
+ export interface LogTargetResolution {
27
+ logDir: string | null;
28
+ sourcePath: string | null;
29
+ reason: LogTargetReason;
30
+ }
31
+
12
32
  const LOG_FILENAME_PATTERN = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
13
33
 
14
34
  export function resolveLogDir(cwd: string = process.cwd()): string {
@@ -16,6 +36,46 @@ export function resolveLogDir(cwd: string = process.cwd()): string {
16
36
  return path.join(os.homedir(), ".gwt", "logs", cwdBase);
17
37
  }
18
38
 
39
+ export function resolveLogTarget(
40
+ branch: LogTargetBranch | null,
41
+ workingDirectory: string = process.cwd(),
42
+ ): LogTargetResolution {
43
+ if (!branch) {
44
+ return {
45
+ logDir: resolveLogDir(workingDirectory),
46
+ sourcePath: workingDirectory,
47
+ reason: "working-directory",
48
+ };
49
+ }
50
+
51
+ const worktreePath = branch.worktree?.path;
52
+ if (worktreePath) {
53
+ const accessible = branch.worktree?.isAccessible !== false;
54
+ if (accessible) {
55
+ return {
56
+ logDir: resolveLogDir(worktreePath),
57
+ sourcePath: worktreePath,
58
+ reason: "worktree",
59
+ };
60
+ }
61
+ return {
62
+ logDir: null,
63
+ sourcePath: worktreePath,
64
+ reason: "worktree-inaccessible",
65
+ };
66
+ }
67
+
68
+ if (branch.isCurrent) {
69
+ return {
70
+ logDir: resolveLogDir(workingDirectory),
71
+ sourcePath: workingDirectory,
72
+ reason: "current-working-directory",
73
+ };
74
+ }
75
+
76
+ return { logDir: null, sourcePath: null, reason: "no-worktree" };
77
+ }
78
+
19
79
  export function buildLogFilePath(logDir: string, date: string): string {
20
80
  return path.join(logDir, `${date}.jsonl`);
21
81
  }
@@ -56,7 +116,7 @@ export async function listLogFiles(logDir: string): Promise<LogFileInfo[]> {
56
116
  }
57
117
  }
58
118
 
59
- return files.sort((a, b) => b.date.localeCompare(a.date));
119
+ return files.sort((a, b) => b.mtimeMs - a.mtimeMs);
60
120
  } catch (error) {
61
121
  const err = error as NodeJS.ErrnoException;
62
122
  if (err.code === "ENOENT") {
@@ -66,6 +126,69 @@ export async function listLogFiles(logDir: string): Promise<LogFileInfo[]> {
66
126
  }
67
127
  }
68
128
 
129
+ const getLatestLogMtimeWithContent = async (
130
+ logDir: string,
131
+ ): Promise<number | null> => {
132
+ const files = await listLogFiles(logDir);
133
+ for (const file of files) {
134
+ const lines = await readLogFileLines(file.path);
135
+ if (lines.length > 0) {
136
+ return file.mtimeMs;
137
+ }
138
+ }
139
+ return null;
140
+ };
141
+
142
+ export async function selectLogTargetByRecency(
143
+ primary: LogTargetResolution,
144
+ fallback: LogTargetResolution,
145
+ ): Promise<LogTargetResolution> {
146
+ if (!primary.logDir || !primary.sourcePath) {
147
+ return primary;
148
+ }
149
+ if (!fallback.logDir || !fallback.sourcePath) {
150
+ return primary;
151
+ }
152
+ if (primary.logDir === fallback.logDir) {
153
+ return primary;
154
+ }
155
+ if (primary.reason !== "worktree") {
156
+ return primary;
157
+ }
158
+
159
+ const [primaryMtime, fallbackMtime] = await Promise.all([
160
+ getLatestLogMtimeWithContent(primary.logDir),
161
+ getLatestLogMtimeWithContent(fallback.logDir),
162
+ ]);
163
+
164
+ if (
165
+ fallbackMtime !== null &&
166
+ (primaryMtime === null || fallbackMtime > primaryMtime)
167
+ ) {
168
+ return {
169
+ ...fallback,
170
+ reason: "working-directory-fallback",
171
+ };
172
+ }
173
+ return primary;
174
+ }
175
+ export async function clearLogFiles(logDir: string): Promise<number> {
176
+ const files = await listLogFiles(logDir);
177
+ let cleared = 0;
178
+ for (const file of files) {
179
+ try {
180
+ await fs.truncate(file.path, 0);
181
+ cleared += 1;
182
+ } catch (error) {
183
+ const err = error as NodeJS.ErrnoException;
184
+ if (err.code !== "ENOENT") {
185
+ throw error;
186
+ }
187
+ }
188
+ }
189
+ return cleared;
190
+ }
191
+
69
192
  export async function listRecentLogFiles(
70
193
  logDir: string,
71
194
  days = 7,
@@ -74,3 +197,44 @@ export async function listRecentLogFiles(
74
197
  const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
75
198
  return files.filter((file) => file.mtimeMs >= cutoff);
76
199
  }
200
+
201
+ export interface LogReadResult {
202
+ date: string;
203
+ lines: string[];
204
+ }
205
+
206
+ export async function readLogLinesForDate(
207
+ logDir: string,
208
+ preferredDate: string,
209
+ ): Promise<LogReadResult | null> {
210
+ const files = await listLogFiles(logDir);
211
+ if (files.length === 0) {
212
+ return null;
213
+ }
214
+
215
+ const ordered: LogFileInfo[] = [];
216
+ const preferred = files.find((file) => file.date === preferredDate);
217
+ if (preferred) {
218
+ ordered.push(preferred);
219
+ }
220
+ for (const file of files) {
221
+ if (preferred && file.date === preferred.date) {
222
+ continue;
223
+ }
224
+ ordered.push(file);
225
+ }
226
+
227
+ for (const file of ordered) {
228
+ const lines = await readLogFileLines(file.path);
229
+ if (lines.length > 0) {
230
+ return { date: file.date, lines };
231
+ }
232
+ }
233
+
234
+ const fallback = files[0];
235
+ if (!fallback) {
236
+ return { date: preferredDate, lines: [] };
237
+ }
238
+ const fallbackDate = preferred?.date ?? fallback.date;
239
+ return { date: fallbackDate, lines: [] };
240
+ }
@@ -1,6 +1,6 @@
1
1
  import { describe, it, expect, mock, beforeEach, afterEach } from "bun:test";
2
2
  import { BatchMergeService } from "../BatchMergeService";
3
- import type { BatchMergeConfig } from "../../ui/types";
3
+ import type { BatchMergeConfig, BatchMergeProgress } from "../../cli/ui/types";
4
4
 
5
5
  // Mock git module
6
6
  mock.module("../../git", () => ({
@@ -235,7 +235,9 @@ describe("BatchMergeService", () => {
235
235
  (
236
236
  worktree.generateWorktreePath as ReturnType<typeof mock>
237
237
  ).mockResolvedValue("/repo/.worktrees/feature-b");
238
- (worktree.createWorktree as ReturnType<typeof mock>).mockResolvedValue();
238
+ (worktree.createWorktree as ReturnType<typeof mock>).mockResolvedValue(
239
+ undefined,
240
+ );
239
241
 
240
242
  const worktreePath = await service.ensureWorktree("feature/b");
241
243
 
@@ -271,7 +273,9 @@ describe("BatchMergeService", () => {
271
273
  });
272
274
 
273
275
  it("should successfully merge without conflicts", async () => {
274
- (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue();
276
+ (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
277
+ undefined,
278
+ );
275
279
  (git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(
276
280
  false,
277
281
  );
@@ -293,7 +297,7 @@ describe("BatchMergeService", () => {
293
297
  new Error("Merge conflict"),
294
298
  );
295
299
  (git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(true);
296
- (git.abortMerge as ReturnType<typeof mock>).mockResolvedValue();
300
+ (git.abortMerge as ReturnType<typeof mock>).mockResolvedValue(undefined);
297
301
 
298
302
  const status = await service.mergeBranch("feature/a", "main", config);
299
303
 
@@ -339,11 +343,13 @@ describe("BatchMergeService", () => {
339
343
  });
340
344
 
341
345
  it("should rollback with resetToHead after successful dry-run merge", async () => {
342
- (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue();
346
+ (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
347
+ undefined,
348
+ );
343
349
  (git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(
344
350
  false,
345
351
  );
346
- (git.resetToHead as ReturnType<typeof mock>).mockResolvedValue();
352
+ (git.resetToHead as ReturnType<typeof mock>).mockResolvedValue(undefined);
347
353
 
348
354
  const status = await service.mergeBranch(
349
355
  "feature/a",
@@ -368,7 +374,7 @@ describe("BatchMergeService", () => {
368
374
  new Error("CONFLICT (content)"),
369
375
  );
370
376
  (git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(true);
371
- (git.abortMerge as ReturnType<typeof mock>).mockResolvedValue();
377
+ (git.abortMerge as ReturnType<typeof mock>).mockResolvedValue(undefined);
372
378
 
373
379
  const status = await service.mergeBranch(
374
380
  "feature/a",
@@ -405,14 +411,18 @@ describe("BatchMergeService", () => {
405
411
  });
406
412
 
407
413
  it("should push successfully after merge when autoPush is enabled", async () => {
408
- (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue();
414
+ (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
415
+ undefined,
416
+ );
409
417
  (git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(
410
418
  false,
411
419
  );
412
420
  (git.getCurrentBranchName as ReturnType<typeof mock>).mockResolvedValue(
413
421
  "feature/a",
414
422
  );
415
- (git.pushBranchToRemote as ReturnType<typeof mock>).mockResolvedValue();
423
+ (git.pushBranchToRemote as ReturnType<typeof mock>).mockResolvedValue(
424
+ undefined,
425
+ );
416
426
 
417
427
  const status = await service.mergeBranch(
418
428
  "feature/a",
@@ -431,7 +441,9 @@ describe("BatchMergeService", () => {
431
441
  });
432
442
 
433
443
  it("should handle push failure without failing merge", async () => {
434
- (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue();
444
+ (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
445
+ undefined,
446
+ );
435
447
  (git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(
436
448
  false,
437
449
  );
@@ -455,7 +467,9 @@ describe("BatchMergeService", () => {
455
467
 
456
468
  it("should not push when autoPush is false", async () => {
457
469
  const noPushConfig = { ...autoPushConfig, autoPush: false };
458
- (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue();
470
+ (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
471
+ undefined,
472
+ );
459
473
  (git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(
460
474
  false,
461
475
  );
@@ -482,7 +496,9 @@ describe("BatchMergeService", () => {
482
496
  autoPush: false,
483
497
  };
484
498
 
485
- (git.fetchAllRemotes as ReturnType<typeof mock>).mockResolvedValue();
499
+ (git.fetchAllRemotes as ReturnType<typeof mock>).mockResolvedValue(
500
+ undefined,
501
+ );
486
502
  (git.getRepositoryRoot as ReturnType<typeof mock>).mockResolvedValue(
487
503
  "/repo",
488
504
  );
@@ -492,8 +508,12 @@ describe("BatchMergeService", () => {
492
508
  (worktree.generateWorktreePath as ReturnType<typeof mock>)
493
509
  .mockResolvedValueOnce("/repo/.worktrees/feature-a")
494
510
  .mockResolvedValueOnce("/repo/.worktrees/feature-b");
495
- (worktree.createWorktree as ReturnType<typeof mock>).mockResolvedValue();
496
- (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue();
511
+ (worktree.createWorktree as ReturnType<typeof mock>).mockResolvedValue(
512
+ undefined,
513
+ );
514
+ (git.mergeFromBranch as ReturnType<typeof mock>).mockResolvedValue(
515
+ undefined,
516
+ );
497
517
  (git.hasMergeConflict as ReturnType<typeof mock>).mockResolvedValue(
498
518
  false,
499
519
  );