@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.
Files changed (96) hide show
  1. package/dist/cli/ui/components/App.d.ts.map +1 -1
  2. package/dist/cli/ui/components/App.js +5 -47
  3. package/dist/cli/ui/components/App.js.map +1 -1
  4. package/dist/cli/ui/components/common/Input.d.ts +1 -1
  5. package/dist/cli/ui/components/screens/BranchListScreen.d.ts +1 -2
  6. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  7. package/dist/cli/ui/components/screens/BranchListScreen.js +19 -14
  8. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  9. package/dist/cli/ui/components/screens/ModelSelectorScreen.d.ts.map +1 -1
  10. package/dist/cli/ui/components/screens/ModelSelectorScreen.js +6 -6
  11. package/dist/cli/ui/components/screens/ModelSelectorScreen.js.map +1 -1
  12. package/dist/cli/ui/components/screens/PRCleanupScreen.d.ts.map +1 -1
  13. package/dist/cli/ui/components/screens/PRCleanupScreen.js +0 -3
  14. package/dist/cli/ui/components/screens/PRCleanupScreen.js.map +1 -1
  15. package/dist/cli/ui/hooks/useGitData.d.ts.map +1 -1
  16. package/dist/cli/ui/hooks/useGitData.js +43 -2
  17. package/dist/cli/ui/hooks/useGitData.js.map +1 -1
  18. package/dist/cli/ui/types.d.ts +16 -4
  19. package/dist/cli/ui/types.d.ts.map +1 -1
  20. package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
  21. package/dist/cli/ui/utils/branchFormatter.js +124 -15
  22. package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
  23. package/dist/cli/ui/utils/modelOptions.d.ts.map +1 -1
  24. package/dist/cli/ui/utils/modelOptions.js.map +1 -1
  25. package/dist/client/assets/{index-CNWntAlF.js → index-Dl798X5w.js} +1 -1
  26. package/dist/client/index.html +1 -1
  27. package/dist/config/index.d.ts.map +1 -1
  28. package/dist/config/index.js.map +1 -1
  29. package/dist/git.d.ts +6 -0
  30. package/dist/git.d.ts.map +1 -1
  31. package/dist/git.js +40 -5
  32. package/dist/git.js.map +1 -1
  33. package/dist/web/client/src/components/BranchGraph.js +1 -1
  34. package/dist/web/client/src/components/BranchGraph.js.map +1 -1
  35. package/dist/web/client/src/pages/BranchDetailPage.d.ts.map +1 -1
  36. package/dist/web/client/src/pages/BranchDetailPage.js +8 -3
  37. package/dist/web/client/src/pages/BranchDetailPage.js.map +1 -1
  38. package/dist/web/server/routes/sessions.d.ts.map +1 -1
  39. package/dist/web/server/routes/sessions.js +4 -2
  40. package/dist/web/server/routes/sessions.js.map +1 -1
  41. package/dist/worktree.d.ts.map +1 -1
  42. package/dist/worktree.js +31 -44
  43. package/dist/worktree.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/cli/ui/__tests__/acceptance/navigation.acceptance.test.tsx +9 -17
  46. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +14 -20
  47. package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +14 -44
  48. package/src/cli/ui/__tests__/components/App.test.tsx +8 -15
  49. package/src/cli/ui/__tests__/components/ModelSelectorScreen.initial.test.tsx +12 -5
  50. package/src/cli/ui/__tests__/components/common/Confirm.test.tsx +3 -2
  51. package/src/cli/ui/__tests__/components/common/ErrorBoundary.test.tsx +3 -2
  52. package/src/cli/ui/__tests__/components/common/Input.test.tsx +3 -2
  53. package/src/cli/ui/__tests__/components/common/LoadingIndicator.test.tsx +15 -14
  54. package/src/cli/ui/__tests__/components/common/Select.memo.test.tsx +3 -3
  55. package/src/cli/ui/__tests__/components/common/Select.test.tsx +1 -4
  56. package/src/cli/ui/__tests__/components/parts/Footer.test.tsx +3 -2
  57. package/src/cli/ui/__tests__/components/parts/Header.test.tsx +3 -2
  58. package/src/cli/ui/__tests__/components/parts/ScrollableList.test.tsx +3 -2
  59. package/src/cli/ui/__tests__/components/parts/Stats.test.tsx +3 -2
  60. package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +3 -2
  61. package/src/cli/ui/__tests__/components/screens/BranchCreatorScreen.test.tsx +3 -2
  62. package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +31 -41
  63. package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +3 -2
  64. package/src/cli/ui/__tests__/components/screens/PRCleanupScreen.test.tsx +3 -2
  65. package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +3 -2
  66. package/src/cli/ui/__tests__/hooks/useScreenState.test.ts +18 -17
  67. package/src/cli/ui/__tests__/hooks/useTerminalSize.test.ts +3 -2
  68. package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +5 -12
  69. package/src/cli/ui/__tests__/integration/navigation.test.tsx +15 -10
  70. package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx +3 -2
  71. package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +0 -4
  72. package/src/cli/ui/__tests__/performance/useMemoOptimization.test.tsx +3 -5
  73. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +24 -22
  74. package/src/cli/ui/components/App.tsx +3 -74
  75. package/src/cli/ui/components/common/Input.tsx +1 -1
  76. package/src/cli/ui/components/screens/BranchListScreen.tsx +32 -22
  77. package/src/cli/ui/components/screens/ModelSelectorScreen.tsx +46 -49
  78. package/src/cli/ui/components/screens/PRCleanupScreen.tsx +0 -3
  79. package/src/cli/ui/hooks/useGitData.ts +59 -1
  80. package/src/cli/ui/screens/__tests__/BranchActionSelectorScreen.test.tsx +3 -2
  81. package/src/cli/ui/types.ts +24 -5
  82. package/src/cli/ui/utils/branchFormatter.ts +123 -15
  83. package/src/cli/ui/utils/modelOptions.test.ts +4 -6
  84. package/src/cli/ui/utils/modelOptions.ts +1 -2
  85. package/src/config/index.ts +2 -1
  86. package/src/git.ts +56 -16
  87. package/src/web/client/src/components/BranchGraph.tsx +1 -1
  88. package/src/web/client/src/pages/BranchDetailPage.tsx +8 -3
  89. package/src/web/server/routes/sessions.ts +12 -5
  90. package/src/worktree.ts +31 -59
  91. package/dist/cli/ui/components/screens/WorktreeManagerScreen.d.ts +0 -20
  92. package/dist/cli/ui/components/screens/WorktreeManagerScreen.d.ts.map +0 -1
  93. package/dist/cli/ui/components/screens/WorktreeManagerScreen.js +0 -65
  94. package/dist/cli/ui/components/screens/WorktreeManagerScreen.js.map +0 -1
  95. package/src/cli/ui/__tests__/components/screens/WorktreeManagerScreen.test.tsx +0 -151
  96. 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 any;
67
- globalThis.document = window.document as any;
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(""); // current icon
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(""); // not current
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
- const localNameIndex = localResult.label.indexOf(localResult.name);
161
- const remoteNameIndex = remoteResult.label.indexOf(remoteResult.name);
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
- expect(localNameIndex).toBeGreaterThan(0);
164
- expect(localNameIndex).toBe(remoteNameIndex);
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
- // Four icon columns should occupy exactly 8 columns (4 * COLUMN_WIDTH)
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(8);
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("✏️"); // changes icon
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("⬆️"); // unpushed icon
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("🔀"); // open PR icon
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
- if (target.hasRemoteBranch && target.reasons?.includes("merged-pr")) {
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', 'm' while typing
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
- "": 1,
31
- "✏️": 1,
32
- "🔀": 1,
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/m/f) using blockKeys prop
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 === "m" && onNavigate) {
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(0, maxLeftDisplayWidth - staticPrefixWidth);
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", "m", "f"]} // Block shortcuts while typing
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
- <Select
413
- items={filteredBranches}
414
- onSelect={onSelect}
415
- limit={limit}
416
- disabled={Boolean(cleanupUI?.inputLocked)}
417
- renderIndicator={() => null}
418
- renderItem={renderBranchRow}
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
- return inferenceOptions.map((level) => {
112
- if (selectedModel?.id === "gpt-5.1-codex-max") {
113
- if (level === "low") {
114
- return {
115
- label: "Low",
116
- value: level,
117
- hint: "Fast responses with lighter reasoning",
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
- return {
145
- label: INFERENCE_LABELS[level],
146
- value: level,
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(selectedModel ?? undefined);
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> {item.label}</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: "No model selection required. Press Enter to continue.",
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> {item.label}</Text>
302
+ <Text> {item.label}</Text>
306
303
  )}
307
304
  {"hint" in item && item.hint ? (
308
- <Text color="gray"> {item.hint}</Text>
305
+ <Text color="gray"> {item.hint}</Text>
309
306
  ) : null}
310
307
  </Box>
311
308
  )}
@@ -58,9 +58,6 @@ export function PRCleanupScreen({
58
58
  } else {
59
59
  flags.push("branch");
60
60
  }
61
- if (target.reasons?.includes("merged-pr")) {
62
- flags.push("merged");
63
- }
64
61
  if (target.reasons?.includes("no-diff-with-base")) {
65
62
  flags.push("base");
66
63
  }
@@ -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
- branchesData.map(async (branch) => {
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 any;
15
- globalThis.document = window.document as any;
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", () => {