merge-steward 0.7.0 → 0.8.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
|
@@ -35,6 +35,14 @@ export function App({ baseUrl, initialPrNumber }) {
|
|
|
35
35
|
const entries = snapshot?.entries ?? [];
|
|
36
36
|
return filter === "active" ? entries.filter(isActiveEntry) : entries;
|
|
37
37
|
}, [filter, snapshot?.entries]);
|
|
38
|
+
// Recently completed entries: terminal entries within the last 60 seconds.
|
|
39
|
+
const recentlyCompleted = useMemo(() => {
|
|
40
|
+
if (filter !== "active")
|
|
41
|
+
return [];
|
|
42
|
+
const entries = snapshot?.entries ?? [];
|
|
43
|
+
const cutoff = Date.now() - 60_000;
|
|
44
|
+
return entries.filter((e) => !isActiveEntry(e) && new Date(e.updatedAt).getTime() > cutoff);
|
|
45
|
+
}, [filter, snapshot?.entries]);
|
|
38
46
|
const selectedEntry = useMemo(() => visibleEntries.find((entry) => entry.id === selectedEntryId) ?? null, [selectedEntryId, visibleEntries]);
|
|
39
47
|
const activeEntries = useMemo(() => (snapshot?.entries ?? []).filter(isActiveEntry), [snapshot?.entries]);
|
|
40
48
|
const selectedActiveIndex = useMemo(() => {
|
|
@@ -188,5 +196,5 @@ export function App({ baseUrl, initialPrNumber }) {
|
|
|
188
196
|
setSelectedEntryId(nextSelection(visibleEntries, selectedEntryId, "prev"));
|
|
189
197
|
}
|
|
190
198
|
});
|
|
191
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(StatusBar, { snapshot: snapshot, connected: connected, filter: filter, lastSnapshotReceivedAt: lastSnapshotReceivedAt, expectedFreshMs: REFRESH_INTERVAL_MS * 2 }), view === "detail" && selectedEntry && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Queue" }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsxs(Text, { bold: true, children: ["#", selectedEntry.prNumber] }), _jsxs(Text, { dimColor: true, children: [" (", selectedEntry.status, ")"] })] })), view === "list" ? (_jsx(QueueListView, { entries: visibleEntries, selectedEntryId: selectedEntryId, recentEvents: snapshot?.recentEvents ?? [], headEntryId: snapshot?.summary.headEntryId ?? null, queueBlock: snapshot?.queueBlock ?? null })) : (_jsx(DetailView, { detail: detail, isHead: isHeadSelected, activeIndex: selectedActiveIndex, activeCount: activeEntries.length, headPrNumber: snapshot?.summary.headPrNumber ?? null, queueBlock: snapshot?.queueBlock ?? null })), flashMessage && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: flashMessage }) })), _jsx(HelpBar, { view: view })] }));
|
|
199
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(StatusBar, { snapshot: snapshot, connected: connected, filter: filter, lastSnapshotReceivedAt: lastSnapshotReceivedAt, expectedFreshMs: REFRESH_INTERVAL_MS * 2 }), view === "detail" && selectedEntry && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "Queue" }), _jsx(Text, { dimColor: true, children: " \u203A " }), _jsxs(Text, { bold: true, children: ["#", selectedEntry.prNumber] }), _jsxs(Text, { dimColor: true, children: [" (", selectedEntry.status, ")"] })] })), view === "list" ? (_jsx(QueueListView, { entries: visibleEntries, recentlyCompleted: recentlyCompleted, selectedEntryId: selectedEntryId, recentEvents: snapshot?.recentEvents ?? [], headEntryId: snapshot?.summary.headEntryId ?? null, queueBlock: snapshot?.queueBlock ?? null })) : (_jsx(DetailView, { detail: detail, isHead: isHeadSelected, activeIndex: selectedActiveIndex, activeCount: activeEntries.length, headPrNumber: snapshot?.summary.headPrNumber ?? null, queueBlock: snapshot?.queueBlock ?? null })), flashMessage && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: flashMessage }) })), _jsx(HelpBar, { view: view })] }));
|
|
192
200
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { QueueBlockState, QueueEntry, QueueEventSummary } from "../types.ts";
|
|
2
2
|
interface QueueListViewProps {
|
|
3
3
|
entries: QueueEntry[];
|
|
4
|
+
recentlyCompleted: QueueEntry[];
|
|
4
5
|
selectedEntryId: string | null;
|
|
5
6
|
recentEvents: QueueEventSummary[];
|
|
6
7
|
headEntryId: string | null;
|
|
7
8
|
queueBlock: QueueBlockState | null;
|
|
8
9
|
}
|
|
9
|
-
export declare function QueueListView({ entries, selectedEntryId, recentEvents, headEntryId, queueBlock, }: QueueListViewProps): React.JSX.Element;
|
|
10
|
+
export declare function QueueListView({ entries, recentlyCompleted, selectedEntryId, recentEvents, headEntryId, queueBlock, }: QueueListViewProps): React.JSX.Element;
|
|
10
11
|
export {};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo } from "react";
|
|
3
3
|
import { Box, Text, useStdout } from "ink";
|
|
4
|
-
import { formatEventSummary, humanStatus, nextStepLabel, progressBar, queueProgress, relativeTime, specChainLabel, statusColor, summarizeQueueBlock, truncate } from "./format.js";
|
|
4
|
+
import { ciStatusIcon, formatEventSummary, humanStatus, nextStepLabel, progressBar, queueProgress, relativeTime, specChainLabel, statusColor, summarizeQueueBlock, truncate } from "./format.js";
|
|
5
|
+
import { TERMINAL_STATUSES } from "../types.js";
|
|
5
6
|
const ENTRY_ROW_HEIGHT = 2;
|
|
6
7
|
const CHROME_ROWS = 13;
|
|
8
|
+
const RECENTLY_COMPLETED_MAX_AGE_MS = 60_000;
|
|
7
9
|
function QueueRow({ entry, selected, infoWidth, isHead, queueBlock, allEntries, }) {
|
|
8
10
|
const retryText = `${entry.retryAttempts}/${entry.maxRetries}`;
|
|
9
11
|
const ciText = entry.ciRetries > 0 ? `CI retries ${entry.ciRetries}` : null;
|
|
@@ -22,13 +24,22 @@ function QueueRow({ entry, selected, infoWidth, isHead, queueBlock, allEntries,
|
|
|
22
24
|
: nextStepLabel(entry.status, entry);
|
|
23
25
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: selected ? "cyan" : "gray", children: selected ? "›" : " " }), _jsx(Text, { color: blockedOnMain ? "red" : isHead ? "green" : "gray", children: blockedOnMain ? "!" : isHead ? "#" : " " }), _jsx(Text, { bold: true, children: ` #${entry.prNumber}` }), entry.issueKey ? _jsx(Text, { children: ` ${entry.issueKey}` }) : null, _jsx(Text, { dimColor: true, children: ` pos ${entry.position}` }), _jsx(Text, { dimColor: true, children: ` ${relativeTime(entry.updatedAt)}` }), _jsx(Text, { children: ` ` }), _jsx(Text, { color: renderedColor, children: renderedStatus })] }), _jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { dimColor: true, children: progressBar(progress.current, progress.total, 8) }), _jsx(Text, { dimColor: true, children: specLabel }), _jsx(Text, { dimColor: true, children: "|" }), _jsx(Text, { dimColor: true, children: nextStep }), _jsx(Text, { dimColor: true, children: ` | retry ${retryText}` }), ciText ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "|" }), _jsx(Text, { dimColor: true, children: ciText })] })) : null] })] }));
|
|
24
26
|
}
|
|
25
|
-
export function QueueListView({ entries, selectedEntryId, recentEvents, headEntryId, queueBlock, }) {
|
|
27
|
+
export function QueueListView({ entries, recentlyCompleted, selectedEntryId, recentEvents, headEntryId, queueBlock, }) {
|
|
26
28
|
const { stdout } = useStdout();
|
|
27
29
|
const rows = stdout?.rows ?? 24;
|
|
28
30
|
const cols = stdout?.columns ?? 100;
|
|
29
31
|
const infoWidth = Math.max(32, cols - 4);
|
|
30
|
-
const
|
|
32
|
+
const totalRows = entries.length + recentlyCompleted.length;
|
|
33
|
+
const eventRows = Math.min(8, Math.max(4, rows - (totalRows * ENTRY_ROW_HEIGHT) - CHROME_ROWS));
|
|
31
34
|
const displayedEvents = useMemo(() => recentEvents.slice(-eventRows), [eventRows, recentEvents]);
|
|
32
35
|
const queueBlockLabel = summarizeQueueBlock(queueBlock);
|
|
33
|
-
|
|
36
|
+
// Spec chain: main ─ #A ✓ ─ #B ● ─ #C ○
|
|
37
|
+
const chainEntries = useMemo(() => {
|
|
38
|
+
const active = entries.filter((e) => !TERMINAL_STATUSES.includes(e.status));
|
|
39
|
+
return active.sort((a, b) => a.position - b.position);
|
|
40
|
+
}, [entries]);
|
|
41
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [chainEntries.length > 0 && (_jsxs(Box, { marginBottom: 1, gap: 0, children: [_jsx(Text, { dimColor: true, children: "main" }), chainEntries.map((entry) => {
|
|
42
|
+
const ci = ciStatusIcon(entry);
|
|
43
|
+
return (_jsxs(Box, { gap: 0, children: [_jsx(Text, { dimColor: true, children: " \u2500 " }), _jsxs(Text, { bold: true, children: ["#", entry.prNumber] }), _jsx(Text, { color: ci.color, children: ` ${ci.icon}` })] }, entry.id));
|
|
44
|
+
})] })), queueBlock && (_jsxs(Box, { marginBottom: 1, flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: ["Queue paused: ", queueBlockLabel ?? "main is unhealthy", queueBlock.baseSha ? ` at ${truncate(queueBlock.baseSha, 10)}` : "", "."] }), _jsxs(Text, { dimColor: true, children: ["Head PR #", queueBlock.headPrNumber ?? "?", " will resume automatically once main recovers."] })] })), entries.length === 0 && recentlyCompleted.length === 0 ? (_jsx(Text, { dimColor: true, children: "No queue entries in this filter." })) : (_jsxs(_Fragment, { children: [entries.map((entry) => (_jsx(QueueRow, { entry: entry, selected: entry.id === selectedEntryId, infoWidth: infoWidth, isHead: entry.id === headEntryId, queueBlock: queueBlock, allEntries: entries }, entry.id))), recentlyCompleted.length > 0 && (_jsx(_Fragment, { children: recentlyCompleted.map((entry) => (_jsx(Box, { flexDirection: "column", children: _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { color: entry.status === "merged" ? "green" : "red", children: entry.status === "merged" ? "\u2713" : "\u2717" }), _jsx(Text, { dimColor: true, children: ` #${entry.prNumber}` }), entry.issueKey ? _jsx(Text, { dimColor: true, children: ` ${entry.issueKey}` }) : null, _jsx(Text, { dimColor: true, children: ` ${humanStatus(entry.status, entry)} ${relativeTime(entry.updatedAt)}` })] }) }, entry.id))) }))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Recent Events" }), displayedEvents.length === 0 ? (_jsx(Text, { dimColor: true, children: "No queue events yet." })) : (displayedEvents.map((event) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: relativeTime(event.at).padStart(4, " ") }), _jsx(Text, { children: formatEventSummary(event) })] }, event.id ?? `${event.entryId}-${event.at}`))))] })] }));
|
|
34
45
|
}
|
package/dist/watch/format.d.ts
CHANGED
|
@@ -31,4 +31,12 @@ export declare function formatDuration(ms: number): string;
|
|
|
31
31
|
export declare function progressBar(current: number, total: number, width: number): string;
|
|
32
32
|
export declare function truncate(value: string, width: number): string;
|
|
33
33
|
export declare function summarizeCheckNames(checks: CheckResult[], limit?: number): string;
|
|
34
|
+
/** CI status icon for the spec chain summary line. */
|
|
35
|
+
export declare function ciStatusIcon(entry: {
|
|
36
|
+
status: QueueEntryStatus;
|
|
37
|
+
ciRunId: string | null;
|
|
38
|
+
}): {
|
|
39
|
+
icon: string;
|
|
40
|
+
color: string;
|
|
41
|
+
};
|
|
34
42
|
export declare function summarizeQueueBlock(block: QueueBlockState | null | undefined): string | null;
|
package/dist/watch/format.js
CHANGED
|
@@ -155,6 +155,22 @@ export function summarizeCheckNames(checks, limit = 3) {
|
|
|
155
155
|
return names.join(", ");
|
|
156
156
|
return `${names.slice(0, limit).join(", ")} +${names.length - limit} more`;
|
|
157
157
|
}
|
|
158
|
+
/** CI status icon for the spec chain summary line. */
|
|
159
|
+
export function ciStatusIcon(entry) {
|
|
160
|
+
switch (entry.status) {
|
|
161
|
+
case "merged": return { icon: "\u2713", color: "green" }; // ✓
|
|
162
|
+
case "merging": return { icon: "\u2713", color: "cyan" }; // ✓
|
|
163
|
+
case "evicted": return { icon: "\u2717", color: "red" }; // ✗
|
|
164
|
+
case "dequeued": return { icon: "\u2012", color: "gray" }; // ‒
|
|
165
|
+
case "validating":
|
|
166
|
+
return entry.ciRunId ? { icon: "\u25cf", color: "cyan" } : { icon: "\u25cb", color: "gray" }; // ● or ○
|
|
167
|
+
case "preparing_head":
|
|
168
|
+
return { icon: "\u25cb", color: "yellow" }; // ○
|
|
169
|
+
case "queued":
|
|
170
|
+
default:
|
|
171
|
+
return { icon: "\u25cb", color: "gray" }; // ○
|
|
172
|
+
}
|
|
173
|
+
}
|
|
158
174
|
export function summarizeQueueBlock(block) {
|
|
159
175
|
if (!block)
|
|
160
176
|
return null;
|