@linxiraos/pi-tui 1.4.0 → 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 +178 -182
- 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/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 +5 -0
- 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/irc.d.ts +6 -0
- 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 +5 -0
- package/package.json +9 -9
- 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/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 +5 -0
- package/src/terminal-capabilities.ts +43 -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/irc.d.ts +6 -0
- package/src/tools/irc.ts +6 -0
- 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/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 +5 -0
- package/src/tools/wait.ts +290 -147
- package/src/tui.ts +45 -5
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. */
|
|
@@ -200,6 +205,40 @@ function flattenStructuredPreview(text: string): string {
|
|
|
200
205
|
return text.slice(0, PREVIEW_LINES_EXPANDED * PREVIEW_LINE_WIDTH * 2).replace(/\s+/g, " ");
|
|
201
206
|
}
|
|
202
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
|
+
|
|
203
242
|
/** Pending wait frame. */
|
|
204
243
|
function waitRenderCall(_args: object, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
205
244
|
return new Text(renderStatusLine({ icon: "pending", title: "Wait" }, uiTheme), 0, 0);
|
|
@@ -207,7 +246,11 @@ function waitRenderCall(_args: object, _options: RenderResultOptions, uiTheme: T
|
|
|
207
246
|
|
|
208
247
|
/** Result frame for wait snapshots and the agents roster. */
|
|
209
248
|
function jobsRenderResult(
|
|
210
|
-
result: {
|
|
249
|
+
result: {
|
|
250
|
+
content: Array<{ type: string; text?: string }>;
|
|
251
|
+
details?: CoordinationDetails;
|
|
252
|
+
isError?: boolean;
|
|
253
|
+
},
|
|
211
254
|
options: RenderResultOptions,
|
|
212
255
|
uiTheme: Theme,
|
|
213
256
|
): Component {
|
|
@@ -314,6 +357,87 @@ function jobsRenderResult(
|
|
|
314
357
|
previewRows.set(job, { expanded, width: continuationWidth, lines });
|
|
315
358
|
return lines;
|
|
316
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
|
+
};
|
|
317
441
|
return {
|
|
318
442
|
render(width: number): readonly string[] {
|
|
319
443
|
const expanded = options.expanded;
|
|
@@ -332,96 +456,125 @@ function jobsRenderResult(
|
|
|
332
456
|
.u32(spinnerFrame)
|
|
333
457
|
.bool(shimmerActive)
|
|
334
458
|
.bool(showModelBadge)
|
|
459
|
+
.str(widgetStamp(jobs))
|
|
335
460
|
.digest();
|
|
336
461
|
if (!shimmerActive && cached?.key === key) return cached.lines;
|
|
337
462
|
|
|
338
|
-
const
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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),
|
|
384
567
|
),
|
|
385
|
-
)
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
? shimmerEnabled()
|
|
396
|
-
? shimmerText(headRaw, uiTheme)
|
|
397
|
-
: uiTheme.fg("accent", headRaw)
|
|
398
|
-
: uiTheme.fg("toolOutput", headRaw);
|
|
399
|
-
let row = `${rowPrefix}${modelLead}${headLabel}`;
|
|
400
|
-
const distinctLabel = job.label.trim() !== job.id;
|
|
401
|
-
const label = visibleLabelLines[0] ?? "";
|
|
402
|
-
const inlineLabel = distinctLabel && visibleWidth(`${row} ${label}${durationSuffix}`) <= rowWidth;
|
|
403
|
-
if (inlineLabel) row += ` ${uiTheme.fg("toolOutput", label)}`;
|
|
404
|
-
row += durationSuffix;
|
|
405
|
-
lines.push(truncateToWidth(row, rowWidth, ""));
|
|
406
|
-
const continuationWidth = Math.max(0, rowWidth - visibleWidth(" "));
|
|
407
|
-
for (let i = distinctLabel && !inlineLabel ? 0 : 1; i < visibleLabelLines.length; i++) {
|
|
408
|
-
lines.push(
|
|
409
|
-
` ${uiTheme.fg("toolOutput", truncateToWidth(visibleLabelLines[i]!, continuationWidth))}`,
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
const artifactError = job.meta?.artifactError ?? job.artifactError;
|
|
413
|
-
if (artifactError) {
|
|
414
|
-
lines.push(
|
|
415
|
-
uiTheme.fg("warning", truncateToWidth(formatArtifactErrorNotice(artifactError), rowWidth)),
|
|
416
|
-
);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
for (const previewLine of jobPreviewRows(job, expanded, continuationWidth)) lines.push(previewLine);
|
|
420
|
-
return lines;
|
|
421
|
-
},
|
|
422
|
-
},
|
|
423
|
-
uiTheme,
|
|
424
|
-
);
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
for (const previewLine of jobPreviewRows(job, expanded, continuationWidth))
|
|
572
|
+
lines.push(previewLine);
|
|
573
|
+
return lines;
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
uiTheme,
|
|
577
|
+
);
|
|
425
578
|
|
|
426
579
|
// Agents run outside job control; render them as their own tree so
|
|
427
580
|
// they never skew the job counts or the "waiting on N jobs" title.
|
|
@@ -464,7 +617,7 @@ function jobsRenderResult(
|
|
|
464
617
|
uiTheme,
|
|
465
618
|
);
|
|
466
619
|
|
|
467
|
-
const all = [header];
|
|
620
|
+
const all = compactView && settledJobs.length > 0 ? [] : [header];
|
|
468
621
|
if (aggregateArtifactError) {
|
|
469
622
|
all.push(uiTheme.fg("warning", formatArtifactErrorNotice(aggregateArtifactError)));
|
|
470
623
|
}
|
|
@@ -504,7 +657,11 @@ function bodyLines(
|
|
|
504
657
|
body: string,
|
|
505
658
|
expanded: boolean,
|
|
506
659
|
theme: Theme,
|
|
507
|
-
options: {
|
|
660
|
+
options: {
|
|
661
|
+
indent?: string;
|
|
662
|
+
tone?: "dim" | "toolOutput";
|
|
663
|
+
collapsedLines?: number;
|
|
664
|
+
} = {},
|
|
508
665
|
): string[] {
|
|
509
666
|
const indent = options.indent ?? "";
|
|
510
667
|
const tone = options.tone ?? "toolOutput";
|
|
@@ -562,17 +719,18 @@ export function createIrcMessageCard(
|
|
|
562
719
|
if (card.replyTo) meta.push("reply");
|
|
563
720
|
const age = messageAge(card.timestamp);
|
|
564
721
|
if (age) meta.push(age);
|
|
565
|
-
const component = createCachedComponent(
|
|
566
|
-
|
|
567
|
-
(
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
+
});
|
|
576
734
|
// Terminal-local collapse replaces `getExpanded`; the node never changes after creation.
|
|
577
735
|
const described = cardNode(
|
|
578
736
|
{
|
|
@@ -595,47 +753,6 @@ export function createIrcMessageCard(
|
|
|
595
753
|
return Object.assign(component, { describe: () => described });
|
|
596
754
|
}
|
|
597
755
|
|
|
598
|
-
/**
|
|
599
|
-
* One job row on one line: type badge, id + label, terminal-clocked duration
|
|
600
|
-
* (live while running). The job's output and errors fill the card of the call
|
|
601
|
-
* that started it (`parksAsBackground`), so the row carries no preview.
|
|
602
|
-
*/
|
|
603
|
-
function describeJob(job: JobSnapshot, isPartial: boolean): NativeNode {
|
|
604
|
-
const running = job.status === "running" && isPartial;
|
|
605
|
-
const label = job.label.trim() !== job.id ? plainText(job.label.split(/\r?\n/)[0] ?? "") : "";
|
|
606
|
-
const spans: TspSpan[] = [
|
|
607
|
-
span(plainText(job.id), running ? "accent" : "toolOutput", running ? { fx: "shimmer" } : undefined),
|
|
608
|
-
];
|
|
609
|
-
if (label) spans.push(span(` ${label}`, "toolOutput"));
|
|
610
|
-
if (job.exitCode !== undefined) spans.push(span(` exit ${job.exitCode}`, job.exitCode === 0 ? "muted" : "error"));
|
|
611
|
-
const tone =
|
|
612
|
-
job.status === "completed"
|
|
613
|
-
? "success"
|
|
614
|
-
: job.status === "failed"
|
|
615
|
-
? "error"
|
|
616
|
-
: job.status === "cancelled"
|
|
617
|
-
? "warning"
|
|
618
|
-
: "accent";
|
|
619
|
-
return node(
|
|
620
|
-
"row",
|
|
621
|
-
{ role: "zeta.wait.job", tone, gap: "sm" },
|
|
622
|
-
compact([
|
|
623
|
-
node("badge", { text: job.type, tone }),
|
|
624
|
-
text(spans, { truncate: "end", grow: 1 }),
|
|
625
|
-
job.status === "running" && job.completionPercent !== undefined
|
|
626
|
-
? node("progress", {
|
|
627
|
-
value: job.completionPercent / 100,
|
|
628
|
-
label: `${job.completionPercent}%`,
|
|
629
|
-
tone: "accent",
|
|
630
|
-
title: "Agent's own completion estimate",
|
|
631
|
-
})
|
|
632
|
-
: undefined,
|
|
633
|
-
elapsed(job.durationMs, !running),
|
|
634
|
-
]),
|
|
635
|
-
job.id,
|
|
636
|
-
);
|
|
637
|
-
}
|
|
638
|
-
|
|
639
756
|
function describeJobsResult(
|
|
640
757
|
result: ToolRenderResult<CoordinationDetails>,
|
|
641
758
|
isPartial: boolean,
|
|
@@ -664,9 +781,25 @@ function describeJobsResult(
|
|
|
664
781
|
if (counts.completed > 0) head.push(span(` ${counts.completed} done`, "success"));
|
|
665
782
|
if (counts.failed > 0) head.push(span(` ${counts.failed} failed`, "error"));
|
|
666
783
|
if (counts.cancelled > 0) head.push(span(` ${counts.cancelled} cancelled`, "warning"));
|
|
667
|
-
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
|
+
};
|
|
668
790
|
const sorted = [...jobs].sort((a, b) => order[a.status] - order[b.status] || b.durationMs - a.durationMs);
|
|
669
|
-
|
|
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
|
+
}
|
|
670
803
|
for (const agent of agents) {
|
|
671
804
|
const spans: TspSpan[] = [span(plainText(agent.id), "muted")];
|
|
672
805
|
if (agent.activity) spans.push(span(` ${plainText(agent.activity)}`, "toolOutput"));
|
|
@@ -690,6 +823,8 @@ function describeJobsResult(
|
|
|
690
823
|
return {
|
|
691
824
|
head,
|
|
692
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",
|
|
693
828
|
body,
|
|
694
829
|
};
|
|
695
830
|
}
|
|
@@ -735,11 +870,15 @@ export const waitToolRenderer = {
|
|
|
735
870
|
result: ToolRenderResult<CoordinationDetails>,
|
|
736
871
|
options: RenderResultOptions,
|
|
737
872
|
): NativeToolView | undefined {
|
|
738
|
-
return waitResultMemo.get(result, [options.isPartial], () => {
|
|
873
|
+
return waitResultMemo.get(result, [options.isPartial, widgetStamp(result.details?.jobs)], () => {
|
|
739
874
|
if (result.isError) return errorView("Wait", resultText(result));
|
|
740
875
|
const details = result.details;
|
|
741
876
|
if (details?.interrupted) {
|
|
742
|
-
return {
|
|
877
|
+
return {
|
|
878
|
+
head: toolHead("Wait", "interrupted by message"),
|
|
879
|
+
tone: "info",
|
|
880
|
+
inline: true,
|
|
881
|
+
};
|
|
743
882
|
}
|
|
744
883
|
const waited = details?.waited;
|
|
745
884
|
if (!waited) return describeJobsResult(result, options.isPartial);
|
|
@@ -747,7 +886,11 @@ export const waitToolRenderer = {
|
|
|
747
886
|
});
|
|
748
887
|
},
|
|
749
888
|
renderResult(
|
|
750
|
-
result: {
|
|
889
|
+
result: {
|
|
890
|
+
content: Array<{ type: string; text?: string }>;
|
|
891
|
+
details?: CoordinationDetails;
|
|
892
|
+
isError?: boolean;
|
|
893
|
+
},
|
|
751
894
|
options: RenderResultOptions,
|
|
752
895
|
uiTheme: Theme,
|
|
753
896
|
): Component {
|
package/src/tui.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { assumedTspHello, NativeBackend, type NativeHost } from "./native/backen
|
|
|
28
28
|
import { col } from "./native/describe";
|
|
29
29
|
import { TSP_PREFIX, type TspHello } from "./native/encode";
|
|
30
30
|
import type { DescribeContext, NativeNode, NativeScreen, NativeSurfaceProvider, NativeUiEvent } from "./native/node";
|
|
31
|
+
import { cropSixelTop } from "./sixel";
|
|
31
32
|
import { STDOUT_BACKLOG_CLEAR_BYTES, setAltScreenActive, type Terminal } from "./terminal";
|
|
32
33
|
import { classifyTerminalMultiplexerModule, terminalMultiplexerSessions } from "./terminal-multiplexer";
|
|
33
34
|
import {
|
|
@@ -36,6 +37,7 @@ import {
|
|
|
36
37
|
encodeKittyDeleteImage,
|
|
37
38
|
encodeKittyDeletePlacement,
|
|
38
39
|
encodeKittyPlacementLine,
|
|
40
|
+
getCellDimensions,
|
|
39
41
|
getTerminalBackgroundColor,
|
|
40
42
|
ImageProtocol,
|
|
41
43
|
isImageProtocolForced,
|
|
@@ -1022,6 +1024,8 @@ export class TUI extends Container {
|
|
|
1022
1024
|
#screenImagePlacements = new Map<number, LiveImagePlacement>();
|
|
1023
1025
|
#altImagePlacements = new Map<number, LiveImagePlacement>();
|
|
1024
1026
|
#nextImagePlacementId = 1;
|
|
1027
|
+
/** Last top-clipped SIXEL crop; only the block straddling row 0 is ever cropped. */
|
|
1028
|
+
#sixelCrop: { source: string; pixels: number; maxHeightPixels: number; output: string } | undefined;
|
|
1025
1029
|
#ghosttyInitialImageDelayDone = false;
|
|
1026
1030
|
#ghosttyInitialImageDelayTimer: RenderTimer | undefined;
|
|
1027
1031
|
#ghosttyImageReadyAtMs = 0;
|
|
@@ -3386,10 +3390,11 @@ export class TUI extends Container {
|
|
|
3386
3390
|
}
|
|
3387
3391
|
|
|
3388
3392
|
/**
|
|
3389
|
-
* Clip
|
|
3390
|
-
*
|
|
3391
|
-
*
|
|
3392
|
-
*
|
|
3393
|
+
* Clip direct placements to the visible source slice: Kitty tracks a live
|
|
3394
|
+
* placement owner, while SIXEL crops the encoded raster (iTerm2 images are
|
|
3395
|
+
* not cropped). SIXEL and iTerm2 have no delete command and composite over
|
|
3396
|
+
* existing pixels, so erase the visible rows they paint above `screenRow`
|
|
3397
|
+
* before repainting.
|
|
3393
3398
|
*/
|
|
3394
3399
|
#imageLineSequence(
|
|
3395
3400
|
line: string,
|
|
@@ -3400,7 +3405,16 @@ export class TUI extends Container {
|
|
|
3400
3405
|
if (screenRow < 0) return line;
|
|
3401
3406
|
const parsed = parseKittyDirectPlacementLine(line);
|
|
3402
3407
|
if (!parsed) {
|
|
3403
|
-
const
|
|
3408
|
+
const requestedLift = directPlacementLift(line);
|
|
3409
|
+
const lift = Math.min(requestedLift, screenRow);
|
|
3410
|
+
if (requestedLift > lift) {
|
|
3411
|
+
const cellHeight = getCellDimensions().heightPx;
|
|
3412
|
+
const cropPixels = Math.round((requestedLift - lift) * cellHeight);
|
|
3413
|
+
const maxHeightPixels = Math.floor((screenRow + 1) * cellHeight);
|
|
3414
|
+
line = line.replace(/\x1bP[0-9;]*q[\s\S]*?\x1b\\/g, sequence =>
|
|
3415
|
+
this.#cropSixel(sequence, cropPixels, maxHeightPixels),
|
|
3416
|
+
);
|
|
3417
|
+
}
|
|
3404
3418
|
return lift > 0 ? `\x1b[${lift}A${`${ERASE_LINE}\x1b[B`.repeat(lift)}${line}` : line;
|
|
3405
3419
|
}
|
|
3406
3420
|
const previous = placements?.get(placementRow);
|
|
@@ -3420,6 +3434,32 @@ export class TUI extends Container {
|
|
|
3420
3434
|
);
|
|
3421
3435
|
}
|
|
3422
3436
|
|
|
3437
|
+
/**
|
|
3438
|
+
* Crop a SIXEL raster whose top rows lie above the viewport. A raster the
|
|
3439
|
+
* cropper cannot parse is dropped: painting it whole would overwrite the
|
|
3440
|
+
* rows below the visible slice.
|
|
3441
|
+
*/
|
|
3442
|
+
#cropSixel(source: string, pixels: number, maxHeightPixels: number): string {
|
|
3443
|
+
const cached = this.#sixelCrop;
|
|
3444
|
+
if (
|
|
3445
|
+
cached !== undefined &&
|
|
3446
|
+
cached.pixels === pixels &&
|
|
3447
|
+
cached.maxHeightPixels === maxHeightPixels &&
|
|
3448
|
+
cached.source === source
|
|
3449
|
+
) {
|
|
3450
|
+
return cached.output;
|
|
3451
|
+
}
|
|
3452
|
+
let output: string;
|
|
3453
|
+
try {
|
|
3454
|
+
output = cropSixelTop(source, pixels, maxHeightPixels);
|
|
3455
|
+
} catch (err) {
|
|
3456
|
+
logger.warn("Dropping top-clipped SIXEL image that cannot be cropped", { err });
|
|
3457
|
+
output = "";
|
|
3458
|
+
}
|
|
3459
|
+
this.#sixelCrop = { source, pixels, maxHeightPixels, output };
|
|
3460
|
+
return output;
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3423
3463
|
#terminalLine(line: PreparedLine): string {
|
|
3424
3464
|
if (line.hasOsc8) return line.terminalContent + LINE_TERMINATOR;
|
|
3425
3465
|
return line.terminalContent.endsWith(SEGMENT_RESET) ? line.terminalContent : line.terminalContent + SEGMENT_RESET;
|