nugit-cli 0.0.1 → 0.1.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 (58) hide show
  1. package/package.json +1 -1
  2. package/src/api-client.js +10 -23
  3. package/src/github-device-flow.js +1 -1
  4. package/src/github-oauth-client-id.js +11 -0
  5. package/src/github-pr-social.js +42 -0
  6. package/src/github-rest.js +149 -6
  7. package/src/nugit-config.js +84 -0
  8. package/src/nugit-stack.js +40 -257
  9. package/src/nugit.js +104 -647
  10. package/src/review-hub/review-autoapprove.js +95 -0
  11. package/src/review-hub/review-hub-back.js +10 -0
  12. package/src/review-hub/review-hub-ink.js +169 -0
  13. package/src/review-hub/run-review-hub.js +131 -0
  14. package/src/services/repo-branches.js +151 -0
  15. package/src/services/stack-inference.js +90 -0
  16. package/src/split-view/run-split.js +14 -76
  17. package/src/split-view/split-ink.js +2 -2
  18. package/src/stack-infer-from-prs.js +71 -0
  19. package/src/stack-view/diff-line-map.js +62 -0
  20. package/src/stack-view/fetch-pr-data.js +104 -4
  21. package/src/stack-view/infer-chains-to-pick-stacks.js +80 -0
  22. package/src/stack-view/ink-app.js +3 -421
  23. package/src/stack-view/loader.js +19 -93
  24. package/src/stack-view/loading-ink.js +2 -0
  25. package/src/stack-view/merge-alternate-pick-stacks.js +245 -0
  26. package/src/stack-view/patch-preview-merge.js +108 -0
  27. package/src/stack-view/remote-infer-doc.js +76 -0
  28. package/src/stack-view/repo-picker-back.js +10 -0
  29. package/src/stack-view/run-stack-view.js +508 -150
  30. package/src/stack-view/run-view-entry.js +115 -0
  31. package/src/stack-view/sgr-mouse.js +56 -0
  32. package/src/stack-view/stack-branch-graph.js +95 -0
  33. package/src/stack-view/stack-pick-graph.js +93 -0
  34. package/src/stack-view/stack-pick-ink.js +308 -0
  35. package/src/stack-view/stack-pick-layout.js +19 -0
  36. package/src/stack-view/stack-pick-sort.js +188 -0
  37. package/src/stack-view/stack-picker-graph-pane.js +118 -0
  38. package/src/stack-view/terminal-fullscreen.js +7 -0
  39. package/src/stack-view/tree-ascii.js +73 -0
  40. package/src/stack-view/view-md-plain.js +23 -0
  41. package/src/stack-view/view-repo-picker-ink.js +293 -0
  42. package/src/stack-view/view-tui-sequential.js +126 -0
  43. package/src/tui/pages/home.js +122 -0
  44. package/src/tui/pages/repo-actions.js +81 -0
  45. package/src/tui/pages/repo-branches.js +259 -0
  46. package/src/tui/pages/viewer.js +2129 -0
  47. package/src/tui/router.js +40 -0
  48. package/src/tui/run-tui.js +281 -0
  49. package/src/utilities/loading.js +37 -0
  50. package/src/utilities/terminal.js +31 -0
  51. package/src/cli-output.js +0 -228
  52. package/src/nugit-start.js +0 -211
  53. package/src/stack-discover.js +0 -284
  54. package/src/stack-discovery-config.js +0 -91
  55. package/src/stack-extra-commands.js +0 -353
  56. package/src/stack-graph.js +0 -214
  57. package/src/stack-helpers.js +0 -58
  58. package/src/stack-propagate.js +0 -422
@@ -0,0 +1,126 @@
1
+ import React from "react";
2
+ import { render } from "ink";
3
+ import { StackPickInk } from "./stack-pick-ink.js";
4
+ import { inferChainsToPickStacks } from "./infer-chains-to-pick-stacks.js";
5
+ import { clearInkScreen } from "./terminal-fullscreen.js";
6
+
7
+ /** Resolved by {@link pickStackIndexWithInk} when Backspace requests the repo picker (allowBackToRepo only). */
8
+ export const STACK_PICK_BACK_TO_REPO = Symbol("STACK_PICK_BACK_TO_REPO");
9
+
10
+ /**
11
+ * @param {object} args
12
+ * @param {{ tip_pr_number: number, tip_head_branch: string, pr_count: number, created_by: string, prs: unknown[], tip_updated_at?: string, inferChainIndex?: number, inferDiffAdd?: number, inferDiffDel?: number }[]} args.stacks
13
+ * @param {string} [args.title]
14
+ * @param {boolean} [args.allowBackToRepo]
15
+ * @param {number | null | undefined} [args.viewingTipPrNumber]
16
+ * @param {string | null | undefined} [args.viewingHeadRef]
17
+ * @returns {Promise<(typeof args.stacks)[0] | null | typeof STACK_PICK_BACK_TO_REPO>}
18
+ */
19
+ function runStackPickInkOnce({
20
+ stacks,
21
+ title = "Choose stack to view",
22
+ allowBackToRepo = false,
23
+ /** When true with {@link allowBackToRepo}, Esc and q also return to repo (repo-picker loop only). */
24
+ escapeToRepo = false,
25
+ viewingTipPrNumber,
26
+ viewingHeadRef
27
+ }) {
28
+ clearInkScreen();
29
+ return new Promise((resolve) => {
30
+ let settled = false;
31
+ const inst = render(
32
+ React.createElement(StackPickInk, {
33
+ stacks,
34
+ title,
35
+ viewingTipPrNumber,
36
+ viewingHeadRef,
37
+ escapeToRepo: escapeToRepo === true,
38
+ onPick: (picked) => {
39
+ if (!settled) {
40
+ settled = true;
41
+ try {
42
+ inst.clear();
43
+ } catch {
44
+ /* ignore */
45
+ }
46
+ resolve(picked);
47
+ }
48
+ },
49
+ onRequestBack:
50
+ allowBackToRepo === true
51
+ ? () => {
52
+ if (!settled) {
53
+ settled = true;
54
+ try {
55
+ inst.clear();
56
+ } catch {
57
+ /* ignore */
58
+ }
59
+ resolve(STACK_PICK_BACK_TO_REPO);
60
+ }
61
+ }
62
+ : undefined
63
+ })
64
+ );
65
+ void inst.waitUntilExit().then(() => {
66
+ try {
67
+ inst.clear();
68
+ } catch {
69
+ /* ignore */
70
+ }
71
+ if (!settled) {
72
+ settled = true;
73
+ resolve(null);
74
+ }
75
+ });
76
+ });
77
+ }
78
+
79
+ /**
80
+ * @param {{ tip_pr_number: number, tip_head_branch: string, pr_count: number, created_by: string, prs: unknown[], tip_updated_at?: string }[]} stacks
81
+ * @param {{ allowBackToRepo?: boolean, escapeToRepo?: boolean, viewingTipPrNumber?: number | null, viewingHeadRef?: string | null }} [opts]
82
+ * @returns {Promise<(typeof stacks)[0] | null | typeof STACK_PICK_BACK_TO_REPO>}
83
+ */
84
+ export async function pickStackIndexWithInk(stacks, opts) {
85
+ return runStackPickInkOnce({
86
+ stacks,
87
+ title: "Choose stack to view",
88
+ allowBackToRepo: opts?.allowBackToRepo === true,
89
+ escapeToRepo: opts?.escapeToRepo === true,
90
+ viewingTipPrNumber: opts?.viewingTipPrNumber,
91
+ viewingHeadRef: opts?.viewingHeadRef
92
+ });
93
+ }
94
+
95
+ /**
96
+ * @param {number[][]} chains bottom → tip PR numbers per chain
97
+ * @param {unknown[]} pulls GitHub pull objects (same list used to build chains)
98
+ * @param {{ allowBackToRepo?: boolean }} [opts] Backspace resolves -2 for repo-picker loop
99
+ * @returns {Promise<number>} chain index, or -1 for q / cancel (use largest stack), or -2 back to repo
100
+ */
101
+ export async function pickInferChainIndexWithInk(chains, pulls, opts) {
102
+ const stacks = inferChainsToPickStacks(chains, pulls);
103
+ const r = await runStackPickInkOnce({
104
+ stacks,
105
+ title: "Choose stack to view",
106
+ allowBackToRepo: opts?.allowBackToRepo === true,
107
+ escapeToRepo: opts?.allowBackToRepo === true
108
+ });
109
+ if (r === STACK_PICK_BACK_TO_REPO) {
110
+ return -2;
111
+ }
112
+ if (r == null) {
113
+ return -1;
114
+ }
115
+ if (typeof r.inferChainIndex === "number") {
116
+ return r.inferChainIndex;
117
+ }
118
+ const tip = typeof r.tip_pr_number === "number" ? r.tip_pr_number : null;
119
+ if (tip != null) {
120
+ const fi = chains.findIndex((c) => Array.isArray(c) && c.length > 0 && c[c.length - 1] === tip);
121
+ if (fi >= 0) {
122
+ return fi;
123
+ }
124
+ }
125
+ return -1;
126
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Home page — the first screen shown when `nugit` is run bare on a TTY.
3
+ *
4
+ * Keys:
5
+ * Enter / c → current directory (view stack in current git repo)
6
+ * s / / → review hub (repos with pending reviews)
7
+ * o → open remote (search GitHub for a repo)
8
+ * q / Esc → quit
9
+ */
10
+ import React, { useEffect, useRef, useState } from "react";
11
+ import { Box, Text, useApp, useInput, render, useStdout } from "ink";
12
+ import chalk from "chalk";
13
+ import {
14
+ parseSgrMouse,
15
+ enableSgrMouse,
16
+ disableSgrMouse
17
+ } from "../../stack-view/sgr-mouse.js";
18
+ import { getRepoFullNameFromGitRoot } from "../../git-info.js";
19
+ import { findGitRoot } from "../../nugit-stack.js";
20
+ import { clearInkScreen } from "../../utilities/terminal.js";
21
+
22
+ /**
23
+ * @typedef {"cwd" | "review" | "search" | "quit"} HomeAction
24
+ */
25
+
26
+ /**
27
+ * Render the home page and resolve with the user's choice.
28
+ * @returns {Promise<HomeAction>}
29
+ */
30
+ export async function runHomePage() {
31
+ clearInkScreen();
32
+ const result = { action: /** @type {HomeAction} */ ("quit") };
33
+ const { waitUntilExit } = render(React.createElement(HomeApp, { result }));
34
+ await waitUntilExit();
35
+ return result.action;
36
+ }
37
+
38
+ /**
39
+ * @param {{ result: { action: HomeAction } }} props
40
+ */
41
+ function HomeApp({ result }) {
42
+ const { exit } = useApp();
43
+ const { stdout } = useStdout();
44
+
45
+ useEffect(() => {
46
+ enableSgrMouse(stdout);
47
+ return () => disableSgrMouse(stdout);
48
+ }, [stdout]);
49
+
50
+ const root = findGitRoot();
51
+ let cwdRepo = null;
52
+ if (root) {
53
+ try { cwdRepo = getRepoFullNameFromGitRoot(root); } catch { cwdRepo = null; }
54
+ }
55
+
56
+ const finish = (action) => {
57
+ result.action = action;
58
+ exit();
59
+ };
60
+
61
+ useInput((input, key) => {
62
+ const mouse = parseSgrMouse(input);
63
+ if (mouse) return;
64
+
65
+ if (key.escape || input === "q" || key.backspace || key.delete) {
66
+ finish("quit");
67
+ return;
68
+ }
69
+ if ((key.return || input === "c") && cwdRepo) {
70
+ finish("cwd");
71
+ return;
72
+ }
73
+ if (input === "s" || input === "/") {
74
+ finish("review");
75
+ return;
76
+ }
77
+ if (input === "o") {
78
+ finish("search");
79
+ return;
80
+ }
81
+ });
82
+
83
+ return React.createElement(
84
+ Box,
85
+ { flexDirection: "column", padding: 1, gap: 0 },
86
+ React.createElement(
87
+ Text,
88
+ { color: "cyan", bold: true },
89
+ "nugit"
90
+ ),
91
+ React.createElement(Text, null, ""),
92
+ cwdRepo
93
+ ? React.createElement(
94
+ Text,
95
+ null,
96
+ chalk.whiteBright("Enter"),
97
+ ` view this directory: ${chalk.bold(cwdRepo)}`
98
+ )
99
+ : React.createElement(
100
+ Text,
101
+ { dimColor: true },
102
+ "Enter (no github.com remote — navigate to a repo with [o])"
103
+ ),
104
+ React.createElement(
105
+ Text,
106
+ null,
107
+ chalk.whiteBright("[s]"),
108
+ " review hub — repos with pending reviews"
109
+ ),
110
+ React.createElement(
111
+ Text,
112
+ null,
113
+ chalk.whiteBright("[o]"),
114
+ " open remote — search GitHub for any repo"
115
+ ),
116
+ React.createElement(
117
+ Text,
118
+ { dimColor: true },
119
+ "[q] quit"
120
+ )
121
+ );
122
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Repo Actions page — shown after a repo has been chosen, before entering the viewer.
3
+ *
4
+ * Keys:
5
+ * Enter / s → Review stacks (stack picker + viewer)
6
+ * b → Branches / PRs
7
+ * Backspace → go back
8
+ * q / Esc → go back
9
+ */
10
+ import React from "react";
11
+ import { Box, Text, useApp, useInput, render } from "ink";
12
+ import chalk from "chalk";
13
+ import { clearInkScreen } from "../../utilities/terminal.js";
14
+
15
+ /**
16
+ * @typedef {"stacks" | "branches" | "back"} RepoAction
17
+ */
18
+
19
+ /**
20
+ * @param {{ repo: string }} opts
21
+ * @returns {Promise<RepoAction>}
22
+ */
23
+ export async function runRepoActionsPage({ repo }) {
24
+ clearInkScreen();
25
+ const result = { action: /** @type {RepoAction} */ ("back") };
26
+ const { waitUntilExit } = render(
27
+ React.createElement(RepoActionsApp, { repo, result })
28
+ );
29
+ await waitUntilExit();
30
+ return result.action;
31
+ }
32
+
33
+ /**
34
+ * @param {{ repo: string, result: { action: RepoAction } }} props
35
+ */
36
+ function RepoActionsApp({ repo, result }) {
37
+ const { exit } = useApp();
38
+
39
+ const finish = (/** @type {RepoAction} */ action) => {
40
+ result.action = action;
41
+ exit();
42
+ };
43
+
44
+ useInput((input, key) => {
45
+ const backKey = key.backspace || key.delete || input === "\x7f" || input === "\b";
46
+ if (backKey || input === "q" || key.escape) {
47
+ finish("back");
48
+ return;
49
+ }
50
+ if (key.return || input === "s") {
51
+ finish("stacks");
52
+ return;
53
+ }
54
+ if (input === "b") {
55
+ finish("branches");
56
+ return;
57
+ }
58
+ });
59
+
60
+ return React.createElement(
61
+ Box,
62
+ { flexDirection: "column", padding: 1, gap: 0 },
63
+ React.createElement(Text, { color: "cyan", bold: true }, "nugit"),
64
+ React.createElement(Text, { dimColor: true }, repo),
65
+ React.createElement(Text, {}, ""),
66
+ React.createElement(
67
+ Text,
68
+ {},
69
+ chalk.whiteBright("Enter"),
70
+ " Review stacks — browse and approve stacked PRs"
71
+ ),
72
+ React.createElement(
73
+ Text,
74
+ {},
75
+ chalk.whiteBright("[b]"),
76
+ " Branches / PRs — list, push, and open PRs for branches"
77
+ ),
78
+ React.createElement(Text, {}, ""),
79
+ React.createElement(Text, { dimColor: true }, "Backspace / [q] back")
80
+ );
81
+ }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Branches / PRs page — list local + remote branches for a repo, with actions:
3
+ * Enter / p → push local branch to remote (when local_ahead or diverged)
4
+ * Enter / c → create PR (opens gh pr create or browser compare URL)
5
+ * o → open compare URL in browser
6
+ * j / k → move cursor
7
+ * Backspace / q → go back
8
+ */
9
+ import React, { useEffect, useRef, useState } from "react";
10
+ import { Box, Text, useApp, useInput, render } from "ink";
11
+ import chalk from "chalk";
12
+ import { execFileSync, spawnSync } from "child_process";
13
+ import { clearInkScreen } from "../../utilities/terminal.js";
14
+ import { openUrl } from "../../stack-view/open-url.js";
15
+ import {
16
+ listLocalBranches,
17
+ listRemoteBranches,
18
+ mergeBranchModel
19
+ } from "../../services/repo-branches.js";
20
+ import { findGitRoot } from "../../nugit-stack.js";
21
+ import { getRepoFullNameFromGitRoot } from "../../git-info.js";
22
+ import { parseRepoFullName } from "../../nugit-stack.js";
23
+
24
+ /**
25
+ * @param {{ repo: string }} opts
26
+ * @returns {Promise<void>}
27
+ */
28
+ export async function runRepoBranchesPage({ repo }) {
29
+ clearInkScreen();
30
+ const { waitUntilExit } = render(
31
+ React.createElement(RepoBranchesApp, { repo })
32
+ );
33
+ await waitUntilExit();
34
+ }
35
+
36
+ /**
37
+ * @param {{ repo: string }} props
38
+ */
39
+ function RepoBranchesApp({ repo }) {
40
+ const { exit } = useApp();
41
+ const { owner, repo: repoName } = parseRepoFullName(repo);
42
+
43
+ const [loading, setLoading] = useState(true);
44
+ const [err, setErr] = useState(/** @type {string | null} */ (null));
45
+ const [rows, setRows] = useState(/** @type {import("../../services/repo-branches.js").BranchRow[]} */ ([]));
46
+ const [cursor, setCursor] = useState(0);
47
+ const [action, setAction] = useState(/** @type {string | null} */ (null));
48
+ const [actionErr, setActionErr] = useState(/** @type {string | null} */ (null));
49
+
50
+ // Resolve local git root (may not match repo if user launched from a different dir)
51
+ const localRoot = (() => {
52
+ const root = findGitRoot();
53
+ if (!root) return null;
54
+ try {
55
+ const remoteRepo = getRepoFullNameFromGitRoot(root);
56
+ return remoteRepo === repo ? root : null;
57
+ } catch {
58
+ return null;
59
+ }
60
+ })();
61
+
62
+ const rowsRef = useRef(rows);
63
+ rowsRef.current = rows;
64
+
65
+ const loadBranches = () => {
66
+ setLoading(true);
67
+ setErr(null);
68
+ setActionErr(null);
69
+
70
+ const local = localRoot ? listLocalBranches(localRoot) : [];
71
+ listRemoteBranches(owner, repoName)
72
+ .then((remote) => mergeBranchModel(local, remote, localRoot))
73
+ .then((merged) => {
74
+ setRows(merged);
75
+ setCursor(0);
76
+ })
77
+ .catch((e) => setErr(String(e?.message || e)))
78
+ .finally(() => setLoading(false));
79
+ };
80
+
81
+ useEffect(() => {
82
+ loadBranches();
83
+ // eslint-disable-next-line react-hooks/exhaustive-deps
84
+ }, []);
85
+
86
+ useInput((input, key) => {
87
+ if (loading) return;
88
+ const backKey = key.backspace || key.delete || input === "\x7f" || input === "\b";
89
+ if (backKey || input === "q" || key.escape) {
90
+ exit();
91
+ return;
92
+ }
93
+ if (input === "j" || key.downArrow) {
94
+ setCursor((c) => Math.min(c + 1, Math.max(0, rowsRef.current.length - 1)));
95
+ return;
96
+ }
97
+ if (input === "k" || key.upArrow) {
98
+ setCursor((c) => Math.max(c - 1, 0));
99
+ return;
100
+ }
101
+ if (input === "r") {
102
+ loadBranches();
103
+ return;
104
+ }
105
+
106
+ const row = rowsRef.current[cursor];
107
+ if (!row) return;
108
+
109
+ // Push local → remote
110
+ if (input === "p" && localRoot && (row.status === "local_ahead" || row.status === "local_only" || row.status === "diverged")) {
111
+ setAction(`Pushing ${row.name}…`);
112
+ setActionErr(null);
113
+ try {
114
+ spawnSync("git", ["push", "origin", row.name], {
115
+ cwd: localRoot,
116
+ stdio: "pipe"
117
+ });
118
+ setAction(null);
119
+ loadBranches();
120
+ } catch (e) {
121
+ setActionErr(String(/** @type {{ message?: string }} */ (e)?.message || e));
122
+ setAction(null);
123
+ }
124
+ return;
125
+ }
126
+
127
+ // Create PR via gh CLI or browser
128
+ if (key.return || input === "c") {
129
+ if (row.status === "remote_only" || row.status === "in_sync" || row.status === "remote_ahead") {
130
+ // Branch is on remote — open compare URL
131
+ const compareUrl = `https://github.com/${repo}/compare/${encodeURIComponent(row.name)}`;
132
+ openUrl(compareUrl);
133
+ setAction(`Opened compare URL for ${row.name}`);
134
+ setTimeout(() => setAction(null), 2000);
135
+ } else if (row.status === "local_only" || row.status === "local_ahead") {
136
+ if (!localRoot) {
137
+ setActionErr("No local clone found for push/create PR.");
138
+ return;
139
+ }
140
+ // Try gh pr create, fall back to browser
141
+ const ghResult = spawnSync("gh", ["pr", "create", "--head", row.name, "--fill"], {
142
+ cwd: localRoot,
143
+ encoding: "utf8",
144
+ stdio: "pipe"
145
+ });
146
+ if (ghResult.status === 0) {
147
+ setAction(`PR created via gh for ${row.name}`);
148
+ setTimeout(() => { setAction(null); loadBranches(); }, 2000);
149
+ } else {
150
+ const compareUrl = `https://github.com/${repo}/compare/${encodeURIComponent(row.name)}`;
151
+ openUrl(compareUrl);
152
+ setAction(`Opened compare URL for ${row.name} (gh not available)`);
153
+ setTimeout(() => setAction(null), 2000);
154
+ }
155
+ }
156
+ return;
157
+ }
158
+
159
+ // Open compare URL in browser
160
+ if (input === "o") {
161
+ const compareUrl = `https://github.com/${repo}/compare/${encodeURIComponent(row.name)}`;
162
+ openUrl(compareUrl);
163
+ setAction(`Opened compare URL for ${row.name}`);
164
+ setTimeout(() => setAction(null), 2000);
165
+ return;
166
+ }
167
+ });
168
+
169
+ const safeRow = rows.length ? rows[Math.min(cursor, rows.length - 1)] : null;
170
+
171
+ /** @param {import("../../services/repo-branches.js").BranchStatus} status */
172
+ function statusBadge(status) {
173
+ switch (status) {
174
+ case "local_ahead": return chalk.green("↑ local ahead");
175
+ case "remote_ahead": return chalk.yellow("↓ remote ahead");
176
+ case "diverged": return chalk.red("⇅ diverged");
177
+ case "local_only": return chalk.cyan("local only");
178
+ case "remote_only": return chalk.gray("remote only");
179
+ case "in_sync": return chalk.dim("in sync");
180
+ default: return "";
181
+ }
182
+ }
183
+
184
+ const VIEWPORT = 14;
185
+ const n = rows.length;
186
+ const viewStart =
187
+ n <= VIEWPORT ? 0 : Math.max(0, Math.min(cursor - Math.floor(VIEWPORT / 2), n - VIEWPORT));
188
+ const viewSlice = rows.slice(viewStart, viewStart + VIEWPORT);
189
+
190
+ /** @param {import("../../services/repo-branches.js").BranchRow} row */
191
+ function rowActions(row) {
192
+ const parts = [];
193
+ if (
194
+ localRoot &&
195
+ (row.status === "local_ahead" || row.status === "local_only" || row.status === "diverged")
196
+ ) {
197
+ parts.push("[p] push");
198
+ }
199
+ if (row.status !== "in_sync") {
200
+ parts.push("[Enter/c] create PR");
201
+ }
202
+ parts.push("[o] compare");
203
+ return chalk.dim(parts.join(" · "));
204
+ }
205
+
206
+ return React.createElement(
207
+ Box,
208
+ { flexDirection: "column", padding: 1 },
209
+ React.createElement(
210
+ Text,
211
+ { color: "cyan", bold: true },
212
+ `Branches / PRs — ${repo}`
213
+ ),
214
+ React.createElement(
215
+ Text,
216
+ { dimColor: true },
217
+ `j/k navigate · [r] refresh · Backspace/q back${localRoot ? " · [p] push" : " (no local clone)"}${" · [Enter/c] create PR · [o] compare"}`
218
+ ),
219
+ loading
220
+ ? React.createElement(Text, { color: "yellow" }, "Loading branches…")
221
+ : err
222
+ ? React.createElement(Text, { color: "red" }, err)
223
+ : React.createElement(
224
+ Box,
225
+ { flexDirection: "column", marginTop: 1 },
226
+ n === 0
227
+ ? React.createElement(Text, { dimColor: true }, "No branches found.")
228
+ : viewSlice.map((row, localI) => {
229
+ const i = viewStart + localI;
230
+ const sel = i === cursor;
231
+ const mark = sel ? "▶ " : " ";
232
+ const nameColor = sel ? chalk.yellowBright : chalk.white;
233
+ const aheadBehind =
234
+ row.ahead || row.behind
235
+ ? chalk.dim(` (${row.ahead}↑ ${row.behind}↓)`)
236
+ : "";
237
+
238
+ return React.createElement(
239
+ Box,
240
+ { key: row.name, flexDirection: "column", marginBottom: sel ? 1 : 0 },
241
+ React.createElement(
242
+ Text,
243
+ {},
244
+ mark,
245
+ nameColor(row.name),
246
+ " ",
247
+ statusBadge(row.status),
248
+ aheadBehind
249
+ ),
250
+ sel
251
+ ? React.createElement(Text, {}, " ", rowActions(row))
252
+ : null
253
+ );
254
+ })
255
+ ),
256
+ action ? React.createElement(Text, { color: "green" }, action) : null,
257
+ actionErr ? React.createElement(Text, { color: "red" }, actionErr) : null
258
+ );
259
+ }