@akiojin/gwt 2.10.0 → 2.11.1

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 -28
  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 +33 -43
  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 -23
  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 -28
  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
package/src/worktree.ts CHANGED
@@ -6,10 +6,9 @@ import {
6
6
  WorktreeConfig,
7
7
  WorktreeWithPR,
8
8
  CleanupTarget,
9
- MergedPullRequest,
10
9
  CleanupReason,
11
10
  } from "./cli/ui/types.js";
12
- import { getPullRequestByBranch, getMergedPullRequests } from "./github.js";
11
+ import { getPullRequestByBranch } from "./github.js";
13
12
  import {
14
13
  hasUncommittedChanges,
15
14
  hasUnpushedCommits,
@@ -27,6 +26,23 @@ import { getConfig } from "./config/index.js";
27
26
  import { GIT_CONFIG } from "./config/constants.js";
28
27
  import { startSpinner } from "./utils/spinner.js";
29
28
 
29
+ async function getUpstreamBranch(branch: string): Promise<string | null> {
30
+ try {
31
+ const result = await execa("git", [
32
+ "rev-parse",
33
+ "--abbrev-ref",
34
+ `${branch}@{upstream}`,
35
+ ]);
36
+ const stdout =
37
+ typeof (result as { stdout?: unknown })?.stdout === "string"
38
+ ? (result as { stdout: string }).stdout.trim()
39
+ : "";
40
+ return stdout.length ? stdout : null;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
30
46
  // Re-export WorktreeConfig for external use
31
47
  export type { WorktreeConfig };
32
48
 
@@ -414,15 +430,13 @@ async function getWorktreesWithPRStatus(): Promise<WorktreeWithPR[]> {
414
430
  }
415
431
 
416
432
  /**
417
- * worktreeに存在しないローカルブランチの中でマージ済みPRに関連するクリーンアップ候補を取得
433
+ * worktreeに存在しないローカルブランチのクリーンアップ候補を取得
418
434
  * @returns {Promise<CleanupTarget[]>} クリーンアップ候補の配列
419
435
  */
420
436
  async function getOrphanedLocalBranches({
421
- mergedPRs,
422
437
  baseBranch,
423
438
  repoRoot,
424
439
  }: {
425
- mergedPRs: MergedPullRequest[];
426
440
  baseBranch: string;
427
441
  repoRoot: string;
428
442
  }): Promise<CleanupTarget[]> {
@@ -467,7 +481,6 @@ async function getOrphanedLocalBranches({
467
481
 
468
482
  // worktreeに存在しないローカルブランチのみ対象
469
483
  if (!worktreeBranches.has(localBranch.name)) {
470
- const mergedPR = findMatchingPR(localBranch.name, mergedPRs);
471
484
  let hasUnpushed = false;
472
485
  try {
473
486
  hasUnpushed = await hasUnpushedCommitsInRepo(
@@ -480,13 +493,12 @@ async function getOrphanedLocalBranches({
480
493
 
481
494
  const reasons: CleanupReason[] = [];
482
495
 
483
- if (mergedPR) {
484
- reasons.push("merged-pr");
485
- }
496
+ const upstreamBranch = await getUpstreamBranch(localBranch.name);
497
+ const comparisonBase = upstreamBranch ?? baseBranch;
486
498
 
487
499
  const hasUniqueCommits = await branchHasUniqueCommitsComparedToBase(
488
500
  localBranch.name,
489
- baseBranch,
501
+ comparisonBase,
490
502
  repoRoot,
491
503
  );
492
504
 
@@ -497,7 +509,7 @@ async function getOrphanedLocalBranches({
497
509
  if (process.env.DEBUG_CLEANUP) {
498
510
  console.log(
499
511
  chalk.gray(
500
- `Debug: Checking orphaned branch ${localBranch.name} -> PR: ${mergedPR ? "MATCH" : "NO MATCH"}, reasons: ${reasons.join(", ")}`,
512
+ `Debug: Checking orphaned branch ${localBranch.name} -> reasons: ${reasons.join(", ")}`,
501
513
  ),
502
514
  );
503
515
  }
@@ -517,7 +529,7 @@ async function getOrphanedLocalBranches({
517
529
  cleanupTargets.push({
518
530
  worktreePath: null, // worktreeは存在しない
519
531
  branch: localBranch.name,
520
- pullRequest: mergedPR ?? null,
532
+ pullRequest: null,
521
533
  hasUncommittedChanges: false, // worktreeが存在しないため常にfalse
522
534
  hasUnpushedCommits: hasUnpushed,
523
535
  cleanupType: "branch-only",
@@ -546,49 +558,19 @@ async function getOrphanedLocalBranches({
546
558
  }
547
559
  }
548
560
 
549
- function normalizeBranchName(branchName: string): string {
550
- return branchName
551
- .replace(/^origin\//, "")
552
- .replace(/^refs\/heads\//, "")
553
- .replace(/^refs\/remotes\/origin\//, "")
554
- .trim();
555
- }
556
-
557
- function findMatchingPR(
558
- worktreeBranch: string,
559
- mergedPRs: MergedPullRequest[],
560
- ): MergedPullRequest | null {
561
- const normalizedWorktreeBranch = normalizeBranchName(worktreeBranch);
562
-
563
- for (const pr of mergedPRs) {
564
- const normalizedPRBranch = normalizeBranchName(pr.branch);
565
-
566
- if (normalizedWorktreeBranch === normalizedPRBranch) {
567
- return pr;
568
- }
569
- }
570
-
571
- return null;
572
- }
573
-
574
561
  /**
575
562
  * マージ済みPRに関連するworktreeおよびローカルブランチのクリーンアップ候補を取得
576
563
  * @returns {Promise<CleanupTarget[]>} クリーンアップ候補の配列
577
564
  */
578
565
  export async function getMergedPRWorktrees(): Promise<CleanupTarget[]> {
579
- const [config, repoRoot] = await Promise.all([
566
+ const [config, repoRoot, worktreesWithPR] = await Promise.all([
580
567
  getConfig(),
581
568
  getRepositoryRoot(),
569
+ getWorktreesWithPRStatus(),
582
570
  ]);
583
571
  const baseBranch = config.defaultBaseBranch || GIT_CONFIG.DEFAULT_BASE_BRANCH;
584
572
 
585
- // 並列実行で高速化 - worktreeとマージ済みPRの両方を取得
586
- const [mergedPRs, worktreesWithPR] = await Promise.all([
587
- getMergedPullRequests(),
588
- getWorktreesWithPRStatus(),
589
- ]);
590
573
  const orphanedBranches = await getOrphanedLocalBranches({
591
- mergedPRs,
592
574
  baseBranch,
593
575
  repoRoot,
594
576
  });
@@ -599,8 +581,6 @@ export async function getMergedPRWorktrees(): Promise<CleanupTarget[]> {
599
581
  worktreesWithPR.forEach((w) =>
600
582
  console.log(` ${w.branch} -> ${w.worktreePath}`),
601
583
  );
602
- console.log(chalk.cyan("Debug: Merged PRs:"));
603
- mergedPRs.forEach((pr) => console.log(` ${pr.branch} (PR #${pr.number})`));
604
584
  }
605
585
 
606
586
  for (const worktree of worktreesWithPR) {
@@ -614,15 +594,8 @@ export async function getMergedPRWorktrees(): Promise<CleanupTarget[]> {
614
594
  continue;
615
595
  }
616
596
 
617
- const mergedPR = findMatchingPR(worktree.branch, mergedPRs);
618
-
619
597
  if (process.env.DEBUG_CLEANUP) {
620
- const normalizedWorktree = normalizeBranchName(worktree.branch);
621
- console.log(
622
- chalk.gray(
623
- `Debug: Checking worktree ${worktree.branch} (normalized: ${normalizedWorktree}) -> ${mergedPR ? "MATCH" : "NO MATCH"}`,
624
- ),
625
- );
598
+ console.log(chalk.gray(`Debug: Checking worktree ${worktree.branch}`));
626
599
  }
627
600
 
628
601
  const cleanupReasons: CleanupReason[] = [];
@@ -670,13 +643,12 @@ export async function getMergedPRWorktrees(): Promise<CleanupTarget[]> {
670
643
  hasRemoteBranch = false;
671
644
  }
672
645
 
673
- if (mergedPR) {
674
- cleanupReasons.push("merged-pr");
675
- }
646
+ const upstreamBranch = await getUpstreamBranch(worktree.branch);
647
+ const comparisonBase = upstreamBranch ?? baseBranch;
676
648
 
677
649
  const hasUniqueCommits = await branchHasUniqueCommitsComparedToBase(
678
650
  worktree.branch,
679
- baseBranch,
651
+ comparisonBase,
680
652
  repoRoot,
681
653
  );
682
654
 
@@ -703,7 +675,7 @@ export async function getMergedPRWorktrees(): Promise<CleanupTarget[]> {
703
675
  const target: CleanupTarget = {
704
676
  worktreePath: worktree.worktreePath,
705
677
  branch: worktree.branch,
706
- pullRequest: mergedPR ?? null,
678
+ pullRequest: null,
707
679
  hasUncommittedChanges: hasUncommitted,
708
680
  hasUnpushedCommits: hasUnpushed,
709
681
  cleanupType: "worktree-and-branch",
@@ -1,20 +0,0 @@
1
- import React from "react";
2
- export interface WorktreeItem {
3
- branch: string;
4
- path: string;
5
- isAccessible: boolean;
6
- label?: string;
7
- value?: string;
8
- }
9
- export interface WorktreeManagerScreenProps {
10
- worktrees: WorktreeItem[];
11
- onBack: () => void;
12
- onSelect: (worktree: WorktreeItem) => void;
13
- version?: string | null;
14
- }
15
- /**
16
- * WorktreeManagerScreen - Screen for managing worktrees
17
- * Layout: Header + Stats + Worktree List + Footer
18
- */
19
- export declare function WorktreeManagerScreen({ worktrees, onBack, onSelect, version, }: WorktreeManagerScreenProps): React.JSX.Element;
20
- //# sourceMappingURL=WorktreeManagerScreen.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"WorktreeManagerScreen.d.ts","sourceRoot":"","sources":["../../../../../src/cli/ui/components/screens/WorktreeManagerScreen.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAO1B,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,OAAO,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,YAAY,EAAE,CAAC;IAC1B,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,QAAQ,EAAE,CAAC,QAAQ,EAAE,YAAY,KAAK,IAAI,CAAC;IAC3C,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,SAAS,EACT,MAAM,EACN,QAAQ,EACR,OAAO,GACR,EAAE,0BAA0B,qBAqF5B"}
@@ -1,65 +0,0 @@
1
- import React from "react";
2
- import { Box, Text, useInput } from "ink";
3
- import { Header } from "../parts/Header.js";
4
- import { Footer } from "../parts/Footer.js";
5
- import { Select } from "../common/Select.js";
6
- import { useTerminalSize } from "../../hooks/useTerminalSize.js";
7
- /**
8
- * WorktreeManagerScreen - Screen for managing worktrees
9
- * Layout: Header + Stats + Worktree List + Footer
10
- */
11
- export function WorktreeManagerScreen({ worktrees, onBack, onSelect, version, }) {
12
- const { rows } = useTerminalSize();
13
- // Handle keyboard input
14
- // Note: Select component handles Enter and arrow keys
15
- useInput((input, key) => {
16
- if (key.escape) {
17
- onBack();
18
- }
19
- });
20
- // Calculate accessible and inaccessible counts
21
- const accessibleCount = worktrees.filter((w) => w.isAccessible).length;
22
- const inaccessibleCount = worktrees.filter((w) => !w.isAccessible).length;
23
- // Format worktrees for Select component
24
- const worktreeItems = worktrees.map((wt) => ({
25
- ...wt,
26
- label: wt.isAccessible
27
- ? `${wt.branch} (${wt.path})`
28
- : `${wt.branch} (${wt.path}) [Inaccessible]`,
29
- value: wt.branch,
30
- }));
31
- // Calculate available space for worktree list
32
- const headerLines = 2;
33
- const statsLines = 1;
34
- const emptyLine = 1;
35
- const footerLines = 1;
36
- const fixedLines = headerLines + statsLines + emptyLine + footerLines;
37
- const contentHeight = rows - fixedLines;
38
- const limit = Math.max(5, contentHeight);
39
- // Footer actions
40
- const footerActions = [
41
- { key: "enter", description: "Select" },
42
- { key: "esc", description: "Back" },
43
- ];
44
- return (React.createElement(Box, { flexDirection: "column", height: rows },
45
- React.createElement(Header, { title: "Worktree Manager", titleColor: "magenta", version: version }),
46
- React.createElement(Box, { marginTop: 1 },
47
- React.createElement(Box, { flexDirection: "row" },
48
- React.createElement(Box, { marginRight: 2 },
49
- React.createElement(Text, null,
50
- "Total: ",
51
- React.createElement(Text, { bold: true }, worktrees.length))),
52
- React.createElement(Box, { marginRight: 2 },
53
- React.createElement(Text, { color: "green" },
54
- "Accessible: ",
55
- React.createElement(Text, { bold: true }, accessibleCount))),
56
- inaccessibleCount > 0 && (React.createElement(Box, null,
57
- React.createElement(Text, { color: "red" },
58
- "Inaccessible: ",
59
- React.createElement(Text, { bold: true }, inaccessibleCount)))))),
60
- React.createElement(Box, { height: 1 }),
61
- React.createElement(Box, { flexDirection: "column", flexGrow: 1 }, worktrees.length === 0 ? (React.createElement(Box, null,
62
- React.createElement(Text, { dimColor: true }, "No worktrees found"))) : (React.createElement(Select, { items: worktreeItems, onSelect: onSelect, limit: limit }))),
63
- React.createElement(Footer, { actions: footerActions })));
64
- }
65
- //# sourceMappingURL=WorktreeManagerScreen.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"WorktreeManagerScreen.js","sourceRoot":"","sources":["../../../../../src/cli/ui/components/screens/WorktreeManagerScreen.tsx"],"names":[],"mappings":"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"}
@@ -1,151 +0,0 @@
1
- /**
2
- * @vitest-environment happy-dom
3
- */
4
- import { describe, it, expect, beforeEach, vi } from "vitest";
5
- import { render } from "@testing-library/react";
6
- import React from "react";
7
- import { WorktreeManagerScreen } from "../../../components/screens/WorktreeManagerScreen.js";
8
- import { Window } from "happy-dom";
9
-
10
- describe("WorktreeManagerScreen", () => {
11
- beforeEach(() => {
12
- // Setup happy-dom
13
- const window = new Window();
14
- globalThis.window = window as any;
15
- globalThis.document = window.document as any;
16
- });
17
-
18
- const mockWorktrees = [
19
- {
20
- branch: "feature/test-1",
21
- path: "/path/to/worktree-1",
22
- isAccessible: true,
23
- },
24
- {
25
- branch: "feature/test-2",
26
- path: "/path/to/worktree-2",
27
- isAccessible: true,
28
- },
29
- ];
30
-
31
- it("should render header with title", () => {
32
- const onBack = vi.fn();
33
- const onSelect = vi.fn();
34
- const { getByText } = render(
35
- <WorktreeManagerScreen
36
- worktrees={mockWorktrees}
37
- onBack={onBack}
38
- onSelect={onSelect}
39
- />,
40
- );
41
-
42
- expect(getByText(/Worktree Manager/i)).toBeDefined();
43
- });
44
-
45
- it("should render worktree list", () => {
46
- const onBack = vi.fn();
47
- const onSelect = vi.fn();
48
- const { getByText } = render(
49
- <WorktreeManagerScreen
50
- worktrees={mockWorktrees}
51
- onBack={onBack}
52
- onSelect={onSelect}
53
- />,
54
- );
55
-
56
- expect(getByText(/feature\/test-1/)).toBeDefined();
57
- expect(getByText(/feature\/test-2/)).toBeDefined();
58
- });
59
-
60
- it("should render footer with actions", () => {
61
- const onBack = vi.fn();
62
- const onSelect = vi.fn();
63
- const { getAllByText } = render(
64
- <WorktreeManagerScreen
65
- worktrees={mockWorktrees}
66
- onBack={onBack}
67
- onSelect={onSelect}
68
- />,
69
- );
70
-
71
- expect(getAllByText(/enter/i).length).toBeGreaterThan(0);
72
- expect(getAllByText(/esc/i).length).toBeGreaterThan(0);
73
- });
74
-
75
- it("should handle empty worktree list", () => {
76
- const onBack = vi.fn();
77
- const onSelect = vi.fn();
78
- const { getByText } = render(
79
- <WorktreeManagerScreen
80
- worktrees={[]}
81
- onBack={onBack}
82
- onSelect={onSelect}
83
- />,
84
- );
85
-
86
- expect(getByText(/No worktrees found/i)).toBeDefined();
87
- });
88
-
89
- it("should display inaccessible worktrees differently", () => {
90
- const worktreesWithInaccessible = [
91
- {
92
- branch: "feature/accessible",
93
- path: "/path/accessible",
94
- isAccessible: true,
95
- },
96
- {
97
- branch: "feature/inaccessible",
98
- path: "/path/inaccessible",
99
- isAccessible: false,
100
- },
101
- ];
102
-
103
- const onBack = vi.fn();
104
- const onSelect = vi.fn();
105
- const { getByText } = render(
106
- <WorktreeManagerScreen
107
- worktrees={worktreesWithInaccessible}
108
- onBack={onBack}
109
- onSelect={onSelect}
110
- />,
111
- );
112
-
113
- expect(getByText(/feature\/accessible/)).toBeDefined();
114
- expect(getByText(/feature\/inaccessible/)).toBeDefined();
115
- });
116
-
117
- it("should use terminal height for layout calculation", () => {
118
- const originalRows = process.stdout.rows;
119
- process.stdout.rows = 30;
120
-
121
- const onBack = vi.fn();
122
- const onSelect = vi.fn();
123
- const { container } = render(
124
- <WorktreeManagerScreen
125
- worktrees={mockWorktrees}
126
- onBack={onBack}
127
- onSelect={onSelect}
128
- />,
129
- );
130
-
131
- expect(container).toBeDefined();
132
-
133
- process.stdout.rows = originalRows;
134
- });
135
-
136
- it("should display worktree count in stats", () => {
137
- const onBack = vi.fn();
138
- const onSelect = vi.fn();
139
- const { getByText, getAllByText } = render(
140
- <WorktreeManagerScreen
141
- worktrees={mockWorktrees}
142
- onBack={onBack}
143
- onSelect={onSelect}
144
- />,
145
- );
146
-
147
- // Check for worktree count
148
- expect(getByText(/Total:/i)).toBeDefined();
149
- expect(getAllByText(/2/).length).toBeGreaterThan(0);
150
- });
151
- });
@@ -1,117 +0,0 @@
1
- import React from "react";
2
- import { Box, Text, useInput } from "ink";
3
- import { Header } from "../parts/Header.js";
4
- import { Footer } from "../parts/Footer.js";
5
- import { Select } from "../common/Select.js";
6
- import { useTerminalSize } from "../../hooks/useTerminalSize.js";
7
-
8
- export interface WorktreeItem {
9
- branch: string;
10
- path: string;
11
- isAccessible: boolean;
12
- label?: string;
13
- value?: string;
14
- }
15
-
16
- export interface WorktreeManagerScreenProps {
17
- worktrees: WorktreeItem[];
18
- onBack: () => void;
19
- onSelect: (worktree: WorktreeItem) => void;
20
- version?: string | null;
21
- }
22
-
23
- /**
24
- * WorktreeManagerScreen - Screen for managing worktrees
25
- * Layout: Header + Stats + Worktree List + Footer
26
- */
27
- export function WorktreeManagerScreen({
28
- worktrees,
29
- onBack,
30
- onSelect,
31
- version,
32
- }: WorktreeManagerScreenProps) {
33
- const { rows } = useTerminalSize();
34
-
35
- // Handle keyboard input
36
- // Note: Select component handles Enter and arrow keys
37
- useInput((input, key) => {
38
- if (key.escape) {
39
- onBack();
40
- }
41
- });
42
-
43
- // Calculate accessible and inaccessible counts
44
- const accessibleCount = worktrees.filter((w) => w.isAccessible).length;
45
- const inaccessibleCount = worktrees.filter((w) => !w.isAccessible).length;
46
-
47
- // Format worktrees for Select component
48
- const worktreeItems = worktrees.map((wt) => ({
49
- ...wt,
50
- label: wt.isAccessible
51
- ? `${wt.branch} (${wt.path})`
52
- : `${wt.branch} (${wt.path}) [Inaccessible]`,
53
- value: wt.branch,
54
- }));
55
-
56
- // Calculate available space for worktree list
57
- const headerLines = 2;
58
- const statsLines = 1;
59
- const emptyLine = 1;
60
- const footerLines = 1;
61
- const fixedLines = headerLines + statsLines + emptyLine + footerLines;
62
- const contentHeight = rows - fixedLines;
63
- const limit = Math.max(5, contentHeight);
64
-
65
- // Footer actions
66
- const footerActions = [
67
- { key: "enter", description: "Select" },
68
- { key: "esc", description: "Back" },
69
- ];
70
-
71
- return (
72
- <Box flexDirection="column" height={rows}>
73
- {/* Header */}
74
- <Header title="Worktree Manager" titleColor="magenta" version={version} />
75
-
76
- {/* Stats */}
77
- <Box marginTop={1}>
78
- <Box flexDirection="row">
79
- <Box marginRight={2}>
80
- <Text>
81
- Total: <Text bold>{worktrees.length}</Text>
82
- </Text>
83
- </Box>
84
- <Box marginRight={2}>
85
- <Text color="green">
86
- Accessible: <Text bold>{accessibleCount}</Text>
87
- </Text>
88
- </Box>
89
- {inaccessibleCount > 0 && (
90
- <Box>
91
- <Text color="red">
92
- Inaccessible: <Text bold>{inaccessibleCount}</Text>
93
- </Text>
94
- </Box>
95
- )}
96
- </Box>
97
- </Box>
98
-
99
- {/* Empty line */}
100
- <Box height={1} />
101
-
102
- {/* Content */}
103
- <Box flexDirection="column" flexGrow={1}>
104
- {worktrees.length === 0 ? (
105
- <Box>
106
- <Text dimColor>No worktrees found</Text>
107
- </Box>
108
- ) : (
109
- <Select items={worktreeItems} onSelect={onSelect} limit={limit} />
110
- )}
111
- </Box>
112
-
113
- {/* Footer */}
114
- <Footer actions={footerActions} />
115
- </Box>
116
- );
117
- }