patchrelay 0.45.0 → 0.46.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 +61 -28
- package/dist/cli/watch/HelpBar.js +25 -26
- package/dist/cli/watch/IssueDetailView.js +3 -3
- package/dist/cli/watch/IssueListView.js +14 -32
- package/dist/cli/watch/IssueRow.js +19 -204
- package/dist/cli/watch/LogView.js +74 -0
- package/dist/cli/watch/StatusBar.js +3 -27
- package/dist/cli/watch/codex-log-rows.js +82 -0
- package/dist/cli/watch/detail-rows.js +56 -574
- package/dist/cli/watch/event-log-rows.js +119 -0
- package/dist/cli/watch/issue-token.js +81 -0
- package/dist/cli/watch/watch-actions.js +9 -5
- package/dist/cli/watch/watch-state.js +8 -0
- package/package.json +2 -2
|
@@ -1,597 +1,79 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import { hasDisplayPrBlocker, isApprovedReviewState, isAwaitingReviewState, isChangesRequestedReviewState, isRereviewNeeded, prChecksFact, } from "./pr-status.js";
|
|
7
|
-
import { renderRichTextLines, renderTextLines } from "./render-rich-text.js";
|
|
8
|
-
const SESSION_DISPLAY = {
|
|
9
|
-
idle: { label: "idle", color: "blueBright" },
|
|
10
|
-
running: { label: "running", color: "cyan" },
|
|
11
|
-
waiting_input: { label: "needs input", color: "yellow" },
|
|
12
|
-
done: { label: "done", color: "green" },
|
|
13
|
-
failed: { label: "needs help", color: "red" },
|
|
14
|
-
};
|
|
15
|
-
const STAGE_DISPLAY = {
|
|
16
|
-
blocked: "blocked",
|
|
17
|
-
ready: "ready",
|
|
18
|
-
delegated: "delegated",
|
|
19
|
-
implementing: "implementing",
|
|
20
|
-
completion_check: "completion check",
|
|
21
|
-
pr_open: "PR open",
|
|
22
|
-
changes_requested: "review changes",
|
|
23
|
-
repairing_ci: "repairing CI",
|
|
24
|
-
awaiting_queue: "waiting downstream",
|
|
25
|
-
repairing_queue: "repairing queue",
|
|
26
|
-
done: "merged",
|
|
27
|
-
failed: "failed",
|
|
28
|
-
escalated: "escalated",
|
|
29
|
-
awaiting_input: "needs input",
|
|
30
|
-
};
|
|
31
|
-
const RUN_LABELS = {
|
|
32
|
-
implementation: "implementation",
|
|
33
|
-
ci_repair: "ci repair",
|
|
34
|
-
review_fix: "review fix",
|
|
35
|
-
branch_upkeep: "branch upkeep",
|
|
36
|
-
queue_repair: "queue repair",
|
|
37
|
-
};
|
|
38
|
-
const STATE_LABELS = {
|
|
39
|
-
delegated: "delegated",
|
|
40
|
-
implementing: "implementing",
|
|
41
|
-
pr_open: "pr open",
|
|
42
|
-
changes_requested: "changes requested",
|
|
43
|
-
repairing_ci: "repairing ci",
|
|
44
|
-
awaiting_queue: "awaiting queue",
|
|
45
|
-
repairing_queue: "repairing queue",
|
|
46
|
-
awaiting_input: "awaiting input",
|
|
47
|
-
escalated: "escalated",
|
|
48
|
-
done: "done",
|
|
49
|
-
failed: "failed",
|
|
50
|
-
};
|
|
1
|
+
import { issueTokenFor, prTokenFor } from "./issue-token.js";
|
|
2
|
+
import { buildEventLogLines, formatEventAge } from "./event-log-rows.js";
|
|
3
|
+
import { renderTextLines } from "./render-rich-text.js";
|
|
4
|
+
const AGE_WIDTH = 4;
|
|
5
|
+
const CATEGORY_WIDTH = 7;
|
|
51
6
|
export function buildDetailLines(input) {
|
|
52
7
|
const width = Math.max(20, input.width);
|
|
53
8
|
const lines = [];
|
|
54
|
-
|
|
55
|
-
lines.push(...buildHeaderLines(input, width));
|
|
9
|
+
lines.push(...buildHeaderLines(input.issue, width));
|
|
56
10
|
lines.push(blankLine("header-gap"));
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (input.plan?.length) {
|
|
60
|
-
lines.push(blankLine("plan-gap"));
|
|
61
|
-
}
|
|
62
|
-
lines.push(...buildTimelineLines(input.timeline, width));
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
lines.push(...buildHistoryIntroLines(width));
|
|
66
|
-
lines.push(blankLine("history-intro-gap"));
|
|
67
|
-
lines.push(...buildHistoryLines(history, input.plan, input.activeRunId, width));
|
|
68
|
-
}
|
|
69
|
-
return trimTrailingBlankLines(lines);
|
|
11
|
+
lines.push(...buildEventLines({ rawRuns: input.rawRuns, rawFeedEvents: input.rawFeedEvents }, width));
|
|
12
|
+
return lines;
|
|
70
13
|
}
|
|
71
|
-
function buildHeaderLines(
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
{ text:
|
|
14
|
+
function buildHeaderLines(issue, width) {
|
|
15
|
+
const token = issueTokenFor(issue);
|
|
16
|
+
const pr = prTokenFor(issue);
|
|
17
|
+
const key = issue.issueKey ?? issue.projectId;
|
|
18
|
+
const segments = [
|
|
19
|
+
{ text: key, color: token.color, bold: true },
|
|
20
|
+
{ text: " " },
|
|
21
|
+
{ text: token.glyph, color: token.color },
|
|
79
22
|
{ text: " " },
|
|
80
|
-
{ text:
|
|
81
|
-
{ text: " ", dimColor: true },
|
|
82
|
-
{ text: stage, dimColor: true },
|
|
23
|
+
{ text: token.phrase },
|
|
83
24
|
];
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
25
|
+
if (pr) {
|
|
26
|
+
segments.push({ text: " " });
|
|
27
|
+
segments.push({ text: `#${pr.prNumber} ${pr.glyph}`, color: pr.color });
|
|
87
28
|
}
|
|
88
|
-
if (meta.length > 0) {
|
|
89
|
-
headerSegments.push({ text: " ", dimColor: true });
|
|
90
|
-
headerSegments.push({ text: meta.join(" "), dimColor: true });
|
|
91
|
-
}
|
|
92
|
-
const lines = renderTextLines(segmentsToText(headerSegments), {
|
|
93
|
-
key: "detail-header",
|
|
94
|
-
width,
|
|
95
|
-
});
|
|
96
|
-
lines[0] = { key: lines[0].key, segments: headerSegments };
|
|
97
29
|
if (issue.title) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
width,
|
|
101
|
-
style: { bold: true },
|
|
102
|
-
}));
|
|
103
|
-
}
|
|
104
|
-
const blocker = blockerText(issue, input.issueContext);
|
|
105
|
-
if (blocker) {
|
|
106
|
-
lines.push(...renderTextLines(blocker, {
|
|
107
|
-
key: "detail-blocker",
|
|
108
|
-
width,
|
|
109
|
-
style: { color: "yellow" },
|
|
110
|
-
}));
|
|
111
|
-
}
|
|
112
|
-
if (issue.statusNote && issue.statusNote !== blocker) {
|
|
113
|
-
lines.push(...renderRichTextLines(issue.statusNote, {
|
|
114
|
-
key: "detail-note",
|
|
115
|
-
width,
|
|
116
|
-
}));
|
|
30
|
+
segments.push({ text: " " });
|
|
31
|
+
segments.push({ text: issue.title, dimColor: true });
|
|
117
32
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
lines.push(...renderTextLines(`${failurePrefix}${input.issueContext.latestFailureSummary}${head}`, {
|
|
122
|
-
key: "detail-failure",
|
|
123
|
-
width,
|
|
124
|
-
style: { color: input.issueContext.latestFailureSource === "queue_eviction" ? "yellow" : "red" },
|
|
125
|
-
}));
|
|
33
|
+
const plainLines = renderTextLines(segmentsToText(segments), { key: "detail-header", width });
|
|
34
|
+
if (plainLines.length > 0) {
|
|
35
|
+
plainLines[0] = { key: plainLines[0].key, segments };
|
|
126
36
|
}
|
|
127
|
-
return
|
|
37
|
+
return plainLines;
|
|
128
38
|
}
|
|
129
|
-
function
|
|
130
|
-
|
|
39
|
+
function buildEventLines(source, width) {
|
|
40
|
+
const events = buildEventLogLines(source);
|
|
41
|
+
if (events.length === 0)
|
|
131
42
|
return [];
|
|
132
|
-
const completed = plan.filter((step) => step.status === "completed").length;
|
|
133
|
-
const lines = renderTextLines(`Plan ${progressBar(completed, plan.length, 16)} ${completed}/${plan.length}`, {
|
|
134
|
-
key: "detail-plan-header",
|
|
135
|
-
width,
|
|
136
|
-
style: { dimColor: true },
|
|
137
|
-
});
|
|
138
|
-
for (const [index, entry] of plan.entries()) {
|
|
139
|
-
lines.push({
|
|
140
|
-
key: `detail-plan-${index}`,
|
|
141
|
-
segments: [
|
|
142
|
-
{ text: `[${planStepSymbol(entry.status)}]`, color: planStepColor(entry.status) },
|
|
143
|
-
{ text: " " },
|
|
144
|
-
{ text: entry.step },
|
|
145
|
-
],
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
return lines;
|
|
149
|
-
}
|
|
150
|
-
function buildTimelineLines(entries, width) {
|
|
151
|
-
const rows = buildTimelineRows(entries);
|
|
152
|
-
if (rows.length === 0) {
|
|
153
|
-
return renderTextLines("No timeline events yet.", {
|
|
154
|
-
key: "timeline-empty",
|
|
155
|
-
width,
|
|
156
|
-
style: { dimColor: true },
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
43
|
const lines = [];
|
|
160
|
-
for (const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
key: row.id,
|
|
172
|
-
width,
|
|
173
|
-
style: { color: row.ciChecks.overall === "failed" ? "red" : row.ciChecks.overall === "passed" ? "green" : "yellow", bold: true },
|
|
174
|
-
}));
|
|
175
|
-
break;
|
|
176
|
-
case "item":
|
|
177
|
-
lines.push(...renderTimelineItemLines(row.id, row.item, width, 0));
|
|
178
|
-
break;
|
|
179
|
-
case "run":
|
|
180
|
-
lines.push(...buildTimelineRunLines(row.id, row.run, row.items.map((item) => item.item), row.details, width));
|
|
181
|
-
break;
|
|
182
|
-
}
|
|
183
|
-
lines.push(blankLine(`${row.id}-gap`));
|
|
184
|
-
}
|
|
185
|
-
return trimTrailingBlankLines(lines);
|
|
186
|
-
}
|
|
187
|
-
function buildTimelineRunLines(key, run, items, details, width) {
|
|
188
|
-
const statusColor = run.status === "completed" ? "green" : run.status === "failed" ? "red" : run.status === "running" ? "yellow" : "white";
|
|
189
|
-
const headerText = `● ${RUN_LABELS[run.runType] ?? run.runType} ${run.status}${run.endedAt ? ` ${formatDuration(run.startedAt, run.endedAt)}` : ""}`;
|
|
190
|
-
const lines = renderTextLines(headerText, {
|
|
191
|
-
key: `${key}-header`,
|
|
192
|
-
width,
|
|
193
|
-
style: { color: statusColor, bold: true },
|
|
194
|
-
});
|
|
195
|
-
const showVerboseItems = run.status === "running";
|
|
196
|
-
if (showVerboseItems) {
|
|
197
|
-
for (const item of items) {
|
|
198
|
-
lines.push(...renderTimelineItemLines(`${key}-${item.id}`, item, width, 2));
|
|
199
|
-
}
|
|
200
|
-
return lines;
|
|
201
|
-
}
|
|
202
|
-
for (const [index, detail] of details.entries()) {
|
|
203
|
-
if (detail.tone === "message" || detail.tone === "user") {
|
|
204
|
-
lines.push(...renderRichTextLines(detail.tone === "user" ? `you: ${detail.text}` : detail.text, {
|
|
205
|
-
key: `${key}-detail-${index}`,
|
|
206
|
-
width,
|
|
207
|
-
firstPrefix: [{ text: " " }],
|
|
208
|
-
continuationPrefix: [{ text: " " }],
|
|
209
|
-
style: { color: detail.tone === "user" ? "yellow" : undefined },
|
|
210
|
-
}));
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
lines.push(...renderTextLines(`${detail.tone === "command" ? "$ " : ""}${detail.text}`, {
|
|
214
|
-
key: `${key}-detail-${index}`,
|
|
215
|
-
width,
|
|
216
|
-
firstPrefix: [{ text: " " }],
|
|
217
|
-
continuationPrefix: [{ text: " " }],
|
|
218
|
-
style: detail.tone === "command" ? { color: "white" } : { dimColor: true },
|
|
219
|
-
}));
|
|
220
|
-
}
|
|
221
|
-
return lines;
|
|
222
|
-
}
|
|
223
|
-
function renderTimelineItemLines(key, item, width, indent) {
|
|
224
|
-
const prefix = [{ text: " ".repeat(indent) }];
|
|
225
|
-
if (item.type === "agentMessage" || item.type === "userMessage" || item.type === "plan" || item.type === "reasoning") {
|
|
226
|
-
return renderRichTextLines(item.type === "userMessage" ? `you: ${item.text ?? ""}` : item.text ?? "", {
|
|
227
|
-
key,
|
|
228
|
-
width,
|
|
229
|
-
firstPrefix: prefix,
|
|
230
|
-
continuationPrefix: prefix,
|
|
231
|
-
style: item.type === "userMessage" ? { color: "yellow" } : undefined,
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
const summary = itemSummary(item);
|
|
235
|
-
const lines = renderTextLines(summary, {
|
|
236
|
-
key,
|
|
237
|
-
width,
|
|
238
|
-
firstPrefix: prefix,
|
|
239
|
-
continuationPrefix: prefix,
|
|
240
|
-
style: item.status === "failed" || item.status === "declined"
|
|
241
|
-
? { color: "red" }
|
|
242
|
-
: item.status === "inProgress"
|
|
243
|
-
? { color: "yellow" }
|
|
244
|
-
: item.type === "commandExecution"
|
|
245
|
-
? { color: "white" }
|
|
246
|
-
: { dimColor: item.type !== "commandExecution" },
|
|
247
|
-
});
|
|
248
|
-
if (item.output && item.type === "commandExecution") {
|
|
249
|
-
lines.push(...renderTextLines(lastNonEmptyLine(item.output), {
|
|
250
|
-
key: `${key}-output`,
|
|
251
|
-
width,
|
|
252
|
-
firstPrefix: [{ text: `${" ".repeat(indent + 2)}` }],
|
|
253
|
-
continuationPrefix: [{ text: `${" ".repeat(indent + 2)}` }],
|
|
254
|
-
style: { dimColor: true },
|
|
255
|
-
}));
|
|
256
|
-
}
|
|
257
|
-
return lines;
|
|
258
|
-
}
|
|
259
|
-
function buildHistoryIntroLines(width) {
|
|
260
|
-
return [
|
|
261
|
-
...renderTextLines("PatchRelay activity history.", {
|
|
262
|
-
key: "history-intro-1",
|
|
263
|
-
width,
|
|
264
|
-
style: { dimColor: true },
|
|
265
|
-
}),
|
|
266
|
-
...renderTextLines("Runs, waits, and wake-ups are shown here in PatchRelay order.", {
|
|
267
|
-
key: "history-intro-2",
|
|
268
|
-
width,
|
|
269
|
-
style: { dimColor: true },
|
|
270
|
-
}),
|
|
271
|
-
];
|
|
272
|
-
}
|
|
273
|
-
function buildHistoryLines(history, plan, activeRunId, width) {
|
|
274
|
-
if (history.length === 0) {
|
|
275
|
-
return renderTextLines("No state history available.", {
|
|
276
|
-
key: "history-empty",
|
|
277
|
-
width,
|
|
278
|
-
style: { dimColor: true },
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
const lines = [];
|
|
282
|
-
let runCounter = 0;
|
|
283
|
-
for (const [nodeIndex, node] of history.entries()) {
|
|
284
|
-
lines.push(...renderTextLines(`${node.isCurrent ? "◉" : "○"} ${STATE_LABELS[node.state] ?? node.state} ${formatTime(node.enteredAt)}`, {
|
|
285
|
-
key: `history-node-${nodeIndex}`,
|
|
286
|
-
width,
|
|
287
|
-
style: { color: node.isCurrent ? "green" : undefined, bold: node.isCurrent },
|
|
288
|
-
}));
|
|
289
|
-
if (node.reason) {
|
|
290
|
-
lines.push(...renderRichTextLines(node.reason, {
|
|
291
|
-
key: `history-node-${nodeIndex}-reason`,
|
|
292
|
-
width,
|
|
293
|
-
firstPrefix: [{ text: "│ ", dimColor: true }],
|
|
294
|
-
continuationPrefix: [{ text: "│ ", dimColor: true }],
|
|
44
|
+
for (const event of events) {
|
|
45
|
+
lines.push(renderEventLine(event));
|
|
46
|
+
if (event.continuation) {
|
|
47
|
+
const continuation = event.continuation;
|
|
48
|
+
const indent = AGE_WIDTH + 2 + CATEGORY_WIDTH + 2;
|
|
49
|
+
const wrapWidth = Math.max(20, width - indent);
|
|
50
|
+
const wrapped = renderTextLines(continuation, {
|
|
51
|
+
key: `${event.id}-cont`,
|
|
52
|
+
width: wrapWidth,
|
|
53
|
+
firstPrefix: [{ text: " ".repeat(indent) }],
|
|
54
|
+
continuationPrefix: [{ text: " ".repeat(indent) }],
|
|
295
55
|
style: { dimColor: true },
|
|
296
|
-
})
|
|
56
|
+
});
|
|
57
|
+
lines.push(...wrapped);
|
|
297
58
|
}
|
|
298
|
-
if (node.runs.length > 5) {
|
|
299
|
-
lines.push(...renderTextLines(historyRunSummary(node.runs), {
|
|
300
|
-
key: `history-node-${nodeIndex}-summary`,
|
|
301
|
-
width,
|
|
302
|
-
firstPrefix: [{ text: "│ ", dimColor: true }],
|
|
303
|
-
continuationPrefix: [{ text: "│ ", dimColor: true }],
|
|
304
|
-
style: { dimColor: true },
|
|
305
|
-
}));
|
|
306
|
-
}
|
|
307
|
-
for (const run of node.runs) {
|
|
308
|
-
lines.push(...renderHistoryRunLines(run, runCounter, width, "│ "));
|
|
309
|
-
if (run.id === activeRunId && plan?.length) {
|
|
310
|
-
for (const [index, entry] of plan.entries()) {
|
|
311
|
-
lines.push({
|
|
312
|
-
key: `history-run-${run.id}-plan-${index}`,
|
|
313
|
-
segments: [
|
|
314
|
-
{ text: "│ ", dimColor: true },
|
|
315
|
-
{ text: `[${planStepSymbol(entry.status)}]`, color: planStepColor(entry.status) },
|
|
316
|
-
{ text: " " },
|
|
317
|
-
{ text: entry.step },
|
|
318
|
-
],
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
runCounter += 1;
|
|
323
|
-
}
|
|
324
|
-
for (const [tripIndex, trip] of node.sideTrips.entries()) {
|
|
325
|
-
lines.push(...renderSideTripLines(trip, runCounter, width));
|
|
326
|
-
runCounter += trip.runs.length;
|
|
327
|
-
if (tripIndex < node.sideTrips.length - 1) {
|
|
328
|
-
lines.push({ key: `history-trip-gap-${nodeIndex}-${tripIndex}`, segments: [{ text: "│", dimColor: true }] });
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
if (nodeIndex < history.length - 1) {
|
|
332
|
-
lines.push({ key: `history-node-connector-${nodeIndex}`, segments: [{ text: "│", dimColor: true }] });
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
return lines;
|
|
336
|
-
}
|
|
337
|
-
function renderHistoryRunLines(run, index, width, gutter) {
|
|
338
|
-
const statusColor = run.status === "completed" ? "green" : run.status === "failed" ? "red" : run.status === "running" ? "yellow" : "white";
|
|
339
|
-
const duration = run.endedAt ? formatDuration(run.startedAt, run.endedAt) : undefined;
|
|
340
|
-
const stats = [
|
|
341
|
-
run.messageCount !== undefined ? `${run.messageCount} msgs` : null,
|
|
342
|
-
run.commandCount ? `${run.commandCount} cmds` : null,
|
|
343
|
-
run.fileChangeCount ? `${run.fileChangeCount} files` : null,
|
|
344
|
-
].filter((value) => Boolean(value));
|
|
345
|
-
const lines = renderTextLines(`${gutter}${run.status === "completed" ? "✓" : run.status === "failed" ? "✗" : run.status === "running" ? "▶" : "•"} #${index + 1} (${RUN_LABELS[run.runType] ?? run.runType})${duration ? ` ${duration}` : ""}${stats.length ? ` ${stats.join(", ")}` : ""}`, {
|
|
346
|
-
key: `history-run-${run.id}`,
|
|
347
|
-
width,
|
|
348
|
-
style: { color: statusColor },
|
|
349
|
-
});
|
|
350
|
-
if (run.lastMessage) {
|
|
351
|
-
lines.push(...renderRichTextLines(run.lastMessage, {
|
|
352
|
-
key: `history-run-${run.id}-message`,
|
|
353
|
-
width,
|
|
354
|
-
firstPrefix: [{ text: `${gutter} `, dimColor: true }],
|
|
355
|
-
continuationPrefix: [{ text: `${gutter} `, dimColor: true }],
|
|
356
|
-
style: { dimColor: true },
|
|
357
|
-
}));
|
|
358
|
-
}
|
|
359
|
-
return lines;
|
|
360
|
-
}
|
|
361
|
-
function renderSideTripLines(trip, runOffset, width) {
|
|
362
|
-
const lines = renderTextLines(`│ ┌ ${STATE_LABELS[trip.state] ?? trip.state} ${formatTime(trip.enteredAt)}`, {
|
|
363
|
-
key: `history-trip-${trip.state}-${trip.enteredAt}`,
|
|
364
|
-
width,
|
|
365
|
-
style: { color: "magenta", bold: true },
|
|
366
|
-
});
|
|
367
|
-
if (trip.reason) {
|
|
368
|
-
lines.push(...renderRichTextLines(trip.reason, {
|
|
369
|
-
key: `history-trip-${trip.state}-${trip.enteredAt}-reason`,
|
|
370
|
-
width,
|
|
371
|
-
firstPrefix: [{ text: "│ │ ", dimColor: true }],
|
|
372
|
-
continuationPrefix: [{ text: "│ │ ", dimColor: true }],
|
|
373
|
-
style: { dimColor: true },
|
|
374
|
-
}));
|
|
375
|
-
}
|
|
376
|
-
for (const [index, run] of trip.runs.entries()) {
|
|
377
|
-
lines.push(...renderHistoryRunLines(run, runOffset + index, width, "│ │ "));
|
|
378
59
|
}
|
|
379
|
-
const returnLabel = trip.returnedAt
|
|
380
|
-
? `│ └→ ${STATE_LABELS[trip.returnState] ?? trip.returnState} ${formatTime(trip.returnedAt)}`
|
|
381
|
-
: "│ └─ (active)";
|
|
382
|
-
lines.push(...renderTextLines(returnLabel, {
|
|
383
|
-
key: `history-trip-${trip.state}-${trip.enteredAt}-return`,
|
|
384
|
-
width,
|
|
385
|
-
style: { dimColor: !trip.returnedAt },
|
|
386
|
-
}));
|
|
387
60
|
return lines;
|
|
388
61
|
}
|
|
389
|
-
function
|
|
390
|
-
const
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
else if (hasOpenPr(issue.prNumber, issue.prState)
|
|
401
|
-
&& (isAwaitingReviewState(issue.prReviewState) || (!issue.prReviewState && issue.factoryState === "pr_open")))
|
|
402
|
-
facts.push([{ text: "awaiting review", color: "yellow" }]);
|
|
403
|
-
if (issue.factoryState === "awaiting_queue")
|
|
404
|
-
facts.push([{ text: "downstream ready", color: "cyan" }]);
|
|
405
|
-
if (issue.waitingReason && issue.sessionState === "waiting_input")
|
|
406
|
-
facts.push([{ text: issue.waitingReason, color: "yellow" }]);
|
|
407
|
-
const checks = prChecksFact({
|
|
408
|
-
...issue,
|
|
409
|
-
latestFailureCheckName: issueContext?.latestFailureCheckName ?? issue.latestFailureCheckName,
|
|
410
|
-
});
|
|
411
|
-
if (checks) {
|
|
412
|
-
facts.push([{ text: checks.text, color: checks.color }]);
|
|
413
|
-
}
|
|
414
|
-
return facts;
|
|
415
|
-
}
|
|
416
|
-
function joinFactSegments(facts) {
|
|
417
|
-
const segments = [];
|
|
418
|
-
for (const [index, fact] of facts.entries()) {
|
|
419
|
-
if (index > 0) {
|
|
420
|
-
segments.push({ text: " · ", dimColor: true });
|
|
421
|
-
}
|
|
422
|
-
segments.push(...fact);
|
|
423
|
-
}
|
|
424
|
-
return segments;
|
|
425
|
-
}
|
|
426
|
-
function buildMeta(tokenUsage, diffSummary, issueContext) {
|
|
427
|
-
const meta = [];
|
|
428
|
-
if (tokenUsage)
|
|
429
|
-
meta.push(`${formatTokens(tokenUsage.inputTokens)} in / ${formatTokens(tokenUsage.outputTokens)} out`);
|
|
430
|
-
if (diffSummary && diffSummary.filesChanged > 0)
|
|
431
|
-
meta.push(`${diffSummary.filesChanged}f +${diffSummary.linesAdded} -${diffSummary.linesRemoved}`);
|
|
432
|
-
if (issueContext?.runCount)
|
|
433
|
-
meta.push(`${issueContext.runCount} runs`);
|
|
434
|
-
return meta;
|
|
435
|
-
}
|
|
436
|
-
function formatTokens(value) {
|
|
437
|
-
if (value >= 1_000_000)
|
|
438
|
-
return `${(value / 1_000_000).toFixed(1)}M`;
|
|
439
|
-
if (value >= 1_000)
|
|
440
|
-
return `${(value / 1_000).toFixed(1)}k`;
|
|
441
|
-
return String(value);
|
|
442
|
-
}
|
|
443
|
-
function formatDuration(startedAt, endedAt) {
|
|
444
|
-
const ms = new Date(endedAt).getTime() - new Date(startedAt).getTime();
|
|
445
|
-
const seconds = Math.floor(ms / 1000);
|
|
446
|
-
if (seconds < 60)
|
|
447
|
-
return `${seconds}s`;
|
|
448
|
-
const minutes = Math.floor(seconds / 60);
|
|
449
|
-
const remainder = seconds % 60;
|
|
450
|
-
return `${minutes}m${remainder > 0 ? ` ${String(remainder).padStart(2, "0")}s` : ""}`;
|
|
451
|
-
}
|
|
452
|
-
function formatTime(iso) {
|
|
453
|
-
return new Date(iso).toLocaleTimeString("en-GB", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
454
|
-
}
|
|
455
|
-
function itemSummary(item) {
|
|
456
|
-
switch (item.type) {
|
|
457
|
-
case "commandExecution": {
|
|
458
|
-
const exit = item.exitCode !== undefined && item.exitCode !== 0 ? ` exit ${item.exitCode}` : "";
|
|
459
|
-
const duration = item.durationMs && item.durationMs >= 1000 ? ` ${Math.floor(item.durationMs / 1000)}s` : "";
|
|
460
|
-
return `$ ${cleanCommand(item.command ?? "?")}${exit}${duration}`;
|
|
461
|
-
}
|
|
462
|
-
case "fileChange":
|
|
463
|
-
return summarizeFileChanges(item.changes ?? []);
|
|
464
|
-
case "mcpToolCall":
|
|
465
|
-
case "dynamicToolCall":
|
|
466
|
-
return `used ${item.toolName ?? item.type}`;
|
|
467
|
-
default:
|
|
468
|
-
return (item.text ?? item.type).replace(/\s+/g, " ").trim();
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
function summarizeFileChanges(changes) {
|
|
472
|
-
const files = Array.from(new Set(changes
|
|
473
|
-
.map((change) => {
|
|
474
|
-
if (!change || typeof change !== "object")
|
|
475
|
-
return undefined;
|
|
476
|
-
const path = change.path;
|
|
477
|
-
return typeof path === "string" && path.trim().length > 0 ? path : undefined;
|
|
478
|
-
})
|
|
479
|
-
.filter((path) => Boolean(path))));
|
|
480
|
-
if (files.length === 0) {
|
|
481
|
-
return `updated ${changes.length} file${changes.length === 1 ? "" : "s"}`;
|
|
482
|
-
}
|
|
483
|
-
const names = files.map((path) => path.split("/").at(-1) ?? path);
|
|
484
|
-
return `updated ${files.length} file${files.length === 1 ? "" : "s"}: ${names.slice(0, 3).join(", ")}${names.length > 3 ? ` +${names.length - 3}` : ""}`;
|
|
485
|
-
}
|
|
486
|
-
function sessionDisplay(issue) {
|
|
487
|
-
if (issue.sessionState === "failed" || issue.factoryState === "failed" || issue.factoryState === "escalated") {
|
|
488
|
-
return { label: "needs help", color: "red" };
|
|
489
|
-
}
|
|
490
|
-
const state = issue.sessionState ?? "unknown";
|
|
491
|
-
return SESSION_DISPLAY[state] ?? { label: state, color: "white" };
|
|
492
|
-
}
|
|
493
|
-
function stageDisplay(issue) {
|
|
494
|
-
return STAGE_DISPLAY[effectiveState(issue)] ?? issue.factoryState;
|
|
495
|
-
}
|
|
496
|
-
function effectiveState(issue) {
|
|
497
|
-
if (issue.sessionState === "done")
|
|
498
|
-
return "done";
|
|
499
|
-
if (issue.sessionState === "failed")
|
|
500
|
-
return "failed";
|
|
501
|
-
if (issue.completionCheckActive)
|
|
502
|
-
return "completion_check";
|
|
503
|
-
if (issue.blockedByCount > 0 && !issue.activeRunType)
|
|
504
|
-
return "blocked";
|
|
505
|
-
if (issue.sessionState === "waiting_input")
|
|
506
|
-
return "awaiting_input";
|
|
507
|
-
if (hasOpenPr(issue.prNumber, issue.prState))
|
|
508
|
-
return issue.factoryState;
|
|
509
|
-
if (issue.readyForExecution && !issue.activeRunType && !hasDisplayPrBlocker(issue))
|
|
510
|
-
return "ready";
|
|
511
|
-
return issue.factoryState;
|
|
512
|
-
}
|
|
513
|
-
function blockerText(issue, issueContext) {
|
|
514
|
-
const rereviewNeeded = isRereviewNeeded(issue);
|
|
515
|
-
if (issue.sessionState === "waiting_input")
|
|
516
|
-
return issue.waitingReason ?? "Waiting for input";
|
|
517
|
-
if (issue.completionCheckActive)
|
|
518
|
-
return "No PR found; checking next step";
|
|
519
|
-
if (issue.sessionState === "failed" || issue.factoryState === "failed" || issue.factoryState === "escalated") {
|
|
520
|
-
return issue.statusNote ?? issue.waitingReason ?? "Needs operator intervention";
|
|
521
|
-
}
|
|
522
|
-
if (issue.waitingReason && issue.activeRunType && issue.factoryState === "pr_open")
|
|
523
|
-
return issue.waitingReason;
|
|
524
|
-
if (issue.waitingReason && issue.activeRunType && issue.factoryState === "awaiting_queue")
|
|
525
|
-
return issue.waitingReason;
|
|
526
|
-
if (issue.waitingReason && !issue.activeRunType)
|
|
527
|
-
return issue.waitingReason;
|
|
528
|
-
if (issue.blockedByCount > 0)
|
|
529
|
-
return `Waiting on ${issue.blockedByKeys.join(", ")}`;
|
|
530
|
-
if (effectiveState(issue) === "repairing_queue")
|
|
531
|
-
return "Merge queue conflict, repairing branch";
|
|
532
|
-
if (effectiveState(issue) === "repairing_ci") {
|
|
533
|
-
const check = issueContext?.latestFailureCheckName ?? issue.latestFailureCheckName ?? "CI";
|
|
534
|
-
return `Repairing ${check}`;
|
|
535
|
-
}
|
|
536
|
-
const checks = prChecksFact({
|
|
537
|
-
...issue,
|
|
538
|
-
latestFailureCheckName: issueContext?.latestFailureCheckName ?? issue.latestFailureCheckName,
|
|
539
|
-
});
|
|
540
|
-
if (checks?.color === "red") {
|
|
541
|
-
return checks.text;
|
|
542
|
-
}
|
|
543
|
-
if (checks?.color === "yellow" && checks.text.startsWith("checks ")) {
|
|
544
|
-
return `${checks.text} still running`;
|
|
545
|
-
}
|
|
546
|
-
if (rereviewNeeded)
|
|
547
|
-
return "Awaiting re-review after requested changes";
|
|
548
|
-
if (isChangesRequestedReviewState(issue.prReviewState))
|
|
549
|
-
return "Review changes requested";
|
|
550
|
-
if (hasOpenPr(issue.prNumber, issue.prState) && (isAwaitingReviewState(issue.prReviewState) || (!issue.prReviewState && effectiveState(issue) !== "done"))) {
|
|
551
|
-
return "Awaiting review";
|
|
552
|
-
}
|
|
553
|
-
return null;
|
|
554
|
-
}
|
|
555
|
-
function cleanCommand(raw) {
|
|
556
|
-
const bashMatch = raw.match(/^\/bin\/(?:ba)?sh\s+-\w*c\s+['"](.+?)['"]$/s);
|
|
557
|
-
if (bashMatch?.[1])
|
|
558
|
-
return bashMatch[1];
|
|
559
|
-
const bashMatch2 = raw.match(/^\/bin\/(?:ba)?sh\s+-\w*c\s+"(.+?)"$/s);
|
|
560
|
-
if (bashMatch2?.[1])
|
|
561
|
-
return bashMatch2[1];
|
|
562
|
-
return raw;
|
|
563
|
-
}
|
|
564
|
-
function lastNonEmptyLine(output) {
|
|
565
|
-
return output.split("\n").filter((line) => line.trim().length > 0).at(-1) ?? "";
|
|
566
|
-
}
|
|
567
|
-
function historyRunSummary(runs) {
|
|
568
|
-
const completed = runs.filter((run) => run.status === "completed").length;
|
|
569
|
-
const failed = runs.filter((run) => run.status === "failed").length;
|
|
570
|
-
const running = runs.filter((run) => run.status === "running").length;
|
|
571
|
-
const parts = [
|
|
572
|
-
completed > 0 ? `${completed} completed` : null,
|
|
573
|
-
failed > 0 ? `${failed} failed` : null,
|
|
574
|
-
running > 0 ? `${running} active` : null,
|
|
575
|
-
].filter((value) => Boolean(value));
|
|
576
|
-
return `${runs.length} runs: ${parts.join(", ")}`;
|
|
577
|
-
}
|
|
578
|
-
function feedGlyph(status) {
|
|
579
|
-
if (status === "failed")
|
|
580
|
-
return "✗";
|
|
581
|
-
if (status === "completed" || status === "pr_merged")
|
|
582
|
-
return "✓";
|
|
583
|
-
return "●";
|
|
584
|
-
}
|
|
585
|
-
function blankLine(key) {
|
|
586
|
-
return { key, segments: [] };
|
|
587
|
-
}
|
|
588
|
-
function trimTrailingBlankLines(lines) {
|
|
589
|
-
const result = [...lines];
|
|
590
|
-
while (result.length > 0 && result[result.length - 1]?.segments.length === 0) {
|
|
591
|
-
result.pop();
|
|
592
|
-
}
|
|
593
|
-
return result;
|
|
62
|
+
function renderEventLine(event) {
|
|
63
|
+
const age = formatEventAge(event.at).padStart(AGE_WIDTH, " ");
|
|
64
|
+
const category = event.category.padEnd(CATEGORY_WIDTH, " ");
|
|
65
|
+
const segments = [
|
|
66
|
+
{ text: age, dimColor: true },
|
|
67
|
+
{ text: " " },
|
|
68
|
+
{ text: category, dimColor: true },
|
|
69
|
+
{ text: " " },
|
|
70
|
+
{ text: event.phrase, ...(event.color ? { color: event.color } : {}) },
|
|
71
|
+
];
|
|
72
|
+
return { key: event.id, segments };
|
|
594
73
|
}
|
|
595
74
|
function segmentsToText(segments) {
|
|
596
75
|
return segments.map((segment) => segment.text).join("");
|
|
597
76
|
}
|
|
77
|
+
function blankLine(key) {
|
|
78
|
+
return { key, segments: [{ text: "" }] };
|
|
79
|
+
}
|