@akiojin/gwt 2.12.1 → 2.13.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.
- package/dist/cli/ui/components/App.d.ts.map +1 -1
- package/dist/cli/ui/components/App.js +72 -3
- package/dist/cli/ui/components/App.js.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts +3 -1
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.js +154 -32
- package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
- package/dist/cli/ui/hooks/useGitData.d.ts.map +1 -1
- package/dist/cli/ui/hooks/useGitData.js +17 -0
- package/dist/cli/ui/hooks/useGitData.js.map +1 -1
- package/dist/cli/ui/types.d.ts +2 -0
- package/dist/cli/ui/types.d.ts.map +1 -1
- package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
- package/dist/cli/ui/utils/branchFormatter.js +7 -2
- package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
- package/dist/cli/ui/utils/modelOptions.d.ts.map +1 -1
- package/dist/cli/ui/utils/modelOptions.js +7 -0
- package/dist/cli/ui/utils/modelOptions.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/logging/logger.d.ts +24 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +57 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/logging/rotation.d.ts +6 -0
- package/dist/logging/rotation.d.ts.map +1 -0
- package/dist/logging/rotation.js +26 -0
- package/dist/logging/rotation.js.map +1 -0
- package/dist/web/server/index.d.ts.map +1 -1
- package/dist/web/server/index.js +3 -3
- package/dist/web/server/index.js.map +1 -1
- package/dist/web/server/routes/branches.d.ts +2 -2
- package/dist/web/server/routes/branches.d.ts.map +1 -1
- package/dist/web/server/routes/branches.js.map +1 -1
- package/dist/web/server/routes/config.d.ts +2 -2
- package/dist/web/server/routes/config.d.ts.map +1 -1
- package/dist/web/server/routes/config.js.map +1 -1
- package/dist/web/server/routes/index.d.ts +2 -2
- package/dist/web/server/routes/index.d.ts.map +1 -1
- package/dist/web/server/routes/index.js.map +1 -1
- package/dist/web/server/routes/sessions.d.ts +2 -2
- package/dist/web/server/routes/sessions.d.ts.map +1 -1
- package/dist/web/server/routes/sessions.js.map +1 -1
- package/dist/web/server/routes/worktrees.d.ts +2 -2
- package/dist/web/server/routes/worktrees.d.ts.map +1 -1
- package/dist/web/server/routes/worktrees.js.map +1 -1
- package/dist/web/server/types.d.ts +4 -0
- package/dist/web/server/types.d.ts.map +1 -0
- package/dist/web/server/types.js +2 -0
- package/dist/web/server/types.js.map +1 -0
- package/dist/worktree.d.ts +1 -0
- package/dist/worktree.d.ts.map +1 -1
- package/dist/worktree.js.map +1 -1
- package/package.json +3 -2
- package/src/cli/ui/__tests__/components/ModelSelectorScreen.initial.test.tsx +13 -13
- package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +81 -33
- package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +7 -3
- package/src/cli/ui/components/App.tsx +88 -2
- package/src/cli/ui/components/screens/BranchListScreen.tsx +198 -32
- package/src/cli/ui/hooks/useGitData.ts +20 -0
- package/src/cli/ui/types.ts +3 -0
- package/src/cli/ui/utils/branchFormatter.ts +7 -2
- package/src/cli/ui/utils/modelOptions.test.ts +14 -0
- package/src/cli/ui/utils/modelOptions.ts +7 -0
- package/src/index.ts +7 -0
- package/src/logging/logger.ts +79 -0
- package/src/logging/rotation.ts +25 -0
- package/src/web/server/index.ts +6 -4
- package/src/web/server/routes/branches.ts +2 -2
- package/src/web/server/routes/config.ts +2 -2
- package/src/web/server/routes/index.ts +2 -2
- package/src/web/server/routes/sessions.ts +2 -2
- package/src/web/server/routes/worktrees.ts +2 -2
- package/src/web/server/types.ts +14 -0
- package/src/worktree.ts +1 -0
|
@@ -172,6 +172,8 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
|
172
172
|
color?: "cyan" | "green" | "yellow" | "red";
|
|
173
173
|
} | null>(null);
|
|
174
174
|
const [hiddenBranches, setHiddenBranches] = useState<string[]>([]);
|
|
175
|
+
const [selectedBranches, setSelectedBranches] = useState<string[]>([]);
|
|
176
|
+
const [safeBranches, setSafeBranches] = useState<Set<string>>(new Set());
|
|
175
177
|
const spinnerFrameIndexRef = useRef(0);
|
|
176
178
|
const [spinnerFrameIndex, setSpinnerFrameIndex] = useState(0);
|
|
177
179
|
const completionTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
@@ -252,6 +254,42 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
|
252
254
|
}
|
|
253
255
|
}, [branches, hiddenBranches]);
|
|
254
256
|
|
|
257
|
+
// Remove selections that no longer exist (hidden or disappeared)
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
setSelectedBranches((prev) =>
|
|
260
|
+
prev.filter(
|
|
261
|
+
(name) =>
|
|
262
|
+
branches.some((b) => b.name === name) &&
|
|
263
|
+
!hiddenBranches.includes(name),
|
|
264
|
+
),
|
|
265
|
+
);
|
|
266
|
+
}, [branches, hiddenBranches]);
|
|
267
|
+
|
|
268
|
+
// Precompute safe-to-clean branches using cleanup candidate logic
|
|
269
|
+
useEffect(() => {
|
|
270
|
+
let cancelled = false;
|
|
271
|
+
(async () => {
|
|
272
|
+
try {
|
|
273
|
+
const targets = await getMergedPRWorktrees();
|
|
274
|
+
if (cancelled) return;
|
|
275
|
+
const safe = new Set(
|
|
276
|
+
targets
|
|
277
|
+
.filter(
|
|
278
|
+
(t) => !t.hasUncommittedChanges && !t.hasUnpushedCommits,
|
|
279
|
+
)
|
|
280
|
+
.map((t) => t.branch),
|
|
281
|
+
);
|
|
282
|
+
setSafeBranches(safe);
|
|
283
|
+
} catch {
|
|
284
|
+
if (cancelled) return;
|
|
285
|
+
setSafeBranches(new Set());
|
|
286
|
+
}
|
|
287
|
+
})();
|
|
288
|
+
return () => {
|
|
289
|
+
cancelled = true;
|
|
290
|
+
};
|
|
291
|
+
}, [branches, worktrees]);
|
|
292
|
+
|
|
255
293
|
// Load available sessions when entering session selector
|
|
256
294
|
useEffect(() => {
|
|
257
295
|
if (currentScreen !== "session-selector") {
|
|
@@ -530,10 +568,22 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
|
530
568
|
locked: false,
|
|
531
569
|
prunable: wt.isAccessible === false,
|
|
532
570
|
isAccessible: wt.isAccessible ?? true,
|
|
571
|
+
...(wt.hasUncommittedChanges !== undefined
|
|
572
|
+
? { hasUncommittedChanges: wt.hasUncommittedChanges }
|
|
573
|
+
: {}),
|
|
533
574
|
});
|
|
534
575
|
}
|
|
535
|
-
|
|
536
|
-
|
|
576
|
+
const baseItems = formatBranchItems(visibleBranches, worktreeMap);
|
|
577
|
+
return baseItems.map((item) => ({
|
|
578
|
+
...item,
|
|
579
|
+
safeToCleanup: safeBranches.has(item.name),
|
|
580
|
+
}));
|
|
581
|
+
}, [branchHash, worktreeHash, visibleBranches, worktrees, safeBranches]);
|
|
582
|
+
|
|
583
|
+
const selectedBranchSet = useMemo(
|
|
584
|
+
() => new Set(selectedBranches),
|
|
585
|
+
[selectedBranches],
|
|
586
|
+
);
|
|
537
587
|
|
|
538
588
|
// Calculate statistics (memoized for performance)
|
|
539
589
|
const stats = useMemo(
|
|
@@ -626,6 +676,18 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
|
626
676
|
[isProtectedBranchName],
|
|
627
677
|
);
|
|
628
678
|
|
|
679
|
+
const toggleBranchSelection = useCallback((branchName: string) => {
|
|
680
|
+
setSelectedBranches((prev) => {
|
|
681
|
+
const set = new Set(prev);
|
|
682
|
+
if (set.has(branchName)) {
|
|
683
|
+
set.delete(branchName);
|
|
684
|
+
} else {
|
|
685
|
+
set.add(branchName);
|
|
686
|
+
}
|
|
687
|
+
return Array.from(set);
|
|
688
|
+
});
|
|
689
|
+
}, []);
|
|
690
|
+
|
|
629
691
|
const protectedBranchInfo = useMemo(() => {
|
|
630
692
|
if (!selectedBranch) {
|
|
631
693
|
return null;
|
|
@@ -850,6 +912,9 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
|
850
912
|
succeededBranches.forEach((branch) => merged.add(branch));
|
|
851
913
|
return Array.from(merged);
|
|
852
914
|
});
|
|
915
|
+
setSelectedBranches((prev) =>
|
|
916
|
+
prev.filter((name) => !succeededBranches.includes(name)),
|
|
917
|
+
);
|
|
853
918
|
}
|
|
854
919
|
refresh();
|
|
855
920
|
completionTimerRef.current = null;
|
|
@@ -896,6 +961,24 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
|
896
961
|
return;
|
|
897
962
|
}
|
|
898
963
|
|
|
964
|
+
// Manual selection: restrict targets when選択がある
|
|
965
|
+
if (selectedBranchSet.size > 0) {
|
|
966
|
+
targets = targets.filter((t) => selectedBranchSet.has(t.branch));
|
|
967
|
+
if (targets.length === 0) {
|
|
968
|
+
setCleanupIndicators({});
|
|
969
|
+
setCleanupFooterMessage({
|
|
970
|
+
text: "⚠️ No cleanup candidates among selected branches.",
|
|
971
|
+
color: "yellow",
|
|
972
|
+
});
|
|
973
|
+
setCleanupInputLocked(false);
|
|
974
|
+
completionTimerRef.current = setTimeout(() => {
|
|
975
|
+
setCleanupFooterMessage(null);
|
|
976
|
+
completionTimerRef.current = null;
|
|
977
|
+
}, COMPLETION_HOLD_DURATION_MS);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
899
982
|
// Reset hidden branches that may already be gone
|
|
900
983
|
setHiddenBranches((prev) =>
|
|
901
984
|
prev.filter(
|
|
@@ -1012,6 +1095,7 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
|
1012
1095
|
getMergedPRWorktrees,
|
|
1013
1096
|
refresh,
|
|
1014
1097
|
removeWorktree,
|
|
1098
|
+
selectedBranchSet,
|
|
1015
1099
|
]);
|
|
1016
1100
|
|
|
1017
1101
|
// Handle AI tool selection
|
|
@@ -1177,6 +1261,8 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
|
1177
1261
|
}}
|
|
1178
1262
|
version={version}
|
|
1179
1263
|
workingDirectory={workingDirectory}
|
|
1264
|
+
selectedBranches={selectedBranches}
|
|
1265
|
+
onToggleSelect={toggleBranchSelection}
|
|
1180
1266
|
/>
|
|
1181
1267
|
);
|
|
1182
1268
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useState, useMemo } from "react";
|
|
1
|
+
import React, { useCallback, useState, useMemo, useEffect } from "react";
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
3
|
import { Header } from "../parts/Header.js";
|
|
4
4
|
import { Stats } from "../parts/Stats.js";
|
|
@@ -9,8 +9,11 @@ import { LoadingIndicator } from "../common/LoadingIndicator.js";
|
|
|
9
9
|
import { useTerminalSize } from "../../hooks/useTerminalSize.js";
|
|
10
10
|
import type { BranchItem, Statistics } from "../../types.js";
|
|
11
11
|
import stringWidth from "string-width";
|
|
12
|
+
import stripAnsi from "strip-ansi";
|
|
12
13
|
import chalk from "chalk";
|
|
13
14
|
|
|
15
|
+
// Emoji 幅は端末によって 1 または 2 になることがあるため、最小幅を上書きして
|
|
16
|
+
// 実測より小さくならないようにする(過小評価=折り返しの原因を防ぐ)
|
|
14
17
|
const WIDTH_OVERRIDES: Record<string, number> = {
|
|
15
18
|
// Remote icon
|
|
16
19
|
"☁": 1,
|
|
@@ -22,19 +25,24 @@ const WIDTH_OVERRIDES: Record<string, number> = {
|
|
|
22
25
|
"🚀": 1,
|
|
23
26
|
"📌": 1,
|
|
24
27
|
// Worktree status icons
|
|
25
|
-
"🟢":
|
|
28
|
+
"🟢": 2,
|
|
29
|
+
"⚪": 2,
|
|
26
30
|
"🟠": 1,
|
|
27
31
|
// Change status icons
|
|
28
32
|
"👉": 1,
|
|
29
33
|
"💾": 1,
|
|
30
34
|
"📤": 1,
|
|
31
35
|
"🔃": 1,
|
|
32
|
-
"✅":
|
|
33
|
-
"
|
|
36
|
+
"✅": 2,
|
|
37
|
+
"⚠": 2,
|
|
38
|
+
"⚠️": 2,
|
|
39
|
+
"🛡": 2,
|
|
34
40
|
// Remote markers
|
|
35
|
-
"🔗":
|
|
36
|
-
"💻":
|
|
37
|
-
"☁️":
|
|
41
|
+
"🔗": 2,
|
|
42
|
+
"💻": 2,
|
|
43
|
+
"☁️": 2,
|
|
44
|
+
"☑": 2,
|
|
45
|
+
"☐": 2,
|
|
38
46
|
};
|
|
39
47
|
|
|
40
48
|
const getCharWidth = (char: string): number => {
|
|
@@ -88,6 +96,8 @@ export interface BranchListScreenProps {
|
|
|
88
96
|
testOnFilterModeChange?: (mode: boolean) => void;
|
|
89
97
|
testFilterQuery?: string;
|
|
90
98
|
testOnFilterQueryChange?: (query: string) => void;
|
|
99
|
+
selectedBranches?: string[];
|
|
100
|
+
onToggleSelect?: (branchName: string) => void;
|
|
91
101
|
}
|
|
92
102
|
|
|
93
103
|
/**
|
|
@@ -111,11 +121,13 @@ export function BranchListScreen({
|
|
|
111
121
|
testOnFilterModeChange,
|
|
112
122
|
testFilterQuery,
|
|
113
123
|
testOnFilterQueryChange,
|
|
124
|
+
selectedBranches = [],
|
|
125
|
+
onToggleSelect,
|
|
114
126
|
}: BranchListScreenProps) {
|
|
115
127
|
const { rows } = useTerminalSize();
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
const
|
|
128
|
+
const headerText =
|
|
129
|
+
" Legend: [ ]/[ * ] select 🟢/⚪ worktree 🛡/⚠ safe";
|
|
130
|
+
const selectedSet = useMemo(() => new Set(selectedBranches), [selectedBranches]);
|
|
119
131
|
|
|
120
132
|
// Filter state - allow test control via props
|
|
121
133
|
const [internalFilterQuery, setInternalFilterQuery] = useState("");
|
|
@@ -142,6 +154,9 @@ export function BranchListScreen({
|
|
|
142
154
|
[testOnFilterModeChange],
|
|
143
155
|
);
|
|
144
156
|
|
|
157
|
+
// Cursor position for Select (controlled to enable space toggle)
|
|
158
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
159
|
+
|
|
145
160
|
// Handle keyboard input
|
|
146
161
|
// Note: Input component blocks specific keys (c/r/f) using blockKeys prop
|
|
147
162
|
// This prevents shortcuts from triggering while typing in the filter
|
|
@@ -170,6 +185,15 @@ export function BranchListScreen({
|
|
|
170
185
|
return;
|
|
171
186
|
}
|
|
172
187
|
|
|
188
|
+
// Toggle selection with space (only in branch selection mode)
|
|
189
|
+
if (input === " " && !filterMode) {
|
|
190
|
+
const target = filteredBranches[selectedIndex];
|
|
191
|
+
if (target) {
|
|
192
|
+
onToggleSelect?.(target.name);
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
173
197
|
// Disable global shortcuts while in filter mode
|
|
174
198
|
if (filterMode) {
|
|
175
199
|
return;
|
|
@@ -205,6 +229,15 @@ export function BranchListScreen({
|
|
|
205
229
|
});
|
|
206
230
|
}, [branches, filterQuery]);
|
|
207
231
|
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
setSelectedIndex((prev) => {
|
|
234
|
+
if (filteredBranches.length === 0) {
|
|
235
|
+
return 0;
|
|
236
|
+
}
|
|
237
|
+
return Math.min(prev, filteredBranches.length - 1);
|
|
238
|
+
});
|
|
239
|
+
}, [filteredBranches.length]);
|
|
240
|
+
|
|
208
241
|
// Calculate available space for branch list
|
|
209
242
|
// Header: 2 lines (title + divider)
|
|
210
243
|
// Filter input: 1 line
|
|
@@ -275,18 +308,67 @@ export function BranchListScreen({
|
|
|
275
308
|
return result + ellipsis;
|
|
276
309
|
}, []);
|
|
277
310
|
|
|
311
|
+
const colorToolLabel = useCallback((label: string, toolId?: string | null) => {
|
|
312
|
+
switch (toolId) {
|
|
313
|
+
case "claude-code":
|
|
314
|
+
return chalk.hex("#ffaf00")(label); // orange-ish
|
|
315
|
+
case "codex-cli":
|
|
316
|
+
return chalk.cyan(label);
|
|
317
|
+
case "gemini-cli":
|
|
318
|
+
return chalk.magenta(label);
|
|
319
|
+
case "qwen-cli":
|
|
320
|
+
return chalk.green(label);
|
|
321
|
+
default: {
|
|
322
|
+
const trimmed = label.trim().toLowerCase();
|
|
323
|
+
if (!toolId || trimmed === "unknown") {
|
|
324
|
+
return chalk.gray(label);
|
|
325
|
+
}
|
|
326
|
+
return chalk.white(label);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}, []);
|
|
330
|
+
|
|
278
331
|
const renderBranchRow = useCallback(
|
|
279
332
|
(item: BranchItem, isSelected: boolean, context: { columns: number }) => {
|
|
280
|
-
//
|
|
333
|
+
// 端末幅ピッタリでの自動折返しを避けるため、1桁だけ余白を取る
|
|
281
334
|
const columns = Math.max(20, context.columns - 1);
|
|
335
|
+
const visibleWidth = (value: string) =>
|
|
336
|
+
measureDisplayWidth(stripAnsi(value));
|
|
282
337
|
const arrow = isSelected ? ">" : " ";
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
338
|
+
let commitText = "---";
|
|
339
|
+
if (item.latestCommitTimestamp) {
|
|
340
|
+
commitText = formatLatestCommit(item.latestCommitTimestamp);
|
|
341
|
+
} else if (item.lastToolUsage?.timestamp) {
|
|
342
|
+
const seconds = Math.floor(item.lastToolUsage.timestamp / 1000);
|
|
343
|
+
commitText = formatLatestCommit(seconds);
|
|
344
|
+
}
|
|
345
|
+
const toolLabelRaw =
|
|
346
|
+
item.lastToolUsageLabel?.split("|")?.[0]?.trim() ??
|
|
347
|
+
item.lastToolUsage?.toolId ??
|
|
348
|
+
"Unknown";
|
|
349
|
+
|
|
350
|
+
const formatFixedWidth = (value: string, targetWidth: number) => {
|
|
351
|
+
let v = value;
|
|
352
|
+
if (measureDisplayWidth(v) > targetWidth) {
|
|
353
|
+
v = truncateToWidth(v, targetWidth);
|
|
354
|
+
}
|
|
355
|
+
const padding = Math.max(0, targetWidth - measureDisplayWidth(v));
|
|
356
|
+
return v + " ".repeat(padding);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const TOOL_WIDTH = 7;
|
|
360
|
+
const DATE_WIDTH = 16; // "YYYY-MM-DD HH:mm"
|
|
361
|
+
const paddedTool = formatFixedWidth(toolLabelRaw, TOOL_WIDTH);
|
|
362
|
+
const paddedDate =
|
|
363
|
+
commitText === "---"
|
|
364
|
+
? " ".repeat(DATE_WIDTH)
|
|
365
|
+
: commitText.padStart(DATE_WIDTH, " ");
|
|
366
|
+
const timestampText = `${paddedTool} | ${paddedDate}`;
|
|
367
|
+
const displayTimestampText = `${colorToolLabel(
|
|
368
|
+
paddedTool,
|
|
369
|
+
item.lastToolUsage?.toolId,
|
|
370
|
+
)} | ${paddedDate}`;
|
|
371
|
+
const timestampWidth = measureDisplayWidth(timestampText);
|
|
290
372
|
|
|
291
373
|
const indicatorInfo = cleanupUI?.indicators?.[item.name];
|
|
292
374
|
let indicatorIcon = indicatorInfo?.icon ?? "";
|
|
@@ -309,30 +391,112 @@ export function BranchListScreen({
|
|
|
309
391
|
}
|
|
310
392
|
}
|
|
311
393
|
const indicatorPrefix = indicatorIcon ? `${indicatorIcon} ` : "";
|
|
312
|
-
|
|
313
|
-
const
|
|
394
|
+
|
|
395
|
+
const isChecked = selectedSet.has(item.name);
|
|
396
|
+
const selectionIcon = isChecked ? "[*]" : "[ ]";
|
|
397
|
+
const hasWorktree =
|
|
398
|
+
item.worktreeStatus === "active" ||
|
|
399
|
+
item.worktreeStatus === "inaccessible";
|
|
400
|
+
const worktreeIcon = hasWorktree ? chalk.green("🟢") : chalk.gray("⚪");
|
|
401
|
+
const safeIcon =
|
|
402
|
+
item.safeToCleanup === true ? chalk.green("🛡") : chalk.yellow("⚠");
|
|
403
|
+
const stateCluster = `${selectionIcon} ${worktreeIcon} ${safeIcon}`;
|
|
404
|
+
|
|
405
|
+
const staticPrefix = `${arrow} ${indicatorPrefix}${stateCluster} `;
|
|
406
|
+
const staticPrefixWidth = visibleWidth(staticPrefix);
|
|
314
407
|
const maxLeftDisplayWidth = Math.max(0, columns - timestampWidth - 1);
|
|
315
408
|
const maxLabelWidth = Math.max(
|
|
316
409
|
0,
|
|
317
410
|
maxLeftDisplayWidth - staticPrefixWidth,
|
|
318
411
|
);
|
|
319
|
-
const
|
|
320
|
-
|
|
412
|
+
const displayLabel =
|
|
413
|
+
item.type === "remote" && item.remoteName ? item.remoteName : item.name;
|
|
414
|
+
let truncatedLabel = truncateToWidth(displayLabel, maxLabelWidth);
|
|
415
|
+
let leftText = `${staticPrefix}${truncatedLabel}`;
|
|
416
|
+
|
|
417
|
+
let leftDisplayWidth = visibleWidth(leftText);
|
|
418
|
+
// Gap between labelとツール/日時。右端に寄せるため必要分だけ確保。
|
|
419
|
+
let gapWidth = Math.max(1, columns - leftDisplayWidth - timestampWidth);
|
|
420
|
+
|
|
421
|
+
// もしまだオーバーする場合、隙間→ラベルの順で削って収める
|
|
422
|
+
let totalWidth = leftDisplayWidth + gapWidth + timestampWidth;
|
|
423
|
+
if (totalWidth > columns) {
|
|
424
|
+
const overflow = totalWidth - columns;
|
|
425
|
+
const reducedGap = Math.max(1, gapWidth - overflow);
|
|
426
|
+
gapWidth = reducedGap;
|
|
427
|
+
totalWidth = leftDisplayWidth + gapWidth + timestampWidth;
|
|
428
|
+
}
|
|
429
|
+
if (leftDisplayWidth + gapWidth + timestampWidth > columns) {
|
|
430
|
+
const extra = leftDisplayWidth + gapWidth + timestampWidth - columns;
|
|
431
|
+
const newLabelWidth = Math.max(
|
|
432
|
+
0,
|
|
433
|
+
measureDisplayWidth(truncatedLabel) - extra,
|
|
434
|
+
);
|
|
435
|
+
truncatedLabel = truncateToWidth(displayLabel, newLabelWidth);
|
|
436
|
+
leftText = `${staticPrefix}${truncatedLabel}`;
|
|
437
|
+
leftDisplayWidth = visibleWidth(leftText);
|
|
438
|
+
gapWidth = Math.max(1, columns - leftDisplayWidth - timestampWidth);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const buildLine = () =>
|
|
442
|
+
`${leftText}${" ".repeat(gapWidth)}${timestampText}`;
|
|
443
|
+
|
|
444
|
+
let line = buildLine();
|
|
445
|
+
// Replace timestamp with colorized tool name (keep alignment from width calc)
|
|
446
|
+
let lineWithColoredTimestamp = line.replace(
|
|
447
|
+
timestampText,
|
|
448
|
+
displayTimestampText,
|
|
449
|
+
);
|
|
321
450
|
|
|
322
|
-
|
|
323
|
-
const
|
|
451
|
+
// 端末幅を超えた場合は隙間→ラベルの順で詰めて収める
|
|
452
|
+
const clampToWidth = () => {
|
|
453
|
+
const finalWidth = measureDisplayWidth(stripAnsi(lineWithColoredTimestamp));
|
|
454
|
+
if (finalWidth <= columns) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const overflow = finalWidth - columns;
|
|
458
|
+
const reducedGap = Math.max(1, gapWidth - overflow);
|
|
459
|
+
gapWidth = reducedGap;
|
|
460
|
+
line = buildLine();
|
|
461
|
+
lineWithColoredTimestamp = line.replace(
|
|
462
|
+
timestampText,
|
|
463
|
+
displayTimestampText,
|
|
464
|
+
);
|
|
465
|
+
const widthAfterGap = measureDisplayWidth(
|
|
466
|
+
stripAnsi(lineWithColoredTimestamp),
|
|
467
|
+
);
|
|
468
|
+
if (widthAfterGap > columns) {
|
|
469
|
+
const extra = widthAfterGap - columns;
|
|
470
|
+
const newLabelWidth = Math.max(
|
|
471
|
+
0,
|
|
472
|
+
measureDisplayWidth(truncatedLabel) - extra,
|
|
473
|
+
);
|
|
474
|
+
truncatedLabel = truncateToWidth(displayLabel, newLabelWidth);
|
|
475
|
+
leftText = `${staticPrefix}${truncatedLabel}`;
|
|
476
|
+
leftDisplayWidth = visibleWidth(leftText);
|
|
477
|
+
gapWidth = Math.max(1, columns - leftDisplayWidth - timestampWidth);
|
|
478
|
+
line = buildLine();
|
|
479
|
+
lineWithColoredTimestamp = line.replace(
|
|
480
|
+
timestampText,
|
|
481
|
+
displayTimestampText,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
};
|
|
324
485
|
|
|
325
|
-
|
|
326
|
-
const totalDisplayWidth = leftDisplayWidth + gapWidth + timestampWidth;
|
|
327
|
-
const paddingWidth = Math.max(0, columns - totalDisplayWidth);
|
|
328
|
-
if (paddingWidth > 0) {
|
|
329
|
-
line += " ".repeat(paddingWidth);
|
|
330
|
-
}
|
|
486
|
+
clampToWidth();
|
|
331
487
|
|
|
332
|
-
const output = isSelected
|
|
488
|
+
const output = isSelected
|
|
489
|
+
? `\u001b[46m\u001b[30m${lineWithColoredTimestamp}\u001b[0m`
|
|
490
|
+
: lineWithColoredTimestamp;
|
|
333
491
|
return <Text>{output}</Text>;
|
|
334
492
|
},
|
|
335
|
-
[
|
|
493
|
+
[
|
|
494
|
+
cleanupUI,
|
|
495
|
+
formatLatestCommit,
|
|
496
|
+
truncateToWidth,
|
|
497
|
+
selectedSet,
|
|
498
|
+
colorToolLabel,
|
|
499
|
+
],
|
|
336
500
|
);
|
|
337
501
|
|
|
338
502
|
return (
|
|
@@ -425,6 +589,8 @@ export function BranchListScreen({
|
|
|
425
589
|
disabled={Boolean(cleanupUI?.inputLocked)}
|
|
426
590
|
renderIndicator={() => null}
|
|
427
591
|
renderItem={renderBranchRow}
|
|
592
|
+
selectedIndex={selectedIndex}
|
|
593
|
+
onSelectedIndexChange={setSelectedIndex}
|
|
428
594
|
/>
|
|
429
595
|
</>
|
|
430
596
|
)}
|
|
@@ -12,6 +12,7 @@ import { getPullRequestByBranch } from "../../../github.js";
|
|
|
12
12
|
import type { BranchInfo, WorktreeInfo } from "../types.js";
|
|
13
13
|
import type { WorktreeInfo as GitWorktreeInfo } from "../../../worktree.js";
|
|
14
14
|
import { getLastToolUsageMap } from "../../../config/index.js";
|
|
15
|
+
import { hasUncommittedChanges } from "../../../git.js";
|
|
15
16
|
|
|
16
17
|
export interface UseGitDataOptions {
|
|
17
18
|
enableAutoRefresh?: boolean;
|
|
@@ -65,6 +66,22 @@ export function useGitData(options?: UseGitDataOptions): UseGitDataResult {
|
|
|
65
66
|
}
|
|
66
67
|
worktreesData = [];
|
|
67
68
|
}
|
|
69
|
+
|
|
70
|
+
// enrich worktrees with uncommitted status (only for accessible paths)
|
|
71
|
+
worktreesData = await Promise.all(
|
|
72
|
+
worktreesData.map(async (wt) => {
|
|
73
|
+
if (wt.isAccessible === false) {
|
|
74
|
+
return wt;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const hasUncommitted = await hasUncommittedChanges(wt.path);
|
|
78
|
+
return { ...wt, hasUncommittedChanges: hasUncommitted };
|
|
79
|
+
} catch {
|
|
80
|
+
return wt;
|
|
81
|
+
}
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
|
|
68
85
|
const lastToolUsageMap = await getLastToolUsageMap(repoRoot);
|
|
69
86
|
|
|
70
87
|
// upstream情報とdivergence情報を取得
|
|
@@ -109,6 +126,9 @@ export function useGitData(options?: UseGitDataOptions): UseGitDataResult {
|
|
|
109
126
|
locked: false, // worktree.ts doesn't expose locked status
|
|
110
127
|
prunable: worktree.isAccessible === false,
|
|
111
128
|
isAccessible: worktree.isAccessible ?? true, // Default to true if undefined
|
|
129
|
+
...(worktree.hasUncommittedChanges !== undefined
|
|
130
|
+
? { hasUncommittedChanges: worktree.hasUncommittedChanges }
|
|
131
|
+
: {}),
|
|
112
132
|
};
|
|
113
133
|
worktreeMap.set(worktree.branch, uiWorktreeInfo);
|
|
114
134
|
}
|
package/src/cli/ui/types.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface WorktreeInfo {
|
|
|
6
6
|
locked: boolean;
|
|
7
7
|
prunable: boolean;
|
|
8
8
|
isAccessible?: boolean;
|
|
9
|
+
hasUncommittedChanges?: boolean;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export type AITool = string;
|
|
@@ -229,6 +230,8 @@ export interface BranchItem extends BranchInfo {
|
|
|
229
230
|
syncStatus?: SyncStatus;
|
|
230
231
|
syncInfo?: string | undefined;
|
|
231
232
|
remoteName?: string | undefined;
|
|
233
|
+
// クリーンアップ判定で「未コミット/未プッシュなし」と評価された場合に true
|
|
234
|
+
safeToCleanup?: boolean;
|
|
232
235
|
}
|
|
233
236
|
|
|
234
237
|
/**
|
|
@@ -65,7 +65,8 @@ const iconWidthOverrides: Record<string, number> = {
|
|
|
65
65
|
"🚀": 1,
|
|
66
66
|
"📌": 1,
|
|
67
67
|
// Worktree status icons
|
|
68
|
-
"🟢":
|
|
68
|
+
"🟢": 2,
|
|
69
|
+
"⚪": 2,
|
|
69
70
|
"🟠": 1,
|
|
70
71
|
// Change status icons
|
|
71
72
|
"👉": 1,
|
|
@@ -73,7 +74,11 @@ const iconWidthOverrides: Record<string, number> = {
|
|
|
73
74
|
"📤": 1,
|
|
74
75
|
"🔃": 1,
|
|
75
76
|
"✅": 1,
|
|
76
|
-
"⚠️":
|
|
77
|
+
"⚠️": 2,
|
|
78
|
+
"⚠": 1,
|
|
79
|
+
"🛡": 2,
|
|
80
|
+
"☑": 2,
|
|
81
|
+
"☐": 2,
|
|
77
82
|
// Remote markers
|
|
78
83
|
"🔗": 1,
|
|
79
84
|
"💻": 1,
|
|
@@ -23,6 +23,7 @@ describe("modelOptions", () => {
|
|
|
23
23
|
expect(unique.size).toBe(ids.length);
|
|
24
24
|
expect(ids).toEqual([
|
|
25
25
|
"gpt-5.1-codex",
|
|
26
|
+
"gpt-5.2",
|
|
26
27
|
"gpt-5.1-codex-max",
|
|
27
28
|
"gpt-5.1-codex-mini",
|
|
28
29
|
"gpt-5.1",
|
|
@@ -36,6 +37,19 @@ describe("modelOptions", () => {
|
|
|
36
37
|
expect(getDefaultInferenceForModel(codexMax)).toBe("medium");
|
|
37
38
|
});
|
|
38
39
|
|
|
40
|
+
it("exposes gpt-5.2 with xhigh reasoning and medium default", () => {
|
|
41
|
+
const codex52 = getModelOptions("codex-cli").find(
|
|
42
|
+
(m) => m.id === "gpt-5.2",
|
|
43
|
+
);
|
|
44
|
+
expect(codex52?.inferenceLevels).toEqual([
|
|
45
|
+
"xhigh",
|
|
46
|
+
"high",
|
|
47
|
+
"medium",
|
|
48
|
+
"low",
|
|
49
|
+
]);
|
|
50
|
+
expect(getDefaultInferenceForModel(codex52)).toBe("medium");
|
|
51
|
+
});
|
|
52
|
+
|
|
39
53
|
it("lists expected Gemini models", () => {
|
|
40
54
|
expect(byId("gemini-cli")).toEqual([
|
|
41
55
|
"gemini-3-pro-preview",
|
|
@@ -33,6 +33,13 @@ const MODEL_OPTIONS: Record<string, ModelOption[]> = {
|
|
|
33
33
|
defaultInference: "high",
|
|
34
34
|
isDefault: true,
|
|
35
35
|
},
|
|
36
|
+
{
|
|
37
|
+
id: "gpt-5.2",
|
|
38
|
+
label: "gpt-5.2",
|
|
39
|
+
description: "Latest frontier model with extra high reasoning",
|
|
40
|
+
inferenceLevels: CODEX_MAX_LEVELS,
|
|
41
|
+
defaultInference: "medium",
|
|
42
|
+
},
|
|
36
43
|
{
|
|
37
44
|
id: "gpt-5.1-codex-max",
|
|
38
45
|
label: "gpt-5.1-codex-max",
|
package/src/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
getTerminalStreams,
|
|
34
34
|
waitForUserAcknowledgement,
|
|
35
35
|
} from "./utils/terminal.js";
|
|
36
|
+
import { createLogger } from "./logging/logger.js";
|
|
36
37
|
import { getToolById, getSharedEnvironment } from "./config/tools.js";
|
|
37
38
|
import { launchCustomAITool } from "./launcher.js";
|
|
38
39
|
import { saveSession, loadSession } from "./config/index.js";
|
|
@@ -55,6 +56,9 @@ const ERROR_PROMPT = chalk.yellow(
|
|
|
55
56
|
"Review the error details, then press Enter to continue.",
|
|
56
57
|
);
|
|
57
58
|
|
|
59
|
+
// Category: cli
|
|
60
|
+
const appLogger = createLogger({ category: "cli" });
|
|
61
|
+
|
|
58
62
|
async function waitForErrorAcknowledgement(): Promise<void> {
|
|
59
63
|
await waitForUserAcknowledgement(ERROR_PROMPT);
|
|
60
64
|
}
|
|
@@ -64,14 +68,17 @@ async function waitForErrorAcknowledgement(): Promise<void> {
|
|
|
64
68
|
*/
|
|
65
69
|
function printError(message: string): void {
|
|
66
70
|
console.error(chalk.red(`❌ ${message}`));
|
|
71
|
+
appLogger.error({ message });
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
function printInfo(message: string): void {
|
|
70
75
|
console.log(chalk.blue(`ℹ️ ${message}`));
|
|
76
|
+
appLogger.info({ message });
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
function printWarning(message: string): void {
|
|
74
80
|
console.warn(chalk.yellow(`⚠️ ${message}`));
|
|
81
|
+
appLogger.warn({ message });
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
type GitStepResult<T> = { ok: true; value: T } | { ok: false };
|