pi-open-tui 0.3.13 → 0.3.14
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.
|
@@ -40,6 +40,8 @@ export interface InlineFooterRenderer {
|
|
|
40
40
|
|
|
41
41
|
interface HiddenThinkingLabelComponent {
|
|
42
42
|
children: unknown[];
|
|
43
|
+
outputPad?: unknown;
|
|
44
|
+
render(width: number): string[];
|
|
43
45
|
setHiddenThinkingLabel(label: string): void;
|
|
44
46
|
setHideThinkingBlock(hide: boolean): void;
|
|
45
47
|
updateContent(message: unknown, isStreaming?: boolean): void;
|
|
@@ -48,6 +50,7 @@ interface HiddenThinkingLabelComponent {
|
|
|
48
50
|
function isHiddenThinkingLabelComponent(value: object): value is HiddenThinkingLabelComponent {
|
|
49
51
|
const component = value as unknown as Record<string, unknown>;
|
|
50
52
|
return Array.isArray(component.children)
|
|
53
|
+
&& typeof component.render === "function"
|
|
51
54
|
&& typeof component.setHiddenThinkingLabel === "function"
|
|
52
55
|
&& typeof component.setHideThinkingBlock === "function"
|
|
53
56
|
&& typeof component.updateContent === "function";
|
|
@@ -71,6 +74,13 @@ function findLatestHiddenThinkingLabel(tui: TUI): HiddenThinkingLabelComponent {
|
|
|
71
74
|
return latest;
|
|
72
75
|
}
|
|
73
76
|
|
|
77
|
+
/** Width Pi's hidden-thinking `Text(label, outputPad, 0)` wraps at when its message renders at `width`. */
|
|
78
|
+
function hiddenThinkingTextWidth(width: number, outputPad: unknown): number {
|
|
79
|
+
const pad = typeof outputPad === "number" && Number.isFinite(outputPad) ? Math.max(0, Math.floor(outputPad)) : 1;
|
|
80
|
+
const paddingX = Math.min(pad, Math.max(0, Math.floor((width - 1) / 2)));
|
|
81
|
+
return Math.max(1, width - paddingX * 2);
|
|
82
|
+
}
|
|
83
|
+
|
|
74
84
|
function removeSoftwareCursor(line: string, cursorMarker = ""): string {
|
|
75
85
|
return line.replace(/\x1b\[7m([\s\S]*?)\x1b\[0m/g, (_match, cursor: string) => {
|
|
76
86
|
const replacement = `${cursorMarker}${cursor}`;
|
|
@@ -308,6 +318,42 @@ export class OpenTuiEditor extends CustomEditor {
|
|
|
308
318
|
}
|
|
309
319
|
}
|
|
310
320
|
|
|
321
|
+
type StreamWrite = (...args: unknown[]) => boolean;
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Calls `requestRepaint` after each raw write to a TTY stream. Pi's fullscreen renderer only
|
|
325
|
+
* rewrites changed rows, so text other extensions print straight to the terminal would
|
|
326
|
+
* otherwise stay over the editor and footer until a resize.
|
|
327
|
+
*/
|
|
328
|
+
export function repaintAfterStreamWrites(
|
|
329
|
+
requestRepaint: () => void,
|
|
330
|
+
stream: NodeJS.WriteStream = process.stderr,
|
|
331
|
+
): () => void {
|
|
332
|
+
if (!stream.isTTY) return () => {};
|
|
333
|
+
const original = stream.write as unknown as StreamWrite;
|
|
334
|
+
let enabled = true;
|
|
335
|
+
const patched = ((...args: unknown[]) => {
|
|
336
|
+
const callback = typeof args[args.length - 1] === "function"
|
|
337
|
+
? (args.pop() as (error?: Error | null) => void)
|
|
338
|
+
: undefined;
|
|
339
|
+
// Repaint from the write callback so the redraw lands after the stray text.
|
|
340
|
+
return original.call(stream, ...args, (error?: Error | null) => {
|
|
341
|
+
try {
|
|
342
|
+
if (enabled) requestRepaint();
|
|
343
|
+
} catch {
|
|
344
|
+
// A failed repaint must not break the caller's write; logging it here would recurse.
|
|
345
|
+
}
|
|
346
|
+
callback?.(error);
|
|
347
|
+
});
|
|
348
|
+
}) as typeof stream.write;
|
|
349
|
+
stream.write = patched;
|
|
350
|
+
return () => {
|
|
351
|
+
enabled = false;
|
|
352
|
+
// A later wrapper may still delegate to ours; leave it as a passthrough then.
|
|
353
|
+
if (stream.write === patched) stream.write = original as unknown as typeof stream.write;
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
311
357
|
export function installEditor(
|
|
312
358
|
_pi: ExtensionAPI,
|
|
313
359
|
ctx: ExtensionContext,
|
|
@@ -319,41 +365,69 @@ export function installEditor(
|
|
|
319
365
|
let previousHardwareCursor: boolean | undefined;
|
|
320
366
|
let currentCursorStyle = cursorStyle;
|
|
321
367
|
let hiddenThinkingTarget: HiddenThinkingLabelComponent | undefined;
|
|
368
|
+
let restoreHiddenThinkingRender: (() => void) | undefined;
|
|
369
|
+
let buildHiddenThinkingLabel: ((width: number) => string) | undefined;
|
|
322
370
|
const getActiveTui = (): TUI => {
|
|
323
371
|
if (!activeTui) throw new Error("Open TUI editor is not mounted");
|
|
324
372
|
return activeTui;
|
|
325
373
|
};
|
|
374
|
+
const resetHiddenThinkingTarget = (): void => {
|
|
375
|
+
restoreHiddenThinkingRender?.();
|
|
376
|
+
restoreHiddenThinkingRender = undefined;
|
|
377
|
+
hiddenThinkingTarget = undefined;
|
|
378
|
+
buildHiddenThinkingLabel = undefined;
|
|
379
|
+
};
|
|
380
|
+
// Build the label at the width Pi actually renders the message with; layout
|
|
381
|
+
// extensions (e.g. pi-sidebar) can make it much narrower than the terminal.
|
|
382
|
+
const patchHiddenThinkingRender = (target: HiddenThinkingLabelComponent): void => {
|
|
383
|
+
const originalRender = target.render;
|
|
384
|
+
let lastWidth: number | undefined;
|
|
385
|
+
let lastBuild: ((width: number) => string) | undefined;
|
|
386
|
+
target.render = function (this: HiddenThinkingLabelComponent, width: number): string[] {
|
|
387
|
+
const build = buildHiddenThinkingLabel;
|
|
388
|
+
const textWidth = hiddenThinkingTextWidth(width, this.outputPad);
|
|
389
|
+
if (build && (build !== lastBuild || textWidth !== lastWidth)) {
|
|
390
|
+
lastBuild = build;
|
|
391
|
+
lastWidth = textWidth;
|
|
392
|
+
this.setHiddenThinkingLabel(build(textWidth));
|
|
393
|
+
}
|
|
394
|
+
return originalRender.call(this, width);
|
|
395
|
+
};
|
|
396
|
+
restoreHiddenThinkingRender = () => {
|
|
397
|
+
target.render = originalRender;
|
|
398
|
+
};
|
|
399
|
+
};
|
|
400
|
+
// Only the fullscreen renderer clears and rewrites the whole screen on a forced render.
|
|
401
|
+
const restoreStderr = repaintAfterStreamWrites(() => {
|
|
402
|
+
if (activeTui?.mode === "fullscreen") activeTui.requestRender(true);
|
|
403
|
+
});
|
|
326
404
|
|
|
327
405
|
ctx.ui.setEditorComponent((tui, editorTheme, keybindings) => {
|
|
328
406
|
activeTui = tui;
|
|
329
|
-
|
|
407
|
+
resetHiddenThinkingTarget();
|
|
330
408
|
previousHardwareCursor = tui.getShowHardwareCursor();
|
|
331
409
|
activeEditor = new OpenTuiEditor(tui, editorTheme, keybindings, currentCursorStyle, inlineFooter);
|
|
332
410
|
return activeEditor;
|
|
333
411
|
});
|
|
334
412
|
return {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (typeof columns !== "number" || !Number.isFinite(columns)) {
|
|
338
|
-
throw new Error("Open TUI editor terminal has an invalid width");
|
|
339
|
-
}
|
|
340
|
-
return Math.max(1, Math.floor(columns));
|
|
341
|
-
},
|
|
342
|
-
setLatestHiddenThinkingLabel(label: string): void {
|
|
413
|
+
/** `build` receives the hidden-thinking text width and runs again whenever that width changes. */
|
|
414
|
+
setLatestHiddenThinkingLabel(build: (width: number) => string): void {
|
|
343
415
|
const tui = getActiveTui();
|
|
344
|
-
|
|
345
|
-
|
|
416
|
+
if (!hiddenThinkingTarget) {
|
|
417
|
+
hiddenThinkingTarget = findLatestHiddenThinkingLabel(tui);
|
|
418
|
+
patchHiddenThinkingRender(hiddenThinkingTarget);
|
|
419
|
+
}
|
|
420
|
+
buildHiddenThinkingLabel = build;
|
|
346
421
|
tui.requestRender();
|
|
347
422
|
},
|
|
348
|
-
resetHiddenThinkingLabelTarget
|
|
349
|
-
hiddenThinkingTarget = undefined;
|
|
350
|
-
},
|
|
423
|
+
resetHiddenThinkingLabelTarget: resetHiddenThinkingTarget,
|
|
351
424
|
setCursorStyle(nextCursorStyle: CursorStyle): void {
|
|
352
425
|
currentCursorStyle = nextCursorStyle;
|
|
353
426
|
activeEditor?.setCursorStyle(nextCursorStyle, previousHardwareCursor);
|
|
354
427
|
},
|
|
355
428
|
cleanup(): void {
|
|
356
|
-
|
|
429
|
+
restoreStderr();
|
|
430
|
+
resetHiddenThinkingTarget();
|
|
357
431
|
ctx.ui.setEditorComponent(undefined);
|
|
358
432
|
if (activeTui) {
|
|
359
433
|
if (currentCursorStyle !== "block") activeTui.terminal.write(DEFAULT_CURSOR_STYLE_SEQUENCE);
|
|
@@ -24,9 +24,6 @@ import {
|
|
|
24
24
|
} from "./peek.ts";
|
|
25
25
|
|
|
26
26
|
type PendingUiChange = "install" | "uninstall";
|
|
27
|
-
const HIDDEN_THINKING_HORIZONTAL_PADDING = 2;
|
|
28
|
-
// Pi's fullscreen transcript can reserve one more column for its scrollbar.
|
|
29
|
-
const HIDDEN_THINKING_SCROLLBAR_RESERVE = 1;
|
|
30
27
|
|
|
31
28
|
export function getPendingUiChange(enabled: boolean, active: boolean): PendingUiChange | undefined {
|
|
32
29
|
if (enabled === active) return undefined;
|
|
@@ -78,29 +75,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
78
75
|
/** Pi decides whether this label is visible; our toggle only controls updates. */
|
|
79
76
|
const isPeekEnabled = () => sessionLifecycle.isCurrent() && config.enabled && config.thinkingPeek.lines > 0 && active;
|
|
80
77
|
|
|
81
|
-
/** Keep each native hidden-thinking label row within Pi's narrowest transcript width. */
|
|
82
|
-
const hiddenThinkingLabelWidth = (): number => {
|
|
83
|
-
if (!editor) throw new Error("Open TUI editor is not installed");
|
|
84
|
-
return Math.max(
|
|
85
|
-
1,
|
|
86
|
-
editor.getViewportWidth() - HIDDEN_THINKING_HORIZONTAL_PADDING - HIDDEN_THINKING_SCROLLBAR_RESERVE,
|
|
87
|
-
);
|
|
88
|
-
};
|
|
89
|
-
|
|
90
78
|
const setPeekLabel = (ctx?: ExtensionContext): void => {
|
|
91
79
|
if (!isPeekEnabled() || peek.phase === "idle") return;
|
|
92
80
|
const target = ctx ?? lastCtx;
|
|
93
81
|
if (!target || !isTuiContext(target)) return;
|
|
94
82
|
if (!editor) throw new Error("Open TUI editor is not installed");
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
hiddenThinkingLabelWidth(),
|
|
101
|
-
config.thinkingPeek.lines,
|
|
102
|
-
),
|
|
103
|
-
);
|
|
83
|
+
const snapshot = { ...peek };
|
|
84
|
+
const frame = peekFrame;
|
|
85
|
+
const glyphs = resolveGlyphs(config.icons.mode);
|
|
86
|
+
const lines = config.thinkingPeek.lines;
|
|
87
|
+
editor.setLatestHiddenThinkingLabel((width) => buildPeekLabel(snapshot, frame, glyphs, width, lines));
|
|
104
88
|
peekLabelActive = true;
|
|
105
89
|
};
|
|
106
90
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-open-tui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.14",
|
|
4
4
|
"description": "A polished TUI for Pi coding agent: animated logo header, Starship-style footer, rounded editor with model metadata, and prompt-box user messages.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|