patchrelay 0.16.0 → 0.17.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/build-info.json +3 -3
- package/dist/cli/watch/App.js +1 -1
- package/dist/cli/watch/IssueDetailView.js +28 -13
- package/dist/cli/watch/Timeline.js +14 -0
- package/dist/cli/watch/TimelineRow.js +62 -0
- package/dist/cli/watch/timeline-builder.js +363 -0
- package/dist/cli/watch/use-detail-stream.js +29 -107
- package/dist/cli/watch/watch-state.js +62 -193
- package/dist/http.js +8 -0
- package/dist/issue-query-service.js +23 -0
- package/dist/run-orchestrator.js +44 -0
- package/dist/service.js +3 -0
- package/package.json +1 -1
- package/dist/cli/watch/FeedTimeline.js +0 -23
- package/dist/cli/watch/ThreadView.js +0 -26
- package/dist/cli/watch/TurnSection.js +0 -20
package/dist/build-info.json
CHANGED
package/dist/cli/watch/App.js
CHANGED
|
@@ -64,5 +64,5 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
});
|
|
67
|
-
return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { issues: filtered, selectedIndex: state.selectedIndex, connected: state.connected, filter: state.filter, totalCount: state.issues.length })) : (_jsx(IssueDetailView, { issue: state.issues.find((i) => i.issueKey === state.activeDetailKey),
|
|
67
|
+
return (_jsx(Box, { flexDirection: "column", children: state.view === "list" ? (_jsx(IssueListView, { issues: filtered, selectedIndex: state.selectedIndex, connected: state.connected, filter: state.filter, totalCount: state.issues.length })) : (_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 })) }));
|
|
68
68
|
}
|
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useReducer } from "react";
|
|
2
3
|
import { Box, Text } from "ink";
|
|
3
|
-
import {
|
|
4
|
-
import { FeedTimeline } from "./FeedTimeline.js";
|
|
4
|
+
import { Timeline } from "./Timeline.js";
|
|
5
5
|
import { HelpBar } from "./HelpBar.js";
|
|
6
|
-
function truncate(text, max) {
|
|
7
|
-
const line = text.replace(/\n/g, " ").trim();
|
|
8
|
-
return line.length > max ? `${line.slice(0, max - 3)}...` : line;
|
|
9
|
-
}
|
|
10
6
|
function formatTokens(n) {
|
|
11
7
|
if (n >= 1_000_000)
|
|
12
8
|
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
@@ -14,16 +10,35 @@ function formatTokens(n) {
|
|
|
14
10
|
return `${(n / 1_000).toFixed(1)}k`;
|
|
15
11
|
return String(n);
|
|
16
12
|
}
|
|
17
|
-
function
|
|
18
|
-
|
|
13
|
+
function ElapsedTime({ startedAt }) {
|
|
14
|
+
const [, tick] = useReducer((c) => c + 1, 0);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const id = setInterval(tick, 1000);
|
|
17
|
+
return () => clearInterval(id);
|
|
18
|
+
}, []);
|
|
19
|
+
const elapsed = Math.max(0, Math.floor((Date.now() - new Date(startedAt).getTime()) / 1000));
|
|
20
|
+
const minutes = Math.floor(elapsed / 60);
|
|
21
|
+
const seconds = elapsed % 60;
|
|
22
|
+
return _jsxs(Text, { dimColor: true, children: [minutes, "m ", String(seconds).padStart(2, "0"), "s"] });
|
|
23
|
+
}
|
|
24
|
+
function planStepSymbol(status) {
|
|
25
|
+
if (status === "completed")
|
|
26
|
+
return "\u2713";
|
|
27
|
+
if (status === "inProgress")
|
|
28
|
+
return "\u25b8";
|
|
29
|
+
return " ";
|
|
19
30
|
}
|
|
20
|
-
function
|
|
21
|
-
|
|
31
|
+
function planStepColor(status) {
|
|
32
|
+
if (status === "completed")
|
|
33
|
+
return "green";
|
|
34
|
+
if (status === "inProgress")
|
|
35
|
+
return "yellow";
|
|
36
|
+
return "white";
|
|
22
37
|
}
|
|
23
|
-
export function IssueDetailView({ issue,
|
|
38
|
+
export function IssueDetailView({ issue, timeline, follow, activeRunStartedAt, tokenUsage, diffSummary, plan, }) {
|
|
24
39
|
if (!issue) {
|
|
25
40
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", children: "Issue not found." }), _jsx(HelpBar, { view: "detail", follow: follow })] }));
|
|
26
41
|
}
|
|
27
42
|
const key = issue.issueKey ?? issue.projectId;
|
|
28
|
-
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: ["PR #", issue.prNumber] })] }), issue.title && _jsx(Text, { dimColor: true, children: issue.title }),
|
|
43
|
+
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: ["PR #", issue.prNumber] }), activeRunStartedAt && _jsx(ElapsedTime, { startedAt: activeRunStartedAt })] }), issue.title && _jsx(Text, { dimColor: true, children: issue.title }), _jsxs(Box, { gap: 2, children: [tokenUsage && (_jsxs(Text, { dimColor: true, children: ["tokens: ", formatTokens(tokenUsage.inputTokens), " in / ", formatTokens(tokenUsage.outputTokens), " out"] })), diffSummary && diffSummary.filesChanged > 0 && (_jsxs(Text, { dimColor: true, children: ["diff: ", diffSummary.filesChanged, " file", diffSummary.filesChanged !== 1 ? "s" : "", " ", "+", diffSummary.linesAdded, " -", diffSummary.linesRemoved] })), follow && _jsx(Text, { color: "yellow", children: "follow" })] }), plan && plan.length > 0 && (_jsx(Box, { flexDirection: "column", 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(Text, { dimColor: true, children: "─".repeat(72) }), _jsx(Timeline, { entries: timeline, follow: follow }), _jsx(Text, { dimColor: true, children: "─".repeat(72) }), _jsx(HelpBar, { view: "detail", follow: follow })] }));
|
|
29
44
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { TimelineRow } from "./TimelineRow.js";
|
|
4
|
+
const FOLLOW_TAIL_SIZE = 20;
|
|
5
|
+
export function Timeline({ entries, follow }) {
|
|
6
|
+
const visible = follow && entries.length > FOLLOW_TAIL_SIZE
|
|
7
|
+
? entries.slice(-FOLLOW_TAIL_SIZE)
|
|
8
|
+
: entries;
|
|
9
|
+
const skipped = entries.length - visible.length;
|
|
10
|
+
if (entries.length === 0) {
|
|
11
|
+
return _jsx(Text, { dimColor: true, children: "No timeline events yet." });
|
|
12
|
+
}
|
|
13
|
+
return (_jsxs(Box, { flexDirection: "column", children: [skipped > 0 && _jsxs(Text, { dimColor: true, children: [" ... ", skipped, " earlier events"] }), visible.map((entry) => (_jsx(TimelineRow, { entry: entry }, entry.id)))] }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { ItemLine } from "./ItemLine.js";
|
|
4
|
+
function formatTime(iso) {
|
|
5
|
+
return new Date(iso).toLocaleTimeString("en-GB", { hour12: false });
|
|
6
|
+
}
|
|
7
|
+
function formatDuration(startedAt, endedAt) {
|
|
8
|
+
const ms = new Date(endedAt).getTime() - new Date(startedAt).getTime();
|
|
9
|
+
const seconds = Math.floor(ms / 1000);
|
|
10
|
+
if (seconds < 60)
|
|
11
|
+
return `${seconds}s`;
|
|
12
|
+
const minutes = Math.floor(seconds / 60);
|
|
13
|
+
const remainingSeconds = seconds % 60;
|
|
14
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
15
|
+
}
|
|
16
|
+
const CHECK_SYMBOLS = {
|
|
17
|
+
passed: "\u2713",
|
|
18
|
+
failed: "\u2717",
|
|
19
|
+
pending: "\u25cf",
|
|
20
|
+
};
|
|
21
|
+
const CHECK_COLORS = {
|
|
22
|
+
passed: "green",
|
|
23
|
+
failed: "red",
|
|
24
|
+
pending: "yellow",
|
|
25
|
+
};
|
|
26
|
+
function FeedRow({ entry }) {
|
|
27
|
+
const feed = entry.feed;
|
|
28
|
+
const statusLabel = feed.status ?? feed.feedKind;
|
|
29
|
+
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: formatTime(entry.at) }), _jsx(Text, { color: "cyan", children: statusLabel.padEnd(16) }), _jsx(Text, { children: feed.summary })] }));
|
|
30
|
+
}
|
|
31
|
+
function RunStartRow({ entry }) {
|
|
32
|
+
const run = entry.run;
|
|
33
|
+
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: formatTime(entry.at) }), _jsx(Text, { bold: true, color: "yellow", children: run.runType.padEnd(16) }), _jsx(Text, { bold: true, children: "run started" })] }));
|
|
34
|
+
}
|
|
35
|
+
function RunEndRow({ entry }) {
|
|
36
|
+
const run = entry.run;
|
|
37
|
+
const color = run.status === "completed" ? "green" : "red";
|
|
38
|
+
const duration = run.endedAt ? formatDuration(run.startedAt, run.endedAt) : "";
|
|
39
|
+
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: formatTime(entry.at) }), _jsx(Text, { bold: true, color: color, children: run.runType.padEnd(16) }), _jsx(Text, { bold: true, color: color, children: run.status }), duration ? _jsxs(Text, { dimColor: true, children: ["(", duration, ")"] }) : null] }));
|
|
40
|
+
}
|
|
41
|
+
function ItemRow({ entry }) {
|
|
42
|
+
const item = entry.item;
|
|
43
|
+
return (_jsx(Box, { paddingLeft: 2, children: _jsx(ItemLine, { item: item, isLast: false }) }));
|
|
44
|
+
}
|
|
45
|
+
function CIChecksRow({ entry }) {
|
|
46
|
+
const ci = entry.ciChecks;
|
|
47
|
+
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: formatTime(entry.at) }), _jsx(Text, { color: CHECK_COLORS[ci.overall] ?? "white", children: "ci_checks".padEnd(16) }), ci.checks.map((check, i) => (_jsxs(Text, { children: [_jsx(Text, { color: CHECK_COLORS[check.status] ?? "white", children: CHECK_SYMBOLS[check.status] ?? " " }), _jsxs(Text, { dimColor: true, children: [" ", check.name, " "] })] }, `check-${i}`)))] }));
|
|
48
|
+
}
|
|
49
|
+
export function TimelineRow({ entry }) {
|
|
50
|
+
switch (entry.kind) {
|
|
51
|
+
case "feed":
|
|
52
|
+
return _jsx(FeedRow, { entry: entry });
|
|
53
|
+
case "run-start":
|
|
54
|
+
return _jsx(RunStartRow, { entry: entry });
|
|
55
|
+
case "run-end":
|
|
56
|
+
return _jsx(RunEndRow, { entry: entry });
|
|
57
|
+
case "item":
|
|
58
|
+
return _jsx(ItemRow, { entry: entry });
|
|
59
|
+
case "ci-checks":
|
|
60
|
+
return _jsx(CIChecksRow, { entry: entry });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
// ─── Build Timeline from Rehydration Data ─────────────────────────
|
|
2
|
+
export function buildTimelineFromRehydration(runs, feedEvents, liveThread, activeRunId) {
|
|
3
|
+
const entries = [];
|
|
4
|
+
// 1. Add run boundaries and items from reports
|
|
5
|
+
for (const run of runs) {
|
|
6
|
+
entries.push({
|
|
7
|
+
id: `run-start-${run.id}`,
|
|
8
|
+
at: run.startedAt,
|
|
9
|
+
kind: "run-start",
|
|
10
|
+
runId: run.id,
|
|
11
|
+
run: { runType: run.runType, status: run.status, startedAt: run.startedAt, endedAt: run.endedAt },
|
|
12
|
+
});
|
|
13
|
+
if (run.endedAt) {
|
|
14
|
+
entries.push({
|
|
15
|
+
id: `run-end-${run.id}`,
|
|
16
|
+
at: run.endedAt,
|
|
17
|
+
kind: "run-end",
|
|
18
|
+
runId: run.id,
|
|
19
|
+
run: { runType: run.runType, status: run.status, startedAt: run.startedAt, endedAt: run.endedAt },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
// Items from completed run reports
|
|
23
|
+
if (run.report && run.id !== activeRunId) {
|
|
24
|
+
entries.push(...itemsFromReport(run.id, run.report, run.startedAt, run.endedAt));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// 2. Items from live thread (active run)
|
|
28
|
+
if (liveThread && activeRunId) {
|
|
29
|
+
entries.push(...itemsFromThread(activeRunId, liveThread));
|
|
30
|
+
}
|
|
31
|
+
// 3. Feed events → feed entries + CI check aggregation
|
|
32
|
+
entries.push(...feedEventsToEntries(feedEvents));
|
|
33
|
+
// 4. Sort by timestamp, then by entry order for stability
|
|
34
|
+
entries.sort((a, b) => {
|
|
35
|
+
const cmp = a.at.localeCompare(b.at);
|
|
36
|
+
if (cmp !== 0)
|
|
37
|
+
return cmp;
|
|
38
|
+
// Within same timestamp: run-start before items, items before run-end
|
|
39
|
+
return kindOrder(a.kind) - kindOrder(b.kind);
|
|
40
|
+
});
|
|
41
|
+
return entries;
|
|
42
|
+
}
|
|
43
|
+
function kindOrder(kind) {
|
|
44
|
+
switch (kind) {
|
|
45
|
+
case "run-start": return 0;
|
|
46
|
+
case "feed": return 1;
|
|
47
|
+
case "ci-checks": return 2;
|
|
48
|
+
case "item": return 3;
|
|
49
|
+
case "run-end": return 4;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// ─── Items from Report ────────────────────────────────────────────
|
|
53
|
+
function itemsFromReport(runId, report, startedAt, endedAt) {
|
|
54
|
+
const entries = [];
|
|
55
|
+
const start = new Date(startedAt).getTime();
|
|
56
|
+
const end = endedAt ? new Date(endedAt).getTime() : start + 60000;
|
|
57
|
+
let idx = 0;
|
|
58
|
+
const total = report.commands.length + report.assistantMessages.length + report.toolCalls.length;
|
|
59
|
+
for (const msg of report.assistantMessages) {
|
|
60
|
+
entries.push({
|
|
61
|
+
id: `report-${runId}-msg-${idx}`,
|
|
62
|
+
at: syntheticTimestamp(start, end, idx, total),
|
|
63
|
+
kind: "item",
|
|
64
|
+
runId,
|
|
65
|
+
item: { id: `report-${runId}-msg-${idx}`, type: "agentMessage", status: "completed", text: msg },
|
|
66
|
+
});
|
|
67
|
+
idx++;
|
|
68
|
+
}
|
|
69
|
+
for (const cmd of report.commands) {
|
|
70
|
+
entries.push({
|
|
71
|
+
id: `report-${runId}-cmd-${idx}`,
|
|
72
|
+
at: syntheticTimestamp(start, end, idx, total),
|
|
73
|
+
kind: "item",
|
|
74
|
+
runId,
|
|
75
|
+
item: {
|
|
76
|
+
id: `report-${runId}-cmd-${idx}`,
|
|
77
|
+
type: "commandExecution",
|
|
78
|
+
status: "completed",
|
|
79
|
+
command: cmd.command,
|
|
80
|
+
...(typeof cmd.exitCode === "number" ? { exitCode: cmd.exitCode } : {}),
|
|
81
|
+
...(typeof cmd.durationMs === "number" ? { durationMs: cmd.durationMs } : {}),
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
idx++;
|
|
85
|
+
}
|
|
86
|
+
for (const tool of report.toolCalls) {
|
|
87
|
+
entries.push({
|
|
88
|
+
id: `report-${runId}-tool-${idx}`,
|
|
89
|
+
at: syntheticTimestamp(start, end, idx, total),
|
|
90
|
+
kind: "item",
|
|
91
|
+
runId,
|
|
92
|
+
item: {
|
|
93
|
+
id: `report-${runId}-tool-${idx}`,
|
|
94
|
+
type: tool.type === "mcp" ? "mcpToolCall" : "dynamicToolCall",
|
|
95
|
+
status: "completed",
|
|
96
|
+
toolName: tool.name,
|
|
97
|
+
...(typeof tool.durationMs === "number" ? { durationMs: tool.durationMs } : {}),
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
idx++;
|
|
101
|
+
}
|
|
102
|
+
if (report.fileChanges.length > 0) {
|
|
103
|
+
entries.push({
|
|
104
|
+
id: `report-${runId}-files`,
|
|
105
|
+
at: syntheticTimestamp(start, end, idx, total),
|
|
106
|
+
kind: "item",
|
|
107
|
+
runId,
|
|
108
|
+
item: {
|
|
109
|
+
id: `report-${runId}-files`,
|
|
110
|
+
type: "fileChange",
|
|
111
|
+
status: "completed",
|
|
112
|
+
changes: report.fileChanges,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return entries;
|
|
117
|
+
}
|
|
118
|
+
function syntheticTimestamp(startMs, endMs, index, total) {
|
|
119
|
+
if (total <= 1)
|
|
120
|
+
return new Date(startMs).toISOString();
|
|
121
|
+
const fraction = index / (total - 1);
|
|
122
|
+
return new Date(startMs + fraction * (endMs - startMs)).toISOString();
|
|
123
|
+
}
|
|
124
|
+
// ─── Items from Live Thread ───────────────────────────────────────
|
|
125
|
+
function itemsFromThread(runId, thread) {
|
|
126
|
+
const entries = [];
|
|
127
|
+
for (const turn of thread.turns) {
|
|
128
|
+
for (const item of turn.items) {
|
|
129
|
+
entries.push({
|
|
130
|
+
id: `live-${item.id}`,
|
|
131
|
+
at: new Date().toISOString(), // live items don't have timestamps; they'll sort to the end
|
|
132
|
+
kind: "item",
|
|
133
|
+
runId,
|
|
134
|
+
item: materializeItem(item),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return entries;
|
|
139
|
+
}
|
|
140
|
+
function materializeItem(item) {
|
|
141
|
+
const r = item;
|
|
142
|
+
const id = String(r.id ?? "unknown");
|
|
143
|
+
const type = String(r.type ?? "unknown");
|
|
144
|
+
const base = { id, type, status: "completed" };
|
|
145
|
+
switch (type) {
|
|
146
|
+
case "agentMessage":
|
|
147
|
+
return { ...base, text: String(r.text ?? "") };
|
|
148
|
+
case "commandExecution":
|
|
149
|
+
return {
|
|
150
|
+
...base,
|
|
151
|
+
command: String(r.command ?? ""),
|
|
152
|
+
status: String(r.status ?? "completed"),
|
|
153
|
+
...(typeof r.exitCode === "number" ? { exitCode: r.exitCode } : {}),
|
|
154
|
+
...(typeof r.durationMs === "number" ? { durationMs: r.durationMs } : {}),
|
|
155
|
+
...(typeof r.aggregatedOutput === "string" ? { output: r.aggregatedOutput } : {}),
|
|
156
|
+
};
|
|
157
|
+
case "fileChange":
|
|
158
|
+
return { ...base, status: String(r.status ?? "completed"), changes: Array.isArray(r.changes) ? r.changes : [] };
|
|
159
|
+
case "mcpToolCall":
|
|
160
|
+
return {
|
|
161
|
+
...base,
|
|
162
|
+
status: String(r.status ?? "completed"),
|
|
163
|
+
toolName: `${String(r.server ?? "")}/${String(r.tool ?? "")}`,
|
|
164
|
+
...(typeof r.durationMs === "number" ? { durationMs: r.durationMs } : {}),
|
|
165
|
+
};
|
|
166
|
+
case "dynamicToolCall":
|
|
167
|
+
return {
|
|
168
|
+
...base,
|
|
169
|
+
status: String(r.status ?? "completed"),
|
|
170
|
+
toolName: String(r.tool ?? ""),
|
|
171
|
+
...(typeof r.durationMs === "number" ? { durationMs: r.durationMs } : {}),
|
|
172
|
+
};
|
|
173
|
+
case "plan":
|
|
174
|
+
return { ...base, text: String(r.text ?? "") };
|
|
175
|
+
case "reasoning":
|
|
176
|
+
return { ...base, text: Array.isArray(r.summary) ? r.summary.join("\n") : "" };
|
|
177
|
+
default:
|
|
178
|
+
return base;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// ─── Feed Events to Timeline Entries ──────────────────────────────
|
|
182
|
+
function feedEventsToEntries(feedEvents) {
|
|
183
|
+
const entries = [];
|
|
184
|
+
const ciAggregator = new CICheckAggregator();
|
|
185
|
+
for (const event of feedEvents) {
|
|
186
|
+
// GitHub check events get aggregated
|
|
187
|
+
if (event.kind === "github" && (event.status === "check_passed" || event.status === "check_failed") && event.detail) {
|
|
188
|
+
const ciEntry = ciAggregator.add(event);
|
|
189
|
+
if (ciEntry) {
|
|
190
|
+
// Replace the last ci-checks entry if it was updated
|
|
191
|
+
const lastIdx = entries.findLastIndex((e) => e.kind === "ci-checks" && e.id === ciEntry.id);
|
|
192
|
+
if (lastIdx >= 0) {
|
|
193
|
+
entries[lastIdx] = ciEntry;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
entries.push(ciEntry);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
entries.push({
|
|
202
|
+
id: `feed-${event.id}`,
|
|
203
|
+
at: event.at,
|
|
204
|
+
kind: "feed",
|
|
205
|
+
feed: {
|
|
206
|
+
feedKind: event.kind,
|
|
207
|
+
...(event.status ? { status: event.status } : {}),
|
|
208
|
+
summary: event.summary,
|
|
209
|
+
...(event.detail ? { detail: event.detail } : {}),
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return entries;
|
|
214
|
+
}
|
|
215
|
+
// ─── CI Check Aggregation ─────────────────────────────────────────
|
|
216
|
+
const CI_CHECK_WINDOW_MS = 60_000;
|
|
217
|
+
class CICheckAggregator {
|
|
218
|
+
currentGroup = null;
|
|
219
|
+
groupCounter = 0;
|
|
220
|
+
add(event) {
|
|
221
|
+
const name = event.detail ?? "unknown";
|
|
222
|
+
const status = event.status === "check_passed" ? "passed" : "failed";
|
|
223
|
+
const eventMs = new Date(event.at).getTime();
|
|
224
|
+
if (this.currentGroup && eventMs - this.currentGroup.windowStart < CI_CHECK_WINDOW_MS) {
|
|
225
|
+
this.currentGroup.checks.set(name, status);
|
|
226
|
+
return this.toEntry();
|
|
227
|
+
}
|
|
228
|
+
this.groupCounter++;
|
|
229
|
+
this.currentGroup = {
|
|
230
|
+
id: `ci-checks-${this.groupCounter}`,
|
|
231
|
+
at: event.at,
|
|
232
|
+
checks: new Map([[name, status]]),
|
|
233
|
+
windowStart: eventMs,
|
|
234
|
+
};
|
|
235
|
+
return this.toEntry();
|
|
236
|
+
}
|
|
237
|
+
toEntry() {
|
|
238
|
+
const group = this.currentGroup;
|
|
239
|
+
const checks = [...group.checks.entries()].map(([name, status]) => ({ name, status }));
|
|
240
|
+
const overall = checks.every((c) => c.status === "passed") ? "passed"
|
|
241
|
+
: checks.some((c) => c.status === "failed") ? "failed"
|
|
242
|
+
: "pending";
|
|
243
|
+
return {
|
|
244
|
+
id: group.id,
|
|
245
|
+
at: group.at,
|
|
246
|
+
kind: "ci-checks",
|
|
247
|
+
ciChecks: { checks, overall },
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// ─── Live Append Helpers ──────────────────────────────────────────
|
|
252
|
+
export function appendFeedToTimeline(timeline, event) {
|
|
253
|
+
// GitHub check events: aggregate into existing ci-checks entry
|
|
254
|
+
if (event.kind === "github" && (event.status === "check_passed" || event.status === "check_failed") && event.detail) {
|
|
255
|
+
return aggregateCICheckIntoTimeline(timeline, event);
|
|
256
|
+
}
|
|
257
|
+
return [...timeline, {
|
|
258
|
+
id: `feed-${event.id}`,
|
|
259
|
+
at: event.at,
|
|
260
|
+
kind: "feed",
|
|
261
|
+
feed: {
|
|
262
|
+
feedKind: event.kind,
|
|
263
|
+
...(event.status ? { status: event.status } : {}),
|
|
264
|
+
summary: event.summary,
|
|
265
|
+
...(event.detail ? { detail: event.detail } : {}),
|
|
266
|
+
},
|
|
267
|
+
}];
|
|
268
|
+
}
|
|
269
|
+
function aggregateCICheckIntoTimeline(timeline, event) {
|
|
270
|
+
const name = event.detail ?? "unknown";
|
|
271
|
+
const status = event.status === "check_passed" ? "passed" : "failed";
|
|
272
|
+
const eventMs = new Date(event.at).getTime();
|
|
273
|
+
// Find the most recent ci-checks entry within the window
|
|
274
|
+
for (let i = timeline.length - 1; i >= 0; i--) {
|
|
275
|
+
const entry = timeline[i];
|
|
276
|
+
if (entry.kind === "ci-checks" && entry.ciChecks) {
|
|
277
|
+
const entryMs = new Date(entry.at).getTime();
|
|
278
|
+
if (eventMs - entryMs < CI_CHECK_WINDOW_MS) {
|
|
279
|
+
const updatedChecks = [...entry.ciChecks.checks.filter((c) => c.name !== name), { name, status }];
|
|
280
|
+
const overall = updatedChecks.every((c) => c.status === "passed") ? "passed"
|
|
281
|
+
: updatedChecks.some((c) => c.status === "failed") ? "failed"
|
|
282
|
+
: "pending";
|
|
283
|
+
const updated = [...timeline];
|
|
284
|
+
updated[i] = { ...entry, ciChecks: { checks: updatedChecks, overall } };
|
|
285
|
+
return updated;
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// No recent ci-checks entry; create new one
|
|
291
|
+
return [...timeline, {
|
|
292
|
+
id: `ci-checks-live-${event.id}`,
|
|
293
|
+
at: event.at,
|
|
294
|
+
kind: "ci-checks",
|
|
295
|
+
ciChecks: { checks: [{ name, status }], overall: status },
|
|
296
|
+
}];
|
|
297
|
+
}
|
|
298
|
+
export function appendCodexItemToTimeline(timeline, params, activeRunId) {
|
|
299
|
+
const itemObj = params.item;
|
|
300
|
+
if (!itemObj)
|
|
301
|
+
return timeline;
|
|
302
|
+
const id = typeof itemObj.id === "string" ? itemObj.id : "unknown";
|
|
303
|
+
const type = typeof itemObj.type === "string" ? itemObj.type : "unknown";
|
|
304
|
+
const status = typeof itemObj.status === "string" ? itemObj.status : "inProgress";
|
|
305
|
+
const item = { id, type, status };
|
|
306
|
+
if (type === "agentMessage" && typeof itemObj.text === "string")
|
|
307
|
+
item.text = itemObj.text;
|
|
308
|
+
if (type === "commandExecution") {
|
|
309
|
+
const cmd = itemObj.command;
|
|
310
|
+
item.command = Array.isArray(cmd) ? cmd.join(" ") : typeof cmd === "string" ? cmd : undefined;
|
|
311
|
+
}
|
|
312
|
+
if (type === "mcpToolCall") {
|
|
313
|
+
item.toolName = `${String(itemObj.server ?? "")}/${String(itemObj.tool ?? "")}`;
|
|
314
|
+
}
|
|
315
|
+
if (type === "dynamicToolCall") {
|
|
316
|
+
item.toolName = typeof itemObj.tool === "string" ? itemObj.tool : undefined;
|
|
317
|
+
}
|
|
318
|
+
return [...timeline, {
|
|
319
|
+
id: `live-${id}`,
|
|
320
|
+
at: new Date().toISOString(),
|
|
321
|
+
kind: "item",
|
|
322
|
+
runId: activeRunId ?? undefined,
|
|
323
|
+
item,
|
|
324
|
+
}];
|
|
325
|
+
}
|
|
326
|
+
export function completeCodexItemInTimeline(timeline, params) {
|
|
327
|
+
const itemObj = params.item;
|
|
328
|
+
if (!itemObj)
|
|
329
|
+
return timeline;
|
|
330
|
+
const id = typeof itemObj.id === "string" ? itemObj.id : undefined;
|
|
331
|
+
if (!id)
|
|
332
|
+
return timeline;
|
|
333
|
+
const status = typeof itemObj.status === "string" ? itemObj.status : "completed";
|
|
334
|
+
const exitCode = typeof itemObj.exitCode === "number" ? itemObj.exitCode : undefined;
|
|
335
|
+
const durationMs = typeof itemObj.durationMs === "number" ? itemObj.durationMs : undefined;
|
|
336
|
+
const text = typeof itemObj.text === "string" ? itemObj.text : undefined;
|
|
337
|
+
const changes = Array.isArray(itemObj.changes) ? itemObj.changes : undefined;
|
|
338
|
+
return timeline.map((entry) => {
|
|
339
|
+
if (entry.kind !== "item" || entry.item?.id !== id)
|
|
340
|
+
return entry;
|
|
341
|
+
return {
|
|
342
|
+
...entry,
|
|
343
|
+
item: {
|
|
344
|
+
...entry.item,
|
|
345
|
+
status,
|
|
346
|
+
...(exitCode !== undefined ? { exitCode } : {}),
|
|
347
|
+
...(durationMs !== undefined ? { durationMs } : {}),
|
|
348
|
+
...(text !== undefined ? { text } : {}),
|
|
349
|
+
...(changes !== undefined ? { changes } : {}),
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
export function appendDeltaToTimelineItem(timeline, itemId, field, delta) {
|
|
355
|
+
return timeline.map((entry) => {
|
|
356
|
+
if (entry.kind !== "item" || entry.item?.id !== itemId)
|
|
357
|
+
return entry;
|
|
358
|
+
return {
|
|
359
|
+
...entry,
|
|
360
|
+
item: { ...entry.item, [field]: (entry.item[field] ?? "") + delta },
|
|
361
|
+
};
|
|
362
|
+
});
|
|
363
|
+
}
|
|
@@ -8,70 +8,50 @@ export function useDetailStream(options) {
|
|
|
8
8
|
return;
|
|
9
9
|
const abortController = new AbortController();
|
|
10
10
|
const { baseUrl, bearerToken, dispatch } = optionsRef.current;
|
|
11
|
-
const headers = {
|
|
11
|
+
const headers = {};
|
|
12
12
|
if (bearerToken) {
|
|
13
13
|
headers.authorization = `Bearer ${bearerToken}`;
|
|
14
14
|
}
|
|
15
|
-
// Rehydrate from
|
|
15
|
+
// Rehydrate from timeline endpoint
|
|
16
16
|
void rehydrate(baseUrl, issueKey, headers, abortController.signal, dispatch);
|
|
17
|
-
// Stream codex notifications via filtered SSE
|
|
18
|
-
void
|
|
17
|
+
// Stream codex notifications + feed events via filtered SSE
|
|
18
|
+
void streamEvents(baseUrl, issueKey, headers, abortController.signal, dispatch);
|
|
19
19
|
return () => {
|
|
20
20
|
abortController.abort();
|
|
21
21
|
};
|
|
22
22
|
}, [options.issueKey]);
|
|
23
23
|
}
|
|
24
|
+
// ─── Rehydration ──────────────────────────────────────────────────
|
|
24
25
|
async function rehydrate(baseUrl, issueKey, headers, signal, dispatch) {
|
|
25
26
|
try {
|
|
26
|
-
const url = new URL(`/api/issues/${encodeURIComponent(issueKey)}/
|
|
27
|
-
const response = await fetch(url, { headers, signal });
|
|
28
|
-
if (!response.ok)
|
|
29
|
-
return;
|
|
30
|
-
const data = await response.json();
|
|
31
|
-
const threadData = data.thread;
|
|
32
|
-
if (threadData) {
|
|
33
|
-
dispatch({ type: "thread-snapshot", thread: materializeThread(threadData) });
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
// No active thread — fall back to latest run report
|
|
37
|
-
await rehydrateFromReport(baseUrl, issueKey, headers, signal, dispatch);
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
// Rehydration is best-effort — SSE stream will provide updates
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
async function rehydrateFromReport(baseUrl, issueKey, headers, signal, dispatch) {
|
|
44
|
-
try {
|
|
45
|
-
const url = new URL(`/api/issues/${encodeURIComponent(issueKey)}/report`, baseUrl);
|
|
46
|
-
const response = await fetch(url, { headers, signal });
|
|
27
|
+
const url = new URL(`/api/issues/${encodeURIComponent(issueKey)}/timeline`, baseUrl);
|
|
28
|
+
const response = await fetch(url, { headers: { ...headers, accept: "application/json" }, signal });
|
|
47
29
|
if (!response.ok)
|
|
48
30
|
return;
|
|
49
31
|
const data = await response.json();
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
assistantMessages: latest.report?.assistantMessages ?? [],
|
|
67
|
-
};
|
|
68
|
-
dispatch({ type: "report-snapshot", report });
|
|
32
|
+
const runs = (data.runs ?? []).map((r) => ({
|
|
33
|
+
id: r.id,
|
|
34
|
+
runType: r.runType,
|
|
35
|
+
status: r.status,
|
|
36
|
+
startedAt: r.startedAt,
|
|
37
|
+
endedAt: r.endedAt,
|
|
38
|
+
threadId: r.threadId,
|
|
39
|
+
...(r.report ? { report: r.report } : {}),
|
|
40
|
+
}));
|
|
41
|
+
dispatch({
|
|
42
|
+
type: "timeline-rehydrate",
|
|
43
|
+
runs,
|
|
44
|
+
feedEvents: data.feedEvents ?? [],
|
|
45
|
+
liveThread: data.liveThread ?? null,
|
|
46
|
+
activeRunId: data.activeRunId ?? null,
|
|
47
|
+
});
|
|
69
48
|
}
|
|
70
49
|
catch {
|
|
71
|
-
//
|
|
50
|
+
// Rehydration is best-effort
|
|
72
51
|
}
|
|
73
52
|
}
|
|
74
|
-
|
|
53
|
+
// ─── Live SSE Stream ──────────────────────────────────────────────
|
|
54
|
+
async function streamEvents(baseUrl, issueKey, baseHeaders, signal, dispatch) {
|
|
75
55
|
try {
|
|
76
56
|
const url = new URL("/api/watch", baseUrl);
|
|
77
57
|
url.searchParams.set("issue", issueKey);
|
|
@@ -96,7 +76,7 @@ async function streamCodexEvents(baseUrl, issueKey, baseHeaders, signal, dispatc
|
|
|
96
76
|
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
97
77
|
if (!line) {
|
|
98
78
|
if (dataLines.length > 0) {
|
|
99
|
-
|
|
79
|
+
processEvent(dispatch, eventType, dataLines.join("\n"));
|
|
100
80
|
dataLines = [];
|
|
101
81
|
eventType = "";
|
|
102
82
|
}
|
|
@@ -121,73 +101,15 @@ async function streamCodexEvents(baseUrl, issueKey, baseHeaders, signal, dispatc
|
|
|
121
101
|
// Stream ended or aborted
|
|
122
102
|
}
|
|
123
103
|
}
|
|
124
|
-
function
|
|
104
|
+
function processEvent(dispatch, eventType, data) {
|
|
125
105
|
try {
|
|
126
106
|
if (eventType === "codex") {
|
|
127
107
|
const parsed = JSON.parse(data);
|
|
128
108
|
dispatch({ type: "codex-notification", method: parsed.method, params: parsed.params });
|
|
129
109
|
}
|
|
130
|
-
// Feed events are
|
|
110
|
+
// Feed events are handled by the main watch stream
|
|
131
111
|
}
|
|
132
112
|
catch {
|
|
133
113
|
// Ignore parse errors
|
|
134
114
|
}
|
|
135
115
|
}
|
|
136
|
-
// ─── Thread Materialization from thread/read ──────────────────────
|
|
137
|
-
function materializeThread(summary) {
|
|
138
|
-
return {
|
|
139
|
-
threadId: summary.id,
|
|
140
|
-
status: summary.status,
|
|
141
|
-
turns: summary.turns.map(materializeTurn),
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
function materializeTurn(turn) {
|
|
145
|
-
return {
|
|
146
|
-
id: turn.id,
|
|
147
|
-
status: turn.status,
|
|
148
|
-
items: turn.items.map(materializeItem),
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
function materializeItem(item) {
|
|
152
|
-
// CodexThreadItem has an index-signature catch-all that defeats narrowing.
|
|
153
|
-
// Access fields via Record<string, unknown> and coerce explicitly.
|
|
154
|
-
const r = item;
|
|
155
|
-
const id = String(r.id ?? "unknown");
|
|
156
|
-
const type = String(r.type ?? "unknown");
|
|
157
|
-
const base = { id, type, status: "completed" };
|
|
158
|
-
switch (type) {
|
|
159
|
-
case "agentMessage":
|
|
160
|
-
return { ...base, text: String(r.text ?? "") };
|
|
161
|
-
case "commandExecution":
|
|
162
|
-
return {
|
|
163
|
-
...base,
|
|
164
|
-
command: String(r.command ?? ""),
|
|
165
|
-
status: String(r.status ?? "completed"),
|
|
166
|
-
...(typeof r.exitCode === "number" ? { exitCode: r.exitCode } : {}),
|
|
167
|
-
...(typeof r.durationMs === "number" ? { durationMs: r.durationMs } : {}),
|
|
168
|
-
...(typeof r.aggregatedOutput === "string" ? { output: r.aggregatedOutput } : {}),
|
|
169
|
-
};
|
|
170
|
-
case "fileChange":
|
|
171
|
-
return { ...base, status: String(r.status ?? "completed"), changes: Array.isArray(r.changes) ? r.changes : [] };
|
|
172
|
-
case "mcpToolCall":
|
|
173
|
-
return {
|
|
174
|
-
...base,
|
|
175
|
-
status: String(r.status ?? "completed"),
|
|
176
|
-
toolName: `${String(r.server ?? "")}/${String(r.tool ?? "")}`,
|
|
177
|
-
...(typeof r.durationMs === "number" ? { durationMs: r.durationMs } : {}),
|
|
178
|
-
};
|
|
179
|
-
case "dynamicToolCall":
|
|
180
|
-
return {
|
|
181
|
-
...base,
|
|
182
|
-
status: String(r.status ?? "completed"),
|
|
183
|
-
toolName: String(r.tool ?? ""),
|
|
184
|
-
...(typeof r.durationMs === "number" ? { durationMs: r.durationMs } : {}),
|
|
185
|
-
};
|
|
186
|
-
case "plan":
|
|
187
|
-
return { ...base, text: String(r.text ?? "") };
|
|
188
|
-
case "reasoning":
|
|
189
|
-
return { ...base, text: Array.isArray(r.summary) ? r.summary.join("\n") : "" };
|
|
190
|
-
default:
|
|
191
|
-
return base;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
import { buildTimelineFromRehydration, appendFeedToTimeline, appendCodexItemToTimeline, completeCodexItemInTimeline, appendDeltaToTimelineItem, } from "./timeline-builder.js";
|
|
2
|
+
const DETAIL_INITIAL = {
|
|
3
|
+
timeline: [],
|
|
4
|
+
activeRunId: null,
|
|
5
|
+
activeRunStartedAt: null,
|
|
6
|
+
tokenUsage: null,
|
|
7
|
+
diffSummary: null,
|
|
8
|
+
plan: null,
|
|
9
|
+
};
|
|
1
10
|
export const initialWatchState = {
|
|
2
11
|
connected: false,
|
|
3
12
|
issues: [],
|
|
4
13
|
selectedIndex: 0,
|
|
5
14
|
view: "list",
|
|
6
15
|
activeDetailKey: null,
|
|
7
|
-
thread: null,
|
|
8
|
-
report: null,
|
|
9
16
|
filter: "non-done",
|
|
10
17
|
follow: true,
|
|
11
|
-
|
|
18
|
+
...DETAIL_INITIAL,
|
|
12
19
|
};
|
|
13
20
|
const TERMINAL_FACTORY_STATES = new Set(["done", "failed"]);
|
|
14
21
|
export function filterIssues(issues, filter) {
|
|
@@ -28,6 +35,7 @@ function nextFilter(filter) {
|
|
|
28
35
|
case "all": return "non-done";
|
|
29
36
|
}
|
|
30
37
|
}
|
|
38
|
+
// ─── Reducer ──────────────────────────────────────────────────────
|
|
31
39
|
export function watchReducer(state, action) {
|
|
32
40
|
switch (action.type) {
|
|
33
41
|
case "connected":
|
|
@@ -48,13 +56,19 @@ export function watchReducer(state, action) {
|
|
|
48
56
|
selectedIndex: Math.max(0, Math.min(action.index, state.issues.length - 1)),
|
|
49
57
|
};
|
|
50
58
|
case "enter-detail":
|
|
51
|
-
return { ...state, view: "detail", activeDetailKey: action.issueKey,
|
|
59
|
+
return { ...state, view: "detail", activeDetailKey: action.issueKey, ...DETAIL_INITIAL };
|
|
52
60
|
case "exit-detail":
|
|
53
|
-
return { ...state, view: "list", activeDetailKey: null,
|
|
54
|
-
case "
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return {
|
|
61
|
+
return { ...state, view: "list", activeDetailKey: null, ...DETAIL_INITIAL };
|
|
62
|
+
case "timeline-rehydrate": {
|
|
63
|
+
const timeline = buildTimelineFromRehydration(action.runs, action.feedEvents, action.liveThread, action.activeRunId);
|
|
64
|
+
const activeRun = action.runs.find((r) => r.id === action.activeRunId);
|
|
65
|
+
return {
|
|
66
|
+
...state,
|
|
67
|
+
timeline,
|
|
68
|
+
activeRunId: action.activeRunId,
|
|
69
|
+
activeRunStartedAt: activeRun?.startedAt ?? null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
58
72
|
case "codex-notification":
|
|
59
73
|
return applyCodexNotification(state, action.method, action.params);
|
|
60
74
|
case "cycle-filter":
|
|
@@ -63,7 +77,7 @@ export function watchReducer(state, action) {
|
|
|
63
77
|
return { ...state, follow: !state.follow };
|
|
64
78
|
}
|
|
65
79
|
}
|
|
66
|
-
// ─── Feed Event
|
|
80
|
+
// ─── Feed Event → Issue List + Timeline ───────────────────────────
|
|
67
81
|
function applyFeedEvent(state, event) {
|
|
68
82
|
if (!event.issueKey) {
|
|
69
83
|
return state;
|
|
@@ -93,103 +107,52 @@ function applyFeedEvent(state, event) {
|
|
|
93
107
|
}
|
|
94
108
|
issue.updatedAt = event.at;
|
|
95
109
|
updated[index] = issue;
|
|
96
|
-
// Append to
|
|
97
|
-
const
|
|
98
|
-
?
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
summary: event.summary,
|
|
102
|
-
...(event.status ? { status: event.status } : {}),
|
|
103
|
-
...(event.detail ? { detail: event.detail } : {}),
|
|
104
|
-
}]
|
|
105
|
-
: state.detailFeed;
|
|
106
|
-
return { ...state, issues: updated, detailFeed };
|
|
110
|
+
// Append to timeline if this event matches the active detail issue
|
|
111
|
+
const timeline = state.view === "detail" && state.activeDetailKey === event.issueKey
|
|
112
|
+
? appendFeedToTimeline(state.timeline, event)
|
|
113
|
+
: state.timeline;
|
|
114
|
+
return { ...state, issues: updated, timeline };
|
|
107
115
|
}
|
|
108
|
-
// ─── Codex Notification
|
|
116
|
+
// ─── Codex Notification → Timeline + Metadata ─────────────────────
|
|
109
117
|
function applyCodexNotification(state, method, params) {
|
|
110
|
-
if (!state.thread) {
|
|
111
|
-
// No thread loaded yet — only turn/started can bootstrap one
|
|
112
|
-
if (method === "turn/started") {
|
|
113
|
-
return bootstrapThreadFromTurnStarted(state, params);
|
|
114
|
-
}
|
|
115
|
-
return state;
|
|
116
|
-
}
|
|
117
118
|
switch (method) {
|
|
118
|
-
case "turn/started":
|
|
119
|
-
return withThread(state, addTurn(state.thread, params));
|
|
120
|
-
case "turn/completed":
|
|
121
|
-
return withThread(state, completeTurn(state.thread, params));
|
|
122
|
-
case "turn/plan/updated":
|
|
123
|
-
return withThread(state, updatePlan(state.thread, params));
|
|
124
|
-
case "turn/diff/updated":
|
|
125
|
-
return withThread(state, updateDiff(state.thread, params));
|
|
126
119
|
case "item/started":
|
|
127
|
-
return
|
|
120
|
+
return { ...state, timeline: appendCodexItemToTimeline(state.timeline, params, state.activeRunId) };
|
|
128
121
|
case "item/completed":
|
|
129
|
-
return
|
|
122
|
+
return { ...state, timeline: completeCodexItemInTimeline(state.timeline, params) };
|
|
130
123
|
case "item/agentMessage/delta":
|
|
131
|
-
return withThread(state, appendItemText(state.thread, params));
|
|
132
|
-
case "item/commandExecution/outputDelta":
|
|
133
|
-
return withThread(state, appendItemOutput(state.thread, params));
|
|
134
124
|
case "item/plan/delta":
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
125
|
+
case "item/reasoning/summaryTextDelta": {
|
|
126
|
+
const itemId = typeof params.itemId === "string" ? params.itemId : undefined;
|
|
127
|
+
const delta = typeof params.delta === "string" ? params.delta : undefined;
|
|
128
|
+
if (!itemId || !delta)
|
|
129
|
+
return state;
|
|
130
|
+
return { ...state, timeline: appendDeltaToTimelineItem(state.timeline, itemId, "text", delta) };
|
|
131
|
+
}
|
|
132
|
+
case "item/commandExecution/outputDelta": {
|
|
133
|
+
const itemId = typeof params.itemId === "string" ? params.itemId : undefined;
|
|
134
|
+
const delta = typeof params.delta === "string" ? params.delta : undefined;
|
|
135
|
+
if (!itemId || !delta)
|
|
136
|
+
return state;
|
|
137
|
+
return { ...state, timeline: appendDeltaToTimelineItem(state.timeline, itemId, "output", delta) };
|
|
138
|
+
}
|
|
139
|
+
case "turn/plan/updated":
|
|
140
|
+
return applyPlanUpdate(state, params);
|
|
141
|
+
case "turn/diff/updated":
|
|
142
|
+
return applyDiffUpdate(state, params);
|
|
140
143
|
case "thread/tokenUsage/updated":
|
|
141
|
-
return
|
|
144
|
+
return applyTokenUsageUpdate(state, params);
|
|
142
145
|
default:
|
|
143
146
|
return state;
|
|
144
147
|
}
|
|
145
148
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
function bootstrapThreadFromTurnStarted(state, params) {
|
|
150
|
-
const turnObj = params.turn;
|
|
151
|
-
const threadId = typeof params.threadId === "string" ? params.threadId : "unknown";
|
|
152
|
-
const turnId = typeof turnObj?.id === "string" ? turnObj.id : "unknown";
|
|
153
|
-
return {
|
|
154
|
-
...state,
|
|
155
|
-
thread: {
|
|
156
|
-
threadId,
|
|
157
|
-
status: "active",
|
|
158
|
-
turns: [{ id: turnId, status: "inProgress", items: [] }],
|
|
159
|
-
},
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
// ─── Turn Handlers ────────────────────────────────────────────────
|
|
163
|
-
function addTurn(thread, params) {
|
|
164
|
-
const turnObj = params.turn;
|
|
165
|
-
const turnId = typeof turnObj?.id === "string" ? turnObj.id : "unknown";
|
|
166
|
-
const existing = thread.turns.find((t) => t.id === turnId);
|
|
167
|
-
if (existing) {
|
|
168
|
-
return thread;
|
|
169
|
-
}
|
|
170
|
-
return {
|
|
171
|
-
...thread,
|
|
172
|
-
status: "active",
|
|
173
|
-
turns: [...thread.turns, { id: turnId, status: "inProgress", items: [] }],
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
function completeTurn(thread, params) {
|
|
177
|
-
const turnObj = params.turn;
|
|
178
|
-
const turnId = typeof turnObj?.id === "string" ? turnObj.id : undefined;
|
|
179
|
-
const status = typeof turnObj?.status === "string" ? turnObj.status : "completed";
|
|
180
|
-
if (!turnId)
|
|
181
|
-
return thread;
|
|
182
|
-
return {
|
|
183
|
-
...thread,
|
|
184
|
-
turns: thread.turns.map((t) => t.id === turnId ? { ...t, status } : t),
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
function updatePlan(thread, params) {
|
|
149
|
+
// ─── Metadata Handlers (header, not timeline) ─────────────────────
|
|
150
|
+
function applyPlanUpdate(state, params) {
|
|
188
151
|
const plan = params.plan;
|
|
189
152
|
if (!Array.isArray(plan))
|
|
190
|
-
return
|
|
153
|
+
return state;
|
|
191
154
|
return {
|
|
192
|
-
...
|
|
155
|
+
...state,
|
|
193
156
|
plan: plan.map((entry) => {
|
|
194
157
|
const e = entry;
|
|
195
158
|
return {
|
|
@@ -199,9 +162,11 @@ function updatePlan(thread, params) {
|
|
|
199
162
|
}),
|
|
200
163
|
};
|
|
201
164
|
}
|
|
202
|
-
function
|
|
165
|
+
function applyDiffUpdate(state, params) {
|
|
203
166
|
const diff = typeof params.diff === "string" ? params.diff : undefined;
|
|
204
|
-
|
|
167
|
+
if (!diff)
|
|
168
|
+
return state;
|
|
169
|
+
return { ...state, diffSummary: parseDiffSummary(diff) };
|
|
205
170
|
}
|
|
206
171
|
function parseDiffSummary(diff) {
|
|
207
172
|
const files = new Set();
|
|
@@ -220,109 +185,13 @@ function parseDiffSummary(diff) {
|
|
|
220
185
|
}
|
|
221
186
|
return { filesChanged: files.size, linesAdded: added, linesRemoved: removed };
|
|
222
187
|
}
|
|
223
|
-
function
|
|
188
|
+
function applyTokenUsageUpdate(state, params) {
|
|
224
189
|
const usage = params.usage;
|
|
225
190
|
if (!usage)
|
|
226
|
-
return
|
|
191
|
+
return state;
|
|
227
192
|
const inputTokens = typeof usage.inputTokens === "number" ? usage.inputTokens
|
|
228
193
|
: typeof usage.input_tokens === "number" ? usage.input_tokens : 0;
|
|
229
194
|
const outputTokens = typeof usage.outputTokens === "number" ? usage.outputTokens
|
|
230
195
|
: typeof usage.output_tokens === "number" ? usage.output_tokens : 0;
|
|
231
|
-
return { ...
|
|
232
|
-
}
|
|
233
|
-
function updateThreadStatus(thread, params) {
|
|
234
|
-
const statusObj = params.status;
|
|
235
|
-
const statusType = typeof statusObj?.type === "string" ? statusObj.type : undefined;
|
|
236
|
-
if (!statusType)
|
|
237
|
-
return thread;
|
|
238
|
-
return { ...thread, status: statusType };
|
|
239
|
-
}
|
|
240
|
-
// ─── Item Handlers ────────────────────────────────────────────────
|
|
241
|
-
function getLatestTurn(thread) {
|
|
242
|
-
return thread.turns[thread.turns.length - 1];
|
|
243
|
-
}
|
|
244
|
-
function updateLatestTurn(thread, updater) {
|
|
245
|
-
const last = getLatestTurn(thread);
|
|
246
|
-
if (!last)
|
|
247
|
-
return thread;
|
|
248
|
-
return {
|
|
249
|
-
...thread,
|
|
250
|
-
turns: [...thread.turns.slice(0, -1), updater(last)],
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
function addItem(thread, params) {
|
|
254
|
-
const itemObj = params.item;
|
|
255
|
-
if (!itemObj)
|
|
256
|
-
return thread;
|
|
257
|
-
const id = typeof itemObj.id === "string" ? itemObj.id : "unknown";
|
|
258
|
-
const type = typeof itemObj.type === "string" ? itemObj.type : "unknown";
|
|
259
|
-
const status = typeof itemObj.status === "string" ? itemObj.status : "inProgress";
|
|
260
|
-
const item = { id, type, status };
|
|
261
|
-
if (type === "agentMessage" && typeof itemObj.text === "string") {
|
|
262
|
-
item.text = itemObj.text;
|
|
263
|
-
}
|
|
264
|
-
if (type === "commandExecution") {
|
|
265
|
-
const cmd = itemObj.command;
|
|
266
|
-
item.command = Array.isArray(cmd) ? cmd.join(" ") : typeof cmd === "string" ? cmd : undefined;
|
|
267
|
-
}
|
|
268
|
-
if (type === "mcpToolCall") {
|
|
269
|
-
const server = typeof itemObj.server === "string" ? itemObj.server : "";
|
|
270
|
-
const tool = typeof itemObj.tool === "string" ? itemObj.tool : "";
|
|
271
|
-
item.toolName = `${server}/${tool}`;
|
|
272
|
-
}
|
|
273
|
-
if (type === "dynamicToolCall") {
|
|
274
|
-
item.toolName = typeof itemObj.tool === "string" ? itemObj.tool : undefined;
|
|
275
|
-
}
|
|
276
|
-
return updateLatestTurn(thread, (turn) => ({
|
|
277
|
-
...turn,
|
|
278
|
-
items: [...turn.items, item],
|
|
279
|
-
}));
|
|
280
|
-
}
|
|
281
|
-
function completeItem(thread, params) {
|
|
282
|
-
const itemObj = params.item;
|
|
283
|
-
if (!itemObj)
|
|
284
|
-
return thread;
|
|
285
|
-
const id = typeof itemObj.id === "string" ? itemObj.id : undefined;
|
|
286
|
-
if (!id)
|
|
287
|
-
return thread;
|
|
288
|
-
const status = typeof itemObj.status === "string" ? itemObj.status : "completed";
|
|
289
|
-
const exitCode = typeof itemObj.exitCode === "number" ? itemObj.exitCode : undefined;
|
|
290
|
-
const durationMs = typeof itemObj.durationMs === "number" ? itemObj.durationMs : undefined;
|
|
291
|
-
const text = typeof itemObj.text === "string" ? itemObj.text : undefined;
|
|
292
|
-
const changes = Array.isArray(itemObj.changes) ? itemObj.changes : undefined;
|
|
293
|
-
return updateLatestTurn(thread, (turn) => ({
|
|
294
|
-
...turn,
|
|
295
|
-
items: turn.items.map((item) => {
|
|
296
|
-
if (item.id !== id)
|
|
297
|
-
return item;
|
|
298
|
-
return {
|
|
299
|
-
...item,
|
|
300
|
-
status,
|
|
301
|
-
...(exitCode !== undefined ? { exitCode } : {}),
|
|
302
|
-
...(durationMs !== undefined ? { durationMs } : {}),
|
|
303
|
-
...(text !== undefined ? { text } : {}),
|
|
304
|
-
...(changes !== undefined ? { changes } : {}),
|
|
305
|
-
};
|
|
306
|
-
}),
|
|
307
|
-
}));
|
|
308
|
-
}
|
|
309
|
-
function appendItemText(thread, params) {
|
|
310
|
-
const itemId = typeof params.itemId === "string" ? params.itemId : undefined;
|
|
311
|
-
const delta = typeof params.delta === "string" ? params.delta : undefined;
|
|
312
|
-
if (!itemId || !delta)
|
|
313
|
-
return thread;
|
|
314
|
-
return updateLatestTurn(thread, (turn) => ({
|
|
315
|
-
...turn,
|
|
316
|
-
items: turn.items.map((item) => item.id === itemId ? { ...item, text: (item.text ?? "") + delta } : item),
|
|
317
|
-
}));
|
|
318
|
-
}
|
|
319
|
-
function appendItemOutput(thread, params) {
|
|
320
|
-
const itemId = typeof params.itemId === "string" ? params.itemId : undefined;
|
|
321
|
-
const delta = typeof params.delta === "string" ? params.delta : undefined;
|
|
322
|
-
if (!itemId || !delta)
|
|
323
|
-
return thread;
|
|
324
|
-
return updateLatestTurn(thread, (turn) => ({
|
|
325
|
-
...turn,
|
|
326
|
-
items: turn.items.map((item) => item.id === itemId ? { ...item, output: (item.output ?? "") + delta } : item),
|
|
327
|
-
}));
|
|
196
|
+
return { ...state, tokenUsage: { inputTokens, outputTokens } };
|
|
328
197
|
}
|
package/dist/http.js
CHANGED
|
@@ -259,6 +259,14 @@ export async function buildHttpServer(config, service, logger) {
|
|
|
259
259
|
}
|
|
260
260
|
return reply.send({ ok: true, ...result });
|
|
261
261
|
});
|
|
262
|
+
app.get("/api/issues/:issueKey/timeline", async (request, reply) => {
|
|
263
|
+
const issueKey = request.params.issueKey;
|
|
264
|
+
const result = await service.getIssueTimeline(issueKey);
|
|
265
|
+
if (!result) {
|
|
266
|
+
return reply.code(404).send({ ok: false, reason: "issue_not_found" });
|
|
267
|
+
}
|
|
268
|
+
return reply.send({ ok: true, ...result });
|
|
269
|
+
});
|
|
262
270
|
app.get("/api/issues/:issueKey/live", async (request, reply) => {
|
|
263
271
|
const issueKey = request.params.issueKey;
|
|
264
272
|
const result = await service.getActiveRunStatus(issueKey);
|
|
@@ -58,6 +58,29 @@ export class IssueQueryService {
|
|
|
58
58
|
})),
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
+
async getIssueTimeline(issueKey) {
|
|
62
|
+
const issue = this.db.getTrackedIssueByKey(issueKey);
|
|
63
|
+
if (!issue)
|
|
64
|
+
return undefined;
|
|
65
|
+
const fullIssue = this.db.getIssueByKey(issueKey);
|
|
66
|
+
const runs = this.db.listRunsForIssue(issue.projectId, issue.linearIssueId).map((run) => ({
|
|
67
|
+
id: run.id,
|
|
68
|
+
runType: run.runType,
|
|
69
|
+
status: run.status,
|
|
70
|
+
startedAt: run.startedAt,
|
|
71
|
+
endedAt: run.endedAt,
|
|
72
|
+
threadId: run.threadId,
|
|
73
|
+
...(run.reportJson ? { report: JSON.parse(run.reportJson) } : {}),
|
|
74
|
+
}));
|
|
75
|
+
const feedEvents = this.db.operatorFeed.list({ issueKey, limit: 500 });
|
|
76
|
+
let liveThread = undefined;
|
|
77
|
+
const activeRunId = fullIssue?.activeRunId;
|
|
78
|
+
const activeRun = activeRunId !== undefined ? runs.find((r) => r.id === activeRunId) : undefined;
|
|
79
|
+
if (activeRun?.threadId) {
|
|
80
|
+
liveThread = await this.codex.readThread(activeRun.threadId, true).catch(() => undefined);
|
|
81
|
+
}
|
|
82
|
+
return { issue, runs, feedEvents, liveThread, activeRunId };
|
|
83
|
+
}
|
|
61
84
|
async getActiveRunStatus(issueKey) {
|
|
62
85
|
return await this.runStatusProvider.getActiveRunStatus(issueKey);
|
|
63
86
|
}
|
package/dist/run-orchestrator.js
CHANGED
|
@@ -72,6 +72,7 @@ function buildRunPrompt(issue, runType, repoPath, context) {
|
|
|
72
72
|
}
|
|
73
73
|
return lines.join("\n");
|
|
74
74
|
}
|
|
75
|
+
const PROGRESS_THROTTLE_MS = 10_000;
|
|
75
76
|
export class RunOrchestrator {
|
|
76
77
|
config;
|
|
77
78
|
db;
|
|
@@ -81,6 +82,7 @@ export class RunOrchestrator {
|
|
|
81
82
|
logger;
|
|
82
83
|
feed;
|
|
83
84
|
worktreeManager;
|
|
85
|
+
progressThrottle = new Map();
|
|
84
86
|
botIdentity;
|
|
85
87
|
constructor(config, db, codex, linearProvider, enqueueIssue, logger, feed) {
|
|
86
88
|
this.config = config;
|
|
@@ -253,6 +255,8 @@ export class RunOrchestrator {
|
|
|
253
255
|
eventJson: JSON.stringify(notification.params),
|
|
254
256
|
});
|
|
255
257
|
}
|
|
258
|
+
// Emit ephemeral progress activity to Linear for notable in-flight events
|
|
259
|
+
this.maybeEmitProgressActivity(notification, run);
|
|
256
260
|
if (notification.method !== "turn/completed")
|
|
257
261
|
return;
|
|
258
262
|
const thread = await this.readThreadWithRetry(threadId);
|
|
@@ -286,6 +290,7 @@ export class RunOrchestrator {
|
|
|
286
290
|
const failedIssue = this.db.getIssue(run.projectId, run.linearIssueId) ?? issue;
|
|
287
291
|
void this.emitLinearActivity(failedIssue, buildRunFailureActivity(run.runType));
|
|
288
292
|
void this.syncLinearSession(failedIssue, { activeRunType: run.runType });
|
|
293
|
+
this.progressThrottle.delete(run.id);
|
|
289
294
|
return;
|
|
290
295
|
}
|
|
291
296
|
// Complete the run
|
|
@@ -347,6 +352,45 @@ export class RunOrchestrator {
|
|
|
347
352
|
...(updatedIssue.prNumber !== undefined ? { prNumber: updatedIssue.prNumber } : {}),
|
|
348
353
|
}));
|
|
349
354
|
void this.syncLinearSession(updatedIssue);
|
|
355
|
+
this.progressThrottle.delete(run.id);
|
|
356
|
+
}
|
|
357
|
+
// ─── In-flight progress ──────────────────────────────────────────
|
|
358
|
+
maybeEmitProgressActivity(notification, run) {
|
|
359
|
+
const activity = this.resolveProgressActivity(notification);
|
|
360
|
+
if (!activity)
|
|
361
|
+
return;
|
|
362
|
+
const now = Date.now();
|
|
363
|
+
const lastEmit = this.progressThrottle.get(run.id) ?? 0;
|
|
364
|
+
if (now - lastEmit < PROGRESS_THROTTLE_MS)
|
|
365
|
+
return;
|
|
366
|
+
this.progressThrottle.set(run.id, now);
|
|
367
|
+
const issue = this.db.getIssue(run.projectId, run.linearIssueId);
|
|
368
|
+
if (issue) {
|
|
369
|
+
void this.emitLinearActivity(issue, activity, { ephemeral: true });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
resolveProgressActivity(notification) {
|
|
373
|
+
if (notification.method === "item/started") {
|
|
374
|
+
const item = notification.params.item;
|
|
375
|
+
if (!item)
|
|
376
|
+
return undefined;
|
|
377
|
+
const type = typeof item.type === "string" ? item.type : undefined;
|
|
378
|
+
if (type === "commandExecution") {
|
|
379
|
+
const cmd = item.command;
|
|
380
|
+
const cmdStr = Array.isArray(cmd) ? cmd.join(" ") : typeof cmd === "string" ? cmd : undefined;
|
|
381
|
+
return { type: "action", action: "Running", parameter: cmdStr?.slice(0, 120) ?? "command" };
|
|
382
|
+
}
|
|
383
|
+
if (type === "mcpToolCall") {
|
|
384
|
+
const server = typeof item.server === "string" ? item.server : "";
|
|
385
|
+
const tool = typeof item.tool === "string" ? item.tool : "";
|
|
386
|
+
return { type: "action", action: "Using", parameter: `${server}/${tool}` };
|
|
387
|
+
}
|
|
388
|
+
if (type === "dynamicToolCall") {
|
|
389
|
+
const tool = typeof item.tool === "string" ? item.tool : "tool";
|
|
390
|
+
return { type: "action", action: "Using", parameter: tool };
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return undefined;
|
|
350
394
|
}
|
|
351
395
|
// ─── Active status for query ──────────────────────────────────────
|
|
352
396
|
async getActiveRunStatus(issueKey) {
|
package/dist/service.js
CHANGED
|
@@ -289,6 +289,9 @@ export class PatchRelayService {
|
|
|
289
289
|
async getIssueReport(issueKey) {
|
|
290
290
|
return await this.queryService.getIssueReport(issueKey);
|
|
291
291
|
}
|
|
292
|
+
async getIssueTimeline(issueKey) {
|
|
293
|
+
return await this.queryService.getIssueTimeline(issueKey);
|
|
294
|
+
}
|
|
292
295
|
async getRunEvents(issueKey, runId) {
|
|
293
296
|
return await this.queryService.getRunEvents(issueKey, runId);
|
|
294
297
|
}
|
package/package.json
CHANGED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
3
|
-
const KIND_COLORS = {
|
|
4
|
-
stage: "cyan",
|
|
5
|
-
turn: "yellow",
|
|
6
|
-
github: "green",
|
|
7
|
-
webhook: "blue",
|
|
8
|
-
workflow: "magenta",
|
|
9
|
-
hook: "white",
|
|
10
|
-
};
|
|
11
|
-
function kindColor(kind) {
|
|
12
|
-
return KIND_COLORS[kind] ?? "white";
|
|
13
|
-
}
|
|
14
|
-
function formatTime(iso) {
|
|
15
|
-
return new Date(iso).toLocaleTimeString("en-GB", { hour12: false });
|
|
16
|
-
}
|
|
17
|
-
export function FeedTimeline({ entries, maxEntries }) {
|
|
18
|
-
const visible = maxEntries ? entries.slice(-maxEntries) : entries;
|
|
19
|
-
if (visible.length === 0) {
|
|
20
|
-
return _jsx(Text, { dimColor: true, children: "No events yet." });
|
|
21
|
-
}
|
|
22
|
-
return (_jsx(Box, { flexDirection: "column", children: visible.map((entry, i) => (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: formatTime(entry.at) }), _jsx(Text, { color: kindColor(entry.kind), children: (entry.status ?? entry.kind).padEnd(15) }), _jsx(Text, { children: entry.summary })] }, `feed-${i}`))) }));
|
|
23
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
3
|
-
import { TurnSection } from "./TurnSection.js";
|
|
4
|
-
function planStepSymbol(status) {
|
|
5
|
-
if (status === "completed")
|
|
6
|
-
return "\u2713";
|
|
7
|
-
if (status === "inProgress")
|
|
8
|
-
return "\u25b8";
|
|
9
|
-
return " ";
|
|
10
|
-
}
|
|
11
|
-
function planStepColor(status) {
|
|
12
|
-
if (status === "completed")
|
|
13
|
-
return "green";
|
|
14
|
-
if (status === "inProgress")
|
|
15
|
-
return "yellow";
|
|
16
|
-
return "white";
|
|
17
|
-
}
|
|
18
|
-
export function ThreadView({ thread, follow }) {
|
|
19
|
-
const visibleTurns = follow && thread.turns.length > 1
|
|
20
|
-
? thread.turns.slice(-1)
|
|
21
|
-
: thread.turns;
|
|
22
|
-
const turnOffset = follow && thread.turns.length > 1
|
|
23
|
-
? thread.turns.length - 1
|
|
24
|
-
: 0;
|
|
25
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 2, children: [_jsxs(Text, { dimColor: true, children: ["Thread: ", thread.threadId.slice(0, 16)] }), _jsxs(Text, { dimColor: true, children: ["Status: ", thread.status] }), _jsxs(Text, { dimColor: true, children: ["Turns: ", thread.turns.length] })] }), thread.plan && thread.plan.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: "Plan:" }), thread.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, { flexDirection: "column", marginTop: 1, children: visibleTurns.map((turn, i) => (_jsx(TurnSection, { turn: turn, index: i + turnOffset, follow: follow }, turn.id))) })] }));
|
|
26
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
3
|
-
import { ItemLine } from "./ItemLine.js";
|
|
4
|
-
function turnStatusColor(status) {
|
|
5
|
-
if (status === "completed")
|
|
6
|
-
return "green";
|
|
7
|
-
if (status === "failed" || status === "interrupted")
|
|
8
|
-
return "red";
|
|
9
|
-
if (status === "inProgress")
|
|
10
|
-
return "yellow";
|
|
11
|
-
return "white";
|
|
12
|
-
}
|
|
13
|
-
const FOLLOW_TAIL_SIZE = 8;
|
|
14
|
-
export function TurnSection({ turn, index, follow }) {
|
|
15
|
-
const items = follow && turn.items.length > FOLLOW_TAIL_SIZE
|
|
16
|
-
? turn.items.slice(-FOLLOW_TAIL_SIZE)
|
|
17
|
-
: turn.items;
|
|
18
|
-
const skipped = turn.items.length - items.length;
|
|
19
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { gap: 1, children: [_jsxs(Text, { bold: true, children: ["Turn #", index + 1] }), _jsx(Text, { color: turnStatusColor(turn.status), children: turn.status }), _jsxs(Text, { dimColor: true, children: ["(", turn.items.length, " items)"] })] }), skipped > 0 && _jsxs(Text, { dimColor: true, children: [" ... ", skipped, " earlier items"] }), items.map((item, i) => (_jsx(ItemLine, { item: item, isLast: i === items.length - 1 }, item.id)))] }));
|
|
20
|
-
}
|