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