@oh-my-pi/pi-tui 18.12.2 → 18.12.4
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 +44 -48
- package/dist/types/autocomplete.d.ts +6 -0
- package/dist/types/chat/skill-message.d.ts +3 -4
- 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/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.ts +12 -1
- package/src/chat/skill-message.ts +19 -21
- package/src/chat/tool-execution.ts +35 -0
- package/src/chrome/transcript-container.ts +85 -12
- 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.ts +89 -0
- package/src/index.ts +1 -0
- package/src/native/backend.ts +25 -5
- package/src/native/describe.ts +18 -1
- 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/approval-dialog.ts +59 -13
- package/src/overlays/jobs-panel.ts +24 -2
- package/src/overlays/model-browser.ts +3 -0
- package/src/prompt/attachment-chips.ts +1 -1
- package/src/render/output-block.ts +25 -12
- package/src/render/tool-card.ts +6 -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.ts +43 -3
- package/src/theme/symbols.ts +5 -3
- package/src/tools/bash.ts +25 -9
- package/src/tools/default-renderer.ts +54 -17
- 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 +4 -6
- package/src/tools/irc.ts +6 -0
- package/src/tools/job-widget.ts +1036 -0
- package/src/tools/json-tree.ts +8 -0
- package/src/tools/line-ranges.ts +23 -13
- package/src/tools/mcp.ts +44 -1
- package/src/tools/output-meta.ts +1 -1
- package/src/tools/read.ts +64 -21
- package/src/tools/renderer.ts +8 -2
- package/src/tools/result-card.ts +1 -2
- package/src/tools/streaming-output.ts +11 -9
- package/src/tools/task.ts +25 -1
- package/src/tools/wait.ts +288 -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 "@oh-my-pi/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. */
|
|
@@ -194,6 +199,40 @@ function flattenStructuredPreview(text: string): string {
|
|
|
194
199
|
return text.slice(0, PREVIEW_LINES_EXPANDED * PREVIEW_LINE_WIDTH * 2).replace(/\s+/g, " ");
|
|
195
200
|
}
|
|
196
201
|
|
|
202
|
+
/** The row label: a running task's own stage line when it reports one, else the job's label. */
|
|
203
|
+
function jobRowLabel(job: JobSnapshot): string {
|
|
204
|
+
return job.type === "task" && job.status === "running" && job.stage?.trim() ? job.stage : job.label;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** A job as its tile; `live` while the card's clocks run. */
|
|
208
|
+
function jobTile(job: JobSnapshot, live: boolean): JobTile {
|
|
209
|
+
return {
|
|
210
|
+
id: job.id,
|
|
211
|
+
type: job.type,
|
|
212
|
+
status: job.status,
|
|
213
|
+
label: job.label,
|
|
214
|
+
durationMs: job.durationMs,
|
|
215
|
+
live: live && job.status === "running",
|
|
216
|
+
...(job.widget ? { widget: job.widget } : {}),
|
|
217
|
+
...(job.status === "running" && job.completionPercent !== undefined
|
|
218
|
+
? { completionPercent: job.completionPercent }
|
|
219
|
+
: {}),
|
|
220
|
+
...(job.stage ? { stage: job.stage } : {}),
|
|
221
|
+
...(job.exitCode === undefined ? {} : { exitCode: job.exitCode }),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Identity of the widgets a job list draws: changes whenever any widget's data or state does. */
|
|
226
|
+
function widgetStamp(jobs: readonly JobSnapshot[] | undefined): string {
|
|
227
|
+
if (!jobs) return "";
|
|
228
|
+
let stamp = "";
|
|
229
|
+
for (const job of jobs) if (job.widget) stamp += `${job.id}:${job.widget.updatedAt}:${job.widget.state};`;
|
|
230
|
+
return stamp;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Tiles a native wait card shows before it counts the rest. */
|
|
234
|
+
const TILES_SHOWN = 9;
|
|
235
|
+
|
|
197
236
|
/** Pending wait frame. */
|
|
198
237
|
function waitRenderCall(_args: object, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
199
238
|
return new Text(renderStatusLine({ icon: "pending", title: "Wait" }, uiTheme), 0, 0);
|
|
@@ -201,7 +240,11 @@ function waitRenderCall(_args: object, _options: RenderResultOptions, uiTheme: T
|
|
|
201
240
|
|
|
202
241
|
/** Result frame for wait snapshots and the agents roster. */
|
|
203
242
|
function jobsRenderResult(
|
|
204
|
-
result: {
|
|
243
|
+
result: {
|
|
244
|
+
content: Array<{ type: string; text?: string }>;
|
|
245
|
+
details?: CoordinationDetails;
|
|
246
|
+
isError?: boolean;
|
|
247
|
+
},
|
|
205
248
|
options: RenderResultOptions,
|
|
206
249
|
uiTheme: Theme,
|
|
207
250
|
): Component {
|
|
@@ -308,6 +351,87 @@ function jobsRenderResult(
|
|
|
308
351
|
previewRows.set(job, { expanded, width: continuationWidth, lines });
|
|
309
352
|
return lines;
|
|
310
353
|
};
|
|
354
|
+
// Collapsed card: one line per settled job, or the bare "waiting on N jobs"
|
|
355
|
+
// header while nothing has settled. Repeated polls then read as a short
|
|
356
|
+
// completion log instead of a re-printed (and shrinking) job tree; Ctrl+O
|
|
357
|
+
// restores the full tree with output previews. A running job's live widget
|
|
358
|
+
// still draws its tile: that chart is the job's progress, not a copy of it.
|
|
359
|
+
const settledJobs = sortedJobs.filter(job => job.status !== "running");
|
|
360
|
+
const widgetJobs = sortedJobs.filter(job => job.status === "running" && job.widget?.state === "live");
|
|
361
|
+
const compactJobLines = (width: number, live: boolean): string[] => {
|
|
362
|
+
const lines: string[] = [];
|
|
363
|
+
const indent = " ".repeat(visibleWidth(formatStatusIcon("done", uiTheme)) + 1);
|
|
364
|
+
// Failures sort first, so the cap never hides one behind successes;
|
|
365
|
+
// Ctrl+O lists the rest.
|
|
366
|
+
for (const job of settledJobs.slice(0, PREVIEW_LIMITS.COLLAPSED_ITEMS)) {
|
|
367
|
+
const icon = formatStatusIcon(statusToIcon(job.status), uiTheme);
|
|
368
|
+
const word = job.status === "completed" ? "done" : job.status;
|
|
369
|
+
const exit =
|
|
370
|
+
job.exitCode !== undefined && job.exitCode !== 0
|
|
371
|
+
? `${uiTheme.sep.dot}${uiTheme.fg("error", `exit ${job.exitCode}`)}`
|
|
372
|
+
: "";
|
|
373
|
+
// The outcome (status, exit, duration) is reserved first; a long id
|
|
374
|
+
// gives way so a failure is never cut off the row.
|
|
375
|
+
const outcome = ` ${uiTheme.fg(statusToColor(job.status), word)}${exit}${uiTheme.sep.dot}${uiTheme.fg("dim", formatDuration(job.durationMs))}`;
|
|
376
|
+
const id = truncateToWidth(
|
|
377
|
+
replaceTabs(job.id).replace(/\s+/g, " "),
|
|
378
|
+
Math.max(1, width - visibleWidth(`${icon} `) - visibleWidth(outcome)),
|
|
379
|
+
Ellipsis.Unicode,
|
|
380
|
+
);
|
|
381
|
+
let line = `${icon} ${uiTheme.fg("accent", id)}${outcome}`;
|
|
382
|
+
const label = replaceTabs(jobRowLabel(job).split(/\r?\n/)[0] ?? "").trim();
|
|
383
|
+
const labelBudget = width - visibleWidth(line) - 2;
|
|
384
|
+
if (label && label !== job.id && labelBudget >= 8) {
|
|
385
|
+
line += ` ${uiTheme.fg("muted", truncateToWidth(label, labelBudget, Ellipsis.Unicode))}`;
|
|
386
|
+
}
|
|
387
|
+
lines.push(line);
|
|
388
|
+
// A failure says why on the collapsed row; the rest of its output
|
|
389
|
+
// stays behind Ctrl+O.
|
|
390
|
+
const reason =
|
|
391
|
+
job.status === "failed"
|
|
392
|
+
? stripOutputNotice(job.errorText ?? "", job.meta)
|
|
393
|
+
.split(/\r?\n/)
|
|
394
|
+
.find(part => part.trim())
|
|
395
|
+
?.trim()
|
|
396
|
+
: undefined;
|
|
397
|
+
if (reason) {
|
|
398
|
+
lines.push(
|
|
399
|
+
`${indent}${uiTheme.fg("error", truncateToWidth(replaceTabs(reason), Math.max(1, width - indent.length), Ellipsis.Unicode))}`,
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
// The root warning is suppressed when a row owns the failure, so the
|
|
403
|
+
// compact row carries it too — otherwise it only shows after Ctrl+O.
|
|
404
|
+
const artifactError = job.meta?.artifactError ?? job.artifactError;
|
|
405
|
+
if (artifactError) {
|
|
406
|
+
lines.push(uiTheme.fg("warning", truncateToWidth(formatArtifactErrorNotice(artifactError), width)));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const hiddenSettled = settledJobs.length - PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
410
|
+
if (hiddenSettled > 0) lines.push(`${indent}${uiTheme.fg("dim", `… ${hiddenSettled} more settled`)}`);
|
|
411
|
+
const shownWidgetJobs = widgetJobs.slice(0, PREVIEW_LIMITS.COLLAPSED_ITEMS);
|
|
412
|
+
for (const job of shownWidgetJobs) {
|
|
413
|
+
const typeBadge = formatBadge(job.type, statusToColor(job.status), uiTheme);
|
|
414
|
+
const icon = formatStatusIcon("running", uiTheme, live ? options.spinnerFrame : undefined);
|
|
415
|
+
const meta = compact([
|
|
416
|
+
job.completionPercent !== undefined ? uiTheme.fg("accent", `${job.completionPercent}%`) : undefined,
|
|
417
|
+
uiTheme.fg("dim", formatDuration(job.durationMs)),
|
|
418
|
+
]).join(" ");
|
|
419
|
+
lines.push(
|
|
420
|
+
...renderJobTileLines(jobTile(job, live), width, uiTheme, {
|
|
421
|
+
lead: `${icon} ${typeBadge} `,
|
|
422
|
+
meta,
|
|
423
|
+
full: false,
|
|
424
|
+
}),
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
// Running jobs without a drawn tile (no widget, or past the cap) collapse
|
|
428
|
+
// into one count; the header already carries it while nothing settled.
|
|
429
|
+
const quietRunning = counts.running - shownWidgetJobs.length;
|
|
430
|
+
if (quietRunning > 0 && (settledJobs.length > 0 || shownWidgetJobs.length < widgetJobs.length)) {
|
|
431
|
+
lines.push(`${indent}${uiTheme.fg("dim", `${quietRunning} still running`)}`);
|
|
432
|
+
}
|
|
433
|
+
return lines;
|
|
434
|
+
};
|
|
311
435
|
return {
|
|
312
436
|
render(width: number): readonly string[] {
|
|
313
437
|
const expanded = options.expanded;
|
|
@@ -326,96 +450,125 @@ function jobsRenderResult(
|
|
|
326
450
|
.u32(spinnerFrame)
|
|
327
451
|
.bool(shimmerActive)
|
|
328
452
|
.bool(showModelBadge)
|
|
453
|
+
.str(widgetStamp(jobs))
|
|
329
454
|
.digest();
|
|
330
455
|
if (!shimmerActive && cached?.key === key) return cached.lines;
|
|
331
456
|
|
|
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
|
-
|
|
457
|
+
const compactView = !expanded && jobs.length > 0;
|
|
458
|
+
const itemLines = compactView
|
|
459
|
+
? compactJobLines(width, counts.running > 0 && options.spinnerFrame !== undefined)
|
|
460
|
+
: renderTreeList<JobSnapshot>(
|
|
461
|
+
{
|
|
462
|
+
items: sortedJobs,
|
|
463
|
+
expanded,
|
|
464
|
+
maxCollapsed: PREVIEW_LIMITS.COLLAPSED_ITEMS,
|
|
465
|
+
itemType: "job",
|
|
466
|
+
renderItem: (job, context) => {
|
|
467
|
+
const rowWidth = Math.max(0, width - (context.prefixWidth ?? 0));
|
|
468
|
+
const lines: string[] = [];
|
|
469
|
+
const icon = `${formatStatusIcon(
|
|
470
|
+
statusToIcon(job.status),
|
|
471
|
+
uiTheme,
|
|
472
|
+
job.status === "running" ? options.spinnerFrame : undefined,
|
|
473
|
+
)}${job.exitCode === undefined ? "" : `${uiTheme.sep.dot}${uiTheme.fg(job.exitCode === 0 ? "muted" : "error", `exit ${job.exitCode}`)}`}`;
|
|
474
|
+
const typeBadge = formatBadge(job.type, statusToColor(job.status), uiTheme);
|
|
475
|
+
const completion =
|
|
476
|
+
job.status === "running" && job.completionPercent !== undefined
|
|
477
|
+
? `${uiTheme.sep.dot}${uiTheme.fg("accent", `${job.completionPercent}%`)}`
|
|
478
|
+
: "";
|
|
479
|
+
const durationSuffix = `${completion}${uiTheme.sep.dot}${uiTheme.fg("dim", formatDuration(job.durationMs))}`;
|
|
480
|
+
const displayId = truncateToWidth(
|
|
481
|
+
replaceTabs(job.id).replace(/\s+/g, " "),
|
|
482
|
+
Math.max(0, rowWidth - visibleWidth(`${icon} ${typeBadge} ${durationSuffix}`)),
|
|
483
|
+
Ellipsis.Unicode,
|
|
484
|
+
);
|
|
485
|
+
const rowLabel = jobRowLabel(job);
|
|
486
|
+
const rawLabelLines = (rowLabel || "(no label)").split(/\r?\n/);
|
|
487
|
+
const maxLabelLines = expanded ? LABEL_LINES_EXPANDED : LABEL_LINES_COLLAPSED;
|
|
488
|
+
const visibleLabelLines = rawLabelLines
|
|
489
|
+
.slice(0, maxLabelLines)
|
|
490
|
+
.map(l => truncateToWidth(replaceTabs(l), LABEL_MAX_WIDTH, Ellipsis.Unicode));
|
|
491
|
+
if (rawLabelLines.length > maxLabelLines && visibleLabelLines.length > 0) {
|
|
492
|
+
const last = visibleLabelLines[visibleLabelLines.length - 1]!;
|
|
493
|
+
visibleLabelLines[visibleLabelLines.length - 1] = `${last} …`;
|
|
494
|
+
}
|
|
495
|
+
const rowPrefix = `${icon} ${typeBadge} `;
|
|
496
|
+
const live = job.status === "running" && options.spinnerFrame !== undefined;
|
|
497
|
+
const continuationWidth = Math.max(0, rowWidth - visibleWidth(" "));
|
|
498
|
+
if (job.widget?.state === "live") {
|
|
499
|
+
const meta = compact([
|
|
500
|
+
job.status === "running" && job.completionPercent !== undefined
|
|
501
|
+
? uiTheme.fg("accent", `${job.completionPercent}%`)
|
|
502
|
+
: undefined,
|
|
503
|
+
uiTheme.fg("dim", formatDuration(job.durationMs)),
|
|
504
|
+
]).join(" ");
|
|
505
|
+
lines.push(
|
|
506
|
+
...renderJobTileLines(jobTile(job, live), rowWidth, uiTheme, {
|
|
507
|
+
lead: rowPrefix,
|
|
508
|
+
meta,
|
|
509
|
+
full: expanded,
|
|
510
|
+
}),
|
|
511
|
+
);
|
|
512
|
+
} else {
|
|
513
|
+
const modelIdentity = job.resolvedModelIdentity ?? job.resolvedModel;
|
|
514
|
+
const modelBadge =
|
|
515
|
+
job.type === "task" && showModelBadge && typeof modelIdentity === "string"
|
|
516
|
+
? formatFeedModelBadge(
|
|
517
|
+
modelIdentity,
|
|
518
|
+
job.resolvedThinkingLevel,
|
|
519
|
+
job.advisor === true,
|
|
520
|
+
uiTheme,
|
|
521
|
+
Math.min(
|
|
522
|
+
FEED_MODEL_BADGE_WIDTH,
|
|
523
|
+
Math.max(
|
|
524
|
+
0,
|
|
525
|
+
rowWidth - visibleWidth(`${rowPrefix}${displayId}${durationSuffix}`) - 1,
|
|
526
|
+
),
|
|
527
|
+
),
|
|
528
|
+
)
|
|
529
|
+
: "";
|
|
530
|
+
const modelLead = modelBadge ? `${modelBadge} ` : "";
|
|
531
|
+
const headRaw = displayId;
|
|
532
|
+
// Running rows in a live block shimmer their label; once the block
|
|
533
|
+
// stops animating (sealed, or a settled snapshot — spinnerFrame
|
|
534
|
+
// cleared) they render static so scrollback never keeps a mid-sweep
|
|
535
|
+
// shimmer band.
|
|
536
|
+
const headLabel = live
|
|
537
|
+
? shimmerEnabled()
|
|
538
|
+
? shimmerText(headRaw, uiTheme)
|
|
539
|
+
: uiTheme.fg("accent", headRaw)
|
|
540
|
+
: uiTheme.fg("toolOutput", headRaw);
|
|
541
|
+
let row = `${rowPrefix}${modelLead}${headLabel}`;
|
|
542
|
+
const distinctLabel = rowLabel.trim() !== job.id;
|
|
543
|
+
const label = visibleLabelLines[0] ?? "";
|
|
544
|
+
const inlineLabel =
|
|
545
|
+
distinctLabel && visibleWidth(`${row} ${label}${durationSuffix}`) <= rowWidth;
|
|
546
|
+
if (inlineLabel) row += ` ${uiTheme.fg("toolOutput", label)}`;
|
|
547
|
+
row += durationSuffix;
|
|
548
|
+
lines.push(truncateToWidth(row, rowWidth, ""));
|
|
549
|
+
for (let i = distinctLabel && !inlineLabel ? 0 : 1; i < visibleLabelLines.length; i++) {
|
|
550
|
+
lines.push(
|
|
551
|
+
` ${uiTheme.fg("toolOutput", truncateToWidth(visibleLabelLines[i]!, continuationWidth))}`,
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const artifactError = job.meta?.artifactError ?? job.artifactError;
|
|
556
|
+
if (artifactError) {
|
|
557
|
+
lines.push(
|
|
558
|
+
uiTheme.fg(
|
|
559
|
+
"warning",
|
|
560
|
+
truncateToWidth(formatArtifactErrorNotice(artifactError), rowWidth),
|
|
378
561
|
),
|
|
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
|
-
);
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
for (const previewLine of jobPreviewRows(job, expanded, continuationWidth))
|
|
566
|
+
lines.push(previewLine);
|
|
567
|
+
return lines;
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
uiTheme,
|
|
571
|
+
);
|
|
419
572
|
|
|
420
573
|
// Agents run outside job control; render them as their own tree so
|
|
421
574
|
// they never skew the job counts or the "waiting on N jobs" title.
|
|
@@ -458,7 +611,7 @@ function jobsRenderResult(
|
|
|
458
611
|
uiTheme,
|
|
459
612
|
);
|
|
460
613
|
|
|
461
|
-
const all = [header];
|
|
614
|
+
const all = compactView && settledJobs.length > 0 ? [] : [header];
|
|
462
615
|
if (aggregateArtifactError) {
|
|
463
616
|
all.push(uiTheme.fg("warning", formatArtifactErrorNotice(aggregateArtifactError)));
|
|
464
617
|
}
|
|
@@ -498,7 +651,11 @@ function bodyLines(
|
|
|
498
651
|
body: string,
|
|
499
652
|
expanded: boolean,
|
|
500
653
|
theme: Theme,
|
|
501
|
-
options: {
|
|
654
|
+
options: {
|
|
655
|
+
indent?: string;
|
|
656
|
+
tone?: "dim" | "toolOutput";
|
|
657
|
+
collapsedLines?: number;
|
|
658
|
+
} = {},
|
|
502
659
|
): string[] {
|
|
503
660
|
const indent = options.indent ?? "";
|
|
504
661
|
const tone = options.tone ?? "toolOutput";
|
|
@@ -556,17 +713,18 @@ export function createIrcMessageCard(
|
|
|
556
713
|
if (card.replyTo) meta.push("reply");
|
|
557
714
|
const age = messageAge(card.timestamp);
|
|
558
715
|
if (age) meta.push(age);
|
|
559
|
-
const component = createCachedComponent(
|
|
560
|
-
|
|
561
|
-
(
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
716
|
+
const component = createCachedComponent(getExpanded, (width, expanded) => {
|
|
717
|
+
const lines = [renderStatusLine({ iconOverride: ircGlyph(uiTheme), title, meta }, uiTheme)];
|
|
718
|
+
if (body.trim()) {
|
|
719
|
+
lines.push(
|
|
720
|
+
...bodyLines(body, expanded, uiTheme, {
|
|
721
|
+
indent: " ",
|
|
722
|
+
collapsedLines: 3,
|
|
723
|
+
}),
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
return lines.map(line => truncateToWidth(line, width, Ellipsis.Unicode));
|
|
727
|
+
});
|
|
570
728
|
// Terminal-local collapse replaces `getExpanded`; the node never changes after creation.
|
|
571
729
|
const described = cardNode(
|
|
572
730
|
{
|
|
@@ -589,47 +747,6 @@ export function createIrcMessageCard(
|
|
|
589
747
|
return Object.assign(component, { describe: () => described });
|
|
590
748
|
}
|
|
591
749
|
|
|
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: "omp.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
750
|
function describeJobsResult(
|
|
634
751
|
result: ToolRenderResult<CoordinationDetails>,
|
|
635
752
|
isPartial: boolean,
|
|
@@ -658,9 +775,23 @@ function describeJobsResult(
|
|
|
658
775
|
if (counts.completed > 0) head.push(span(` ${counts.completed} done`, "success"));
|
|
659
776
|
if (counts.failed > 0) head.push(span(` ${counts.failed} failed`, "error"));
|
|
660
777
|
if (counts.cancelled > 0) head.push(span(` ${counts.cancelled} cancelled`, "warning"));
|
|
661
|
-
const order: Record<JobSnapshot["status"], number> = {
|
|
778
|
+
const order: Record<JobSnapshot["status"], number> = {
|
|
779
|
+
running: 0,
|
|
780
|
+
failed: 1,
|
|
781
|
+
cancelled: 2,
|
|
782
|
+
completed: 3,
|
|
783
|
+
};
|
|
662
784
|
const sorted = [...jobs].sort((a, b) => order[a.status] - order[b.status] || b.durationMs - a.durationMs);
|
|
663
|
-
|
|
785
|
+
// One frame of tiles: each job's status, title, figure and widget together. The job's output
|
|
786
|
+
// and errors fill the card of the call that started it (`parksAsBackground`), so no previews.
|
|
787
|
+
const body: NativeNode[] = [];
|
|
788
|
+
if (sorted.length > 0) {
|
|
789
|
+
body.push(keyed(describeJobTiles(sorted.slice(0, TILES_SHOWN).map(job => jobTile(job, isPartial))), "tiles"));
|
|
790
|
+
}
|
|
791
|
+
if (sorted.length > TILES_SHOWN) {
|
|
792
|
+
const rest = sorted.length - TILES_SHOWN;
|
|
793
|
+
body.push(text([span(`+${rest} more ${rest === 1 ? "job" : "jobs"} · /jobs`, "dim")], { role: "omp.wait.more" }));
|
|
794
|
+
}
|
|
664
795
|
for (const agent of agents) {
|
|
665
796
|
const spans: TspSpan[] = [span(plainText(agent.id), "muted")];
|
|
666
797
|
if (agent.activity) spans.push(span(` ${plainText(agent.activity)}`, "toolOutput"));
|
|
@@ -684,6 +815,8 @@ function describeJobsResult(
|
|
|
684
815
|
return {
|
|
685
816
|
head,
|
|
686
817
|
tone: counts.failed > 0 ? "warning" : counts.running > 0 || agents.length > 0 ? "info" : "success",
|
|
818
|
+
// The tiles are what a wait shows: clamping the body by lines would cut them.
|
|
819
|
+
preview: "children",
|
|
687
820
|
body,
|
|
688
821
|
};
|
|
689
822
|
}
|
|
@@ -729,11 +862,15 @@ export const waitToolRenderer = {
|
|
|
729
862
|
result: ToolRenderResult<CoordinationDetails>,
|
|
730
863
|
options: RenderResultOptions,
|
|
731
864
|
): NativeToolView | undefined {
|
|
732
|
-
return waitResultMemo.get(result, [options.isPartial], () => {
|
|
865
|
+
return waitResultMemo.get(result, [options.isPartial, widgetStamp(result.details?.jobs)], () => {
|
|
733
866
|
if (result.isError) return errorView("Wait", resultText(result));
|
|
734
867
|
const details = result.details;
|
|
735
868
|
if (details?.interrupted) {
|
|
736
|
-
return {
|
|
869
|
+
return {
|
|
870
|
+
head: toolHead("Wait", "interrupted by message"),
|
|
871
|
+
tone: "info",
|
|
872
|
+
inline: true,
|
|
873
|
+
};
|
|
737
874
|
}
|
|
738
875
|
const waited = details?.waited;
|
|
739
876
|
if (!waited) return describeJobsResult(result, options.isPartial);
|
|
@@ -741,7 +878,11 @@ export const waitToolRenderer = {
|
|
|
741
878
|
});
|
|
742
879
|
},
|
|
743
880
|
renderResult(
|
|
744
|
-
result: {
|
|
881
|
+
result: {
|
|
882
|
+
content: Array<{ type: string; text?: string }>;
|
|
883
|
+
details?: CoordinationDetails;
|
|
884
|
+
isError?: boolean;
|
|
885
|
+
},
|
|
745
886
|
options: RenderResultOptions,
|
|
746
887
|
uiTheme: Theme,
|
|
747
888
|
): 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,
|
|
@@ -1004,6 +1006,8 @@ export class TUI extends Container {
|
|
|
1004
1006
|
#screenImagePlacements = new Map<number, LiveImagePlacement>();
|
|
1005
1007
|
#altImagePlacements = new Map<number, LiveImagePlacement>();
|
|
1006
1008
|
#nextImagePlacementId = 1;
|
|
1009
|
+
/** Last top-clipped SIXEL crop; only the block straddling row 0 is ever cropped. */
|
|
1010
|
+
#sixelCrop: { source: string; pixels: number; maxHeightPixels: number; output: string } | undefined;
|
|
1007
1011
|
#ghosttyInitialImageDelayDone = false;
|
|
1008
1012
|
#ghosttyInitialImageDelayTimer: RenderTimer | undefined;
|
|
1009
1013
|
#ghosttyImageReadyAtMs = 0;
|
|
@@ -3287,10 +3291,11 @@ export class TUI extends Container {
|
|
|
3287
3291
|
}
|
|
3288
3292
|
|
|
3289
3293
|
/**
|
|
3290
|
-
* Clip
|
|
3291
|
-
*
|
|
3292
|
-
*
|
|
3293
|
-
*
|
|
3294
|
+
* Clip direct placements to the visible source slice: Kitty tracks a live
|
|
3295
|
+
* placement owner, while SIXEL crops the encoded raster (iTerm2 images are
|
|
3296
|
+
* not cropped). SIXEL and iTerm2 have no delete command and composite over
|
|
3297
|
+
* existing pixels, so erase the visible rows they paint above `screenRow`
|
|
3298
|
+
* before repainting.
|
|
3294
3299
|
*/
|
|
3295
3300
|
#imageLineSequence(
|
|
3296
3301
|
line: string,
|
|
@@ -3301,7 +3306,16 @@ export class TUI extends Container {
|
|
|
3301
3306
|
if (screenRow < 0) return line;
|
|
3302
3307
|
const parsed = parseKittyDirectPlacementLine(line);
|
|
3303
3308
|
if (!parsed) {
|
|
3304
|
-
const
|
|
3309
|
+
const requestedLift = directPlacementLift(line);
|
|
3310
|
+
const lift = Math.min(requestedLift, screenRow);
|
|
3311
|
+
if (requestedLift > lift) {
|
|
3312
|
+
const cellHeight = getCellDimensions().heightPx;
|
|
3313
|
+
const cropPixels = Math.round((requestedLift - lift) * cellHeight);
|
|
3314
|
+
const maxHeightPixels = Math.floor((screenRow + 1) * cellHeight);
|
|
3315
|
+
line = line.replace(/\x1bP[0-9;]*q[\s\S]*?\x1b\\/g, sequence =>
|
|
3316
|
+
this.#cropSixel(sequence, cropPixels, maxHeightPixels),
|
|
3317
|
+
);
|
|
3318
|
+
}
|
|
3305
3319
|
return lift > 0 ? `\x1b[${lift}A${`${ERASE_LINE}\x1b[B`.repeat(lift)}${line}` : line;
|
|
3306
3320
|
}
|
|
3307
3321
|
const previous = placements?.get(placementRow);
|
|
@@ -3321,6 +3335,32 @@ export class TUI extends Container {
|
|
|
3321
3335
|
);
|
|
3322
3336
|
}
|
|
3323
3337
|
|
|
3338
|
+
/**
|
|
3339
|
+
* Crop a SIXEL raster whose top rows lie above the viewport. A raster the
|
|
3340
|
+
* cropper cannot parse is dropped: painting it whole would overwrite the
|
|
3341
|
+
* rows below the visible slice.
|
|
3342
|
+
*/
|
|
3343
|
+
#cropSixel(source: string, pixels: number, maxHeightPixels: number): string {
|
|
3344
|
+
const cached = this.#sixelCrop;
|
|
3345
|
+
if (
|
|
3346
|
+
cached !== undefined &&
|
|
3347
|
+
cached.pixels === pixels &&
|
|
3348
|
+
cached.maxHeightPixels === maxHeightPixels &&
|
|
3349
|
+
cached.source === source
|
|
3350
|
+
) {
|
|
3351
|
+
return cached.output;
|
|
3352
|
+
}
|
|
3353
|
+
let output: string;
|
|
3354
|
+
try {
|
|
3355
|
+
output = cropSixelTop(source, pixels, maxHeightPixels);
|
|
3356
|
+
} catch (err) {
|
|
3357
|
+
logger.warn("Dropping top-clipped SIXEL image that cannot be cropped", { err });
|
|
3358
|
+
output = "";
|
|
3359
|
+
}
|
|
3360
|
+
this.#sixelCrop = { source, pixels, maxHeightPixels, output };
|
|
3361
|
+
return output;
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3324
3364
|
#terminalLine(line: PreparedLine): string {
|
|
3325
3365
|
if (line.hasOsc8) return line.terminalContent + LINE_TERMINATOR;
|
|
3326
3366
|
return line.terminalContent.endsWith(SEGMENT_RESET) ? line.terminalContent : line.terminalContent + SEGMENT_RESET;
|