@oh-my-pi/pi-tui 18.12.2 → 18.12.3
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 +17 -0
- 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/sixel.d.ts +28 -0
- package/dist/types/status-line/host.d.ts +3 -0
- package/dist/types/tools/irc.d.ts +6 -0
- package/dist/types/tools/job-widget.d.ts +226 -0
- package/dist/types/tools/output-meta.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/components/editor.ts +17 -7
- package/src/components/image.ts +5 -4
- package/src/components/sparkline.ts +89 -0
- package/src/index.ts +1 -0
- 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 +10 -0
- package/src/overlays/model-browser.ts +3 -0
- package/src/prompt/attachment-chips.ts +1 -1
- 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 +4 -1
- package/src/theme/symbols.ts +5 -3
- package/src/tools/irc.ts +6 -0
- package/src/tools/job-widget.ts +1036 -0
- package/src/tools/output-meta.ts +1 -1
- package/src/tools/streaming-output.ts +11 -9
- package/src/tools/task.ts +25 -1
- package/src/tools/wait.ts +141 -85
- package/src/tui.ts +45 -5
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 omp to open. */
|
|
63
67
|
export type StatusLineNativeAction = "status.model" | "status.context" | "status.git" | "status.cost" | "status.path";
|
|
64
68
|
|
|
@@ -446,6 +450,8 @@ class StatusLineExternalInputs {
|
|
|
446
450
|
isFastModeActive = false;
|
|
447
451
|
anthropicSlowModeLabel: string | undefined = undefined;
|
|
448
452
|
compactionSpeculation: unknown = undefined;
|
|
453
|
+
/** The freshest running job widget's view (stable identity while its data is unchanged). */
|
|
454
|
+
jobWidgetRef: unknown = undefined;
|
|
449
455
|
}
|
|
450
456
|
|
|
451
457
|
interface CachedStatusLine {
|
|
@@ -2796,6 +2802,13 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2796
2802
|
? this.session.getAnthropicSlowModeLabel()
|
|
2797
2803
|
: undefined;
|
|
2798
2804
|
target.compactionSpeculation = this.session.compactionSpeculation;
|
|
2805
|
+
target.jobWidgetRef = this.#latestJobWidget()?.view;
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
/** The running job widget that saw data last; the ANSI jobs segment appends its glance. */
|
|
2809
|
+
#latestJobWidget(): JobWidgetEntry | undefined {
|
|
2810
|
+
const entries = this.session.getRunningJobWidgets?.();
|
|
2811
|
+
return entries ? latestJobWidget(entries) : undefined;
|
|
2799
2812
|
}
|
|
2800
2813
|
|
|
2801
2814
|
#sameStatusLineExternalInputs(left: StatusLineExternalInputs, right: StatusLineExternalInputs): boolean {
|
|
@@ -2849,7 +2862,8 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
2849
2862
|
left.isAutoThinking === right.isAutoThinking &&
|
|
2850
2863
|
left.isFastModeActive === right.isFastModeActive &&
|
|
2851
2864
|
left.anthropicSlowModeLabel === right.anthropicSlowModeLabel &&
|
|
2852
|
-
left.compactionSpeculation === right.compactionSpeculation
|
|
2865
|
+
left.compactionSpeculation === right.compactionSpeculation &&
|
|
2866
|
+
left.jobWidgetRef === right.jobWidgetRef
|
|
2853
2867
|
);
|
|
2854
2868
|
}
|
|
2855
2869
|
|
|
@@ -3058,7 +3072,10 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
3058
3072
|
if (layout !== "plain-left") {
|
|
3059
3073
|
const runningBackgroundJobs = this.runningBackgroundJobCount();
|
|
3060
3074
|
if (runningBackgroundJobs > 0) {
|
|
3061
|
-
|
|
3075
|
+
const widget = this.#latestJobWidget();
|
|
3076
|
+
const glance = widget ? formatJobWidgetGlance(widget.view, theme, JOBS_SEGMENT_SPARK_CELLS) : "";
|
|
3077
|
+
const count = theme.fg("statusLineSubagents", `${theme.icon.job} ${runningBackgroundJobs}`);
|
|
3078
|
+
rightParts.unshift(glance ? `${count} ${glance}` : count);
|
|
3062
3079
|
}
|
|
3063
3080
|
if (subagentBadge) rightParts.unshift(subagentBadge);
|
|
3064
3081
|
}
|
package/src/status-line/host.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import type { Tool, UsageLimit, UsageReport } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import type { Model } from "@oh-my-pi/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;
|
|
@@ -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,
|
|
@@ -1281,7 +1282,9 @@ export function encodeSixelNow(
|
|
|
1281
1282
|
backgroundRgb = getTerminalBackgroundColor(),
|
|
1282
1283
|
): string | null {
|
|
1283
1284
|
try {
|
|
1284
|
-
return
|
|
1285
|
+
return ensureSixelRasterWidth(
|
|
1286
|
+
encodeSixel(new Uint8Array(Buffer.from(base64Data, "base64")), widthPx, heightPx, backgroundRgb),
|
|
1287
|
+
);
|
|
1285
1288
|
} catch {
|
|
1286
1289
|
return null;
|
|
1287
1290
|
}
|
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/irc.ts
CHANGED
|
@@ -21,6 +21,12 @@ export interface IrcMessage {
|
|
|
21
21
|
* ping-pong forever.
|
|
22
22
|
*/
|
|
23
23
|
wakeRelay?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* One leg of an `all` broadcast. Like relays, broadcasts never draw an
|
|
26
|
+
* automatic wake-turn reply: every woken idle peer would otherwise answer
|
|
27
|
+
* the broadcaster, mostly to say the message was not for it.
|
|
28
|
+
*/
|
|
29
|
+
broadcast?: boolean;
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
/** Delivery outcome for one peer recipient. */
|