@linxiraos/pi-tui 1.3.2 → 1.4.1
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/CHANGELOG.md +181 -181
- package/dist/types/apps/session-picker.d.ts +13 -1
- package/dist/types/autocomplete.d.ts +6 -0
- package/dist/types/components/sparkline.d.ts +34 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/native/describe.d.ts +7 -1
- package/dist/types/native/overlay.d.ts +2 -0
- package/dist/types/native/tone.d.ts +2 -0
- package/dist/types/overlays/agent-hub.d.ts +3 -3
- package/dist/types/overlays/approval-dialog.d.ts +15 -1
- package/dist/types/overlays/jobs-panel.d.ts +3 -0
- package/dist/types/overlays/model-browser.d.ts +1 -1
- package/dist/types/overlays/resume-picker.d.ts +293 -0
- package/dist/types/overlays/resume-tree.d.ts +353 -0
- package/dist/types/overlays/session-selector.d.ts +22 -4
- package/dist/types/render/output-block.d.ts +2 -0
- package/dist/types/render/tool-card.d.ts +2 -0
- package/dist/types/sixel.d.ts +28 -0
- package/dist/types/status-line/host.d.ts +3 -0
- package/dist/types/terminal-capabilities.d.ts +6 -1
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/default-renderer.d.ts +3 -0
- package/dist/types/tools/eval.d.ts +9 -8
- package/dist/types/tools/hub.d.ts +3 -1
- package/dist/types/tools/irc.d.ts +11 -1
- package/dist/types/tools/job-widget.d.ts +226 -0
- package/dist/types/tools/json-tree.d.ts +2 -0
- package/dist/types/tools/line-ranges.d.ts +8 -1
- package/dist/types/tools/mcp.d.ts +23 -0
- package/dist/types/tools/output-meta.d.ts +1 -1
- package/dist/types/tools/read.d.ts +8 -2
- package/dist/types/tools/renderer.d.ts +8 -2
- package/dist/types/tools/result-card.d.ts +1 -1
- package/dist/types/tools/streaming-output.d.ts +1 -0
- package/dist/types/tools/task.d.ts +12 -0
- package/dist/types/tools/wait.d.ts +11 -0
- package/package.json +9 -9
- package/src/apps/session-picker.ts +20 -2
- package/src/autocomplete.d.ts +6 -0
- package/src/autocomplete.ts +21 -6
- package/src/chat/skill-message.ts +16 -17
- package/src/chat/tool-execution.ts +35 -0
- package/src/chrome/transcript-container.ts +85 -13
- package/src/components/editor.ts +17 -7
- package/src/components/image.ts +5 -4
- package/src/components/markdown.ts +11 -3
- package/src/components/sparkline.d.ts +34 -0
- package/src/components/sparkline.ts +89 -0
- package/src/index.d.ts +1 -0
- package/src/index.ts +1 -0
- package/src/native/backend.ts +32 -6
- package/src/native/describe.d.ts +7 -1
- package/src/native/describe.ts +18 -1
- package/src/native/overlay.d.ts +2 -0
- package/src/native/overlay.ts +3 -0
- package/src/native/tone.d.ts +2 -0
- package/src/native/tone.ts +22 -0
- package/src/overlays/agent-hub.ts +14 -10
- package/src/overlays/agent-transcript-viewer.ts +2 -2
- package/src/overlays/agents-hub.ts +2 -1
- package/src/overlays/approval-dialog.ts +59 -13
- package/src/overlays/hub-frame.ts +2 -1
- package/src/overlays/jobs-panel.ts +24 -2
- package/src/overlays/model-browser.ts +3 -0
- package/src/overlays/model-hub.ts +2 -1
- package/src/overlays/resume-picker.ts +706 -0
- package/src/overlays/resume-tree.ts +1901 -0
- package/src/overlays/session-selector.ts +579 -39
- package/src/prompt/attachment-chips.ts +1 -1
- package/src/render/output-block.d.ts +2 -0
- package/src/render/output-block.ts +25 -12
- package/src/render/tool-card.d.ts +2 -0
- package/src/render/tool-card.ts +6 -0
- package/src/sixel.d.ts +28 -0
- package/src/sixel.ts +185 -0
- package/src/status-line/component.ts +19 -2
- package/src/status-line/host.ts +3 -0
- package/src/terminal-capabilities.d.ts +6 -1
- package/src/terminal-capabilities.ts +54 -3
- package/src/theme/symbols.ts +5 -3
- package/src/tools/bash.d.ts +2 -0
- package/src/tools/bash.ts +25 -9
- package/src/tools/default-renderer.d.ts +3 -0
- package/src/tools/default-renderer.ts +54 -17
- package/src/tools/eval.d.ts +9 -8
- package/src/tools/eval.ts +32 -14
- package/src/tools/find.ts +4 -5
- package/src/tools/glob.ts +4 -6
- package/src/tools/grep.ts +2 -4
- package/src/tools/hub.ts +4 -1
- package/src/tools/irc.d.ts +11 -1
- package/src/tools/irc.ts +11 -1
- package/src/tools/job-widget.d.ts +226 -0
- package/src/tools/job-widget.ts +1036 -0
- package/src/tools/json-tree.d.ts +2 -0
- package/src/tools/json-tree.ts +8 -0
- package/src/tools/line-ranges.d.ts +8 -1
- package/src/tools/line-ranges.ts +23 -13
- package/src/tools/mcp.d.ts +23 -0
- package/src/tools/mcp.ts +44 -1
- package/src/tools/output-meta.d.ts +1 -1
- package/src/tools/output-meta.ts +1 -1
- package/src/tools/proc-render.ts +1 -0
- package/src/tools/read.d.ts +8 -2
- package/src/tools/read.ts +64 -21
- package/src/tools/renderer.d.ts +8 -2
- package/src/tools/renderer.ts +8 -2
- package/src/tools/result-card.d.ts +1 -1
- package/src/tools/result-card.ts +1 -2
- package/src/tools/streaming-output.d.ts +1 -0
- package/src/tools/streaming-output.ts +11 -9
- package/src/tools/task.d.ts +12 -0
- package/src/tools/task.ts +25 -1
- package/src/tools/wait.d.ts +11 -0
- package/src/tools/wait.ts +296 -147
- package/src/tui.ts +45 -5
package/src/tools/wait.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Theme } from "../theme/theme";
|
|
|
3
3
|
import { type OutputMeta } from "./output-meta";
|
|
4
4
|
import { type ConfiguredThinkingLevel } from "../render/render-utils";
|
|
5
5
|
import type { StructuredSubagentOutput } from "./task";
|
|
6
|
+
import { type JobWidgetView } from "./job-widget";
|
|
6
7
|
import type { RenderResultOptions, ToolActivitySummary } from "./renderer";
|
|
7
8
|
import type { IrcDeliveryReceipt, IrcMessage } from "./irc";
|
|
8
9
|
import type { NativeToolView, ToolRenderResult } from "./renderer";
|
|
@@ -29,6 +30,10 @@ export interface JobSnapshot {
|
|
|
29
30
|
advisor?: boolean;
|
|
30
31
|
/** The task agent's latest self-estimated completion (0–100); see `AgentProgress.completionPercent`. */
|
|
31
32
|
completionPercent?: number;
|
|
33
|
+
/** The task agent's latest stage in its own words; see `AgentProgress.stage`. */
|
|
34
|
+
stage?: string;
|
|
35
|
+
/** The job's live widget, while one is being drawn; frozen at its last state once the job settles. */
|
|
36
|
+
widget?: JobWidgetView;
|
|
32
37
|
resultText?: string;
|
|
33
38
|
errorText?: string;
|
|
34
39
|
/** Source-output metadata retained for per-job warnings and persisted row rendering. */
|
|
@@ -96,6 +101,12 @@ export interface CoordinationDetails {
|
|
|
96
101
|
agents?: AgentActivitySnapshot[];
|
|
97
102
|
/** `wait` was cut short by steering, a peer message, or a completion notice that injects next. */
|
|
98
103
|
interrupted?: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* `wait` returned early without a message because continuing would deadlock:
|
|
106
|
+
* the peer it was waiting on is itself waiting on this agent. Both sides can
|
|
107
|
+
* keep working — answer the peer, then wait again.
|
|
108
|
+
*/
|
|
109
|
+
status?: "peer-waiting-on-you";
|
|
99
110
|
}
|
|
100
111
|
/** Pending wait frame. */
|
|
101
112
|
declare function waitRenderCall(_args: object, _options: RenderResultOptions, uiTheme: Theme): Component;
|
package/src/tools/wait.ts
CHANGED
|
@@ -24,10 +24,11 @@ import {
|
|
|
24
24
|
type ConfiguredThinkingLevel,
|
|
25
25
|
} from "../render/render-utils";
|
|
26
26
|
import type { StructuredSubagentOutput } from "./task";
|
|
27
|
+
import { describeJobTiles, type JobTile, type JobWidgetView, renderJobTileLines } from "./job-widget";
|
|
27
28
|
import type { RenderResultOptions, ToolRenderer, ToolActivitySummary } from "./renderer";
|
|
28
29
|
import type { IrcDeliveryReceipt, IrcMessage } from "./irc";
|
|
29
30
|
import type { TspSpan } from "@linxiraos/pi-wire";
|
|
30
|
-
import { card as cardNode, compact, elapsed, md, node, row, span, text } from "../native/describe";
|
|
31
|
+
import { card as cardNode, compact, elapsed, keyed, md, node, row, span, text } from "../native/describe";
|
|
31
32
|
import type { NativeNode } from "../native/node";
|
|
32
33
|
import { OwnerMemo } from "../native/memo";
|
|
33
34
|
import { plainText } from "../native/spans";
|
|
@@ -63,6 +64,10 @@ export interface JobSnapshot {
|
|
|
63
64
|
advisor?: boolean;
|
|
64
65
|
/** The task agent's latest self-estimated completion (0–100); see `AgentProgress.completionPercent`. */
|
|
65
66
|
completionPercent?: number;
|
|
67
|
+
/** The task agent's latest stage in its own words; see `AgentProgress.stage`. */
|
|
68
|
+
stage?: string;
|
|
69
|
+
/** The job's live widget, while one is being drawn; frozen at its last state once the job settles. */
|
|
70
|
+
widget?: JobWidgetView;
|
|
66
71
|
resultText?: string;
|
|
67
72
|
errorText?: string;
|
|
68
73
|
/** Source-output metadata retained for per-job warnings and persisted row rendering. */
|
|
@@ -131,6 +136,12 @@ export interface CoordinationDetails {
|
|
|
131
136
|
agents?: AgentActivitySnapshot[];
|
|
132
137
|
/** `wait` was cut short by steering, a peer message, or a completion notice that injects next. */
|
|
133
138
|
interrupted?: boolean;
|
|
139
|
+
/**
|
|
140
|
+
* `wait` returned early without a message because continuing would deadlock:
|
|
141
|
+
* the peer it was waiting on is itself waiting on this agent. Both sides can
|
|
142
|
+
* keep working — answer the peer, then wait again.
|
|
143
|
+
*/
|
|
144
|
+
status?: "peer-waiting-on-you";
|
|
134
145
|
}
|
|
135
146
|
|
|
136
147
|
// =============================================================================
|
|
@@ -194,6 +205,40 @@ function flattenStructuredPreview(text: string): string {
|
|
|
194
205
|
return text.slice(0, PREVIEW_LINES_EXPANDED * PREVIEW_LINE_WIDTH * 2).replace(/\s+/g, " ");
|
|
195
206
|
}
|
|
196
207
|
|
|
208
|
+
/** The row label: a running task's own stage line when it reports one, else the job's label. */
|
|
209
|
+
function jobRowLabel(job: JobSnapshot): string {
|
|
210
|
+
return job.type === "task" && job.status === "running" && job.stage?.trim() ? job.stage : job.label;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** A job as its tile; `live` while the card's clocks run. */
|
|
214
|
+
function jobTile(job: JobSnapshot, live: boolean): JobTile {
|
|
215
|
+
return {
|
|
216
|
+
id: job.id,
|
|
217
|
+
type: job.type,
|
|
218
|
+
status: job.status,
|
|
219
|
+
label: job.label,
|
|
220
|
+
durationMs: job.durationMs,
|
|
221
|
+
live: live && job.status === "running",
|
|
222
|
+
...(job.widget ? { widget: job.widget } : {}),
|
|
223
|
+
...(job.status === "running" && job.completionPercent !== undefined
|
|
224
|
+
? { completionPercent: job.completionPercent }
|
|
225
|
+
: {}),
|
|
226
|
+
...(job.stage ? { stage: job.stage } : {}),
|
|
227
|
+
...(job.exitCode === undefined ? {} : { exitCode: job.exitCode }),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Identity of the widgets a job list draws: changes whenever any widget's data or state does. */
|
|
232
|
+
function widgetStamp(jobs: readonly JobSnapshot[] | undefined): string {
|
|
233
|
+
if (!jobs) return "";
|
|
234
|
+
let stamp = "";
|
|
235
|
+
for (const job of jobs) if (job.widget) stamp += `${job.id}:${job.widget.updatedAt}:${job.widget.state};`;
|
|
236
|
+
return stamp;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Tiles a native wait card shows before it counts the rest. */
|
|
240
|
+
const TILES_SHOWN = 9;
|
|
241
|
+
|
|
197
242
|
/** Pending wait frame. */
|
|
198
243
|
function waitRenderCall(_args: object, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
199
244
|
return new Text(renderStatusLine({ icon: "pending", title: "Wait" }, uiTheme), 0, 0);
|
|
@@ -201,7 +246,11 @@ function waitRenderCall(_args: object, _options: RenderResultOptions, uiTheme: T
|
|
|
201
246
|
|
|
202
247
|
/** Result frame for wait snapshots and the agents roster. */
|
|
203
248
|
function jobsRenderResult(
|
|
204
|
-
result: {
|
|
249
|
+
result: {
|
|
250
|
+
content: Array<{ type: string; text?: string }>;
|
|
251
|
+
details?: CoordinationDetails;
|
|
252
|
+
isError?: boolean;
|
|
253
|
+
},
|
|
205
254
|
options: RenderResultOptions,
|
|
206
255
|
uiTheme: Theme,
|
|
207
256
|
): Component {
|
|
@@ -308,6 +357,87 @@ function jobsRenderResult(
|
|
|
308
357
|
previewRows.set(job, { expanded, width: continuationWidth, lines });
|
|
309
358
|
return lines;
|
|
310
359
|
};
|
|
360
|
+
// Collapsed card: one line per settled job, or the bare "waiting on N jobs"
|
|
361
|
+
// header while nothing has settled. Repeated polls then read as a short
|
|
362
|
+
// completion log instead of a re-printed (and shrinking) job tree; Ctrl+O
|
|
363
|
+
// restores the full tree with output previews. A running job's live widget
|
|
364
|
+
// still draws its tile: that chart is the job's progress, not a copy of it.
|
|
365
|
+
const settledJobs = sortedJobs.filter(job => job.status !== "running");
|
|
366
|
+
const widgetJobs = sortedJobs.filter(job => job.status === "running" && job.widget?.state === "live");
|
|
367
|
+
const compactJobLines = (width: number, live: boolean): string[] => {
|
|
368
|
+
const lines: string[] = [];
|
|
369
|
+
const indent = " ".repeat(visibleWidth(formatStatusIcon("done", uiTheme)) + 1);
|
|
370
|
+
// Failures sort first, so the cap never hides one behind successes;
|
|
371
|
+
// Ctrl+O lists the rest.
|
|
372
|
+
for (const job of settledJobs.slice(0, PREVIEW_LIMITS.COLLAPSED_ITEMS)) {
|
|
373
|
+
const icon = formatStatusIcon(statusToIcon(job.status), uiTheme);
|
|
374
|
+
const word = job.status === "completed" ? "done" : job.status;
|
|
375
|
+
const exit =
|
|
376
|
+
job.exitCode !== undefined && job.exitCode !== 0
|
|
377
|
+
? `${uiTheme.sep.dot}${uiTheme.fg("error", `exit ${job.exitCode}`)}`
|
|
378
|
+
: "";
|
|
379
|
+
// The outcome (status, exit, duration) is reserved first; a long id
|
|
380
|
+
// gives way so a failure is never cut off the row.
|
|
381
|
+
const outcome = ` ${uiTheme.fg(statusToColor(job.status), word)}${exit}${uiTheme.sep.dot}${uiTheme.fg("dim", formatDuration(job.durationMs))}`;
|
|
382
|
+
const id = truncateToWidth(
|
|
383
|
+
replaceTabs(job.id).replace(/\s+/g, " "),
|
|
384
|
+
Math.max(1, width - visibleWidth(`${icon} `) - visibleWidth(outcome)),
|
|
385
|
+
Ellipsis.Unicode,
|
|
386
|
+
);
|
|
387
|
+
let line = `${icon} ${uiTheme.fg("accent", id)}${outcome}`;
|
|
388
|
+
const label = replaceTabs(jobRowLabel(job).split(/\r?\n/)[0] ?? "").trim();
|
|
389
|
+
const labelBudget = width - visibleWidth(line) - 2;
|
|
390
|
+
if (label && label !== job.id && labelBudget >= 8) {
|
|
391
|
+
line += ` ${uiTheme.fg("muted", truncateToWidth(label, labelBudget, Ellipsis.Unicode))}`;
|
|
392
|
+
}
|
|
393
|
+
lines.push(line);
|
|
394
|
+
// A failure says why on the collapsed row; the rest of its output
|
|
395
|
+
// stays behind Ctrl+O.
|
|
396
|
+
const reason =
|
|
397
|
+
job.status === "failed"
|
|
398
|
+
? stripOutputNotice(job.errorText ?? "", job.meta)
|
|
399
|
+
.split(/\r?\n/)
|
|
400
|
+
.find(part => part.trim())
|
|
401
|
+
?.trim()
|
|
402
|
+
: undefined;
|
|
403
|
+
if (reason) {
|
|
404
|
+
lines.push(
|
|
405
|
+
`${indent}${uiTheme.fg("error", truncateToWidth(replaceTabs(reason), Math.max(1, width - indent.length), Ellipsis.Unicode))}`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
// The root warning is suppressed when a row owns the failure, so the
|
|
409
|
+
// compact row carries it too — otherwise it only shows after Ctrl+O.
|
|
410
|
+
const artifactError = job.meta?.artifactError ?? job.artifactError;
|
|
411
|
+
if (artifactError) {
|
|
412
|
+
lines.push(uiTheme.fg("warning", truncateToWidth(formatArtifactErrorNotice(artifactError), width)));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const hiddenSettled = settledJobs.length - PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
416
|
+
if (hiddenSettled > 0) lines.push(`${indent}${uiTheme.fg("dim", `… ${hiddenSettled} more settled`)}`);
|
|
417
|
+
const shownWidgetJobs = widgetJobs.slice(0, PREVIEW_LIMITS.COLLAPSED_ITEMS);
|
|
418
|
+
for (const job of shownWidgetJobs) {
|
|
419
|
+
const typeBadge = formatBadge(job.type, statusToColor(job.status), uiTheme);
|
|
420
|
+
const icon = formatStatusIcon("running", uiTheme, live ? options.spinnerFrame : undefined);
|
|
421
|
+
const meta = compact([
|
|
422
|
+
job.completionPercent !== undefined ? uiTheme.fg("accent", `${job.completionPercent}%`) : undefined,
|
|
423
|
+
uiTheme.fg("dim", formatDuration(job.durationMs)),
|
|
424
|
+
]).join(" ");
|
|
425
|
+
lines.push(
|
|
426
|
+
...renderJobTileLines(jobTile(job, live), width, uiTheme, {
|
|
427
|
+
lead: `${icon} ${typeBadge} `,
|
|
428
|
+
meta,
|
|
429
|
+
full: false,
|
|
430
|
+
}),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
// Running jobs without a drawn tile (no widget, or past the cap) collapse
|
|
434
|
+
// into one count; the header already carries it while nothing settled.
|
|
435
|
+
const quietRunning = counts.running - shownWidgetJobs.length;
|
|
436
|
+
if (quietRunning > 0 && (settledJobs.length > 0 || shownWidgetJobs.length < widgetJobs.length)) {
|
|
437
|
+
lines.push(`${indent}${uiTheme.fg("dim", `${quietRunning} still running`)}`);
|
|
438
|
+
}
|
|
439
|
+
return lines;
|
|
440
|
+
};
|
|
311
441
|
return {
|
|
312
442
|
render(width: number): readonly string[] {
|
|
313
443
|
const expanded = options.expanded;
|
|
@@ -326,96 +456,125 @@ function jobsRenderResult(
|
|
|
326
456
|
.u32(spinnerFrame)
|
|
327
457
|
.bool(shimmerActive)
|
|
328
458
|
.bool(showModelBadge)
|
|
459
|
+
.str(widgetStamp(jobs))
|
|
329
460
|
.digest();
|
|
330
461
|
if (!shimmerActive && cached?.key === key) return cached.lines;
|
|
331
462
|
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
463
|
+
const compactView = !expanded && jobs.length > 0;
|
|
464
|
+
const itemLines = compactView
|
|
465
|
+
? compactJobLines(width, counts.running > 0 && options.spinnerFrame !== undefined)
|
|
466
|
+
: renderTreeList<JobSnapshot>(
|
|
467
|
+
{
|
|
468
|
+
items: sortedJobs,
|
|
469
|
+
expanded,
|
|
470
|
+
maxCollapsed: PREVIEW_LIMITS.COLLAPSED_ITEMS,
|
|
471
|
+
itemType: "job",
|
|
472
|
+
renderItem: (job, context) => {
|
|
473
|
+
const rowWidth = Math.max(0, width - (context.prefixWidth ?? 0));
|
|
474
|
+
const lines: string[] = [];
|
|
475
|
+
const icon = `${formatStatusIcon(
|
|
476
|
+
statusToIcon(job.status),
|
|
477
|
+
uiTheme,
|
|
478
|
+
job.status === "running" ? options.spinnerFrame : undefined,
|
|
479
|
+
)}${job.exitCode === undefined ? "" : `${uiTheme.sep.dot}${uiTheme.fg(job.exitCode === 0 ? "muted" : "error", `exit ${job.exitCode}`)}`}`;
|
|
480
|
+
const typeBadge = formatBadge(job.type, statusToColor(job.status), uiTheme);
|
|
481
|
+
const completion =
|
|
482
|
+
job.status === "running" && job.completionPercent !== undefined
|
|
483
|
+
? `${uiTheme.sep.dot}${uiTheme.fg("accent", `${job.completionPercent}%`)}`
|
|
484
|
+
: "";
|
|
485
|
+
const durationSuffix = `${completion}${uiTheme.sep.dot}${uiTheme.fg("dim", formatDuration(job.durationMs))}`;
|
|
486
|
+
const displayId = truncateToWidth(
|
|
487
|
+
replaceTabs(job.id).replace(/\s+/g, " "),
|
|
488
|
+
Math.max(0, rowWidth - visibleWidth(`${icon} ${typeBadge} ${durationSuffix}`)),
|
|
489
|
+
Ellipsis.Unicode,
|
|
490
|
+
);
|
|
491
|
+
const rowLabel = jobRowLabel(job);
|
|
492
|
+
const rawLabelLines = (rowLabel || "(no label)").split(/\r?\n/);
|
|
493
|
+
const maxLabelLines = expanded ? LABEL_LINES_EXPANDED : LABEL_LINES_COLLAPSED;
|
|
494
|
+
const visibleLabelLines = rawLabelLines
|
|
495
|
+
.slice(0, maxLabelLines)
|
|
496
|
+
.map(l => truncateToWidth(replaceTabs(l), LABEL_MAX_WIDTH, Ellipsis.Unicode));
|
|
497
|
+
if (rawLabelLines.length > maxLabelLines && visibleLabelLines.length > 0) {
|
|
498
|
+
const last = visibleLabelLines[visibleLabelLines.length - 1]!;
|
|
499
|
+
visibleLabelLines[visibleLabelLines.length - 1] = `${last} …`;
|
|
500
|
+
}
|
|
501
|
+
const rowPrefix = `${icon} ${typeBadge} `;
|
|
502
|
+
const live = job.status === "running" && options.spinnerFrame !== undefined;
|
|
503
|
+
const continuationWidth = Math.max(0, rowWidth - visibleWidth(" "));
|
|
504
|
+
if (job.widget?.state === "live") {
|
|
505
|
+
const meta = compact([
|
|
506
|
+
job.status === "running" && job.completionPercent !== undefined
|
|
507
|
+
? uiTheme.fg("accent", `${job.completionPercent}%`)
|
|
508
|
+
: undefined,
|
|
509
|
+
uiTheme.fg("dim", formatDuration(job.durationMs)),
|
|
510
|
+
]).join(" ");
|
|
511
|
+
lines.push(
|
|
512
|
+
...renderJobTileLines(jobTile(job, live), rowWidth, uiTheme, {
|
|
513
|
+
lead: rowPrefix,
|
|
514
|
+
meta,
|
|
515
|
+
full: expanded,
|
|
516
|
+
}),
|
|
517
|
+
);
|
|
518
|
+
} else {
|
|
519
|
+
const modelIdentity = job.resolvedModelIdentity ?? job.resolvedModel;
|
|
520
|
+
const modelBadge =
|
|
521
|
+
job.type === "task" && showModelBadge && typeof modelIdentity === "string"
|
|
522
|
+
? formatFeedModelBadge(
|
|
523
|
+
modelIdentity,
|
|
524
|
+
job.resolvedThinkingLevel,
|
|
525
|
+
job.advisor === true,
|
|
526
|
+
uiTheme,
|
|
527
|
+
Math.min(
|
|
528
|
+
FEED_MODEL_BADGE_WIDTH,
|
|
529
|
+
Math.max(
|
|
530
|
+
0,
|
|
531
|
+
rowWidth - visibleWidth(`${rowPrefix}${displayId}${durationSuffix}`) - 1,
|
|
532
|
+
),
|
|
533
|
+
),
|
|
534
|
+
)
|
|
535
|
+
: "";
|
|
536
|
+
const modelLead = modelBadge ? `${modelBadge} ` : "";
|
|
537
|
+
const headRaw = displayId;
|
|
538
|
+
// Running rows in a live block shimmer their label; once the block
|
|
539
|
+
// stops animating (sealed, or a settled snapshot — spinnerFrame
|
|
540
|
+
// cleared) they render static so scrollback never keeps a mid-sweep
|
|
541
|
+
// shimmer band.
|
|
542
|
+
const headLabel = live
|
|
543
|
+
? shimmerEnabled()
|
|
544
|
+
? shimmerText(headRaw, uiTheme)
|
|
545
|
+
: uiTheme.fg("accent", headRaw)
|
|
546
|
+
: uiTheme.fg("toolOutput", headRaw);
|
|
547
|
+
let row = `${rowPrefix}${modelLead}${headLabel}`;
|
|
548
|
+
const distinctLabel = rowLabel.trim() !== job.id;
|
|
549
|
+
const label = visibleLabelLines[0] ?? "";
|
|
550
|
+
const inlineLabel =
|
|
551
|
+
distinctLabel && visibleWidth(`${row} ${label}${durationSuffix}`) <= rowWidth;
|
|
552
|
+
if (inlineLabel) row += ` ${uiTheme.fg("toolOutput", label)}`;
|
|
553
|
+
row += durationSuffix;
|
|
554
|
+
lines.push(truncateToWidth(row, rowWidth, ""));
|
|
555
|
+
for (let i = distinctLabel && !inlineLabel ? 0 : 1; i < visibleLabelLines.length; i++) {
|
|
556
|
+
lines.push(
|
|
557
|
+
` ${uiTheme.fg("toolOutput", truncateToWidth(visibleLabelLines[i]!, continuationWidth))}`,
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const artifactError = job.meta?.artifactError ?? job.artifactError;
|
|
562
|
+
if (artifactError) {
|
|
563
|
+
lines.push(
|
|
564
|
+
uiTheme.fg(
|
|
565
|
+
"warning",
|
|
566
|
+
truncateToWidth(formatArtifactErrorNotice(artifactError), rowWidth),
|
|
378
567
|
),
|
|
379
|
-
)
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
? shimmerEnabled()
|
|
390
|
-
? shimmerText(headRaw, uiTheme)
|
|
391
|
-
: uiTheme.fg("accent", headRaw)
|
|
392
|
-
: uiTheme.fg("toolOutput", headRaw);
|
|
393
|
-
let row = `${rowPrefix}${modelLead}${headLabel}`;
|
|
394
|
-
const distinctLabel = job.label.trim() !== job.id;
|
|
395
|
-
const label = visibleLabelLines[0] ?? "";
|
|
396
|
-
const inlineLabel = distinctLabel && visibleWidth(`${row} ${label}${durationSuffix}`) <= rowWidth;
|
|
397
|
-
if (inlineLabel) row += ` ${uiTheme.fg("toolOutput", label)}`;
|
|
398
|
-
row += durationSuffix;
|
|
399
|
-
lines.push(truncateToWidth(row, rowWidth, ""));
|
|
400
|
-
const continuationWidth = Math.max(0, rowWidth - visibleWidth(" "));
|
|
401
|
-
for (let i = distinctLabel && !inlineLabel ? 0 : 1; i < visibleLabelLines.length; i++) {
|
|
402
|
-
lines.push(
|
|
403
|
-
` ${uiTheme.fg("toolOutput", truncateToWidth(visibleLabelLines[i]!, continuationWidth))}`,
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
const artifactError = job.meta?.artifactError ?? job.artifactError;
|
|
407
|
-
if (artifactError) {
|
|
408
|
-
lines.push(
|
|
409
|
-
uiTheme.fg("warning", truncateToWidth(formatArtifactErrorNotice(artifactError), rowWidth)),
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
for (const previewLine of jobPreviewRows(job, expanded, continuationWidth)) lines.push(previewLine);
|
|
414
|
-
return lines;
|
|
415
|
-
},
|
|
416
|
-
},
|
|
417
|
-
uiTheme,
|
|
418
|
-
);
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
for (const previewLine of jobPreviewRows(job, expanded, continuationWidth))
|
|
572
|
+
lines.push(previewLine);
|
|
573
|
+
return lines;
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
uiTheme,
|
|
577
|
+
);
|
|
419
578
|
|
|
420
579
|
// Agents run outside job control; render them as their own tree so
|
|
421
580
|
// they never skew the job counts or the "waiting on N jobs" title.
|
|
@@ -458,7 +617,7 @@ function jobsRenderResult(
|
|
|
458
617
|
uiTheme,
|
|
459
618
|
);
|
|
460
619
|
|
|
461
|
-
const all = [header];
|
|
620
|
+
const all = compactView && settledJobs.length > 0 ? [] : [header];
|
|
462
621
|
if (aggregateArtifactError) {
|
|
463
622
|
all.push(uiTheme.fg("warning", formatArtifactErrorNotice(aggregateArtifactError)));
|
|
464
623
|
}
|
|
@@ -498,7 +657,11 @@ function bodyLines(
|
|
|
498
657
|
body: string,
|
|
499
658
|
expanded: boolean,
|
|
500
659
|
theme: Theme,
|
|
501
|
-
options: {
|
|
660
|
+
options: {
|
|
661
|
+
indent?: string;
|
|
662
|
+
tone?: "dim" | "toolOutput";
|
|
663
|
+
collapsedLines?: number;
|
|
664
|
+
} = {},
|
|
502
665
|
): string[] {
|
|
503
666
|
const indent = options.indent ?? "";
|
|
504
667
|
const tone = options.tone ?? "toolOutput";
|
|
@@ -556,17 +719,18 @@ export function createIrcMessageCard(
|
|
|
556
719
|
if (card.replyTo) meta.push("reply");
|
|
557
720
|
const age = messageAge(card.timestamp);
|
|
558
721
|
if (age) meta.push(age);
|
|
559
|
-
const component = createCachedComponent(
|
|
560
|
-
|
|
561
|
-
(
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
722
|
+
const component = createCachedComponent(getExpanded, (width, expanded) => {
|
|
723
|
+
const lines = [renderStatusLine({ iconOverride: ircGlyph(uiTheme), title, meta }, uiTheme)];
|
|
724
|
+
if (body.trim()) {
|
|
725
|
+
lines.push(
|
|
726
|
+
...bodyLines(body, expanded, uiTheme, {
|
|
727
|
+
indent: " ",
|
|
728
|
+
collapsedLines: 3,
|
|
729
|
+
}),
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
return lines.map(line => truncateToWidth(line, width, Ellipsis.Unicode));
|
|
733
|
+
});
|
|
570
734
|
// Terminal-local collapse replaces `getExpanded`; the node never changes after creation.
|
|
571
735
|
const described = cardNode(
|
|
572
736
|
{
|
|
@@ -589,47 +753,6 @@ export function createIrcMessageCard(
|
|
|
589
753
|
return Object.assign(component, { describe: () => described });
|
|
590
754
|
}
|
|
591
755
|
|
|
592
|
-
/**
|
|
593
|
-
* One job row on one line: type badge, id + label, terminal-clocked duration
|
|
594
|
-
* (live while running). The job's output and errors fill the card of the call
|
|
595
|
-
* that started it (`parksAsBackground`), so the row carries no preview.
|
|
596
|
-
*/
|
|
597
|
-
function describeJob(job: JobSnapshot, isPartial: boolean): NativeNode {
|
|
598
|
-
const running = job.status === "running" && isPartial;
|
|
599
|
-
const label = job.label.trim() !== job.id ? plainText(job.label.split(/\r?\n/)[0] ?? "") : "";
|
|
600
|
-
const spans: TspSpan[] = [
|
|
601
|
-
span(plainText(job.id), running ? "accent" : "toolOutput", running ? { fx: "shimmer" } : undefined),
|
|
602
|
-
];
|
|
603
|
-
if (label) spans.push(span(` ${label}`, "toolOutput"));
|
|
604
|
-
if (job.exitCode !== undefined) spans.push(span(` exit ${job.exitCode}`, job.exitCode === 0 ? "muted" : "error"));
|
|
605
|
-
const tone =
|
|
606
|
-
job.status === "completed"
|
|
607
|
-
? "success"
|
|
608
|
-
: job.status === "failed"
|
|
609
|
-
? "error"
|
|
610
|
-
: job.status === "cancelled"
|
|
611
|
-
? "warning"
|
|
612
|
-
: "accent";
|
|
613
|
-
return node(
|
|
614
|
-
"row",
|
|
615
|
-
{ role: "zeta.wait.job", tone, gap: "sm" },
|
|
616
|
-
compact([
|
|
617
|
-
node("badge", { text: job.type, tone }),
|
|
618
|
-
text(spans, { truncate: "end", grow: 1 }),
|
|
619
|
-
job.status === "running" && job.completionPercent !== undefined
|
|
620
|
-
? node("progress", {
|
|
621
|
-
value: job.completionPercent / 100,
|
|
622
|
-
label: `${job.completionPercent}%`,
|
|
623
|
-
tone: "accent",
|
|
624
|
-
title: "Agent's own completion estimate",
|
|
625
|
-
})
|
|
626
|
-
: undefined,
|
|
627
|
-
elapsed(job.durationMs, !running),
|
|
628
|
-
]),
|
|
629
|
-
job.id,
|
|
630
|
-
);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
756
|
function describeJobsResult(
|
|
634
757
|
result: ToolRenderResult<CoordinationDetails>,
|
|
635
758
|
isPartial: boolean,
|
|
@@ -658,9 +781,25 @@ function describeJobsResult(
|
|
|
658
781
|
if (counts.completed > 0) head.push(span(` ${counts.completed} done`, "success"));
|
|
659
782
|
if (counts.failed > 0) head.push(span(` ${counts.failed} failed`, "error"));
|
|
660
783
|
if (counts.cancelled > 0) head.push(span(` ${counts.cancelled} cancelled`, "warning"));
|
|
661
|
-
const order: Record<JobSnapshot["status"], number> = {
|
|
784
|
+
const order: Record<JobSnapshot["status"], number> = {
|
|
785
|
+
running: 0,
|
|
786
|
+
failed: 1,
|
|
787
|
+
cancelled: 2,
|
|
788
|
+
completed: 3,
|
|
789
|
+
};
|
|
662
790
|
const sorted = [...jobs].sort((a, b) => order[a.status] - order[b.status] || b.durationMs - a.durationMs);
|
|
663
|
-
|
|
791
|
+
// One frame of tiles: each job's status, title, figure and widget together. The job's output
|
|
792
|
+
// and errors fill the card of the call that started it (`parksAsBackground`), so no previews.
|
|
793
|
+
const body: NativeNode[] = [];
|
|
794
|
+
if (sorted.length > 0) {
|
|
795
|
+
body.push(keyed(describeJobTiles(sorted.slice(0, TILES_SHOWN).map(job => jobTile(job, isPartial))), "tiles"));
|
|
796
|
+
}
|
|
797
|
+
if (sorted.length > TILES_SHOWN) {
|
|
798
|
+
const rest = sorted.length - TILES_SHOWN;
|
|
799
|
+
body.push(
|
|
800
|
+
text([span(`+${rest} more ${rest === 1 ? "job" : "jobs"} · /jobs`, "dim")], { role: "zeta.wait.more" }),
|
|
801
|
+
);
|
|
802
|
+
}
|
|
664
803
|
for (const agent of agents) {
|
|
665
804
|
const spans: TspSpan[] = [span(plainText(agent.id), "muted")];
|
|
666
805
|
if (agent.activity) spans.push(span(` ${plainText(agent.activity)}`, "toolOutput"));
|
|
@@ -684,6 +823,8 @@ function describeJobsResult(
|
|
|
684
823
|
return {
|
|
685
824
|
head,
|
|
686
825
|
tone: counts.failed > 0 ? "warning" : counts.running > 0 || agents.length > 0 ? "info" : "success",
|
|
826
|
+
// The tiles are what a wait shows: clamping the body by lines would cut them.
|
|
827
|
+
preview: "children",
|
|
687
828
|
body,
|
|
688
829
|
};
|
|
689
830
|
}
|
|
@@ -729,11 +870,15 @@ export const waitToolRenderer = {
|
|
|
729
870
|
result: ToolRenderResult<CoordinationDetails>,
|
|
730
871
|
options: RenderResultOptions,
|
|
731
872
|
): NativeToolView | undefined {
|
|
732
|
-
return waitResultMemo.get(result, [options.isPartial], () => {
|
|
873
|
+
return waitResultMemo.get(result, [options.isPartial, widgetStamp(result.details?.jobs)], () => {
|
|
733
874
|
if (result.isError) return errorView("Wait", resultText(result));
|
|
734
875
|
const details = result.details;
|
|
735
876
|
if (details?.interrupted) {
|
|
736
|
-
return {
|
|
877
|
+
return {
|
|
878
|
+
head: toolHead("Wait", "interrupted by message"),
|
|
879
|
+
tone: "info",
|
|
880
|
+
inline: true,
|
|
881
|
+
};
|
|
737
882
|
}
|
|
738
883
|
const waited = details?.waited;
|
|
739
884
|
if (!waited) return describeJobsResult(result, options.isPartial);
|
|
@@ -741,7 +886,11 @@ export const waitToolRenderer = {
|
|
|
741
886
|
});
|
|
742
887
|
},
|
|
743
888
|
renderResult(
|
|
744
|
-
result: {
|
|
889
|
+
result: {
|
|
890
|
+
content: Array<{ type: string; text?: string }>;
|
|
891
|
+
details?: CoordinationDetails;
|
|
892
|
+
isError?: boolean;
|
|
893
|
+
},
|
|
745
894
|
options: RenderResultOptions,
|
|
746
895
|
uiTheme: Theme,
|
|
747
896
|
): Component {
|