patchrelay 0.23.5 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/cli/watch/App.js +7 -1
- package/dist/cli/watch/HelpBar.js +9 -4
- package/dist/cli/watch/IssueDetailView.js +9 -19
- package/dist/cli/watch/StateHistoryView.js +111 -0
- package/dist/cli/watch/history-builder.js +253 -0
- package/dist/cli/watch/plan-helpers.js +14 -0
- package/dist/cli/watch/watch-state.js +14 -3
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
package/dist/cli/watch/App.js
CHANGED
|
@@ -171,6 +171,12 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
171
171
|
});
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
+
else if (input === "h") {
|
|
175
|
+
dispatch({ type: "switch-detail-tab", tab: "history" });
|
|
176
|
+
}
|
|
177
|
+
else if (input === "t") {
|
|
178
|
+
dispatch({ type: "switch-detail-tab", tab: "timeline" });
|
|
179
|
+
}
|
|
174
180
|
else if (input === "j" || key.downArrow) {
|
|
175
181
|
dispatch({ type: "detail-navigate", direction: "next", filtered });
|
|
176
182
|
}
|
|
@@ -184,5 +190,5 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
184
190
|
}
|
|
185
191
|
}
|
|
186
192
|
});
|
|
187
|
-
return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { issues: filtered, allIssues: state.issues, selectedIndex: state.selectedIndex, connected: state.connected, filter: state.filter, totalCount: state.issues.length })) : state.view === "detail" ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(IssueDetailView, { issue: state.issues.find((i) => i.issueKey === state.activeDetailKey), timeline: state.timeline, follow: state.follow, activeRunStartedAt: state.activeRunStartedAt, tokenUsage: state.tokenUsage, diffSummary: state.diffSummary, plan: state.plan, issueContext: state.issueContext }), promptMode && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "prompt> " }), _jsx(Text, { children: promptBuffer }), _jsx(Text, { dimColor: true, children: "_" })] })), promptStatus && !promptMode && (_jsx(Text, { dimColor: true, children: promptStatus }))] })) : (_jsx(FeedView, { events: state.feedEvents, connected: state.connected })) }));
|
|
193
|
+
return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { issues: filtered, allIssues: state.issues, selectedIndex: state.selectedIndex, connected: state.connected, filter: state.filter, totalCount: state.issues.length })) : state.view === "detail" ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(IssueDetailView, { issue: state.issues.find((i) => i.issueKey === state.activeDetailKey), timeline: state.timeline, follow: state.follow, activeRunStartedAt: state.activeRunStartedAt, activeRunId: state.activeRunId, tokenUsage: state.tokenUsage, diffSummary: state.diffSummary, plan: state.plan, issueContext: state.issueContext, detailTab: state.detailTab, rawRuns: state.rawRuns, rawFeedEvents: state.rawFeedEvents }), promptMode && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "prompt> " }), _jsx(Text, { children: promptBuffer }), _jsx(Text, { dimColor: true, children: "_" })] })), promptStatus && !promptMode && (_jsx(Text, { dimColor: true, children: promptStatus }))] })) : (_jsx(FeedView, { events: state.feedEvents, connected: state.connected })) }));
|
|
188
194
|
}
|
|
@@ -5,9 +5,14 @@ const HELP_TEXT = {
|
|
|
5
5
|
detail: "",
|
|
6
6
|
feed: "Esc: list q: quit",
|
|
7
7
|
};
|
|
8
|
-
export function HelpBar({ view, follow }) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
:
|
|
8
|
+
export function HelpBar({ view, follow, detailTab }) {
|
|
9
|
+
let text;
|
|
10
|
+
if (view === "detail") {
|
|
11
|
+
const tabHint = detailTab === "history" ? "t: timeline" : "h: history";
|
|
12
|
+
text = `${tabHint} j/k: prev/next Esc: list f: follow ${follow ? "on" : "off"} p: prompt s: stop r: retry q: quit`;
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
text = HELP_TEXT[view];
|
|
16
|
+
}
|
|
12
17
|
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: text }) }));
|
|
13
18
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useReducer } from "react";
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useReducer } from "react";
|
|
3
3
|
import { Box, Text } from "ink";
|
|
4
4
|
import { Timeline } from "./Timeline.js";
|
|
5
|
+
import { StateHistoryView } from "./StateHistoryView.js";
|
|
6
|
+
import { buildStateHistory } from "./history-builder.js";
|
|
5
7
|
import { HelpBar } from "./HelpBar.js";
|
|
8
|
+
import { planStepSymbol, planStepColor } from "./plan-helpers.js";
|
|
6
9
|
function formatTokens(n) {
|
|
7
10
|
if (n >= 1_000_000)
|
|
8
11
|
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
@@ -21,23 +24,9 @@ function ElapsedTime({ startedAt }) {
|
|
|
21
24
|
const seconds = elapsed % 60;
|
|
22
25
|
return _jsxs(Text, { dimColor: true, children: [minutes, "m ", String(seconds).padStart(2, "0"), "s"] });
|
|
23
26
|
}
|
|
24
|
-
function
|
|
25
|
-
if (status === "completed")
|
|
26
|
-
return "\u2713";
|
|
27
|
-
if (status === "inProgress")
|
|
28
|
-
return "\u25b8";
|
|
29
|
-
return " ";
|
|
30
|
-
}
|
|
31
|
-
function planStepColor(status) {
|
|
32
|
-
if (status === "completed")
|
|
33
|
-
return "green";
|
|
34
|
-
if (status === "inProgress")
|
|
35
|
-
return "yellow";
|
|
36
|
-
return "white";
|
|
37
|
-
}
|
|
38
|
-
export function IssueDetailView({ issue, timeline, follow, activeRunStartedAt, tokenUsage, diffSummary, plan, issueContext, }) {
|
|
27
|
+
export function IssueDetailView({ issue, timeline, follow, activeRunStartedAt, activeRunId, tokenUsage, diffSummary, plan, issueContext, detailTab, rawRuns, rawFeedEvents, }) {
|
|
39
28
|
if (!issue) {
|
|
40
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", children: "Issue not found." }), _jsx(HelpBar, { view: "detail", follow: follow })] }));
|
|
29
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", children: "Issue not found." }), _jsx(HelpBar, { view: "detail", follow: follow, detailTab: detailTab })] }));
|
|
41
30
|
}
|
|
42
31
|
const key = issue.issueKey ?? issue.projectId;
|
|
43
32
|
const meta = [];
|
|
@@ -47,5 +36,6 @@ export function IssueDetailView({ issue, timeline, follow, activeRunStartedAt, t
|
|
|
47
36
|
meta.push(`${diffSummary.filesChanged}f +${diffSummary.linesAdded} -${diffSummary.linesRemoved}`);
|
|
48
37
|
if (issueContext?.runCount)
|
|
49
38
|
meta.push(`${issueContext.runCount} runs`);
|
|
50
|
-
|
|
39
|
+
const history = useMemo(() => buildStateHistory(rawRuns, rawFeedEvents, issue.factoryState, activeRunId), [rawRuns, rawFeedEvents, issue.factoryState, activeRunId]);
|
|
40
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 2, children: [_jsx(Text, { bold: true, children: key }), _jsx(Text, { color: "cyan", children: issue.factoryState }), issue.activeRunType && _jsx(Text, { color: "yellow", children: issue.activeRunType }), issue.prNumber !== undefined && _jsxs(Text, { dimColor: true, children: ["#", issue.prNumber] }), activeRunStartedAt && _jsx(ElapsedTime, { startedAt: activeRunStartedAt }), meta.length > 0 && _jsx(Text, { dimColor: true, children: meta.join(" ") }), follow && _jsx(Text, { color: "yellow", children: "follow" })] }), issue.title && _jsx(Text, { children: issue.title }), detailTab === "timeline" ? (_jsxs(_Fragment, { children: [plan && plan.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: plan.map((entry, i) => (_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: planStepColor(entry.status), children: ["[", planStepSymbol(entry.status), "]"] }), _jsx(Text, { children: entry.step })] }, `plan-${i}`))) })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Timeline, { entries: timeline, follow: follow }) })] })) : (_jsx(StateHistoryView, { history: history, plan: plan, activeRunId: activeRunId })), _jsx(Box, { marginTop: 1, children: _jsx(HelpBar, { view: "detail", follow: follow, detailTab: detailTab }) })] }));
|
|
51
41
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { planStepSymbol, planStepColor } from "./plan-helpers.js";
|
|
4
|
+
// ─── Formatting helpers ──────────────────────────────────────────
|
|
5
|
+
function formatTime(iso) {
|
|
6
|
+
return new Date(iso).toLocaleTimeString("en-GB", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
7
|
+
}
|
|
8
|
+
function formatDuration(startedAt, endedAt) {
|
|
9
|
+
const ms = new Date(endedAt).getTime() - new Date(startedAt).getTime();
|
|
10
|
+
const seconds = Math.floor(ms / 1000);
|
|
11
|
+
if (seconds < 60)
|
|
12
|
+
return `${seconds}s`;
|
|
13
|
+
const minutes = Math.floor(seconds / 60);
|
|
14
|
+
const s = seconds % 60;
|
|
15
|
+
return `${minutes}m${s > 0 ? `${String(s).padStart(2, "0")}s` : ""}`;
|
|
16
|
+
}
|
|
17
|
+
const RUN_LABELS = {
|
|
18
|
+
implementation: "implementation",
|
|
19
|
+
ci_repair: "ci repair",
|
|
20
|
+
review_fix: "review fix",
|
|
21
|
+
queue_repair: "queue repair",
|
|
22
|
+
};
|
|
23
|
+
function runStatusSymbol(status) {
|
|
24
|
+
if (status === "completed")
|
|
25
|
+
return "\u2713";
|
|
26
|
+
if (status === "failed")
|
|
27
|
+
return "\u2717";
|
|
28
|
+
if (status === "running")
|
|
29
|
+
return "\u25b8";
|
|
30
|
+
return " ";
|
|
31
|
+
}
|
|
32
|
+
function runStatusColor(status) {
|
|
33
|
+
if (status === "completed")
|
|
34
|
+
return "green";
|
|
35
|
+
if (status === "failed")
|
|
36
|
+
return "red";
|
|
37
|
+
if (status === "running")
|
|
38
|
+
return "yellow";
|
|
39
|
+
return "white";
|
|
40
|
+
}
|
|
41
|
+
const STATE_LABELS = {
|
|
42
|
+
delegated: "delegated",
|
|
43
|
+
preparing: "preparing",
|
|
44
|
+
implementing: "implementing",
|
|
45
|
+
pr_open: "pr open",
|
|
46
|
+
changes_requested: "changes requested",
|
|
47
|
+
repairing_ci: "repairing ci",
|
|
48
|
+
awaiting_queue: "awaiting queue",
|
|
49
|
+
repairing_queue: "repairing queue",
|
|
50
|
+
awaiting_input: "awaiting input",
|
|
51
|
+
escalated: "escalated",
|
|
52
|
+
done: "done",
|
|
53
|
+
failed: "failed",
|
|
54
|
+
};
|
|
55
|
+
// ─── Sub-components ──────────────────────────────────────────────
|
|
56
|
+
function RunLine({ run, index }) {
|
|
57
|
+
const label = RUN_LABELS[run.runType] ?? run.runType;
|
|
58
|
+
const dur = run.endedAt
|
|
59
|
+
? formatDuration(run.startedAt, run.endedAt)
|
|
60
|
+
: undefined;
|
|
61
|
+
const isActive = run.status === "running";
|
|
62
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { color: runStatusColor(run.status), children: [runStatusSymbol(run.status), " "] }), _jsxs(Text, { dimColor: true, children: ["#", index + 1, " "] }), _jsxs(Text, { children: ["(", label, ")"] }), dur && _jsxs(Text, { dimColor: true, children: [" ", dur] }), isActive && _jsx(Text, { dimColor: true, children: " ..." })] }));
|
|
63
|
+
}
|
|
64
|
+
const MAX_VISIBLE_RUNS = 5;
|
|
65
|
+
function RunSummary({ runs }) {
|
|
66
|
+
const completed = runs.filter((r) => r.status === "completed").length;
|
|
67
|
+
const failed = runs.filter((r) => r.status === "failed").length;
|
|
68
|
+
const running = runs.filter((r) => r.status === "running").length;
|
|
69
|
+
const parts = [];
|
|
70
|
+
if (completed > 0)
|
|
71
|
+
parts.push(`${completed} completed`);
|
|
72
|
+
if (failed > 0)
|
|
73
|
+
parts.push(`${failed} failed`);
|
|
74
|
+
if (running > 0)
|
|
75
|
+
parts.push(`${running} active`);
|
|
76
|
+
return _jsxs(Text, { dimColor: true, children: [runs.length, " runs: ", parts.join(", ")] });
|
|
77
|
+
}
|
|
78
|
+
function PlanSteps({ plan }) {
|
|
79
|
+
return (_jsx(Box, { flexDirection: "column", paddingLeft: 2, children: plan.map((entry, i) => (_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: planStepColor(entry.status), children: ["[", planStepSymbol(entry.status), "]"] }), _jsx(Text, { children: entry.step })] }, `plan-${i}`))) }));
|
|
80
|
+
}
|
|
81
|
+
function SideTripBlock({ trip, runOffset, isLast, }) {
|
|
82
|
+
const stateLabel = STATE_LABELS[trip.state] ?? trip.state;
|
|
83
|
+
const hasReturn = trip.returnedAt && trip.returnState !== trip.state;
|
|
84
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " \u2502 \u250c " }), _jsx(Text, { color: "magenta", bold: true, children: stateLabel }), _jsxs(Text, { dimColor: true, children: [" ", formatTime(trip.enteredAt)] })] }), trip.reason && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " \u2502 \u2502 " }), _jsx(Text, { dimColor: true, children: trip.reason })] })), trip.runs.map((run, ri) => (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " \u2502 \u2502 " }), _jsx(RunLine, { run: run, index: runOffset + ri })] }, `st-run-${run.id}`))), hasReturn ? (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " \u2502 \u2514\u2192 " }), _jsx(Text, { children: STATE_LABELS[trip.returnState] ?? trip.returnState }), trip.returnedAt && _jsxs(Text, { dimColor: true, children: [" ", formatTime(trip.returnedAt)] })] })) : (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: " \u2502 \u2514\u2500" }), _jsx(Text, { dimColor: true, children: " (active)" })] })), !isLast && (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: " \u2502" }) }))] }));
|
|
85
|
+
}
|
|
86
|
+
function MainPathNode({ node, isLast, runOffset, plan, activeRunId, }) {
|
|
87
|
+
const stateLabel = STATE_LABELS[node.state] ?? node.state;
|
|
88
|
+
const marker = node.isCurrent ? "\u25c9" : "\u25cb";
|
|
89
|
+
const stateColor = node.isCurrent ? "green" : "white";
|
|
90
|
+
const hasActiveRun = node.runs.some((r) => r.id === activeRunId);
|
|
91
|
+
const gutter = isLast && node.sideTrips.length === 0 ? " " : " \u2502 ";
|
|
92
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: stateColor, bold: node.isCurrent, children: [" ", marker, " "] }), _jsx(Text, { color: stateColor, bold: node.isCurrent, children: stateLabel }), _jsxs(Text, { dimColor: true, children: [" ", formatTime(node.enteredAt)] })] }), node.reason && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: gutter }), _jsx(Text, { dimColor: true, children: node.reason })] })), node.runs.length > MAX_VISIBLE_RUNS ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: gutter }), _jsx(RunSummary, { runs: node.runs })] }), node.runs.slice(0, 3).map((run, ri) => (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: gutter }), _jsx(RunLine, { run: run, index: runOffset + ri })] }, `run-${run.id}`))), _jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [gutter, " ... ", node.runs.length - 4, " more"] }) }), _jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: gutter }), _jsx(RunLine, { run: node.runs[node.runs.length - 1], index: runOffset + node.runs.length - 1 })] }, `run-${node.runs[node.runs.length - 1].id}`)] })) : (node.runs.map((run, ri) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: gutter }), _jsx(RunLine, { run: run, index: runOffset + ri })] }), run.id === activeRunId && plan && plan.length > 0 && (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: gutter }), _jsx(PlanSteps, { plan: plan })] }))] }, `run-${run.id}`)))), node.sideTrips.length > 0 && (_jsx(Box, { flexDirection: "column", children: node.sideTrips.map((trip, ti) => {
|
|
93
|
+
// Count runs before this side-trip for numbering
|
|
94
|
+
const priorSideTripRuns = node.sideTrips.slice(0, ti).reduce((acc, st) => acc + st.runs.length, 0);
|
|
95
|
+
const tripRunOffset = runOffset + node.runs.length + priorSideTripRuns;
|
|
96
|
+
return (_jsx(SideTripBlock, { trip: trip, runOffset: tripRunOffset, isLast: ti === node.sideTrips.length - 1 }, `trip-${ti}`));
|
|
97
|
+
}) })), !isLast && (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: " \u2502" }) }))] }));
|
|
98
|
+
}
|
|
99
|
+
// ─── Main component ──────────────────────────────────────────────
|
|
100
|
+
export function StateHistoryView({ history, plan, activeRunId }) {
|
|
101
|
+
if (history.length === 0) {
|
|
102
|
+
return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: "No state history available." }) }));
|
|
103
|
+
}
|
|
104
|
+
// Compute global run numbering (sequential across all nodes)
|
|
105
|
+
let runCounter = 0;
|
|
106
|
+
return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: history.map((node, i) => {
|
|
107
|
+
const offset = runCounter;
|
|
108
|
+
runCounter += node.runs.length + node.sideTrips.reduce((acc, st) => acc + st.runs.length, 0);
|
|
109
|
+
return (_jsx(MainPathNode, { node: node, isLast: i === history.length - 1, runOffset: offset, plan: plan, activeRunId: activeRunId }, `node-${i}`));
|
|
110
|
+
}) }));
|
|
111
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// ─── Constants ───────────────────────────────────────────────────
|
|
2
|
+
const SIDE_TRIP_STATES = new Set(["changes_requested", "repairing_ci", "repairing_queue"]);
|
|
3
|
+
const RUN_TYPE_TO_STATE = {
|
|
4
|
+
implementation: "implementing",
|
|
5
|
+
ci_repair: "repairing_ci",
|
|
6
|
+
review_fix: "changes_requested",
|
|
7
|
+
queue_repair: "repairing_queue",
|
|
8
|
+
};
|
|
9
|
+
function extractTransitions(feedEvents) {
|
|
10
|
+
const transitions = [];
|
|
11
|
+
for (const event of feedEvents) {
|
|
12
|
+
if (!event.stage)
|
|
13
|
+
continue;
|
|
14
|
+
let state;
|
|
15
|
+
let reason;
|
|
16
|
+
if (event.kind === "stage") {
|
|
17
|
+
if (event.status === "starting") {
|
|
18
|
+
state = RUN_TYPE_TO_STATE[event.stage] ?? event.stage;
|
|
19
|
+
reason = event.summary;
|
|
20
|
+
}
|
|
21
|
+
else if (event.status === "reconciled" || event.status === "retry" || event.status === "queued") {
|
|
22
|
+
state = event.stage;
|
|
23
|
+
reason = event.summary;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else if (event.kind === "github") {
|
|
27
|
+
state = event.stage;
|
|
28
|
+
reason = event.summary;
|
|
29
|
+
}
|
|
30
|
+
if (state) {
|
|
31
|
+
// Deduplicate consecutive identical states
|
|
32
|
+
if (transitions.length > 0 && transitions[transitions.length - 1].state === state) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
transitions.push({ state, at: event.at, reason });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return transitions;
|
|
39
|
+
}
|
|
40
|
+
// ─── Run helpers ─────────────────────────────────────────────────
|
|
41
|
+
function toRunInfo(run) {
|
|
42
|
+
return {
|
|
43
|
+
id: run.id,
|
|
44
|
+
runType: run.runType,
|
|
45
|
+
status: run.status,
|
|
46
|
+
startedAt: run.startedAt,
|
|
47
|
+
endedAt: run.endedAt,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function runToState(run) {
|
|
51
|
+
return RUN_TYPE_TO_STATE[run.runType] ?? run.runType;
|
|
52
|
+
}
|
|
53
|
+
// ─── Build from runs only (no feed events) ───────────────────────
|
|
54
|
+
function buildFromRuns(runs, currentFactoryState) {
|
|
55
|
+
if (runs.length === 0)
|
|
56
|
+
return [];
|
|
57
|
+
const nodes = [];
|
|
58
|
+
const earliest = runs[0];
|
|
59
|
+
// Seed with delegated
|
|
60
|
+
nodes.push({
|
|
61
|
+
state: "delegated",
|
|
62
|
+
enteredAt: earliest.startedAt,
|
|
63
|
+
isCurrent: false,
|
|
64
|
+
runs: [],
|
|
65
|
+
sideTrips: [],
|
|
66
|
+
});
|
|
67
|
+
// Group consecutive runs by their mapped state.
|
|
68
|
+
// When the state changes, create a new node.
|
|
69
|
+
let currentState = "";
|
|
70
|
+
let currentNode = null;
|
|
71
|
+
for (const run of runs) {
|
|
72
|
+
const state = runToState(run);
|
|
73
|
+
if (state !== currentState) {
|
|
74
|
+
currentState = state;
|
|
75
|
+
currentNode = {
|
|
76
|
+
state,
|
|
77
|
+
enteredAt: run.startedAt,
|
|
78
|
+
isCurrent: false,
|
|
79
|
+
runs: [toRunInfo(run)],
|
|
80
|
+
sideTrips: [],
|
|
81
|
+
};
|
|
82
|
+
nodes.push(currentNode);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
currentNode.runs.push(toRunInfo(run));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// If the current factory state differs from the last node's state,
|
|
89
|
+
// add a final node (e.g., implementing → failed)
|
|
90
|
+
const lastNodeState = nodes[nodes.length - 1].state;
|
|
91
|
+
if (currentFactoryState !== lastNodeState && currentFactoryState !== "delegated") {
|
|
92
|
+
const lastRun = runs[runs.length - 1];
|
|
93
|
+
nodes.push({
|
|
94
|
+
state: currentFactoryState,
|
|
95
|
+
enteredAt: lastRun.endedAt ?? lastRun.startedAt,
|
|
96
|
+
isCurrent: false,
|
|
97
|
+
runs: [],
|
|
98
|
+
sideTrips: [],
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return nodes;
|
|
102
|
+
}
|
|
103
|
+
// ─── Build from events + runs ────────────────────────────────────
|
|
104
|
+
function buildFromEvents(runs, transitions, currentFactoryState) {
|
|
105
|
+
// Build a chronological queue of runs per state
|
|
106
|
+
const runQueues = new Map();
|
|
107
|
+
for (const run of runs) {
|
|
108
|
+
const state = runToState(run);
|
|
109
|
+
const queue = runQueues.get(state);
|
|
110
|
+
if (queue) {
|
|
111
|
+
queue.push(toRunInfo(run));
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
runQueues.set(state, [toRunInfo(run)]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function consumeNextRun(state) {
|
|
118
|
+
const queue = runQueues.get(state);
|
|
119
|
+
if (!queue || queue.length === 0)
|
|
120
|
+
return [];
|
|
121
|
+
return [queue.shift()];
|
|
122
|
+
}
|
|
123
|
+
const nodes = [];
|
|
124
|
+
let currentMainNode = null;
|
|
125
|
+
let currentSideTrip = null;
|
|
126
|
+
for (const t of transitions) {
|
|
127
|
+
const isSideTrip = SIDE_TRIP_STATES.has(t.state);
|
|
128
|
+
if (isSideTrip) {
|
|
129
|
+
if (currentSideTrip) {
|
|
130
|
+
closeSideTrip(currentMainNode, currentSideTrip, t.state, t.at);
|
|
131
|
+
}
|
|
132
|
+
currentSideTrip = {
|
|
133
|
+
state: t.state,
|
|
134
|
+
enteredAt: t.at,
|
|
135
|
+
reason: t.reason,
|
|
136
|
+
returnState: "",
|
|
137
|
+
runs: [],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
if (currentSideTrip && currentMainNode) {
|
|
142
|
+
currentSideTrip.runs = consumeNextRun(currentSideTrip.state);
|
|
143
|
+
currentSideTrip.returnState = t.state;
|
|
144
|
+
currentSideTrip.returnedAt = t.at;
|
|
145
|
+
currentMainNode.sideTrips.push(currentSideTrip);
|
|
146
|
+
currentSideTrip = null;
|
|
147
|
+
}
|
|
148
|
+
// Skip duplicate when returning from a side-trip to the same state
|
|
149
|
+
if (currentMainNode && currentMainNode.state === t.state) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
currentMainNode = {
|
|
153
|
+
state: t.state,
|
|
154
|
+
enteredAt: t.at,
|
|
155
|
+
reason: t.reason,
|
|
156
|
+
isCurrent: false,
|
|
157
|
+
runs: consumeNextRun(t.state),
|
|
158
|
+
sideTrips: [],
|
|
159
|
+
};
|
|
160
|
+
nodes.push(currentMainNode);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Close any open side-trip
|
|
164
|
+
if (currentSideTrip && currentMainNode) {
|
|
165
|
+
currentSideTrip.runs = consumeNextRun(currentSideTrip.state);
|
|
166
|
+
currentSideTrip.returnState = currentFactoryState;
|
|
167
|
+
currentMainNode.sideTrips.push(currentSideTrip);
|
|
168
|
+
}
|
|
169
|
+
// Distribute remaining unconsumed runs to matching nodes
|
|
170
|
+
for (const [state, remaining] of runQueues) {
|
|
171
|
+
if (remaining.length === 0)
|
|
172
|
+
continue;
|
|
173
|
+
// Find the last node (or side-trip) matching this state
|
|
174
|
+
let target;
|
|
175
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
176
|
+
const node = nodes[i];
|
|
177
|
+
if (node.state === state) {
|
|
178
|
+
target = node;
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
for (let j = node.sideTrips.length - 1; j >= 0; j--) {
|
|
182
|
+
if (node.sideTrips[j].state === state) {
|
|
183
|
+
target = node.sideTrips[j];
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (target)
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
if (target) {
|
|
191
|
+
target.runs.push(...remaining);
|
|
192
|
+
}
|
|
193
|
+
remaining.length = 0;
|
|
194
|
+
}
|
|
195
|
+
return nodes;
|
|
196
|
+
}
|
|
197
|
+
// ─── Main entry point ────────────────────────────────────────────
|
|
198
|
+
export function buildStateHistory(runs, feedEvents, currentFactoryState, activeRunId) {
|
|
199
|
+
const transitions = extractTransitions(feedEvents);
|
|
200
|
+
const nodes = transitions.length > 0
|
|
201
|
+
? buildFromEvents(runs, transitions, currentFactoryState)
|
|
202
|
+
: buildFromRuns(runs, currentFactoryState);
|
|
203
|
+
if (nodes.length === 0)
|
|
204
|
+
return [];
|
|
205
|
+
markCurrent(nodes, currentFactoryState);
|
|
206
|
+
if (activeRunId !== null) {
|
|
207
|
+
markActiveRun(nodes, activeRunId);
|
|
208
|
+
}
|
|
209
|
+
return nodes;
|
|
210
|
+
}
|
|
211
|
+
// ─── Helpers ─────────────────────────────────────────────────────
|
|
212
|
+
function closeSideTrip(mainNode, sideTrip, returnState, returnedAt) {
|
|
213
|
+
if (!mainNode)
|
|
214
|
+
return;
|
|
215
|
+
sideTrip.returnState = returnState;
|
|
216
|
+
sideTrip.returnedAt = returnedAt;
|
|
217
|
+
mainNode.sideTrips.push(sideTrip);
|
|
218
|
+
}
|
|
219
|
+
function markCurrent(nodes, currentState) {
|
|
220
|
+
if (SIDE_TRIP_STATES.has(currentState)) {
|
|
221
|
+
if (nodes.length > 0) {
|
|
222
|
+
nodes[nodes.length - 1].isCurrent = true;
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
227
|
+
if (nodes[i].state === currentState) {
|
|
228
|
+
nodes[i].isCurrent = true;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (nodes.length > 0) {
|
|
233
|
+
nodes[nodes.length - 1].isCurrent = true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function markActiveRun(nodes, activeRunId) {
|
|
237
|
+
for (const node of nodes) {
|
|
238
|
+
for (const run of node.runs) {
|
|
239
|
+
if (run.id === activeRunId) {
|
|
240
|
+
run.status = "running";
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
for (const trip of node.sideTrips) {
|
|
245
|
+
for (const run of trip.runs) {
|
|
246
|
+
if (run.id === activeRunId) {
|
|
247
|
+
run.status = "running";
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function planStepSymbol(status) {
|
|
2
|
+
if (status === "completed")
|
|
3
|
+
return "\u2713";
|
|
4
|
+
if (status === "inProgress")
|
|
5
|
+
return "\u25b8";
|
|
6
|
+
return " ";
|
|
7
|
+
}
|
|
8
|
+
export function planStepColor(status) {
|
|
9
|
+
if (status === "completed")
|
|
10
|
+
return "green";
|
|
11
|
+
if (status === "inProgress")
|
|
12
|
+
return "yellow";
|
|
13
|
+
return "white";
|
|
14
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { buildTimelineFromRehydration, appendFeedToTimeline, appendCodexItemToTimeline, completeCodexItemInTimeline, appendDeltaToTimelineItem, } from "./timeline-builder.js";
|
|
2
2
|
const DETAIL_INITIAL = {
|
|
3
|
+
detailTab: "timeline",
|
|
3
4
|
timeline: [],
|
|
5
|
+
rawRuns: [],
|
|
6
|
+
rawFeedEvents: [],
|
|
4
7
|
activeRunId: null,
|
|
5
8
|
activeRunStartedAt: null,
|
|
6
9
|
tokenUsage: null,
|
|
@@ -96,6 +99,8 @@ export function watchReducer(state, action) {
|
|
|
96
99
|
return {
|
|
97
100
|
...state,
|
|
98
101
|
timeline,
|
|
102
|
+
rawRuns: action.runs,
|
|
103
|
+
rawFeedEvents: action.feedEvents,
|
|
99
104
|
activeRunId: action.activeRunId,
|
|
100
105
|
activeRunStartedAt: activeRun?.startedAt ?? null,
|
|
101
106
|
issueContext: action.issueContext,
|
|
@@ -115,6 +120,8 @@ export function watchReducer(state, action) {
|
|
|
115
120
|
return { ...state, feedEvents: action.events };
|
|
116
121
|
case "feed-new-event":
|
|
117
122
|
return { ...state, feedEvents: [...state.feedEvents, action.event] };
|
|
123
|
+
case "switch-detail-tab":
|
|
124
|
+
return { ...state, detailTab: action.tab };
|
|
118
125
|
}
|
|
119
126
|
}
|
|
120
127
|
// ─── Feed Event → Issue List + Timeline ───────────────────────────
|
|
@@ -147,11 +154,15 @@ function applyFeedEvent(state, event) {
|
|
|
147
154
|
}
|
|
148
155
|
issue.updatedAt = event.at;
|
|
149
156
|
updated[index] = issue;
|
|
150
|
-
// Append to timeline if this event matches the active detail issue
|
|
151
|
-
const
|
|
157
|
+
// Append to timeline and raw feed events if this event matches the active detail issue
|
|
158
|
+
const isActiveDetail = state.view === "detail" && state.activeDetailKey === event.issueKey;
|
|
159
|
+
const timeline = isActiveDetail
|
|
152
160
|
? appendFeedToTimeline(state.timeline, event)
|
|
153
161
|
: state.timeline;
|
|
154
|
-
|
|
162
|
+
const rawFeedEvents = isActiveDetail
|
|
163
|
+
? [...state.rawFeedEvents, event]
|
|
164
|
+
: state.rawFeedEvents;
|
|
165
|
+
return { ...state, issues: updated, timeline, rawFeedEvents };
|
|
155
166
|
}
|
|
156
167
|
// ─── Codex Notification → Timeline + Metadata ─────────────────────
|
|
157
168
|
function applyCodexNotification(state, method, params) {
|