@oh-my-pi/pi-tui 18.2.8 → 18.2.10
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 +22 -0
- package/dist/types/kitty-graphics.d.ts +1 -1
- package/dist/types/overlays/reset-usage-selector.d.ts +14 -2
- package/dist/types/overlays/usage-dashboard.d.ts +6 -0
- package/dist/types/overlays/usage-display.d.ts +16 -1
- package/dist/types/prompt/image-source.d.ts +26 -0
- package/dist/types/prompt/video.d.ts +7 -11
- package/dist/types/status-line/component.d.ts +2 -0
- package/dist/types/status-line/types.d.ts +9 -0
- package/dist/types/stdin-buffer.d.ts +9 -0
- package/dist/types/terminal-capabilities.d.ts +1 -1
- package/dist/types/tmux.d.ts +8 -0
- package/dist/types/tools/eval.d.ts +15 -9
- package/dist/types/tools/find.d.ts +1 -1
- package/dist/types/tools/hub.d.ts +2 -0
- package/dist/types/tui.d.ts +8 -3
- package/dist/types/utils.d.ts +2 -0
- package/package.json +9 -9
- package/src/components/editor.ts +21 -1
- package/src/kitty-graphics.ts +2 -2
- package/src/overlays/composer-shape-preview.ts +3 -1
- package/src/overlays/reset-usage-selector.ts +77 -25
- package/src/overlays/usage-dashboard.ts +62 -2
- package/src/overlays/usage-display.ts +74 -1
- package/src/prompt/custom-editor.ts +3 -2
- package/src/prompt/image-source.ts +51 -0
- package/src/prompt/video.ts +10 -20
- package/src/status-line/component.ts +57 -5
- package/src/status-line/segments.ts +23 -4
- package/src/status-line/types.ts +9 -0
- package/src/stdin-buffer.ts +22 -0
- package/src/terminal-capabilities.ts +38 -12
- package/src/terminal.ts +10 -0
- package/src/tmux.ts +38 -0
- package/src/tools/eval-format/index.ts +1 -0
- package/src/tools/eval.ts +22 -14
- package/src/tools/find.ts +18 -5
- package/src/tools/hub.ts +4 -2
- package/src/tui.ts +21 -10
- package/src/utils.ts +64 -0
package/src/tmux.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { $which } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { isBunTestRuntime } from "@oh-my-pi/pi-utils/env";
|
|
3
|
+
|
|
1
4
|
/** Whether the process is running inside a tmux session. */
|
|
2
5
|
export function isInsideTmux(env: NodeJS.ProcessEnv = Bun.env): boolean {
|
|
3
6
|
return Boolean(env.TMUX);
|
|
@@ -12,3 +15,38 @@ export function wrapTmuxPassthrough(payload: string): string {
|
|
|
12
15
|
export function wrapTmuxPassthroughIfNeeded(payload: string, env: NodeJS.ProcessEnv = Bun.env): string {
|
|
13
16
|
return isInsideTmux(env) ? wrapTmuxPassthrough(payload) : payload;
|
|
14
17
|
}
|
|
18
|
+
|
|
19
|
+
const CLIENT_TERMTYPE_NAME = /^([A-Za-z][A-Za-z0-9._+-]*)(?=\s|\(|$)/u;
|
|
20
|
+
const CLIENT_TERMTYPE_TIMEOUT_MS = 500;
|
|
21
|
+
let cachedClientTerminalName: string | null | undefined;
|
|
22
|
+
|
|
23
|
+
function queryTmuxClientTerminalName(env: NodeJS.ProcessEnv): string | null {
|
|
24
|
+
const tmux = $which("tmux", { PATH: env.PATH });
|
|
25
|
+
if (!tmux) return null;
|
|
26
|
+
try {
|
|
27
|
+
const result = Bun.spawnSync([tmux, "display-message", "-p", "#{client_termtype}"], {
|
|
28
|
+
env,
|
|
29
|
+
stdout: "pipe",
|
|
30
|
+
stderr: "ignore",
|
|
31
|
+
timeout: CLIENT_TERMTYPE_TIMEOUT_MS,
|
|
32
|
+
killSignal: "SIGKILL",
|
|
33
|
+
});
|
|
34
|
+
if (result.exitCode !== 0) return null;
|
|
35
|
+
return CLIENT_TERMTYPE_NAME.exec(result.stdout.toString().trim())?.[1] ?? null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the terminal emulator name recorded for this tmux client.
|
|
43
|
+
*
|
|
44
|
+
* tmux overwrites pane identity variables with its own values, but retains the
|
|
45
|
+
* attached client's terminal-type reply in `#{client_termtype}`. The local IPC
|
|
46
|
+
* query runs once per process and degrades to `null` when unavailable.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveTmuxClientTerminalName(env: NodeJS.ProcessEnv = Bun.env): string | null {
|
|
49
|
+
if (!isInsideTmux(env) || isBunTestRuntime()) return null;
|
|
50
|
+
if (cachedClientTerminalName === undefined) cachedClientTerminalName = queryTmuxClientTerminalName(env);
|
|
51
|
+
return cachedClientTerminalName;
|
|
52
|
+
}
|
|
@@ -7,6 +7,7 @@ export * from "./python";
|
|
|
7
7
|
|
|
8
8
|
/** Formats an arbitrary eval-code prefix for display without changing the executed source. */
|
|
9
9
|
export function formatEvalCodeForDisplay(source: string, language: EvalLanguage): string {
|
|
10
|
+
if (/^\s*%/.test(source)) return source;
|
|
10
11
|
switch (language) {
|
|
11
12
|
case "js":
|
|
12
13
|
return formatJavaScriptForDisplay(source);
|
package/src/tools/eval.ts
CHANGED
|
@@ -93,10 +93,7 @@ interface EvalRenderCellArg {
|
|
|
93
93
|
title?: string;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
interface EvalRenderArgs {
|
|
97
|
-
language?: string;
|
|
98
|
-
code?: string;
|
|
99
|
-
title?: string;
|
|
96
|
+
interface EvalRenderArgs extends EvalRenderCellArg {
|
|
100
97
|
cells?: EvalRenderCellArg[];
|
|
101
98
|
__partialJson?: string;
|
|
102
99
|
}
|
|
@@ -126,10 +123,9 @@ function getRenderCells(args: EvalRenderArgs | undefined): EvalRenderCell[] {
|
|
|
126
123
|
for (const cell of raw) {
|
|
127
124
|
if (!cell || typeof cell !== "object") continue;
|
|
128
125
|
const language = normalizeRenderLanguage(typeof cell.language === "string" ? cell.language : undefined);
|
|
129
|
-
const code = typeof cell.code === "string" ? cell.code : "";
|
|
130
126
|
out.push({
|
|
131
127
|
language,
|
|
132
|
-
code: formatEvalCodeForDisplay(code, language),
|
|
128
|
+
code: formatEvalCodeForDisplay(cell.code ?? "", language),
|
|
133
129
|
title: typeof cell.title === "string" ? cell.title : undefined,
|
|
134
130
|
});
|
|
135
131
|
}
|
|
@@ -139,16 +135,28 @@ function getRenderCells(args: EvalRenderArgs | undefined): EvalRenderCell[] {
|
|
|
139
135
|
type AgentEventStatus = "pending" | "running" | "completed" | "failed" | "aborted";
|
|
140
136
|
|
|
141
137
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* hundreds of throttled progress ticks.
|
|
138
|
+
* Coalescing key of a progress-snapshot event: `agent` and `judge_batch`
|
|
139
|
+
* events keyed by `id`, where only the newest snapshot matters. Discrete
|
|
140
|
+
* actions (everything else) have no key. Shared by {@link upsertStatusEvent}
|
|
141
|
+
* and the executors' display collectors so both coalesce identically.
|
|
147
142
|
*/
|
|
148
|
-
export function
|
|
143
|
+
export function statusEventKey(event: { op: string; [key: string]: unknown }): string | undefined {
|
|
149
144
|
if ((event.op === "agent" || event.op === "judge_batch") && typeof event.id === "string") {
|
|
150
|
-
|
|
151
|
-
|
|
145
|
+
return `${event.op}\0${event.id}`;
|
|
146
|
+
}
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Append or replace a status event. Progress snapshots (see
|
|
152
|
+
* {@link statusEventKey}) coalesce in place, preserving first-seen order; every
|
|
153
|
+
* other op is a discrete action and simply appends. Keeps the persisted event
|
|
154
|
+
* list bounded even when a subagent or batch emits hundreds of progress ticks.
|
|
155
|
+
*/
|
|
156
|
+
export function upsertStatusEvent(events: EvalStatusEvent[], event: EvalStatusEvent): void {
|
|
157
|
+
const key = statusEventKey(event);
|
|
158
|
+
if (key !== undefined) {
|
|
159
|
+
const idx = events.findIndex(e => statusEventKey(e) === key);
|
|
152
160
|
if (idx >= 0) {
|
|
153
161
|
events[idx] = event;
|
|
154
162
|
return;
|
package/src/tools/find.ts
CHANGED
|
@@ -7,7 +7,15 @@ import * as path from "node:path";
|
|
|
7
7
|
import { formatDuration, formatNumber } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import { renderProgressBar } from "../components/progress-bar";
|
|
9
9
|
import { Text } from "../components/text";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
Ellipsis,
|
|
12
|
+
fileHyperlink,
|
|
13
|
+
getTreeBranch,
|
|
14
|
+
renderStatusLine,
|
|
15
|
+
renderTreeList,
|
|
16
|
+
truncateToWidth,
|
|
17
|
+
uriHyperlink,
|
|
18
|
+
} from "../render";
|
|
11
19
|
import {
|
|
12
20
|
createCachedComponent,
|
|
13
21
|
formatCount,
|
|
@@ -31,7 +39,7 @@ export interface FindRange {
|
|
|
31
39
|
|
|
32
40
|
/** A file whose verified passages cleared the threshold; `ranges` are merged positive spans, strongest first. */
|
|
33
41
|
export interface FindHit {
|
|
34
|
-
/** Display path relative to {@link FindToolDetails.cwd}. */
|
|
42
|
+
/** Display path relative to {@link FindToolDetails.cwd}, or an `omp://` doc URL for docs scopes. */
|
|
35
43
|
rel: string;
|
|
36
44
|
/** Filename judgment, when the name batch answered. */
|
|
37
45
|
nameScore?: number;
|
|
@@ -116,9 +124,14 @@ function gauge(p: number, theme: Theme): string {
|
|
|
116
124
|
}
|
|
117
125
|
|
|
118
126
|
function renderHit(hit: FindHit, rangeLimit: number, cwd: string | undefined, theme: Theme): string[] {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
127
|
+
// `omp://` hits are virtual docs, not files under `cwd`: link the URL
|
|
128
|
+
// itself instead of joining it onto a filesystem base.
|
|
129
|
+
const isOmpHit = /^omp:\/\//i.test(hit.rel);
|
|
130
|
+
const link = (text: string, line?: number): string => {
|
|
131
|
+
if (isOmpHit) return uriHyperlink(line === undefined ? hit.rel : `${hit.rel}:${line}`, text);
|
|
132
|
+
const absPath = cwd === undefined ? undefined : path.join(cwd, hit.rel);
|
|
133
|
+
return absPath === undefined ? text : fileHyperlink(absPath, text, { line });
|
|
134
|
+
};
|
|
122
135
|
const coverage = hit.truncated ? `${hit.linesSeen} lines judged, partial` : `${hit.linesSeen} lines judged`;
|
|
123
136
|
const lines = [
|
|
124
137
|
`${gauge(hit.contentScore, theme)} ${theme.fg(scoreColor(hit.contentScore), hit.contentScore.toFixed(2))} ${link(theme.fg("accent", hit.rel))} ${theme.fg("dim", coverage)}`,
|
package/src/tools/hub.ts
CHANGED
|
@@ -100,6 +100,8 @@ export interface JobSnapshot {
|
|
|
100
100
|
status: "running" | "completed" | "failed" | "cancelled";
|
|
101
101
|
label: string;
|
|
102
102
|
durationMs: number;
|
|
103
|
+
/** Process exit status when the job reports one. */
|
|
104
|
+
exitCode?: number;
|
|
103
105
|
/** Effective task model selector, including an explicit reasoning suffix when configured. */
|
|
104
106
|
resolvedModel?: string;
|
|
105
107
|
/** Provider/id including routing, with no added thinking suffix. */
|
|
@@ -570,11 +572,11 @@ export function jobsRenderResult(
|
|
|
570
572
|
renderItem: (job, context) => {
|
|
571
573
|
const rowWidth = Math.max(0, width - (context.prefixWidth ?? 0));
|
|
572
574
|
const lines: string[] = [];
|
|
573
|
-
const icon = formatStatusIcon(
|
|
575
|
+
const icon = `${formatStatusIcon(
|
|
574
576
|
statusToIcon(job.status),
|
|
575
577
|
uiTheme,
|
|
576
578
|
job.status === "running" ? options.spinnerFrame : undefined,
|
|
577
|
-
)
|
|
579
|
+
)}${job.exitCode === undefined ? "" : `${uiTheme.sep.dot}${uiTheme.fg(job.exitCode === 0 ? "muted" : "error", `exit ${job.exitCode}`)}`}`;
|
|
578
580
|
const typeBadge = formatBadge(job.type, statusToColor(job.status), uiTheme);
|
|
579
581
|
const durationSuffix = `${uiTheme.sep.dot}${uiTheme.fg("dim", formatDuration(job.durationMs))}`;
|
|
580
582
|
const displayId = truncateToWidth(
|
package/src/tui.ts
CHANGED
|
@@ -137,9 +137,12 @@ export interface TuiPaint {
|
|
|
137
137
|
readonly rows: number;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/** Observer of completed terminal paints; see {@link TUI.addPaintListener}. */
|
|
141
|
+
export type PaintListener = (paint: TuiPaint) => void;
|
|
142
|
+
|
|
140
143
|
export interface TUIOptions {
|
|
141
144
|
renderScheduler?: RenderScheduler;
|
|
142
|
-
onPaint?:
|
|
145
|
+
onPaint?: PaintListener;
|
|
143
146
|
}
|
|
144
147
|
/** Physical terminal dimensions supplied to a frame provider. */
|
|
145
148
|
export interface ViewportSize {
|
|
@@ -789,7 +792,7 @@ export class TUI extends Container {
|
|
|
789
792
|
#debugNextWindowTop = 0;
|
|
790
793
|
#inputListeners = new Set<InputListener>();
|
|
791
794
|
#startListeners = new Set<StartListener>();
|
|
792
|
-
#
|
|
795
|
+
#paintListeners = new Set<PaintListener>();
|
|
793
796
|
|
|
794
797
|
/** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */
|
|
795
798
|
onDebug?: () => void;
|
|
@@ -922,7 +925,7 @@ export class TUI extends Container {
|
|
|
922
925
|
super();
|
|
923
926
|
this.terminal = terminal;
|
|
924
927
|
this.#renderScheduler = options?.renderScheduler ?? DEFAULT_RENDER_SCHEDULER;
|
|
925
|
-
|
|
928
|
+
if (options?.onPaint) this.#paintListeners.add(options.onPaint);
|
|
926
929
|
this.#showHardwareCursor = showHardwareCursor === undefined ? this.#showHardwareCursor : showHardwareCursor;
|
|
927
930
|
this.#watchdog = new LoopWatchdog();
|
|
928
931
|
}
|
|
@@ -931,9 +934,15 @@ export class TUI extends Container {
|
|
|
931
934
|
return mode === "append" || mode === "rebuild" || mode === "preserve" ? mode : "preserve";
|
|
932
935
|
}
|
|
933
936
|
|
|
934
|
-
/**
|
|
935
|
-
|
|
936
|
-
|
|
937
|
+
/**
|
|
938
|
+
* Observe completed terminal paints; returns the unsubscribe. Independent
|
|
939
|
+
* observers (live stream publisher, session recorder) coexist.
|
|
940
|
+
*/
|
|
941
|
+
addPaintListener(listener: PaintListener): () => void {
|
|
942
|
+
this.#paintListeners.add(listener);
|
|
943
|
+
return () => {
|
|
944
|
+
this.#paintListeners.delete(listener);
|
|
945
|
+
};
|
|
937
946
|
}
|
|
938
947
|
|
|
939
948
|
/** Install the product-owned bounded frame provider. */
|
|
@@ -2636,10 +2645,12 @@ export class TUI extends Container {
|
|
|
2636
2645
|
}
|
|
2637
2646
|
|
|
2638
2647
|
#notifyPaint(paint: TuiPaint): void {
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2648
|
+
for (const listener of this.#paintListeners) {
|
|
2649
|
+
try {
|
|
2650
|
+
listener(paint);
|
|
2651
|
+
} catch (err) {
|
|
2652
|
+
logger.error("TUI paint listener failed", { err });
|
|
2653
|
+
}
|
|
2643
2654
|
}
|
|
2644
2655
|
}
|
|
2645
2656
|
|
package/src/utils.ts
CHANGED
|
@@ -384,6 +384,70 @@ export function visibleWidth(str: string): number {
|
|
|
384
384
|
return width;
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
/** Remove ANSI, OSC, and APC control sequences while preserving visible text. */
|
|
388
|
+
export function stripTerminalSequences(str: string): string {
|
|
389
|
+
if (!str.includes("\x1b")) return str;
|
|
390
|
+
let result = "";
|
|
391
|
+
let i = 0;
|
|
392
|
+
while (i < str.length) {
|
|
393
|
+
const ansi = extractAnsiCode(str, i);
|
|
394
|
+
if (ansi) {
|
|
395
|
+
i += ansi.length;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
result += str[i];
|
|
399
|
+
i++;
|
|
400
|
+
}
|
|
401
|
+
return result;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Extract ANSI escape sequences from a string at the given position.
|
|
406
|
+
* Copied verbatim from pi-mono `packages/tui/src/utils.ts` alongside
|
|
407
|
+
* `stripTerminalSequences` so both behave identically to upstream pi. Kept
|
|
408
|
+
* module-private: it was removed from the public API in 9.6.2 and this port
|
|
409
|
+
* does not reintroduce that export.
|
|
410
|
+
*/
|
|
411
|
+
function extractAnsiCode(str: string, pos: number): { code: string; length: number } | null {
|
|
412
|
+
if (pos >= str.length || str[pos] !== "\x1b") return null;
|
|
413
|
+
|
|
414
|
+
const next = str[pos + 1];
|
|
415
|
+
|
|
416
|
+
// CSI sequence: ESC [ ... m/G/K/H/J
|
|
417
|
+
if (next === "[") {
|
|
418
|
+
let j = pos + 2;
|
|
419
|
+
while (j < str.length && !/[mGKHJ]/.test(str[j]!)) j++;
|
|
420
|
+
if (j < str.length) return { code: str.substring(pos, j + 1), length: j + 1 - pos };
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// OSC sequence: ESC ] ... BEL or ESC ] ... ST (ESC \)
|
|
425
|
+
// Used for hyperlinks (OSC 8), window titles, etc.
|
|
426
|
+
if (next === "]") {
|
|
427
|
+
let j = pos + 2;
|
|
428
|
+
while (j < str.length) {
|
|
429
|
+
if (str[j] === "\x07") return { code: str.substring(pos, j + 1), length: j + 1 - pos };
|
|
430
|
+
if (str[j] === "\x1b" && str[j + 1] === "\\") return { code: str.substring(pos, j + 2), length: j + 2 - pos };
|
|
431
|
+
j++;
|
|
432
|
+
}
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// APC sequence: ESC _ ... BEL or ESC _ ... ST (ESC \)
|
|
437
|
+
// Used for cursor marker and application-specific commands
|
|
438
|
+
if (next === "_") {
|
|
439
|
+
let j = pos + 2;
|
|
440
|
+
while (j < str.length) {
|
|
441
|
+
if (str[j] === "\x07") return { code: str.substring(pos, j + 1), length: j + 1 - pos };
|
|
442
|
+
if (str[j] === "\x1b" && str[j + 1] === "\\") return { code: str.substring(pos, j + 2), length: j + 2 - pos };
|
|
443
|
+
j++;
|
|
444
|
+
}
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
387
451
|
/**
|
|
388
452
|
* Scaled spans must bypass wrapping/padding and, when scaled up, reserve the
|
|
389
453
|
* terminal rows their multicell glyphs flow into.
|