@linxiraos/pi-tui 1.3.2 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +181 -181
- package/dist/types/apps/session-picker.d.ts +13 -1
- package/dist/types/autocomplete.d.ts +6 -0
- package/dist/types/components/sparkline.d.ts +34 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/native/describe.d.ts +7 -1
- package/dist/types/native/overlay.d.ts +2 -0
- package/dist/types/native/tone.d.ts +2 -0
- package/dist/types/overlays/agent-hub.d.ts +3 -3
- package/dist/types/overlays/approval-dialog.d.ts +15 -1
- package/dist/types/overlays/jobs-panel.d.ts +3 -0
- package/dist/types/overlays/model-browser.d.ts +1 -1
- package/dist/types/overlays/resume-picker.d.ts +293 -0
- package/dist/types/overlays/resume-tree.d.ts +353 -0
- package/dist/types/overlays/session-selector.d.ts +22 -4
- package/dist/types/render/output-block.d.ts +2 -0
- package/dist/types/render/tool-card.d.ts +2 -0
- package/dist/types/sixel.d.ts +28 -0
- package/dist/types/status-line/host.d.ts +3 -0
- package/dist/types/terminal-capabilities.d.ts +6 -1
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/default-renderer.d.ts +3 -0
- package/dist/types/tools/eval.d.ts +9 -8
- package/dist/types/tools/hub.d.ts +3 -1
- package/dist/types/tools/irc.d.ts +11 -1
- package/dist/types/tools/job-widget.d.ts +226 -0
- package/dist/types/tools/json-tree.d.ts +2 -0
- package/dist/types/tools/line-ranges.d.ts +8 -1
- package/dist/types/tools/mcp.d.ts +23 -0
- package/dist/types/tools/output-meta.d.ts +1 -1
- package/dist/types/tools/read.d.ts +8 -2
- package/dist/types/tools/renderer.d.ts +8 -2
- package/dist/types/tools/result-card.d.ts +1 -1
- package/dist/types/tools/streaming-output.d.ts +1 -0
- package/dist/types/tools/task.d.ts +12 -0
- package/dist/types/tools/wait.d.ts +11 -0
- package/package.json +9 -9
- package/src/apps/session-picker.ts +20 -2
- package/src/autocomplete.d.ts +6 -0
- package/src/autocomplete.ts +21 -6
- package/src/chat/skill-message.ts +16 -17
- package/src/chat/tool-execution.ts +35 -0
- package/src/chrome/transcript-container.ts +85 -13
- package/src/components/editor.ts +17 -7
- package/src/components/image.ts +5 -4
- package/src/components/markdown.ts +11 -3
- package/src/components/sparkline.d.ts +34 -0
- package/src/components/sparkline.ts +89 -0
- package/src/index.d.ts +1 -0
- package/src/index.ts +1 -0
- package/src/native/backend.ts +32 -6
- package/src/native/describe.d.ts +7 -1
- package/src/native/describe.ts +18 -1
- package/src/native/overlay.d.ts +2 -0
- package/src/native/overlay.ts +3 -0
- package/src/native/tone.d.ts +2 -0
- package/src/native/tone.ts +22 -0
- package/src/overlays/agent-hub.ts +14 -10
- package/src/overlays/agent-transcript-viewer.ts +2 -2
- package/src/overlays/agents-hub.ts +2 -1
- package/src/overlays/approval-dialog.ts +59 -13
- package/src/overlays/hub-frame.ts +2 -1
- package/src/overlays/jobs-panel.ts +24 -2
- package/src/overlays/model-browser.ts +3 -0
- package/src/overlays/model-hub.ts +2 -1
- package/src/overlays/resume-picker.ts +706 -0
- package/src/overlays/resume-tree.ts +1901 -0
- package/src/overlays/session-selector.ts +579 -39
- package/src/prompt/attachment-chips.ts +1 -1
- package/src/render/output-block.d.ts +2 -0
- package/src/render/output-block.ts +25 -12
- package/src/render/tool-card.d.ts +2 -0
- package/src/render/tool-card.ts +6 -0
- package/src/sixel.d.ts +28 -0
- package/src/sixel.ts +185 -0
- package/src/status-line/component.ts +19 -2
- package/src/status-line/host.ts +3 -0
- package/src/terminal-capabilities.d.ts +6 -1
- package/src/terminal-capabilities.ts +54 -3
- package/src/theme/symbols.ts +5 -3
- package/src/tools/bash.d.ts +2 -0
- package/src/tools/bash.ts +25 -9
- package/src/tools/default-renderer.d.ts +3 -0
- package/src/tools/default-renderer.ts +54 -17
- package/src/tools/eval.d.ts +9 -8
- package/src/tools/eval.ts +32 -14
- package/src/tools/find.ts +4 -5
- package/src/tools/glob.ts +4 -6
- package/src/tools/grep.ts +2 -4
- package/src/tools/hub.ts +4 -1
- package/src/tools/irc.d.ts +11 -1
- package/src/tools/irc.ts +11 -1
- package/src/tools/job-widget.d.ts +226 -0
- package/src/tools/job-widget.ts +1036 -0
- package/src/tools/json-tree.d.ts +2 -0
- package/src/tools/json-tree.ts +8 -0
- package/src/tools/line-ranges.d.ts +8 -1
- package/src/tools/line-ranges.ts +23 -13
- package/src/tools/mcp.d.ts +23 -0
- package/src/tools/mcp.ts +44 -1
- package/src/tools/output-meta.d.ts +1 -1
- package/src/tools/output-meta.ts +1 -1
- package/src/tools/proc-render.ts +1 -0
- package/src/tools/read.d.ts +8 -2
- package/src/tools/read.ts +64 -21
- package/src/tools/renderer.d.ts +8 -2
- package/src/tools/renderer.ts +8 -2
- package/src/tools/result-card.d.ts +1 -1
- package/src/tools/result-card.ts +1 -2
- package/src/tools/streaming-output.d.ts +1 -0
- package/src/tools/streaming-output.ts +11 -9
- package/src/tools/task.d.ts +12 -0
- package/src/tools/task.ts +25 -1
- package/src/tools/wait.d.ts +11 -0
- package/src/tools/wait.ts +296 -147
- package/src/tui.ts +45 -5
|
@@ -93,7 +93,7 @@ export class AttachmentChipsBand implements Component {
|
|
|
93
93
|
nativeImageNode(Buffer.from(image.data, "base64"), image.mimeType);
|
|
94
94
|
images.set(image, native);
|
|
95
95
|
content = node("image", {
|
|
96
|
-
...native.p,
|
|
96
|
+
...(native.k === "image" ? native.p : undefined),
|
|
97
97
|
alt: title,
|
|
98
98
|
w: dims?.width,
|
|
99
99
|
h: dims?.height,
|
|
@@ -10,6 +10,8 @@ import type { State } from "./types";
|
|
|
10
10
|
export interface OutputBlockOptions {
|
|
11
11
|
header?: string;
|
|
12
12
|
headerMeta?: string;
|
|
13
|
+
/** Facts drawn inside the bottom border (`╰── Wall 0.16s · Timeout 300s ──╯`). */
|
|
14
|
+
footerMeta?: string;
|
|
13
15
|
state?: State;
|
|
14
16
|
sections?: Array<{
|
|
15
17
|
label?: string;
|
|
@@ -17,6 +17,8 @@ import { getStateBgColor, Hasher, padToWidth } from "./utils";
|
|
|
17
17
|
export interface OutputBlockOptions {
|
|
18
18
|
header?: string;
|
|
19
19
|
headerMeta?: string;
|
|
20
|
+
/** Facts drawn inside the bottom border (`╰── Wall 0.16s · Timeout 300s ──╯`). */
|
|
21
|
+
footerMeta?: string;
|
|
20
22
|
state?: State;
|
|
21
23
|
sections?: Array<{ label?: string; lines: readonly string[]; separator?: boolean }>;
|
|
22
24
|
width: number;
|
|
@@ -46,7 +48,7 @@ export function isFramedBlockComponent(component: Component): boolean {
|
|
|
46
48
|
|
|
47
49
|
type BlockRow =
|
|
48
50
|
| { kind: "bar"; leftChar: string; rightChar: string; label?: string; meta?: string }
|
|
49
|
-
| { kind: "bottom"; leftChar: string; rightChar: string }
|
|
51
|
+
| { kind: "bottom"; leftChar: string; rightChar: string; meta?: string }
|
|
50
52
|
| { kind: "content"; inner: string }
|
|
51
53
|
| { kind: "sixel"; raw: string };
|
|
52
54
|
|
|
@@ -157,7 +159,26 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
|
|
157
159
|
}
|
|
158
160
|
}
|
|
159
161
|
|
|
160
|
-
|
|
162
|
+
// Footer facts ride the bottom border only when they fit whole; a clipped
|
|
163
|
+
// border would silently drop trailing facts (exit code, artifact id), so an
|
|
164
|
+
// overflowing footer wraps into body rows above a plain border instead.
|
|
165
|
+
const footerMeta = options.footerMeta;
|
|
166
|
+
const footerFits =
|
|
167
|
+
footerMeta === undefined ||
|
|
168
|
+
visibleWidth(` ${footerMeta} `) <=
|
|
169
|
+
lineWidth - visibleWidth(`${theme.boxRound.bottomLeft}${cap}`) - visibleWidth(theme.boxRound.bottomRight);
|
|
170
|
+
if (footerMeta !== undefined && !footerFits) {
|
|
171
|
+
for (const wrappedLine of wrapTextWithAnsi(footerMeta, contentWidth)) {
|
|
172
|
+
const innerPadding = padding(Math.max(0, contentWidth - visibleWidth(wrappedLine)));
|
|
173
|
+
rows.push({ kind: "content", inner: `${wrappedLine}${innerPadding}` });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
rows.push({
|
|
177
|
+
kind: "bottom",
|
|
178
|
+
leftChar: theme.boxRound.bottomLeft,
|
|
179
|
+
rightChar: theme.boxRound.bottomRight,
|
|
180
|
+
meta: footerFits ? footerMeta : undefined,
|
|
181
|
+
});
|
|
161
182
|
|
|
162
183
|
const H = rows.length;
|
|
163
184
|
|
|
@@ -182,14 +203,6 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
|
|
182
203
|
return `${border(leftGlyphs)}${trimmedLabel}${border(fillGlyphs)}${border(rightGlyph)}`;
|
|
183
204
|
};
|
|
184
205
|
|
|
185
|
-
const renderBottom = (row: { leftChar: string; rightChar: string }): string => {
|
|
186
|
-
const leftGlyphs = `${row.leftChar}${cap}`;
|
|
187
|
-
const rightGlyph = row.rightChar;
|
|
188
|
-
const fillCount = Math.max(0, lineWidth - visibleWidth(leftGlyphs) - visibleWidth(rightGlyph));
|
|
189
|
-
const fillGlyphs = h.repeat(fillCount);
|
|
190
|
-
return `${border(leftGlyphs)}${border(fillGlyphs)}${border(rightGlyph)}`;
|
|
191
|
-
};
|
|
192
|
-
|
|
193
206
|
const renderContent = (inner: string): string =>
|
|
194
207
|
`${border(v)}${contentLeftPadding}${inner}${contentRightPadding}${border(v)}`;
|
|
195
208
|
|
|
@@ -201,8 +214,7 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
|
|
201
214
|
lines.push(row.raw);
|
|
202
215
|
continue;
|
|
203
216
|
}
|
|
204
|
-
const line =
|
|
205
|
-
row.kind === "bar" ? renderBar(row) : row.kind === "bottom" ? renderBottom(row) : renderContent(row.inner);
|
|
217
|
+
const line = row.kind === "bar" || row.kind === "bottom" ? renderBar(row) : renderContent(row.inner);
|
|
206
218
|
lines.push(padToWidth(clipFrame ? truncateToWidth(line, lineWidth, Ellipsis.Omit) : line, lineWidth, bgFn));
|
|
207
219
|
}
|
|
208
220
|
|
|
@@ -360,6 +372,7 @@ export class CachedOutputBlock {
|
|
|
360
372
|
);
|
|
361
373
|
h.optional(options.header);
|
|
362
374
|
h.optional(options.headerMeta);
|
|
375
|
+
h.optional(options.footerMeta);
|
|
363
376
|
h.optional(options.state);
|
|
364
377
|
h.optional(options.borderColor);
|
|
365
378
|
h.bool(options.applyBg ?? true);
|
|
@@ -22,6 +22,8 @@ export interface ToolCardSnapshot {
|
|
|
22
22
|
/** Structured status header, rendered by the shared status-line primitive. */
|
|
23
23
|
status?: StatusLineOptions;
|
|
24
24
|
headerMeta?: string;
|
|
25
|
+
/** Short facts drawn inside the frame's bottom border (framed); a trailing row otherwise. */
|
|
26
|
+
footerMeta?: string;
|
|
25
27
|
phase?: ToolCardPhase;
|
|
26
28
|
body?: ToolCardContent;
|
|
27
29
|
sections?: readonly ToolCardSection[];
|
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;
|
|
@@ -18,7 +18,7 @@ export declare enum NotifyProtocol {
|
|
|
18
18
|
Osc99 = "\u001B]99;;",
|
|
19
19
|
Osc9 = "\u001B]9;"
|
|
20
20
|
}
|
|
21
|
-
export type TerminalId = "kitty" | "ghostty" | "wezterm" | "iterm2" | "vscode" | "alacritty" | "warp" | "orca" | "otty" | "rio" | "tern" | "monstar" | "base" | "trueColor";
|
|
21
|
+
export type TerminalId = "kitty" | "ghostty" | "wezterm" | "iterm2" | "vscode" | "alacritty" | "warp" | "orca" | "otty" | "rio" | "tern" | "monstar" | "zetawork" | "base" | "trueColor";
|
|
22
22
|
/** Terminal capability details used for rendering and protocol selection. */
|
|
23
23
|
export declare class TerminalInfo {
|
|
24
24
|
readonly id: TerminalId;
|
|
@@ -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,
|
|
@@ -66,6 +67,7 @@ export type TerminalId =
|
|
|
66
67
|
| "rio"
|
|
67
68
|
| "tern"
|
|
68
69
|
| "monstar"
|
|
70
|
+
| "zetawork"
|
|
69
71
|
| "base"
|
|
70
72
|
| "trueColor";
|
|
71
73
|
|
|
@@ -403,7 +405,11 @@ export function detectStyledUnderlineSupport(terminalId: TerminalId, env: NodeJS
|
|
|
403
405
|
// render. Enable only on a confirmed major.minor >= 3.5; an absent or
|
|
404
406
|
// unparseable version keeps the flat fallback so only proven terminals
|
|
405
407
|
// get the colon form.
|
|
406
|
-
|
|
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);
|
|
407
413
|
return version !== null && (version.major > 3 || (version.major === 3 && version.minor >= 5));
|
|
408
414
|
}
|
|
409
415
|
default:
|
|
@@ -656,6 +662,15 @@ const KNOWN_TERMINALS = Object.freeze({
|
|
|
656
662
|
// activating it focuses the Monstar window. The BEL path uses omp's own
|
|
657
663
|
// `notify-send` fallback instead, which cannot focus a window.
|
|
658
664
|
monstar: new TerminalInfo("monstar", ImageProtocol.Kitty, true, true, NotifyProtocol.Osc9, false, false, false, 2),
|
|
665
|
+
// zetawork is the workbench GUI's own terminal pane (`termide`
|
|
666
|
+
// `panel-terminal`): it sets TERM_PROGRAM=zetawork and renders Kitty
|
|
667
|
+
// graphics itself — direct placement only, no Unicode placeholders, which
|
|
668
|
+
// is why the id is not in the placeholder allowlist. It answers the cell
|
|
669
|
+
// size query (CSI 16 t) with its real device-pixel cell, so images are
|
|
670
|
+
// rasterized at the density the surface paints at. Its emulator renders
|
|
671
|
+
// OSC 8 links itself; notifications stay on BEL because the GUI has no
|
|
672
|
+
// notifier.
|
|
673
|
+
zetawork: new TerminalInfo("zetawork", ImageProtocol.Kitty, true, true),
|
|
659
674
|
});
|
|
660
675
|
|
|
661
676
|
/** Resolve terminal identity from environment markers used by common emulators. */
|
|
@@ -678,6 +693,7 @@ export function detectTerminalId(env: NodeJS.ProcessEnv = Bun.env): TerminalId {
|
|
|
678
693
|
if (caseEq(program, "rio")) return "rio";
|
|
679
694
|
if (caseEq(program, "tern")) return "tern";
|
|
680
695
|
if (caseEq(program, "monstar")) return "monstar";
|
|
696
|
+
if (caseEq(program, "zetawork")) return "zetawork";
|
|
681
697
|
return null;
|
|
682
698
|
}
|
|
683
699
|
|
|
@@ -689,6 +705,7 @@ export function detectTerminalId(env: NodeJS.ProcessEnv = Bun.env): TerminalId {
|
|
|
689
705
|
VSCODE_PID,
|
|
690
706
|
ALACRITTY_WINDOW_ID,
|
|
691
707
|
TERM_PROGRAM,
|
|
708
|
+
LC_TERMINAL,
|
|
692
709
|
TERM,
|
|
693
710
|
COLORTERM,
|
|
694
711
|
} = env;
|
|
@@ -703,6 +720,22 @@ export function detectTerminalId(env: NodeJS.ProcessEnv = Bun.env): TerminalId {
|
|
|
703
720
|
const programId = fromProgram(TERM_PROGRAM);
|
|
704
721
|
if (programId) return programId;
|
|
705
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
|
+
|
|
706
739
|
// tmux >= 3.2 replaces the pane's identity with `TERM_PROGRAM=tmux`.
|
|
707
740
|
// Its server still holds the attached client's terminal-type reply and TERM.
|
|
708
741
|
for (const clientProgram of resolveTmuxClientTerminalNames(env)) {
|
|
@@ -1104,6 +1137,19 @@ export function encodeITerm2(
|
|
|
1104
1137
|
return `\x1b]1337;File=${params.join(";")}:${base64Data}\x07`;
|
|
1105
1138
|
}
|
|
1106
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
|
+
|
|
1107
1153
|
export function calculateImageRows(
|
|
1108
1154
|
imageDimensions: ImageDimensions,
|
|
1109
1155
|
targetWidthCells: number,
|
|
@@ -1298,7 +1344,9 @@ export function encodeSixelNow(
|
|
|
1298
1344
|
backgroundRgb = getTerminalBackgroundColor(),
|
|
1299
1345
|
): string | null {
|
|
1300
1346
|
try {
|
|
1301
|
-
return
|
|
1347
|
+
return ensureSixelRasterWidth(
|
|
1348
|
+
encodeSixel(new Uint8Array(Buffer.from(base64Data, "base64")), widthPx, heightPx, backgroundRgb),
|
|
1349
|
+
);
|
|
1302
1350
|
} catch {
|
|
1303
1351
|
return null;
|
|
1304
1352
|
}
|
|
@@ -1371,9 +1419,12 @@ export function renderImage(
|
|
|
1371
1419
|
return { sequence, rows };
|
|
1372
1420
|
}
|
|
1373
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.
|
|
1374
1425
|
const sequence = encodeITerm2(base64Data, {
|
|
1375
1426
|
width: fit.columns,
|
|
1376
|
-
height:
|
|
1427
|
+
height: fit.rows,
|
|
1377
1428
|
preserveAspectRatio: options.preserveAspectRatio ?? true,
|
|
1378
1429
|
});
|
|
1379
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
|
};
|