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