@bermudi/pi-delegate 0.1.13 → 0.1.15
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 +14 -17
- package/delegate.ts +0 -8
- package/dispatch.ts +25 -2
- package/extension.ts +14 -6
- package/file-tracking.ts +286 -15
- package/format.ts +107 -21
- package/isolated-workspace.ts +550 -62
- package/key-hints.ts +38 -0
- package/lifecycle.ts +559 -80
- package/manual.ts +1 -1
- package/model.ts +6 -1
- package/package.json +13 -9
- package/pool.ts +37 -6
- package/quiescence.ts +19 -6
- package/render-branches.ts +243 -85
- package/render-result.ts +116 -16
- package/runner.ts +361 -82
- package/session-quarantine.ts +266 -0
- package/spill.ts +93 -22
- package/task-resolution.ts +63 -50
- package/telemetry.ts +1 -1
- package/ticket-format.ts +15 -9
- package/tickets.ts +36 -14
- package/tools.ts +5 -4
- package/types.ts +92 -8
- package/utils.ts +123 -2
- package/workspace.ts +133 -0
- package/settings.ts +0 -418
package/render-branches.ts
CHANGED
|
@@ -12,10 +12,18 @@ import {
|
|
|
12
12
|
spinnerFrame,
|
|
13
13
|
formatToolCallShort,
|
|
14
14
|
previewOutputLine,
|
|
15
|
+
taskTokenLabel,
|
|
15
16
|
waitingLabel,
|
|
17
|
+
resumeMarker,
|
|
16
18
|
} from "./format.ts";
|
|
17
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
resolveCarriageReturn,
|
|
21
|
+
sanitizeTerminalLine,
|
|
22
|
+
sanitizeTerminalText,
|
|
23
|
+
stripAnsi,
|
|
24
|
+
} from "./utils.ts";
|
|
18
25
|
import { getMaxConcurrent } from "./config.ts";
|
|
26
|
+
import { toolExpandHint } from "./key-hints.ts";
|
|
19
27
|
import type { TaskProgress, TaskResult } from "./types.ts";
|
|
20
28
|
|
|
21
29
|
/**
|
|
@@ -24,7 +32,7 @@ import type { TaskProgress, TaskResult } from "./types.ts";
|
|
|
24
32
|
* so a single missing host hook cannot crash the renderer.
|
|
25
33
|
*/
|
|
26
34
|
function renderOutputLines(raw: string, width: number): string[] {
|
|
27
|
-
const trimmed = raw.trim();
|
|
35
|
+
const trimmed = sanitizeTerminalText(raw).trim();
|
|
28
36
|
if (!trimmed) return [];
|
|
29
37
|
try {
|
|
30
38
|
if (typeof getMarkdownTheme !== "function") return trimmed.split("\n");
|
|
@@ -59,6 +67,8 @@ export interface BranchCtx {
|
|
|
59
67
|
ticketId?: string;
|
|
60
68
|
/** Async ticket status, when known, so the renderer can show cancelling/cancelled. */
|
|
61
69
|
ticketStatus?: "running" | "cancelling" | "done" | "failed" | "cancelled";
|
|
70
|
+
/** Actual batch wall time when no live render state is available. */
|
|
71
|
+
elapsedMs?: number;
|
|
62
72
|
}
|
|
63
73
|
|
|
64
74
|
/** Helpers shared across both branches. `pushWarnings` mutates `lines`.
|
|
@@ -77,28 +87,53 @@ export function renderPartialBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
77
87
|
const { progress, total, w, expanded, state, theme, lines } = ctx;
|
|
78
88
|
const { statJoin, modelLabel, compactActivity, pushWarnings } = h;
|
|
79
89
|
|
|
80
|
-
const
|
|
90
|
+
const finished = progress.filter(
|
|
81
91
|
(p) => p.status === "done" || p.status === "failed",
|
|
82
92
|
).length;
|
|
93
|
+
const failed = progress.filter((p) => p.status === "failed").length;
|
|
83
94
|
const running = progress.filter((p) => p.status === "running").length;
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
: "";
|
|
95
|
+
const totalTokens = progress.reduce((sum, p) => sum + p.tokens, 0);
|
|
96
|
+
const hasIncomplete = progress.some((p) => p.incomplete !== undefined);
|
|
87
97
|
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
// task just repeats the same line N times.
|
|
98
|
+
// Keep the live and final summaries in the same order so the header remains
|
|
99
|
+
// easy to scan as a partial result resolves into its final form.
|
|
91
100
|
const headerParts: string[] = [];
|
|
92
|
-
if (ctx.ticketStatus === "cancelling") headerParts.push("CANCELLING");
|
|
93
101
|
if (running > 0) headerParts.push(`${running} running`);
|
|
94
|
-
headerParts.push(`${
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
102
|
+
headerParts.push(`${finished}/${total} finished`);
|
|
103
|
+
if (failed > 0) headerParts.push(`${failed} failed`);
|
|
104
|
+
headerParts.push(
|
|
105
|
+
hasIncomplete
|
|
106
|
+
? `≥${fmtTokens(totalTokens)} tokens · incomplete accounting/evidence`
|
|
107
|
+
: `${fmtTokens(totalTokens)} tokens`,
|
|
108
|
+
);
|
|
109
|
+
if (state.startedAt)
|
|
110
|
+
headerParts.push(fmtDuration(Date.now() - state.startedAt));
|
|
111
|
+
const stateLabel =
|
|
112
|
+
ctx.ticketStatus === "cancelling"
|
|
113
|
+
? `${theme.fg("error", "■ cancelling")} · `
|
|
114
|
+
: "";
|
|
115
|
+
const expandHint = toolExpandHint();
|
|
116
|
+
const detailHint =
|
|
117
|
+
!expanded && running > 0 && expandHint
|
|
118
|
+
? ` · ${theme.fg("accent", expandHint)}`
|
|
119
|
+
: "";
|
|
120
|
+
lines.push(
|
|
121
|
+
`${stateLabel}${theme.fg("muted", headerParts.join(" · "))}${detailHint}`,
|
|
122
|
+
"",
|
|
123
|
+
);
|
|
98
124
|
|
|
99
125
|
for (let i = 0; i < total; i++) {
|
|
100
126
|
const p = progress[i]!;
|
|
101
127
|
const ind = indent(i, total);
|
|
128
|
+
const agent = sanitizeTerminalLine(p.agent);
|
|
129
|
+
const task = sanitizeTerminalLine(p.task);
|
|
130
|
+
const taskId = formatTaskId(p.id);
|
|
131
|
+
const taskIdTag = taskId ? theme.fg("accent", taskId) : "";
|
|
132
|
+
// Revival marker: a resumed row must never read as a fresh spawn. Empty
|
|
133
|
+
// when the identity already carries the resume label (omitted-agent
|
|
134
|
+
// resumes resolve to `resume:<tag>` at task resolution).
|
|
135
|
+
const resumeMarkRaw = resumeMarker(p);
|
|
136
|
+
const resumeMark = resumeMarkRaw ? theme.fg("warning", resumeMarkRaw) : "";
|
|
102
137
|
const runParts: string[] = [];
|
|
103
138
|
if (p.toolUses > 0)
|
|
104
139
|
runParts.push(`${p.toolUses} tool${p.toolUses > 1 ? "s" : ""}`);
|
|
@@ -108,18 +143,20 @@ export function renderPartialBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
108
143
|
case "done":
|
|
109
144
|
lines.push(
|
|
110
145
|
truncLine(
|
|
111
|
-
`${tree(i, total)} ${theme.fg("success", "✓")} ${theme.bold(
|
|
146
|
+
`${tree(i, total)} ${theme.fg("success", "✓")} ${theme.bold(agent)}${resumeMark}${taskIdTag}${theme.fg("muted", ` — ${task}`)}${expanded ? `${modelLabel(p)}${statJoin([fmtDuration(p.durationMs), taskTokenLabel(p)])}` : ""}`,
|
|
112
147
|
w,
|
|
113
148
|
),
|
|
114
149
|
);
|
|
115
150
|
if (expanded) {
|
|
116
151
|
for (const activity of p.activities.slice(-3)) {
|
|
117
|
-
const call =
|
|
152
|
+
const call = sanitizeTerminalLine(
|
|
153
|
+
formatToolCallShort(activity.name, activity.args),
|
|
154
|
+
);
|
|
118
155
|
const icon = activity.result?.isError
|
|
119
156
|
? theme.fg("error", "✗")
|
|
120
157
|
: theme.fg("success", "✓");
|
|
121
158
|
lines.push(
|
|
122
|
-
truncLine(`${ind}${theme.fg("
|
|
159
|
+
truncLine(`${ind}${theme.fg("dim", `→ ${call}`)} ${icon}`, w),
|
|
123
160
|
);
|
|
124
161
|
}
|
|
125
162
|
}
|
|
@@ -127,18 +164,20 @@ export function renderPartialBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
127
164
|
case "failed":
|
|
128
165
|
lines.push(
|
|
129
166
|
truncLine(
|
|
130
|
-
`${tree(i, total)} ${theme.fg("error", "✗")} ${theme.bold(
|
|
167
|
+
`${tree(i, total)} ${theme.fg("error", "✗")} ${theme.bold(agent)}${resumeMark}${taskIdTag}${theme.fg("muted", ` — ${task}`)}${expanded ? modelLabel(p) : ""}${p.error ? theme.fg("error", ` · ${sanitizeTerminalLine(p.error)}`) : ""}`,
|
|
131
168
|
w,
|
|
132
169
|
),
|
|
133
170
|
);
|
|
134
171
|
if (expanded) {
|
|
135
172
|
for (const activity of p.activities.slice(-3)) {
|
|
136
|
-
const call =
|
|
173
|
+
const call = sanitizeTerminalLine(
|
|
174
|
+
formatToolCallShort(activity.name, activity.args),
|
|
175
|
+
);
|
|
137
176
|
const icon = activity.result?.isError
|
|
138
177
|
? theme.fg("error", "✗")
|
|
139
178
|
: theme.fg("success", "✓");
|
|
140
179
|
lines.push(
|
|
141
|
-
truncLine(`${ind}${theme.fg("
|
|
180
|
+
truncLine(`${ind}${theme.fg("dim", `→ ${call}`)} ${icon}`, w),
|
|
142
181
|
);
|
|
143
182
|
}
|
|
144
183
|
}
|
|
@@ -159,7 +198,7 @@ export function renderPartialBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
159
198
|
const glyph = theme.fg("warning", spinnerFrame());
|
|
160
199
|
lines.push(
|
|
161
200
|
truncLine(
|
|
162
|
-
`${tree(i, total)} ${glyph} ${theme.bold(
|
|
201
|
+
`${tree(i, total)} ${glyph} ${theme.bold(agent)}${resumeMark}${taskIdTag}${theme.fg("muted", ` — ${task}`)}${expanded ? `${modelLabel(p)}${statJoin(runParts)}` : ""}${issueTag}${theme.fg("dim", ageTag)}`,
|
|
163
202
|
w,
|
|
164
203
|
),
|
|
165
204
|
);
|
|
@@ -168,24 +207,27 @@ export function renderPartialBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
168
207
|
// ── Expanded: recent activity history (like done/failed) ──
|
|
169
208
|
if (p.activities.length > 0) {
|
|
170
209
|
for (const activity of p.activities.slice(-5)) {
|
|
171
|
-
const call =
|
|
210
|
+
const call = sanitizeTerminalLine(
|
|
211
|
+
formatToolCallShort(activity.name, activity.args),
|
|
212
|
+
);
|
|
172
213
|
if (!activity.result) {
|
|
173
214
|
// In-flight
|
|
174
215
|
const elapsed = ` | ${fmtDuration(Date.now() - activity.startTime)}`;
|
|
175
216
|
lines.push(
|
|
176
217
|
truncLine(
|
|
177
|
-
`${ind}${theme.fg("warning",
|
|
218
|
+
`${ind}${theme.fg("warning", "›")} ${call}${theme.fg("dim", elapsed)}`,
|
|
178
219
|
w,
|
|
179
220
|
),
|
|
180
221
|
);
|
|
181
222
|
// Show live stdout/stderr preview for streaming tools
|
|
182
223
|
if (activity.liveOutput) {
|
|
183
|
-
const clean =
|
|
184
|
-
|
|
224
|
+
const clean = resolveCarriageReturn(
|
|
225
|
+
stripAnsi(activity.liveOutput),
|
|
185
226
|
);
|
|
186
227
|
const preview = clean
|
|
187
228
|
.split("\n")
|
|
188
|
-
.
|
|
229
|
+
.map(sanitizeTerminalLine)
|
|
230
|
+
.filter(Boolean)
|
|
189
231
|
.slice(-3);
|
|
190
232
|
for (const outLine of preview) {
|
|
191
233
|
lines.push(
|
|
@@ -202,7 +244,7 @@ export function renderPartialBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
202
244
|
: theme.fg("success", "✓");
|
|
203
245
|
lines.push(
|
|
204
246
|
truncLine(
|
|
205
|
-
`${ind}${theme.fg("
|
|
247
|
+
`${ind}${theme.fg("dim", `→ ${call}`)} ${icon}`,
|
|
206
248
|
w,
|
|
207
249
|
),
|
|
208
250
|
);
|
|
@@ -210,16 +252,16 @@ export function renderPartialBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
210
252
|
}
|
|
211
253
|
} else {
|
|
212
254
|
lines.push(
|
|
213
|
-
truncLine(`${ind}${theme.fg("
|
|
255
|
+
truncLine(`${ind}${theme.fg("dim", " thinking…")}`, w),
|
|
214
256
|
);
|
|
215
257
|
}
|
|
216
258
|
} else {
|
|
217
259
|
// ── Collapsed: compact tool line with duration ─────
|
|
218
|
-
// The
|
|
219
|
-
// it per task
|
|
260
|
+
// The host-configured expand affordance lives once in the header;
|
|
261
|
+
// emitting it per task repeats the hint for every running agent.
|
|
220
262
|
lines.push(
|
|
221
263
|
truncLine(
|
|
222
|
-
`${ind}${theme.fg("
|
|
264
|
+
`${ind}${theme.fg("warning", "›")} ${sanitizeTerminalLine(compactActivity(p))}`,
|
|
223
265
|
w,
|
|
224
266
|
),
|
|
225
267
|
);
|
|
@@ -235,7 +277,7 @@ export function renderPartialBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
235
277
|
);
|
|
236
278
|
lines.push(
|
|
237
279
|
truncLine(
|
|
238
|
-
`${tree(i, total)} ${theme.fg("muted", "○")} ${theme.bold(
|
|
280
|
+
`${tree(i, total)} ${theme.fg("muted", "○")} ${theme.bold(agent)}${resumeMark}${taskIdTag}${theme.fg("muted", ` — ${task}`)}${expanded ? modelLabel(p) : ""}${queuedTag}`,
|
|
239
281
|
w,
|
|
240
282
|
),
|
|
241
283
|
);
|
|
@@ -245,6 +287,18 @@ export function renderPartialBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
245
287
|
}
|
|
246
288
|
}
|
|
247
289
|
|
|
290
|
+
function hasUserCancellationMarker(
|
|
291
|
+
progress: TaskProgress,
|
|
292
|
+
result: TaskResult | { error: string } | undefined,
|
|
293
|
+
): boolean {
|
|
294
|
+
if (progress.failureKind === "cancelled") return true;
|
|
295
|
+
return (
|
|
296
|
+
result !== undefined &&
|
|
297
|
+
"failureKind" in result &&
|
|
298
|
+
result.failureKind === "cancelled"
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
248
302
|
/** Final (or async-ticket poll) view — static status glyphs, output previews,
|
|
249
303
|
* markdown rendering in expanded mode, and the ticket banner for live
|
|
250
304
|
* background tickets. */
|
|
@@ -259,11 +313,20 @@ export function renderFinalBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
259
313
|
// this branch must render every status. A ticket banner is shown when
|
|
260
314
|
// details.ticketId is present so the human sees this is background work.
|
|
261
315
|
const succeeded = progress.filter((p) => p.status === "done").length;
|
|
262
|
-
const
|
|
263
|
-
|
|
316
|
+
const cancelled = progress.filter(
|
|
317
|
+
(p, index) =>
|
|
318
|
+
p.status === "failed" && hasUserCancellationMarker(p, taskResults[index]),
|
|
319
|
+
).length;
|
|
320
|
+
const failed = progress.filter(
|
|
321
|
+
(p, index) =>
|
|
322
|
+
p.status === "failed" &&
|
|
323
|
+
!hasUserCancellationMarker(p, taskResults[index]),
|
|
324
|
+
).length;
|
|
325
|
+
const finalized = succeeded + failed + cancelled;
|
|
264
326
|
const running = progress.filter((p) => p.status === "running").length;
|
|
265
327
|
const pending = progress.filter((p) => p.status === "pending").length;
|
|
266
328
|
const totalTokens = progress.reduce((sum, p) => sum + p.tokens, 0);
|
|
329
|
+
const hasIncomplete = progress.some((p) => p.incomplete !== undefined);
|
|
267
330
|
const ticketId = ctx.ticketId;
|
|
268
331
|
const ticketStatus = ctx.ticketStatus;
|
|
269
332
|
// A terminal ticket can retain a stale running/pending row while its workers
|
|
@@ -273,29 +336,71 @@ export function renderFinalBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
273
336
|
ticketStatus === undefined ||
|
|
274
337
|
ticketStatus === "running" ||
|
|
275
338
|
ticketStatus === "cancelling";
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
339
|
+
// Terminal tickets render stale running/pending rows with a terminal glyph.
|
|
340
|
+
// A caller-aborted task retains a structured cancellation marker after
|
|
341
|
+
// settling; count it as cancelled rather than as a generic failure. Provider
|
|
342
|
+
// failures whose human-facing text happens to be "Aborted" remain failures.
|
|
343
|
+
const terminalUnfinished = ticketIsLive ? 0 : running + pending;
|
|
344
|
+
const terminalFinalized = finalized + terminalUnfinished;
|
|
345
|
+
const terminalCancelled =
|
|
346
|
+
cancelled + (ticketStatus === "cancelled" ? terminalUnfinished : 0);
|
|
347
|
+
const terminalFailed =
|
|
348
|
+
failed + (ticketStatus === "cancelled" ? 0 : terminalUnfinished);
|
|
349
|
+
const ticketWasCancelled = ticketStatus === "cancelled";
|
|
350
|
+
let elapsed: string | undefined;
|
|
351
|
+
if (ctx.elapsedMs !== undefined) {
|
|
352
|
+
elapsed = fmtDuration(ctx.elapsedMs);
|
|
353
|
+
} else if (state.startedAt !== undefined) {
|
|
354
|
+
elapsed = fmtDuration(Date.now() - state.startedAt);
|
|
355
|
+
}
|
|
279
356
|
|
|
357
|
+
const safeTicketId = ticketId ? sanitizeTerminalLine(ticketId) : "";
|
|
358
|
+
const ticketLabel = safeTicketId ? `ticket ${safeTicketId} · ` : "";
|
|
359
|
+
const expandHint = toolExpandHint();
|
|
360
|
+
const detailHint =
|
|
361
|
+
!expanded && expandHint ? ` · ${theme.fg("accent", expandHint)}` : "";
|
|
280
362
|
if (ticketId && ticketIsLive) {
|
|
281
363
|
// Background ticket — frame it as in-progress, not a finished result.
|
|
282
|
-
const ticketParts = [
|
|
283
|
-
`ticket ${ticketId}`,
|
|
284
|
-
`${finalized}/${total} finalized`,
|
|
285
|
-
];
|
|
364
|
+
const ticketParts = [`${finalized}/${total} finished`];
|
|
286
365
|
if (running > 0) ticketParts.push(`${running} active`);
|
|
287
366
|
if (pending > 0) ticketParts.push(`${pending} queued`);
|
|
288
367
|
if (failed > 0) ticketParts.push(`${failed} failed`);
|
|
289
|
-
ticketParts.push(
|
|
290
|
-
|
|
368
|
+
if (cancelled > 0) ticketParts.push(`${cancelled} cancelled`);
|
|
369
|
+
const glyph =
|
|
370
|
+
ticketStatus === "cancelling"
|
|
371
|
+
? theme.fg("error", "■")
|
|
372
|
+
: theme.fg("warning", "◐");
|
|
373
|
+
const stateLabel =
|
|
374
|
+
ticketStatus === "cancelling"
|
|
375
|
+
? ` ${theme.fg("error", "cancelling")}`
|
|
376
|
+
: "";
|
|
377
|
+
lines.push(
|
|
378
|
+
`${glyph}${stateLabel} ${theme.fg("muted", `${ticketLabel}${ticketParts.join(" · ")}`)}${detailHint}`,
|
|
379
|
+
"",
|
|
291
380
|
);
|
|
292
|
-
lines.push(theme.fg("warning", `⏳ ${ticketParts.join(" · ")}`), "");
|
|
293
381
|
} else {
|
|
382
|
+
const headerParts = [`${terminalFinalized}/${total} finished`];
|
|
383
|
+
if (terminalFailed > 0) headerParts.push(`${terminalFailed} failed`);
|
|
384
|
+
if (terminalCancelled > 0)
|
|
385
|
+
headerParts.push(`${terminalCancelled} cancelled`);
|
|
386
|
+
// Cancellation is a ticket-level outcome even when every worker returned a
|
|
387
|
+
// normal terminal row. Keep it distinct from actual failed-row counts.
|
|
388
|
+
if (ticketWasCancelled) headerParts.push("ticket cancelled");
|
|
389
|
+
headerParts.push(
|
|
390
|
+
hasIncomplete
|
|
391
|
+
? `≥${fmtTokens(totalTokens)} tokens · incomplete accounting/evidence`
|
|
392
|
+
: `${fmtTokens(totalTokens)} tokens`,
|
|
393
|
+
);
|
|
394
|
+
if (elapsed) headerParts.push(elapsed);
|
|
395
|
+
const glyph =
|
|
396
|
+
ticketStatus === "cancelled" ||
|
|
397
|
+
ticketStatus === "failed" ||
|
|
398
|
+
terminalFailed > 0 ||
|
|
399
|
+
terminalCancelled > 0
|
|
400
|
+
? theme.fg("error", "✗")
|
|
401
|
+
: theme.fg("success", "✓");
|
|
294
402
|
lines.push(
|
|
295
|
-
theme.fg(
|
|
296
|
-
"muted",
|
|
297
|
-
`${succeeded}/${total} completed · ${elapsed} wall · ${fmtTokens(totalTokens)} tokens`,
|
|
298
|
-
),
|
|
403
|
+
`${glyph} ${theme.fg("muted", `${ticketLabel}${headerParts.join(" · ")}`)}${detailHint}`,
|
|
299
404
|
"",
|
|
300
405
|
);
|
|
301
406
|
}
|
|
@@ -304,54 +409,75 @@ export function renderFinalBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
304
409
|
const p = progress[i]!;
|
|
305
410
|
const r = taskResults[i];
|
|
306
411
|
const ind = indent(i, total);
|
|
307
|
-
const
|
|
308
|
-
|
|
412
|
+
const agent = sanitizeTerminalLine(p.agent);
|
|
413
|
+
const task = sanitizeTerminalLine(p.task);
|
|
414
|
+
const isTerminalUnfinished =
|
|
415
|
+
!ticketIsLive && (p.status === "running" || p.status === "pending");
|
|
416
|
+
const isCancelledUnfinished =
|
|
417
|
+
ticketStatus === "cancelled" && isTerminalUnfinished;
|
|
418
|
+
const isCancelledResult =
|
|
419
|
+
p.status === "failed" && hasUserCancellationMarker(p, r);
|
|
420
|
+
const isCancelledTask = isCancelledUnfinished || isCancelledResult;
|
|
309
421
|
|
|
310
|
-
// Unified status glyphs: ✓ done, ✗ failed, ◐ running, ○ pending.
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
422
|
+
// Unified status glyphs: ✓ done, ✗ failed/cancelled, ◐ running, ○ pending.
|
|
423
|
+
// Terminal unfinished rows never retain a live glyph; their tail identifies
|
|
424
|
+
// cancellation separately from failure.
|
|
425
|
+
let icon: string;
|
|
426
|
+
if (isTerminalUnfinished) {
|
|
427
|
+
icon = theme.fg("error", "✗");
|
|
428
|
+
} else if (p.status === "done") {
|
|
429
|
+
icon = theme.fg("success", "✓");
|
|
430
|
+
} else if (p.status === "failed") {
|
|
431
|
+
icon = theme.fg("error", "✗");
|
|
432
|
+
} else if (p.status === "running") {
|
|
433
|
+
icon = theme.fg("warning", "◐");
|
|
434
|
+
} else {
|
|
435
|
+
icon = theme.fg("muted", "○");
|
|
436
|
+
}
|
|
437
|
+
const taskId = formatTaskId(p.id);
|
|
438
|
+
const taskIdTag = taskId ? theme.fg("accent", taskId) : "";
|
|
439
|
+
const taskIdWidth = taskId.length;
|
|
440
|
+
// Revival marker: a resumed row must never read as a fresh spawn. Empty
|
|
441
|
+
// when the identity already carries the resume label (omitted-agent
|
|
442
|
+
// resumes resolve to `resume:<tag>` at task resolution).
|
|
443
|
+
const resumeMarkRaw = resumeMarker(p);
|
|
444
|
+
const resumeMark = resumeMarkRaw ? theme.fg("warning", resumeMarkRaw) : "";
|
|
325
445
|
const previewBudget = Math.max(1, w - 30 - taskIdWidth);
|
|
326
|
-
const taskPreview = theme.fg("muted", trunc(
|
|
446
|
+
const taskPreview = theme.fg("muted", ` — ${trunc(task, previewBudget)}`);
|
|
327
447
|
const isLive =
|
|
328
|
-
ticketIsLive &&
|
|
329
|
-
(p.status === "running" ||
|
|
330
|
-
(p.status === "pending" && !isCancelledPending));
|
|
448
|
+
ticketIsLive && (p.status === "running" || p.status === "pending");
|
|
331
449
|
// Live tasks show an activity/waiting hint instead of final stats.
|
|
332
450
|
const liveTail =
|
|
333
|
-
p.status === "
|
|
334
|
-
? theme.fg("muted", `
|
|
335
|
-
:
|
|
336
|
-
|
|
337
|
-
: "";
|
|
338
|
-
const cancelledTail = isCancelledPending
|
|
451
|
+
p.status === "pending"
|
|
452
|
+
? theme.fg("muted", ` ${waitingLabel(running, getMaxConcurrent())}`)
|
|
453
|
+
: "";
|
|
454
|
+
const cancelledTail = isCancelledTask
|
|
339
455
|
? theme.fg("error", " · CANCELLED")
|
|
340
456
|
: "";
|
|
341
457
|
lines.push(
|
|
342
458
|
truncLine(
|
|
343
|
-
`${tree(i, total)} ${icon} ${theme.bold(
|
|
459
|
+
`${tree(i, total)} ${icon} ${theme.bold(agent)}${resumeMark}${taskIdTag}${taskPreview}${expanded ? modelLabel(p) : ""}${isLive ? liveTail : cancelledTail || (expanded ? statJoin([fmtDuration(p.durationMs), taskTokenLabel(p)]) : "")}`,
|
|
344
460
|
w,
|
|
345
461
|
),
|
|
346
462
|
);
|
|
347
463
|
|
|
464
|
+
if (isLive && p.status === "running") {
|
|
465
|
+
lines.push(
|
|
466
|
+
truncLine(
|
|
467
|
+
`${ind}${theme.fg("warning", "›")} ${sanitizeTerminalLine(compactActivity(p))}`,
|
|
468
|
+
w,
|
|
469
|
+
),
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
348
473
|
// Warnings (e.g. unknown tools ignored) — muted line under the task.
|
|
349
474
|
pushWarnings(p, ind);
|
|
350
475
|
|
|
351
476
|
// Tool activities: compact summary only in expanded mode, terminal tasks only.
|
|
352
477
|
if (p.activities.length > 0 && expanded && !isLive) {
|
|
353
478
|
const names = p.activities
|
|
354
|
-
.map((a) => a.name)
|
|
479
|
+
.map((a) => sanitizeTerminalLine(a.name))
|
|
480
|
+
.filter(Boolean)
|
|
355
481
|
.filter((n, i, arr) => arr.indexOf(n) === i);
|
|
356
482
|
const nameList =
|
|
357
483
|
names.slice(0, 4).join(", ") +
|
|
@@ -366,17 +492,31 @@ export function renderFinalBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
366
492
|
const status = statusParts.length ? ` · ${statusParts.join(", ")}` : "";
|
|
367
493
|
lines.push(
|
|
368
494
|
truncLine(
|
|
369
|
-
`${ind}${theme.fg("
|
|
495
|
+
`${ind}${theme.fg("dim", `${p.activities.length} tool${p.activities.length > 1 ? "s" : ""}: ${nameList}${status}`)}`,
|
|
496
|
+
w,
|
|
497
|
+
),
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (
|
|
502
|
+
!isLive &&
|
|
503
|
+
((r && "incomplete" in r && r.incomplete === "quiescence_abandoned") ||
|
|
504
|
+
p.incomplete === "quiescence_abandoned")
|
|
505
|
+
) {
|
|
506
|
+
lines.push(
|
|
507
|
+
truncLine(
|
|
508
|
+
`${ind}${theme.fg("warning", "INCOMPLETE: accounting and output/file evidence are lower bounds; session quarantined")}`,
|
|
370
509
|
w,
|
|
371
510
|
),
|
|
372
511
|
);
|
|
373
512
|
}
|
|
374
513
|
|
|
375
514
|
// Surface errors even when output exists (agent may have emitted text before failing).
|
|
376
|
-
// Live
|
|
377
|
-
//
|
|
378
|
-
if (!isLive && !
|
|
379
|
-
|
|
515
|
+
// Live and cancelled tasks already show their status on the row, so don't
|
|
516
|
+
// duplicate it as an error line.
|
|
517
|
+
if (!isLive && !isCancelledTask && r && "error" in r && r.error) {
|
|
518
|
+
const error = sanitizeTerminalLine(r.error);
|
|
519
|
+
if (error) lines.push(truncLine(`${ind}${theme.fg("error", error)}`, w));
|
|
380
520
|
}
|
|
381
521
|
if (r && "integration" in r && r.integration) {
|
|
382
522
|
const integration = r.integration;
|
|
@@ -395,17 +535,33 @@ export function renderFinalBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
395
535
|
if (integration.patchPath)
|
|
396
536
|
lines.push(
|
|
397
537
|
truncLine(
|
|
398
|
-
`${ind}${theme.fg("
|
|
538
|
+
`${ind}${theme.fg("dim", `patch: ${sanitizeTerminalLine(integration.patchPath)}`)}`,
|
|
399
539
|
w,
|
|
400
540
|
),
|
|
401
541
|
);
|
|
402
542
|
if (integration.worktreePath)
|
|
403
543
|
lines.push(
|
|
404
544
|
truncLine(
|
|
405
|
-
`${ind}${theme.fg("
|
|
545
|
+
`${ind}${theme.fg("dim", `worktree: ${sanitizeTerminalLine(integration.worktreePath)}`)}`,
|
|
546
|
+
w,
|
|
547
|
+
),
|
|
548
|
+
);
|
|
549
|
+
if (integration.cleanupIssue) {
|
|
550
|
+
lines.push(
|
|
551
|
+
truncLine(
|
|
552
|
+
`${ind}${theme.fg("error", `cleanup ${integration.cleanupIssue.status}: ${sanitizeTerminalLine(integration.cleanupIssue.reason)}`)}`,
|
|
406
553
|
w,
|
|
407
554
|
),
|
|
408
555
|
);
|
|
556
|
+
if (integration.cleanupIssue.recoveryPath) {
|
|
557
|
+
lines.push(
|
|
558
|
+
truncLine(
|
|
559
|
+
`${ind}${theme.fg("dim", `cleanup recovery: ${sanitizeTerminalLine(integration.cleanupIssue.recoveryPath)}`)}`,
|
|
560
|
+
w,
|
|
561
|
+
),
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
409
565
|
}
|
|
410
566
|
}
|
|
411
567
|
// Collapsed: one-line output preview so a human scanning the TUI sees
|
|
@@ -417,7 +573,9 @@ export function renderFinalBranch(ctx: BranchCtx, h: RenderHelpers): void {
|
|
|
417
573
|
? previewOutputLine(r.output ?? "", w - ind.length - 3)
|
|
418
574
|
: "";
|
|
419
575
|
if (preview) {
|
|
420
|
-
lines.push(
|
|
576
|
+
lines.push(
|
|
577
|
+
truncLine(`${ind}${theme.fg("success", "⎿")} ${preview}`, w),
|
|
578
|
+
);
|
|
421
579
|
}
|
|
422
580
|
}
|
|
423
581
|
// Output: render markdown only in expanded mode.
|