merge-steward 0.18.0 → 0.19.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/dist/watch/App.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useState } from "react";
|
|
3
|
-
import { Box, Text, useApp, useInput } from "ink";
|
|
3
|
+
import { Box, Text, useApp, useInput, useStdout } from "ink";
|
|
4
4
|
import { fetchGatewayHealth, fetchSnapshot, triggerReconcile } from "./api.js";
|
|
5
5
|
import { buildDashboard, matchRepoRef, repoSelector, stepRepo } from "./dashboard-model.js";
|
|
6
6
|
import { HelpBar } from "./HelpBar.js";
|
|
@@ -10,6 +10,7 @@ import { StatusBar } from "./StatusBar.js";
|
|
|
10
10
|
const REFRESH_INTERVAL_MS = 1_500;
|
|
11
11
|
export function App({ gatewayBaseUrl, repos, initialRepoRef, initialPrNumber }) {
|
|
12
12
|
const { exit } = useApp();
|
|
13
|
+
const { stdout } = useStdout();
|
|
13
14
|
const [repoStates, setRepoStates] = useState(() => repos.map((repo) => ({
|
|
14
15
|
...repo,
|
|
15
16
|
serviceState: "offline",
|
|
@@ -27,6 +28,7 @@ export function App({ gatewayBaseUrl, repos, initialRepoRef, initialPrNumber })
|
|
|
27
28
|
const [view, setView] = useState(initialRepoRef || initialPrNumber !== undefined ? "detail" : "list");
|
|
28
29
|
const [flashMessage, setFlashMessage] = useState(null);
|
|
29
30
|
const [pendingInitialPrNumber, setPendingInitialPrNumber] = useState(initialPrNumber ?? null);
|
|
31
|
+
const [detailScrollOffset, setDetailScrollOffset] = useState(0);
|
|
30
32
|
const model = useMemo(() => buildDashboard(repoStates), [repoStates]);
|
|
31
33
|
useEffect(() => {
|
|
32
34
|
setSelectedRepoId((current) => repoSelector(model.repos, current));
|
|
@@ -150,6 +152,9 @@ export function App({ gatewayBaseUrl, repos, initialRepoRef, initialPrNumber })
|
|
|
150
152
|
setFlashMessage(error instanceof Error ? error.message : String(error));
|
|
151
153
|
}
|
|
152
154
|
}
|
|
155
|
+
const rows = Math.max(8, stdout?.rows ?? 24);
|
|
156
|
+
const chromeRows = 1 /* status */ + 1 /* body marginTop */ + (flashMessage ? 2 : 0) + 2 /* help bar + its marginTop */;
|
|
157
|
+
const bodyRows = Math.max(2, rows - chromeRows);
|
|
153
158
|
useInput((input, key) => {
|
|
154
159
|
if (input === "q") {
|
|
155
160
|
exit();
|
|
@@ -159,22 +164,58 @@ export function App({ gatewayBaseUrl, repos, initialRepoRef, initialPrNumber })
|
|
|
159
164
|
void runReconcile(selectedRepoId);
|
|
160
165
|
return;
|
|
161
166
|
}
|
|
162
|
-
if (
|
|
167
|
+
if (view === "list") {
|
|
168
|
+
if (input === "j" || key.downArrow || input === "]") {
|
|
169
|
+
setSelectedRepoId((current) => stepRepo(model.repos, current, 1));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (input === "k" || key.upArrow || input === "[") {
|
|
173
|
+
setSelectedRepoId((current) => stepRepo(model.repos, current, -1));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (key.return)
|
|
177
|
+
setView("detail");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// detail view
|
|
181
|
+
if (input === "]") {
|
|
163
182
|
setSelectedRepoId((current) => stepRepo(model.repos, current, 1));
|
|
183
|
+
setDetailScrollOffset(0);
|
|
164
184
|
return;
|
|
165
185
|
}
|
|
166
|
-
if (input === "
|
|
186
|
+
if (input === "[") {
|
|
167
187
|
setSelectedRepoId((current) => stepRepo(model.repos, current, -1));
|
|
188
|
+
setDetailScrollOffset(0);
|
|
168
189
|
return;
|
|
169
190
|
}
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
191
|
+
if (input === "j" || key.downArrow) {
|
|
192
|
+
setDetailScrollOffset((offset) => offset + 1);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (input === "k" || key.upArrow) {
|
|
196
|
+
setDetailScrollOffset((offset) => Math.max(0, offset - 1));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (key.pageDown || input === " ") {
|
|
200
|
+
setDetailScrollOffset((offset) => offset + Math.max(1, bodyRows - 2));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (key.pageUp) {
|
|
204
|
+
setDetailScrollOffset((offset) => Math.max(0, offset - Math.max(1, bodyRows - 2)));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (input === "g") {
|
|
208
|
+
setDetailScrollOffset(0);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (input === "G") {
|
|
212
|
+
setDetailScrollOffset(Number.MAX_SAFE_INTEGER);
|
|
173
213
|
return;
|
|
174
214
|
}
|
|
175
215
|
if (key.escape || key.backspace || key.delete) {
|
|
176
216
|
setView("list");
|
|
217
|
+
setDetailScrollOffset(0);
|
|
177
218
|
}
|
|
178
219
|
});
|
|
179
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(StatusBar, { connected: model.repos.some((repo) => !repo.offlineMessage), lastSnapshotReceivedAt: lastSnapshotReceivedAt, gatewayError: gatewayError }), view === "detail" ? (_jsx(ProjectDetailView, { model: model, selectedRepoId: selectedRepoId })) : (_jsx(OverviewView, { model: model, selectedRepoId: selectedRepoId, showCursor: true })), flashMessage ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: flashMessage }) })) : null, _jsx(HelpBar, { view: view })] }));
|
|
220
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(StatusBar, { connected: model.repos.some((repo) => !repo.offlineMessage), lastSnapshotReceivedAt: lastSnapshotReceivedAt, gatewayError: gatewayError }), view === "detail" ? (_jsx(ProjectDetailView, { model: model, selectedRepoId: selectedRepoId, bodyRows: bodyRows, scrollOffset: detailScrollOffset })) : (_jsx(OverviewView, { model: model, selectedRepoId: selectedRepoId, showCursor: true, bodyRows: bodyRows })), flashMessage ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: flashMessage }) })) : null, _jsx(HelpBar, { view: view })] }));
|
|
180
221
|
}
|
package/dist/watch/HelpBar.js
CHANGED
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
export function HelpBar({ view }) {
|
|
4
4
|
const text = view === "list"
|
|
5
|
-
? "↑↓
|
|
6
|
-
: "
|
|
5
|
+
? "↑↓ repo enter detail r reconcile q quit"
|
|
6
|
+
: "[ ] repo ↑↓ scroll g/G top/end esc back r reconcile q quit";
|
|
7
7
|
return (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: text }) }));
|
|
8
8
|
}
|
|
@@ -3,6 +3,7 @@ interface ListViewProps {
|
|
|
3
3
|
model: DashboardModel;
|
|
4
4
|
selectedRepoId: string | null;
|
|
5
5
|
showCursor: boolean;
|
|
6
|
+
bodyRows: number;
|
|
6
7
|
}
|
|
7
8
|
export declare function RepoRow({ repo, selected, showCursor, width, }: {
|
|
8
9
|
repo: DashboardRepo;
|
|
@@ -14,5 +15,5 @@ export declare function pickVisibleWindow(total: number, selectedIndex: number,
|
|
|
14
15
|
start: number;
|
|
15
16
|
end: number;
|
|
16
17
|
};
|
|
17
|
-
export declare function OverviewView({ model, selectedRepoId, showCursor }: ListViewProps): React.JSX.Element;
|
|
18
|
+
export declare function OverviewView({ model, selectedRepoId, showCursor, bodyRows }: ListViewProps): React.JSX.Element;
|
|
18
19
|
export {};
|
|
@@ -45,21 +45,36 @@ export function pickVisibleWindow(total, selectedIndex, availableRows) {
|
|
|
45
45
|
}
|
|
46
46
|
return { start, end };
|
|
47
47
|
}
|
|
48
|
-
export function OverviewView({ model, selectedRepoId, showCursor }) {
|
|
48
|
+
export function OverviewView({ model, selectedRepoId, showCursor, bodyRows }) {
|
|
49
49
|
const { stdout } = useStdout();
|
|
50
|
-
const rows = Math.max(3, stdout?.rows ?? 24);
|
|
51
50
|
const width = Math.max(40, stdout?.columns ?? 80);
|
|
52
|
-
const
|
|
51
|
+
const total = model.repos.length;
|
|
53
52
|
const selectedIndex = Math.max(0, model.repos.findIndex((repo) => repo.repoId === selectedRepoId));
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
let { start, end } = pickVisibleWindow(total, selectedIndex, Math.max(1, bodyRows));
|
|
54
|
+
let needTop = start > 0;
|
|
55
|
+
let needBottom = end < total;
|
|
56
|
+
if (needTop || needBottom) {
|
|
57
|
+
const reserve = (needTop ? 1 : 0) + (needBottom ? 1 : 0);
|
|
58
|
+
const windowRows = Math.max(1, bodyRows - reserve);
|
|
59
|
+
({ start, end } = pickVisibleWindow(total, selectedIndex, windowRows));
|
|
60
|
+
needTop = start > 0;
|
|
61
|
+
needBottom = end < total;
|
|
62
|
+
}
|
|
57
63
|
const visible = model.repos.slice(start, end);
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (visible.length === 0) {
|
|
64
|
+
const above = start;
|
|
65
|
+
const below = total - end;
|
|
66
|
+
if (total === 0 || bodyRows <= 0) {
|
|
62
67
|
return _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: " " }) });
|
|
63
68
|
}
|
|
64
|
-
|
|
69
|
+
const children = [];
|
|
70
|
+
if (needTop) {
|
|
71
|
+
children.push(_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: `\u2191${above} more above` }) }, "above"));
|
|
72
|
+
}
|
|
73
|
+
for (const repo of visible) {
|
|
74
|
+
children.push(_jsx(RepoRow, { repo: repo, selected: repo.repoId === selectedRepoId, showCursor: showCursor, width: width - 2 }, repo.repoId));
|
|
75
|
+
}
|
|
76
|
+
if (needBottom) {
|
|
77
|
+
children.push(_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: `\u2193${below} more below` }) }, "below"));
|
|
78
|
+
}
|
|
79
|
+
return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: children.slice(0, Math.max(1, bodyRows)) }));
|
|
65
80
|
}
|
|
@@ -1,7 +1,20 @@
|
|
|
1
|
-
import { type DashboardModel } from "./dashboard-model.ts";
|
|
1
|
+
import { type DashboardModel, type DashboardPrEntry, type DashboardRepo } from "./dashboard-model.ts";
|
|
2
2
|
interface ProjectDetailViewProps {
|
|
3
3
|
model: DashboardModel;
|
|
4
4
|
selectedRepoId: string | null;
|
|
5
|
+
bodyRows: number;
|
|
6
|
+
scrollOffset: number;
|
|
5
7
|
}
|
|
6
|
-
|
|
8
|
+
type ContentLine = {
|
|
9
|
+
kind: "blank";
|
|
10
|
+
} | {
|
|
11
|
+
kind: "entry-header";
|
|
12
|
+
entry: DashboardPrEntry;
|
|
13
|
+
} | {
|
|
14
|
+
kind: "summary-line";
|
|
15
|
+
text: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function buildContentLines(repo: DashboardRepo, width: number): ContentLine[];
|
|
18
|
+
export declare function clampScrollOffset(requested: number, totalLines: number, scrollArea: number): number;
|
|
19
|
+
export declare function ProjectDetailView({ model, selectedRepoId, bodyRows, scrollOffset }: ProjectDetailViewProps): React.JSX.Element;
|
|
7
20
|
export {};
|
|
@@ -5,28 +5,58 @@ import { clipSummary } from "./dashboard-model.js";
|
|
|
5
5
|
const PR_ID_WIDTH = 7;
|
|
6
6
|
const PR_PHRASE_WIDTH = 20;
|
|
7
7
|
const SUMMARY_INDENT = 9;
|
|
8
|
-
function
|
|
8
|
+
function truncate(value, maxWidth) {
|
|
9
|
+
if (value.length <= maxWidth)
|
|
10
|
+
return value;
|
|
11
|
+
if (maxWidth <= 1)
|
|
12
|
+
return value.slice(0, maxWidth);
|
|
13
|
+
return `${value.slice(0, Math.max(0, maxWidth - 1))}\u2026`;
|
|
14
|
+
}
|
|
15
|
+
export function buildContentLines(repo, width) {
|
|
16
|
+
const lines = [];
|
|
17
|
+
repo.entries.forEach((entry, index) => {
|
|
18
|
+
if (index > 0)
|
|
19
|
+
lines.push({ kind: "blank" });
|
|
20
|
+
lines.push({ kind: "entry-header", entry });
|
|
21
|
+
if (entry.summary) {
|
|
22
|
+
const summaryText = clipSummary(entry.summary, {
|
|
23
|
+
maxLines: 3,
|
|
24
|
+
width: Math.max(20, width - SUMMARY_INDENT),
|
|
25
|
+
});
|
|
26
|
+
if (summaryText) {
|
|
27
|
+
summaryText.split("\n").forEach((line) => lines.push({ kind: "summary-line", text: line }));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
return lines;
|
|
32
|
+
}
|
|
33
|
+
function EntryHeaderRow({ entry, width }) {
|
|
9
34
|
const idText = `#${entry.prNumber}`.padEnd(PR_ID_WIDTH, " ");
|
|
10
35
|
const paddedPhrase = entry.phrase.padEnd(PR_PHRASE_WIDTH, " ");
|
|
11
|
-
const summaryText = includeSummary
|
|
12
|
-
? clipSummary(entry.summary, { maxLines: 3, width: Math.max(20, width - SUMMARY_INDENT) })
|
|
13
|
-
: "";
|
|
14
36
|
const titleSpace = Math.max(0, width - (PR_ID_WIDTH + 3 + PR_PHRASE_WIDTH + 1));
|
|
15
37
|
const title = entry.title && entry.title !== entry.phrase && titleSpace >= 8
|
|
16
38
|
? truncate(entry.title, titleSpace)
|
|
17
39
|
: "";
|
|
18
|
-
return (_jsxs(Box, {
|
|
40
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: entry.color, children: idText }), _jsx(Text, { color: entry.color, children: entry.glyph }), _jsx(Text, { children: ` ${paddedPhrase}` }), title ? _jsx(Text, { dimColor: true, children: ` ${title}` }) : null] }));
|
|
19
41
|
}
|
|
20
|
-
function
|
|
21
|
-
if (
|
|
22
|
-
return
|
|
23
|
-
if (
|
|
24
|
-
return
|
|
25
|
-
|
|
42
|
+
function renderLine(line, key, width) {
|
|
43
|
+
if (line.kind === "blank")
|
|
44
|
+
return _jsx(Box, { children: _jsx(Text, { children: " " }) }, key);
|
|
45
|
+
if (line.kind === "summary-line") {
|
|
46
|
+
return (_jsxs(Box, { children: [_jsx(Text, { children: " ".repeat(SUMMARY_INDENT) }), _jsx(Text, { dimColor: true, children: line.text })] }, key));
|
|
47
|
+
}
|
|
48
|
+
return _jsx(EntryHeaderRow, { entry: line.entry, width: width }, key);
|
|
26
49
|
}
|
|
27
|
-
export function
|
|
50
|
+
export function clampScrollOffset(requested, totalLines, scrollArea) {
|
|
51
|
+
if (scrollArea <= 0 || totalLines <= scrollArea)
|
|
52
|
+
return 0;
|
|
53
|
+
// When scrolled at all, a one-row top indicator eats into the viewport, so the
|
|
54
|
+
// largest useful offset lets (scrollArea - 1) rows of content fill the remainder.
|
|
55
|
+
const maxOffset = Math.max(0, totalLines - Math.max(1, scrollArea - 1));
|
|
56
|
+
return Math.max(0, Math.min(requested, maxOffset));
|
|
57
|
+
}
|
|
58
|
+
export function ProjectDetailView({ model, selectedRepoId, bodyRows, scrollOffset }) {
|
|
28
59
|
const { stdout } = useStdout();
|
|
29
|
-
const rows = Math.max(3, stdout?.rows ?? 24);
|
|
30
60
|
const width = Math.max(40, stdout?.columns ?? 80);
|
|
31
61
|
const repo = selectedRepoId
|
|
32
62
|
? model.repos.find((candidate) => candidate.repoId === selectedRepoId) ?? model.repos[0] ?? null
|
|
@@ -34,42 +64,32 @@ export function ProjectDetailView({ model, selectedRepoId }) {
|
|
|
34
64
|
if (!repo) {
|
|
35
65
|
return _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: " " }) });
|
|
36
66
|
}
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
67
|
+
const repoHeaderRows = 1;
|
|
68
|
+
const separatorRows = bodyRows >= 3 ? 1 : 0;
|
|
69
|
+
const scrollArea = Math.max(0, bodyRows - repoHeaderRows - separatorRows);
|
|
70
|
+
const contentWidth = Math.max(20, width - 2);
|
|
71
|
+
const contentLines = buildContentLines(repo, contentWidth);
|
|
72
|
+
const offset = clampScrollOffset(scrollOffset, contentLines.length, scrollArea);
|
|
73
|
+
const children = [];
|
|
74
|
+
const couldHaveAbove = offset > 0;
|
|
75
|
+
const reserveTop = couldHaveAbove && scrollArea > 0 ? 1 : 0;
|
|
76
|
+
let remaining = scrollArea - reserveTop;
|
|
77
|
+
let tentativeEnd = Math.min(contentLines.length, offset + remaining);
|
|
78
|
+
const belowCount = contentLines.length - tentativeEnd;
|
|
79
|
+
const reserveBottom = belowCount > 0 && remaining > 0 ? 1 : 0;
|
|
80
|
+
if (reserveBottom) {
|
|
81
|
+
remaining = Math.max(0, remaining - 1);
|
|
82
|
+
tentativeEnd = Math.min(contentLines.length, offset + remaining);
|
|
50
83
|
}
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (!summaryText)
|
|
61
|
-
continue;
|
|
62
|
-
const summaryLines = summaryText.split("\n").length;
|
|
63
|
-
const isLast = i === planned.length - 1;
|
|
64
|
-
const extra = summaryLines + (isLast ? 0 : 1);
|
|
65
|
-
if (used + extra > afterRepoRow)
|
|
66
|
-
break;
|
|
67
|
-
item.includeSummary = true;
|
|
68
|
-
used += extra;
|
|
69
|
-
}
|
|
84
|
+
if (reserveTop) {
|
|
85
|
+
children.push(_jsx(Box, { children: _jsx(Text, { dimColor: true, children: ` \u2191${offset} more above` }) }, "above"));
|
|
86
|
+
}
|
|
87
|
+
for (let i = offset; i < tentativeEnd; i += 1) {
|
|
88
|
+
children.push(renderLine(contentLines[i], i, contentWidth));
|
|
89
|
+
}
|
|
90
|
+
if (reserveBottom) {
|
|
91
|
+
const hidden = contentLines.length - tentativeEnd;
|
|
92
|
+
children.push(_jsx(Box, { children: _jsx(Text, { dimColor: true, children: ` \u2193${hidden} more below` }) }, "below"));
|
|
70
93
|
}
|
|
71
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(RepoRow, { repo: repo, selected: false, showCursor: false, width: width - 2 }),
|
|
72
|
-
const isLast = index === planned.length - 1;
|
|
73
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(PrEntryRow, { entry: entry, width: width, includeSummary: includeSummary }), includeSummary && !isLast ? _jsx(Box, { children: _jsx(Text, { children: " " }) }) : null] }, entry.prNumber));
|
|
74
|
-
})] }));
|
|
94
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(RepoRow, { repo: repo, selected: false, showCursor: false, width: width - 2 }), separatorRows > 0 ? _jsx(Box, { children: _jsx(Text, { children: " " }) }) : null, children] }));
|
|
75
95
|
}
|