@akiojin/gwt 2.1.1 → 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 +7 -2
  16. package/dist/cli/ui/components/common/Input.d.ts.map +1 -1
  17. package/dist/cli/ui/components/common/Input.js +12 -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 +8 -4
  36. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  37. package/dist/cli/ui/components/screens/BranchListScreen.js +122 -48
  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 +496 -75
  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 +26 -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 +187 -62
  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,18 +1,19 @@
1
- import React, { useCallback } from 'react';
2
- import { Box, Text, useInput } from 'ink';
3
- import { Header } from '../parts/Header.js';
4
- import { Stats } from '../parts/Stats.js';
5
- import { Footer } from '../parts/Footer.js';
6
- import { Select } from '../common/Select.js';
7
- import { LoadingIndicator } from '../common/LoadingIndicator.js';
8
- import { useTerminalSize } from '../../hooks/useTerminalSize.js';
9
- import type { BranchItem, Statistics } from '../../types.js';
10
- import stringWidth from 'string-width';
11
- import chalk from 'chalk';
1
+ import React, { useCallback, useState, useMemo } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { Header } from "../parts/Header.js";
4
+ import { Stats } from "../parts/Stats.js";
5
+ import { Footer } from "../parts/Footer.js";
6
+ import { Select } from "../common/Select.js";
7
+ import { Input } from "../common/Input.js";
8
+ import { LoadingIndicator } from "../common/LoadingIndicator.js";
9
+ import { useTerminalSize } from "../../hooks/useTerminalSize.js";
10
+ import type { BranchItem, Statistics } from "../../types.js";
11
+ import stringWidth from "string-width";
12
+ import chalk from "chalk";
12
13
 
13
14
  const WIDTH_OVERRIDES: Record<string, number> = {
14
- '': 1,
15
- '': 1,
15
+ "": 1,
16
+ "": 1,
16
17
  };
17
18
 
18
19
  const measureDisplayWidth = (value: string): number => {
@@ -28,7 +29,7 @@ const measureDisplayWidth = (value: string): number => {
28
29
  return width;
29
30
  };
30
31
 
31
- type IndicatorColor = 'cyan' | 'green' | 'yellow' | 'red';
32
+ type IndicatorColor = "cyan" | "green" | "yellow" | "red";
32
33
 
33
34
  interface CleanupIndicator {
34
35
  icon: string;
@@ -61,6 +62,11 @@ export interface BranchListScreenProps {
61
62
  cleanupUI?: CleanupUIState;
62
63
  version?: string | null;
63
64
  workingDirectory?: string;
65
+ // Test support: allow external control of filter mode and query
66
+ testFilterMode?: boolean;
67
+ testOnFilterModeChange?: (mode: boolean) => void;
68
+ testFilterQuery?: string;
69
+ testOnFilterQueryChange?: (query: string) => void;
64
70
  }
65
71
 
66
72
  /**
@@ -81,79 +87,156 @@ export function BranchListScreen({
81
87
  cleanupUI,
82
88
  version,
83
89
  workingDirectory,
90
+ testFilterMode,
91
+ testOnFilterModeChange,
92
+ testFilterQuery,
93
+ testOnFilterQueryChange,
84
94
  }: BranchListScreenProps) {
85
95
  const { rows } = useTerminalSize();
86
96
 
97
+ // Filter state - allow test control via props
98
+ const [internalFilterQuery, setInternalFilterQuery] = useState("");
99
+ const filterQuery =
100
+ testFilterQuery !== undefined ? testFilterQuery : internalFilterQuery;
101
+ const setFilterQuery = useCallback(
102
+ (query: string) => {
103
+ setInternalFilterQuery(query);
104
+ testOnFilterQueryChange?.(query);
105
+ },
106
+ [testOnFilterQueryChange],
107
+ );
108
+
109
+ // Focus management: true = filter mode, false = branch selection mode
110
+ // Allow test control via props
111
+ const [internalFilterMode, setInternalFilterMode] = useState(false);
112
+ const filterMode =
113
+ testFilterMode !== undefined ? testFilterMode : internalFilterMode;
114
+ const setFilterMode = useCallback(
115
+ (mode: boolean) => {
116
+ setInternalFilterMode(mode);
117
+ testOnFilterModeChange?.(mode);
118
+ },
119
+ [testOnFilterModeChange],
120
+ );
121
+
87
122
  // Handle keyboard input
88
- // Note: Select component handles Enter and arrow keys
89
- useInput((input) => {
123
+ // Note: Input component blocks specific keys (c/r/m/f) using blockKeys prop
124
+ // This prevents shortcuts from triggering while typing in the filter
125
+ useInput((input, key) => {
90
126
  if (cleanupUI?.inputLocked) {
91
127
  return;
92
128
  }
93
129
 
94
- if (input === 'm' && onNavigate) {
95
- onNavigate('worktree-manager');
96
- } else if (input === 'c') {
130
+ // Escape key handling
131
+ if (key.escape) {
132
+ if (filterQuery) {
133
+ // Clear filter query first
134
+ setFilterQuery("");
135
+ return;
136
+ }
137
+ if (filterMode) {
138
+ // Exit filter mode if query is empty
139
+ setFilterMode(false);
140
+ return;
141
+ }
142
+ }
143
+
144
+ // Enter filter mode with 'f' key (only in branch selection mode)
145
+ if (input === "f" && !filterMode) {
146
+ setFilterMode(true);
147
+ return;
148
+ }
149
+
150
+ // Global shortcuts (blocked by Input component when typing in filter mode)
151
+ if (input === "m" && onNavigate) {
152
+ onNavigate("worktree-manager");
153
+ } else if (input === "c") {
97
154
  onCleanupCommand?.();
98
- } else if (input === 'r' && onRefresh) {
155
+ } else if (input === "r" && onRefresh) {
99
156
  onRefresh();
100
157
  }
101
158
  });
102
159
 
160
+ // Filter branches based on query
161
+ const filteredBranches = useMemo(() => {
162
+ if (!filterQuery.trim()) {
163
+ return branches;
164
+ }
165
+
166
+ const query = filterQuery.toLowerCase();
167
+ return branches.filter((branch) => {
168
+ // Search in branch name
169
+ if (branch.name.toLowerCase().includes(query)) {
170
+ return true;
171
+ }
172
+
173
+ // Search in PR title if available (only openPR has title)
174
+ if (branch.openPR?.title?.toLowerCase().includes(query)) {
175
+ return true;
176
+ }
177
+
178
+ return false;
179
+ });
180
+ }, [branches, filterQuery]);
181
+
103
182
  // Calculate available space for branch list
104
183
  // Header: 2 lines (title + divider)
184
+ // Filter input: 1 line
105
185
  // Stats: 1 line
106
186
  // Empty line: 1 line
107
187
  // Footer: 1 line
108
- // Total fixed: 5 lines
188
+ // Total fixed: 6 lines
109
189
  const headerLines = 2;
190
+ const filterLines = 1;
110
191
  const statsLines = 1;
111
192
  const emptyLine = 1;
112
193
  const footerLines = 1;
113
- const fixedLines = headerLines + statsLines + emptyLine + footerLines;
194
+ const fixedLines =
195
+ headerLines + filterLines + statsLines + emptyLine + footerLines;
114
196
  const contentHeight = rows - fixedLines;
115
197
  const limit = Math.max(5, contentHeight); // Minimum 5 items visible
116
198
 
117
199
  // Footer actions
118
200
  const footerActions = [
119
- { key: 'enter', description: 'Select' },
120
- { key: 'r', description: 'Refresh' },
121
- { key: 'm', description: 'Manage worktrees' },
122
- { key: 'c', description: 'Cleanup branches' },
201
+ { key: "enter", description: "Select" },
202
+ { key: "f", description: "Filter" },
203
+ { key: "r", description: "Refresh" },
204
+ { key: "m", description: "Manage worktrees" },
205
+ { key: "c", description: "Cleanup branches" },
123
206
  ];
124
207
 
125
208
  const formatLatestCommit = useCallback((timestamp?: number) => {
126
209
  if (!timestamp || Number.isNaN(timestamp)) {
127
- return '---';
210
+ return "---";
128
211
  }
129
212
 
130
213
  const date = new Date(timestamp * 1000);
131
214
  const year = date.getFullYear();
132
- const month = String(date.getMonth() + 1).padStart(2, '0');
133
- const day = String(date.getDate()).padStart(2, '0');
134
- const hours = String(date.getHours()).padStart(2, '0');
135
- const minutes = String(date.getMinutes()).padStart(2, '0');
215
+ const month = String(date.getMonth() + 1).padStart(2, "0");
216
+ const day = String(date.getDate()).padStart(2, "0");
217
+ const hours = String(date.getHours()).padStart(2, "0");
218
+ const minutes = String(date.getMinutes()).padStart(2, "0");
136
219
 
137
220
  return `${year}-${month}-${day} ${hours}:${minutes}`;
138
221
  }, []);
139
222
 
140
223
  const truncateToWidth = useCallback((value: string, maxWidth: number) => {
141
224
  if (maxWidth <= 0) {
142
- return '';
225
+ return "";
143
226
  }
144
227
 
145
228
  if (stringWidth(value) <= maxWidth) {
146
229
  return value;
147
230
  }
148
231
 
149
- const ellipsis = '';
232
+ const ellipsis = "";
150
233
  const ellipsisWidth = stringWidth(ellipsis);
151
234
  if (ellipsisWidth >= maxWidth) {
152
235
  return ellipsis;
153
236
  }
154
237
 
155
238
  let currentWidth = 0;
156
- let result = '';
239
+ let result = "";
157
240
 
158
241
  for (const char of value) {
159
242
  const charWidth = stringWidth(char);
@@ -170,60 +253,67 @@ export function BranchListScreen({
170
253
  const renderBranchRow = useCallback(
171
254
  (item: BranchItem, isSelected: boolean, context: { columns: number }) => {
172
255
  const columns = Math.max(20, context.columns);
173
- const arrow = isSelected ? '>' : ' ';
256
+ const arrow = isSelected ? ">" : " ";
174
257
  const timestampText = formatLatestCommit(item.latestCommitTimestamp);
175
258
  const timestampWidth = stringWidth(timestampText);
176
259
 
177
260
  const indicatorInfo = cleanupUI?.indicators?.[item.name];
178
- let indicatorIcon = indicatorInfo?.icon ?? '';
261
+ let indicatorIcon = indicatorInfo?.icon ?? "";
179
262
  if (indicatorIcon && indicatorInfo?.color && !isSelected) {
180
263
  switch (indicatorInfo.color) {
181
- case 'cyan':
264
+ case "cyan":
182
265
  indicatorIcon = chalk.cyan(indicatorIcon);
183
266
  break;
184
- case 'green':
267
+ case "green":
185
268
  indicatorIcon = chalk.green(indicatorIcon);
186
269
  break;
187
- case 'yellow':
270
+ case "yellow":
188
271
  indicatorIcon = chalk.yellow(indicatorIcon);
189
272
  break;
190
- case 'red':
273
+ case "red":
191
274
  indicatorIcon = chalk.red(indicatorIcon);
192
275
  break;
193
276
  default:
194
277
  break;
195
278
  }
196
279
  }
197
- const indicatorPrefix = indicatorIcon ? `${indicatorIcon} ` : '';
280
+ const indicatorPrefix = indicatorIcon ? `${indicatorIcon} ` : "";
198
281
  const staticPrefix = `${arrow} ${indicatorPrefix}`;
199
282
  const staticPrefixWidth = stringWidth(staticPrefix);
200
283
 
201
- const availableLeftWidth = Math.max(staticPrefixWidth, columns - timestampWidth - 1);
284
+ const availableLeftWidth = Math.max(
285
+ staticPrefixWidth,
286
+ columns - timestampWidth - 1,
287
+ );
202
288
  const maxLabelWidth = Math.max(0, availableLeftWidth - staticPrefixWidth);
203
289
  const truncatedLabel = truncateToWidth(item.label, maxLabelWidth);
204
290
  const leftText = `${staticPrefix}${truncatedLabel}`;
205
291
 
206
292
  const leftMeasuredWidth = stringWidth(leftText);
207
293
  const leftDisplayWidth = measureDisplayWidth(leftText);
208
- const baseGapWidth = Math.max(1, columns - leftMeasuredWidth - timestampWidth);
209
- const displayGapWidth = Math.max(1, columns - leftDisplayWidth - timestampWidth);
294
+ const baseGapWidth = Math.max(
295
+ 1,
296
+ columns - leftMeasuredWidth - timestampWidth,
297
+ );
298
+ const displayGapWidth = Math.max(
299
+ 1,
300
+ columns - leftDisplayWidth - timestampWidth,
301
+ );
210
302
  const cursorShift = Math.max(0, displayGapWidth - baseGapWidth);
211
303
 
212
- const gap = ' '.repeat(baseGapWidth);
213
- const cursorAdjust = cursorShift > 0 ? `\u001b[${cursorShift}C` : '';
304
+ const gap = " ".repeat(baseGapWidth);
305
+ const cursorAdjust = cursorShift > 0 ? `\u001b[${cursorShift}C` : "";
214
306
 
215
307
  let line = `${leftText}${gap}${cursorAdjust}${timestampText}`;
216
308
  const paddingWidth = Math.max(0, columns - stringWidth(line));
217
309
  if (paddingWidth > 0) {
218
- line += ' '.repeat(paddingWidth);
310
+ line += " ".repeat(paddingWidth);
219
311
  }
220
312
 
221
- const output = isSelected
222
- ? `${line}`
223
- : line;
313
+ const output = isSelected ? `\u001b[46m\u001b[30m${line}\u001b[0m` : line;
224
314
  return <Text>{output}</Text>;
225
315
  },
226
- [cleanupUI, formatLatestCommit, truncateToWidth]
316
+ [cleanupUI, formatLatestCommit, truncateToWidth],
227
317
  );
228
318
 
229
319
  return (
@@ -236,8 +326,30 @@ export function BranchListScreen({
236
326
  {...(workingDirectory !== undefined && { workingDirectory })}
237
327
  />
238
328
 
329
+ {/* Filter Input - Always visible */}
330
+ <Box>
331
+ <Text dimColor>Filter: </Text>
332
+ {filterMode ? (
333
+ <Input
334
+ value={filterQuery}
335
+ onChange={setFilterQuery}
336
+ onSubmit={() => {}} // No-op: filter is applied in real-time
337
+ placeholder="Type to search..."
338
+ blockKeys={["c", "r", "m", "f"]} // Block shortcuts while typing
339
+ />
340
+ ) : (
341
+ <Text dimColor>{filterQuery || "(press f to filter)"}</Text>
342
+ )}
343
+ {filterQuery && (
344
+ <Text dimColor>
345
+ {" "}
346
+ (Showing {filteredBranches.length} of {branches.length})
347
+ </Text>
348
+ )}
349
+ </Box>
350
+
239
351
  {/* Stats */}
240
- <Box marginTop={1}>
352
+ <Box>
241
353
  <Stats stats={stats} lastUpdated={lastUpdated} />
242
354
  </Box>
243
355
 
@@ -268,16 +380,29 @@ export function BranchListScreen({
268
380
  </Box>
269
381
  )}
270
382
 
271
- {!loading && !error && branches.length > 0 && (
272
- <Select
273
- items={branches}
274
- onSelect={onSelect}
275
- limit={limit}
276
- disabled={Boolean(cleanupUI?.inputLocked)}
277
- renderIndicator={() => null}
278
- renderItem={renderBranchRow}
279
- />
280
- )}
383
+ {!loading &&
384
+ !error &&
385
+ branches.length > 0 &&
386
+ filteredBranches.length === 0 &&
387
+ filterQuery && (
388
+ <Box>
389
+ <Text dimColor>No branches match your filter</Text>
390
+ </Box>
391
+ )}
392
+
393
+ {!loading &&
394
+ !error &&
395
+ branches.length > 0 &&
396
+ filteredBranches.length > 0 && (
397
+ <Select
398
+ items={filteredBranches}
399
+ onSelect={onSelect}
400
+ limit={limit}
401
+ disabled={Boolean(cleanupUI?.inputLocked)}
402
+ renderIndicator={() => null}
403
+ renderItem={renderBranchRow}
404
+ />
405
+ )}
281
406
  </Box>
282
407
 
283
408
  {cleanupUI?.footerMessage && (
@@ -1,11 +1,11 @@
1
- import React, { useState } from 'react';
2
- import { Box, Text, useInput } from 'ink';
3
- import { Header } from '../parts/Header.js';
4
- import { Footer } from '../parts/Footer.js';
5
- import { Select } from '../common/Select.js';
6
- import { useTerminalSize } from '../../hooks/useTerminalSize.js';
1
+ import React, { useState } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { Header } from "../parts/Header.js";
4
+ import { Footer } from "../parts/Footer.js";
5
+ import { Select } from "../common/Select.js";
6
+ import { useTerminalSize } from "../../hooks/useTerminalSize.js";
7
7
 
8
- export type ExecutionMode = 'normal' | 'continue' | 'resume';
8
+ export type ExecutionMode = "normal" | "continue" | "resume";
9
9
 
10
10
  export interface ExecutionModeItem {
11
11
  label: string;
@@ -62,33 +62,34 @@ export function ExecutionModeSelectorScreen({
62
62
  // Execution mode options (Step 1)
63
63
  const modeItems: ExecutionModeItem[] = [
64
64
  {
65
- label: 'Normal',
66
- value: 'normal',
67
- description: 'Start fresh session',
65
+ label: "Normal",
66
+ value: "normal",
67
+ description: "Start fresh session",
68
68
  },
69
69
  {
70
- label: 'Continue',
71
- value: 'continue',
72
- description: 'Continue from last session',
70
+ label: "Continue",
71
+ value: "continue",
72
+ description: "Continue from last session",
73
73
  },
74
74
  {
75
- label: 'Resume',
76
- value: 'resume',
77
- description: 'Resume specific session',
75
+ label: "Resume",
76
+ value: "resume",
77
+ description: "Resume specific session",
78
78
  },
79
79
  ];
80
80
 
81
81
  // Skip permissions options (Step 2)
82
82
  const skipPermissionsItems: SkipPermissionsItem[] = [
83
83
  {
84
- label: 'No',
85
- value: 'no',
86
- description: 'Normal permission checks',
84
+ label: "No",
85
+ value: "no",
86
+ description: "Normal permission checks",
87
87
  },
88
88
  {
89
- label: 'Yes',
90
- value: 'yes',
91
- description: 'Skip permission checks (--dangerously-skip-permissions / --yolo)',
89
+ label: "Yes",
90
+ value: "yes",
91
+ description:
92
+ "Skip permission checks (--dangerously-skip-permissions / --yolo)",
92
93
  },
93
94
  ];
94
95
 
@@ -103,22 +104,22 @@ export function ExecutionModeSelectorScreen({
103
104
  if (selectedMode) {
104
105
  onSelect({
105
106
  mode: selectedMode,
106
- skipPermissions: item.value === 'yes',
107
+ skipPermissions: item.value === "yes",
107
108
  });
108
109
  }
109
110
  };
110
111
 
111
112
  // Footer actions
112
113
  const footerActions = [
113
- { key: 'enter', description: 'Select' },
114
- { key: 'esc', description: step === 2 ? 'Back to mode selection' : 'Back' },
114
+ { key: "enter", description: "Select" },
115
+ { key: "esc", description: step === 2 ? "Back to mode selection" : "Back" },
115
116
  ];
116
117
 
117
118
  return (
118
119
  <Box flexDirection="column" height={rows}>
119
120
  {/* Header */}
120
121
  <Header
121
- title={step === 1 ? 'Execution Mode' : 'Skip Permissions'}
122
+ title={step === 1 ? "Execution Mode" : "Skip Permissions"}
122
123
  titleColor="magenta"
123
124
  version={version}
124
125
  />
@@ -135,9 +136,15 @@ export function ExecutionModeSelectorScreen({
135
136
  ) : (
136
137
  <>
137
138
  <Box marginBottom={1}>
138
- <Text>Skip permission checks? (--dangerously-skip-permissions / --yolo)</Text>
139
+ <Text>
140
+ Skip permission checks? (--dangerously-skip-permissions /
141
+ --yolo)
142
+ </Text>
139
143
  </Box>
140
- <Select items={skipPermissionsItems} onSelect={handleSkipPermissionsSelect} />
144
+ <Select
145
+ items={skipPermissionsItems}
146
+ onSelect={handleSkipPermissionsSelect}
147
+ />
141
148
  </>
142
149
  )}
143
150
  </Box>
@@ -1,10 +1,10 @@
1
- import React from 'react';
2
- import { Box, Text, useInput } from 'ink';
3
- import { Header } from '../parts/Header.js';
4
- import { Footer } from '../parts/Footer.js';
5
- import { Select } from '../common/Select.js';
6
- import { useTerminalSize } from '../../hooks/useTerminalSize.js';
7
- import type { CleanupTarget } from '../../types.js';
1
+ import React from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { Header } from "../parts/Header.js";
4
+ import { Footer } from "../parts/Footer.js";
5
+ import { Select } from "../common/Select.js";
6
+ import { useTerminalSize } from "../../hooks/useTerminalSize.js";
7
+ import type { CleanupTarget } from "../../types.js";
8
8
 
9
9
  export interface PRItem {
10
10
  label: string;
@@ -44,7 +44,7 @@ export function PRCleanupScreen({
44
44
  useInput((input, key) => {
45
45
  if (key.escape) {
46
46
  onBack();
47
- } else if (input === 'r') {
47
+ } else if (input === "r") {
48
48
  onRefresh();
49
49
  }
50
50
  });
@@ -53,28 +53,28 @@ export function PRCleanupScreen({
53
53
  const prItems: PRItem[] = targets.map((target) => {
54
54
  const pr = target.pullRequest;
55
55
  const flags: string[] = [];
56
- if (target.cleanupType === 'worktree-and-branch') {
57
- flags.push('worktree');
56
+ if (target.cleanupType === "worktree-and-branch") {
57
+ flags.push("worktree");
58
58
  } else {
59
- flags.push('branch');
59
+ flags.push("branch");
60
60
  }
61
- if (target.reasons?.includes('merged-pr')) {
62
- flags.push('merged');
61
+ if (target.reasons?.includes("merged-pr")) {
62
+ flags.push("merged");
63
63
  }
64
- if (target.reasons?.includes('no-diff-with-base')) {
65
- flags.push('base');
64
+ if (target.reasons?.includes("no-diff-with-base")) {
65
+ flags.push("base");
66
66
  }
67
67
  if (target.hasUncommittedChanges) {
68
- flags.push('changes');
68
+ flags.push("changes");
69
69
  }
70
70
  if (target.hasUnpushedCommits) {
71
- flags.push('unpushed');
71
+ flags.push("unpushed");
72
72
  }
73
73
  if (target.isAccessible === false) {
74
- flags.push('inaccessible');
74
+ flags.push("inaccessible");
75
75
  }
76
76
 
77
- const flagText = flags.length > 0 ? ` [${flags.join(', ')}]` : '';
77
+ const flagText = flags.length > 0 ? ` [${flags.join(", ")}]` : "";
78
78
 
79
79
  const label = pr
80
80
  ? `${target.branch} - #${pr.number} ${pr.title}${flagText}`
@@ -98,9 +98,9 @@ export function PRCleanupScreen({
98
98
 
99
99
  // Footer actions
100
100
  const footerActions = [
101
- { key: 'enter', description: 'Cleanup' },
102
- { key: 'r', description: 'Refresh' },
103
- { key: 'esc', description: 'Back' },
101
+ { key: "enter", description: "Cleanup" },
102
+ { key: "r", description: "Refresh" },
103
+ { key: "esc", description: "Back" },
104
104
  ];
105
105
 
106
106
  return (
@@ -1,9 +1,9 @@
1
- import React from 'react';
2
- import { Box, Text, useInput } from 'ink';
3
- import { Header } from '../parts/Header.js';
4
- import { Footer } from '../parts/Footer.js';
5
- import { Select } from '../common/Select.js';
6
- import { useTerminalSize } from '../../hooks/useTerminalSize.js';
1
+ import React from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { Header } from "../parts/Header.js";
4
+ import { Footer } from "../parts/Footer.js";
5
+ import { Select } from "../common/Select.js";
6
+ import { useTerminalSize } from "../../hooks/useTerminalSize.js";
7
7
 
8
8
  export interface SessionItem {
9
9
  label: string;
@@ -59,8 +59,8 @@ export function SessionSelectorScreen({
59
59
 
60
60
  // Footer actions
61
61
  const footerActions = [
62
- { key: 'enter', description: 'Select' },
63
- { key: 'esc', description: 'Back' },
62
+ { key: "enter", description: "Select" },
63
+ { key: "esc", description: "Back" },
64
64
  ];
65
65
 
66
66
  return (
@@ -1,9 +1,9 @@
1
- import React from 'react';
2
- import { Box, Text, useInput } from 'ink';
3
- import { Header } from '../parts/Header.js';
4
- import { Footer } from '../parts/Footer.js';
5
- import { Select } from '../common/Select.js';
6
- import { useTerminalSize } from '../../hooks/useTerminalSize.js';
1
+ import React from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { Header } from "../parts/Header.js";
4
+ import { Footer } from "../parts/Footer.js";
5
+ import { Select } from "../common/Select.js";
6
+ import { useTerminalSize } from "../../hooks/useTerminalSize.js";
7
7
 
8
8
  export interface WorktreeItem {
9
9
  branch: string;
@@ -64,8 +64,8 @@ export function WorktreeManagerScreen({
64
64
 
65
65
  // Footer actions
66
66
  const footerActions = [
67
- { key: 'enter', description: 'Select' },
68
- { key: 'esc', description: 'Back' },
67
+ { key: "enter", description: "Select" },
68
+ { key: "esc", description: "Back" },
69
69
  ];
70
70
 
71
71
  return (
@@ -46,7 +46,9 @@ export function BranchActionSelectorScreen({
46
46
  (mode === "protected" ? "Switch to root branch" : "Use existing branch");
47
47
  const secondaryActionLabel =
48
48
  secondaryLabel ??
49
- (mode === "protected" ? "Create new branch from this branch" : "Create new branch");
49
+ (mode === "protected"
50
+ ? "Create new branch from this branch"
51
+ : "Create new branch");
50
52
 
51
53
  const items: SelectItem[] = [
52
54
  {
@@ -74,15 +76,18 @@ export function BranchActionSelectorScreen({
74
76
 
75
77
  // Footer actions
76
78
  const footerActions = [
77
- { key: 'enter', description: 'Select' },
78
- { key: 'esc', description: 'Back' },
79
+ { key: "enter", description: "Select" },
80
+ { key: "esc", description: "Back" },
79
81
  ];
80
82
 
81
83
  return (
82
84
  <Box flexDirection="column">
83
85
  <Box marginBottom={1}>
84
86
  <Text>
85
- Selected branch: <Text bold color="cyan">{selectedBranch}</Text>
87
+ Selected branch:{" "}
88
+ <Text bold color="cyan">
89
+ {selectedBranch}
90
+ </Text>
86
91
  </Text>
87
92
  </Box>
88
93
  {infoMessage ? (