patchrelay 0.25.3 → 0.25.4
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 +4 -1
- package/dist/cli/watch/HelpBar.js +5 -2
- package/dist/cli/watch/IssueDetailView.js +3 -3
- package/dist/cli/watch/ItemLine.js +40 -66
- package/dist/cli/watch/Timeline.js +8 -13
- package/dist/cli/watch/TimelineRow.js +47 -19
- package/dist/cli/watch/timeline-builder.js +175 -4
- package/dist/cli/watch/timeline-presentation.js +289 -0
- package/dist/cli/watch/use-detail-stream.js +1 -0
- package/dist/cli/watch/watch-state.js +3 -0
- package/dist/issue-query-service.js +6 -0
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
package/dist/cli/watch/App.js
CHANGED
|
@@ -177,6 +177,9 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
177
177
|
else if (input === "t") {
|
|
178
178
|
dispatch({ type: "switch-detail-tab", tab: "timeline" });
|
|
179
179
|
}
|
|
180
|
+
else if (input === "v") {
|
|
181
|
+
dispatch({ type: "toggle-timeline-mode" });
|
|
182
|
+
}
|
|
180
183
|
else if (input === "j" || key.downArrow) {
|
|
181
184
|
dispatch({ type: "detail-navigate", direction: "next", filtered });
|
|
182
185
|
}
|
|
@@ -190,5 +193,5 @@ export function App({ baseUrl, bearerToken, initialIssueKey }) {
|
|
|
190
193
|
}
|
|
191
194
|
}
|
|
192
195
|
});
|
|
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 })) }));
|
|
196
|
+
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, timelineMode: state.timelineMode, 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 })) }));
|
|
194
197
|
}
|
|
@@ -5,11 +5,14 @@ const HELP_TEXT = {
|
|
|
5
5
|
detail: "",
|
|
6
6
|
feed: "Esc: list q: quit",
|
|
7
7
|
};
|
|
8
|
-
export function HelpBar({ view, follow, detailTab }) {
|
|
8
|
+
export function HelpBar({ view, follow, detailTab, timelineMode }) {
|
|
9
9
|
let text;
|
|
10
10
|
if (view === "detail") {
|
|
11
11
|
const tabHint = detailTab === "history" ? "t: timeline" : "h: history";
|
|
12
|
-
|
|
12
|
+
const timelineHint = detailTab === "timeline" ? `v: ${timelineMode === "verbose" ? "compact" : "verbose"}` : undefined;
|
|
13
|
+
text = [tabHint, timelineHint, "j/k: prev/next", "Esc: list", `f: follow ${follow ? "on" : "off"}`, "p: prompt", "s: stop", "r: retry", "q: quit"]
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.join(" ");
|
|
13
16
|
}
|
|
14
17
|
else {
|
|
15
18
|
text = HELP_TEXT[view];
|
|
@@ -24,9 +24,9 @@ function ElapsedTime({ startedAt }) {
|
|
|
24
24
|
const seconds = elapsed % 60;
|
|
25
25
|
return _jsxs(Text, { dimColor: true, children: [minutes, "m ", String(seconds).padStart(2, "0"), "s"] });
|
|
26
26
|
}
|
|
27
|
-
export function IssueDetailView({ issue, timeline, follow, activeRunStartedAt, activeRunId, tokenUsage, diffSummary, plan, issueContext, detailTab, rawRuns, rawFeedEvents, }) {
|
|
27
|
+
export function IssueDetailView({ issue, timeline, follow, activeRunStartedAt, activeRunId, tokenUsage, diffSummary, plan, issueContext, detailTab, timelineMode, rawRuns, rawFeedEvents, }) {
|
|
28
28
|
if (!issue) {
|
|
29
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", children: "Issue not found." }), _jsx(HelpBar, { view: "detail", follow: follow, detailTab: detailTab })] }));
|
|
29
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", children: "Issue not found." }), _jsx(HelpBar, { view: "detail", follow: follow, detailTab: detailTab, timelineMode: timelineMode })] }));
|
|
30
30
|
}
|
|
31
31
|
const key = issue.issueKey ?? issue.projectId;
|
|
32
32
|
const meta = [];
|
|
@@ -37,5 +37,5 @@ export function IssueDetailView({ issue, timeline, follow, activeRunStartedAt, a
|
|
|
37
37
|
if (issueContext?.runCount)
|
|
38
38
|
meta.push(`${issueContext.runCount} runs`);
|
|
39
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 }) })] }));
|
|
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(" ") }), detailTab === "timeline" && _jsx(Text, { dimColor: true, children: timelineMode }), 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, mode: timelineMode }) })] })) : (_jsx(StateHistoryView, { history: history, plan: plan, activeRunId: activeRunId })), _jsx(Box, { marginTop: 1, children: _jsx(HelpBar, { view: "detail", follow: follow, detailTab: detailTab, timelineMode: timelineMode }) })] }));
|
|
41
41
|
}
|
|
@@ -1,92 +1,66 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
const STATUS_SYMBOL = {
|
|
4
|
-
completed: "\u2713",
|
|
5
|
-
failed: "\u2717",
|
|
6
|
-
declined: "\u2717",
|
|
7
|
-
inProgress: "\u25cf",
|
|
8
|
-
};
|
|
9
|
-
function statusChar(status) {
|
|
10
|
-
return STATUS_SYMBOL[status] ?? " ";
|
|
11
|
-
}
|
|
12
|
-
function statusColor(status) {
|
|
13
|
-
if (status === "completed")
|
|
14
|
-
return "green";
|
|
15
|
-
if (status === "failed" || status === "declined")
|
|
16
|
-
return "red";
|
|
17
|
-
if (status === "inProgress")
|
|
18
|
-
return "yellow";
|
|
19
|
-
return "white";
|
|
20
|
-
}
|
|
21
3
|
function truncate(text, max) {
|
|
22
|
-
const line = text.replace(/\
|
|
23
|
-
return line.length > max ? `${line.slice(0, max - 3)}...` : line;
|
|
24
|
-
}
|
|
25
|
-
function renderAgentMessage(item) {
|
|
26
|
-
return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "message: " }), _jsx(Text, { wrap: "wrap", children: item.text ?? "" })] }));
|
|
4
|
+
const line = text.replace(/\s+/g, " ").trim();
|
|
5
|
+
return line.length > max ? `${line.slice(0, Math.max(0, max - 3))}...` : line;
|
|
27
6
|
}
|
|
28
7
|
function cleanCommand(raw) {
|
|
29
|
-
// Strip /bin/bash -lc '...' wrapper — show the inner command
|
|
30
8
|
const bashMatch = raw.match(/^\/bin\/(?:ba)?sh\s+-\w*c\s+['"](.+?)['"]$/s);
|
|
31
9
|
if (bashMatch?.[1])
|
|
32
10
|
return bashMatch[1];
|
|
33
|
-
// Strip /bin/bash -lc "..." (double quotes)
|
|
34
11
|
const bashMatch2 = raw.match(/^\/bin\/(?:ba)?sh\s+-\w*c\s+"(.+?)"$/s);
|
|
35
12
|
if (bashMatch2?.[1])
|
|
36
13
|
return bashMatch2[1];
|
|
37
14
|
return raw;
|
|
38
15
|
}
|
|
39
|
-
function
|
|
40
|
-
const cmd = cleanCommand(item.command ?? "?");
|
|
41
|
-
const exitCode = item.exitCode;
|
|
42
|
-
const exitLabel = exitCode !== undefined && exitCode !== 0 ? ` exit:${exitCode}` : "";
|
|
43
|
-
const duration = item.durationMs !== undefined ? ` ${(item.durationMs / 1000).toFixed(1)}s` : "";
|
|
44
|
-
const suffix = `${exitLabel}${duration}`;
|
|
45
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "$ " }), _jsx(Text, { children: cmd }), exitLabel && _jsx(Text, { color: "red", children: exitLabel }), !exitLabel && suffix && _jsx(Text, { dimColor: true, children: suffix })] }), item.output && item.status === "inProgress" && (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [" ", item.output.split("\n").filter(Boolean).at(-1) ?? ""] }))] }));
|
|
46
|
-
}
|
|
47
|
-
function renderFileChange(item) {
|
|
16
|
+
function summarizeFileChange(item) {
|
|
48
17
|
const count = item.changes?.length ?? 0;
|
|
49
|
-
return
|
|
18
|
+
return `updated ${count} file${count === 1 ? "" : "s"}`;
|
|
50
19
|
}
|
|
51
|
-
function
|
|
52
|
-
return
|
|
20
|
+
function summarizeToolCall(item) {
|
|
21
|
+
return `used ${item.toolName ?? item.type}`;
|
|
53
22
|
}
|
|
54
|
-
function
|
|
55
|
-
return
|
|
23
|
+
function summarizeText(item) {
|
|
24
|
+
return truncate(item.text ?? "", 160);
|
|
56
25
|
}
|
|
57
|
-
function
|
|
58
|
-
|
|
26
|
+
function itemPrefix(item) {
|
|
27
|
+
if (item.type === "commandExecution")
|
|
28
|
+
return "$ ";
|
|
29
|
+
return "";
|
|
59
30
|
}
|
|
60
|
-
|
|
61
|
-
const prefix = isLast ? "\u2514" : "\u251c";
|
|
62
|
-
let content;
|
|
31
|
+
function itemText(item) {
|
|
63
32
|
switch (item.type) {
|
|
64
33
|
case "agentMessage":
|
|
65
|
-
|
|
66
|
-
|
|
34
|
+
case "plan":
|
|
35
|
+
case "reasoning":
|
|
36
|
+
return summarizeText(item);
|
|
67
37
|
case "commandExecution":
|
|
68
|
-
|
|
69
|
-
break;
|
|
38
|
+
return truncate(cleanCommand(item.command ?? "?"), 140);
|
|
70
39
|
case "fileChange":
|
|
71
|
-
|
|
72
|
-
break;
|
|
40
|
+
return summarizeFileChange(item);
|
|
73
41
|
case "mcpToolCall":
|
|
74
42
|
case "dynamicToolCall":
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
content = renderPlan(item);
|
|
79
|
-
break;
|
|
80
|
-
case "userMessage": {
|
|
81
|
-
const userText = item.text?.trim();
|
|
82
|
-
if (!userText)
|
|
83
|
-
return _jsx(_Fragment, {});
|
|
84
|
-
content = (_jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: "you: " }), _jsx(Text, { wrap: "wrap", children: userText })] }));
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
43
|
+
return summarizeToolCall(item);
|
|
44
|
+
case "userMessage":
|
|
45
|
+
return `you: ${summarizeText(item)}`;
|
|
87
46
|
default:
|
|
88
|
-
|
|
89
|
-
|
|
47
|
+
return item.text ? summarizeText(item) : item.type;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function itemColor(item) {
|
|
51
|
+
if (item.status === "failed" || item.status === "declined")
|
|
52
|
+
return "red";
|
|
53
|
+
if (item.status === "inProgress")
|
|
54
|
+
return "yellow";
|
|
55
|
+
if (item.type === "userMessage")
|
|
56
|
+
return "yellow";
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
export function ItemLine({ item }) {
|
|
60
|
+
const text = itemText(item);
|
|
61
|
+
if (!text) {
|
|
62
|
+
return _jsx(_Fragment, {});
|
|
90
63
|
}
|
|
91
|
-
|
|
64
|
+
const color = itemColor(item);
|
|
65
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "wrap", ...(color ? { color } : {}), children: [itemPrefix(item), text] }), item.output && item.status === "inProgress" && (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: truncate(item.output.split("\n").filter(Boolean).at(-1) ?? "", 120) }))] }));
|
|
92
66
|
}
|
|
@@ -1,29 +1,24 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useMemo } from "react";
|
|
3
3
|
import { Box, Static, Text, useStdout } from "ink";
|
|
4
|
+
import { buildTimelineRows } from "./timeline-presentation.js";
|
|
4
5
|
import { TimelineRow } from "./TimelineRow.js";
|
|
5
6
|
const ACTIVE_TAIL = 8;
|
|
6
|
-
function
|
|
7
|
-
if (entry.kind === "item" && entry.item?.status === "inProgress")
|
|
8
|
-
return false;
|
|
9
|
-
if (entry.kind === "run-start")
|
|
10
|
-
return false; // keep run-start in active area until run ends
|
|
11
|
-
return true;
|
|
12
|
-
}
|
|
13
|
-
export function Timeline({ entries, follow }) {
|
|
7
|
+
export function Timeline({ entries, follow, mode }) {
|
|
14
8
|
const { stdout } = useStdout();
|
|
15
9
|
const rows = stdout?.rows ?? 24;
|
|
16
10
|
const maxActive = Math.max(ACTIVE_TAIL, rows - 12);
|
|
11
|
+
const displayRows = useMemo(() => buildTimelineRows(entries, mode), [entries, mode]);
|
|
17
12
|
// Split: finalized entries go to Static (terminal scrollback), active entries re-render
|
|
18
13
|
const splitIndex = useMemo(() => {
|
|
19
14
|
if (!follow)
|
|
20
15
|
return 0; // follow OFF: everything in active area (re-renders)
|
|
21
16
|
// Find the boundary: keep the last maxActive entries in the active area
|
|
22
|
-
return Math.max(0,
|
|
23
|
-
}, [
|
|
24
|
-
const finalized =
|
|
25
|
-
const active =
|
|
26
|
-
if (
|
|
17
|
+
return Math.max(0, displayRows.length - maxActive);
|
|
18
|
+
}, [displayRows.length, follow, maxActive]);
|
|
19
|
+
const finalized = displayRows.slice(0, splitIndex);
|
|
20
|
+
const active = displayRows.slice(splitIndex);
|
|
21
|
+
if (displayRows.length === 0) {
|
|
27
22
|
return _jsx(Text, { dimColor: true, children: "No timeline events yet." });
|
|
28
23
|
}
|
|
29
24
|
return (_jsxs(Box, { flexDirection: "column", children: [finalized.length > 0 && (_jsx(Static, { items: finalized, children: (entry) => _jsx(TimelineRow, { entry: entry }, entry.id) })), active.map((entry) => (_jsx(TimelineRow, { entry: entry }, entry.id)))] }));
|
|
@@ -11,7 +11,7 @@ function formatDuration(startedAt, endedAt) {
|
|
|
11
11
|
return `${seconds}s`;
|
|
12
12
|
const minutes = Math.floor(seconds / 60);
|
|
13
13
|
const s = seconds % 60;
|
|
14
|
-
return `${minutes}m
|
|
14
|
+
return `${minutes}m ${String(s).padStart(2, "0")}s`;
|
|
15
15
|
}
|
|
16
16
|
const CHECK_SYMBOLS = { passed: "\u2713", failed: "\u2717", pending: "\u25cf" };
|
|
17
17
|
const CHECK_COLORS = { passed: "green", failed: "red", pending: "yellow" };
|
|
@@ -21,34 +21,62 @@ const RUN_LABELS = {
|
|
|
21
21
|
review_fix: "review fix",
|
|
22
22
|
queue_repair: "merge fix",
|
|
23
23
|
};
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
function runStatusColor(status) {
|
|
25
|
+
if (status === "completed")
|
|
26
|
+
return "green";
|
|
27
|
+
if (status === "failed")
|
|
28
|
+
return "red";
|
|
29
|
+
if (status === "released")
|
|
30
|
+
return "magenta";
|
|
31
|
+
if (status === "running")
|
|
32
|
+
return "yellow";
|
|
33
|
+
return "white";
|
|
28
34
|
}
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
function runStatusLabel(status) {
|
|
36
|
+
if (status === "running")
|
|
37
|
+
return "running";
|
|
38
|
+
if (status === "released")
|
|
39
|
+
return "released";
|
|
40
|
+
return status;
|
|
41
|
+
}
|
|
42
|
+
function detailColor(detail) {
|
|
43
|
+
if (detail.tone === "command")
|
|
44
|
+
return "white";
|
|
45
|
+
if (detail.tone === "user")
|
|
46
|
+
return "yellow";
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
function detailPrefix(detail) {
|
|
50
|
+
if (detail.tone === "command")
|
|
51
|
+
return "$ ";
|
|
52
|
+
return "";
|
|
53
|
+
}
|
|
54
|
+
function FeedRow({ entry }) {
|
|
55
|
+
const label = entry.feed.status ?? entry.feed.feedKind;
|
|
56
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [formatTime(entry.at), " "] }), _jsx(Text, { color: "cyan", children: label.padEnd(12) }), _jsxs(Text, { children: [" ", entry.feed.summary] })] }));
|
|
32
57
|
}
|
|
33
|
-
function
|
|
58
|
+
function RunRow({ entry }) {
|
|
34
59
|
const run = entry.run;
|
|
35
|
-
const color = run.status
|
|
36
|
-
const
|
|
37
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [formatTime(entry.at), " "] }), _jsx(Text, { bold: true, color:
|
|
60
|
+
const color = runStatusColor(run.status);
|
|
61
|
+
const duration = run.endedAt ? formatDuration(run.startedAt, run.endedAt) : undefined;
|
|
62
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [formatTime(entry.at), " "] }), _jsx(Text, { bold: true, color: "yellow", children: (RUN_LABELS[run.runType] ?? run.runType).padEnd(12) }), _jsxs(Text, { color: color, children: [" ", runStatusLabel(run.status)] }), duration ? _jsx(Text, { dimColor: true, children: ` ${duration}` }) : null] }), entry.details.map((detail, index) => (_jsxs(Box, { paddingLeft: 6, children: [_jsx(Text, { dimColor: true, children: " " }), _jsxs(Text, { wrap: "wrap", ...(detailColor(detail) ? { color: detailColor(detail) } : {}), children: [detailPrefix(detail), detail.text] })] }, `${entry.id}-detail-${index}`)))] }));
|
|
38
63
|
}
|
|
39
64
|
function ItemRow({ entry }) {
|
|
40
|
-
return (_jsx(Box, { paddingLeft:
|
|
65
|
+
return (_jsx(Box, { paddingLeft: 6, children: _jsx(ItemLine, { item: entry.item }) }));
|
|
41
66
|
}
|
|
42
67
|
function CIChecksRow({ entry }) {
|
|
43
68
|
const ci = entry.ciChecks;
|
|
44
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [formatTime(entry.at), " "] }), _jsx(Text, { color: CHECK_COLORS[ci.overall] ?? "white", children: "checks".padEnd(
|
|
69
|
+
return (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [formatTime(entry.at), " "] }), _jsx(Text, { color: CHECK_COLORS[ci.overall] ?? "white", children: "checks".padEnd(12) }), _jsx(Text, { children: " " }), 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, " "] })] }, `c-${i}`)))] }));
|
|
45
70
|
}
|
|
46
71
|
export function TimelineRow({ entry }) {
|
|
47
72
|
switch (entry.kind) {
|
|
48
|
-
case "feed":
|
|
49
|
-
|
|
50
|
-
case "run
|
|
51
|
-
|
|
52
|
-
case "
|
|
73
|
+
case "feed":
|
|
74
|
+
return _jsx(FeedRow, { entry: entry });
|
|
75
|
+
case "run":
|
|
76
|
+
return _jsx(RunRow, { entry: entry });
|
|
77
|
+
case "item":
|
|
78
|
+
return _jsx(ItemRow, { entry: entry });
|
|
79
|
+
case "ci-checks":
|
|
80
|
+
return _jsx(CIChecksRow, { entry: entry });
|
|
53
81
|
}
|
|
54
82
|
}
|
|
@@ -19,9 +19,14 @@ export function buildTimelineFromRehydration(runs, feedEvents, liveThread, activ
|
|
|
19
19
|
run: { runType: run.runType, status: run.status, startedAt: run.startedAt, endedAt: run.endedAt },
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
|
-
// Items from completed run
|
|
23
|
-
if (run.
|
|
24
|
-
|
|
22
|
+
// Items from completed run event history, with report fallback
|
|
23
|
+
if (run.id !== activeRunId) {
|
|
24
|
+
if (run.events && run.events.length > 0) {
|
|
25
|
+
entries.push(...itemsFromThreadEvents(run.id, run.events));
|
|
26
|
+
}
|
|
27
|
+
else if (run.report) {
|
|
28
|
+
entries.push(...itemsFromReport(run.id, run.report, run.startedAt, run.endedAt));
|
|
29
|
+
}
|
|
25
30
|
}
|
|
26
31
|
}
|
|
27
32
|
// 2. Items from live thread (active run)
|
|
@@ -36,7 +41,10 @@ export function buildTimelineFromRehydration(runs, feedEvents, liveThread, activ
|
|
|
36
41
|
if (cmp !== 0)
|
|
37
42
|
return cmp;
|
|
38
43
|
// Within same timestamp: run-start before items, items before run-end
|
|
39
|
-
|
|
44
|
+
const kindCmp = kindOrder(a.kind) - kindOrder(b.kind);
|
|
45
|
+
if (kindCmp !== 0)
|
|
46
|
+
return kindCmp;
|
|
47
|
+
return a.id.localeCompare(b.id);
|
|
40
48
|
});
|
|
41
49
|
return entries;
|
|
42
50
|
}
|
|
@@ -178,6 +186,169 @@ function materializeItem(item) {
|
|
|
178
186
|
return base;
|
|
179
187
|
}
|
|
180
188
|
}
|
|
189
|
+
function itemsFromThreadEvents(runId, events) {
|
|
190
|
+
const entries = [];
|
|
191
|
+
for (const event of events) {
|
|
192
|
+
const params = event.parsedEvent;
|
|
193
|
+
if (!params)
|
|
194
|
+
continue;
|
|
195
|
+
switch (event.method) {
|
|
196
|
+
case "item/started": {
|
|
197
|
+
const item = materializeNotificationItem(params.item);
|
|
198
|
+
if (!item)
|
|
199
|
+
break;
|
|
200
|
+
entries.push({
|
|
201
|
+
id: `event-${event.id}-item-${item.id}`,
|
|
202
|
+
at: event.createdAt,
|
|
203
|
+
kind: "item",
|
|
204
|
+
runId,
|
|
205
|
+
item,
|
|
206
|
+
});
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
case "item/completed": {
|
|
210
|
+
const item = materializeNotificationItem(params.item);
|
|
211
|
+
if (!item)
|
|
212
|
+
break;
|
|
213
|
+
const existing = findTimelineItem(entries, item.id);
|
|
214
|
+
if (existing) {
|
|
215
|
+
existing.item = mergeDefinedItemFields(existing.item, item);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
entries.push({
|
|
219
|
+
id: `event-${event.id}-item-${item.id}`,
|
|
220
|
+
at: event.createdAt,
|
|
221
|
+
kind: "item",
|
|
222
|
+
runId,
|
|
223
|
+
item,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
case "item/agentMessage/delta":
|
|
229
|
+
case "item/plan/delta":
|
|
230
|
+
case "item/reasoning/summaryTextDelta": {
|
|
231
|
+
const itemId = typeof params.itemId === "string" ? params.itemId : undefined;
|
|
232
|
+
const delta = typeof params.delta === "string" ? params.delta : undefined;
|
|
233
|
+
if (!itemId || !delta)
|
|
234
|
+
break;
|
|
235
|
+
const existing = findTimelineItem(entries, itemId);
|
|
236
|
+
const target = existing ?? createReplayPlaceholder(entries, runId, event.createdAt, event.id, itemId, inferItemTypeFromDeltaMethod(event.method));
|
|
237
|
+
target.item = {
|
|
238
|
+
...target.item,
|
|
239
|
+
text: `${target.item?.text ?? ""}${delta}`,
|
|
240
|
+
};
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case "item/commandExecution/outputDelta": {
|
|
244
|
+
const itemId = typeof params.itemId === "string" ? params.itemId : undefined;
|
|
245
|
+
const delta = typeof params.delta === "string" ? params.delta : undefined;
|
|
246
|
+
if (!itemId || !delta)
|
|
247
|
+
break;
|
|
248
|
+
const existing = findTimelineItem(entries, itemId);
|
|
249
|
+
const target = existing ?? createReplayPlaceholder(entries, runId, event.createdAt, event.id, itemId, "commandExecution");
|
|
250
|
+
target.item = {
|
|
251
|
+
...target.item,
|
|
252
|
+
output: `${target.item?.output ?? ""}${delta}`,
|
|
253
|
+
};
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return entries;
|
|
259
|
+
}
|
|
260
|
+
function findTimelineItem(entries, itemId) {
|
|
261
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
262
|
+
const entry = entries[i];
|
|
263
|
+
if (entry.kind === "item" && entry.item?.id === itemId) {
|
|
264
|
+
return entry;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
function createReplayPlaceholder(entries, runId, at, eventId, itemId, type) {
|
|
270
|
+
const entry = {
|
|
271
|
+
id: `event-${eventId}-item-${itemId}`,
|
|
272
|
+
at,
|
|
273
|
+
kind: "item",
|
|
274
|
+
runId,
|
|
275
|
+
item: { id: itemId, type, status: "inProgress" },
|
|
276
|
+
};
|
|
277
|
+
entries.push(entry);
|
|
278
|
+
return entry;
|
|
279
|
+
}
|
|
280
|
+
function inferItemTypeFromDeltaMethod(method) {
|
|
281
|
+
switch (method) {
|
|
282
|
+
case "item/agentMessage/delta":
|
|
283
|
+
return "agentMessage";
|
|
284
|
+
case "item/plan/delta":
|
|
285
|
+
return "plan";
|
|
286
|
+
case "item/reasoning/summaryTextDelta":
|
|
287
|
+
return "reasoning";
|
|
288
|
+
default:
|
|
289
|
+
return "unknown";
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function materializeNotificationItem(raw) {
|
|
293
|
+
if (!raw || typeof raw !== "object")
|
|
294
|
+
return undefined;
|
|
295
|
+
const itemObj = raw;
|
|
296
|
+
const id = typeof itemObj.id === "string" ? itemObj.id : undefined;
|
|
297
|
+
const type = typeof itemObj.type === "string" ? itemObj.type : "unknown";
|
|
298
|
+
if (!id)
|
|
299
|
+
return undefined;
|
|
300
|
+
const item = {
|
|
301
|
+
id,
|
|
302
|
+
type,
|
|
303
|
+
status: typeof itemObj.status === "string" ? itemObj.status : "inProgress",
|
|
304
|
+
};
|
|
305
|
+
if ((type === "agentMessage" || type === "userMessage" || type === "plan") && typeof itemObj.text === "string") {
|
|
306
|
+
item.text = itemObj.text;
|
|
307
|
+
}
|
|
308
|
+
if (type === "reasoning") {
|
|
309
|
+
if (Array.isArray(itemObj.summary)) {
|
|
310
|
+
item.text = itemObj.summary.join("\n");
|
|
311
|
+
}
|
|
312
|
+
else if (typeof itemObj.text === "string") {
|
|
313
|
+
item.text = itemObj.text;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (type === "commandExecution") {
|
|
317
|
+
const cmd = itemObj.command;
|
|
318
|
+
item.command = Array.isArray(cmd) ? cmd.join(" ") : typeof cmd === "string" ? cmd : undefined;
|
|
319
|
+
if (typeof itemObj.aggregatedOutput === "string")
|
|
320
|
+
item.output = itemObj.aggregatedOutput;
|
|
321
|
+
}
|
|
322
|
+
if (type === "fileChange" && Array.isArray(itemObj.changes)) {
|
|
323
|
+
item.changes = itemObj.changes;
|
|
324
|
+
}
|
|
325
|
+
if (type === "mcpToolCall") {
|
|
326
|
+
item.toolName = `${String(itemObj.server ?? "")}/${String(itemObj.tool ?? "")}`;
|
|
327
|
+
}
|
|
328
|
+
if (type === "dynamicToolCall" && typeof itemObj.tool === "string") {
|
|
329
|
+
item.toolName = itemObj.tool;
|
|
330
|
+
}
|
|
331
|
+
if (typeof itemObj.exitCode === "number")
|
|
332
|
+
item.exitCode = itemObj.exitCode;
|
|
333
|
+
if (typeof itemObj.durationMs === "number")
|
|
334
|
+
item.durationMs = itemObj.durationMs;
|
|
335
|
+
return item;
|
|
336
|
+
}
|
|
337
|
+
function mergeDefinedItemFields(base, patch) {
|
|
338
|
+
return {
|
|
339
|
+
...base,
|
|
340
|
+
id: patch.id,
|
|
341
|
+
type: patch.type,
|
|
342
|
+
status: patch.status,
|
|
343
|
+
...(patch.text !== undefined ? { text: patch.text } : {}),
|
|
344
|
+
...(patch.command !== undefined ? { command: patch.command } : {}),
|
|
345
|
+
...(patch.output !== undefined ? { output: patch.output } : {}),
|
|
346
|
+
...(patch.exitCode !== undefined ? { exitCode: patch.exitCode } : {}),
|
|
347
|
+
...(patch.durationMs !== undefined ? { durationMs: patch.durationMs } : {}),
|
|
348
|
+
...(patch.changes !== undefined ? { changes: patch.changes } : {}),
|
|
349
|
+
...(patch.toolName !== undefined ? { toolName: patch.toolName } : {}),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
181
352
|
// ─── Feed Events to Timeline Entries ──────────────────────────────
|
|
182
353
|
function feedEventsToEntries(feedEvents) {
|
|
183
354
|
const entries = [];
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
export function buildTimelineRows(entries, mode) {
|
|
2
|
+
return mode === "compact" ? buildCompactTimelineRows(entries) : buildVerboseTimelineRows(entries);
|
|
3
|
+
}
|
|
4
|
+
function buildVerboseTimelineRows(entries) {
|
|
5
|
+
return entries.flatMap((entry) => {
|
|
6
|
+
switch (entry.kind) {
|
|
7
|
+
case "run-start":
|
|
8
|
+
return [{
|
|
9
|
+
id: entry.id,
|
|
10
|
+
kind: "run",
|
|
11
|
+
at: entry.at,
|
|
12
|
+
finalized: false,
|
|
13
|
+
run: entry.run,
|
|
14
|
+
details: [],
|
|
15
|
+
}];
|
|
16
|
+
case "run-end":
|
|
17
|
+
return [{
|
|
18
|
+
id: entry.id,
|
|
19
|
+
kind: "run",
|
|
20
|
+
at: entry.at,
|
|
21
|
+
finalized: true,
|
|
22
|
+
run: entry.run,
|
|
23
|
+
details: [],
|
|
24
|
+
}];
|
|
25
|
+
case "feed":
|
|
26
|
+
return [{
|
|
27
|
+
id: entry.id,
|
|
28
|
+
kind: "feed",
|
|
29
|
+
at: entry.at,
|
|
30
|
+
finalized: true,
|
|
31
|
+
feed: entry.feed,
|
|
32
|
+
}];
|
|
33
|
+
case "ci-checks":
|
|
34
|
+
return [{
|
|
35
|
+
id: entry.id,
|
|
36
|
+
kind: "ci-checks",
|
|
37
|
+
at: entry.at,
|
|
38
|
+
finalized: true,
|
|
39
|
+
ciChecks: entry.ciChecks,
|
|
40
|
+
}];
|
|
41
|
+
case "item":
|
|
42
|
+
return [{
|
|
43
|
+
id: entry.id,
|
|
44
|
+
kind: "item",
|
|
45
|
+
at: entry.at,
|
|
46
|
+
finalized: entry.item?.status !== "inProgress",
|
|
47
|
+
item: entry.item,
|
|
48
|
+
}];
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function buildCompactTimelineRows(entries) {
|
|
53
|
+
const rows = [];
|
|
54
|
+
const runs = new Map();
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (entry.kind === "run-start" && entry.runId !== undefined) {
|
|
57
|
+
const existing = runs.get(entry.runId);
|
|
58
|
+
if (!existing) {
|
|
59
|
+
const run = { ...entry.run };
|
|
60
|
+
runs.set(entry.runId, {
|
|
61
|
+
id: `run-${entry.runId}`,
|
|
62
|
+
at: run.startedAt,
|
|
63
|
+
run,
|
|
64
|
+
items: [],
|
|
65
|
+
endedAt: run.endedAt,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (entry.kind === "run-end" && entry.runId !== undefined) {
|
|
71
|
+
const existing = runs.get(entry.runId);
|
|
72
|
+
if (existing) {
|
|
73
|
+
existing.run = { ...entry.run };
|
|
74
|
+
existing.endedAt = entry.run?.endedAt;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const run = { ...entry.run };
|
|
78
|
+
runs.set(entry.runId, {
|
|
79
|
+
id: `run-${entry.runId}`,
|
|
80
|
+
at: run.startedAt,
|
|
81
|
+
run,
|
|
82
|
+
items: [],
|
|
83
|
+
endedAt: run.endedAt,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (entry.kind === "item" && entry.runId !== undefined && runs.has(entry.runId)) {
|
|
89
|
+
runs.get(entry.runId).items.push(entry.item);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (entry.kind === "feed" && shouldHideFeedInCompact(entry.feed)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (entry.kind === "feed") {
|
|
96
|
+
rows.push({
|
|
97
|
+
id: entry.id,
|
|
98
|
+
kind: "feed",
|
|
99
|
+
at: entry.at,
|
|
100
|
+
finalized: true,
|
|
101
|
+
feed: entry.feed,
|
|
102
|
+
});
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (entry.kind === "ci-checks") {
|
|
106
|
+
rows.push({
|
|
107
|
+
id: entry.id,
|
|
108
|
+
kind: "ci-checks",
|
|
109
|
+
at: entry.at,
|
|
110
|
+
finalized: true,
|
|
111
|
+
ciChecks: entry.ciChecks,
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (entry.kind === "item") {
|
|
116
|
+
rows.push({
|
|
117
|
+
id: entry.id,
|
|
118
|
+
kind: "item",
|
|
119
|
+
at: entry.at,
|
|
120
|
+
finalized: entry.item?.status !== "inProgress",
|
|
121
|
+
item: entry.item,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (const run of runs.values()) {
|
|
126
|
+
const status = resolveCompactRunStatus(run.run, run.items);
|
|
127
|
+
rows.push({
|
|
128
|
+
id: run.id,
|
|
129
|
+
kind: "run",
|
|
130
|
+
at: run.at,
|
|
131
|
+
finalized: status !== "running",
|
|
132
|
+
run: { ...run.run, status, ...(run.endedAt ? { endedAt: run.endedAt } : {}) },
|
|
133
|
+
details: summarizeRunDetails(run.items, status),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
rows.sort((left, right) => {
|
|
137
|
+
const cmp = left.at.localeCompare(right.at);
|
|
138
|
+
if (cmp !== 0)
|
|
139
|
+
return cmp;
|
|
140
|
+
const kindCmp = rowKindOrder(left.kind) - rowKindOrder(right.kind);
|
|
141
|
+
if (kindCmp !== 0)
|
|
142
|
+
return kindCmp;
|
|
143
|
+
return left.id.localeCompare(right.id);
|
|
144
|
+
});
|
|
145
|
+
return rows;
|
|
146
|
+
}
|
|
147
|
+
function shouldHideFeedInCompact(feed) {
|
|
148
|
+
if (feed.feedKind === "stage" && feed.status === "starting") {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
if (feed.feedKind === "turn" && (feed.status === "completed" || feed.status === "failed")) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
function resolveCompactRunStatus(run, items) {
|
|
157
|
+
if (run.endedAt || run.status === "completed" || run.status === "failed" || run.status === "released") {
|
|
158
|
+
return run.status;
|
|
159
|
+
}
|
|
160
|
+
if (items.some((item) => item.status === "inProgress")) {
|
|
161
|
+
return "running";
|
|
162
|
+
}
|
|
163
|
+
return run.status === "queued" ? "queued" : "running";
|
|
164
|
+
}
|
|
165
|
+
function summarizeRunDetails(items, status) {
|
|
166
|
+
const details = [];
|
|
167
|
+
const latestAgentMessage = findLatest(items, (item) => item.type === "agentMessage" && Boolean(item.text?.trim()));
|
|
168
|
+
const latestUserMessage = findLatest(items, (item) => item.type === "userMessage" && Boolean(item.text?.trim()));
|
|
169
|
+
const activeCommand = findLatest(items, (item) => item.type === "commandExecution" && item.status === "inProgress");
|
|
170
|
+
const latestCommand = activeCommand ?? findLatest(items, (item) => item.type === "commandExecution" && Boolean(item.command?.trim()));
|
|
171
|
+
const latestFileChange = findLatest(items, (item) => item.type === "fileChange" && Array.isArray(item.changes) && item.changes.length > 0);
|
|
172
|
+
if (latestUserMessage && !latestAgentMessage) {
|
|
173
|
+
details.push({
|
|
174
|
+
tone: "user",
|
|
175
|
+
text: `you: ${summarizeNarrative(latestUserMessage.text ?? "", 120)}`,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (latestAgentMessage) {
|
|
179
|
+
details.push({
|
|
180
|
+
tone: "message",
|
|
181
|
+
text: summarizeNarrative(latestAgentMessage.text ?? "", status === "running" ? 140 : 180),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (latestCommand?.command) {
|
|
185
|
+
details.push({
|
|
186
|
+
tone: "command",
|
|
187
|
+
text: cleanCommand(latestCommand.command),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (latestFileChange?.changes?.length) {
|
|
191
|
+
details.push({
|
|
192
|
+
tone: "meta",
|
|
193
|
+
text: summarizeFileChanges(latestFileChange.changes),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
const tools = summarizeToolCalls(items);
|
|
198
|
+
if (tools) {
|
|
199
|
+
details.push({
|
|
200
|
+
tone: "meta",
|
|
201
|
+
text: tools,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return dedupeDetails(details).slice(0, 3);
|
|
206
|
+
}
|
|
207
|
+
function summarizeNarrative(input, max) {
|
|
208
|
+
const normalized = input
|
|
209
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
210
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
211
|
+
.replace(/\s+/g, " ")
|
|
212
|
+
.trim();
|
|
213
|
+
if (!normalized)
|
|
214
|
+
return "";
|
|
215
|
+
const sentence = normalized.match(/^(.+?[.!?])(?:\s|$)/)?.[1] ?? normalized;
|
|
216
|
+
return truncate(sentence, max);
|
|
217
|
+
}
|
|
218
|
+
function summarizeFileChanges(changes) {
|
|
219
|
+
const files = Array.from(new Set(changes
|
|
220
|
+
.map((change) => {
|
|
221
|
+
if (!change || typeof change !== "object")
|
|
222
|
+
return undefined;
|
|
223
|
+
const path = change.path;
|
|
224
|
+
return typeof path === "string" && path.trim() ? path : undefined;
|
|
225
|
+
})
|
|
226
|
+
.filter((path) => Boolean(path))));
|
|
227
|
+
if (files.length === 0) {
|
|
228
|
+
return `updated ${changes.length} file${changes.length === 1 ? "" : "s"}`;
|
|
229
|
+
}
|
|
230
|
+
const names = files.map((path) => path.split("/").at(-1) ?? path);
|
|
231
|
+
const preview = names.slice(0, 3).join(", ");
|
|
232
|
+
const remainder = names.length > 3 ? ` +${names.length - 3}` : "";
|
|
233
|
+
return `updated ${files.length} file${files.length === 1 ? "" : "s"}: ${preview}${remainder}`;
|
|
234
|
+
}
|
|
235
|
+
function summarizeToolCalls(items) {
|
|
236
|
+
const names = Array.from(new Set(items
|
|
237
|
+
.filter((item) => item.type === "mcpToolCall" || item.type === "dynamicToolCall")
|
|
238
|
+
.map((item) => item.toolName)
|
|
239
|
+
.filter((name) => Boolean(name))));
|
|
240
|
+
if (names.length === 0)
|
|
241
|
+
return undefined;
|
|
242
|
+
const preview = names.slice(0, 2).join(", ");
|
|
243
|
+
const remainder = names.length > 2 ? ` +${names.length - 2}` : "";
|
|
244
|
+
return `used ${names.length} tool${names.length === 1 ? "" : "s"}: ${preview}${remainder}`;
|
|
245
|
+
}
|
|
246
|
+
function dedupeDetails(details) {
|
|
247
|
+
const seen = new Set();
|
|
248
|
+
return details.filter((detail) => {
|
|
249
|
+
const key = `${detail.tone}:${detail.text.toLowerCase()}`;
|
|
250
|
+
if (!detail.text.trim() || seen.has(key)) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
seen.add(key);
|
|
254
|
+
return true;
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
function findLatest(items, predicate) {
|
|
258
|
+
for (let i = items.length - 1; i >= 0; i -= 1) {
|
|
259
|
+
const item = items[i];
|
|
260
|
+
if (predicate(item)) {
|
|
261
|
+
return item;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
function rowKindOrder(kind) {
|
|
267
|
+
switch (kind) {
|
|
268
|
+
case "run":
|
|
269
|
+
return 0;
|
|
270
|
+
case "feed":
|
|
271
|
+
return 1;
|
|
272
|
+
case "ci-checks":
|
|
273
|
+
return 2;
|
|
274
|
+
case "item":
|
|
275
|
+
return 3;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function truncate(text, max) {
|
|
279
|
+
return text.length > max ? `${text.slice(0, Math.max(0, max - 3))}...` : text;
|
|
280
|
+
}
|
|
281
|
+
function cleanCommand(raw) {
|
|
282
|
+
const bashMatch = raw.match(/^\/bin\/(?:ba)?sh\s+-\w*c\s+['"](.+?)['"]$/s);
|
|
283
|
+
if (bashMatch?.[1])
|
|
284
|
+
return truncate(bashMatch[1], 120);
|
|
285
|
+
const bashMatch2 = raw.match(/^\/bin\/(?:ba)?sh\s+-\w*c\s+"(.+?)"$/s);
|
|
286
|
+
if (bashMatch2?.[1])
|
|
287
|
+
return truncate(bashMatch2[1], 120);
|
|
288
|
+
return truncate(raw, 120);
|
|
289
|
+
}
|
|
@@ -36,6 +36,7 @@ async function rehydrate(baseUrl, issueKey, headers, signal, dispatch) {
|
|
|
36
36
|
startedAt: r.startedAt,
|
|
37
37
|
endedAt: r.endedAt,
|
|
38
38
|
threadId: r.threadId,
|
|
39
|
+
...(r.events ? { events: r.events } : {}),
|
|
39
40
|
...(r.report ? { report: r.report } : {}),
|
|
40
41
|
}));
|
|
41
42
|
let issueContext = null;
|
|
@@ -8,6 +8,7 @@ function capArray(arr, max) {
|
|
|
8
8
|
}
|
|
9
9
|
const DETAIL_INITIAL = {
|
|
10
10
|
detailTab: "timeline",
|
|
11
|
+
timelineMode: "compact",
|
|
11
12
|
timeline: [],
|
|
12
13
|
rawRuns: [],
|
|
13
14
|
rawFeedEvents: [],
|
|
@@ -129,6 +130,8 @@ export function watchReducer(state, action) {
|
|
|
129
130
|
return { ...state, feedEvents: capArray([...state.feedEvents, action.event], MAX_FEED_EVENTS) };
|
|
130
131
|
case "switch-detail-tab":
|
|
131
132
|
return { ...state, detailTab: action.tab };
|
|
133
|
+
case "toggle-timeline-mode":
|
|
134
|
+
return { ...state, timelineMode: state.timelineMode === "compact" ? "verbose" : "compact" };
|
|
132
135
|
}
|
|
133
136
|
}
|
|
134
137
|
// ─── Feed Event → Issue List + Timeline ───────────────────────────
|
|
@@ -70,6 +70,12 @@ export class IssueQueryService {
|
|
|
70
70
|
startedAt: run.startedAt,
|
|
71
71
|
endedAt: run.endedAt,
|
|
72
72
|
threadId: run.threadId,
|
|
73
|
+
events: this.db.listThreadEvents(run.id).map((event) => ({
|
|
74
|
+
id: event.id,
|
|
75
|
+
method: event.method,
|
|
76
|
+
createdAt: event.createdAt,
|
|
77
|
+
parsedEvent: safeJsonParse(event.eventJson),
|
|
78
|
+
})),
|
|
73
79
|
...(run.reportJson ? { report: JSON.parse(run.reportJson) } : {}),
|
|
74
80
|
}));
|
|
75
81
|
const feedEvents = this.db.operatorFeed.list({ issueKey, limit: 500 });
|