@akiojin/gwt 2.9.1 → 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 (98) hide show
  1. package/README.md +6 -0
  2. package/dist/cli/ui/components/App.d.ts.map +1 -1
  3. package/dist/cli/ui/components/App.js +4 -37
  4. package/dist/cli/ui/components/App.js.map +1 -1
  5. package/dist/cli/ui/components/common/Input.d.ts +1 -1
  6. package/dist/cli/ui/components/screens/BranchListScreen.d.ts +1 -2
  7. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  8. package/dist/cli/ui/components/screens/BranchListScreen.js +19 -14
  9. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  10. package/dist/cli/ui/components/screens/ModelSelectorScreen.d.ts.map +1 -1
  11. package/dist/cli/ui/components/screens/ModelSelectorScreen.js +6 -6
  12. package/dist/cli/ui/components/screens/ModelSelectorScreen.js.map +1 -1
  13. package/dist/cli/ui/components/screens/PRCleanupScreen.d.ts.map +1 -1
  14. package/dist/cli/ui/components/screens/PRCleanupScreen.js +0 -3
  15. package/dist/cli/ui/components/screens/PRCleanupScreen.js.map +1 -1
  16. package/dist/cli/ui/hooks/useGitData.d.ts.map +1 -1
  17. package/dist/cli/ui/hooks/useGitData.js +43 -2
  18. package/dist/cli/ui/hooks/useGitData.js.map +1 -1
  19. package/dist/cli/ui/types.d.ts +16 -4
  20. package/dist/cli/ui/types.d.ts.map +1 -1
  21. package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
  22. package/dist/cli/ui/utils/branchFormatter.js +124 -15
  23. package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
  24. package/dist/cli/ui/utils/modelOptions.d.ts.map +1 -1
  25. package/dist/cli/ui/utils/modelOptions.js.map +1 -1
  26. package/dist/client/assets/{index-CNWntAlF.js → index-Dl798X5w.js} +1 -1
  27. package/dist/client/index.html +1 -1
  28. package/dist/config/index.d.ts.map +1 -1
  29. package/dist/config/index.js.map +1 -1
  30. package/dist/git.d.ts +6 -0
  31. package/dist/git.d.ts.map +1 -1
  32. package/dist/git.js +40 -5
  33. package/dist/git.js.map +1 -1
  34. package/dist/web/client/src/components/BranchGraph.js +1 -1
  35. package/dist/web/client/src/components/BranchGraph.js.map +1 -1
  36. package/dist/web/client/src/pages/BranchDetailPage.d.ts.map +1 -1
  37. package/dist/web/client/src/pages/BranchDetailPage.js +8 -3
  38. package/dist/web/client/src/pages/BranchDetailPage.js.map +1 -1
  39. package/dist/web/server/routes/sessions.d.ts.map +1 -1
  40. package/dist/web/server/routes/sessions.js +4 -2
  41. package/dist/web/server/routes/sessions.js.map +1 -1
  42. package/dist/worktree.d.ts.map +1 -1
  43. package/dist/worktree.js +69 -62
  44. package/dist/worktree.js.map +1 -1
  45. package/package.json +1 -1
  46. package/src/cli/ui/__tests__/acceptance/navigation.acceptance.test.tsx +9 -17
  47. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +77 -83
  48. package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +107 -160
  49. package/src/cli/ui/__tests__/components/App.test.tsx +108 -84
  50. package/src/cli/ui/__tests__/components/ModelSelectorScreen.initial.test.tsx +12 -5
  51. package/src/cli/ui/__tests__/components/common/Confirm.test.tsx +3 -2
  52. package/src/cli/ui/__tests__/components/common/ErrorBoundary.test.tsx +3 -2
  53. package/src/cli/ui/__tests__/components/common/Input.test.tsx +3 -2
  54. package/src/cli/ui/__tests__/components/common/LoadingIndicator.test.tsx +15 -14
  55. package/src/cli/ui/__tests__/components/common/Select.memo.test.tsx +3 -3
  56. package/src/cli/ui/__tests__/components/common/Select.test.tsx +1 -4
  57. package/src/cli/ui/__tests__/components/parts/Footer.test.tsx +3 -2
  58. package/src/cli/ui/__tests__/components/parts/Header.test.tsx +3 -2
  59. package/src/cli/ui/__tests__/components/parts/ScrollableList.test.tsx +3 -2
  60. package/src/cli/ui/__tests__/components/parts/Stats.test.tsx +3 -2
  61. package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +3 -2
  62. package/src/cli/ui/__tests__/components/screens/BranchCreatorScreen.test.tsx +3 -2
  63. package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +31 -41
  64. package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +3 -2
  65. package/src/cli/ui/__tests__/components/screens/PRCleanupScreen.test.tsx +3 -2
  66. package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +3 -2
  67. package/src/cli/ui/__tests__/hooks/useScreenState.test.ts +18 -17
  68. package/src/cli/ui/__tests__/hooks/useTerminalSize.test.ts +3 -2
  69. package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +61 -41
  70. package/src/cli/ui/__tests__/integration/navigation.test.tsx +15 -10
  71. package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx +3 -2
  72. package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +0 -4
  73. package/src/cli/ui/__tests__/performance/useMemoOptimization.test.tsx +3 -5
  74. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +24 -22
  75. package/src/cli/ui/components/App.tsx +5 -63
  76. package/src/cli/ui/components/common/Input.tsx +1 -1
  77. package/src/cli/ui/components/screens/BranchListScreen.tsx +32 -22
  78. package/src/cli/ui/components/screens/ModelSelectorScreen.tsx +46 -49
  79. package/src/cli/ui/components/screens/PRCleanupScreen.tsx +0 -3
  80. package/src/cli/ui/hooks/useGitData.ts +59 -1
  81. package/src/cli/ui/screens/__tests__/BranchActionSelectorScreen.test.tsx +3 -2
  82. package/src/cli/ui/types.ts +24 -5
  83. package/src/cli/ui/utils/branchFormatter.ts +123 -15
  84. package/src/cli/ui/utils/modelOptions.test.ts +4 -6
  85. package/src/cli/ui/utils/modelOptions.ts +1 -2
  86. package/src/config/index.ts +2 -1
  87. package/src/git.ts +56 -16
  88. package/src/index.test.ts +1 -1
  89. package/src/web/client/src/components/BranchGraph.tsx +1 -1
  90. package/src/web/client/src/pages/BranchDetailPage.tsx +8 -3
  91. package/src/web/server/routes/sessions.ts +12 -5
  92. package/src/worktree.ts +80 -91
  93. package/dist/cli/ui/components/screens/WorktreeManagerScreen.d.ts +0 -20
  94. package/dist/cli/ui/components/screens/WorktreeManagerScreen.d.ts.map +0 -1
  95. package/dist/cli/ui/components/screens/WorktreeManagerScreen.js +0 -65
  96. package/dist/cli/ui/components/screens/WorktreeManagerScreen.js.map +0 -1
  97. package/src/cli/ui/__tests__/components/screens/WorktreeManagerScreen.test.tsx +0 -151
  98. 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
 
@@ -100,11 +116,9 @@ async function listWorktrees(): Promise<WorktreeInfo[]> {
100
116
  try {
101
117
  const { getRepositoryRoot } = await import("./git.js");
102
118
  const repoRoot = await getRepositoryRoot();
103
- const { stdout } = await execa(
104
- "git",
105
- ["worktree", "list", "--porcelain"],
106
- { cwd: repoRoot },
107
- );
119
+ const { stdout } = await execa("git", ["worktree", "list", "--porcelain"], {
120
+ cwd: repoRoot,
121
+ });
108
122
  const worktrees: WorktreeInfo[] = [];
109
123
  const lines = stdout.split("\n");
110
124
 
@@ -416,15 +430,13 @@ async function getWorktreesWithPRStatus(): Promise<WorktreeWithPR[]> {
416
430
  }
417
431
 
418
432
  /**
419
- * worktreeに存在しないローカルブランチの中でマージ済みPRに関連するクリーンアップ候補を取得
433
+ * worktreeに存在しないローカルブランチのクリーンアップ候補を取得
420
434
  * @returns {Promise<CleanupTarget[]>} クリーンアップ候補の配列
421
435
  */
422
436
  async function getOrphanedLocalBranches({
423
- mergedPRs,
424
437
  baseBranch,
425
438
  repoRoot,
426
439
  }: {
427
- mergedPRs: MergedPullRequest[];
428
440
  baseBranch: string;
429
441
  repoRoot: string;
430
442
  }): Promise<CleanupTarget[]> {
@@ -469,7 +481,6 @@ async function getOrphanedLocalBranches({
469
481
 
470
482
  // worktreeに存在しないローカルブランチのみ対象
471
483
  if (!worktreeBranches.has(localBranch.name)) {
472
- const mergedPR = findMatchingPR(localBranch.name, mergedPRs);
473
484
  let hasUnpushed = false;
474
485
  try {
475
486
  hasUnpushed = await hasUnpushedCommitsInRepo(
@@ -482,42 +493,43 @@ async function getOrphanedLocalBranches({
482
493
 
483
494
  const reasons: CleanupReason[] = [];
484
495
 
485
- if (mergedPR) {
486
- reasons.push("merged-pr");
487
- }
496
+ const upstreamBranch = await getUpstreamBranch(localBranch.name);
497
+ const comparisonBase = upstreamBranch ?? baseBranch;
488
498
 
489
- if (!hasUnpushed) {
490
- const hasUniqueCommits = await branchHasUniqueCommitsComparedToBase(
491
- localBranch.name,
492
- baseBranch,
493
- repoRoot,
494
- );
499
+ const hasUniqueCommits = await branchHasUniqueCommitsComparedToBase(
500
+ localBranch.name,
501
+ comparisonBase,
502
+ repoRoot,
503
+ );
495
504
 
496
- if (!hasUniqueCommits) {
497
- reasons.push("no-diff-with-base");
498
- }
505
+ if (!hasUniqueCommits) {
506
+ reasons.push("no-diff-with-base");
499
507
  }
500
508
 
501
509
  if (process.env.DEBUG_CLEANUP) {
502
510
  console.log(
503
511
  chalk.gray(
504
- `Debug: Checking orphaned branch ${localBranch.name} -> PR: ${mergedPR ? "MATCH" : "NO MATCH"}, reasons: ${reasons.join(", ")}`,
512
+ `Debug: Checking orphaned branch ${localBranch.name} -> reasons: ${reasons.join(", ")}`,
505
513
  ),
506
514
  );
507
515
  }
508
516
 
509
- if (reasons.length > 0) {
510
- let hasRemoteBranch = false;
511
- try {
512
- hasRemoteBranch = await checkRemoteBranchExists(localBranch.name);
513
- } catch {
514
- hasRemoteBranch = false;
515
- }
517
+ let hasRemoteBranch = false;
518
+ try {
519
+ hasRemoteBranch = await checkRemoteBranchExists(localBranch.name);
520
+ } catch {
521
+ hasRemoteBranch = false;
522
+ }
516
523
 
524
+ if (!hasUnpushed && hasRemoteBranch && hasUniqueCommits) {
525
+ reasons.push("remote-synced");
526
+ }
527
+
528
+ if (reasons.length > 0) {
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,26 +594,25 @@ 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[] = [];
629
602
 
630
- if (mergedPR) {
631
- cleanupReasons.push("merged-pr");
632
- }
633
-
634
603
  // worktreeパスの存在を確認
635
604
  const fs = await import("node:fs");
636
- const isAccessible = fs.existsSync(worktree.worktreePath);
605
+ // Some test environments mock node:fs without existsSync on the module root.
606
+ const existsSync =
607
+ typeof fs.existsSync === "function"
608
+ ? fs.existsSync
609
+ : typeof (fs as { default?: { existsSync?: unknown } }).default
610
+ ?.existsSync === "function"
611
+ ? (fs as { default: { existsSync: (p: string) => boolean } }).default
612
+ .existsSync
613
+ : null;
614
+
615
+ const isAccessible = existsSync ? existsSync(worktree.worktreePath) : false;
637
616
 
638
617
  let hasUncommitted = false;
639
618
  let hasUnpushed = false;
@@ -657,16 +636,28 @@ export async function getMergedPRWorktrees(): Promise<CleanupTarget[]> {
657
636
  }
658
637
  }
659
638
 
660
- if (!hasUnpushed) {
661
- const hasUniqueCommits = await branchHasUniqueCommitsComparedToBase(
662
- worktree.branch,
663
- baseBranch,
664
- repoRoot,
665
- );
639
+ let hasRemoteBranch = false;
640
+ try {
641
+ hasRemoteBranch = await checkRemoteBranchExists(worktree.branch);
642
+ } catch {
643
+ hasRemoteBranch = false;
644
+ }
666
645
 
667
- if (!hasUniqueCommits) {
668
- cleanupReasons.push("no-diff-with-base");
669
- }
646
+ const upstreamBranch = await getUpstreamBranch(worktree.branch);
647
+ const comparisonBase = upstreamBranch ?? baseBranch;
648
+
649
+ const hasUniqueCommits = await branchHasUniqueCommitsComparedToBase(
650
+ worktree.branch,
651
+ comparisonBase,
652
+ repoRoot,
653
+ );
654
+
655
+ // 差分がない場合はベース同等としてクリーンアップ候補
656
+ if (!hasUniqueCommits) {
657
+ cleanupReasons.push("no-diff-with-base");
658
+ } else if (!hasUncommitted && !hasUnpushed && hasRemoteBranch) {
659
+ // 未マージでも、ローカルに未コミット/未プッシュがなくリモートが最新ならローカルのみクリーンアップ許可
660
+ cleanupReasons.push("remote-synced");
670
661
  }
671
662
 
672
663
  if (process.env.DEBUG_CLEANUP) {
@@ -681,12 +672,10 @@ export async function getMergedPRWorktrees(): Promise<CleanupTarget[]> {
681
672
  continue;
682
673
  }
683
674
 
684
- const hasRemoteBranch = await checkRemoteBranchExists(worktree.branch);
685
-
686
675
  const target: CleanupTarget = {
687
676
  worktreePath: worktree.worktreePath,
688
677
  branch: worktree.branch,
689
- pullRequest: mergedPR ?? null,
678
+ pullRequest: null,
690
679
  hasUncommittedChanges: hasUncommitted,
691
680
  hasUnpushedCommits: hasUnpushed,
692
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
- }