@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/render/tool-card.ts
CHANGED
|
@@ -43,6 +43,8 @@ export interface ToolCardSnapshot {
|
|
|
43
43
|
/** Structured status header, rendered by the shared status-line primitive. */
|
|
44
44
|
status?: StatusLineOptions;
|
|
45
45
|
headerMeta?: string;
|
|
46
|
+
/** Short facts drawn inside the frame's bottom border (framed); a trailing row otherwise. */
|
|
47
|
+
footerMeta?: string;
|
|
46
48
|
phase?: ToolCardPhase;
|
|
47
49
|
body?: ToolCardContent;
|
|
48
50
|
sections?: readonly ToolCardSection[];
|
|
@@ -219,6 +221,7 @@ export class ToolCard implements Component {
|
|
|
219
221
|
previousOptions?.width === width &&
|
|
220
222
|
previousOptions.header === header &&
|
|
221
223
|
previousOptions.headerMeta === snapshot.headerMeta &&
|
|
224
|
+
previousOptions.footerMeta === snapshot.footerMeta &&
|
|
222
225
|
previousOptions.state === state &&
|
|
223
226
|
previousOptions.applyBg === snapshot.applyBg &&
|
|
224
227
|
previousOptions.contentPaddingLeft === snapshot.contentPaddingLeft &&
|
|
@@ -230,6 +233,7 @@ export class ToolCard implements Component {
|
|
|
230
233
|
const blockOptions: OutputBlockOptions = reusableOptions ?? {
|
|
231
234
|
header,
|
|
232
235
|
headerMeta: snapshot.headerMeta,
|
|
236
|
+
footerMeta: snapshot.footerMeta,
|
|
233
237
|
state,
|
|
234
238
|
sections,
|
|
235
239
|
width,
|
|
@@ -251,6 +255,7 @@ export class ToolCard implements Component {
|
|
|
251
255
|
if (section.label) lines.push(section.label);
|
|
252
256
|
lines.push(...section.lines);
|
|
253
257
|
}
|
|
258
|
+
if (snapshot.footerMeta) lines.push(snapshot.footerMeta);
|
|
254
259
|
const text = lines.join("\n");
|
|
255
260
|
const key = `${width}:${text.length}:${Bun.hash(text).toString(36)}:${state ?? "info"}:${snapshot.applyBg ?? true}`;
|
|
256
261
|
if (key !== this.#plainKey) {
|
|
@@ -279,6 +284,7 @@ export class ToolCard implements Component {
|
|
|
279
284
|
}
|
|
280
285
|
if (snapshot.footer)
|
|
281
286
|
slotInputs.push({ label: undefined, separator: slotInputs.length > 0, content: snapshot.footer });
|
|
287
|
+
if (snapshot.footerMeta) slotInputs.push({ label: undefined, separator: false, content: [snapshot.footerMeta] });
|
|
282
288
|
|
|
283
289
|
const nextChildren: Component[] = [];
|
|
284
290
|
const slots = slotInputs.map((input, index) => {
|
package/src/sixel.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Give an empty first band an explicit transparent width. Zellij derives the
|
|
3
|
+
* decoded image width from its first pixel row, ignoring the raster minimum
|
|
4
|
+
* for transparent images; an omitted band otherwise makes the image invisible.
|
|
5
|
+
* Palette definitions stay before drawing so cropping can still parse them.
|
|
6
|
+
*/
|
|
7
|
+
export declare function ensureSixelRasterWidth(sequence: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Crop integer pixel rows from a raw (not tmux-wrapped) SIXEL DCS image.
|
|
10
|
+
* Runs are shifted between adjacent six-pixel bands, never decoded to pixels.
|
|
11
|
+
* Palette selections, overdraw order, transparent gaps and DCS background mode
|
|
12
|
+
* are retained. A nonpositive crop returns the original sequence unchanged.
|
|
13
|
+
*
|
|
14
|
+
* The supported grammar is the native icy_sixel encoder's: one leading raster
|
|
15
|
+
* declaration with positive dimensions and square pixels, prefix RGB/HLS palette
|
|
16
|
+
* definitions, register selections, sixel characters, repeats, `$` and `-`.
|
|
17
|
+
* ESC DCS/ST framing only, as the TUI emits it. Palette definitions after
|
|
18
|
+
* drawing, additional raster declarations, other controls and malformed input
|
|
19
|
+
* throw; forwarding an unsupported image uncropped would reintroduce raster
|
|
20
|
+
* overflow.
|
|
21
|
+
*
|
|
22
|
+
* Without maxHeightPixels the crop is exact, including a partial final band with
|
|
23
|
+
* zero padding bits and the exact declared raster height. Some terminals still
|
|
24
|
+
* allocate all six rows of that final band. The TUI supplies its available pixel
|
|
25
|
+
* height to cap the result at a whole-band boundary, matching the native encoder's
|
|
26
|
+
* policy of dropping bottom pixels rather than overwriting the following row.
|
|
27
|
+
*/
|
|
28
|
+
export declare function cropSixelTop(sequence: string, pixels: number, maxHeightPixels?: number): string;
|
package/src/sixel.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Give an empty first band an explicit transparent width. Zellij derives the
|
|
3
|
+
* decoded image width from its first pixel row, ignoring the raster minimum
|
|
4
|
+
* for transparent images; an omitted band otherwise makes the image invisible.
|
|
5
|
+
* Palette definitions stay before drawing so cropping can still parse them.
|
|
6
|
+
*/
|
|
7
|
+
export function ensureSixelRasterWidth(sequence: string): string {
|
|
8
|
+
const prefix = /^(\x1bP[0-9;]*q"\d+;\d+;(\d+);\d+(?:#\d+(?:;[12];\d+;\d+;\d+)?|\$)*)(?=-)/.exec(sequence);
|
|
9
|
+
if (!prefix) return sequence;
|
|
10
|
+
return `${prefix[1]}!${prefix[2]}?$${sequence.slice(prefix[1]!.length)}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface SixelRun {
|
|
14
|
+
bits: number;
|
|
15
|
+
count: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface SixelBand {
|
|
19
|
+
/** The selected register is inherited across graphics newlines. */
|
|
20
|
+
color: number;
|
|
21
|
+
tokens: (SixelRun | string)[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function invalidSixel(reason: string): never {
|
|
25
|
+
throw new Error(`Cannot crop SIXEL: ${reason}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sixelInteger(value: string): number {
|
|
29
|
+
const number = Number(value);
|
|
30
|
+
if (!Number.isSafeInteger(number)) invalidSixel("integer exceeds the safe range");
|
|
31
|
+
return number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function appendRun(output: string[], bits: number, count: number): void {
|
|
35
|
+
const character = String.fromCharCode(63 + bits);
|
|
36
|
+
// Keep repeats compressed, including long transparent gaps.
|
|
37
|
+
output.push(count > 3 ? `!${count}${character}` : character.repeat(count));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function appendBand(output: string[], band: SixelBand | undefined, shift: number, mask: number): void {
|
|
41
|
+
if (!band) return;
|
|
42
|
+
output.push(`#${band.color}`);
|
|
43
|
+
for (const token of band.tokens) {
|
|
44
|
+
if (typeof token === "string") {
|
|
45
|
+
output.push(token);
|
|
46
|
+
} else {
|
|
47
|
+
const bits = shift >= 0 ? token.bits >>> shift : token.bits << -shift;
|
|
48
|
+
appendRun(output, bits & mask, token.count);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Crop integer pixel rows from a raw (not tmux-wrapped) SIXEL DCS image.
|
|
55
|
+
* Runs are shifted between adjacent six-pixel bands, never decoded to pixels.
|
|
56
|
+
* Palette selections, overdraw order, transparent gaps and DCS background mode
|
|
57
|
+
* are retained. A nonpositive crop returns the original sequence unchanged.
|
|
58
|
+
*
|
|
59
|
+
* The supported grammar is the native icy_sixel encoder's: one leading raster
|
|
60
|
+
* declaration with positive dimensions and square pixels, prefix RGB/HLS palette
|
|
61
|
+
* definitions, register selections, sixel characters, repeats, `$` and `-`.
|
|
62
|
+
* ESC DCS/ST framing only, as the TUI emits it. Palette definitions after
|
|
63
|
+
* drawing, additional raster declarations, other controls and malformed input
|
|
64
|
+
* throw; forwarding an unsupported image uncropped would reintroduce raster
|
|
65
|
+
* overflow.
|
|
66
|
+
*
|
|
67
|
+
* Without maxHeightPixels the crop is exact, including a partial final band with
|
|
68
|
+
* zero padding bits and the exact declared raster height. Some terminals still
|
|
69
|
+
* allocate all six rows of that final band. The TUI supplies its available pixel
|
|
70
|
+
* height to cap the result at a whole-band boundary, matching the native encoder's
|
|
71
|
+
* policy of dropping bottom pixels rather than overwriting the following row.
|
|
72
|
+
*/
|
|
73
|
+
export function cropSixelTop(sequence: string, pixels: number, maxHeightPixels?: number): string {
|
|
74
|
+
if (pixels <= 0) return sequence;
|
|
75
|
+
if (!Number.isSafeInteger(pixels)) throw new RangeError("SIXEL crop must be an integer pixel count");
|
|
76
|
+
if (maxHeightPixels !== undefined && (!Number.isFinite(maxHeightPixels) || maxHeightPixels < 0)) {
|
|
77
|
+
throw new RangeError("SIXEL height limit must be a nonnegative finite pixel count");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const header = /^\x1bP[0-9;]*q/.exec(sequence);
|
|
81
|
+
if (!header) invalidSixel("missing DCS introducer");
|
|
82
|
+
if (!sequence.endsWith("\x1b\\")) invalidSixel("missing string terminator");
|
|
83
|
+
const end = sequence.length - 2;
|
|
84
|
+
const tokenPattern = /[?-~]|!\d+[?-~]|#\d+(?:;\d+)*|"\d+(?:;\d+)*|[-$]/y;
|
|
85
|
+
const palette: string[] = [];
|
|
86
|
+
const bands: SixelBand[] = [];
|
|
87
|
+
let band: SixelBand = { color: 0, tokens: [] };
|
|
88
|
+
let color = 0;
|
|
89
|
+
let drawing = false;
|
|
90
|
+
let raster: string[] | undefined;
|
|
91
|
+
let width = 0;
|
|
92
|
+
let height = 0;
|
|
93
|
+
let x = 0;
|
|
94
|
+
let y = 0;
|
|
95
|
+
let dataWidth = 0;
|
|
96
|
+
let paintedHeight = 0;
|
|
97
|
+
let cursor = header[0].length;
|
|
98
|
+
|
|
99
|
+
while (cursor < end) {
|
|
100
|
+
tokenPattern.lastIndex = cursor;
|
|
101
|
+
const match = tokenPattern.exec(sequence);
|
|
102
|
+
if (!match || tokenPattern.lastIndex > end) invalidSixel(`unsupported token at offset ${cursor}`);
|
|
103
|
+
const token = match[0];
|
|
104
|
+
cursor = tokenPattern.lastIndex;
|
|
105
|
+
switch (token[0]) {
|
|
106
|
+
case '"': {
|
|
107
|
+
if (raster || drawing) invalidSixel("raster attributes must occur once before drawing");
|
|
108
|
+
raster = token.slice(1).split(";");
|
|
109
|
+
if (raster.length !== 4) invalidSixel("expected four raster attributes");
|
|
110
|
+
const pan = sixelInteger(raster[0]!);
|
|
111
|
+
const pad = sixelInteger(raster[1]!);
|
|
112
|
+
width = sixelInteger(raster[2]!);
|
|
113
|
+
height = sixelInteger(raster[3]!);
|
|
114
|
+
if (pan === 0 || pan !== pad) invalidSixel("only square pixels are supported");
|
|
115
|
+
if (width === 0 || height === 0) invalidSixel("raster dimensions must be positive");
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case "#": {
|
|
119
|
+
const parameters = token.slice(1).split(";");
|
|
120
|
+
color = sixelInteger(parameters[0]!);
|
|
121
|
+
if (parameters.length > 1) {
|
|
122
|
+
if (drawing) invalidSixel("palette definitions must precede drawing");
|
|
123
|
+
if (parameters.length !== 5 || (parameters[1] !== "1" && parameters[1] !== "2")) {
|
|
124
|
+
invalidSixel("expected an RGB or HLS palette definition");
|
|
125
|
+
}
|
|
126
|
+
for (let index = 2; index < parameters.length; index++) sixelInteger(parameters[index]!);
|
|
127
|
+
palette.push(token);
|
|
128
|
+
}
|
|
129
|
+
band.tokens.push(`#${color}`);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case "$":
|
|
133
|
+
drawing = true;
|
|
134
|
+
x = 0;
|
|
135
|
+
band.tokens.push(token);
|
|
136
|
+
break;
|
|
137
|
+
case "-":
|
|
138
|
+
drawing = true;
|
|
139
|
+
bands.push(band);
|
|
140
|
+
band = { color, tokens: [] };
|
|
141
|
+
x = 0;
|
|
142
|
+
y += 6;
|
|
143
|
+
break;
|
|
144
|
+
default: {
|
|
145
|
+
drawing = true;
|
|
146
|
+
const count = token[0] === "!" ? sixelInteger(token.slice(1, -1)) : 1;
|
|
147
|
+
if (count === 0) invalidSixel("repeat count must be positive");
|
|
148
|
+
const bits = token.charCodeAt(token.length - 1) - 63;
|
|
149
|
+
x += count;
|
|
150
|
+
if (!Number.isSafeInteger(x)) invalidSixel("run width exceeds the safe range");
|
|
151
|
+
dataWidth = Math.max(dataWidth, x);
|
|
152
|
+
if (bits !== 0) paintedHeight = Math.max(paintedHeight, y + 32 - Math.clz32(bits));
|
|
153
|
+
band.tokens.push({ bits, count });
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
bands.push(band);
|
|
159
|
+
if (!raster) invalidSixel("missing raster dimensions");
|
|
160
|
+
// Raster attributes specify a minimum, not a clip rectangle. Account for
|
|
161
|
+
// drawing beyond them rather than silently discarding those source pixels.
|
|
162
|
+
width = Math.max(width, dataWidth);
|
|
163
|
+
height = Math.max(height, paintedHeight);
|
|
164
|
+
let croppedHeight = Math.max(0, height - pixels);
|
|
165
|
+
if (maxHeightPixels !== undefined) {
|
|
166
|
+
croppedHeight = Math.min(croppedHeight, Math.floor(maxHeightPixels / 6) * 6);
|
|
167
|
+
}
|
|
168
|
+
if (croppedHeight === 0) return "";
|
|
169
|
+
|
|
170
|
+
const output = [header[0], `"${raster[0]};${raster[1]};${width};${croppedHeight}`, ...palette];
|
|
171
|
+
const firstBand = Math.floor(pixels / 6);
|
|
172
|
+
const shift = pixels % 6;
|
|
173
|
+
const bandCount = Math.ceil(croppedHeight / 6);
|
|
174
|
+
for (let index = 0; index < bandCount; index++) {
|
|
175
|
+
if (index > 0) output.push("-");
|
|
176
|
+
const mask = (1 << Math.min(6, croppedHeight - index * 6)) - 1;
|
|
177
|
+
appendBand(output, bands[firstBand + index], shift, mask);
|
|
178
|
+
if (shift !== 0) {
|
|
179
|
+
output.push("$");
|
|
180
|
+
appendBand(output, bands[firstBand + index + 1], shift - 6, mask);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
output.push("\x1b\\");
|
|
184
|
+
return ensureSixelRasterWidth(output.join(""));
|
|
185
|
+
}
|
|
@@ -45,6 +45,7 @@ import { col, node, span } from "../native/describe";
|
|
|
45
45
|
import { getContextMeterThresholds } from "../chrome/context-thresholds";
|
|
46
46
|
import { isNativeRendering, onNativeRenderingChange } from "../native/state";
|
|
47
47
|
import { getSeparator } from "./separators";
|
|
48
|
+
import { formatJobWidgetGlance, type JobWidgetEntry, latestJobWidget } from "../tools/job-widget";
|
|
48
49
|
import type {
|
|
49
50
|
CollabStatus,
|
|
50
51
|
ComposerFacts,
|
|
@@ -59,6 +60,9 @@ import type {
|
|
|
59
60
|
StatusResetExpiry,
|
|
60
61
|
} from "./types";
|
|
61
62
|
|
|
63
|
+
/** Sparkline cells the ANSI jobs segment draws after the freshest widget's headline. */
|
|
64
|
+
const JOBS_SEGMENT_SPARK_CELLS = 8;
|
|
65
|
+
|
|
62
66
|
/** What a click on a native status segment asks zeta to open. */
|
|
63
67
|
export type StatusLineNativeAction = "status.model" | "status.context" | "status.git" | "status.cost" | "status.path";
|
|
64
68
|
|
|
@@ -454,6 +458,8 @@ class StatusLineExternalInputs {
|
|
|
454
458
|
isFastModeActive = false;
|
|
455
459
|
anthropicSlowModeLabel: string | undefined = undefined;
|
|
456
460
|
compactionSpeculation: unknown = undefined;
|
|
461
|
+
/** The freshest running job widget's view (stable identity while its data is unchanged). */
|
|
462
|
+
jobWidgetRef: unknown = undefined;
|
|
457
463
|
}
|
|
458
464
|
|
|
459
465
|
interface CachedStatusLine {
|
|
@@ -2818,6 +2824,13 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2818
2824
|
? this.session.getAnthropicSlowModeLabel()
|
|
2819
2825
|
: undefined;
|
|
2820
2826
|
target.compactionSpeculation = this.session.compactionSpeculation;
|
|
2827
|
+
target.jobWidgetRef = this.#latestJobWidget()?.view;
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
/** The running job widget that saw data last; the ANSI jobs segment appends its glance. */
|
|
2831
|
+
#latestJobWidget(): JobWidgetEntry | undefined {
|
|
2832
|
+
const entries = this.session.getRunningJobWidgets?.();
|
|
2833
|
+
return entries ? latestJobWidget(entries) : undefined;
|
|
2821
2834
|
}
|
|
2822
2835
|
|
|
2823
2836
|
#sameStatusLineExternalInputs(left: StatusLineExternalInputs, right: StatusLineExternalInputs): boolean {
|
|
@@ -2871,7 +2884,8 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2871
2884
|
left.isAutoThinking === right.isAutoThinking &&
|
|
2872
2885
|
left.isFastModeActive === right.isFastModeActive &&
|
|
2873
2886
|
left.anthropicSlowModeLabel === right.anthropicSlowModeLabel &&
|
|
2874
|
-
left.compactionSpeculation === right.compactionSpeculation
|
|
2887
|
+
left.compactionSpeculation === right.compactionSpeculation &&
|
|
2888
|
+
left.jobWidgetRef === right.jobWidgetRef
|
|
2875
2889
|
);
|
|
2876
2890
|
}
|
|
2877
2891
|
|
|
@@ -3080,7 +3094,10 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
3080
3094
|
if (layout !== "plain-left") {
|
|
3081
3095
|
const runningBackgroundJobs = this.runningBackgroundJobCount();
|
|
3082
3096
|
if (runningBackgroundJobs > 0) {
|
|
3083
|
-
|
|
3097
|
+
const widget = this.#latestJobWidget();
|
|
3098
|
+
const glance = widget ? formatJobWidgetGlance(widget.view, theme, JOBS_SEGMENT_SPARK_CELLS) : "";
|
|
3099
|
+
const count = theme.fg("statusLineSubagents", `${theme.icon.job} ${runningBackgroundJobs}`);
|
|
3100
|
+
rightParts.unshift(glance ? `${count} ${glance}` : count);
|
|
3084
3101
|
}
|
|
3085
3102
|
if (subagentBadge) rightParts.unshift(subagentBadge);
|
|
3086
3103
|
}
|
package/src/status-line/host.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentMessage, ThinkingLevel } from "@linxiraos/pi-agent-core";
|
|
2
2
|
import type { Tool, UsageLimit, UsageReport } from "@linxiraos/pi-ai";
|
|
3
3
|
import type { Model } from "@linxiraos/pi-catalog/types";
|
|
4
|
+
import type { JobWidgetEntry } from "../tools/job-widget";
|
|
4
5
|
import type { CompactionBoundaries } from "./context-usage";
|
|
5
6
|
import type { StatusLineSettings, StatusResetExpiry } from "./types";
|
|
6
7
|
|
|
@@ -59,6 +60,8 @@ export interface StatusLineSession {
|
|
|
59
60
|
getAnthropicSlowModeLabel?(): string | undefined;
|
|
60
61
|
getPrewalkState?(): unknown;
|
|
61
62
|
getAsyncJobSnapshot(): { running: readonly { type: string; agentId?: string }[] } | null | undefined;
|
|
63
|
+
/** Widgets of the running background jobs, in job order; the jobs segment appends the freshest one's glance. */
|
|
64
|
+
getRunningJobWidgets?(): readonly JobWidgetEntry[];
|
|
62
65
|
getGoalModeState(): { goal?: { status: string; tokensUsed: number; tokenBudget?: number } } | undefined;
|
|
63
66
|
getAdvisorStatusOverview?(): { configured: boolean; advisors: readonly { status: string; yielded: boolean }[] };
|
|
64
67
|
getAdvisorCost?(): number;
|
|
@@ -401,6 +401,11 @@ export declare function encodeITerm2(base64Data: string, options?: {
|
|
|
401
401
|
preserveAspectRatio?: boolean;
|
|
402
402
|
inline?: boolean;
|
|
403
403
|
}): string;
|
|
404
|
+
/**
|
|
405
|
+
* The encoded image `encodeITerm2` wrote into `line`, without the size
|
|
406
|
+
* parameters a rewrap changes; undefined for a line without one.
|
|
407
|
+
*/
|
|
408
|
+
export declare function iterm2ImagePayload(line: string): string | undefined;
|
|
404
409
|
export declare function calculateImageRows(imageDimensions: ImageDimensions, targetWidthCells: number, cellDimensions?: CellDimensions): number;
|
|
405
410
|
export declare function getPngDimensions(base64Data: string): ImageDimensions | null;
|
|
406
411
|
export declare function getJpegDimensions(base64Data: string): ImageDimensions | null;
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
renderKittyPlaceholderLines,
|
|
11
11
|
setKittyGraphics,
|
|
12
12
|
} from "./kitty-graphics";
|
|
13
|
+
import { ensureSixelRasterWidth } from "./sixel";
|
|
13
14
|
import {
|
|
14
15
|
isInsideTerminalMultiplexer,
|
|
15
16
|
isTerminalBackgroundQueryTrusted,
|
|
@@ -404,7 +405,11 @@ export function detectStyledUnderlineSupport(terminalId: TerminalId, env: NodeJS
|
|
|
404
405
|
// render. Enable only on a confirmed major.minor >= 3.5; an absent or
|
|
405
406
|
// unparseable version keeps the flat fallback so only proven terminals
|
|
406
407
|
// get the colon form.
|
|
407
|
-
|
|
408
|
+
//
|
|
409
|
+
// `LC_TERMINAL_VERSION` carries the same version over SSH, where
|
|
410
|
+
// `TERM_PROGRAM_VERSION` is stripped, so it is the fallback source.
|
|
411
|
+
const version =
|
|
412
|
+
parseMajorMinorVersion(env.TERM_PROGRAM_VERSION) ?? parseMajorMinorVersion(env.LC_TERMINAL_VERSION);
|
|
408
413
|
return version !== null && (version.major > 3 || (version.major === 3 && version.minor >= 5));
|
|
409
414
|
}
|
|
410
415
|
default:
|
|
@@ -700,6 +705,7 @@ export function detectTerminalId(env: NodeJS.ProcessEnv = Bun.env): TerminalId {
|
|
|
700
705
|
VSCODE_PID,
|
|
701
706
|
ALACRITTY_WINDOW_ID,
|
|
702
707
|
TERM_PROGRAM,
|
|
708
|
+
LC_TERMINAL,
|
|
703
709
|
TERM,
|
|
704
710
|
COLORTERM,
|
|
705
711
|
} = env;
|
|
@@ -714,6 +720,22 @@ export function detectTerminalId(env: NodeJS.ProcessEnv = Bun.env): TerminalId {
|
|
|
714
720
|
const programId = fromProgram(TERM_PROGRAM);
|
|
715
721
|
if (programId) return programId;
|
|
716
722
|
|
|
723
|
+
// iTerm2 sets `LC_TERMINAL=iTerm2` (plus `LC_TERMINAL_VERSION`) in the local
|
|
724
|
+
// session precisely so it survives SSH: openssh/mosh forward `LC_*` to the
|
|
725
|
+
// remote host, while `ITERM_SESSION_ID` and `TERM_PROGRAM` are stripped. Its
|
|
726
|
+
// own shell integration says the same: "TERM_PROGRAM is set by the local
|
|
727
|
+
// terminal but not forwarded over SSH. LC_TERMINAL is set by iTerm2 and may
|
|
728
|
+
// be forwarded over SSH." Without this branch the remote session falls to
|
|
729
|
+
// the true-color profile and loses the iTerm2 inline-image protocol, OSC 8
|
|
730
|
+
// hyperlinks and OSC 9 notifications.
|
|
731
|
+
//
|
|
732
|
+
// Gated on the absence of a multiplexer: iTerm2's OSC 1337 encoder is the
|
|
733
|
+
// only image encoder that does not wrap its payload in a tmux passthrough
|
|
734
|
+
// envelope, so inside a multiplexer the sequence would be swallowed by the
|
|
735
|
+
// multiplexer instead of reaching the outer terminal. The multiplexer path
|
|
736
|
+
// resolves the outer client on its own.
|
|
737
|
+
if (LC_TERMINAL && caseEq(LC_TERMINAL, "iTerm2") && !isInsideTerminalMultiplexer(env)) return "iterm2";
|
|
738
|
+
|
|
717
739
|
// tmux >= 3.2 replaces the pane's identity with `TERM_PROGRAM=tmux`.
|
|
718
740
|
// Its server still holds the attached client's terminal-type reply and TERM.
|
|
719
741
|
for (const clientProgram of resolveTmuxClientTerminalNames(env)) {
|
|
@@ -1115,6 +1137,19 @@ export function encodeITerm2(
|
|
|
1115
1137
|
return `\x1b]1337;File=${params.join(";")}:${base64Data}\x07`;
|
|
1116
1138
|
}
|
|
1117
1139
|
|
|
1140
|
+
/**
|
|
1141
|
+
* The encoded image `encodeITerm2` wrote into `line`, without the size
|
|
1142
|
+
* parameters a rewrap changes; undefined for a line without one.
|
|
1143
|
+
*/
|
|
1144
|
+
export function iterm2ImagePayload(line: string): string | undefined {
|
|
1145
|
+
const start = line.indexOf(ImageProtocol.Iterm2);
|
|
1146
|
+
if (start === -1) return undefined;
|
|
1147
|
+
const colon = line.indexOf(":", start + ImageProtocol.Iterm2.length);
|
|
1148
|
+
if (colon === -1) return undefined;
|
|
1149
|
+
const end = line.indexOf("\x07", colon + 1);
|
|
1150
|
+
return line.slice(colon + 1, end === -1 ? undefined : end);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1118
1153
|
export function calculateImageRows(
|
|
1119
1154
|
imageDimensions: ImageDimensions,
|
|
1120
1155
|
targetWidthCells: number,
|
|
@@ -1309,7 +1344,9 @@ export function encodeSixelNow(
|
|
|
1309
1344
|
backgroundRgb = getTerminalBackgroundColor(),
|
|
1310
1345
|
): string | null {
|
|
1311
1346
|
try {
|
|
1312
|
-
return
|
|
1347
|
+
return ensureSixelRasterWidth(
|
|
1348
|
+
encodeSixel(new Uint8Array(Buffer.from(base64Data, "base64")), widthPx, heightPx, backgroundRgb),
|
|
1349
|
+
);
|
|
1313
1350
|
} catch {
|
|
1314
1351
|
return null;
|
|
1315
1352
|
}
|
|
@@ -1382,9 +1419,12 @@ export function renderImage(
|
|
|
1382
1419
|
return { sequence, rows };
|
|
1383
1420
|
}
|
|
1384
1421
|
if (TERMINAL.imageProtocol === ImageProtocol.Iterm2) {
|
|
1422
|
+
// The height is the rows reserved for the image, not `auto`: iTerm2
|
|
1423
|
+
// resolves `auto` only for a session on screen, so an image written to a
|
|
1424
|
+
// background tab is dropped, and its rows stay blank in scrollback too.
|
|
1385
1425
|
const sequence = encodeITerm2(base64Data, {
|
|
1386
1426
|
width: fit.columns,
|
|
1387
|
-
height:
|
|
1427
|
+
height: fit.rows,
|
|
1388
1428
|
preserveAspectRatio: options.preserveAspectRatio ?? true,
|
|
1389
1429
|
});
|
|
1390
1430
|
return { sequence, rows: fit.rows };
|
package/src/theme/symbols.ts
CHANGED
|
@@ -628,9 +628,11 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
|
|
628
628
|
"lang.pdf": "📕",
|
|
629
629
|
"lang.archive": "🗜",
|
|
630
630
|
"lang.binary": "⚙",
|
|
631
|
-
// Composer attachment chips
|
|
632
|
-
|
|
633
|
-
|
|
631
|
+
// Composer attachment chips. These sit inside the editable buffer, so the caret column
|
|
632
|
+
// depends on their width: use Emoji_Presentation glyphs, which every terminal draws 2 cells
|
|
633
|
+
// wide. Text-presentation pictographs (🖼, 🎞) measure 1 cell but many terminals draw them 2.
|
|
634
|
+
"chip.image": "📷",
|
|
635
|
+
"chip.video": "🎬",
|
|
634
636
|
"chip.paste": "📄",
|
|
635
637
|
// Settings tabs
|
|
636
638
|
"tab.appearance": "🎨",
|
package/src/tools/bash.d.ts
CHANGED
|
@@ -117,6 +117,7 @@ export declare function createShellRenderer<TArgs>(config: ShellRendererConfig<T
|
|
|
117
117
|
}, args?: TArgs): NativeToolView;
|
|
118
118
|
mergeCallAndResult: true;
|
|
119
119
|
inline: true;
|
|
120
|
+
tickElapsed: true;
|
|
120
121
|
};
|
|
121
122
|
/** Renders bash command previews and output. */
|
|
122
123
|
export declare const bashToolRenderer: {
|
|
@@ -146,4 +147,5 @@ export declare const bashToolRenderer: {
|
|
|
146
147
|
}, args?: BashRenderArgs | undefined): NativeToolView;
|
|
147
148
|
mergeCallAndResult: true;
|
|
148
149
|
inline: true;
|
|
150
|
+
tickElapsed: true;
|
|
149
151
|
};
|
package/src/tools/bash.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { formatOutputPaneLines } from "../render/output-pane";
|
|
|
7
7
|
import {
|
|
8
8
|
capPreviewLines,
|
|
9
9
|
DEFAULT_TERMINAL_PREVIEW_LINES,
|
|
10
|
+
formatDuration,
|
|
10
11
|
formatToolWorkingDirectory,
|
|
11
12
|
previewWindowRows,
|
|
12
13
|
replaceTabs,
|
|
@@ -470,9 +471,14 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
470
471
|
},
|
|
471
472
|
uiTheme,
|
|
472
473
|
);
|
|
474
|
+
const elapsedMs = options.executionStarted ? options.elapsedMs : undefined;
|
|
473
475
|
return {
|
|
474
476
|
header,
|
|
475
477
|
phase: options.spinnerFrame !== undefined ? "running" : "pending",
|
|
478
|
+
footerMeta:
|
|
479
|
+
elapsedMs === undefined || elapsedMs < 1000
|
|
480
|
+
? undefined
|
|
481
|
+
: uiTheme.fg("dim", `elapsed ${formatDuration(Math.floor(elapsedMs / 1000) * 1000)}`),
|
|
476
482
|
sections: [{ content: capPreviewLines(cmdLines, uiTheme, { expanded: options.expanded }) }],
|
|
477
483
|
};
|
|
478
484
|
});
|
|
@@ -528,6 +534,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
528
534
|
let cachedIsPartial: boolean | undefined;
|
|
529
535
|
let cachedPreviewWindow: number | undefined;
|
|
530
536
|
let cachedSnapshot: ToolCardSnapshot | undefined;
|
|
537
|
+
let cachedElapsedSecond: number | undefined;
|
|
531
538
|
const dropSnapshot = () => {
|
|
532
539
|
cachedSnapshot = undefined;
|
|
533
540
|
cachedWidth = undefined;
|
|
@@ -536,6 +543,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
536
543
|
cachedRawOutput = undefined;
|
|
537
544
|
cachedIsPartial = undefined;
|
|
538
545
|
cachedPreviewWindow = undefined;
|
|
546
|
+
cachedElapsedSecond = undefined;
|
|
539
547
|
};
|
|
540
548
|
|
|
541
549
|
return framedToolCard(
|
|
@@ -553,6 +561,11 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
553
561
|
|
|
554
562
|
const isPartial = options.isPartial === true;
|
|
555
563
|
const previewWindow = previewWindowRows();
|
|
564
|
+
// A running command shows its live elapsed time in the bottom
|
|
565
|
+
// border; key the cache on whole seconds so it advances once a
|
|
566
|
+
// second instead of rebuilding every spinner frame.
|
|
567
|
+
const liveElapsedMs = isPartial && details?.wallTimeMs === undefined ? options.elapsedMs : undefined;
|
|
568
|
+
const elapsedSecond = liveElapsedMs === undefined ? undefined : Math.floor(liveElapsedMs / 1000);
|
|
556
569
|
|
|
557
570
|
if (
|
|
558
571
|
cachedSnapshot !== undefined &&
|
|
@@ -561,7 +574,8 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
561
574
|
cachedExpanded === expanded &&
|
|
562
575
|
cachedRawOutput === rawOutput &&
|
|
563
576
|
cachedIsPartial === isPartial &&
|
|
564
|
-
cachedPreviewWindow === previewWindow
|
|
577
|
+
cachedPreviewWindow === previewWindow &&
|
|
578
|
+
cachedElapsedSecond === elapsedSecond
|
|
565
579
|
) {
|
|
566
580
|
return cachedSnapshot;
|
|
567
581
|
}
|
|
@@ -576,13 +590,11 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
576
590
|
rawOutputArtifact.artifactId,
|
|
577
591
|
isError,
|
|
578
592
|
);
|
|
579
|
-
|
|
580
|
-
statsParts.
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
)
|
|
585
|
-
: undefined;
|
|
593
|
+
if (elapsedSecond !== undefined && elapsedSecond > 0) {
|
|
594
|
+
statsParts.unshift(`elapsed ${formatDuration(elapsedSecond * 1000)}`);
|
|
595
|
+
}
|
|
596
|
+
const footerMeta =
|
|
597
|
+
statsParts.length > 0 ? uiTheme.fg("dim", statsParts.join(uiTheme.sep.dot)) : undefined;
|
|
586
598
|
let warningLine: string | undefined;
|
|
587
599
|
if (details?.meta?.artifactError || (details?.meta?.truncation && !showingFullOutput)) {
|
|
588
600
|
warningLine = formatStyledTruncationWarning(details.meta, uiTheme) ?? undefined;
|
|
@@ -610,12 +622,12 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
610
622
|
uiTheme,
|
|
611
623
|
);
|
|
612
624
|
const outputLines: string[] = [...formatted.lines];
|
|
613
|
-
if (timeoutLine) outputLines.push(timeoutLine);
|
|
614
625
|
if (warningLine) outputLines.push(warningLine);
|
|
615
626
|
|
|
616
627
|
const snapshot: ToolCardSnapshot = {
|
|
617
628
|
header,
|
|
618
629
|
phase: isPartial ? "partial" : isError ? (isTimeout ? "warning" : "error") : "success",
|
|
630
|
+
footerMeta,
|
|
619
631
|
sections: [
|
|
620
632
|
{
|
|
621
633
|
// Viewport-sized tail window in every state — streaming and final
|
|
@@ -636,6 +648,7 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
636
648
|
cachedRawOutput = rawOutput;
|
|
637
649
|
cachedIsPartial = isPartial;
|
|
638
650
|
cachedPreviewWindow = previewWindow;
|
|
651
|
+
cachedElapsedSecond = elapsedSecond;
|
|
639
652
|
cachedSnapshot = snapshot;
|
|
640
653
|
return snapshot;
|
|
641
654
|
},
|
|
@@ -714,6 +727,9 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
714
727
|
},
|
|
715
728
|
mergeCallAndResult: true,
|
|
716
729
|
inline: true,
|
|
730
|
+
// The live elapsed footer reads `options.elapsedMs` at render time; a
|
|
731
|
+
// once-a-second repaint keeps it moving for a quiet running command.
|
|
732
|
+
tickElapsed: true,
|
|
717
733
|
} satisfies ToolRenderer<TArgs, BashToolDetails>;
|
|
718
734
|
}
|
|
719
735
|
|
|
@@ -23,6 +23,9 @@ export declare function formatDefaultToolExecution(input: DefaultToolRenderInput
|
|
|
23
23
|
/**
|
|
24
24
|
* TSP view of the generic fallback card: the tool label with a one-line args
|
|
25
25
|
* summary as the head target; the result as a JSON tree or raw `ansi` output.
|
|
26
|
+
* The complete output goes over the wire — the card-level disclosure clamp the
|
|
27
|
+
* terminal applies is the only bound, so an expanded card keeps every row. JSON
|
|
28
|
+
* the tree's depth, node or scalar caps would cut keeps the raw path instead.
|
|
26
29
|
*/
|
|
27
30
|
export declare function describeDefaultToolExecution(input: DefaultToolRenderInput): NativeToolView;
|
|
28
31
|
/** Render the generic fallback as the state-tinted card used by direct custom tools. */
|