@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
@@ -103,7 +103,7 @@ describe("BranchListScreen icons", () => {
103
103
 
104
104
  try {
105
105
  const frame = testSetup.captureCharFrame();
106
- expect(frame).toMatch(/\[\*\] w {2,}feature\/active-clean/);
106
+ expect(frame).toMatch(/\[\*\] w o feature\/active-clean/);
107
107
  expect(frame).toContain("[ ] . ! feature/no-worktree");
108
108
  expect(frame).not.toContain(">[*]");
109
109
  expect(frame).not.toContain(">[ ]");
@@ -112,7 +112,7 @@ describe("BranchListScreen icons", () => {
112
112
  }
113
113
  });
114
114
 
115
- it("shows spinner during safety checks and blank icon for remote branches", async () => {
115
+ it("shows spinner for pending safety checks and blank icon for remote branches", async () => {
116
116
  const branches = [
117
117
  createBranch({
118
118
  name: "feature/loading",
@@ -146,7 +146,7 @@ describe("BranchListScreen icons", () => {
146
146
  indicators: {},
147
147
  footerMessage: null,
148
148
  inputLocked: false,
149
- safetyLoading: true,
149
+ safetyPendingBranches: new Set(["feature/loading"]),
150
150
  },
151
151
  });
152
152
 
@@ -183,8 +183,6 @@ describe("BranchListScreen worktree footer", () => {
183
183
  it("falls back to working directory for current branch without worktree", async () => {
184
184
  const branch = createBranch({
185
185
  isCurrent: true,
186
- worktree: undefined,
187
- worktreeStatus: undefined,
188
186
  });
189
187
  const testSetup = await renderBranchList({
190
188
  branches: [branch],
@@ -241,3 +239,105 @@ describe("BranchListScreen shortcut hints", () => {
241
239
  }
242
240
  });
243
241
  });
242
+
243
+ describe("BranchListScreen status legend", () => {
244
+ it("shows legend for uncommitted/unpushed/unmerged indicators", async () => {
245
+ const branch = createBranch({
246
+ name: "feature/legend",
247
+ label: "feature/legend",
248
+ value: "feature/legend",
249
+ });
250
+ const testSetup = await renderBranchList({
251
+ branches: [branch],
252
+ stats: makeStats({ localCount: 1 }),
253
+ });
254
+
255
+ try {
256
+ const frame = testSetup.captureCharFrame();
257
+ expect(frame).toContain(
258
+ "Legend: o Safe ! Uncommitted ! Unpushed * Unmerged",
259
+ );
260
+ } finally {
261
+ testSetup.renderer.destroy();
262
+ }
263
+ });
264
+ });
265
+
266
+ describe("BranchListScreen cursor position stability (FR-037a)", () => {
267
+ it("preserves cursor position when safety check completes", async () => {
268
+ const { createSignal } = await import("solid-js");
269
+ const branches = [
270
+ createBranch({
271
+ name: "feature/first",
272
+ label: "feature/first",
273
+ value: "feature/first",
274
+ worktreeStatus: "active",
275
+ }),
276
+ createBranch({
277
+ name: "feature/second",
278
+ label: "feature/second",
279
+ value: "feature/second",
280
+ worktreeStatus: "active",
281
+ }),
282
+ createBranch({
283
+ name: "feature/third",
284
+ label: "feature/third",
285
+ value: "feature/third",
286
+ worktreeStatus: "active",
287
+ }),
288
+ ];
289
+
290
+ // safetyPendingBranchesを動的に変更するためのシグナル
291
+ const [safetyPending, setSafetyPending] = createSignal<Set<string>>(
292
+ new Set(["feature/first", "feature/second", "feature/third"]),
293
+ );
294
+
295
+ const testSetup = await testRender(
296
+ () => (
297
+ <BranchListScreen
298
+ branches={branches}
299
+ stats={statsForBranches(branches)}
300
+ onSelect={() => {}}
301
+ cleanupUI={{
302
+ indicators: {},
303
+ footerMessage: null,
304
+ inputLocked: false,
305
+ safetyPendingBranches: safetyPending(),
306
+ }}
307
+ />
308
+ ),
309
+ { width: 80, height: 24 },
310
+ );
311
+ await testSetup.renderOnce();
312
+
313
+ try {
314
+ // 1. 初期状態ではカーソルは最初のブランチにある
315
+ let frame = testSetup.captureCharFrame();
316
+ // 最初のブランチがハイライトされている(選択されている)ことを確認
317
+ expect(frame).toContain("feature/first");
318
+
319
+ // 2. 下矢印キーでカーソルを2番目のブランチに移動
320
+ testSetup.mockInput.pressArrow("down");
321
+ await testSetup.renderOnce();
322
+
323
+ // 3. 安全状態確認が完了したことをシミュレート(pendingから削除)
324
+ setSafetyPending(new Set(["feature/second", "feature/third"]));
325
+ await testSetup.renderOnce();
326
+
327
+ // さらに別のブランチの安全状態確認が完了
328
+ setSafetyPending(new Set(["feature/third"]));
329
+ await testSetup.renderOnce();
330
+
331
+ // 4. カーソル位置が保持されていることを確認
332
+ // カーソルが2番目のブランチにあるので、下矢印でさらに移動できるはず
333
+ testSetup.mockInput.pressArrow("down");
334
+ await testSetup.renderOnce();
335
+
336
+ // 3番目のブランチに移動できていれば、カーソル位置は保持されていた
337
+ frame = testSetup.captureCharFrame();
338
+ expect(frame).toContain("feature/third");
339
+ } finally {
340
+ testSetup.renderer.destroy();
341
+ }
342
+ });
343
+ });
@@ -0,0 +1,77 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import { describe, expect, it } from "bun:test";
3
+ import { testRender } from "@opentui/solid";
4
+ import { TestRecorder } from "@opentui/core/testing";
5
+ import { ConfirmScreen } from "../../screens/solid/ConfirmScreen.js";
6
+
7
+ const getBgColor = (bg: Float32Array, width: number, x: number, y: number) => {
8
+ const index = (y * width + x) * 4;
9
+ return [
10
+ bg[index] ?? 0,
11
+ bg[index + 1] ?? 0,
12
+ bg[index + 2] ?? 0,
13
+ bg[index + 3] ?? 0,
14
+ ];
15
+ };
16
+
17
+ const isSameColor = (a: number[], b: number[], epsilon = 0.001) =>
18
+ a.length === b.length &&
19
+ a.every((value, i) => Math.abs(value - b[i]) < epsilon);
20
+
21
+ describe("ConfirmScreen selection width", () => {
22
+ it("limits highlight to provided width", async () => {
23
+ const width = 20;
24
+ const height = 5;
25
+ const selectionWidth = 8;
26
+ const testSetup = await testRender(
27
+ () => (
28
+ <ConfirmScreen
29
+ message="Confirm?"
30
+ onConfirm={() => {}}
31
+ yesLabel="OK"
32
+ noLabel="Cancel"
33
+ width={selectionWidth}
34
+ />
35
+ ),
36
+ { width, height },
37
+ );
38
+
39
+ const recorder = new TestRecorder(testSetup.renderer, {
40
+ recordBuffers: { bg: true },
41
+ });
42
+ recorder.rec();
43
+
44
+ try {
45
+ await testSetup.renderOnce();
46
+ recorder.stop();
47
+
48
+ const frame = recorder.recordedFrames.at(-1);
49
+ if (!frame?.buffers?.bg) {
50
+ throw new Error("No background buffer recorded");
51
+ }
52
+
53
+ const bg = frame.buffers.bg;
54
+ const defaultColor = getBgColor(bg, width, 0, 0);
55
+ const targetRow = 1;
56
+ let highlightLength = 0;
57
+
58
+ for (let x = 0; x < width; x += 1) {
59
+ const color = getBgColor(bg, width, x, targetRow);
60
+ if (!isSameColor(color, defaultColor)) {
61
+ highlightLength += 1;
62
+ } else {
63
+ break;
64
+ }
65
+ }
66
+
67
+ expect(highlightLength).toBe(selectionWidth);
68
+
69
+ for (let x = selectionWidth; x < width; x += 1) {
70
+ const color = getBgColor(bg, width, x, targetRow);
71
+ expect(isSameColor(color, defaultColor)).toBe(true);
72
+ }
73
+ } finally {
74
+ testSetup.renderer.destroy();
75
+ }
76
+ });
77
+ });
@@ -0,0 +1,351 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import { describe, expect, it, mock } from "bun:test";
3
+ import { testRender } from "@opentui/solid";
4
+ import { parseColor } from "@opentui/core";
5
+ import { createSignal } from "solid-js";
6
+ import type { FormattedLogEntry } from "../../../../logging/formatter.js";
7
+
8
+ const makeEntry = (
9
+ message: string,
10
+ overrides: Partial<FormattedLogEntry> = {},
11
+ ): FormattedLogEntry => ({
12
+ id: "1",
13
+ raw: { level: 30, time: "2026-01-08T00:00:00.000Z", category: "cli" },
14
+ timestamp: 0,
15
+ timeLabel: "00:00:00",
16
+ levelLabel: "INFO",
17
+ category: "cli",
18
+ message,
19
+ summary: `[00:00:00] [INFO] [cli] ${message}`,
20
+ json: JSON.stringify({ message }, null, 2),
21
+ ...overrides,
22
+ });
23
+
24
+ const findLine = (frame: string, needle: string) => {
25
+ const lines = frame.split("\n");
26
+ const index = lines.findIndex((line) => line.includes(needle));
27
+ if (index < 0) {
28
+ throw new Error(`Line not found: ${needle}`);
29
+ }
30
+ return { index, line: lines[index] ?? "" };
31
+ };
32
+
33
+ const readColorAt = (
34
+ buffer: Float32Array,
35
+ width: number,
36
+ row: number,
37
+ col: number,
38
+ ) => {
39
+ const offset = (row * width + col) * 4;
40
+ return [
41
+ Math.round(buffer[offset] * 255),
42
+ Math.round(buffer[offset + 1] * 255),
43
+ Math.round(buffer[offset + 2] * 255),
44
+ Math.round(buffer[offset + 3] * 255),
45
+ ] as const;
46
+ };
47
+
48
+ describe("LogScreen", () => {
49
+ it("updates entries when data changes", async () => {
50
+ const [entries, setEntries] = createSignal<FormattedLogEntry[]>([]);
51
+
52
+ const { LogScreen } = await import("../../screens/solid/LogScreen.js");
53
+
54
+ const testSetup = await testRender(
55
+ () => (
56
+ <LogScreen
57
+ entries={entries()}
58
+ branchLabel="feature/logs"
59
+ sourceLabel="/tmp/feature-logs"
60
+ onBack={() => {}}
61
+ onSelect={() => {}}
62
+ onCopy={() => {}}
63
+ />
64
+ ),
65
+ { width: 80, height: 24 },
66
+ );
67
+
68
+ try {
69
+ await testSetup.renderOnce();
70
+ let frame = testSetup.captureCharFrame();
71
+ expect(frame).toContain("Branch: feature/logs");
72
+ expect(frame).toContain("Source: /tmp/feature-logs");
73
+ expect(frame).toContain("No logs available.");
74
+
75
+ setEntries([makeEntry("Hello")]);
76
+ await testSetup.renderOnce();
77
+ frame = testSetup.captureCharFrame();
78
+ expect(frame).toContain("[00:00:00] [INFO ] [cli ] Hello");
79
+ } finally {
80
+ testSetup.renderer.destroy();
81
+ }
82
+ });
83
+
84
+ it("filters entries by query and shows counts", async () => {
85
+ const entries = [
86
+ makeEntry("alpha", {
87
+ id: "1",
88
+ category: "cli",
89
+ raw: { level: 30, time: "2026-01-08T00:00:00.000Z", category: "cli" },
90
+ }),
91
+ makeEntry("beta", {
92
+ id: "2",
93
+ category: "agent.stdout",
94
+ raw: {
95
+ level: 50,
96
+ time: "2026-01-08T00:00:01.000Z",
97
+ category: "agent.stdout",
98
+ },
99
+ }),
100
+ ];
101
+
102
+ const { LogScreen } = await import("../../screens/solid/LogScreen.js");
103
+
104
+ const testSetup = await testRender(
105
+ () => (
106
+ <LogScreen
107
+ entries={entries}
108
+ onBack={() => {}}
109
+ onSelect={() => {}}
110
+ onCopy={() => {}}
111
+ />
112
+ ),
113
+ { width: 80, height: 24 },
114
+ );
115
+
116
+ try {
117
+ await testSetup.renderOnce();
118
+ let frame = testSetup.captureCharFrame();
119
+ expect(frame).toContain("alpha");
120
+ expect(frame).toContain("beta");
121
+
122
+ await testSetup.mockInput.typeText("f");
123
+ await testSetup.renderOnce();
124
+
125
+ await testSetup.mockInput.typeText("agent");
126
+ await testSetup.renderOnce();
127
+ testSetup.mockInput.pressEnter();
128
+ await testSetup.renderOnce();
129
+
130
+ frame = testSetup.captureCharFrame();
131
+ expect(frame).toContain("beta");
132
+ expect(frame).not.toContain("alpha");
133
+ expect(frame).toContain("Showing 1 of 2");
134
+ } finally {
135
+ testSetup.renderer.destroy();
136
+ }
137
+ });
138
+
139
+ it("cycles level filter with v and hides lower levels", async () => {
140
+ const entries = [
141
+ makeEntry("info-log", {
142
+ id: "1",
143
+ levelLabel: "INFO",
144
+ raw: { level: 30, time: "2026-01-08T00:00:00.000Z", category: "cli" },
145
+ }),
146
+ makeEntry("error-log", {
147
+ id: "2",
148
+ levelLabel: "ERROR",
149
+ raw: { level: 50, time: "2026-01-08T00:00:01.000Z", category: "cli" },
150
+ }),
151
+ ];
152
+
153
+ const { LogScreen } = await import("../../screens/solid/LogScreen.js");
154
+
155
+ const testSetup = await testRender(
156
+ () => (
157
+ <LogScreen
158
+ entries={entries}
159
+ onBack={() => {}}
160
+ onSelect={() => {}}
161
+ onCopy={() => {}}
162
+ />
163
+ ),
164
+ { width: 80, height: 24 },
165
+ );
166
+
167
+ try {
168
+ await testSetup.renderOnce();
169
+ let frame = testSetup.captureCharFrame();
170
+ expect(frame).toContain("info-log");
171
+ expect(frame).toContain("error-log");
172
+
173
+ await testSetup.mockInput.typeText("v");
174
+ await testSetup.renderOnce();
175
+ await testSetup.mockInput.typeText("v");
176
+ await testSetup.renderOnce();
177
+
178
+ frame = testSetup.captureCharFrame();
179
+ expect(frame).toContain("error-log");
180
+ expect(frame).not.toContain("info-log");
181
+ expect(frame).toContain("Level: WARN+");
182
+ } finally {
183
+ testSetup.renderer.destroy();
184
+ }
185
+ });
186
+
187
+ it("triggers reload/tail and truncates long lines", async () => {
188
+ const onReload = mock();
189
+ const onToggleTail = mock();
190
+ const longMessage =
191
+ "this-is-a-very-long-log-line-that-should-be-truncated-with-ellipsis";
192
+ const entries = [
193
+ makeEntry(longMessage, {
194
+ id: "1",
195
+ raw: { level: 30, time: "2026-01-08T00:00:00.000Z", category: "cli" },
196
+ }),
197
+ ];
198
+
199
+ const { LogScreen } = await import("../../screens/solid/LogScreen.js");
200
+
201
+ const testSetup = await testRender(
202
+ () => (
203
+ <LogScreen
204
+ entries={entries}
205
+ onBack={() => {}}
206
+ onSelect={() => {}}
207
+ onCopy={() => {}}
208
+ onReload={onReload}
209
+ onToggleTail={onToggleTail}
210
+ />
211
+ ),
212
+ { width: 40, height: 16 },
213
+ );
214
+
215
+ try {
216
+ await testSetup.renderOnce();
217
+
218
+ await testSetup.mockInput.typeText("r");
219
+ await testSetup.renderOnce();
220
+ expect(onReload).toHaveBeenCalledTimes(1);
221
+
222
+ await testSetup.mockInput.typeText("t");
223
+ await testSetup.renderOnce();
224
+ expect(onToggleTail).toHaveBeenCalledTimes(1);
225
+
226
+ const frame = testSetup.captureCharFrame();
227
+ expect(frame).toContain("...");
228
+ expect(frame).not.toContain("Wrap:");
229
+ } finally {
230
+ testSetup.renderer.destroy();
231
+ }
232
+ });
233
+
234
+ it("triggers reset and shows Reset in footer", async () => {
235
+ const onReset = mock();
236
+ const { LogScreen } = await import("../../screens/solid/LogScreen.js");
237
+
238
+ const testSetup = await testRender(
239
+ () => (
240
+ <LogScreen
241
+ entries={[]}
242
+ onBack={() => {}}
243
+ onSelect={() => {}}
244
+ onCopy={() => {}}
245
+ onReset={onReset}
246
+ />
247
+ ),
248
+ { width: 80, height: 16 },
249
+ );
250
+
251
+ try {
252
+ await testSetup.renderOnce();
253
+ const frame = testSetup.captureCharFrame();
254
+ expect(frame).toContain("Reset");
255
+
256
+ await testSetup.mockInput.typeText("x");
257
+ await testSetup.renderOnce();
258
+ expect(onReset).toHaveBeenCalledTimes(1);
259
+ } finally {
260
+ testSetup.renderer.destroy();
261
+ }
262
+ });
263
+
264
+ it("highlights the selected row with full-width cyan background", async () => {
265
+ const entries = [
266
+ makeEntry("alpha", {
267
+ id: "1",
268
+ raw: { level: 30, time: "2026-01-08T00:00:00.000Z", category: "cli" },
269
+ }),
270
+ makeEntry("beta", {
271
+ id: "2",
272
+ raw: { level: 30, time: "2026-01-08T00:00:01.000Z", category: "cli" },
273
+ }),
274
+ ];
275
+
276
+ const { LogScreen } = await import("../../screens/solid/LogScreen.js");
277
+
278
+ const width = 60;
279
+ const testSetup = await testRender(
280
+ () => (
281
+ <LogScreen
282
+ entries={entries}
283
+ onBack={() => {}}
284
+ onSelect={() => {}}
285
+ onCopy={() => {}}
286
+ />
287
+ ),
288
+ { width, height: 16 },
289
+ );
290
+
291
+ try {
292
+ await testSetup.renderOnce();
293
+ const frame = testSetup.captureCharFrame();
294
+ const { index: row } = findLine(frame, "alpha");
295
+ const buffers = testSetup.renderer.currentRenderBuffer.buffers;
296
+ const bg = readColorAt(buffers.bg, width, row, width - 1);
297
+ const fg = readColorAt(buffers.fg, width, row, width - 1);
298
+
299
+ expect(bg).toEqual(parseColor("cyan").toInts());
300
+ expect(fg).toEqual(parseColor("black").toInts());
301
+ } finally {
302
+ testSetup.renderer.destroy();
303
+ }
304
+ });
305
+
306
+ it("colors the level column for non-selected rows", async () => {
307
+ const entries = [
308
+ makeEntry("first", {
309
+ id: "1",
310
+ levelLabel: "INFO",
311
+ raw: { level: 30, time: "2026-01-08T00:00:00.000Z", category: "cli" },
312
+ }),
313
+ makeEntry("second", {
314
+ id: "2",
315
+ levelLabel: "ERROR",
316
+ raw: { level: 50, time: "2026-01-08T00:00:01.000Z", category: "cli" },
317
+ }),
318
+ ];
319
+
320
+ const { LogScreen } = await import("../../screens/solid/LogScreen.js");
321
+
322
+ const width = 80;
323
+ const testSetup = await testRender(
324
+ () => (
325
+ <LogScreen
326
+ entries={entries}
327
+ onBack={() => {}}
328
+ onSelect={() => {}}
329
+ onCopy={() => {}}
330
+ />
331
+ ),
332
+ { width, height: 16 },
333
+ );
334
+
335
+ try {
336
+ await testSetup.renderOnce();
337
+ testSetup.mockInput.pressArrow("down");
338
+ await testSetup.renderOnce();
339
+ const frame = testSetup.captureCharFrame();
340
+ const { index: row, line } = findLine(frame, "first");
341
+ const levelIndex = line.indexOf("INFO");
342
+ expect(levelIndex).toBeGreaterThanOrEqual(0);
343
+
344
+ const buffers = testSetup.renderer.currentRenderBuffer.buffers;
345
+ const fg = readColorAt(buffers.fg, width, row, levelIndex);
346
+ expect(fg).toEqual(parseColor("green").toInts());
347
+ } finally {
348
+ testSetup.renderer.destroy();
349
+ }
350
+ });
351
+ });
@@ -131,7 +131,11 @@ describe("QuickStartStep", () => {
131
131
  testSetup.mockInput.pressEnter();
132
132
  await testSetup.renderOnce();
133
133
  expect(resumedEntry).not.toBeNull();
134
- expect(resumedEntry?.toolId).toBe("claude-code");
134
+ const assertedEntry = resumedEntry as ToolSessionEntry | null;
135
+ if (!assertedEntry) {
136
+ throw new Error("Expected resumed entry");
137
+ }
138
+ expect(assertedEntry.toolId).toBe("claude-code");
135
139
  } finally {
136
140
  testSetup.renderer.destroy();
137
141
  }
@@ -193,7 +197,7 @@ describe("QuickStartStep", () => {
193
197
 
194
198
  try {
195
199
  // 矢印キーで最後の項目(Choose different settings)を選択
196
- for (let i = 0; i < 5; i++) {
200
+ for (let i = 0; i < 4; i++) {
197
201
  testSetup.mockInput.pressArrow("down");
198
202
  await testSetup.renderOnce();
199
203
  }
@@ -234,4 +238,71 @@ describe("QuickStartStep", () => {
234
238
  }
235
239
  });
236
240
  });
241
+
242
+ describe("missing sessionId", () => {
243
+ it("does not render Resume option when sessionId is missing", async () => {
244
+ const historyWithoutSession: ToolSessionEntry[] = [
245
+ {
246
+ toolId: "claude-code",
247
+ toolLabel: "Claude Code",
248
+ branch: "feature/test",
249
+ worktreePath: "/path/to/worktree",
250
+ model: "claude-sonnet-4-20250514",
251
+ mode: "normal",
252
+ timestamp: Date.now(),
253
+ sessionId: null,
254
+ },
255
+ ];
256
+ const testSetup = await testRender(
257
+ () => (
258
+ <QuickStartStep
259
+ history={historyWithoutSession}
260
+ onResume={() => {}}
261
+ onStartNew={() => {}}
262
+ onChooseDifferent={() => {}}
263
+ onBack={() => {}}
264
+ />
265
+ ),
266
+ { width: 80, height: 24 },
267
+ );
268
+ await testSetup.renderOnce();
269
+
270
+ try {
271
+ const frame = testSetup.captureCharFrame();
272
+ expect(frame).not.toContain("Resume session");
273
+ expect(frame).toContain("Start new (previous settings)");
274
+ expect(frame).toContain("Choose different settings");
275
+ } finally {
276
+ testSetup.renderer.destroy();
277
+ }
278
+ });
279
+ });
280
+
281
+ describe("multi-tool display", () => {
282
+ it("renders tool-specific descriptions and session id only for resume", async () => {
283
+ const testSetup = await testRender(
284
+ () => (
285
+ <QuickStartStep
286
+ history={mockHistory}
287
+ onResume={() => {}}
288
+ onStartNew={() => {}}
289
+ onChooseDifferent={() => {}}
290
+ onBack={() => {}}
291
+ />
292
+ ),
293
+ { width: 90, height: 24 },
294
+ );
295
+ await testSetup.renderOnce();
296
+
297
+ try {
298
+ const frame = testSetup.captureCharFrame();
299
+ expect(frame).toContain("Claude Code, claude-sonnet-4-20250514");
300
+ expect(frame).toContain("Codex CLI, o3-mini, high");
301
+ expect(frame).toContain("Session: session-123");
302
+ expect(frame).toContain("Session: session-456");
303
+ } finally {
304
+ testSetup.renderer.destroy();
305
+ }
306
+ });
307
+ });
237
308
  });
@@ -230,7 +230,10 @@ describe("SkipPermissionsStep", () => {
230
230
  // Enterキーで選択(デフォルトはYes)
231
231
  testSetup.mockInput.pressEnter();
232
232
  await testSetup.renderOnce();
233
- expect(selected).toBe(true);
233
+ if (selected === null) {
234
+ throw new Error("Expected selection");
235
+ }
236
+ expect(selected === true).toBe(true);
234
237
  } finally {
235
238
  testSetup.renderer.destroy();
236
239
  }