nugit-cli 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api-client.js +10 -11
- package/src/github-device-flow.js +1 -1
- package/src/github-oauth-client-id.js +11 -0
- package/src/github-pr-social.js +42 -0
- package/src/github-rest.js +114 -6
- package/src/nugit-stack.js +20 -0
- package/src/nugit-start.js +4 -4
- package/src/nugit.js +37 -22
- package/src/review-hub/review-autoapprove.js +95 -0
- package/src/review-hub/review-hub-back.js +10 -0
- package/src/review-hub/review-hub-ink.js +166 -0
- package/src/review-hub/run-review-hub.js +188 -0
- package/src/split-view/run-split.js +16 -3
- package/src/split-view/split-ink.js +2 -2
- package/src/stack-discover.js +9 -1
- package/src/stack-infer-from-prs.js +71 -0
- package/src/stack-view/diff-line-map.js +62 -0
- package/src/stack-view/fetch-pr-data.js +104 -4
- package/src/stack-view/infer-chains-to-pick-stacks.js +70 -0
- package/src/stack-view/ink-app.js +1853 -156
- package/src/stack-view/loading-ink.js +44 -0
- package/src/stack-view/merge-alternate-pick-stacks.js +223 -0
- package/src/stack-view/patch-preview-merge.js +108 -0
- package/src/stack-view/remote-infer-doc.js +93 -0
- package/src/stack-view/repo-picker-back.js +10 -0
- package/src/stack-view/run-stack-view.js +685 -50
- package/src/stack-view/run-view-entry.js +119 -0
- package/src/stack-view/sgr-mouse.js +56 -0
- package/src/stack-view/stack-branch-graph.js +95 -0
- package/src/stack-view/stack-pick-graph.js +93 -0
- package/src/stack-view/stack-pick-ink.js +270 -0
- package/src/stack-view/stack-pick-layout.js +19 -0
- package/src/stack-view/stack-pick-sort.js +188 -0
- package/src/stack-view/terminal-fullscreen.js +45 -0
- package/src/stack-view/tree-ascii.js +73 -0
- package/src/stack-view/view-md-plain.js +23 -0
- package/src/stack-view/view-repo-picker-ink.js +293 -0
- package/src/stack-view/view-tui-sequential.js +126 -0
|
@@ -1,56 +1,293 @@
|
|
|
1
|
-
import React, { useMemo, useState } from "react";
|
|
2
|
-
import { Box, Text, useApp, useInput } from "ink";
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { Box, Text, useApp, useInput, useStdin, useStdout } from "ink";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { openUrl } from "./open-url.js";
|
|
5
|
+
import { githubGetGitTree, githubGetBlobText } from "../github-rest.js";
|
|
6
|
+
import { buildAsciiTreeRows } from "./tree-ascii.js";
|
|
7
|
+
import { buildMergedPreviewFromPatch } from "./patch-preview-merge.js";
|
|
8
|
+
import { buildStackBranchGraphLines } from "./stack-branch-graph.js";
|
|
9
|
+
import { markdownToPlainLines } from "./view-md-plain.js";
|
|
10
|
+
import {
|
|
11
|
+
parseSgrMouse,
|
|
12
|
+
enableSgrMouse,
|
|
13
|
+
disableSgrMouse,
|
|
14
|
+
isWheelUp,
|
|
15
|
+
isWheelDown
|
|
16
|
+
} from "./sgr-mouse.js";
|
|
5
17
|
|
|
6
18
|
/** @returns {{ next: null | Record<string, unknown> }} */
|
|
7
19
|
export function createExitPayload() {
|
|
8
20
|
return { next: null };
|
|
9
21
|
}
|
|
10
22
|
|
|
23
|
+
const TAB_COUNT = 5;
|
|
24
|
+
const MAX_PREVIEW_CHARS = 800_000;
|
|
25
|
+
const MAX_PREVIEW_LINES = 12_000;
|
|
26
|
+
const SPIN_FRAMES = ["|", "/", "-", "\\"];
|
|
27
|
+
|
|
28
|
+
/** Shell: single full-screen workspace tabs (lazyql-style) */
|
|
29
|
+
const SHELL_TAB_LABELS = ["Tree", "Description", "Conversation", "Review"];
|
|
30
|
+
const SHELL_TAB_COUNT = 4;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Line change totals for a stack row (GitHub pull or sum of file stats).
|
|
34
|
+
* @param {{ pull?: unknown, files?: unknown[] }} r
|
|
35
|
+
*/
|
|
36
|
+
function prAddDelTotals(r) {
|
|
37
|
+
const pull =
|
|
38
|
+
r?.pull && typeof r.pull === "object"
|
|
39
|
+
? /** @type {Record<string, unknown>} */ (r.pull)
|
|
40
|
+
: null;
|
|
41
|
+
let add = pull != null ? Number(pull.additions) : NaN;
|
|
42
|
+
let del = pull != null ? Number(pull.deletions) : NaN;
|
|
43
|
+
if (!Number.isFinite(add)) add = 0;
|
|
44
|
+
if (!Number.isFinite(del)) del = 0;
|
|
45
|
+
if (add === 0 && del === 0 && Array.isArray(r?.files) && r.files.length > 0) {
|
|
46
|
+
for (const f of r.files) {
|
|
47
|
+
if (!f || typeof f !== "object") continue;
|
|
48
|
+
const o = /** @type {Record<string, unknown>} */ (f);
|
|
49
|
+
add += Number(o.additions) || 0;
|
|
50
|
+
del += Number(o.deletions) || 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { add, del };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Tree tab layout: never show patch + file preview at once — preview carries diff styling.
|
|
58
|
+
* @param {number} bodyMax
|
|
59
|
+
* @param {boolean} previewOpen
|
|
60
|
+
* @param {boolean} showPatchPane unified diff below tree (only when preview closed)
|
|
61
|
+
*/
|
|
62
|
+
function computeShellTreePaneHeights(bodyMax, previewOpen, showPatchPane) {
|
|
63
|
+
if (previewOpen) {
|
|
64
|
+
const treeBlockH = Math.max(3, Math.floor(bodyMax * 0.34));
|
|
65
|
+
const previewH = Math.max(4, bodyMax - treeBlockH - 3);
|
|
66
|
+
return { treeBlockH, patchH: 0, previewH };
|
|
67
|
+
}
|
|
68
|
+
if (showPatchPane) {
|
|
69
|
+
const treeBlockH = Math.max(3, Math.floor(bodyMax * 0.36));
|
|
70
|
+
const patchH = Math.max(4, bodyMax - treeBlockH - 3);
|
|
71
|
+
return { treeBlockH, patchH, previewH: 0 };
|
|
72
|
+
}
|
|
73
|
+
return { treeBlockH: Math.max(2, bodyMax - 1), patchH: 0, previewH: 0 };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {string} s
|
|
78
|
+
* @param {number} w
|
|
79
|
+
*/
|
|
80
|
+
function truncVis(s, w) {
|
|
81
|
+
if (w < 4) return "";
|
|
82
|
+
if (s.length <= w) return s;
|
|
83
|
+
return s.slice(0, w - 1) + "\u2026";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @template T
|
|
88
|
+
* @param {T[]} items
|
|
89
|
+
* @param {number} cursor
|
|
90
|
+
* @param {number} maxVisible
|
|
91
|
+
*/
|
|
92
|
+
function sliceViewport(items, cursor, maxVisible) {
|
|
93
|
+
const n = items.length;
|
|
94
|
+
if (n === 0) return { slice: [], start: 0 };
|
|
95
|
+
if (n <= maxVisible) return { slice: items, start: 0 };
|
|
96
|
+
const half = Math.floor(maxVisible / 2);
|
|
97
|
+
const start = Math.max(0, Math.min(cursor - half, n - maxVisible));
|
|
98
|
+
return { slice: items.slice(start, start + maxVisible), start };
|
|
99
|
+
}
|
|
100
|
+
|
|
11
101
|
/**
|
|
12
102
|
* @param {object} props
|
|
13
103
|
* @param {Awaited<ReturnType<import('./fetch-pr-data.js').fetchStackPrDetails>>} props.rows
|
|
14
104
|
* @param {{ next: null | Record<string, unknown> }} props.exitPayload
|
|
105
|
+
* @param {string} [props.viewTitle]
|
|
106
|
+
* @param {string} [props.browseOwner]
|
|
107
|
+
* @param {string} [props.browseRepo]
|
|
108
|
+
* @param {string} [props.repoFullName]
|
|
109
|
+
* @param {boolean} [props.shellMode] multi-page layout with sidebar (nugit view)
|
|
110
|
+
* @param {unknown[] | null} [props.alternateStacks] when >1, Backspace from PR-pick opens stack picker
|
|
15
111
|
*/
|
|
16
|
-
export function StackInkApp({
|
|
112
|
+
export function StackInkApp({
|
|
113
|
+
rows: stackRows,
|
|
114
|
+
exitPayload,
|
|
115
|
+
viewTitle = "nugit view",
|
|
116
|
+
browseOwner = "",
|
|
117
|
+
browseRepo = "",
|
|
118
|
+
repoFullName = "",
|
|
119
|
+
shellMode = false,
|
|
120
|
+
alternateStacks = null
|
|
121
|
+
}) {
|
|
122
|
+
const canPickAlternateStack = Array.isArray(alternateStacks) && alternateStacks.length > 1;
|
|
17
123
|
const { exit } = useApp();
|
|
18
|
-
const
|
|
124
|
+
const { stdout } = useStdout();
|
|
125
|
+
const { stdin } = useStdin();
|
|
126
|
+
const [termCols, setTermCols] = useState(() => stdout?.columns || 80);
|
|
127
|
+
const [termRows, setTermRows] = useState(() => stdout?.rows || 24);
|
|
128
|
+
const [pulse, setPulse] = useState(0);
|
|
129
|
+
|
|
130
|
+
const cols = Math.max(40, termCols || 80);
|
|
131
|
+
const ttyRows = Math.max(16, termRows || 24);
|
|
132
|
+
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
const upd = () => {
|
|
135
|
+
setTermCols(stdout?.columns || 80);
|
|
136
|
+
setTermRows(stdout?.rows || 24);
|
|
137
|
+
};
|
|
138
|
+
upd();
|
|
139
|
+
stdout?.on?.("resize", upd);
|
|
140
|
+
return () => stdout?.off?.("resize", upd);
|
|
141
|
+
}, [stdout]);
|
|
142
|
+
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const t = setInterval(() => setPulse((p) => (p + 1) % 256), 350);
|
|
145
|
+
return () => clearInterval(t);
|
|
146
|
+
}, []);
|
|
147
|
+
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (!shellMode) {
|
|
150
|
+
enableSgrMouse(stdout);
|
|
151
|
+
return () => disableSgrMouse(stdout);
|
|
152
|
+
}
|
|
153
|
+
return undefined;
|
|
154
|
+
}, [stdout, shellMode]);
|
|
155
|
+
|
|
156
|
+
/** @type {React.MutableRefObject<{ bodyStartR: number, nLad: number, ladderStart: number } | null>} */
|
|
157
|
+
const layoutRef = useRef(null);
|
|
158
|
+
/** @type {React.MutableRefObject<Array<{ r1: number, r2: number, kind: string, [k: string]: unknown }>>} */
|
|
159
|
+
const regionsRef = useRef([]);
|
|
160
|
+
/** @type {React.MutableRefObject<{ t: number, idx: number } | null>} */
|
|
161
|
+
const lastTreeClickRef = useRef(null);
|
|
162
|
+
|
|
163
|
+
const [showFullHelp, setShowFullHelp] = useState(false);
|
|
164
|
+
/** Shell: main panel tab (tree first — diffs inline when cursor is on a changed file) */
|
|
165
|
+
const [shellMainTab, setShellMainTab] = useState(0);
|
|
166
|
+
/** Shell: keyboard focus on sidebar PR ladder; true on entry so j/k adjust PRs before the file tree */
|
|
167
|
+
const [shellStackFocus, setShellStackFocus] = useState(() => shellMode);
|
|
168
|
+
|
|
169
|
+
/** Shell: -1 = no PR loaded yet (tree waits); non-shell starts at 0 */
|
|
170
|
+
const [prIndex, setPrIndex] = useState(() => (shellMode ? -1 : 0));
|
|
19
171
|
const [tab, setTab] = useState(0);
|
|
20
172
|
const [lineIndex, setLineIndex] = useState(0);
|
|
21
173
|
const [fileIndex, setFileIndex] = useState(0);
|
|
22
174
|
const [filePatchOffset, setFilePatchOffset] = useState(0);
|
|
175
|
+
const [patchCursorLine, setPatchCursorLine] = useState(0);
|
|
23
176
|
const [fileCommentIndex, setFileCommentIndex] = useState(0);
|
|
24
177
|
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
178
|
+
const [treeLines, setTreeLines] = useState(/** @type {{ path: string, type: string }[]} */ ([]));
|
|
179
|
+
const [treeLoading, setTreeLoading] = useState(false);
|
|
180
|
+
const [treeError, setTreeError] = useState(/** @type {string | null} */ (null));
|
|
181
|
+
const [treeLineIndex, setTreeLineIndex] = useState(0);
|
|
182
|
+
const [treePreview, setTreePreview] = useState("");
|
|
183
|
+
const [treePreviewPath, setTreePreviewPath] = useState("");
|
|
184
|
+
const [treePreviewScroll, setTreePreviewScroll] = useState(0);
|
|
185
|
+
|
|
186
|
+
const asciiTreeRows = useMemo(() => buildAsciiTreeRows(treeLines), [treeLines]);
|
|
187
|
+
const treePreviewLines = useMemo(() => (treePreview ? treePreview.split("\n") : []), [treePreview]);
|
|
188
|
+
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
setTreeLineIndex((i) => Math.min(i, Math.max(0, asciiTreeRows.length - 1)));
|
|
191
|
+
}, [asciiTreeRows.length]);
|
|
192
|
+
|
|
193
|
+
const len = stackRows?.length ?? 0;
|
|
194
|
+
const hasPrPick = !shellMode || prIndex >= 0;
|
|
195
|
+
const safePr = len === 0 || !hasPrPick ? 0 : Math.min(prIndex, len - 1);
|
|
196
|
+
const row = len && hasPrPick ? stackRows[safePr] : null;
|
|
28
197
|
const issueList = row?.issueComments || [];
|
|
29
198
|
const reviewList = row?.reviewComments || [];
|
|
30
199
|
const fileList = row?.files || [];
|
|
200
|
+
const headSha =
|
|
201
|
+
row?.pull?.head && typeof row.pull.head === "object"
|
|
202
|
+
? String(/** @type {Record<string, unknown>} */ (row.pull.head).sha || "")
|
|
203
|
+
: "";
|
|
31
204
|
|
|
32
205
|
const listLen = useMemo(() => {
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
206
|
+
if (shellMode) {
|
|
207
|
+
if (shellMainTab === 0) {
|
|
208
|
+
return Math.max(1, asciiTreeRows.length);
|
|
209
|
+
}
|
|
210
|
+
if (shellMainTab === 1) {
|
|
211
|
+
const bodyRaw = row?.pull && typeof row.pull.body === "string" ? row.pull.body : "";
|
|
212
|
+
const plain = markdownToPlainLines(bodyRaw);
|
|
213
|
+
const lines = plain ? plain.split("\n").filter((ln) => ln.length > 0) : ["(no description)"];
|
|
214
|
+
return Math.max(1, lines.length);
|
|
215
|
+
}
|
|
216
|
+
if (shellMainTab === 2) {
|
|
217
|
+
return issueList.length;
|
|
218
|
+
}
|
|
219
|
+
if (shellMainTab === 3) {
|
|
220
|
+
return reviewList.length;
|
|
221
|
+
}
|
|
41
222
|
}
|
|
223
|
+
if (tab === 1) return issueList.length;
|
|
224
|
+
if (tab === 2) return reviewList.length;
|
|
225
|
+
if (tab === 3) return fileList.length;
|
|
226
|
+
if (tab === 4) return asciiTreeRows.length;
|
|
42
227
|
return 0;
|
|
43
|
-
}, [
|
|
228
|
+
}, [
|
|
229
|
+
shellMode,
|
|
230
|
+
shellMainTab,
|
|
231
|
+
tab,
|
|
232
|
+
issueList.length,
|
|
233
|
+
reviewList.length,
|
|
234
|
+
fileList.length,
|
|
235
|
+
asciiTreeRows.length,
|
|
236
|
+
row
|
|
237
|
+
]);
|
|
44
238
|
|
|
45
|
-
const
|
|
239
|
+
const treeSafeLine = Math.min(treeLineIndex, Math.max(0, asciiTreeRows.length - 1));
|
|
240
|
+
const safeLine = shellMode
|
|
241
|
+
? shellMainTab === 0
|
|
242
|
+
? treeSafeLine
|
|
243
|
+
: Math.min(lineIndex, Math.max(0, listLen - 1))
|
|
244
|
+
: tab === 4
|
|
245
|
+
? treeSafeLine
|
|
246
|
+
: Math.min(lineIndex, Math.max(0, listLen - 1));
|
|
46
247
|
const safeFile = fileList.length === 0 ? 0 : Math.min(fileIndex, fileList.length - 1);
|
|
47
248
|
const selectedFile = fileList[safeFile] || null;
|
|
48
249
|
const patchLines =
|
|
49
250
|
selectedFile && typeof selectedFile.patch === "string"
|
|
50
251
|
? String(selectedFile.patch).split("\n")
|
|
51
252
|
: [];
|
|
52
|
-
|
|
53
|
-
const
|
|
253
|
+
|
|
254
|
+
const browsePreviewOpen = !!(
|
|
255
|
+
treePreviewPath &&
|
|
256
|
+
(shellMode ? shellMainTab === 0 : tab === 4)
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const pathToChangedFile = useMemo(() => {
|
|
260
|
+
/** @type {Map<string, Record<string, unknown>>} */
|
|
261
|
+
const m = new Map();
|
|
262
|
+
for (const f of fileList) {
|
|
263
|
+
const n = String(f?.filename || "");
|
|
264
|
+
if (n) m.set(n, f);
|
|
265
|
+
}
|
|
266
|
+
return m;
|
|
267
|
+
}, [fileList]);
|
|
268
|
+
|
|
269
|
+
const mergedPreviewRows = useMemo(() => {
|
|
270
|
+
if (!browsePreviewOpen || !treePreviewPath || !treePreview) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
const fe = fileList.find((f) => String(f?.filename || "") === treePreviewPath);
|
|
274
|
+
if (!fe || typeof fe.patch !== "string" || !String(fe.patch).trim()) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
const m = buildMergedPreviewFromPatch(treePreview, String(fe.patch), treePreviewPath);
|
|
278
|
+
return m && m.length > 0 ? m : null;
|
|
279
|
+
}, [browsePreviewOpen, treePreviewPath, treePreview, fileList]);
|
|
280
|
+
|
|
281
|
+
const previewLineCount = mergedPreviewRows?.length ?? treePreviewLines.length;
|
|
282
|
+
|
|
283
|
+
useEffect(() => {
|
|
284
|
+
if (!browsePreviewOpen) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const maxScroll = Math.max(0, previewLineCount - 1);
|
|
288
|
+
setTreePreviewScroll((s) => Math.min(s, maxScroll));
|
|
289
|
+
}, [browsePreviewOpen, treePreviewPath, previewLineCount]);
|
|
290
|
+
|
|
54
291
|
const fileComments = useMemo(() => {
|
|
55
292
|
if (!selectedFile) return [];
|
|
56
293
|
const fileName = String(selectedFile.filename || "");
|
|
@@ -60,11 +297,208 @@ export function StackInkApp({ rows, exitPayload }) {
|
|
|
60
297
|
? Math.min(fileCommentIndex, fileComments.length - 1)
|
|
61
298
|
: 0;
|
|
62
299
|
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
300
|
+
/** Layout: keep total output within terminal height */
|
|
301
|
+
const sidebarW = shellMode ? 28 : 0;
|
|
302
|
+
const innerW = shellMode
|
|
303
|
+
? Math.max(24, cols - 2 - sidebarW - 1 - 1)
|
|
304
|
+
: Math.max(20, cols - 2);
|
|
305
|
+
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
if (!shellMode) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (shellMainTab === 0) {
|
|
311
|
+
setTab(4);
|
|
312
|
+
} else if (shellMainTab === 2) {
|
|
313
|
+
setTab(1);
|
|
314
|
+
} else if (shellMainTab === 3) {
|
|
315
|
+
setTab(2);
|
|
316
|
+
} else {
|
|
317
|
+
setTab(0);
|
|
318
|
+
}
|
|
319
|
+
}, [shellMode, shellMainTab]);
|
|
320
|
+
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
if (!shellMode || shellMainTab !== 0) {
|
|
323
|
+
setShellStackFocus(false);
|
|
324
|
+
}
|
|
325
|
+
}, [shellMode, shellMainTab]);
|
|
326
|
+
|
|
327
|
+
useEffect(() => {
|
|
328
|
+
if (shellMode && prIndex < 0 && shellMainTab !== 0) {
|
|
329
|
+
setShellMainTab(0);
|
|
330
|
+
}
|
|
331
|
+
}, [shellMode, prIndex, shellMainTab]);
|
|
332
|
+
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
if (!shellMode || shellMainTab !== 0) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const ent = asciiTreeRows[treeSafeLine];
|
|
338
|
+
const p = ent?.path;
|
|
339
|
+
if (!p) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const idx = fileList.findIndex((f) => String(f?.filename || "") === p);
|
|
343
|
+
if (idx >= 0) {
|
|
344
|
+
setFileIndex(idx);
|
|
345
|
+
}
|
|
346
|
+
}, [shellMode, shellMainTab, treeSafeLine, asciiTreeRows, fileList]);
|
|
347
|
+
|
|
348
|
+
const exitRef = useRef(exit);
|
|
349
|
+
exitRef.current = exit;
|
|
350
|
+
const exitPayloadRef = useRef(exitPayload);
|
|
351
|
+
exitPayloadRef.current = exitPayload;
|
|
352
|
+
/** @type {React.MutableRefObject<{ canScrollPatch: boolean, maxPatchOffset: number, step: number, canScrollMergedPreview: boolean, mergedMaxScroll: number, mergedStep: number }>} */
|
|
353
|
+
const patchScrollRef = useRef({
|
|
354
|
+
canScrollPatch: false,
|
|
355
|
+
maxPatchOffset: 0,
|
|
356
|
+
step: 3,
|
|
357
|
+
canScrollMergedPreview: false,
|
|
358
|
+
mergedMaxScroll: 0,
|
|
359
|
+
mergedStep: 3
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const SIDEBAR_GRAPH_MAX = 7;
|
|
363
|
+
|
|
364
|
+
/** Shell: title + tab row + rule + gap; footer: rule + up to 2 hint rows */
|
|
365
|
+
const reservedTop = shellMode ? 5 : 3;
|
|
366
|
+
const reservedHelp = shellMode ? 5 : 2;
|
|
367
|
+
const ladderMaxForDisplay = shellMode
|
|
368
|
+
? Math.min(len, Math.max(2, ttyRows - (18 + SIDEBAR_GRAPH_MAX)))
|
|
369
|
+
: Math.min(len, Math.max(2, Math.min(7, Math.floor((ttyRows - 10) / 3))));
|
|
370
|
+
const ladderMaxReserved = shellMode ? 0 : ladderMaxForDisplay;
|
|
371
|
+
const bodyMax = Math.max(
|
|
372
|
+
6,
|
|
373
|
+
ttyRows - reservedTop - reservedHelp - ladderMaxReserved - 2
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
const selPathForPatch = asciiTreeRows[treeSafeLine]?.path;
|
|
377
|
+
const hasPatchForShellTree =
|
|
378
|
+
!!selPathForPatch &&
|
|
379
|
+
selPathForPatch === String(selectedFile?.filename || "") &&
|
|
380
|
+
patchLines.length > 0;
|
|
381
|
+
const shellTreePanes =
|
|
382
|
+
shellMode && shellMainTab === 0
|
|
383
|
+
? computeShellTreePaneHeights(
|
|
384
|
+
bodyMax,
|
|
385
|
+
browsePreviewOpen,
|
|
386
|
+
hasPatchForShellTree && !browsePreviewOpen
|
|
387
|
+
)
|
|
388
|
+
: null;
|
|
389
|
+
|
|
390
|
+
let patchPageSize = 12;
|
|
391
|
+
if (tab === 3) {
|
|
392
|
+
patchPageSize = Math.max(4, Math.min(20, bodyMax - 8));
|
|
393
|
+
} else if (shellTreePanes && shellTreePanes.patchH > 0) {
|
|
394
|
+
patchPageSize = Math.max(4, shellTreePanes.patchH);
|
|
395
|
+
} else if (shellMode && shellMainTab === 0) {
|
|
396
|
+
patchPageSize = Math.max(4, Math.min(20, bodyMax - 8));
|
|
397
|
+
}
|
|
398
|
+
const maxPatchOffset = Math.max(0, patchLines.length - patchPageSize);
|
|
399
|
+
|
|
400
|
+
const canScrollPatchShell =
|
|
401
|
+
shellMode &&
|
|
402
|
+
shellMainTab === 0 &&
|
|
403
|
+
hasPatchForShellTree &&
|
|
404
|
+
!browsePreviewOpen;
|
|
405
|
+
const previewHForMergedScroll = shellTreePanes?.previewH ?? 4;
|
|
406
|
+
const mergedLen = mergedPreviewRows?.length ?? 0;
|
|
407
|
+
const mergedMaxScroll = Math.max(0, mergedLen - previewHForMergedScroll);
|
|
408
|
+
const canScrollMergedPreview =
|
|
409
|
+
shellMode && shellMainTab === 0 && browsePreviewOpen && mergedLen > 0;
|
|
410
|
+
patchScrollRef.current = {
|
|
411
|
+
canScrollPatch: canScrollPatchShell,
|
|
412
|
+
maxPatchOffset,
|
|
413
|
+
step: Math.max(3, Math.min(10, patchPageSize)),
|
|
414
|
+
canScrollMergedPreview,
|
|
415
|
+
mergedMaxScroll,
|
|
416
|
+
mergedStep: Math.max(2, Math.min(8, previewHForMergedScroll))
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
useEffect(() => {
|
|
420
|
+
if (!shellMode || !stdin) {
|
|
421
|
+
return undefined;
|
|
422
|
+
}
|
|
423
|
+
const onData = (buf) => {
|
|
424
|
+
const s = buf.toString();
|
|
425
|
+
if (/\x1b\[15~/.test(s)) {
|
|
426
|
+
exitPayloadRef.current.next = { type: "refresh" };
|
|
427
|
+
exitRef.current();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const r = patchScrollRef.current;
|
|
431
|
+
if (/\x1b\[5~/.test(s)) {
|
|
432
|
+
if (r.canScrollMergedPreview) {
|
|
433
|
+
setTreePreviewScroll((o) => Math.max(0, o - r.mergedStep));
|
|
434
|
+
} else if (r.canScrollPatch) {
|
|
435
|
+
setFilePatchOffset((o) => Math.max(0, o - r.step));
|
|
436
|
+
}
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (/\x1b\[6~/.test(s)) {
|
|
440
|
+
if (r.canScrollMergedPreview) {
|
|
441
|
+
setTreePreviewScroll((o) => Math.min(r.mergedMaxScroll, o + r.mergedStep));
|
|
442
|
+
} else if (r.canScrollPatch) {
|
|
443
|
+
setFilePatchOffset((o) => Math.min(r.maxPatchOffset, o + r.step));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
stdin.on("data", onData);
|
|
448
|
+
return () => {
|
|
449
|
+
stdin.off("data", onData);
|
|
450
|
+
};
|
|
451
|
+
}, [shellMode, stdin]);
|
|
452
|
+
|
|
453
|
+
const treeViewActive = shellMode ? shellMainTab === 0 : tab === 4;
|
|
454
|
+
|
|
455
|
+
useEffect(() => {
|
|
456
|
+
if (!treeViewActive || !browseOwner || !browseRepo || !headSha) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
let cancelled = false;
|
|
460
|
+
setTreeLoading(true);
|
|
461
|
+
setTreeError(null);
|
|
462
|
+
setTreeLines([]);
|
|
463
|
+
setTreePreview("");
|
|
464
|
+
setTreePreviewPath("");
|
|
465
|
+
setTreePreviewScroll(0);
|
|
466
|
+
githubGetGitTree(browseOwner, browseRepo, headSha, true)
|
|
467
|
+
.then((data) => {
|
|
468
|
+
if (cancelled) return;
|
|
469
|
+
const tree = Array.isArray(data.tree) ? data.tree : [];
|
|
470
|
+
if (tree.length > 2500) {
|
|
471
|
+
setTreeError(`Large tree (${tree.length} entries); showing first 2500`);
|
|
472
|
+
}
|
|
473
|
+
const cap = tree.slice(0, 2500);
|
|
474
|
+
const lines = cap
|
|
475
|
+
.filter((t) => t && typeof t === "object" && typeof t.path === "string")
|
|
476
|
+
.map((t) => {
|
|
477
|
+
const o = /** @type {Record<string, unknown>} */ (t);
|
|
478
|
+
return { path: String(o.path), type: String(o.type || "blob") };
|
|
479
|
+
})
|
|
480
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
481
|
+
setTreeLines(lines);
|
|
482
|
+
setTreeLoading(false);
|
|
483
|
+
})
|
|
484
|
+
.catch((e) => {
|
|
485
|
+
if (!cancelled) {
|
|
486
|
+
setTreeError(String(/** @type {Error} */ (e)?.message || e));
|
|
487
|
+
setTreeLoading(false);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
return () => {
|
|
491
|
+
cancelled = true;
|
|
492
|
+
};
|
|
493
|
+
}, [treeViewActive, browseOwner, browseRepo, headSha, safePr]);
|
|
494
|
+
|
|
495
|
+
useEffect(() => {
|
|
496
|
+
setTreeLineIndex(0);
|
|
497
|
+
setTreePreview("");
|
|
498
|
+
setTreePreviewPath("");
|
|
499
|
+
setTreePreviewScroll(0);
|
|
500
|
+
}, [tab, treeViewActive, headSha]);
|
|
501
|
+
|
|
68
502
|
const patchOffsetForLine = (lines, lineNo) => {
|
|
69
503
|
if (!lineNo || !Number.isInteger(lineNo) || lineNo < 1) return 0;
|
|
70
504
|
let newLine = 0;
|
|
@@ -85,8 +519,319 @@ export function StackInkApp({ rows, exitPayload }) {
|
|
|
85
519
|
return 0;
|
|
86
520
|
};
|
|
87
521
|
|
|
522
|
+
const closeBrowsePreview = () => {
|
|
523
|
+
setTreePreview("");
|
|
524
|
+
setTreePreviewPath("");
|
|
525
|
+
setTreePreviewScroll(0);
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
const loadBrowseBlob = (blobPath) => {
|
|
529
|
+
if (!browseOwner || !browseRepo || !headSha || !blobPath) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
githubGetBlobText(browseOwner, browseRepo, blobPath, headSha).then((text) => {
|
|
533
|
+
let t = text || "(empty or unreadable)";
|
|
534
|
+
if (t.length > MAX_PREVIEW_CHARS) {
|
|
535
|
+
t = t.slice(0, MAX_PREVIEW_CHARS) + "\n\u2026 [truncated at char limit]";
|
|
536
|
+
}
|
|
537
|
+
const linesArr = t.split("\n");
|
|
538
|
+
if (linesArr.length > MAX_PREVIEW_LINES) {
|
|
539
|
+
t = linesArr.slice(0, MAX_PREVIEW_LINES).join("\n") + "\n\u2026 [truncated at line limit]";
|
|
540
|
+
}
|
|
541
|
+
setTreePreviewPath(blobPath);
|
|
542
|
+
setTreePreview(t);
|
|
543
|
+
setTreePreviewScroll(0);
|
|
544
|
+
});
|
|
545
|
+
};
|
|
546
|
+
|
|
88
547
|
useInput((input, key) => {
|
|
548
|
+
if (shellMode && parseSgrMouse(input)) {
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const mouse = parseSgrMouse(input);
|
|
552
|
+
if (mouse) {
|
|
553
|
+
const L = layoutRef.current;
|
|
554
|
+
if (!L) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const { row, col, button, release } = mouse;
|
|
558
|
+
if (col < 2) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (isWheelUp(button) || isWheelDown(button)) {
|
|
563
|
+
const down = isWheelDown(button);
|
|
564
|
+
const step = down ? 3 : -3;
|
|
565
|
+
const hit = regionsRef.current.find((z) => row >= z.r1 && row <= z.r2);
|
|
566
|
+
if (hit?.kind === "preview" && L.browsePreviewOpen && L.treePreviewLinesLen) {
|
|
567
|
+
const pv = Math.max(2, L.browsePreviewH || 2);
|
|
568
|
+
const maxS = Math.max(0, L.treePreviewLinesLen - pv);
|
|
569
|
+
setTreePreviewScroll((s) => Math.max(0, Math.min(s + step, maxS)));
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (hit?.kind === "patch") {
|
|
573
|
+
setFilePatchOffset((i) =>
|
|
574
|
+
Math.max(0, Math.min(i + (down ? 3 : -3), L.maxPatchOffset || 0))
|
|
575
|
+
);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (hit?.kind === "issueLine" || hit?.kind === "reviewLine") {
|
|
579
|
+
if (down) {
|
|
580
|
+
setLineIndex((i) => Math.min(i + 1, Math.max(0, L.listLen - 1)));
|
|
581
|
+
} else {
|
|
582
|
+
setLineIndex((i) => Math.max(i - 1, 0));
|
|
583
|
+
}
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (hit?.kind === "fileLine") {
|
|
587
|
+
if (down) {
|
|
588
|
+
setFileIndex((i) => Math.min(i + 1, Math.max(0, L.fileListLen - 1)));
|
|
589
|
+
setFilePatchOffset(0); setPatchCursorLine(0);
|
|
590
|
+
setFileCommentIndex(0);
|
|
591
|
+
} else {
|
|
592
|
+
setFileIndex((i) => Math.max(i - 1, 0));
|
|
593
|
+
setFilePatchOffset(0); setPatchCursorLine(0);
|
|
594
|
+
setFileCommentIndex(0);
|
|
595
|
+
}
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
if (
|
|
599
|
+
hit?.kind === "treeLine" &&
|
|
600
|
+
L.treeLen > 0 &&
|
|
601
|
+
((L.tab === 4 && !L.browsePreviewOpen) ||
|
|
602
|
+
(L.shellMainTab === 0 && L.shellMode))
|
|
603
|
+
) {
|
|
604
|
+
if (L.shellMode && L.shellMainTab === 0 && L.shellStackFocus && L.len > 0) {
|
|
605
|
+
if (down) {
|
|
606
|
+
setPrIndex((i) => (i < 0 ? 0 : Math.min(i + 1, L.len - 1)));
|
|
607
|
+
} else {
|
|
608
|
+
setPrIndex((i) => (i <= 0 ? -1 : i - 1));
|
|
609
|
+
}
|
|
610
|
+
setTreeLineIndex(0);
|
|
611
|
+
setFilePatchOffset(0); setPatchCursorLine(0);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (L.shellMode && L.shellMainTab === 0 && L.shellStackFocus) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (down) {
|
|
618
|
+
setTreeLineIndex((i) => Math.min(i + 1, Math.max(0, L.treeLen - 1)));
|
|
619
|
+
} else {
|
|
620
|
+
setTreeLineIndex((i) => Math.max(i - 1, 0));
|
|
621
|
+
}
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
if (L.tab === 0 && L.len > 0) {
|
|
625
|
+
if (down) {
|
|
626
|
+
setPrIndex((i) => Math.min(i + 1, L.len - 1));
|
|
627
|
+
} else {
|
|
628
|
+
setPrIndex((i) => Math.max(i - 1, 0));
|
|
629
|
+
}
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if ((L.tab === 1 || L.tab === 2) && L.listLen > 0) {
|
|
633
|
+
if (down) {
|
|
634
|
+
setLineIndex((i) => Math.min(i + 1, Math.max(0, L.listLen - 1)));
|
|
635
|
+
} else {
|
|
636
|
+
setLineIndex((i) => Math.max(i - 1, 0));
|
|
637
|
+
}
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (L.tab === 3) {
|
|
641
|
+
setFilePatchOffset((i) =>
|
|
642
|
+
Math.max(0, Math.min(i + (down ? 3 : -3), L.maxPatchOffset || 0))
|
|
643
|
+
);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (L.tab === 4 && L.browsePreviewOpen && L.treePreviewLinesLen) {
|
|
647
|
+
const pv = Math.max(2, L.browsePreviewH || 2);
|
|
648
|
+
const maxS = Math.max(0, L.treePreviewLinesLen - pv);
|
|
649
|
+
setTreePreviewScroll((s) => Math.max(0, Math.min(s + step, maxS)));
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (L.tab === 4 && L.treeLen > 0) {
|
|
653
|
+
if (down) {
|
|
654
|
+
setTreeLineIndex((i) => Math.min(i + 1, L.treeLen - 1));
|
|
655
|
+
} else {
|
|
656
|
+
setTreeLineIndex((i) => Math.max(i - 1, 0));
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (release) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const rightish = (button & 3) === 2 || button === 34;
|
|
666
|
+
if (rightish && L.browsePreviewOpen) {
|
|
667
|
+
closeBrowsePreview();
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const nLad = L.nLad;
|
|
672
|
+
if (row >= 3 && row <= 2 + nLad) {
|
|
673
|
+
setPrIndex(L.ladderStart + (row - 3));
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
for (const z of regionsRef.current) {
|
|
678
|
+
if (row < z.r1 || row > z.r2) {
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (z.kind === "issueLine") {
|
|
682
|
+
setLineIndex(/** @type {number} */ (z.index));
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (z.kind === "reviewLine") {
|
|
686
|
+
setLineIndex(/** @type {number} */ (z.index));
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (z.kind === "fileLine") {
|
|
690
|
+
setFileIndex(/** @type {number} */ (z.index));
|
|
691
|
+
setFilePatchOffset(0); setPatchCursorLine(0);
|
|
692
|
+
setFileCommentIndex(0);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (z.kind === "treeLine") {
|
|
696
|
+
const idx = /** @type {number} */ (z.index);
|
|
697
|
+
const p = /** @type {string | null | undefined} */ (z.path);
|
|
698
|
+
setTreeLineIndex(idx);
|
|
699
|
+
const now = Date.now();
|
|
700
|
+
const prev = lastTreeClickRef.current;
|
|
701
|
+
const dbl = prev && prev.idx === idx && now - prev.t < 500;
|
|
702
|
+
lastTreeClickRef.current = { t: now, idx };
|
|
703
|
+
if (dbl && p) {
|
|
704
|
+
loadBrowseBlob(p);
|
|
705
|
+
}
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (z.kind === "preview" || z.kind === "patch") {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const rScroll = patchScrollRef.current;
|
|
716
|
+
if (shellMode && shellMainTab === 0 && rScroll.canScrollMergedPreview) {
|
|
717
|
+
if (key.pageDown) {
|
|
718
|
+
setTreePreviewScroll((i) => Math.min(i + rScroll.mergedStep, rScroll.mergedMaxScroll));
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
if (key.pageUp) {
|
|
722
|
+
setTreePreviewScroll((i) => Math.max(i - rScroll.mergedStep, 0));
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
const shellPatchScroll =
|
|
727
|
+
shellMode &&
|
|
728
|
+
shellMainTab === 0 &&
|
|
729
|
+
asciiTreeRows[treeSafeLine]?.path === String(selectedFile?.filename || "") &&
|
|
730
|
+
patchLines.length > 0 &&
|
|
731
|
+
!browsePreviewOpen;
|
|
732
|
+
if (shellPatchScroll) {
|
|
733
|
+
if (key.pageDown) {
|
|
734
|
+
const { step, maxPatchOffset: maxO } = patchScrollRef.current;
|
|
735
|
+
setFilePatchOffset((i) => Math.min(i + step, maxO));
|
|
736
|
+
setPatchCursorLine((c) => Math.min(c + step, patchLines.length - 1));
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (key.pageUp) {
|
|
740
|
+
const { step } = patchScrollRef.current;
|
|
741
|
+
setFilePatchOffset((i) => Math.max(i - step, 0));
|
|
742
|
+
setPatchCursorLine((c) => Math.max(c - step, 0));
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (input === "?") {
|
|
748
|
+
setShowFullHelp((v) => !v);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (input === "C" && shellPatchScroll && row && selectedFile && !row.error) {
|
|
753
|
+
exitPayload.next = {
|
|
754
|
+
type: "pull_review_line_comment",
|
|
755
|
+
prNumber: row.entry.pr_number,
|
|
756
|
+
path: String(selectedFile.filename || ""),
|
|
757
|
+
patchLineIndex: patchCursorLine,
|
|
758
|
+
commitId: headSha
|
|
759
|
+
};
|
|
760
|
+
exit();
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const backKey = key.backspace || key.delete || input === "\x7f" || input === "\b";
|
|
765
|
+
if (backKey) {
|
|
766
|
+
if (browsePreviewOpen) {
|
|
767
|
+
closeBrowsePreview();
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
if (shellMode && shellStackFocus && canPickAlternateStack) {
|
|
771
|
+
exitPayload.next = { type: "pick_stack" };
|
|
772
|
+
exit();
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
if (shellMode && shellStackFocus) {
|
|
776
|
+
setShellStackFocus(false);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (shellMode && shellMainTab === 0) {
|
|
780
|
+
setShellStackFocus(true);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if ((key.return || input === " ") && shellMode && shellStackFocus) {
|
|
786
|
+
if (prIndex < 0 && len > 0) {
|
|
787
|
+
setPrIndex(0);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
setShellStackFocus(false);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (shellMode && /^[1-9]$/.test(input) && len > 0) {
|
|
795
|
+
const n = Number.parseInt(input, 10) - 1;
|
|
796
|
+
if (n < len) {
|
|
797
|
+
setShellStackFocus(false);
|
|
798
|
+
setPrIndex(n);
|
|
799
|
+
setShellMainTab(0);
|
|
800
|
+
setLineIndex(0);
|
|
801
|
+
setTreeLineIndex(0);
|
|
802
|
+
setFilePatchOffset(0); setPatchCursorLine(0);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (shellMode && input === "[" && len > 0) {
|
|
808
|
+
setShellStackFocus(false);
|
|
809
|
+
setPrIndex((i) => (i <= 0 ? -1 : i - 1));
|
|
810
|
+
setTreeLineIndex(0);
|
|
811
|
+
setFilePatchOffset(0); setPatchCursorLine(0);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
if (shellMode && input === "]" && len > 0) {
|
|
815
|
+
setShellStackFocus(false);
|
|
816
|
+
setPrIndex((i) => (i < 0 ? 0 : Math.min(len - 1, i + 1)));
|
|
817
|
+
setTreeLineIndex(0);
|
|
818
|
+
setFilePatchOffset(0); setPatchCursorLine(0);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (input === "O" && row?.pull?.html_url) {
|
|
822
|
+
openUrl(String(row.pull.html_url));
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
|
|
89
826
|
if (input === "q" || key.escape) {
|
|
827
|
+
if (browsePreviewOpen) {
|
|
828
|
+
closeBrowsePreview();
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
if (shellMode && shellStackFocus) {
|
|
832
|
+
setShellStackFocus(false);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
90
835
|
exitPayload.next = { type: "quit" };
|
|
91
836
|
exit();
|
|
92
837
|
return;
|
|
@@ -97,42 +842,215 @@ export function StackInkApp({ rows, exitPayload }) {
|
|
|
97
842
|
return;
|
|
98
843
|
}
|
|
99
844
|
if (key.tab) {
|
|
100
|
-
|
|
845
|
+
if (shellMode && shellStackFocus) {
|
|
846
|
+
setShellStackFocus(false);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
if (shellMode && (!row || len === 0)) {
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (shellMode) {
|
|
853
|
+
setShellMainTab((t) => (t + 1) % SHELL_TAB_COUNT);
|
|
854
|
+
setLineIndex(0);
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
setTab((t) => (t + 1) % TAB_COUNT);
|
|
101
858
|
setLineIndex(0);
|
|
102
|
-
|
|
859
|
+
setTreeLineIndex(0);
|
|
860
|
+
setFilePatchOffset(0); setPatchCursorLine(0);
|
|
103
861
|
setFileCommentIndex(0);
|
|
104
862
|
return;
|
|
105
863
|
}
|
|
106
|
-
if (input === "[") {
|
|
107
|
-
setTab((t) => (t +
|
|
864
|
+
if (!shellMode && input === "[") {
|
|
865
|
+
setTab((t) => (t + TAB_COUNT - 1) % TAB_COUNT);
|
|
108
866
|
setLineIndex(0);
|
|
109
|
-
|
|
867
|
+
setTreeLineIndex(0);
|
|
868
|
+
setFilePatchOffset(0); setPatchCursorLine(0);
|
|
110
869
|
setFileCommentIndex(0);
|
|
111
870
|
return;
|
|
112
871
|
}
|
|
113
|
-
if (input === "]") {
|
|
114
|
-
setTab((t) => (t + 1) %
|
|
872
|
+
if (!shellMode && input === "]") {
|
|
873
|
+
setTab((t) => (t + 1) % TAB_COUNT);
|
|
115
874
|
setLineIndex(0);
|
|
116
|
-
|
|
875
|
+
setTreeLineIndex(0);
|
|
876
|
+
setFilePatchOffset(0); setPatchCursorLine(0);
|
|
877
|
+
setFileCommentIndex(0);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (input === "b" && browsePreviewOpen && (tab === 4 || (shellMode && shellMainTab === 0))) {
|
|
882
|
+
closeBrowsePreview();
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (
|
|
887
|
+
browsePreviewOpen &&
|
|
888
|
+
previewLineCount > 0 &&
|
|
889
|
+
input === " " &&
|
|
890
|
+
(tab === 4 || (shellMode && shellMainTab === 0))
|
|
891
|
+
) {
|
|
892
|
+
const prevH = Math.max(3, Math.floor(bodyMax * 0.35));
|
|
893
|
+
const pv = Math.max(2, bodyMax - prevH - 2);
|
|
894
|
+
const maxS = Math.max(0, previewLineCount - pv);
|
|
895
|
+
setTreePreviewScroll((s) =>
|
|
896
|
+
Math.min(s + Math.max(1, Math.floor(pv / 2)), maxS)
|
|
897
|
+
);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (!shellMode && (key.return || input === " ") && tab === 0 && row?.pull?.html_url) {
|
|
902
|
+
openUrl(String(row.pull.html_url));
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
if ((key.return || input === " ") && tab === 1) {
|
|
906
|
+
const ic = issueList[safeLine];
|
|
907
|
+
if (ic?.html_url) {
|
|
908
|
+
openUrl(String(ic.html_url));
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if ((key.return || input === " ") && tab === 2) {
|
|
913
|
+
const c = reviewList[safeLine];
|
|
914
|
+
if (c?.html_url) {
|
|
915
|
+
openUrl(String(c.html_url));
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if ((key.return || input === " ") && tab === 3 && fileList.length) {
|
|
920
|
+
setFileIndex((i) => Math.min(i + 1, fileList.length - 1));
|
|
921
|
+
setFilePatchOffset(0); setPatchCursorLine(0);
|
|
117
922
|
setFileCommentIndex(0);
|
|
118
923
|
return;
|
|
119
924
|
}
|
|
120
925
|
|
|
926
|
+
if (
|
|
927
|
+
(key.return || input === " ") &&
|
|
928
|
+
tab === 4 &&
|
|
929
|
+
!browsePreviewOpen &&
|
|
930
|
+
!shellMode &&
|
|
931
|
+
browseOwner &&
|
|
932
|
+
browseRepo &&
|
|
933
|
+
headSha &&
|
|
934
|
+
asciiTreeRows[safeLine]
|
|
935
|
+
) {
|
|
936
|
+
const ent = asciiTreeRows[safeLine];
|
|
937
|
+
if (ent.path) {
|
|
938
|
+
loadBrowseBlob(ent.path);
|
|
939
|
+
}
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
if (
|
|
944
|
+
shellMode &&
|
|
945
|
+
shellMainTab === 0 &&
|
|
946
|
+
(key.return || input === " ") &&
|
|
947
|
+
!shellStackFocus &&
|
|
948
|
+
!browsePreviewOpen &&
|
|
949
|
+
browseOwner &&
|
|
950
|
+
browseRepo &&
|
|
951
|
+
headSha &&
|
|
952
|
+
asciiTreeRows[treeSafeLine]
|
|
953
|
+
) {
|
|
954
|
+
const ent = asciiTreeRows[treeSafeLine];
|
|
955
|
+
if (ent.path) {
|
|
956
|
+
loadBrowseBlob(ent.path);
|
|
957
|
+
}
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
|
|
121
961
|
if (input === "j" || key.downArrow) {
|
|
122
|
-
if (
|
|
962
|
+
if (shellMode && shellStackFocus && len > 0) {
|
|
963
|
+
setPrIndex((i) => (i < 0 ? 0 : Math.min(i + 1, len - 1)));
|
|
964
|
+
setTreeLineIndex(0);
|
|
965
|
+
setFilePatchOffset(0); setPatchCursorLine(0);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
if (shellMode && !shellStackFocus && shellMainTab === 0 && len > 0 && prIndex < 0) {
|
|
969
|
+
setShellStackFocus(true);
|
|
970
|
+
setPrIndex(0);
|
|
971
|
+
setTreeLineIndex(0);
|
|
972
|
+
setFilePatchOffset(0); setPatchCursorLine(0);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
if (
|
|
976
|
+
(shellMode && shellMainTab === 0 && browsePreviewOpen && previewLineCount > 0) ||
|
|
977
|
+
(!shellMode && tab === 4 && browsePreviewOpen && previewLineCount > 0)
|
|
978
|
+
) {
|
|
979
|
+
const prevH = Math.max(3, Math.floor(bodyMax * 0.35));
|
|
980
|
+
const pv = Math.max(2, bodyMax - prevH - 2);
|
|
981
|
+
const maxS = Math.max(0, previewLineCount - pv);
|
|
982
|
+
setTreePreviewScroll((s) => Math.min(s + 1, maxS));
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (
|
|
986
|
+
shellMode &&
|
|
987
|
+
shellMainTab === 0 &&
|
|
988
|
+
!shellStackFocus &&
|
|
989
|
+
!browsePreviewOpen &&
|
|
990
|
+
asciiTreeRows.length
|
|
991
|
+
) {
|
|
992
|
+
setTreeLineIndex((i) => Math.min(i + 1, asciiTreeRows.length - 1));
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
if (shellMode && shellMainTab > 0 && listLen > 0) {
|
|
996
|
+
setLineIndex((i) => Math.min(i + 1, Math.max(0, listLen - 1)));
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (tab === 0 && len > 0 && !shellMode) {
|
|
123
1000
|
setPrIndex((i) => Math.min(i + 1, len - 1));
|
|
124
1001
|
} else if (tab === 3) {
|
|
125
1002
|
setFilePatchOffset((i) => Math.min(i + 1, maxPatchOffset));
|
|
1003
|
+
} else if (tab === 4 && asciiTreeRows.length && !shellMode) {
|
|
1004
|
+
setTreeLineIndex((i) => Math.min(i + 1, asciiTreeRows.length - 1));
|
|
126
1005
|
} else {
|
|
127
1006
|
setLineIndex((i) => Math.min(i + 1, Math.max(0, listLen - 1)));
|
|
128
1007
|
}
|
|
129
1008
|
return;
|
|
130
1009
|
}
|
|
131
1010
|
if (input === "k" || key.upArrow) {
|
|
132
|
-
if (
|
|
1011
|
+
if (shellMode && shellStackFocus && len > 0) {
|
|
1012
|
+
setPrIndex((i) => (i <= 0 ? -1 : i - 1));
|
|
1013
|
+
setTreeLineIndex(0);
|
|
1014
|
+
setFilePatchOffset(0); setPatchCursorLine(0);
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
if (shellMode && !shellStackFocus && shellMainTab === 0 && len > 0 && prIndex < 0) {
|
|
1018
|
+
setShellStackFocus(true);
|
|
1019
|
+
setPrIndex(Math.max(0, len - 1));
|
|
1020
|
+
setTreeLineIndex(0);
|
|
1021
|
+
setFilePatchOffset(0); setPatchCursorLine(0);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
if (
|
|
1025
|
+
(shellMode && shellMainTab === 0 && browsePreviewOpen && previewLineCount > 0) ||
|
|
1026
|
+
(!shellMode && tab === 4 && browsePreviewOpen && previewLineCount > 0)
|
|
1027
|
+
) {
|
|
1028
|
+
const prevH = Math.max(3, Math.floor(bodyMax * 0.35));
|
|
1029
|
+
const pv = Math.max(2, bodyMax - prevH - 2);
|
|
1030
|
+
const maxS = Math.max(0, previewLineCount - pv);
|
|
1031
|
+
setTreePreviewScroll((s) => Math.max(s - 1, 0));
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
if (
|
|
1035
|
+
shellMode &&
|
|
1036
|
+
shellMainTab === 0 &&
|
|
1037
|
+
!shellStackFocus &&
|
|
1038
|
+
!browsePreviewOpen &&
|
|
1039
|
+
asciiTreeRows.length
|
|
1040
|
+
) {
|
|
1041
|
+
setTreeLineIndex((i) => Math.max(i - 1, 0));
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
if (shellMode && shellMainTab > 0 && listLen > 0) {
|
|
1045
|
+
setLineIndex((i) => Math.max(i - 1, 0));
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (tab === 0 && len > 0 && !shellMode) {
|
|
133
1049
|
setPrIndex((i) => Math.max(i - 1, 0));
|
|
134
1050
|
} else if (tab === 3) {
|
|
135
1051
|
setFilePatchOffset((i) => Math.max(i - 1, 0));
|
|
1052
|
+
} else if (tab === 4 && asciiTreeRows.length && !shellMode) {
|
|
1053
|
+
setTreeLineIndex((i) => Math.max(i - 1, 0));
|
|
136
1054
|
} else {
|
|
137
1055
|
setLineIndex((i) => Math.max(i - 1, 0));
|
|
138
1056
|
}
|
|
@@ -140,22 +1058,27 @@ export function StackInkApp({ rows, exitPayload }) {
|
|
|
140
1058
|
}
|
|
141
1059
|
|
|
142
1060
|
if (input === "o" && row && !row.error) {
|
|
1061
|
+
if (shellMode) {
|
|
1062
|
+
setShellStackFocus(false);
|
|
1063
|
+
setShellMainTab(0);
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
143
1066
|
setTab(3);
|
|
144
1067
|
setFileIndex(0);
|
|
145
|
-
setFilePatchOffset(0);
|
|
1068
|
+
setFilePatchOffset(0); setPatchCursorLine(0);
|
|
146
1069
|
setFileCommentIndex(0);
|
|
147
1070
|
return;
|
|
148
1071
|
}
|
|
149
1072
|
|
|
150
1073
|
if (tab === 3 && input === "n") {
|
|
151
1074
|
setFileIndex((i) => Math.min(i + 1, Math.max(0, fileList.length - 1)));
|
|
152
|
-
setFilePatchOffset(0);
|
|
1075
|
+
setFilePatchOffset(0); setPatchCursorLine(0);
|
|
153
1076
|
setFileCommentIndex(0);
|
|
154
1077
|
return;
|
|
155
1078
|
}
|
|
156
1079
|
if (tab === 3 && input === "p") {
|
|
157
1080
|
setFileIndex((i) => Math.max(i - 1, 0));
|
|
158
|
-
setFilePatchOffset(0);
|
|
1081
|
+
setFilePatchOffset(0); setPatchCursorLine(0);
|
|
159
1082
|
setFileCommentIndex(0);
|
|
160
1083
|
return;
|
|
161
1084
|
}
|
|
@@ -175,25 +1098,45 @@ export function StackInkApp({ rows, exitPayload }) {
|
|
|
175
1098
|
return;
|
|
176
1099
|
}
|
|
177
1100
|
|
|
178
|
-
if (input === "
|
|
1101
|
+
if (input === "K" && row && !row.error && (!shellMode ? tab === 4 : shellMainTab === 0)) {
|
|
1102
|
+
exitPayload.next = {
|
|
1103
|
+
type: "materialize_clone",
|
|
1104
|
+
prNumber: row.entry.pr_number
|
|
1105
|
+
};
|
|
1106
|
+
exit();
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
const reviewNav =
|
|
1111
|
+
tab === 2 || (shellMode && shellMainTab === 3);
|
|
1112
|
+
if (input === "l" && reviewNav) {
|
|
179
1113
|
const c = reviewList[safeLine];
|
|
180
1114
|
if (c?.html_url) {
|
|
181
1115
|
openUrl(c.html_url);
|
|
182
1116
|
}
|
|
183
1117
|
return;
|
|
184
1118
|
}
|
|
185
|
-
if (input === "g" &&
|
|
1119
|
+
if (input === "g" && reviewNav && row && !row.error) {
|
|
186
1120
|
const c = reviewList[safeLine];
|
|
187
1121
|
const targetPath = String(c?.path || "");
|
|
188
1122
|
if (targetPath) {
|
|
189
1123
|
const idx = fileList.findIndex((f) => String(f?.filename || "") === targetPath);
|
|
190
1124
|
if (idx >= 0) {
|
|
191
|
-
|
|
1125
|
+
if (shellMode) {
|
|
1126
|
+
setShellMainTab(0);
|
|
1127
|
+
setTab(4);
|
|
1128
|
+
const tidx = asciiTreeRows.findIndex((r) => r.path === targetPath);
|
|
1129
|
+
if (tidx >= 0) {
|
|
1130
|
+
setTreeLineIndex(tidx);
|
|
1131
|
+
}
|
|
1132
|
+
} else {
|
|
1133
|
+
setTab(3);
|
|
1134
|
+
}
|
|
192
1135
|
setFileIndex(idx);
|
|
193
1136
|
const selected = fileList[idx];
|
|
194
|
-
const
|
|
1137
|
+
const pls = typeof selected?.patch === "string" ? String(selected.patch).split("\n") : [];
|
|
195
1138
|
const lineNo = c?.line ?? c?.original_line ?? 0;
|
|
196
|
-
const off = patchOffsetForLine(
|
|
1139
|
+
const off = patchOffsetForLine(pls, Number(lineNo) || 0);
|
|
197
1140
|
setFilePatchOffset(off);
|
|
198
1141
|
const fcIdx = reviewList
|
|
199
1142
|
.filter((rc) => String(rc?.path || "") === targetPath)
|
|
@@ -204,7 +1147,7 @@ export function StackInkApp({ rows, exitPayload }) {
|
|
|
204
1147
|
return;
|
|
205
1148
|
}
|
|
206
1149
|
|
|
207
|
-
if (input === "S" &&
|
|
1150
|
+
if (input === "S" && row && !row.error) {
|
|
208
1151
|
exitPayload.next = {
|
|
209
1152
|
type: "split",
|
|
210
1153
|
prNumber: row.entry.pr_number
|
|
@@ -213,6 +1156,26 @@ export function StackInkApp({ rows, exitPayload }) {
|
|
|
213
1156
|
return;
|
|
214
1157
|
}
|
|
215
1158
|
|
|
1159
|
+
if (input === "a" && row && !row.error) {
|
|
1160
|
+
exitPayload.next = {
|
|
1161
|
+
type: "submit_review",
|
|
1162
|
+
prNumber: row.entry.pr_number,
|
|
1163
|
+
event: "APPROVE",
|
|
1164
|
+
body: ""
|
|
1165
|
+
};
|
|
1166
|
+
exit();
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
if (input === "e" && row && !row.error) {
|
|
1171
|
+
exitPayload.next = {
|
|
1172
|
+
type: "submit_review_changes",
|
|
1173
|
+
prNumber: row.entry.pr_number
|
|
1174
|
+
};
|
|
1175
|
+
exit();
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
216
1179
|
if (input === "r" && row && !row.error) {
|
|
217
1180
|
exitPayload.next = {
|
|
218
1181
|
type: "issue_comment",
|
|
@@ -231,7 +1194,7 @@ export function StackInkApp({ rows, exitPayload }) {
|
|
|
231
1194
|
return;
|
|
232
1195
|
}
|
|
233
1196
|
|
|
234
|
-
if (input === "t" &&
|
|
1197
|
+
if (input === "t" && reviewNav && row && !row.error) {
|
|
235
1198
|
const c = reviewList[safeLine];
|
|
236
1199
|
if (c?.id != null) {
|
|
237
1200
|
exitPayload.next = {
|
|
@@ -244,178 +1207,912 @@ export function StackInkApp({ rows, exitPayload }) {
|
|
|
244
1207
|
});
|
|
245
1208
|
|
|
246
1209
|
if (len === 0) {
|
|
1210
|
+
layoutRef.current = null;
|
|
1211
|
+
regionsRef.current = [];
|
|
247
1212
|
return React.createElement(
|
|
248
1213
|
Box,
|
|
249
|
-
{ flexDirection: "column", padding: 1 },
|
|
1214
|
+
{ flexDirection: "column", width: cols, padding: 1 },
|
|
250
1215
|
React.createElement(Text, { color: "red" }, "No PRs in stack."),
|
|
251
1216
|
React.createElement(Text, { dimColor: true }, "Press q to quit.")
|
|
252
1217
|
);
|
|
253
1218
|
}
|
|
254
1219
|
|
|
255
|
-
const
|
|
256
|
-
|
|
1220
|
+
const ladderTruncW = shellMode ? Math.max(8, sidebarW - 2) : innerW;
|
|
1221
|
+
const ladderBlinkOn = shellStackFocus && Math.floor(pulse / 2) % 2 === 0;
|
|
1222
|
+
const ladderScrollAnchor = hasPrPick ? safePr : 0;
|
|
1223
|
+
const ladderRows = stackRows.map((r, i) => {
|
|
1224
|
+
const mark = hasPrPick && i === safePr ? ">" : " ";
|
|
257
1225
|
const err = r.error ? ` ${r.error}` : "";
|
|
258
1226
|
const title = r.pull?.title || err || "(loading)";
|
|
259
1227
|
const num = r.entry.pr_number;
|
|
260
1228
|
const st = r.pull?.draft ? "draft" : r.pull?.state || "?";
|
|
261
1229
|
const reviewState = String(r.reviewSummary || "none");
|
|
1230
|
+
const vm = r.viewerReviewMeta;
|
|
1231
|
+
const stale = vm?.staleApproval ? "!" : "";
|
|
1232
|
+
const risky = vm?.riskyChangeAfterApproval === true ? "*" : "";
|
|
262
1233
|
const badge =
|
|
263
1234
|
reviewState === "approved"
|
|
264
|
-
?
|
|
1235
|
+
? "A"
|
|
265
1236
|
: reviewState === "changes_requested"
|
|
266
|
-
?
|
|
1237
|
+
? "CR"
|
|
267
1238
|
: reviewState === "commented" || (r.reviewComments?.length || 0) > 0
|
|
268
|
-
?
|
|
269
|
-
:
|
|
270
|
-
|
|
1239
|
+
? "C"
|
|
1240
|
+
: "-";
|
|
1241
|
+
const { add, del } = prAddDelTotals(r);
|
|
1242
|
+
const head = `${mark} #${num} `;
|
|
1243
|
+
const statPlain = `+${add} -${del} `;
|
|
1244
|
+
const meta = `[${st}] ${badge}${stale}${risky} `;
|
|
1245
|
+
const titleBudget = Math.max(4, ladderTruncW - head.length - statPlain.length - meta.length);
|
|
1246
|
+
const titleT = truncVis(title, titleBudget);
|
|
1247
|
+
const lineColored =
|
|
1248
|
+
chalk.white(head) +
|
|
1249
|
+
chalk.green(`+${add}`) +
|
|
1250
|
+
chalk.white(" ") +
|
|
1251
|
+
chalk.red(`-${del}`) +
|
|
1252
|
+
chalk.white(` ${meta}${titleT}`);
|
|
1253
|
+
if (hasPrPick && i === safePr) {
|
|
1254
|
+
if (shellMode && shellStackFocus) {
|
|
1255
|
+
return ladderBlinkOn ? chalk.inverse(lineColored) : lineColored;
|
|
1256
|
+
}
|
|
1257
|
+
return chalk.inverse(lineColored);
|
|
1258
|
+
}
|
|
1259
|
+
return lineColored;
|
|
271
1260
|
});
|
|
1261
|
+
const { slice: ladderVis, start: ladderStart } = sliceViewport(
|
|
1262
|
+
ladderRows,
|
|
1263
|
+
ladderScrollAnchor,
|
|
1264
|
+
ladderMaxForDisplay
|
|
1265
|
+
);
|
|
272
1266
|
|
|
273
|
-
const tabName = ["overview", "conversation", "review", "files"][tab];
|
|
1267
|
+
const tabName = ["overview", "conversation", "review", "files", "browse"][tab];
|
|
1268
|
+
const pageLabel = tabName;
|
|
1269
|
+
const nLad = ladderVis.length;
|
|
1270
|
+
const bodyStartR = shellMode ? 6 : 4 + nLad;
|
|
1271
|
+
let rowPtr = bodyStartR;
|
|
1272
|
+
/** @type {string[]} */
|
|
274
1273
|
let bodyLines = [];
|
|
1274
|
+
/** @type {Array<{ r1: number, r2: number, kind: string, [k: string]: unknown }>} */
|
|
1275
|
+
let regions = [];
|
|
1276
|
+
let layoutBrowsePreviewH = 0;
|
|
1277
|
+
let layoutTreeBlockH = 0;
|
|
1278
|
+
|
|
1279
|
+
const pushBody = (line) => {
|
|
1280
|
+
const r = rowPtr;
|
|
1281
|
+
bodyLines.push(line);
|
|
1282
|
+
rowPtr++;
|
|
1283
|
+
return r;
|
|
1284
|
+
};
|
|
1285
|
+
|
|
275
1286
|
if (row?.error) {
|
|
276
|
-
|
|
277
|
-
} else if (
|
|
1287
|
+
pushBody(truncVis(row.error, innerW));
|
|
1288
|
+
} else if (shellMode && !row) {
|
|
1289
|
+
if (shellMainTab === 0) {
|
|
1290
|
+
pushBody(
|
|
1291
|
+
chalk.dim(
|
|
1292
|
+
"Pick a PR from the ladder: j/k or [ ] \u00b7 Enter selects and moves focus to the file tree"
|
|
1293
|
+
)
|
|
1294
|
+
);
|
|
1295
|
+
} else {
|
|
1296
|
+
pushBody(chalk.dim("Select a PR in the sidebar to use this tab."));
|
|
1297
|
+
}
|
|
1298
|
+
} else if (shellMode && row?.pull) {
|
|
1299
|
+
if (shellMainTab === 0) {
|
|
1300
|
+
const selPath = asciiTreeRows[treeSafeLine]?.path;
|
|
1301
|
+
const showPatch =
|
|
1302
|
+
!!selPath &&
|
|
1303
|
+
selPath === String(selectedFile?.filename || "") &&
|
|
1304
|
+
patchLines.length > 0 &&
|
|
1305
|
+
!browsePreviewOpen;
|
|
1306
|
+
|
|
1307
|
+
const panes =
|
|
1308
|
+
shellTreePanes ||
|
|
1309
|
+
computeShellTreePaneHeights(bodyMax, browsePreviewOpen, showPatch);
|
|
1310
|
+
const treeBlockH = panes.treeBlockH;
|
|
1311
|
+
const patchH = showPatch ? panes.patchH : 0;
|
|
1312
|
+
const previewH = browsePreviewOpen ? panes.previewH : 0;
|
|
1313
|
+
layoutTreeBlockH = treeBlockH;
|
|
1314
|
+
layoutBrowsePreviewH = previewH;
|
|
1315
|
+
|
|
1316
|
+
if (treeLoading) {
|
|
1317
|
+
pushBody("Loading tree\u2026");
|
|
1318
|
+
} else if (treeError) {
|
|
1319
|
+
pushBody(truncVis(treeError, innerW));
|
|
1320
|
+
}
|
|
1321
|
+
const previewPathOpen = browsePreviewOpen && treePreviewPath ? treePreviewPath : "";
|
|
1322
|
+
const treeDisplay = asciiTreeRows.map((r, i) => {
|
|
1323
|
+
const mark = i === treeSafeLine ? ">" : " ";
|
|
1324
|
+
const f = r.path ? pathToChangedFile.get(r.path) : undefined;
|
|
1325
|
+
const add = f ? Number(f.additions) || 0 : 0;
|
|
1326
|
+
const del = f ? Number(f.deletions) || 0 : 0;
|
|
1327
|
+
const flag = f
|
|
1328
|
+
? add > 0 && del > 0
|
|
1329
|
+
? chalk.yellow("*")
|
|
1330
|
+
: add > 0
|
|
1331
|
+
? chalk.green("+")
|
|
1332
|
+
: del > 0
|
|
1333
|
+
? chalk.red("-")
|
|
1334
|
+
: chalk.cyan("*")
|
|
1335
|
+
: " ";
|
|
1336
|
+
const tw = Math.max(8, innerW - 4);
|
|
1337
|
+
const plainT = truncVis(r.text, tw);
|
|
1338
|
+
let lineText = plainT;
|
|
1339
|
+
if (f) {
|
|
1340
|
+
if (add > 0 && del > 0) {
|
|
1341
|
+
lineText = chalk.yellow(plainT);
|
|
1342
|
+
} else if (add > 0) {
|
|
1343
|
+
lineText = chalk.green(plainT);
|
|
1344
|
+
} else if (del > 0) {
|
|
1345
|
+
lineText = chalk.red(plainT);
|
|
1346
|
+
} else {
|
|
1347
|
+
lineText = chalk.cyan(plainT);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
if (r.path && previewPathOpen && r.path === previewPathOpen) {
|
|
1351
|
+
lineText = chalk.inverse(lineText);
|
|
1352
|
+
}
|
|
1353
|
+
return `${mark}${flag} ${lineText}`;
|
|
1354
|
+
});
|
|
1355
|
+
const { slice: treeSlice, start: treeStart } = sliceViewport(treeDisplay, treeSafeLine, treeBlockH);
|
|
1356
|
+
if (treeStart > 0) {
|
|
1357
|
+
pushBody(chalk.dim(`\u2191 tree ${treeStart} more`));
|
|
1358
|
+
}
|
|
1359
|
+
const treeSliceStartR = rowPtr;
|
|
1360
|
+
for (const ln of treeSlice) {
|
|
1361
|
+
pushBody(ln);
|
|
1362
|
+
}
|
|
1363
|
+
for (let li = 0; li < treeSlice.length; li++) {
|
|
1364
|
+
const idx = treeStart + li;
|
|
1365
|
+
const tr = asciiTreeRows[idx];
|
|
1366
|
+
regions.push({
|
|
1367
|
+
kind: "treeLine",
|
|
1368
|
+
r1: treeSliceStartR + li,
|
|
1369
|
+
r2: treeSliceStartR + li,
|
|
1370
|
+
index: idx,
|
|
1371
|
+
path: tr?.path ?? null
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
if (treeStart + treeSlice.length < treeDisplay.length) {
|
|
1375
|
+
pushBody(chalk.dim(`\u2193 tree ${treeDisplay.length - treeStart - treeSlice.length} more`));
|
|
1376
|
+
}
|
|
1377
|
+
if (!treeLoading && asciiTreeRows.length === 0 && !treeError) {
|
|
1378
|
+
pushBody("(empty tree)");
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
if (showPatch) {
|
|
1382
|
+
pushBody("");
|
|
1383
|
+
const safePatchCursor = Math.min(patchCursorLine, patchLines.length - 1);
|
|
1384
|
+
pushBody(
|
|
1385
|
+
chalk.bold(
|
|
1386
|
+
truncVis(
|
|
1387
|
+
`PR diff: ${String(selectedFile?.filename || "?")} (${filePatchOffset + 1}\u2013${Math.min(filePatchOffset + patchPageSize, patchLines.length)}/${patchLines.length}) C: comment on line`,
|
|
1388
|
+
innerW
|
|
1389
|
+
)
|
|
1390
|
+
)
|
|
1391
|
+
);
|
|
1392
|
+
const page = patchLines.slice(filePatchOffset, filePatchOffset + patchPageSize);
|
|
1393
|
+
let used = bodyLines.length;
|
|
1394
|
+
const patchStartR = rowPtr;
|
|
1395
|
+
for (let pi = 0; pi < page.length; pi++) {
|
|
1396
|
+
if (bodyLines.length - used >= patchH) {
|
|
1397
|
+
break;
|
|
1398
|
+
}
|
|
1399
|
+
const absIdx = filePatchOffset + pi;
|
|
1400
|
+
const isCursor = absIdx === safePatchCursor;
|
|
1401
|
+
const pl = page[pi];
|
|
1402
|
+
let line = pl;
|
|
1403
|
+
if (line.startsWith("+++ ") || line.startsWith("--- ")) {
|
|
1404
|
+
line = chalk.bold(line);
|
|
1405
|
+
} else if (line.startsWith("@@")) {
|
|
1406
|
+
line = chalk.cyan(line);
|
|
1407
|
+
} else if (line.startsWith("+")) {
|
|
1408
|
+
line = chalk.green(line);
|
|
1409
|
+
} else if (line.startsWith("-")) {
|
|
1410
|
+
line = chalk.red(line);
|
|
1411
|
+
} else {
|
|
1412
|
+
line = chalk.gray(line);
|
|
1413
|
+
}
|
|
1414
|
+
if (isCursor) {
|
|
1415
|
+
line = chalk.inverse(line);
|
|
1416
|
+
}
|
|
1417
|
+
pushBody(truncVis(line, innerW));
|
|
1418
|
+
}
|
|
1419
|
+
const patchEndR = rowPtr - 1;
|
|
1420
|
+
if (patchEndR >= patchStartR) {
|
|
1421
|
+
regions.push({ kind: "patch", r1: patchStartR, r2: patchEndR });
|
|
1422
|
+
}
|
|
1423
|
+
if (filePatchOffset + patchPageSize < patchLines.length) {
|
|
1424
|
+
pushBody(chalk.dim("PgUp/Dn scroll · C: comment on highlighted line"));
|
|
1425
|
+
}
|
|
1426
|
+
} else if (!browsePreviewOpen && !showPatch && asciiTreeRows.length && !treeLoading) {
|
|
1427
|
+
pushBody(
|
|
1428
|
+
chalk.dim(
|
|
1429
|
+
"Highlight a path marked +/−/* for this PR\u2019s diff. Enter opens file text."
|
|
1430
|
+
)
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
if (browsePreviewOpen && treePreviewPath) {
|
|
1435
|
+
pushBody("");
|
|
1436
|
+
const merged = mergedPreviewRows;
|
|
1437
|
+
const sub =
|
|
1438
|
+
merged && merged.length > 0
|
|
1439
|
+
? "diff in file "
|
|
1440
|
+
: "";
|
|
1441
|
+
pushBody(
|
|
1442
|
+
chalk.bold(truncVis(`File: ${treePreviewPath}`, innerW)) +
|
|
1443
|
+
chalk.whiteBright(
|
|
1444
|
+
` ${sub}[${treePreviewScroll + 1}-${Math.min(treePreviewScroll + previewH, previewLineCount)}/${previewLineCount}] wheel/Space scroll Backspace closes`
|
|
1445
|
+
)
|
|
1446
|
+
);
|
|
1447
|
+
const prevSlice = merged
|
|
1448
|
+
? merged.slice(treePreviewScroll, treePreviewScroll + previewH)
|
|
1449
|
+
: treePreviewLines.slice(treePreviewScroll, treePreviewScroll + previewH).map((t) => ({
|
|
1450
|
+
text: t,
|
|
1451
|
+
kind: /** @type {"ctx"} */ ("ctx")
|
|
1452
|
+
}));
|
|
1453
|
+
const prevStartR = rowPtr;
|
|
1454
|
+
for (const row of prevSlice) {
|
|
1455
|
+
const tv = truncVis(row.text, innerW);
|
|
1456
|
+
const styled =
|
|
1457
|
+
row.kind === "add"
|
|
1458
|
+
? chalk.green(tv)
|
|
1459
|
+
: row.kind === "del"
|
|
1460
|
+
? chalk.red(tv)
|
|
1461
|
+
: chalk.whiteBright(tv);
|
|
1462
|
+
pushBody(styled);
|
|
1463
|
+
}
|
|
1464
|
+
const prevEndR = rowPtr - 1;
|
|
1465
|
+
if (prevEndR >= prevStartR) {
|
|
1466
|
+
regions.push({ kind: "preview", r1: prevStartR, r2: prevEndR });
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
} else if (shellMainTab === 1) {
|
|
1470
|
+
const bodyRaw = typeof row.pull.body === "string" ? row.pull.body : "";
|
|
1471
|
+
const plain = markdownToPlainLines(bodyRaw);
|
|
1472
|
+
const bl = plain ? plain.split("\n") : [];
|
|
1473
|
+
const lines = bl.length ? bl : ["(no description)"];
|
|
1474
|
+
const { slice, start } = sliceViewport(lines, safeLine, bodyMax);
|
|
1475
|
+
if (start > 0) {
|
|
1476
|
+
pushBody(chalk.dim(`\u2191 ${start} lines above`));
|
|
1477
|
+
}
|
|
1478
|
+
for (let si = 0; si < slice.length; si++) {
|
|
1479
|
+
const ln = slice[si];
|
|
1480
|
+
const gi = start + si;
|
|
1481
|
+
const out = truncVis(ln, innerW);
|
|
1482
|
+
pushBody(gi === safeLine ? chalk.inverse(out) : out);
|
|
1483
|
+
}
|
|
1484
|
+
if (start + slice.length < lines.length) {
|
|
1485
|
+
pushBody(chalk.dim(`\u2193 ${lines.length - start - slice.length} more`));
|
|
1486
|
+
}
|
|
1487
|
+
} else if (shellMainTab === 2) {
|
|
1488
|
+
const lines = issueList.map((c, i) => {
|
|
1489
|
+
const mark = i === safeLine ? ">" : " ";
|
|
1490
|
+
const who = c.user?.login || "?";
|
|
1491
|
+
const one = (c.body || "").split("\n")[0];
|
|
1492
|
+
const t = truncVis(`${mark} @${who}: ${one}`, innerW);
|
|
1493
|
+
return i === safeLine ? chalk.inverse(t) : t;
|
|
1494
|
+
});
|
|
1495
|
+
const { slice, start } = sliceViewport(lines, safeLine, bodyMax);
|
|
1496
|
+
if (start > 0) {
|
|
1497
|
+
pushBody(chalk.dim(`\u2191 ${start} more`));
|
|
1498
|
+
}
|
|
1499
|
+
const sliceStartR = rowPtr;
|
|
1500
|
+
for (const ln of slice) {
|
|
1501
|
+
pushBody(ln);
|
|
1502
|
+
}
|
|
1503
|
+
for (let li = 0; li < slice.length; li++) {
|
|
1504
|
+
regions.push({
|
|
1505
|
+
kind: "issueLine",
|
|
1506
|
+
r1: sliceStartR + li,
|
|
1507
|
+
r2: sliceStartR + li,
|
|
1508
|
+
index: start + li
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
if (start + slice.length < lines.length) {
|
|
1512
|
+
pushBody(chalk.dim(`\u2193 ${lines.length - start - slice.length} more`));
|
|
1513
|
+
}
|
|
1514
|
+
if (lines.length === 0) {
|
|
1515
|
+
pushBody("(no conversation comments)");
|
|
1516
|
+
}
|
|
1517
|
+
} else if (shellMainTab === 3) {
|
|
1518
|
+
const lines = reviewList.map((c, i) => {
|
|
1519
|
+
const mark = i === safeLine ? ">" : " ";
|
|
1520
|
+
const path = c.path || "?";
|
|
1521
|
+
const ln = c.line ?? c.original_line ?? "?";
|
|
1522
|
+
const one = (c.body || "").split("\n")[0];
|
|
1523
|
+
const t = truncVis(`${mark} ${path}:${ln} ${one}`, innerW);
|
|
1524
|
+
return i === safeLine ? chalk.inverse(t) : t;
|
|
1525
|
+
});
|
|
1526
|
+
const { slice, start } = sliceViewport(lines, safeLine, bodyMax);
|
|
1527
|
+
if (start > 0) {
|
|
1528
|
+
pushBody(chalk.dim(`\u2191 ${start} more`));
|
|
1529
|
+
}
|
|
1530
|
+
const sliceStartR = rowPtr;
|
|
1531
|
+
for (const ln of slice) {
|
|
1532
|
+
pushBody(ln);
|
|
1533
|
+
}
|
|
1534
|
+
for (let li = 0; li < slice.length; li++) {
|
|
1535
|
+
regions.push({
|
|
1536
|
+
kind: "reviewLine",
|
|
1537
|
+
r1: sliceStartR + li,
|
|
1538
|
+
r2: sliceStartR + li,
|
|
1539
|
+
index: start + li
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
if (start + slice.length < lines.length) {
|
|
1543
|
+
pushBody(chalk.dim(`\u2193 ${lines.length - start - slice.length} more`));
|
|
1544
|
+
}
|
|
1545
|
+
if (lines.length === 0) {
|
|
1546
|
+
pushBody("(no review thread comments)");
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
} else if (tab === 0 && row?.pull && !shellMode) {
|
|
278
1550
|
const p = row.pull;
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
`Comments: ${issueList.length} conv / ${reviewList.length} review (line)
|
|
283
|
-
|
|
1551
|
+
pushBody(truncVis(`Title: ${p.title || ""}`, innerW));
|
|
1552
|
+
pushBody(truncVis(`Head: ${p.head?.ref || ""} Base: ${p.base?.ref || ""}`, innerW));
|
|
1553
|
+
pushBody(
|
|
1554
|
+
truncVis(`Comments: ${issueList.length} conv / ${reviewList.length} review (line)`, innerW)
|
|
1555
|
+
);
|
|
284
1556
|
const rs = String(row.reviewSummary || "none");
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
1557
|
+
pushBody(
|
|
1558
|
+
truncVis(
|
|
1559
|
+
`Review: ${
|
|
1560
|
+
rs === "approved"
|
|
1561
|
+
? "approved"
|
|
1562
|
+
: rs === "changes_requested"
|
|
1563
|
+
? "changes requested"
|
|
1564
|
+
: rs === "commented"
|
|
1565
|
+
? "commented"
|
|
1566
|
+
: "no review state"
|
|
1567
|
+
}`,
|
|
1568
|
+
innerW
|
|
1569
|
+
)
|
|
295
1570
|
);
|
|
296
|
-
|
|
297
|
-
|
|
1571
|
+
const vm = row.viewerReviewMeta;
|
|
1572
|
+
if (vm) {
|
|
1573
|
+
if (vm.dismissedReview) {
|
|
1574
|
+
pushBody(truncVis("Note: a prior review may have been dismissed/superseded.", innerW));
|
|
1575
|
+
}
|
|
1576
|
+
if (vm.staleApproval) {
|
|
1577
|
+
pushBody(
|
|
1578
|
+
truncVis(
|
|
1579
|
+
`Your approval is on an older commit (+${vm.commitsSinceApproval} commits). Press a to re-approve.`,
|
|
1580
|
+
innerW
|
|
1581
|
+
)
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
if (vm.riskyChangeAfterApproval === true) {
|
|
1585
|
+
pushBody(truncVis("Commits since your approval look non-merge — please re-read.", innerW));
|
|
1586
|
+
} else if (vm.riskyChangeAfterApproval === false && vm.staleApproval) {
|
|
1587
|
+
pushBody(truncVis("Delta since your approval looks merge-only.", innerW));
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
} else if (tab === 1 && !shellMode) {
|
|
1591
|
+
const lines = issueList.map((c, i) => {
|
|
298
1592
|
const mark = i === safeLine ? ">" : " ";
|
|
299
1593
|
const who = c.user?.login || "?";
|
|
300
|
-
const one = (c.body || "").split("\n")[0]
|
|
301
|
-
|
|
1594
|
+
const one = (c.body || "").split("\n")[0];
|
|
1595
|
+
return truncVis(`${mark} @${who}: ${one}`, innerW);
|
|
302
1596
|
});
|
|
303
|
-
|
|
304
|
-
|
|
1597
|
+
const { slice, start } = sliceViewport(lines, safeLine, bodyMax);
|
|
1598
|
+
if (start > 0) {
|
|
1599
|
+
pushBody(chalk.dim(`\u2191 ${start} more`));
|
|
305
1600
|
}
|
|
306
|
-
|
|
307
|
-
|
|
1601
|
+
const sliceStartR = rowPtr;
|
|
1602
|
+
for (const ln of slice) {
|
|
1603
|
+
pushBody(ln);
|
|
1604
|
+
}
|
|
1605
|
+
for (let li = 0; li < slice.length; li++) {
|
|
1606
|
+
regions.push({
|
|
1607
|
+
kind: "issueLine",
|
|
1608
|
+
r1: sliceStartR + li,
|
|
1609
|
+
r2: sliceStartR + li,
|
|
1610
|
+
index: start + li
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
if (start + slice.length < lines.length) {
|
|
1614
|
+
pushBody(chalk.dim(`\u2193 ${lines.length - start - slice.length} more`));
|
|
1615
|
+
}
|
|
1616
|
+
if (lines.length === 0) {
|
|
1617
|
+
pushBody("(no issue comments)");
|
|
1618
|
+
}
|
|
1619
|
+
} else if (tab === 2 && !shellMode) {
|
|
1620
|
+
const lines = reviewList.map((c, i) => {
|
|
308
1621
|
const mark = i === safeLine ? ">" : " ";
|
|
309
1622
|
const path = c.path || "?";
|
|
310
1623
|
const ln = c.line ?? c.original_line ?? "?";
|
|
311
|
-
const one = (c.body || "").split("\n")[0]
|
|
312
|
-
|
|
1624
|
+
const one = (c.body || "").split("\n")[0];
|
|
1625
|
+
return truncVis(`${mark} ${path}:${ln} ${one}`, innerW);
|
|
313
1626
|
});
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
1627
|
+
const { slice, start } = sliceViewport(lines, safeLine, bodyMax);
|
|
1628
|
+
if (start > 0) {
|
|
1629
|
+
pushBody(chalk.dim(`\u2191 ${start} more`));
|
|
1630
|
+
}
|
|
1631
|
+
const sliceStartR = rowPtr;
|
|
1632
|
+
for (const ln of slice) {
|
|
1633
|
+
pushBody(ln);
|
|
1634
|
+
}
|
|
1635
|
+
for (let li = 0; li < slice.length; li++) {
|
|
1636
|
+
regions.push({
|
|
1637
|
+
kind: "reviewLine",
|
|
1638
|
+
r1: sliceStartR + li,
|
|
1639
|
+
r2: sliceStartR + li,
|
|
1640
|
+
index: start + li
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
if (start + slice.length < lines.length) {
|
|
1644
|
+
pushBody(chalk.dim(`\u2193 ${lines.length - start - slice.length} more`));
|
|
1645
|
+
}
|
|
1646
|
+
if (lines.length === 0) {
|
|
1647
|
+
pushBody("(no review comments)");
|
|
1648
|
+
pushBody(chalk.dim("Tip: press g on a review comment to jump to file diff"));
|
|
317
1649
|
}
|
|
318
1650
|
} else if (tab === 3) {
|
|
319
|
-
const
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
const f = fileList[i];
|
|
1651
|
+
const fileListH = Math.min(6, Math.max(3, Math.floor(bodyMax * 0.28)));
|
|
1652
|
+
const patchH = Math.max(3, bodyMax - fileListH - 3);
|
|
1653
|
+
const flLines = fileList.map((f, i) => {
|
|
323
1654
|
const mark = i === safeFile ? ">" : " ";
|
|
324
1655
|
const name = String(f.filename || "?");
|
|
325
1656
|
const st = String(f.status || "?");
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
1657
|
+
const add = Number(f.additions) || 0;
|
|
1658
|
+
const del = Number(f.deletions) || 0;
|
|
1659
|
+
const ch = `+${add} -${del}`;
|
|
1660
|
+
const meta = chalk.gray(`[${st}] ${ch}`);
|
|
1661
|
+
const nameTrunc = truncVis(name, Math.max(12, innerW - 18));
|
|
1662
|
+
let nameStyled = chalk.whiteBright(nameTrunc);
|
|
1663
|
+
if (add > 0 && del > 0) {
|
|
1664
|
+
nameStyled = chalk.yellow(nameTrunc);
|
|
1665
|
+
} else if (add > 0) {
|
|
1666
|
+
nameStyled = chalk.green(nameTrunc);
|
|
1667
|
+
} else if (del > 0) {
|
|
1668
|
+
nameStyled = chalk.red(nameTrunc);
|
|
1669
|
+
}
|
|
1670
|
+
return `${mark} ${meta} ${nameStyled}`;
|
|
1671
|
+
});
|
|
1672
|
+
const { slice: flSlice, start: flStart } = sliceViewport(flLines, safeFile, fileListH);
|
|
337
1673
|
if (fileList.length === 0) {
|
|
338
|
-
|
|
339
|
-
} else
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
)
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
1674
|
+
pushBody("(no changed files)");
|
|
1675
|
+
} else {
|
|
1676
|
+
if (flStart > 0) {
|
|
1677
|
+
pushBody(chalk.dim(`\u2191 files ${flStart} more`));
|
|
1678
|
+
}
|
|
1679
|
+
const flStartR = rowPtr;
|
|
1680
|
+
for (const ln of flSlice) {
|
|
1681
|
+
pushBody(ln);
|
|
1682
|
+
}
|
|
1683
|
+
for (let li = 0; li < flSlice.length; li++) {
|
|
1684
|
+
regions.push({
|
|
1685
|
+
kind: "fileLine",
|
|
1686
|
+
r1: flStartR + li,
|
|
1687
|
+
r2: flStartR + li,
|
|
1688
|
+
index: flStart + li
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
if (flStart + flSlice.length < flLines.length) {
|
|
1692
|
+
pushBody(chalk.dim(`\u2193 files ${flLines.length - flStart - flSlice.length} more`));
|
|
1693
|
+
}
|
|
1694
|
+
if (selectedFile) {
|
|
1695
|
+
pushBody("");
|
|
1696
|
+
pushBody(
|
|
1697
|
+
truncVis(
|
|
1698
|
+
`Patch: ${String(selectedFile.filename || "?")} (${filePatchOffset + 1}-${Math.min(filePatchOffset + patchPageSize, patchLines.length)}/${patchLines.length || 0})`,
|
|
1699
|
+
innerW
|
|
1700
|
+
)
|
|
1701
|
+
);
|
|
1702
|
+
const patch = typeof selectedFile.patch === "string" ? selectedFile.patch : "";
|
|
1703
|
+
if (!patch) {
|
|
1704
|
+
pushBody("(patch not available from GitHub API for this file)");
|
|
1705
|
+
} else {
|
|
1706
|
+
const page = patchLines.slice(filePatchOffset, filePatchOffset + patchPageSize);
|
|
1707
|
+
let used = bodyLines.length;
|
|
1708
|
+
const patchStartR = rowPtr;
|
|
1709
|
+
for (const pl of page) {
|
|
1710
|
+
if (bodyLines.length - used >= patchH) {
|
|
1711
|
+
break;
|
|
1712
|
+
}
|
|
1713
|
+
let line = pl;
|
|
1714
|
+
if (line.startsWith("+++ ") || line.startsWith("--- ")) {
|
|
1715
|
+
line = chalk.bold(line);
|
|
1716
|
+
} else if (line.startsWith("@@")) {
|
|
1717
|
+
line = chalk.cyan(line);
|
|
1718
|
+
} else if (line.startsWith("+")) {
|
|
1719
|
+
line = chalk.green(line);
|
|
1720
|
+
} else if (line.startsWith("-")) {
|
|
1721
|
+
line = chalk.red(line);
|
|
1722
|
+
} else {
|
|
1723
|
+
line = chalk.gray(line);
|
|
1724
|
+
}
|
|
1725
|
+
pushBody(truncVis(line, innerW));
|
|
1726
|
+
}
|
|
1727
|
+
const patchEndR = rowPtr - 1;
|
|
1728
|
+
if (patchEndR >= patchStartR) {
|
|
1729
|
+
regions.push({ kind: "patch", r1: patchStartR, r2: patchEndR });
|
|
1730
|
+
}
|
|
1731
|
+
if (filePatchOffset + patchPageSize < patchLines.length) {
|
|
1732
|
+
pushBody(chalk.dim("j/k = more patch"));
|
|
362
1733
|
}
|
|
363
|
-
bodyLines.push(line.slice(0, 130));
|
|
364
1734
|
}
|
|
365
|
-
if (
|
|
366
|
-
|
|
1735
|
+
if (fileComments.length) {
|
|
1736
|
+
const c = fileComments[safeFileComment];
|
|
1737
|
+
pushBody(
|
|
1738
|
+
truncVis(
|
|
1739
|
+
`Comment ${safeFileComment + 1}/${fileComments.length} @${c?.user?.login || "?"}`,
|
|
1740
|
+
innerW
|
|
1741
|
+
)
|
|
1742
|
+
);
|
|
1743
|
+
pushBody(truncVis(String(c?.body || "").split("\n")[0], innerW));
|
|
367
1744
|
}
|
|
368
1745
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
1746
|
+
}
|
|
1747
|
+
} else if (tab === 4 && !shellMode) {
|
|
1748
|
+
const treeBlockH = browsePreviewOpen
|
|
1749
|
+
? Math.max(4, Math.floor(bodyMax * 0.38))
|
|
1750
|
+
: bodyMax - 1;
|
|
1751
|
+
const previewH = browsePreviewOpen ? Math.max(3, bodyMax - treeBlockH - 1) : 0;
|
|
1752
|
+
layoutTreeBlockH = treeBlockH;
|
|
1753
|
+
layoutBrowsePreviewH = previewH;
|
|
1754
|
+
|
|
1755
|
+
if (treeLoading) {
|
|
1756
|
+
pushBody("Loading tree\u2026");
|
|
1757
|
+
} else if (treeError) {
|
|
1758
|
+
pushBody(truncVis(treeError, innerW));
|
|
1759
|
+
}
|
|
1760
|
+
const previewPathOpenNs = browsePreviewOpen && treePreviewPath ? treePreviewPath : "";
|
|
1761
|
+
const treeDisplay = asciiTreeRows.map((r, i) => {
|
|
1762
|
+
const mark = i === safeLine ? ">" : " ";
|
|
1763
|
+
const f = r.path ? pathToChangedFile.get(r.path) : undefined;
|
|
1764
|
+
const add = f ? Number(f.additions) || 0 : 0;
|
|
1765
|
+
const del = f ? Number(f.deletions) || 0 : 0;
|
|
1766
|
+
const flag = f
|
|
1767
|
+
? add > 0 && del > 0
|
|
1768
|
+
? chalk.yellow("*")
|
|
1769
|
+
: add > 0
|
|
1770
|
+
? chalk.green("+")
|
|
1771
|
+
: del > 0
|
|
1772
|
+
? chalk.red("-")
|
|
1773
|
+
: chalk.cyan("*")
|
|
1774
|
+
: " ";
|
|
1775
|
+
const tw = Math.max(8, innerW - 4);
|
|
1776
|
+
const plainT = truncVis(r.text, tw);
|
|
1777
|
+
let lineText = plainT;
|
|
1778
|
+
if (f) {
|
|
1779
|
+
if (add > 0 && del > 0) {
|
|
1780
|
+
lineText = chalk.yellow(plainT);
|
|
1781
|
+
} else if (add > 0) {
|
|
1782
|
+
lineText = chalk.green(plainT);
|
|
1783
|
+
} else if (del > 0) {
|
|
1784
|
+
lineText = chalk.red(plainT);
|
|
1785
|
+
} else {
|
|
1786
|
+
lineText = chalk.cyan(plainT);
|
|
1787
|
+
}
|
|
379
1788
|
}
|
|
1789
|
+
if (r.path && previewPathOpenNs && r.path === previewPathOpenNs) {
|
|
1790
|
+
lineText = chalk.inverse(lineText);
|
|
1791
|
+
}
|
|
1792
|
+
return `${mark}${flag} ${lineText}`;
|
|
1793
|
+
});
|
|
1794
|
+
const { slice: treeSlice, start: treeStart } = sliceViewport(treeDisplay, safeLine, treeBlockH);
|
|
1795
|
+
if (treeStart > 0) {
|
|
1796
|
+
pushBody(chalk.dim(`\u2191 tree ${treeStart} more`));
|
|
1797
|
+
}
|
|
1798
|
+
const treeSliceStartR = rowPtr;
|
|
1799
|
+
for (const ln of treeSlice) {
|
|
1800
|
+
pushBody(ln);
|
|
1801
|
+
}
|
|
1802
|
+
for (let li = 0; li < treeSlice.length; li++) {
|
|
1803
|
+
const idx = treeStart + li;
|
|
1804
|
+
const tr = asciiTreeRows[idx];
|
|
1805
|
+
regions.push({
|
|
1806
|
+
kind: "treeLine",
|
|
1807
|
+
r1: treeSliceStartR + li,
|
|
1808
|
+
r2: treeSliceStartR + li,
|
|
1809
|
+
index: idx,
|
|
1810
|
+
path: tr?.path ?? null
|
|
1811
|
+
});
|
|
380
1812
|
}
|
|
1813
|
+
if (treeStart + treeSlice.length < treeDisplay.length) {
|
|
1814
|
+
pushBody(chalk.dim(`\u2193 tree ${treeDisplay.length - treeStart - treeSlice.length} more`));
|
|
1815
|
+
}
|
|
1816
|
+
if (!treeLoading && asciiTreeRows.length === 0 && !treeError) {
|
|
1817
|
+
pushBody("(empty tree)");
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
if (browsePreviewOpen && treePreviewPath) {
|
|
1821
|
+
pushBody("");
|
|
1822
|
+
const mergedNs = mergedPreviewRows;
|
|
1823
|
+
const subNs = mergedNs && mergedNs.length > 0 ? "diff in file " : "";
|
|
1824
|
+
pushBody(
|
|
1825
|
+
chalk.bold(truncVis(`File: ${treePreviewPath}`, innerW)) +
|
|
1826
|
+
chalk.whiteBright(
|
|
1827
|
+
` ${subNs}[${treePreviewScroll + 1}-${Math.min(treePreviewScroll + previewH, previewLineCount)}/${previewLineCount}] wheel/Space scroll BS/Esc back`
|
|
1828
|
+
)
|
|
1829
|
+
);
|
|
1830
|
+
const prevSliceNs = mergedNs
|
|
1831
|
+
? mergedNs.slice(treePreviewScroll, treePreviewScroll + previewH)
|
|
1832
|
+
: treePreviewLines.slice(treePreviewScroll, treePreviewScroll + previewH).map((t) => ({
|
|
1833
|
+
text: t,
|
|
1834
|
+
kind: /** @type {"ctx"} */ ("ctx")
|
|
1835
|
+
}));
|
|
1836
|
+
const prevStartRNs = rowPtr;
|
|
1837
|
+
for (const row of prevSliceNs) {
|
|
1838
|
+
const tv = truncVis(row.text, innerW);
|
|
1839
|
+
const styled =
|
|
1840
|
+
row.kind === "add"
|
|
1841
|
+
? chalk.green(tv)
|
|
1842
|
+
: row.kind === "del"
|
|
1843
|
+
? chalk.red(tv)
|
|
1844
|
+
: chalk.whiteBright(tv);
|
|
1845
|
+
pushBody(styled);
|
|
1846
|
+
}
|
|
1847
|
+
const prevEndRNs = rowPtr - 1;
|
|
1848
|
+
if (prevEndRNs >= prevStartRNs) {
|
|
1849
|
+
regions.push({ kind: "preview", r1: prevStartRNs, r2: prevEndRNs });
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
if (bodyLines.length > bodyMax) {
|
|
1855
|
+
const lastR = bodyStartR + bodyMax - 1;
|
|
1856
|
+
bodyLines = bodyLines.slice(0, bodyMax);
|
|
1857
|
+
regions = regions
|
|
1858
|
+
.filter((reg) => reg.r1 <= lastR)
|
|
1859
|
+
.map((reg) => ({ ...reg, r2: Math.min(reg.r2, lastR) }))
|
|
1860
|
+
.filter((reg) => reg.r1 <= reg.r2);
|
|
381
1861
|
}
|
|
382
1862
|
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
1863
|
+
const browsePreviewHForLayout = treeViewActive ? layoutBrowsePreviewH : 0;
|
|
1864
|
+
layoutRef.current = {
|
|
1865
|
+
bodyStartR,
|
|
1866
|
+
nLad: shellMode ? 0 : nLad,
|
|
1867
|
+
ladderStart,
|
|
1868
|
+
browsePreviewH: browsePreviewHForLayout,
|
|
1869
|
+
bodyMax,
|
|
1870
|
+
tab,
|
|
1871
|
+
shellMode,
|
|
1872
|
+
shellMainTab: shellMode ? shellMainTab : undefined,
|
|
1873
|
+
shellStackFocus: shellMode ? shellStackFocus : false,
|
|
1874
|
+
browsePreviewOpen,
|
|
1875
|
+
treePreviewLinesLen: previewLineCount,
|
|
1876
|
+
maxPatchOffset,
|
|
1877
|
+
patchPageSize,
|
|
1878
|
+
listLen,
|
|
1879
|
+
len,
|
|
1880
|
+
fileListLen: fileList.length,
|
|
1881
|
+
treeLen: asciiTreeRows.length
|
|
1882
|
+
};
|
|
1883
|
+
regionsRef.current = regions;
|
|
1884
|
+
|
|
1885
|
+
const selectedReview =
|
|
1886
|
+
tab === 2 || (shellMode && shellMainTab === 3) ? reviewList[safeLine] : null;
|
|
1887
|
+
const helpParts = ["Tab/[]", "o files", "j/k", "Enter/Space", "BS back", "wheel", "u", "q"];
|
|
1888
|
+
if (tab === 0) {
|
|
1889
|
+
helpParts.push("PR ladder+mouse");
|
|
387
1890
|
}
|
|
388
1891
|
if (tab === 3) {
|
|
389
|
-
helpParts.
|
|
1892
|
+
helpParts.push("n/p m/M");
|
|
1893
|
+
}
|
|
1894
|
+
if (tab === 4) {
|
|
1895
|
+
if (browsePreviewOpen) {
|
|
1896
|
+
helpParts.push("Space page", "b");
|
|
1897
|
+
} else {
|
|
1898
|
+
helpParts.push("tree click", "dbl-open", "K");
|
|
1899
|
+
}
|
|
390
1900
|
}
|
|
391
1901
|
if (row && !row.error) {
|
|
392
1902
|
if (tab === 0) {
|
|
393
|
-
helpParts.push("S
|
|
1903
|
+
helpParts.push("S a e");
|
|
394
1904
|
}
|
|
395
|
-
helpParts.push("r
|
|
1905
|
+
helpParts.push("r R");
|
|
396
1906
|
}
|
|
397
1907
|
if (tab === 2 && selectedReview?.html_url) {
|
|
398
|
-
helpParts.push("l
|
|
1908
|
+
helpParts.push("l g t");
|
|
399
1909
|
}
|
|
400
|
-
if (
|
|
401
|
-
helpParts.push("
|
|
1910
|
+
if (shellMode) {
|
|
1911
|
+
helpParts.push("[ ] PR", "1-9", "PgUp/Dn diff", "O", "K", "Enter Space", "BS stack", "b preview", "Esc q", "?");
|
|
1912
|
+
}
|
|
1913
|
+
const helpRaw = helpParts.join(" · ");
|
|
1914
|
+
const fullFooterW = Math.max(40, cols - 2);
|
|
1915
|
+
/** @type {string} */
|
|
1916
|
+
let helpLine1;
|
|
1917
|
+
/** @type {string} */
|
|
1918
|
+
let helpLine2;
|
|
1919
|
+
/** @type {string} */
|
|
1920
|
+
let helpLine3 = "";
|
|
1921
|
+
if (shellMode) {
|
|
1922
|
+
if (showFullHelp) {
|
|
1923
|
+
helpLine1 = truncVis(helpRaw, fullFooterW);
|
|
1924
|
+
helpLine2 =
|
|
1925
|
+
helpRaw.length > fullFooterW ? truncVis(helpRaw.slice(fullFooterW, fullFooterW * 2), fullFooterW) : "";
|
|
1926
|
+
helpLine3 =
|
|
1927
|
+
helpRaw.length > fullFooterW * 2 ? truncVis(helpRaw.slice(fullFooterW * 2), fullFooterW) : "";
|
|
1928
|
+
} else if (shellStackFocus) {
|
|
1929
|
+
helpLine1 = "Tab to cycle functions.";
|
|
1930
|
+
helpLine2 = truncVis(
|
|
1931
|
+
canPickAlternateStack
|
|
1932
|
+
? "j/k or [ ] pick PR · Enter/Space tree · Backspace pick another stack · Tab Esc cancel"
|
|
1933
|
+
: "j/k or [ ] pick PR · Enter or Space back to tree · Backspace Tab Esc cancel pick",
|
|
1934
|
+
fullFooterW
|
|
1935
|
+
);
|
|
1936
|
+
} else if (browsePreviewOpen) {
|
|
1937
|
+
helpLine1 = "Tab to cycle functions.";
|
|
1938
|
+
helpLine2 = truncVis(
|
|
1939
|
+
"Enter or Space open file · Backspace or b close file · [ ] 1-9 PR · PgUp/Dn wheel diff/preview · O · K · ? · Esc q",
|
|
1940
|
+
fullFooterW
|
|
1941
|
+
);
|
|
1942
|
+
} else {
|
|
1943
|
+
helpLine1 = "Tab to cycle functions.";
|
|
1944
|
+
helpLine2 = truncVis(
|
|
1945
|
+
canPickAlternateStack
|
|
1946
|
+
? "Enter/Space file · Backspace PR pick · Backspace again other stack · [ ] 1-9 · PgUp/Dn wheel · C comment · O · K · ? · Esc q"
|
|
1947
|
+
: "Enter or Space open file · Backspace pick PR · [ ] 1-9 PR · PgUp/Dn wheel diff · C comment · O · K · ? · Esc q",
|
|
1948
|
+
fullFooterW
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
} else if (showFullHelp) {
|
|
1952
|
+
helpLine1 = truncVis(helpRaw, innerW);
|
|
1953
|
+
helpLine2 =
|
|
1954
|
+
helpRaw.length > innerW ? truncVis(helpRaw.slice(innerW), innerW) : "";
|
|
1955
|
+
} else {
|
|
1956
|
+
helpLine1 = truncVis("Tab/[] · j/k · Enter/Space · BS · mouse · ? details · u · q", innerW);
|
|
1957
|
+
helpLine2 = "";
|
|
402
1958
|
}
|
|
403
|
-
helpParts.push("u refresh", "q quit");
|
|
404
|
-
const help = helpParts.join(" | ");
|
|
405
1959
|
|
|
406
|
-
|
|
1960
|
+
const spin = SPIN_FRAMES[pulse % SPIN_FRAMES.length];
|
|
1961
|
+
const clock = new Date().toLocaleTimeString(undefined, { hour12: false });
|
|
1962
|
+
|
|
1963
|
+
const footerRule = "\u2500".repeat(Math.max(0, innerW));
|
|
1964
|
+
|
|
1965
|
+
const displayRepo =
|
|
1966
|
+
repoFullName || (browseOwner && browseRepo ? `${browseOwner}/${browseRepo}` : viewTitle);
|
|
1967
|
+
|
|
1968
|
+
const mainColumn = React.createElement(
|
|
407
1969
|
Box,
|
|
408
|
-
{ flexDirection: "column",
|
|
409
|
-
React.createElement(Text, { color: "cyan", bold: true }, "nugit stack view"),
|
|
410
|
-
React.createElement(Text, { dimColor: true }, ladder.join("\n")),
|
|
411
|
-
React.createElement(Text, { color: "magenta" }, `Tab: ${tabName}`),
|
|
1970
|
+
{ flexDirection: "column", width: innerW },
|
|
412
1971
|
React.createElement(
|
|
413
1972
|
Box,
|
|
414
|
-
{
|
|
415
|
-
|
|
416
|
-
|
|
1973
|
+
{ flexDirection: "row", width: innerW, justifyContent: "space-between" },
|
|
1974
|
+
React.createElement(Text, { color: "cyan", bold: true }, truncVis(viewTitle, Math.floor(innerW * 0.5))),
|
|
1975
|
+
React.createElement(
|
|
1976
|
+
Text,
|
|
1977
|
+
{ dimColor: false, color: "green" },
|
|
1978
|
+
`${spin} ${clock}`
|
|
417
1979
|
)
|
|
418
1980
|
),
|
|
419
|
-
|
|
1981
|
+
...(shellMode
|
|
1982
|
+
? []
|
|
1983
|
+
: [
|
|
1984
|
+
...(ladderVis.length > 0
|
|
1985
|
+
? [
|
|
1986
|
+
React.createElement(
|
|
1987
|
+
Text,
|
|
1988
|
+
{ key: "ladsum", dimColor: true },
|
|
1989
|
+
ladderStart > 0 || ladderStart + ladderVis.length < ladderRows.length
|
|
1990
|
+
? `PRs ${ladderStart + 1}-${ladderStart + ladderVis.length}/${len}`
|
|
1991
|
+
: ""
|
|
1992
|
+
)
|
|
1993
|
+
]
|
|
1994
|
+
: []),
|
|
1995
|
+
...ladderVis.map((line, i) =>
|
|
1996
|
+
React.createElement(Text, { key: `l-${ladderStart + i}` }, line)
|
|
1997
|
+
)
|
|
1998
|
+
]),
|
|
1999
|
+
shellMode
|
|
2000
|
+
? React.createElement(
|
|
2001
|
+
Box,
|
|
2002
|
+
{ key: "shell-tabs", flexDirection: "column", width: innerW, marginBottom: 1 },
|
|
2003
|
+
React.createElement(
|
|
2004
|
+
Box,
|
|
2005
|
+
{ flexDirection: "row", width: innerW, flexWrap: "wrap" },
|
|
2006
|
+
...SHELL_TAB_LABELS.map((lab, i) =>
|
|
2007
|
+
React.createElement(
|
|
2008
|
+
Text,
|
|
2009
|
+
{
|
|
2010
|
+
key: `st-${i}`,
|
|
2011
|
+
color: i === shellMainTab ? "yellow" : "gray",
|
|
2012
|
+
bold: i === shellMainTab
|
|
2013
|
+
},
|
|
2014
|
+
i === shellMainTab ? `${lab} ` : `${lab} `
|
|
2015
|
+
)
|
|
2016
|
+
)
|
|
2017
|
+
),
|
|
2018
|
+
React.createElement(Text, { dimColor: true, color: "gray" }, footerRule)
|
|
2019
|
+
)
|
|
2020
|
+
: React.createElement(Text, { color: "magenta" }, `Tab: ${tabName}`),
|
|
2021
|
+
React.createElement(
|
|
2022
|
+
Box,
|
|
2023
|
+
{ flexDirection: "column", width: innerW },
|
|
2024
|
+
...bodyLines.map((line, idx) => React.createElement(Text, { key: `b-${idx}` }, line))
|
|
2025
|
+
),
|
|
2026
|
+
...(!shellMode
|
|
2027
|
+
? [
|
|
2028
|
+
React.createElement(Text, { key: "h1", dimColor: true }, helpLine1),
|
|
2029
|
+
helpLine2 ? React.createElement(Text, { key: "h2", dimColor: true }, helpLine2) : null
|
|
2030
|
+
]
|
|
2031
|
+
: [])
|
|
2032
|
+
);
|
|
2033
|
+
|
|
2034
|
+
const footerRuleFull = "\u2500".repeat(fullFooterW);
|
|
2035
|
+
const shellFooterBlock = shellMode
|
|
2036
|
+
? React.createElement(
|
|
2037
|
+
Box,
|
|
2038
|
+
{ key: "shell-foot", flexDirection: "column", width: fullFooterW, marginTop: 1 },
|
|
2039
|
+
React.createElement(Text, { color: "cyan" }, footerRuleFull),
|
|
2040
|
+
React.createElement(Text, { color: "cyan" }, helpLine1),
|
|
2041
|
+
helpLine2 ? React.createElement(Text, { color: "cyan" }, helpLine2) : null,
|
|
2042
|
+
helpLine3 ? React.createElement(Text, { color: "cyan" }, helpLine3) : null
|
|
2043
|
+
)
|
|
2044
|
+
: null;
|
|
2045
|
+
|
|
2046
|
+
if (!shellMode) {
|
|
2047
|
+
return React.createElement(Box, { flexDirection: "column", width: cols, paddingX: 1 }, mainColumn);
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
const sepW = Math.max(2, sidebarW - 2);
|
|
2051
|
+
const sepBar = "\u2500".repeat(sepW);
|
|
2052
|
+
const branchGraphW = Math.max(10, sidebarW - 2);
|
|
2053
|
+
const shellBranchGraphLines = buildStackBranchGraphLines(
|
|
2054
|
+
stackRows,
|
|
2055
|
+
hasPrPick ? safePr : -1,
|
|
2056
|
+
SIDEBAR_GRAPH_MAX,
|
|
2057
|
+
branchGraphW
|
|
2058
|
+
);
|
|
2059
|
+
const sidebarInner = [
|
|
2060
|
+
React.createElement(
|
|
2061
|
+
Text,
|
|
2062
|
+
{ key: "sr", bold: true, color: "cyan" },
|
|
2063
|
+
truncVis(displayRepo, sidebarW - 2)
|
|
2064
|
+
),
|
|
2065
|
+
React.createElement(Text, { key: "ssep0", color: "gray" }, sepBar),
|
|
2066
|
+
React.createElement(Text, { key: "ss1", color: "white" }, `PRs: ${len}`),
|
|
2067
|
+
React.createElement(Text, { key: "ss2", color: "white" }, `Changed files: ${fileList.length}`),
|
|
2068
|
+
React.createElement(
|
|
2069
|
+
Text,
|
|
2070
|
+
{ key: "ss4", color: hasPrPick ? "whiteBright" : "gray", dimColor: !hasPrPick },
|
|
2071
|
+
`Focus: ${hasPrPick ? `#${row.entry.pr_number}` : "\u2014"}`
|
|
2072
|
+
),
|
|
2073
|
+
React.createElement(Text, { key: "ssep1", color: "gray" }, sepBar),
|
|
2074
|
+
ladderVis.length > 0
|
|
2075
|
+
? React.createElement(
|
|
2076
|
+
Text,
|
|
2077
|
+
{ key: "slab", color: "white", dimColor: true },
|
|
2078
|
+
ladderStart > 0 || ladderStart + ladderVis.length < ladderRows.length
|
|
2079
|
+
? `Ladder ${ladderStart + 1}\u2013${ladderStart + ladderVis.length}/${len}`
|
|
2080
|
+
: "Stack"
|
|
2081
|
+
)
|
|
2082
|
+
: null,
|
|
2083
|
+
...ladderVis.map((line, i) =>
|
|
2084
|
+
React.createElement(Text, { key: `ls-${ladderStart + i}` }, line)
|
|
2085
|
+
),
|
|
2086
|
+
React.createElement(Text, { key: "ssep2", color: "gray" }, sepBar),
|
|
2087
|
+
React.createElement(Text, { key: "sbranch", color: "white", dimColor: true }, "Branch"),
|
|
2088
|
+
...shellBranchGraphLines.map((ln, gi) =>
|
|
2089
|
+
React.createElement(Text, { key: `sg-${gi}` }, ln)
|
|
2090
|
+
)
|
|
2091
|
+
];
|
|
2092
|
+
|
|
2093
|
+
const divRows = Math.min(Math.max(14, ttyRows - 8), 64);
|
|
2094
|
+
const dividerCol = React.createElement(
|
|
2095
|
+
Box,
|
|
2096
|
+
{ key: "sdiv", flexDirection: "column", width: 1, marginRight: 0 },
|
|
2097
|
+
...Array.from({ length: divRows }, (_, i) =>
|
|
2098
|
+
React.createElement(Text, { key: `dv${i}`, color: "gray" }, "\u2502")
|
|
2099
|
+
)
|
|
2100
|
+
);
|
|
2101
|
+
|
|
2102
|
+
return React.createElement(
|
|
2103
|
+
Box,
|
|
2104
|
+
{ flexDirection: "column", width: cols, paddingX: 1 },
|
|
2105
|
+
React.createElement(
|
|
2106
|
+
Box,
|
|
2107
|
+
{ flexDirection: "row", width: cols - 2 },
|
|
2108
|
+
React.createElement(
|
|
2109
|
+
Box,
|
|
2110
|
+
{ flexDirection: "column", width: sidebarW, marginRight: 0 },
|
|
2111
|
+
...sidebarInner.filter(Boolean)
|
|
2112
|
+
),
|
|
2113
|
+
dividerCol,
|
|
2114
|
+
mainColumn
|
|
2115
|
+
),
|
|
2116
|
+
shellFooterBlock
|
|
420
2117
|
);
|
|
421
2118
|
}
|