@akiojin/gwt 2.10.0 → 2.11.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 +5 -47
- package/dist/cli/ui/components/App.js.map +1 -1
- package/dist/cli/ui/components/common/Input.d.ts +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts +1 -2
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.js +19 -14
- package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/ModelSelectorScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/ModelSelectorScreen.js +6 -6
- package/dist/cli/ui/components/screens/ModelSelectorScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/PRCleanupScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/PRCleanupScreen.js +0 -3
- package/dist/cli/ui/components/screens/PRCleanupScreen.js.map +1 -1
- package/dist/cli/ui/hooks/useGitData.d.ts.map +1 -1
- package/dist/cli/ui/hooks/useGitData.js +43 -2
- package/dist/cli/ui/hooks/useGitData.js.map +1 -1
- package/dist/cli/ui/types.d.ts +16 -4
- 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 +124 -15
- 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.map +1 -1
- package/dist/client/assets/{index-CNWntAlF.js → index-Dl798X5w.js} +1 -1
- package/dist/client/index.html +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/git.d.ts +6 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +40 -5
- package/dist/git.js.map +1 -1
- package/dist/web/client/src/components/BranchGraph.js +1 -1
- package/dist/web/client/src/components/BranchGraph.js.map +1 -1
- package/dist/web/client/src/pages/BranchDetailPage.d.ts.map +1 -1
- package/dist/web/client/src/pages/BranchDetailPage.js +8 -3
- package/dist/web/client/src/pages/BranchDetailPage.js.map +1 -1
- package/dist/web/server/routes/sessions.d.ts.map +1 -1
- package/dist/web/server/routes/sessions.js +4 -2
- package/dist/web/server/routes/sessions.js.map +1 -1
- package/dist/worktree.d.ts.map +1 -1
- package/dist/worktree.js +31 -44
- package/dist/worktree.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/ui/__tests__/acceptance/navigation.acceptance.test.tsx +9 -17
- package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +14 -20
- package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +14 -44
- package/src/cli/ui/__tests__/components/App.test.tsx +8 -15
- package/src/cli/ui/__tests__/components/ModelSelectorScreen.initial.test.tsx +12 -5
- package/src/cli/ui/__tests__/components/common/Confirm.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/common/ErrorBoundary.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/common/Input.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/common/LoadingIndicator.test.tsx +15 -14
- package/src/cli/ui/__tests__/components/common/Select.memo.test.tsx +3 -3
- package/src/cli/ui/__tests__/components/common/Select.test.tsx +1 -4
- package/src/cli/ui/__tests__/components/parts/Footer.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/parts/Header.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/parts/ScrollableList.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/parts/Stats.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/screens/BranchCreatorScreen.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +31 -41
- package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/screens/PRCleanupScreen.test.tsx +3 -2
- package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +3 -2
- package/src/cli/ui/__tests__/hooks/useScreenState.test.ts +18 -17
- package/src/cli/ui/__tests__/hooks/useTerminalSize.test.ts +3 -2
- package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +5 -12
- package/src/cli/ui/__tests__/integration/navigation.test.tsx +15 -10
- package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx +3 -2
- package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +0 -4
- package/src/cli/ui/__tests__/performance/useMemoOptimization.test.tsx +3 -5
- package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +24 -22
- package/src/cli/ui/components/App.tsx +3 -74
- package/src/cli/ui/components/common/Input.tsx +1 -1
- package/src/cli/ui/components/screens/BranchListScreen.tsx +32 -22
- package/src/cli/ui/components/screens/ModelSelectorScreen.tsx +46 -49
- package/src/cli/ui/components/screens/PRCleanupScreen.tsx +0 -3
- package/src/cli/ui/hooks/useGitData.ts +59 -1
- package/src/cli/ui/screens/__tests__/BranchActionSelectorScreen.test.tsx +3 -2
- package/src/cli/ui/types.ts +24 -5
- package/src/cli/ui/utils/branchFormatter.ts +123 -15
- package/src/cli/ui/utils/modelOptions.test.ts +4 -6
- package/src/cli/ui/utils/modelOptions.ts +1 -2
- package/src/config/index.ts +2 -1
- package/src/git.ts +56 -16
- package/src/web/client/src/components/BranchGraph.tsx +1 -1
- package/src/web/client/src/pages/BranchDetailPage.tsx +8 -3
- package/src/web/server/routes/sessions.ts +12 -5
- package/src/worktree.ts +31 -59
- package/dist/cli/ui/components/screens/WorktreeManagerScreen.d.ts +0 -20
- package/dist/cli/ui/components/screens/WorktreeManagerScreen.d.ts.map +0 -1
- package/dist/cli/ui/components/screens/WorktreeManagerScreen.js +0 -65
- package/dist/cli/ui/components/screens/WorktreeManagerScreen.js.map +0 -1
- package/src/cli/ui/__tests__/components/screens/WorktreeManagerScreen.test.tsx +0 -151
- package/src/cli/ui/components/screens/WorktreeManagerScreen.tsx +0 -117
|
@@ -18,9 +18,6 @@ vi.mock("../../hooks/useGitData.js", () => ({
|
|
|
18
18
|
useGitData: vi.fn(),
|
|
19
19
|
}));
|
|
20
20
|
|
|
21
|
-
import { useGitData } from "../../hooks/useGitData.js";
|
|
22
|
-
// const mockUseGitData = useGitData as ReturnType<typeof vi.fn>;
|
|
23
|
-
|
|
24
21
|
// Helper function to create a stable hash of branch data
|
|
25
22
|
function createBranchHash(branches: BranchInfo[]): string {
|
|
26
23
|
return branches.map((b) => `${b.name}-${b.type}-${b.isCurrent}`).join(",");
|
|
@@ -63,8 +60,9 @@ function TestComponent({
|
|
|
63
60
|
describe("useMemo Optimization (T082-1)", () => {
|
|
64
61
|
beforeEach(() => {
|
|
65
62
|
const window = new Window();
|
|
66
|
-
globalThis.window = window as
|
|
67
|
-
globalThis.document =
|
|
63
|
+
globalThis.window = window as unknown as typeof globalThis.window;
|
|
64
|
+
globalThis.document =
|
|
65
|
+
window.document as unknown as typeof globalThis.document;
|
|
68
66
|
vi.clearAllMocks();
|
|
69
67
|
});
|
|
70
68
|
|
|
@@ -23,7 +23,7 @@ describe("branchFormatter", () => {
|
|
|
23
23
|
expect(result.branchType).toBe("main");
|
|
24
24
|
expect(result.isCurrent).toBe(true);
|
|
25
25
|
expect(result.icons).toContain("⚡"); // main icon
|
|
26
|
-
expect(result.icons).toContain("
|
|
26
|
+
expect(result.icons).toContain("👉"); // current icon
|
|
27
27
|
expect(result.label).toContain("main");
|
|
28
28
|
expect(result.value).toBe("main");
|
|
29
29
|
expect(result.hasChanges).toBe(false);
|
|
@@ -40,7 +40,7 @@ describe("branchFormatter", () => {
|
|
|
40
40
|
const result = formatBranchItem(branchInfo);
|
|
41
41
|
|
|
42
42
|
expect(result.icons).toContain("✨"); // feature icon
|
|
43
|
-
expect(result.icons).not.toContain("
|
|
43
|
+
expect(result.icons).not.toContain("👉"); // not current
|
|
44
44
|
expect(result.label).toContain("feature/new-ui");
|
|
45
45
|
expect(result.value).toBe("feature/new-ui");
|
|
46
46
|
});
|
|
@@ -157,12 +157,12 @@ describe("branchFormatter", () => {
|
|
|
157
157
|
const localResult = formatBranchItem(localBranch);
|
|
158
158
|
const remoteResult = formatBranchItem(remoteBranch);
|
|
159
159
|
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
// Both should have the branch name in the label
|
|
161
|
+
expect(localResult.label).toContain("feature/foo");
|
|
162
|
+
expect(remoteResult.label).toContain("origin/feature/foo");
|
|
162
163
|
|
|
163
|
-
|
|
164
|
-
expect(
|
|
165
|
-
expect(remoteResult.label).toMatch(/☁(?:️|︎)?\s+origin/);
|
|
164
|
+
// Remote branch should have ☁️ marker for remote-only status
|
|
165
|
+
expect(remoteResult.label).toMatch(/☁️/);
|
|
166
166
|
});
|
|
167
167
|
|
|
168
168
|
it("should keep icon columns fixed-width when using wide emoji icons", () => {
|
|
@@ -182,11 +182,13 @@ describe("branchFormatter", () => {
|
|
|
182
182
|
|
|
183
183
|
const result = formatBranchItem(branchInfo, { hasChanges: true });
|
|
184
184
|
|
|
185
|
-
//
|
|
185
|
+
// Icon columns: [Type][Worktree][Changes][Remote] = 4 * 2 = 8
|
|
186
|
+
// Sync column: 6 (fixed width for icon + up to 4 digits + space)
|
|
187
|
+
// Total: 14
|
|
186
188
|
const iconBlockWidth =
|
|
187
189
|
stringWidth(result.label) - stringWidth(branchInfo.name);
|
|
188
190
|
|
|
189
|
-
expect(iconBlockWidth).toBe(
|
|
191
|
+
expect(iconBlockWidth).toBe(14);
|
|
190
192
|
});
|
|
191
193
|
|
|
192
194
|
it("should include worktree status icon when provided", () => {
|
|
@@ -218,7 +220,7 @@ describe("branchFormatter", () => {
|
|
|
218
220
|
|
|
219
221
|
const result = formatBranchItem(branchInfo, { hasChanges: true });
|
|
220
222
|
|
|
221
|
-
expect(result.icons).toContain("
|
|
223
|
+
expect(result.icons).toContain("💾"); // changes icon
|
|
222
224
|
expect(result.hasChanges).toBe(true);
|
|
223
225
|
});
|
|
224
226
|
|
|
@@ -233,8 +235,8 @@ describe("branchFormatter", () => {
|
|
|
233
235
|
|
|
234
236
|
const result = formatBranchItem(branchInfo);
|
|
235
237
|
|
|
236
|
-
expect(result.icons).toContain("
|
|
237
|
-
expect(result.label).toContain("
|
|
238
|
+
expect(result.icons).toContain("📤"); // unpushed icon
|
|
239
|
+
expect(result.label).toContain("📤");
|
|
238
240
|
});
|
|
239
241
|
|
|
240
242
|
it("should show open PR icon", () => {
|
|
@@ -248,8 +250,8 @@ describe("branchFormatter", () => {
|
|
|
248
250
|
|
|
249
251
|
const result = formatBranchItem(branchInfo);
|
|
250
252
|
|
|
251
|
-
expect(result.icons).toContain("
|
|
252
|
-
expect(result.label).toContain("
|
|
253
|
+
expect(result.icons).toContain("🔃"); // open PR icon
|
|
254
|
+
expect(result.label).toContain("🔃");
|
|
253
255
|
});
|
|
254
256
|
|
|
255
257
|
it("should show merged PR icon", () => {
|
|
@@ -300,12 +302,12 @@ describe("branchFormatter", () => {
|
|
|
300
302
|
const resultWithChanges = formatBranchItem(branchInfo, {
|
|
301
303
|
hasChanges: true,
|
|
302
304
|
});
|
|
303
|
-
expect(resultWithChanges.icons).toContain("
|
|
304
|
-
expect(resultWithChanges.icons).not.toContain("
|
|
305
|
+
expect(resultWithChanges.icons).toContain("💾");
|
|
306
|
+
expect(resultWithChanges.icons).not.toContain("📤");
|
|
305
307
|
|
|
306
308
|
const resultWithoutChanges = formatBranchItem(branchInfo);
|
|
307
|
-
expect(resultWithoutChanges.icons).toContain("
|
|
308
|
-
expect(resultWithoutChanges.icons).not.toContain("
|
|
309
|
+
expect(resultWithoutChanges.icons).toContain("📤");
|
|
310
|
+
expect(resultWithoutChanges.icons).not.toContain("💾");
|
|
309
311
|
});
|
|
310
312
|
|
|
311
313
|
it("should prioritize unpushed over open PR", () => {
|
|
@@ -320,8 +322,8 @@ describe("branchFormatter", () => {
|
|
|
320
322
|
|
|
321
323
|
const result = formatBranchItem(branchInfo);
|
|
322
324
|
|
|
323
|
-
expect(result.icons).toContain("
|
|
324
|
-
expect(result.icons).not.toContain("
|
|
325
|
+
expect(result.icons).toContain("📤");
|
|
326
|
+
expect(result.icons).not.toContain("🔃");
|
|
325
327
|
});
|
|
326
328
|
|
|
327
329
|
it("should prioritize open PR over merged PR", () => {
|
|
@@ -336,7 +338,7 @@ describe("branchFormatter", () => {
|
|
|
336
338
|
|
|
337
339
|
const result = formatBranchItem(branchInfo);
|
|
338
340
|
|
|
339
|
-
expect(result.icons).toContain("
|
|
341
|
+
expect(result.icons).toContain("🔃");
|
|
340
342
|
expect(result.icons).not.toContain("✅");
|
|
341
343
|
});
|
|
342
344
|
|
|
@@ -378,7 +380,7 @@ describe("branchFormatter", () => {
|
|
|
378
380
|
const result = formatBranchItem(branchInfo);
|
|
379
381
|
|
|
380
382
|
expect(result.icons).toContain("⚠️");
|
|
381
|
-
expect(result.icons).not.toContain("
|
|
383
|
+
expect(result.icons).not.toContain("👉");
|
|
382
384
|
});
|
|
383
385
|
|
|
384
386
|
it("should handle develop branch", () => {
|
|
@@ -8,7 +8,6 @@ import React, {
|
|
|
8
8
|
import { useApp } from "ink";
|
|
9
9
|
import { ErrorBoundary } from "./common/ErrorBoundary.js";
|
|
10
10
|
import { BranchListScreen } from "./screens/BranchListScreen.js";
|
|
11
|
-
import { WorktreeManagerScreen } from "./screens/WorktreeManagerScreen.js";
|
|
12
11
|
import { BranchCreatorScreen } from "./screens/BranchCreatorScreen.js";
|
|
13
12
|
import { BranchActionSelectorScreen } from "../screens/BranchActionSelectorScreen.js";
|
|
14
13
|
import { AIToolSelectorScreen } from "./screens/AIToolSelectorScreen.js";
|
|
@@ -19,7 +18,6 @@ import {
|
|
|
19
18
|
ModelSelectorScreen,
|
|
20
19
|
type ModelSelectionResult,
|
|
21
20
|
} from "./screens/ModelSelectorScreen.js";
|
|
22
|
-
import type { WorktreeItem } from "./screens/WorktreeManagerScreen.js";
|
|
23
21
|
import { useGitData } from "../hooks/useGitData.js";
|
|
24
22
|
import { useScreenState } from "../hooks/useScreenState.js";
|
|
25
23
|
import { formatBranchItems } from "../utils/branchFormatter.js";
|
|
@@ -31,11 +29,7 @@ import type {
|
|
|
31
29
|
InferenceLevel,
|
|
32
30
|
SelectedBranchState,
|
|
33
31
|
} from "../types.js";
|
|
34
|
-
import {
|
|
35
|
-
getRepositoryRoot,
|
|
36
|
-
deleteBranch,
|
|
37
|
-
deleteRemoteBranch,
|
|
38
|
-
} from "../../../git.js";
|
|
32
|
+
import { getRepositoryRoot, deleteBranch } from "../../../git.js";
|
|
39
33
|
import {
|
|
40
34
|
createWorktree,
|
|
41
35
|
generateWorktreePath,
|
|
@@ -269,19 +263,6 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
|
269
263
|
[visibleBranches],
|
|
270
264
|
);
|
|
271
265
|
|
|
272
|
-
// Format worktrees to WorktreeItems
|
|
273
|
-
const worktreeItems: WorktreeItem[] = useMemo(
|
|
274
|
-
() =>
|
|
275
|
-
worktrees.map(
|
|
276
|
-
(wt): WorktreeItem => ({
|
|
277
|
-
branch: wt.branch,
|
|
278
|
-
path: wt.path,
|
|
279
|
-
isAccessible: wt.isAccessible ?? true,
|
|
280
|
-
}),
|
|
281
|
-
),
|
|
282
|
-
[worktrees],
|
|
283
|
-
);
|
|
284
|
-
|
|
285
266
|
const resolveBaseBranch = useCallback(() => {
|
|
286
267
|
const localMain = branches.find(
|
|
287
268
|
(branch) =>
|
|
@@ -428,40 +409,6 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
|
428
409
|
],
|
|
429
410
|
);
|
|
430
411
|
|
|
431
|
-
// Handle navigation
|
|
432
|
-
const handleNavigate = useCallback(
|
|
433
|
-
(screen: string) => {
|
|
434
|
-
navigateTo(screen as any);
|
|
435
|
-
},
|
|
436
|
-
[navigateTo],
|
|
437
|
-
);
|
|
438
|
-
|
|
439
|
-
const handleWorktreeSelect = useCallback(
|
|
440
|
-
(worktree: WorktreeItem) => {
|
|
441
|
-
const lastTool = branches.find((b) => b.name === worktree.branch)
|
|
442
|
-
?.lastToolUsage?.toolId;
|
|
443
|
-
setSelectedBranch({
|
|
444
|
-
name: worktree.branch,
|
|
445
|
-
displayName: worktree.branch,
|
|
446
|
-
branchType: "local",
|
|
447
|
-
branchCategory: inferBranchCategory(worktree.branch),
|
|
448
|
-
});
|
|
449
|
-
setSelectedTool(null);
|
|
450
|
-
setSelectedModel(null);
|
|
451
|
-
setCreationSourceBranch(null);
|
|
452
|
-
setPreferredToolId(lastTool ?? null);
|
|
453
|
-
setCleanupFooterMessage(null);
|
|
454
|
-
navigateTo("ai-tool-selector");
|
|
455
|
-
},
|
|
456
|
-
[
|
|
457
|
-
inferBranchCategory,
|
|
458
|
-
navigateTo,
|
|
459
|
-
setCleanupFooterMessage,
|
|
460
|
-
setCreationSourceBranch,
|
|
461
|
-
branches,
|
|
462
|
-
],
|
|
463
|
-
);
|
|
464
|
-
|
|
465
412
|
// Handle branch action selection
|
|
466
413
|
const handleProtectedBranchSwitch = useCallback(async () => {
|
|
467
414
|
if (!selectedBranch) {
|
|
@@ -733,14 +680,8 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
|
733
680
|
|
|
734
681
|
await deleteBranch(target.branch, true);
|
|
735
682
|
|
|
736
|
-
//
|
|
737
|
-
|
|
738
|
-
try {
|
|
739
|
-
await deleteRemoteBranch(target.branch);
|
|
740
|
-
} catch {
|
|
741
|
-
// リモート削除失敗はログのみ、処理は続行
|
|
742
|
-
}
|
|
743
|
-
}
|
|
683
|
+
// 自動クリーンアップではリモートブランチは削除しない
|
|
684
|
+
// リモートブランチはユーザーが明示的に削除する必要がある
|
|
744
685
|
|
|
745
686
|
succeededBranches.push(target.branch);
|
|
746
687
|
setCleanupIndicators((prev) => ({
|
|
@@ -864,7 +805,6 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
|
864
805
|
branches={branchItems}
|
|
865
806
|
stats={stats}
|
|
866
807
|
onSelect={handleSelect}
|
|
867
|
-
onNavigate={handleNavigate}
|
|
868
808
|
onQuit={handleQuit}
|
|
869
809
|
onCleanupCommand={handleCleanupCommand}
|
|
870
810
|
onRefresh={refresh}
|
|
@@ -882,16 +822,6 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
|
882
822
|
/>
|
|
883
823
|
);
|
|
884
824
|
|
|
885
|
-
case "worktree-manager":
|
|
886
|
-
return (
|
|
887
|
-
<WorktreeManagerScreen
|
|
888
|
-
worktrees={worktreeItems}
|
|
889
|
-
onBack={goBack}
|
|
890
|
-
onSelect={handleWorktreeSelect}
|
|
891
|
-
version={version}
|
|
892
|
-
/>
|
|
893
|
-
);
|
|
894
|
-
|
|
895
825
|
case "branch-creator":
|
|
896
826
|
return (
|
|
897
827
|
<BranchCreatorScreen
|
|
@@ -978,7 +908,6 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
|
978
908
|
branches={branchItems}
|
|
979
909
|
stats={stats}
|
|
980
910
|
onSelect={handleSelect}
|
|
981
|
-
onNavigate={handleNavigate}
|
|
982
911
|
onQuit={handleQuit}
|
|
983
912
|
onRefresh={refresh}
|
|
984
913
|
loading={loading}
|
|
@@ -11,7 +11,7 @@ export interface InputProps {
|
|
|
11
11
|
mask?: string;
|
|
12
12
|
/**
|
|
13
13
|
* Block specific key bindings to prevent parent handlers from processing them
|
|
14
|
-
* Useful for blocking shortcuts like 'c', 'r'
|
|
14
|
+
* Useful for blocking shortcuts like 'c', 'r' while typing
|
|
15
15
|
*/
|
|
16
16
|
blockKeys?: string[];
|
|
17
17
|
}
|
|
@@ -14,8 +14,6 @@ import chalk from "chalk";
|
|
|
14
14
|
const WIDTH_OVERRIDES: Record<string, number> = {
|
|
15
15
|
// Remote icon
|
|
16
16
|
"☁": 1,
|
|
17
|
-
// Unpushed icon
|
|
18
|
-
"⬆": 1,
|
|
19
17
|
// Branch type icons
|
|
20
18
|
"⚡": 1,
|
|
21
19
|
"✨": 1,
|
|
@@ -27,11 +25,16 @@ const WIDTH_OVERRIDES: Record<string, number> = {
|
|
|
27
25
|
"🟢": 1,
|
|
28
26
|
"🟠": 1,
|
|
29
27
|
// Change status icons
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
28
|
+
"👉": 1,
|
|
29
|
+
"💾": 1,
|
|
30
|
+
"📤": 1,
|
|
31
|
+
"🔃": 1,
|
|
33
32
|
"✅": 1,
|
|
34
33
|
"⚠️": 1,
|
|
34
|
+
// Remote markers
|
|
35
|
+
"🔗": 1,
|
|
36
|
+
"💻": 1,
|
|
37
|
+
"☁️": 1,
|
|
35
38
|
};
|
|
36
39
|
|
|
37
40
|
const getCharWidth = (char: string): number => {
|
|
@@ -70,7 +73,6 @@ export interface BranchListScreenProps {
|
|
|
70
73
|
branches: BranchItem[];
|
|
71
74
|
stats: Statistics;
|
|
72
75
|
onSelect: (branch: BranchItem) => void;
|
|
73
|
-
onNavigate?: (screen: string) => void;
|
|
74
76
|
onQuit?: () => void;
|
|
75
77
|
onCleanupCommand?: () => void;
|
|
76
78
|
onRefresh?: () => void;
|
|
@@ -96,7 +98,6 @@ export function BranchListScreen({
|
|
|
96
98
|
branches,
|
|
97
99
|
stats,
|
|
98
100
|
onSelect,
|
|
99
|
-
onNavigate,
|
|
100
101
|
onCleanupCommand,
|
|
101
102
|
onRefresh,
|
|
102
103
|
loading = false,
|
|
@@ -112,6 +113,9 @@ export function BranchListScreen({
|
|
|
112
113
|
testOnFilterQueryChange,
|
|
113
114
|
}: BranchListScreenProps) {
|
|
114
115
|
const { rows } = useTerminalSize();
|
|
116
|
+
const COLUMN_WIDTH = 2;
|
|
117
|
+
const SYNC_COLUMN_WIDTH = 6;
|
|
118
|
+
const headerText = ` ${"Ty".padEnd(COLUMN_WIDTH)}${"Wt".padEnd(COLUMN_WIDTH)}${"St".padEnd(COLUMN_WIDTH)}${"Rm".padEnd(COLUMN_WIDTH)}${"Sync".padEnd(SYNC_COLUMN_WIDTH)}Branch`;
|
|
115
119
|
|
|
116
120
|
// Filter state - allow test control via props
|
|
117
121
|
const [internalFilterQuery, setInternalFilterQuery] = useState("");
|
|
@@ -139,7 +143,7 @@ export function BranchListScreen({
|
|
|
139
143
|
);
|
|
140
144
|
|
|
141
145
|
// Handle keyboard input
|
|
142
|
-
// Note: Input component blocks specific keys (c/r/
|
|
146
|
+
// Note: Input component blocks specific keys (c/r/f) using blockKeys prop
|
|
143
147
|
// This prevents shortcuts from triggering while typing in the filter
|
|
144
148
|
useInput((input, key) => {
|
|
145
149
|
if (cleanupUI?.inputLocked) {
|
|
@@ -172,9 +176,7 @@ export function BranchListScreen({
|
|
|
172
176
|
}
|
|
173
177
|
|
|
174
178
|
// Global shortcuts (blocked by Input component when typing in filter mode)
|
|
175
|
-
if (input === "
|
|
176
|
-
onNavigate("worktree-manager");
|
|
177
|
-
} else if (input === "c") {
|
|
179
|
+
if (input === "c") {
|
|
178
180
|
onCleanupCommand?.();
|
|
179
181
|
} else if (input === "r" && onRefresh) {
|
|
180
182
|
onRefresh();
|
|
@@ -225,7 +227,6 @@ export function BranchListScreen({
|
|
|
225
227
|
{ key: "enter", description: "Select" },
|
|
226
228
|
{ key: "f", description: "Filter" },
|
|
227
229
|
{ key: "r", description: "Refresh" },
|
|
228
|
-
{ key: "m", description: "Manage worktrees" },
|
|
229
230
|
{ key: "c", description: "Cleanup branches" },
|
|
230
231
|
];
|
|
231
232
|
|
|
@@ -311,7 +312,10 @@ export function BranchListScreen({
|
|
|
311
312
|
const staticPrefix = `${arrow} ${indicatorPrefix}`;
|
|
312
313
|
const staticPrefixWidth = measureDisplayWidth(staticPrefix);
|
|
313
314
|
const maxLeftDisplayWidth = Math.max(0, columns - timestampWidth - 1);
|
|
314
|
-
const maxLabelWidth = Math.max(
|
|
315
|
+
const maxLabelWidth = Math.max(
|
|
316
|
+
0,
|
|
317
|
+
maxLeftDisplayWidth - staticPrefixWidth,
|
|
318
|
+
);
|
|
315
319
|
const truncatedLabel = truncateToWidth(item.label, maxLabelWidth);
|
|
316
320
|
const leftText = `${staticPrefix}${truncatedLabel}`;
|
|
317
321
|
|
|
@@ -350,7 +354,7 @@ export function BranchListScreen({
|
|
|
350
354
|
onChange={setFilterQuery}
|
|
351
355
|
onSubmit={() => {}} // No-op: filter is applied in real-time
|
|
352
356
|
placeholder="Type to search..."
|
|
353
|
-
blockKeys={["c", "r", "
|
|
357
|
+
blockKeys={["c", "r", "f"]} // Block shortcuts while typing
|
|
354
358
|
/>
|
|
355
359
|
) : (
|
|
356
360
|
<Text dimColor>{filterQuery || "(press f to filter)"}</Text>
|
|
@@ -409,14 +413,20 @@ export function BranchListScreen({
|
|
|
409
413
|
!error &&
|
|
410
414
|
branches.length > 0 &&
|
|
411
415
|
filteredBranches.length > 0 && (
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
416
|
+
<>
|
|
417
|
+
{/* Column labels */}
|
|
418
|
+
<Box>
|
|
419
|
+
<Text dimColor>{headerText}</Text>
|
|
420
|
+
</Box>
|
|
421
|
+
<Select
|
|
422
|
+
items={filteredBranches}
|
|
423
|
+
onSelect={onSelect}
|
|
424
|
+
limit={limit}
|
|
425
|
+
disabled={Boolean(cleanupUI?.inputLocked)}
|
|
426
|
+
renderIndicator={() => null}
|
|
427
|
+
renderItem={renderBranchRow}
|
|
428
|
+
/>
|
|
429
|
+
</>
|
|
420
430
|
)}
|
|
421
431
|
</Box>
|
|
422
432
|
|
|
@@ -106,56 +106,54 @@ export function ModelSelectorScreen({
|
|
|
106
106
|
[selectedModel],
|
|
107
107
|
);
|
|
108
108
|
|
|
109
|
-
const inferenceItems: InferenceSelectItem[] = useMemo(
|
|
110
|
-
() => {
|
|
111
|
-
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
if (level === "medium") {
|
|
121
|
-
return {
|
|
122
|
-
label: "Medium (default)",
|
|
123
|
-
value: level,
|
|
124
|
-
hint: "Balances speed and reasoning depth for everyday tasks",
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
if (level === "high") {
|
|
128
|
-
return {
|
|
129
|
-
label: "High",
|
|
130
|
-
value: level,
|
|
131
|
-
hint: "Maximizes reasoning depth for complex problems",
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
if (level === "xhigh") {
|
|
135
|
-
return {
|
|
136
|
-
label: "Extra high",
|
|
137
|
-
value: level,
|
|
138
|
-
hint:
|
|
139
|
-
"Extra high reasoning depth; may quickly consume Plus plan rate limits.",
|
|
140
|
-
};
|
|
141
|
-
}
|
|
109
|
+
const inferenceItems: InferenceSelectItem[] = useMemo(() => {
|
|
110
|
+
return inferenceOptions.map((level) => {
|
|
111
|
+
if (selectedModel?.id === "gpt-5.1-codex-max") {
|
|
112
|
+
if (level === "low") {
|
|
113
|
+
return {
|
|
114
|
+
label: "Low",
|
|
115
|
+
value: level,
|
|
116
|
+
hint: "Fast responses with lighter reasoning",
|
|
117
|
+
};
|
|
142
118
|
}
|
|
119
|
+
if (level === "medium") {
|
|
120
|
+
return {
|
|
121
|
+
label: "Medium (default)",
|
|
122
|
+
value: level,
|
|
123
|
+
hint: "Balances speed and reasoning depth for everyday tasks",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (level === "high") {
|
|
127
|
+
return {
|
|
128
|
+
label: "High",
|
|
129
|
+
value: level,
|
|
130
|
+
hint: "Maximizes reasoning depth for complex problems",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (level === "xhigh") {
|
|
134
|
+
return {
|
|
135
|
+
label: "Extra high",
|
|
136
|
+
value: level,
|
|
137
|
+
hint: "Extra high reasoning depth; may quickly consume Plus plan rate limits.",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
143
141
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
[inferenceOptions, selectedModel?.id],
|
|
151
|
-
);
|
|
142
|
+
return {
|
|
143
|
+
label: INFERENCE_LABELS[level],
|
|
144
|
+
value: level,
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
}, [inferenceOptions, selectedModel?.id]);
|
|
152
148
|
|
|
153
149
|
const defaultInferenceIndex = useMemo(() => {
|
|
154
150
|
const initialLevel = initialSelection?.inferenceLevel;
|
|
155
151
|
if (initialLevel && inferenceOptions.includes(initialLevel)) {
|
|
156
152
|
return inferenceOptions.findIndex((lvl) => lvl === initialLevel);
|
|
157
153
|
}
|
|
158
|
-
const defaultLevel = getDefaultInferenceForModel(
|
|
154
|
+
const defaultLevel = getDefaultInferenceForModel(
|
|
155
|
+
selectedModel ?? undefined,
|
|
156
|
+
);
|
|
159
157
|
if (!defaultLevel) return 0;
|
|
160
158
|
const index = inferenceOptions.findIndex((lvl) => lvl === defaultLevel);
|
|
161
159
|
return index >= 0 ? index : 0;
|
|
@@ -223,11 +221,9 @@ export function ModelSelectorScreen({
|
|
|
223
221
|
{isSelected ? (
|
|
224
222
|
<Text color="cyan">➤ {item.label}</Text>
|
|
225
223
|
) : (
|
|
226
|
-
<Text>
|
|
224
|
+
<Text> {item.label}</Text>
|
|
227
225
|
)}
|
|
228
|
-
{item.description ?
|
|
229
|
-
<Text color="gray"> {item.description}</Text>
|
|
230
|
-
) : null}
|
|
226
|
+
{item.description ? <Text color="gray"> {item.description}</Text> : null}
|
|
231
227
|
</Box>
|
|
232
228
|
);
|
|
233
229
|
|
|
@@ -271,7 +267,8 @@ export function ModelSelectorScreen({
|
|
|
271
267
|
<Select
|
|
272
268
|
items={[
|
|
273
269
|
{
|
|
274
|
-
label:
|
|
270
|
+
label:
|
|
271
|
+
"No model selection required. Press Enter to continue.",
|
|
275
272
|
value: "__continue__",
|
|
276
273
|
},
|
|
277
274
|
]}
|
|
@@ -302,10 +299,10 @@ export function ModelSelectorScreen({
|
|
|
302
299
|
{isSelected ? (
|
|
303
300
|
<Text color="cyan">➤ {item.label}</Text>
|
|
304
301
|
) : (
|
|
305
|
-
<Text>
|
|
302
|
+
<Text> {item.label}</Text>
|
|
306
303
|
)}
|
|
307
304
|
{"hint" in item && item.hint ? (
|
|
308
|
-
<Text color="gray">
|
|
305
|
+
<Text color="gray"> {item.hint}</Text>
|
|
309
306
|
) : null}
|
|
310
307
|
</Box>
|
|
311
308
|
)}
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
hasUnpushedCommitsInRepo,
|
|
5
5
|
getRepositoryRoot,
|
|
6
6
|
fetchAllRemotes,
|
|
7
|
+
collectUpstreamMap,
|
|
8
|
+
getBranchDivergenceStatuses,
|
|
7
9
|
} from "../../../git.js";
|
|
8
10
|
import { listAdditionalWorktrees } from "../../../worktree.js";
|
|
9
11
|
import { getPullRequestByBranch } from "../../../github.js";
|
|
@@ -65,6 +67,36 @@ export function useGitData(options?: UseGitDataOptions): UseGitDataResult {
|
|
|
65
67
|
}
|
|
66
68
|
const lastToolUsageMap = await getLastToolUsageMap(repoRoot);
|
|
67
69
|
|
|
70
|
+
// upstream情報とdivergence情報を取得
|
|
71
|
+
const [upstreamMap, divergenceStatuses] = await Promise.all([
|
|
72
|
+
collectUpstreamMap(repoRoot),
|
|
73
|
+
getBranchDivergenceStatuses({ cwd: repoRoot }).catch(() => []),
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
// divergenceをMapに変換
|
|
77
|
+
const divergenceMap = new Map(
|
|
78
|
+
divergenceStatuses.map((s) => [
|
|
79
|
+
s.branch,
|
|
80
|
+
{
|
|
81
|
+
ahead: s.localAhead,
|
|
82
|
+
behind: s.remoteAhead,
|
|
83
|
+
upToDate: s.localAhead === 0 && s.remoteAhead === 0,
|
|
84
|
+
},
|
|
85
|
+
]),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// ローカルブランチ名のSet(重複除去用)
|
|
89
|
+
const localBranchNames = new Set(
|
|
90
|
+
branchesData.filter((b) => b.type === "local").map((b) => b.name),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// リモートブランチ名のSet(hasRemoteCounterpart判定用)
|
|
94
|
+
const remoteBranchNames = new Set(
|
|
95
|
+
branchesData
|
|
96
|
+
.filter((b) => b.type === "remote")
|
|
97
|
+
.map((b) => b.name.replace(/^origin\//, "")),
|
|
98
|
+
);
|
|
99
|
+
|
|
68
100
|
// Store worktrees separately
|
|
69
101
|
setWorktrees(worktreesData);
|
|
70
102
|
|
|
@@ -81,9 +113,18 @@ export function useGitData(options?: UseGitDataOptions): UseGitDataResult {
|
|
|
81
113
|
worktreeMap.set(worktree.branch, uiWorktreeInfo);
|
|
82
114
|
}
|
|
83
115
|
|
|
116
|
+
// リモートブランチの重複除去(ローカルに同名がある場合は除外)
|
|
117
|
+
const filteredBranches = branchesData.filter((branch) => {
|
|
118
|
+
if (branch.type === "remote") {
|
|
119
|
+
const remoteBranchBaseName = branch.name.replace(/^origin\//, "");
|
|
120
|
+
return !localBranchNames.has(remoteBranchBaseName);
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
});
|
|
124
|
+
|
|
84
125
|
// Attach worktree info and check unpushed/PR status for local branches
|
|
85
126
|
const enrichedBranches = await Promise.all(
|
|
86
|
-
|
|
127
|
+
filteredBranches.map(async (branch) => {
|
|
87
128
|
const worktreeInfo = worktreeMap.get(branch.name);
|
|
88
129
|
let hasUnpushed = false;
|
|
89
130
|
let prInfo = null;
|
|
@@ -110,6 +151,20 @@ export function useGitData(options?: UseGitDataOptions): UseGitDataResult {
|
|
|
110
151
|
}
|
|
111
152
|
}
|
|
112
153
|
|
|
154
|
+
// upstream/divergence/hasRemoteCounterpart情報を付加
|
|
155
|
+
const upstream =
|
|
156
|
+
branch.type === "local"
|
|
157
|
+
? (upstreamMap.get(branch.name) ?? null)
|
|
158
|
+
: null;
|
|
159
|
+
const divergence =
|
|
160
|
+
branch.type === "local"
|
|
161
|
+
? (divergenceMap.get(branch.name) ?? null)
|
|
162
|
+
: null;
|
|
163
|
+
const hasRemoteCounterpart =
|
|
164
|
+
branch.type === "local"
|
|
165
|
+
? remoteBranchNames.has(branch.name)
|
|
166
|
+
: false;
|
|
167
|
+
|
|
113
168
|
return {
|
|
114
169
|
...branch,
|
|
115
170
|
...(worktreeInfo ? { worktree: worktreeInfo } : {}),
|
|
@@ -128,6 +183,9 @@ export function useGitData(options?: UseGitDataOptions): UseGitDataResult {
|
|
|
128
183
|
...(lastToolUsageMap.get(branch.name)
|
|
129
184
|
? { lastToolUsage: lastToolUsageMap.get(branch.name) ?? null }
|
|
130
185
|
: {}),
|
|
186
|
+
...(upstream !== null ? { upstream } : {}),
|
|
187
|
+
...(divergence !== null ? { divergence } : {}),
|
|
188
|
+
...(hasRemoteCounterpart ? { hasRemoteCounterpart } : {}),
|
|
131
189
|
};
|
|
132
190
|
}),
|
|
133
191
|
);
|
|
@@ -11,8 +11,9 @@ describe("BranchActionSelectorScreen", () => {
|
|
|
11
11
|
beforeEach(() => {
|
|
12
12
|
// Setup happy-dom
|
|
13
13
|
const window = new Window();
|
|
14
|
-
globalThis.window = window as
|
|
15
|
-
globalThis.document =
|
|
14
|
+
globalThis.window = window as unknown as typeof globalThis.window;
|
|
15
|
+
globalThis.document =
|
|
16
|
+
window.document as unknown as typeof globalThis.document;
|
|
16
17
|
});
|
|
17
18
|
|
|
18
19
|
it("should render the screen", () => {
|