merge-steward 0.6.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 +9 -1
- package/dist/watch/DetailView.js +1 -1
- package/dist/watch/QueueListView.d.ts +2 -1
- package/dist/watch/QueueListView.js +26 -10
- package/dist/watch/format.d.ts +26 -2
- package/dist/watch/format.js +38 -6
- package/dist/watch/state-visualization.js +9 -3
- package/package.json +1 -1
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
|
}
|
package/dist/watch/DetailView.js
CHANGED
|
@@ -18,5 +18,5 @@ export function DetailView({ detail, isHead, activeIndex, activeCount, headPrNum
|
|
|
18
18
|
queueBlock,
|
|
19
19
|
});
|
|
20
20
|
const pipeline = queueProgress(entry.status);
|
|
21
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { gap: 2, children: [_jsxs(Text, { bold: true, children: ["#", entry.prNumber] }), entry.issueKey ? _jsx(Text, { children: entry.issueKey }) : null, _jsx(Text, { color: statusColor(entry.status), children: humanStatus(entry.status) }), _jsxs(Text, { dimColor: true, children: ["pos ", entry.position] }), _jsxs(Text, { dimColor: true, children: ["
|
|
21
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { gap: 2, children: [_jsxs(Text, { bold: true, children: ["#", entry.prNumber] }), entry.issueKey ? _jsx(Text, { children: entry.issueKey }) : null, _jsx(Text, { color: statusColor(entry.status), children: humanStatus(entry.status, entry) }), _jsxs(Text, { dimColor: true, children: ["pos ", entry.position] }), _jsxs(Text, { dimColor: true, children: ["retry ", entry.retryAttempts, "/", entry.maxRetries] })] }), _jsx(Text, { children: entry.branch }), _jsxs(Box, { gap: 2, children: [_jsxs(Text, { dimColor: true, children: ["head ", shortSha(entry.headSha)] }), _jsxs(Text, { dimColor: true, children: ["base ", shortSha(entry.baseSha)] })] }), entry.specBranch && (_jsxs(Box, { gap: 2, children: [_jsx(Text, { dimColor: true, children: "spec" }), _jsx(Text, { children: entry.specBranch }), _jsx(Text, { dimColor: true, children: shortSha(entry.specSha) }), _jsx(Text, { dimColor: true, children: "\u2190" }), _jsx(Text, { dimColor: true, children: entry.specBasedOn ? `entry ${shortSha(entry.specBasedOn)}` : "main" })] })), _jsxs(Box, { gap: 1, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "progress" }), _jsx(Text, { children: progressBar(pipeline.current, pipeline.total, 12) }), _jsx(Text, { dimColor: true, children: nextStepLabel(entry.status, entry) })] }), isHead && queueBlock && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: ["Queue paused: ", summarizeQueueBlock(queueBlock) ?? "main branch is unhealthy", "."] }), _jsxs(Text, { dimColor: true, children: [queueBlock.baseBranch, queueBlock.baseSha ? ` @ ${shortSha(queueBlock.baseSha)}` : ""] }), _jsxs(Text, { dimColor: true, children: ["Head PR #", queueBlock.headPrNumber ?? entry.prNumber, " will resume automatically once main recovers."] })] })), entry.maxRetries > 0 && (_jsxs(Box, { gap: 1, marginTop: 1, children: [_jsx(Text, { dimColor: true, children: "retry" }), _jsx(Text, { children: progressBar(entry.retryAttempts, entry.maxRetries, 10) }), _jsxs(Text, { dimColor: true, children: [entry.retryAttempts, "/", entry.maxRetries] })] })), _jsx(EntryStateGraph, { main: graph.main, exits: graph.exits }), _jsx(ExternalRepairObservation, { observations: observations }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Incidents" }), incidents.length === 0 ? (_jsx(Text, { dimColor: true, children: "No incidents." })) : (incidents.map((incident) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: relativeTime(incident.at).padStart(4, " ") }), _jsx(Text, { color: "red", children: incident.failureClass }), _jsx(Text, { dimColor: true, children: incident.outcome })] }, incident.id))))] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Events" }), events.length === 0 ? (_jsx(Text, { dimColor: true, children: "No events yet." })) : (events.slice(-16).map((event) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: relativeTime(event.at).padStart(4, " ") }), _jsx(Text, { children: formatEntryEvent(event) })] }, event.id ?? `${event.entryId}-${event.at}`))))] })] }));
|
|
22
22
|
}
|
|
@@ -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,29 +1,45 @@
|
|
|
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, 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;
|
|
7
|
-
|
|
8
|
+
const RECENTLY_COMPLETED_MAX_AGE_MS = 60_000;
|
|
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;
|
|
10
12
|
const blockedOnMain = isHead && queueBlock?.reason === "main_broken" && queueBlock.headPrNumber === entry.prNumber;
|
|
11
|
-
const renderedStatus = blockedOnMain ? "blocked by broken main" : humanStatus(entry.status);
|
|
12
|
-
const renderedColor = blockedOnMain
|
|
13
|
+
const renderedStatus = blockedOnMain ? "blocked by broken main" : humanStatus(entry.status, entry);
|
|
14
|
+
const renderedColor = blockedOnMain
|
|
15
|
+
? "red"
|
|
16
|
+
: entry.status === "preparing_head" && entry.lastFailedBaseSha ? "yellow"
|
|
17
|
+
: statusColor(entry.status);
|
|
13
18
|
const progress = queueProgress(entry.status);
|
|
14
|
-
const
|
|
19
|
+
const specLabel = entry.specBranch
|
|
20
|
+
? specChainLabel(entry, allEntries)
|
|
21
|
+
: truncate(entry.branch, Math.max(12, infoWidth - 34));
|
|
15
22
|
const nextStep = blockedOnMain
|
|
16
23
|
? summarizeQueueBlock(queueBlock) ?? "waiting for main to recover"
|
|
17
|
-
: nextStepLabel(entry.status);
|
|
18
|
-
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:
|
|
24
|
+
: nextStepLabel(entry.status, entry);
|
|
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] })] }));
|
|
19
26
|
}
|
|
20
|
-
export function QueueListView({ entries, selectedEntryId, recentEvents, headEntryId, queueBlock, }) {
|
|
27
|
+
export function QueueListView({ entries, recentlyCompleted, selectedEntryId, recentEvents, headEntryId, queueBlock, }) {
|
|
21
28
|
const { stdout } = useStdout();
|
|
22
29
|
const rows = stdout?.rows ?? 24;
|
|
23
30
|
const cols = stdout?.columns ?? 100;
|
|
24
31
|
const infoWidth = Math.max(32, cols - 4);
|
|
25
|
-
const
|
|
32
|
+
const totalRows = entries.length + recentlyCompleted.length;
|
|
33
|
+
const eventRows = Math.min(8, Math.max(4, rows - (totalRows * ENTRY_ROW_HEIGHT) - CHROME_ROWS));
|
|
26
34
|
const displayedEvents = useMemo(() => recentEvents.slice(-eventRows), [eventRows, recentEvents]);
|
|
27
35
|
const queueBlockLabel = summarizeQueueBlock(queueBlock);
|
|
28
|
-
|
|
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}`))))] })] }));
|
|
29
45
|
}
|
package/dist/watch/format.d.ts
CHANGED
|
@@ -2,12 +2,28 @@ import type { CheckResult, QueueBlockState, QueueEntryStatus, QueueEventRecord,
|
|
|
2
2
|
export declare function shortSha(value: string | null | undefined): string;
|
|
3
3
|
export declare function relativeTime(iso: string | null | undefined): string;
|
|
4
4
|
export declare function statusColor(status: QueueEntryStatus): "yellow" | "cyan" | "green" | "red" | "gray";
|
|
5
|
-
export declare function humanStatus(status: QueueEntryStatus
|
|
5
|
+
export declare function humanStatus(status: QueueEntryStatus, entry?: {
|
|
6
|
+
lastFailedBaseSha: string | null;
|
|
7
|
+
specBranch: string | null;
|
|
8
|
+
}): string;
|
|
6
9
|
export declare function queueProgress(status: QueueEntryStatus): {
|
|
7
10
|
current: number;
|
|
8
11
|
total: number;
|
|
9
12
|
};
|
|
10
|
-
export declare function nextStepLabel(status: QueueEntryStatus
|
|
13
|
+
export declare function nextStepLabel(status: QueueEntryStatus, entry?: {
|
|
14
|
+
lastFailedBaseSha: string | null;
|
|
15
|
+
specBasedOn: string | null;
|
|
16
|
+
}): string;
|
|
17
|
+
/** Describe the spec chain for a queue entry. */
|
|
18
|
+
export declare function specChainLabel(entry: {
|
|
19
|
+
specBranch: string | null;
|
|
20
|
+
specBasedOn: string | null;
|
|
21
|
+
specSha: string | null;
|
|
22
|
+
}, allEntries: Array<{
|
|
23
|
+
id: string;
|
|
24
|
+
prNumber: number;
|
|
25
|
+
specBranch: string | null;
|
|
26
|
+
}>): string;
|
|
11
27
|
export declare function runtimeLabel(runtime: QueueRuntimeStatus): string;
|
|
12
28
|
export declare function formatEventSummary(event: QueueEventSummary): string;
|
|
13
29
|
export declare function formatEntryEvent(event: QueueEventRecord): string;
|
|
@@ -15,4 +31,12 @@ export declare function formatDuration(ms: number): string;
|
|
|
15
31
|
export declare function progressBar(current: number, total: number, width: number): string;
|
|
16
32
|
export declare function truncate(value: string, width: number): string;
|
|
17
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
|
+
};
|
|
18
42
|
export declare function summarizeQueueBlock(block: QueueBlockState | null | undefined): string | null;
|
package/dist/watch/format.js
CHANGED
|
@@ -38,12 +38,14 @@ export function statusColor(status) {
|
|
|
38
38
|
return "gray";
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
export function humanStatus(status) {
|
|
41
|
+
export function humanStatus(status, entry) {
|
|
42
42
|
switch (status) {
|
|
43
43
|
case "queued":
|
|
44
44
|
return "queued";
|
|
45
45
|
case "preparing_head":
|
|
46
|
-
|
|
46
|
+
if (entry?.lastFailedBaseSha)
|
|
47
|
+
return "retry-gated";
|
|
48
|
+
return "building spec";
|
|
47
49
|
case "validating":
|
|
48
50
|
return "running CI";
|
|
49
51
|
case "merging":
|
|
@@ -71,16 +73,18 @@ export function queueProgress(status) {
|
|
|
71
73
|
return { current: 4, total: 4 };
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
|
-
export function nextStepLabel(status) {
|
|
76
|
+
export function nextStepLabel(status, entry) {
|
|
75
77
|
switch (status) {
|
|
76
78
|
case "queued":
|
|
77
79
|
return "waiting for head-of-line turn";
|
|
78
80
|
case "preparing_head":
|
|
79
|
-
|
|
81
|
+
if (entry?.lastFailedBaseSha)
|
|
82
|
+
return "waiting for base to advance";
|
|
83
|
+
return "building cumulative spec branch";
|
|
80
84
|
case "validating":
|
|
81
|
-
return "waiting for CI
|
|
85
|
+
return "waiting for CI on spec branch";
|
|
82
86
|
case "merging":
|
|
83
|
-
return "
|
|
87
|
+
return "pushing spec to main";
|
|
84
88
|
case "merged":
|
|
85
89
|
return "landed on main";
|
|
86
90
|
case "evicted":
|
|
@@ -89,6 +93,18 @@ export function nextStepLabel(status) {
|
|
|
89
93
|
return "removed manually";
|
|
90
94
|
}
|
|
91
95
|
}
|
|
96
|
+
/** Describe the spec chain for a queue entry. */
|
|
97
|
+
export function specChainLabel(entry, allEntries) {
|
|
98
|
+
if (!entry.specBranch)
|
|
99
|
+
return "no spec yet";
|
|
100
|
+
const parent = entry.specBasedOn
|
|
101
|
+
? allEntries.find((e) => e.id === entry.specBasedOn)
|
|
102
|
+
: null;
|
|
103
|
+
const base = parent
|
|
104
|
+
? `#${parent.prNumber}`
|
|
105
|
+
: "main";
|
|
106
|
+
return `${shortSha(entry.specSha)} \u2190 ${base}`;
|
|
107
|
+
}
|
|
92
108
|
export function runtimeLabel(runtime) {
|
|
93
109
|
if (runtime.tickInProgress) {
|
|
94
110
|
return "running";
|
|
@@ -139,6 +155,22 @@ export function summarizeCheckNames(checks, limit = 3) {
|
|
|
139
155
|
return names.join(", ");
|
|
140
156
|
return `${names.slice(0, limit).join(", ")} +${names.length - limit} more`;
|
|
141
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
|
+
}
|
|
142
174
|
export function summarizeQueueBlock(block) {
|
|
143
175
|
if (!block)
|
|
144
176
|
return null;
|
|
@@ -96,19 +96,25 @@ export function buildExternalRepairObservations(detail, options) {
|
|
|
96
96
|
if (entry.lastFailedBaseSha) {
|
|
97
97
|
observations.push({
|
|
98
98
|
tone: "warn",
|
|
99
|
-
text: `
|
|
99
|
+
text: `Retry-gated: conflict on base ${entry.lastFailedBaseSha.slice(0, 7)}. Waiting for base to advance before rebuilding spec.`,
|
|
100
100
|
});
|
|
101
101
|
}
|
|
102
102
|
else if (entry.status === "validating") {
|
|
103
|
+
const specNote = entry.specBranch
|
|
104
|
+
? `CI running on spec branch ${entry.specBranch}.`
|
|
105
|
+
: "Waiting on CI for the spec branch.";
|
|
106
|
+
const cascadeNote = entry.specBasedOn
|
|
107
|
+
? " Will merge automatically when head clears (cascade)."
|
|
108
|
+
: "";
|
|
103
109
|
observations.push({
|
|
104
110
|
tone: "info",
|
|
105
|
-
text:
|
|
111
|
+
text: `${specNote}${cascadeNote}`,
|
|
106
112
|
});
|
|
107
113
|
}
|
|
108
114
|
else if (entry.status === "merging") {
|
|
109
115
|
observations.push({
|
|
110
116
|
tone: "info",
|
|
111
|
-
text: "
|
|
117
|
+
text: "CI passed; pushing spec branch to main (fast-forward).",
|
|
112
118
|
});
|
|
113
119
|
}
|
|
114
120
|
if (entry.generation > 0) {
|