@akiojin/gwt 2.2.0 → 2.3.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 (149) hide show
  1. package/README.ja.md +4 -4
  2. package/README.md +4 -4
  3. package/dist/cli/ui/components/App.d.ts +4 -4
  4. package/dist/cli/ui/components/App.d.ts.map +1 -1
  5. package/dist/cli/ui/components/App.js +144 -105
  6. package/dist/cli/ui/components/App.js.map +1 -1
  7. package/dist/cli/ui/components/common/Confirm.d.ts +1 -1
  8. package/dist/cli/ui/components/common/Confirm.d.ts.map +1 -1
  9. package/dist/cli/ui/components/common/Confirm.js +7 -7
  10. package/dist/cli/ui/components/common/Confirm.js.map +1 -1
  11. package/dist/cli/ui/components/common/ErrorBoundary.d.ts +1 -1
  12. package/dist/cli/ui/components/common/ErrorBoundary.d.ts.map +1 -1
  13. package/dist/cli/ui/components/common/ErrorBoundary.js +4 -4
  14. package/dist/cli/ui/components/common/ErrorBoundary.js.map +1 -1
  15. package/dist/cli/ui/components/common/Input.d.ts +2 -2
  16. package/dist/cli/ui/components/common/Input.d.ts.map +1 -1
  17. package/dist/cli/ui/components/common/Input.js +4 -4
  18. package/dist/cli/ui/components/common/Input.js.map +1 -1
  19. package/dist/cli/ui/components/common/LoadingIndicator.d.ts +1 -1
  20. package/dist/cli/ui/components/common/LoadingIndicator.d.ts.map +1 -1
  21. package/dist/cli/ui/components/common/LoadingIndicator.js +4 -4
  22. package/dist/cli/ui/components/common/LoadingIndicator.js.map +1 -1
  23. package/dist/cli/ui/components/common/Select.d.ts +1 -1
  24. package/dist/cli/ui/components/common/Select.d.ts.map +1 -1
  25. package/dist/cli/ui/components/common/Select.js +11 -12
  26. package/dist/cli/ui/components/common/Select.js.map +1 -1
  27. package/dist/cli/ui/components/screens/AIToolSelectorScreen.d.ts +2 -2
  28. package/dist/cli/ui/components/screens/AIToolSelectorScreen.d.ts.map +1 -1
  29. package/dist/cli/ui/components/screens/AIToolSelectorScreen.js +11 -11
  30. package/dist/cli/ui/components/screens/AIToolSelectorScreen.js.map +1 -1
  31. package/dist/cli/ui/components/screens/BranchCreatorScreen.d.ts +1 -1
  32. package/dist/cli/ui/components/screens/BranchCreatorScreen.d.ts.map +1 -1
  33. package/dist/cli/ui/components/screens/BranchCreatorScreen.js +39 -36
  34. package/dist/cli/ui/components/screens/BranchCreatorScreen.js.map +1 -1
  35. package/dist/cli/ui/components/screens/BranchListScreen.d.ts +3 -3
  36. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  37. package/dist/cli/ui/components/screens/BranchListScreen.js +55 -50
  38. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  39. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.d.ts +2 -2
  40. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.d.ts.map +1 -1
  41. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.js +25 -25
  42. package/dist/cli/ui/components/screens/ExecutionModeSelectorScreen.js.map +1 -1
  43. package/dist/cli/ui/components/screens/PRCleanupScreen.d.ts +2 -2
  44. package/dist/cli/ui/components/screens/PRCleanupScreen.js +21 -21
  45. package/dist/cli/ui/components/screens/SessionSelectorScreen.d.ts +1 -1
  46. package/dist/cli/ui/components/screens/SessionSelectorScreen.js +8 -8
  47. package/dist/cli/ui/components/screens/WorktreeManagerScreen.d.ts +1 -1
  48. package/dist/cli/ui/components/screens/WorktreeManagerScreen.js +8 -8
  49. package/dist/cli/ui/screens/BranchActionSelectorScreen.d.ts.map +1 -1
  50. package/dist/cli/ui/screens/BranchActionSelectorScreen.js +7 -4
  51. package/dist/cli/ui/screens/BranchActionSelectorScreen.js.map +1 -1
  52. package/dist/cli/ui/types.d.ts.map +1 -1
  53. package/dist/client/assets/{index-V6hDu9KS.js → index-Difv1Hwu.js} +2 -2
  54. package/dist/client/index.html +1 -1
  55. package/dist/config/builtin-tools.d.ts +10 -2
  56. package/dist/config/builtin-tools.d.ts.map +1 -1
  57. package/dist/config/builtin-tools.js +40 -4
  58. package/dist/config/builtin-tools.js.map +1 -1
  59. package/dist/config/index.d.ts.map +1 -1
  60. package/dist/config/index.js.map +1 -1
  61. package/dist/config/tools.d.ts.map +1 -1
  62. package/dist/config/tools.js +4 -3
  63. package/dist/config/tools.js.map +1 -1
  64. package/dist/gemini.d.ts +12 -0
  65. package/dist/gemini.d.ts.map +1 -0
  66. package/dist/gemini.js +154 -0
  67. package/dist/gemini.js.map +1 -0
  68. package/dist/git.d.ts.map +1 -1
  69. package/dist/git.js.map +1 -1
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +30 -0
  72. package/dist/index.js.map +1 -1
  73. package/dist/qwen.d.ts +12 -0
  74. package/dist/qwen.d.ts.map +1 -0
  75. package/dist/qwen.js +154 -0
  76. package/dist/qwen.js.map +1 -0
  77. package/dist/services/git.service.d.ts.map +1 -1
  78. package/dist/services/git.service.js.map +1 -1
  79. package/dist/web/client/src/components/BranchGraph.d.ts.map +1 -1
  80. package/dist/web/client/src/components/BranchGraph.js +1 -1
  81. package/dist/web/client/src/components/BranchGraph.js.map +1 -1
  82. package/dist/web/client/src/components/EnvEditor.d.ts.map +1 -1
  83. package/dist/web/client/src/components/EnvEditor.js +7 -4
  84. package/dist/web/client/src/components/EnvEditor.js.map +1 -1
  85. package/dist/web/client/src/pages/BranchDetailPage.d.ts.map +1 -1
  86. package/dist/web/client/src/pages/BranchDetailPage.js +55 -18
  87. package/dist/web/client/src/pages/BranchDetailPage.js.map +1 -1
  88. package/dist/web/client/src/pages/BranchListPage.d.ts.map +1 -1
  89. package/dist/web/client/src/pages/BranchListPage.js +10 -4
  90. package/dist/web/client/src/pages/BranchListPage.js.map +1 -1
  91. package/dist/web/client/src/pages/ConfigManagementPage.d.ts.map +1 -1
  92. package/dist/web/client/src/pages/ConfigManagementPage.js +4 -2
  93. package/dist/web/client/src/pages/ConfigManagementPage.js.map +1 -1
  94. package/package.json +2 -1
  95. package/src/cli/ui/__tests__/acceptance/navigation.acceptance.test.tsx +69 -50
  96. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +67 -45
  97. package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +117 -75
  98. package/src/cli/ui/__tests__/components/App.test.tsx +45 -37
  99. package/src/cli/ui/__tests__/components/common/Confirm.test.tsx +35 -22
  100. package/src/cli/ui/__tests__/components/common/ErrorBoundary.test.tsx +22 -22
  101. package/src/cli/ui/__tests__/components/common/Input.test.tsx +29 -22
  102. package/src/cli/ui/__tests__/components/common/LoadingIndicator.test.tsx +40 -34
  103. package/src/cli/ui/__tests__/components/common/Select.memo.test.tsx +57 -66
  104. package/src/cli/ui/__tests__/components/common/Select.test.tsx +121 -91
  105. package/src/cli/ui/__tests__/components/parts/Footer.test.tsx +18 -16
  106. package/src/cli/ui/__tests__/components/parts/Header.test.tsx +13 -13
  107. package/src/cli/ui/__tests__/components/parts/ScrollableList.test.tsx +20 -20
  108. package/src/cli/ui/__tests__/components/parts/Stats.test.tsx +38 -26
  109. package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +31 -31
  110. package/src/cli/ui/__tests__/components/screens/BranchCreatorScreen.test.tsx +73 -37
  111. package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +261 -153
  112. package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +38 -32
  113. package/src/cli/ui/__tests__/components/screens/PRCleanupScreen.test.tsx +39 -39
  114. package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +49 -21
  115. package/src/cli/ui/__tests__/components/screens/WorktreeManagerScreen.test.tsx +52 -28
  116. package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +84 -48
  117. package/src/cli/ui/__tests__/integration/navigation.test.tsx +111 -83
  118. package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx +111 -108
  119. package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +50 -37
  120. package/src/cli/ui/__tests__/performance/useMemoOptimization.test.tsx +75 -76
  121. package/src/cli/ui/components/App.tsx +247 -150
  122. package/src/cli/ui/components/common/Confirm.tsx +13 -9
  123. package/src/cli/ui/components/common/ErrorBoundary.tsx +8 -5
  124. package/src/cli/ui/components/common/Input.tsx +12 -4
  125. package/src/cli/ui/components/common/LoadingIndicator.tsx +8 -5
  126. package/src/cli/ui/components/common/Select.tsx +28 -17
  127. package/src/cli/ui/components/parts/Header.test.tsx +5 -15
  128. package/src/cli/ui/components/screens/AIToolSelectorScreen.tsx +19 -13
  129. package/src/cli/ui/components/screens/BranchCreatorScreen.tsx +74 -54
  130. package/src/cli/ui/components/screens/BranchListScreen.tsx +92 -75
  131. package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +35 -28
  132. package/src/cli/ui/components/screens/PRCleanupScreen.tsx +22 -22
  133. package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +8 -8
  134. package/src/cli/ui/components/screens/WorktreeManagerScreen.tsx +8 -8
  135. package/src/cli/ui/screens/BranchActionSelectorScreen.tsx +9 -4
  136. package/src/cli/ui/types.ts +8 -1
  137. package/src/config/builtin-tools.ts +42 -4
  138. package/src/config/index.ts +2 -12
  139. package/src/config/tools.ts +16 -6
  140. package/src/gemini.ts +202 -0
  141. package/src/git.ts +2 -1
  142. package/src/index.ts +30 -0
  143. package/src/qwen.ts +208 -0
  144. package/src/services/git.service.ts +2 -1
  145. package/src/web/client/src/components/BranchGraph.tsx +3 -2
  146. package/src/web/client/src/components/EnvEditor.tsx +44 -11
  147. package/src/web/client/src/pages/BranchDetailPage.tsx +165 -54
  148. package/src/web/client/src/pages/BranchListPage.tsx +37 -13
  149. package/src/web/client/src/pages/ConfigManagementPage.tsx +28 -9
@@ -1,27 +1,28 @@
1
1
  /**
2
2
  * @vitest-environment happy-dom
3
3
  */
4
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
5
- import { act, render } from '@testing-library/react';
6
- import { render as inkRender } from 'ink-testing-library';
7
- import React from 'react';
8
- import { BranchListScreen } from '../../../components/screens/BranchListScreen.js';
9
- import type { BranchInfo, BranchItem, Statistics } from '../../../types.js';
10
- import { formatBranchItem } from '../../../utils/branchFormatter.js';
11
- import stringWidth from 'string-width';
12
- import { Window } from 'happy-dom';
13
-
14
- const stripAnsi = (value: string): string => value.replace(/\u001b\[[0-9;]*m/g, '');
4
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
5
+ import { act, render } from "@testing-library/react";
6
+ import { render as inkRender } from "ink-testing-library";
7
+ import React from "react";
8
+ import { BranchListScreen } from "../../../components/screens/BranchListScreen.js";
9
+ import type { BranchInfo, BranchItem, Statistics } from "../../../types.js";
10
+ import { formatBranchItem } from "../../../utils/branchFormatter.js";
11
+ import stringWidth from "string-width";
12
+ import { Window } from "happy-dom";
13
+
14
+ const stripAnsi = (value: string): string =>
15
+ value.replace(/\u001b\[[0-9;]*m/g, "");
15
16
  const stripControlSequences = (value: string): string =>
16
17
  value.replace(/\u001b\[([0-9;?]*)([A-Za-z])/g, (_, params, command) => {
17
- if (command === 'C') {
18
- const count = Number(params || '1');
19
- return ' '.repeat(Number.isNaN(count) ? 0 : count);
18
+ if (command === "C") {
19
+ const count = Number(params || "1");
20
+ return " ".repeat(Number.isNaN(count) ? 0 : count);
20
21
  }
21
- return '';
22
+ return "";
22
23
  });
23
24
 
24
- describe('BranchListScreen', () => {
25
+ describe("BranchListScreen", () => {
25
26
  beforeEach(() => {
26
27
  vi.useFakeTimers();
27
28
  // Setup happy-dom
@@ -36,25 +37,25 @@ describe('BranchListScreen', () => {
36
37
 
37
38
  const mockBranches: BranchItem[] = [
38
39
  {
39
- name: 'main',
40
- type: 'local',
41
- branchType: 'main',
40
+ name: "main",
41
+ type: "local",
42
+ branchType: "main",
42
43
  isCurrent: true,
43
- icons: ['', ''],
44
+ icons: ["", ""],
44
45
  hasChanges: false,
45
- label: '⚡ ⭐ main',
46
- value: 'main',
46
+ label: "⚡ ⭐ main",
47
+ value: "main",
47
48
  latestCommitTimestamp: 1_700_000_000,
48
49
  },
49
50
  {
50
- name: 'feature/test',
51
- type: 'local',
52
- branchType: 'feature',
51
+ name: "feature/test",
52
+ type: "local",
53
+ branchType: "feature",
53
54
  isCurrent: false,
54
- icons: [''],
55
+ icons: [""],
55
56
  hasChanges: false,
56
- label: '✨ feature/test',
57
- value: 'feature/test',
57
+ label: "✨ feature/test",
58
+ value: "feature/test",
58
59
  latestCommitTimestamp: 1_699_000_000,
59
60
  },
60
61
  ];
@@ -67,46 +68,62 @@ describe('BranchListScreen', () => {
67
68
  lastUpdated: new Date(),
68
69
  };
69
70
 
70
- it('should render header with title', () => {
71
+ it("should render header with title", () => {
71
72
  const onSelect = vi.fn();
72
73
  const { getByText } = render(
73
- <BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} />
74
+ <BranchListScreen
75
+ branches={mockBranches}
76
+ stats={mockStats}
77
+ onSelect={onSelect}
78
+ />,
74
79
  );
75
80
 
76
81
  expect(getByText(/gwt - Branch Selection/i)).toBeDefined();
77
82
  });
78
83
 
79
- it('should render statistics', () => {
84
+ it("should render statistics", () => {
80
85
  const onSelect = vi.fn();
81
86
  const { container, getByText } = render(
82
- <BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} />
87
+ <BranchListScreen
88
+ branches={mockBranches}
89
+ stats={mockStats}
90
+ onSelect={onSelect}
91
+ />,
83
92
  );
84
93
 
85
- expect(container.textContent).toContain('Local: 2');
94
+ expect(container.textContent).toContain("Local: 2");
86
95
  expect(getByText(/Remote:/)).toBeDefined();
87
96
  });
88
97
 
89
- it('should render branch list', () => {
98
+ it("should render branch list", () => {
90
99
  const onSelect = vi.fn();
91
100
  const { getByText } = render(
92
- <BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} />
101
+ <BranchListScreen
102
+ branches={mockBranches}
103
+ stats={mockStats}
104
+ onSelect={onSelect}
105
+ />,
93
106
  );
94
107
 
95
108
  expect(getByText(/main/)).toBeDefined();
96
109
  expect(getByText(/feature\/test/)).toBeDefined();
97
110
  });
98
111
 
99
- it('should render footer with actions', () => {
112
+ it("should render footer with actions", () => {
100
113
  const onSelect = vi.fn();
101
114
  const { getAllByText } = render(
102
- <BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} />
115
+ <BranchListScreen
116
+ branches={mockBranches}
117
+ stats={mockStats}
118
+ onSelect={onSelect}
119
+ />,
103
120
  );
104
121
 
105
122
  // Check for enter key (main screen doesn't have q key, exit is Ctrl+C only)
106
123
  expect(getAllByText(/enter/i).length).toBeGreaterThan(0);
107
124
  });
108
125
 
109
- it('should handle empty branch list', () => {
126
+ it("should handle empty branch list", () => {
110
127
  const onSelect = vi.fn();
111
128
  const emptyStats: Statistics = {
112
129
  localCount: 0,
@@ -117,13 +134,13 @@ describe('BranchListScreen', () => {
117
134
  };
118
135
 
119
136
  const { container } = render(
120
- <BranchListScreen branches={[]} stats={emptyStats} onSelect={onSelect} />
137
+ <BranchListScreen branches={[]} stats={emptyStats} onSelect={onSelect} />,
121
138
  );
122
139
 
123
140
  expect(container).toBeDefined();
124
141
  });
125
142
 
126
- it('should display loading indicator after the configured delay', async () => {
143
+ it("should display loading indicator after the configured delay", async () => {
127
144
  const onSelect = vi.fn();
128
145
  const { queryByText, getByText } = render(
129
146
  <BranchListScreen
@@ -132,11 +149,11 @@ describe('BranchListScreen', () => {
132
149
  onSelect={onSelect}
133
150
  loading={true}
134
151
  loadingIndicatorDelay={10}
135
- />
152
+ />,
136
153
  );
137
154
 
138
155
  await act(async () => {
139
- if (typeof (vi as any).advanceTimersByTime === 'function') {
156
+ if (typeof (vi as any).advanceTimersByTime === "function") {
140
157
  (vi as any).advanceTimersByTime(10);
141
158
  } else {
142
159
  await new Promise((resolve) => setTimeout(resolve, 10));
@@ -146,18 +163,23 @@ describe('BranchListScreen', () => {
146
163
  expect(getByText(/Loading Git information/i)).toBeDefined();
147
164
  });
148
165
 
149
- it('should display error state', () => {
166
+ it("should display error state", () => {
150
167
  const onSelect = vi.fn();
151
- const error = new Error('Failed to load branches');
168
+ const error = new Error("Failed to load branches");
152
169
  const { getByText } = render(
153
- <BranchListScreen branches={[]} stats={mockStats} onSelect={onSelect} error={error} />
170
+ <BranchListScreen
171
+ branches={[]}
172
+ stats={mockStats}
173
+ onSelect={onSelect}
174
+ error={error}
175
+ />,
154
176
  );
155
177
 
156
178
  expect(getByText(/Error:/i)).toBeDefined();
157
179
  expect(getByText(/Failed to load branches/i)).toBeDefined();
158
180
  });
159
181
 
160
- it('should use terminal height for layout calculation', () => {
182
+ it("should use terminal height for layout calculation", () => {
161
183
  const onSelect = vi.fn();
162
184
 
163
185
  // Mock process.stdout
@@ -165,7 +187,11 @@ describe('BranchListScreen', () => {
165
187
  process.stdout.rows = 30;
166
188
 
167
189
  const { container } = render(
168
- <BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} />
190
+ <BranchListScreen
191
+ branches={mockBranches}
192
+ stats={mockStats}
193
+ onSelect={onSelect}
194
+ />,
169
195
  );
170
196
 
171
197
  expect(container).toBeDefined();
@@ -174,10 +200,14 @@ describe('BranchListScreen', () => {
174
200
  process.stdout.rows = originalRows;
175
201
  });
176
202
 
177
- it('should display branch icons', () => {
203
+ it("should display branch icons", () => {
178
204
  const onSelect = vi.fn();
179
205
  const { getByText } = render(
180
- <BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} />
206
+ <BranchListScreen
207
+ branches={mockBranches}
208
+ stats={mockStats}
209
+ onSelect={onSelect}
210
+ />,
181
211
  );
182
212
 
183
213
  // Check for icons in labels
@@ -186,34 +216,42 @@ describe('BranchListScreen', () => {
186
216
  expect(getByText(/✨/)).toBeDefined(); // feature icon
187
217
  });
188
218
 
189
- it('should render latest commit timestamp for each branch', () => {
219
+ it("should render latest commit timestamp for each branch", () => {
190
220
  const onSelect = vi.fn();
191
221
  const { container } = render(
192
- <BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} />
222
+ <BranchListScreen
223
+ branches={mockBranches}
224
+ stats={mockStats}
225
+ onSelect={onSelect}
226
+ />,
193
227
  );
194
228
 
195
- const textContent = container.textContent ?? '';
229
+ const textContent = container.textContent ?? "";
196
230
  const matches = textContent.match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}/g) ?? [];
197
231
  expect(matches.length).toBe(mockBranches.length);
198
232
  });
199
233
 
200
- it('should highlight the selected branch with cyan background', async () => {
201
- process.env.FORCE_COLOR = '1';
234
+ it("should highlight the selected branch with cyan background", async () => {
235
+ process.env.FORCE_COLOR = "1";
202
236
  const onSelect = vi.fn();
203
237
  let renderResult: ReturnType<typeof inkRender>;
204
238
  await act(async () => {
205
239
  renderResult = inkRender(
206
- <BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} />,
207
- { stripAnsi: false }
240
+ <BranchListScreen
241
+ branches={mockBranches}
242
+ stats={mockStats}
243
+ onSelect={onSelect}
244
+ />,
245
+ { stripAnsi: false },
208
246
  );
209
247
  });
210
248
 
211
- const frame = renderResult!.lastFrame() ?? '';
212
- expect(frame).toContain('\u001b[46m'); // cyan background ANSI code
249
+ const frame = renderResult!.lastFrame() ?? "";
250
+ expect(frame).toContain("\u001b[46m"); // cyan background ANSI code
213
251
  });
214
252
 
215
- it('should align timestamps even when unpushed icon is displayed', async () => {
216
- process.env.FORCE_COLOR = '1';
253
+ it("should align timestamps even when unpushed icon is displayed", async () => {
254
+ process.env.FORCE_COLOR = "1";
217
255
  const onSelect = vi.fn();
218
256
 
219
257
  const originalColumns = process.stdout.columns;
@@ -221,25 +259,25 @@ describe('BranchListScreen', () => {
221
259
 
222
260
  const branchInfos: BranchInfo[] = [
223
261
  {
224
- name: 'feature/update-ui',
225
- type: 'local',
226
- branchType: 'feature',
262
+ name: "feature/update-ui",
263
+ type: "local",
264
+ branchType: "feature",
227
265
  isCurrent: false,
228
266
  hasUnpushedCommits: true,
229
267
  latestCommitTimestamp: 1_700_000_000,
230
268
  },
231
269
  {
232
- name: 'origin/main',
233
- type: 'remote',
234
- branchType: 'main',
270
+ name: "origin/main",
271
+ type: "remote",
272
+ branchType: "main",
235
273
  isCurrent: false,
236
274
  hasUnpushedCommits: false,
237
275
  latestCommitTimestamp: 1_699_999_000,
238
276
  },
239
277
  {
240
- name: 'main',
241
- type: 'local',
242
- branchType: 'main',
278
+ name: "main",
279
+ type: "local",
280
+ branchType: "main",
243
281
  isCurrent: true,
244
282
  hasUnpushedCommits: false,
245
283
  latestCommitTimestamp: 1_699_998_000,
@@ -247,21 +285,25 @@ describe('BranchListScreen', () => {
247
285
  ];
248
286
 
249
287
  const branchesWithUnpushed: BranchItem[] = branchInfos.map((branch) =>
250
- formatBranchItem(branch)
288
+ formatBranchItem(branch),
251
289
  );
252
290
 
253
291
  try {
254
292
  let renderResult: ReturnType<typeof inkRender>;
255
293
  await act(async () => {
256
294
  renderResult = inkRender(
257
- <BranchListScreen branches={branchesWithUnpushed} stats={mockStats} onSelect={onSelect} />,
258
- { stripAnsi: false }
295
+ <BranchListScreen
296
+ branches={branchesWithUnpushed}
297
+ stats={mockStats}
298
+ onSelect={onSelect}
299
+ />,
300
+ { stripAnsi: false },
259
301
  );
260
302
  });
261
303
 
262
- const frame = renderResult!.lastFrame() ?? '';
304
+ const frame = renderResult!.lastFrame() ?? "";
263
305
  const timestampLines = frame
264
- .split('\n')
306
+ .split("\n")
265
307
  .map((line) => stripControlSequences(stripAnsi(line)))
266
308
  .filter((line) => /\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(line));
267
309
 
@@ -274,7 +316,7 @@ describe('BranchListScreen', () => {
274
316
 
275
317
  let width = 0;
276
318
  for (const char of Array.from(beforeTimestamp)) {
277
- if (char === '\u2B06' || char === '\u2601') {
319
+ if (char === "\u2B06" || char === "\u2601") {
278
320
  width += 1;
279
321
  continue;
280
322
  }
@@ -291,29 +333,40 @@ describe('BranchListScreen', () => {
291
333
  }
292
334
  });
293
335
 
294
- describe('Filter Mode', () => {
295
- it('should always display filter input field', () => {
336
+ describe("Filter Mode", () => {
337
+ it("should always display filter input field", () => {
296
338
  // Note: Filter input is now always visible (no need to press 'f' key)
297
339
  const onSelect = vi.fn();
298
340
  const { container } = render(
299
- <BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} />
341
+ <BranchListScreen
342
+ branches={mockBranches}
343
+ stats={mockStats}
344
+ onSelect={onSelect}
345
+ />,
300
346
  );
301
347
 
302
348
  // Filter input field should be displayed by default
303
- expect(container.textContent).toContain('Filter:');
349
+ expect(container.textContent).toContain("Filter:");
304
350
  });
305
351
 
306
- it('should enter filter mode when f key is pressed', () => {
352
+ it("should enter filter mode when f key is pressed", () => {
307
353
  const onSelect = vi.fn();
308
354
  const { container } = render(
309
- <BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} />
355
+ <BranchListScreen
356
+ branches={mockBranches}
357
+ stats={mockStats}
358
+ onSelect={onSelect}
359
+ />,
310
360
  );
311
361
 
312
362
  // Initially should show prompt to press f
313
- expect(container.textContent).toContain('(press f to filter)');
363
+ expect(container.textContent).toContain("(press f to filter)");
314
364
 
315
365
  // Press 'f' key
316
- const fKeyEvent = new (globalThis.window as any).KeyboardEvent('keydown', { key: 'f' });
366
+ const fKeyEvent = new (globalThis.window as any).KeyboardEvent(
367
+ "keydown",
368
+ { key: "f" },
369
+ );
317
370
  document.dispatchEvent(fKeyEvent);
318
371
 
319
372
  // Filter input should be active (placeholder visible)
@@ -321,45 +374,65 @@ describe('BranchListScreen', () => {
321
374
  expect(container).toBeDefined();
322
375
  });
323
376
 
324
- it('should exit filter mode and return to branch selection when Esc is pressed in filter mode', () => {
377
+ it("should exit filter mode and return to branch selection when Esc is pressed in filter mode", () => {
325
378
  const onSelect = vi.fn();
326
379
  const { container } = render(
327
- <BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} />
380
+ <BranchListScreen
381
+ branches={mockBranches}
382
+ stats={mockStats}
383
+ onSelect={onSelect}
384
+ />,
328
385
  );
329
386
 
330
387
  // Enter filter mode first
331
- const fKeyEvent = new (globalThis.window as any).KeyboardEvent('keydown', { key: 'f' });
388
+ const fKeyEvent = new (globalThis.window as any).KeyboardEvent(
389
+ "keydown",
390
+ { key: "f" },
391
+ );
332
392
  document.dispatchEvent(fKeyEvent);
333
393
 
334
394
  // Press Escape
335
- const escKeyEvent = new (globalThis.window as any).KeyboardEvent('keydown', { key: 'Escape' });
395
+ const escKeyEvent = new (globalThis.window as any).KeyboardEvent(
396
+ "keydown",
397
+ { key: "Escape" },
398
+ );
336
399
  document.dispatchEvent(escKeyEvent);
337
400
 
338
401
  // Should return to branch selection mode
339
402
  // Select should be active, Input should be inactive
340
- expect(container.textContent).toContain('(press f to filter)');
403
+ expect(container.textContent).toContain("(press f to filter)");
341
404
  });
342
405
 
343
- it('should show branch list cursor highlight in filter mode', () => {
344
- process.env.FORCE_COLOR = '1';
406
+ it("should show branch list cursor highlight in filter mode", () => {
407
+ process.env.FORCE_COLOR = "1";
345
408
  const onSelect = vi.fn();
346
409
  let renderResult: ReturnType<typeof inkRender>;
347
410
  act(() => {
348
411
  renderResult = inkRender(
349
- <BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} testFilterMode={true} />,
350
- { stripAnsi: false }
412
+ <BranchListScreen
413
+ branches={mockBranches}
414
+ stats={mockStats}
415
+ onSelect={onSelect}
416
+ testFilterMode={true}
417
+ />,
418
+ { stripAnsi: false },
351
419
  );
352
420
  });
353
421
 
354
- const frame = renderResult!.lastFrame() ?? '';
422
+ const frame = renderResult!.lastFrame() ?? "";
355
423
  // Should contain cyan background (cursor highlight) even in filter mode
356
- expect(frame).toContain('\u001b[46m');
424
+ expect(frame).toContain("\u001b[46m");
357
425
  });
358
426
 
359
- it('should allow cursor movement with arrow keys in filter mode', () => {
427
+ it("should allow cursor movement with arrow keys in filter mode", () => {
360
428
  const onSelect = vi.fn();
361
429
  const { container } = render(
362
- <BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} testFilterMode={true} />
430
+ <BranchListScreen
431
+ branches={mockBranches}
432
+ stats={mockStats}
433
+ onSelect={onSelect}
434
+ testFilterMode={true}
435
+ />,
363
436
  );
364
437
 
365
438
  // Arrow keys should work in filter mode (Select component should not be disabled)
@@ -367,10 +440,15 @@ describe('BranchListScreen', () => {
367
440
  expect(container).toBeDefined();
368
441
  });
369
442
 
370
- it('should allow branch selection with Enter key in filter mode', () => {
443
+ it("should allow branch selection with Enter key in filter mode", () => {
371
444
  const onSelect = vi.fn();
372
445
  const { container } = render(
373
- <BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} testFilterMode={true} />
446
+ <BranchListScreen
447
+ branches={mockBranches}
448
+ stats={mockStats}
449
+ onSelect={onSelect}
450
+ testFilterMode={true}
451
+ />,
374
452
  );
375
453
 
376
454
  // Simulate Enter key (this will trigger onSelect if Select is enabled)
@@ -379,10 +457,14 @@ describe('BranchListScreen', () => {
379
457
  expect(container).toBeDefined();
380
458
  });
381
459
 
382
- it('should disable filter input cursor when in branch selection mode', () => {
460
+ it("should disable filter input cursor when in branch selection mode", () => {
383
461
  const onSelect = vi.fn();
384
462
  const { container } = render(
385
- <BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} />
463
+ <BranchListScreen
464
+ branches={mockBranches}
465
+ stats={mockStats}
466
+ onSelect={onSelect}
467
+ />,
386
468
  );
387
469
 
388
470
  // By default, should be in branch selection mode
@@ -390,19 +472,19 @@ describe('BranchListScreen', () => {
390
472
  expect(container).toBeDefined();
391
473
  });
392
474
 
393
- it('should filter branches in real-time as user types', () => {
475
+ it("should filter branches in real-time as user types", () => {
394
476
  const onSelect = vi.fn();
395
477
  const branches: BranchItem[] = [
396
478
  ...mockBranches,
397
479
  {
398
- name: 'bugfix/issue-123',
399
- type: 'local',
400
- branchType: 'bugfix',
480
+ name: "bugfix/issue-123",
481
+ type: "local",
482
+ branchType: "bugfix",
401
483
  isCurrent: false,
402
- icons: ['🐛'],
484
+ icons: ["🐛"],
403
485
  hasChanges: false,
404
- label: '🐛 bugfix/issue-123',
405
- value: 'bugfix/issue-123',
486
+ label: "🐛 bugfix/issue-123",
487
+ value: "bugfix/issue-123",
406
488
  latestCommitTimestamp: 1_698_000_000,
407
489
  },
408
490
  ];
@@ -414,62 +496,82 @@ describe('BranchListScreen', () => {
414
496
  onSelect={onSelect}
415
497
  testFilterMode={true}
416
498
  testFilterQuery="feature"
417
- />
499
+ />,
418
500
  );
419
501
 
420
502
  // Only feature/test should be visible
421
- expect(container.textContent).toContain('feature/test');
422
- expect(container.textContent).not.toContain('bugfix/issue-123');
503
+ expect(container.textContent).toContain("feature/test");
504
+ expect(container.textContent).not.toContain("bugfix/issue-123");
423
505
  });
424
506
 
425
- it('should clear filter query when Esc key is pressed (with query)', () => {
507
+ it("should clear filter query when Esc key is pressed (with query)", () => {
426
508
  // Note: Filter input remains visible, only the query is cleared
427
509
  const onSelect = vi.fn();
428
510
  const { container } = render(
429
- <BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} />
511
+ <BranchListScreen
512
+ branches={mockBranches}
513
+ stats={mockStats}
514
+ onSelect={onSelect}
515
+ />,
430
516
  );
431
517
 
432
518
  // Enter filter mode
433
- const fKeyEvent = new (globalThis.window as any).KeyboardEvent('keydown', { key: 'f' });
519
+ const fKeyEvent = new (globalThis.window as any).KeyboardEvent(
520
+ "keydown",
521
+ { key: "f" },
522
+ );
434
523
  document.dispatchEvent(fKeyEvent);
435
524
 
436
525
  // Type something in filter
437
- const input = container.querySelector('input');
526
+ const input = container.querySelector("input");
438
527
  if (input) {
439
- input.value = 'feature';
440
- input.dispatchEvent(new Event('input', { bubbles: true }));
528
+ input.value = "feature";
529
+ input.dispatchEvent(new Event("input", { bubbles: true }));
441
530
  }
442
531
 
443
532
  // Press Escape (should clear query first)
444
- const escKeyEvent = new (globalThis.window as any).KeyboardEvent('keydown', { key: 'Escape' });
533
+ const escKeyEvent = new (globalThis.window as any).KeyboardEvent(
534
+ "keydown",
535
+ { key: "Escape" },
536
+ );
445
537
  document.dispatchEvent(escKeyEvent);
446
538
 
447
539
  // Filter input should still be visible, but query cleared
448
540
  // All branches should be visible again
449
- expect(container.textContent).toContain('Filter:');
450
- expect(container.textContent).toContain('main');
451
- expect(container.textContent).toContain('feature/test');
541
+ expect(container.textContent).toContain("Filter:");
542
+ expect(container.textContent).toContain("main");
543
+ expect(container.textContent).toContain("feature/test");
452
544
  });
453
545
 
454
- it('should exit filter mode when Esc is pressed with empty query', () => {
546
+ it("should exit filter mode when Esc is pressed with empty query", () => {
455
547
  const onSelect = vi.fn();
456
548
  const { container } = render(
457
- <BranchListScreen branches={mockBranches} stats={mockStats} onSelect={onSelect} />
549
+ <BranchListScreen
550
+ branches={mockBranches}
551
+ stats={mockStats}
552
+ onSelect={onSelect}
553
+ />,
458
554
  );
459
555
 
460
556
  // Enter filter mode
461
- const fKeyEvent = new (globalThis.window as any).KeyboardEvent('keydown', { key: 'f' });
557
+ const fKeyEvent = new (globalThis.window as any).KeyboardEvent(
558
+ "keydown",
559
+ { key: "f" },
560
+ );
462
561
  document.dispatchEvent(fKeyEvent);
463
562
 
464
563
  // Press Escape with empty query (should exit filter mode)
465
- const escKeyEvent = new (globalThis.window as any).KeyboardEvent('keydown', { key: 'Escape' });
564
+ const escKeyEvent = new (globalThis.window as any).KeyboardEvent(
565
+ "keydown",
566
+ { key: "Escape" },
567
+ );
466
568
  document.dispatchEvent(escKeyEvent);
467
569
 
468
570
  // Should return to branch selection mode
469
- expect(container.textContent).toContain('(press f to filter)');
571
+ expect(container.textContent).toContain("(press f to filter)");
470
572
  });
471
573
 
472
- it('should perform case-insensitive search', () => {
574
+ it("should perform case-insensitive search", () => {
473
575
  const onSelect = vi.fn();
474
576
  const { container } = render(
475
577
  <BranchListScreen
@@ -478,14 +580,14 @@ describe('BranchListScreen', () => {
478
580
  onSelect={onSelect}
479
581
  testFilterMode={true}
480
582
  testFilterQuery="FEATURE"
481
- />
583
+ />,
482
584
  );
483
585
 
484
586
  // "feature/test" should still be visible
485
- expect(container.textContent).toContain('feature/test');
587
+ expect(container.textContent).toContain("feature/test");
486
588
  });
487
589
 
488
- it('should disable other key bindings (m, c, r) while typing in filter', () => {
590
+ it("should disable other key bindings (m, c, r) while typing in filter", () => {
489
591
  // Note: Input component uses blockKeys prop to prevent c/r/m/f from
490
592
  // triggering shortcuts while typing in the filter field
491
593
  // This test verifies the intended behavior (though KeyboardEvent
@@ -503,17 +605,23 @@ describe('BranchListScreen', () => {
503
605
  onNavigate={onNavigate}
504
606
  onCleanupCommand={onCleanupCommand}
505
607
  onRefresh={onRefresh}
506
- />
608
+ />,
507
609
  );
508
610
 
509
611
  // Enter filter mode
510
- const fKeyEvent = new (globalThis.window as any).KeyboardEvent('keydown', { key: 'f' });
612
+ const fKeyEvent = new (globalThis.window as any).KeyboardEvent(
613
+ "keydown",
614
+ { key: "f" },
615
+ );
511
616
  document.dispatchEvent(fKeyEvent);
512
617
 
513
618
  // When user types in filter, Input component blocks c/r/m/f keys
514
619
  // Press m, c, r keys (should be blocked by Input's blockKeys)
515
- ['m', 'c', 'r'].forEach((key) => {
516
- const keyEvent = new (globalThis.window as any).KeyboardEvent('keydown', { key });
620
+ ["m", "c", "r"].forEach((key) => {
621
+ const keyEvent = new (globalThis.window as any).KeyboardEvent(
622
+ "keydown",
623
+ { key },
624
+ );
517
625
  document.dispatchEvent(keyEvent);
518
626
  });
519
627
 
@@ -524,19 +632,19 @@ describe('BranchListScreen', () => {
524
632
  expect(onRefresh).not.toHaveBeenCalled();
525
633
  });
526
634
 
527
- it('should display match count when filtering', () => {
635
+ it("should display match count when filtering", () => {
528
636
  const onSelect = vi.fn();
529
637
  const branches: BranchItem[] = [
530
638
  ...mockBranches,
531
639
  {
532
- name: 'feature/another',
533
- type: 'local',
534
- branchType: 'feature',
640
+ name: "feature/another",
641
+ type: "local",
642
+ branchType: "feature",
535
643
  isCurrent: false,
536
- icons: [''],
644
+ icons: [""],
537
645
  hasChanges: false,
538
- label: '✨ feature/another',
539
- value: 'feature/another',
646
+ label: "✨ feature/another",
647
+ value: "feature/another",
540
648
  latestCommitTimestamp: 1_698_000_000,
541
649
  },
542
650
  ];
@@ -548,14 +656,14 @@ describe('BranchListScreen', () => {
548
656
  onSelect={onSelect}
549
657
  testFilterMode={true}
550
658
  testFilterQuery="feature"
551
- />
659
+ />,
552
660
  );
553
661
 
554
662
  // Should show "Showing 2 of 3 branches"
555
663
  expect(container.textContent).toMatch(/Showing\s+2\s+of\s+3/i);
556
664
  });
557
665
 
558
- it('should show empty list when no branches match', () => {
666
+ it("should show empty list when no branches match", () => {
559
667
  const onSelect = vi.fn();
560
668
  const { container } = render(
561
669
  <BranchListScreen
@@ -564,28 +672,28 @@ describe('BranchListScreen', () => {
564
672
  onSelect={onSelect}
565
673
  testFilterMode={true}
566
674
  testFilterQuery="nonexistent"
567
- />
675
+ />,
568
676
  );
569
677
 
570
678
  // Should show "Showing 0 of 2 branches"
571
679
  expect(container.textContent).toMatch(/Showing\s+0\s+of\s+2/i);
572
680
  });
573
681
 
574
- it('should search in PR titles when available', () => {
682
+ it("should search in PR titles when available", () => {
575
683
  const onSelect = vi.fn();
576
684
  const branchesWithPR: BranchItem[] = [
577
685
  ...mockBranches,
578
686
  {
579
- name: 'feature/add-filter',
580
- type: 'local',
581
- branchType: 'feature',
687
+ name: "feature/add-filter",
688
+ type: "local",
689
+ branchType: "feature",
582
690
  isCurrent: false,
583
- icons: ['', '🔀'],
691
+ icons: ["", "🔀"],
584
692
  hasChanges: false,
585
- label: '✨ 🔀 feature/add-filter',
586
- value: 'feature/add-filter',
693
+ label: "✨ 🔀 feature/add-filter",
694
+ value: "feature/add-filter",
587
695
  latestCommitTimestamp: 1_698_000_000,
588
- openPR: { number: 123, title: 'Add search filter to branch list' },
696
+ openPR: { number: 123, title: "Add search filter to branch list" },
589
697
  },
590
698
  ];
591
699
 
@@ -596,11 +704,11 @@ describe('BranchListScreen', () => {
596
704
  onSelect={onSelect}
597
705
  testFilterMode={true}
598
706
  testFilterQuery="search"
599
- />
707
+ />,
600
708
  );
601
709
 
602
710
  // Branch with matching PR title should be visible
603
- expect(container.textContent).toContain('feature/add-filter');
711
+ expect(container.textContent).toContain("feature/add-filter");
604
712
  });
605
713
  });
606
714
  });