pi-open-tui 0.3.3 → 0.3.4
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.
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
CustomEditor,
|
|
3
|
-
type ExtensionAPI,
|
|
1
|
+
import {
|
|
2
|
+
CustomEditor,
|
|
3
|
+
type ExtensionAPI,
|
|
4
4
|
type ExtensionContext,
|
|
5
5
|
type KeybindingsManager,
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
@@ -23,40 +23,45 @@ const CURSOR_STYLE_SEQUENCES: Partial<Record<CursorStyle, string>> = {
|
|
|
23
23
|
bar: "\x1b[6 q",
|
|
24
24
|
underline: "\x1b[4 q",
|
|
25
25
|
};
|
|
26
|
-
const DEFAULT_CURSOR_STYLE_SEQUENCE = "\x1b[0 q";
|
|
27
|
-
|
|
28
|
-
interface
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
26
|
+
const DEFAULT_CURSOR_STYLE_SEQUENCE = "\x1b[0 q";
|
|
27
|
+
|
|
28
|
+
interface WorkingStatusIndicator {
|
|
29
|
+
renderInBorder(width: number): string;
|
|
30
|
+
renderSpinnerInBorder(width: number): string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface HiddenThinkingLabelComponent {
|
|
34
|
+
children: unknown[];
|
|
35
|
+
setHiddenThinkingLabel(label: string): void;
|
|
36
|
+
setHideThinkingBlock(hide: boolean): void;
|
|
37
|
+
updateContent(message: unknown, isStreaming?: boolean): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isHiddenThinkingLabelComponent(value: object): value is HiddenThinkingLabelComponent {
|
|
41
|
+
const component = value as unknown as Record<string, unknown>;
|
|
42
|
+
return Array.isArray(component.children)
|
|
43
|
+
&& typeof component.setHiddenThinkingLabel === "function"
|
|
44
|
+
&& typeof component.setHideThinkingBlock === "function"
|
|
45
|
+
&& typeof component.updateContent === "function";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Ponytail until Pi exposes a per-message hidden-thinking label API. */
|
|
49
|
+
function findLatestHiddenThinkingLabel(tui: TUI): HiddenThinkingLabelComponent {
|
|
50
|
+
const pending: unknown[] = [tui];
|
|
51
|
+
const visited = new Set<object>();
|
|
52
|
+
let latest: HiddenThinkingLabelComponent | undefined;
|
|
53
|
+
while (pending.length > 0) {
|
|
54
|
+
const value = pending.pop();
|
|
55
|
+
if (!value || typeof value !== "object" || visited.has(value)) continue;
|
|
56
|
+
visited.add(value);
|
|
57
|
+
if (isHiddenThinkingLabelComponent(value)) latest = value;
|
|
58
|
+
const children = (value as { children?: unknown }).children;
|
|
59
|
+
if (!Array.isArray(children)) continue;
|
|
60
|
+
for (let i = children.length - 1; i >= 0; i--) pending.push(children[i]);
|
|
61
|
+
}
|
|
62
|
+
if (!latest) throw new Error("Unable to find the active hidden-thinking component");
|
|
63
|
+
return latest;
|
|
64
|
+
}
|
|
60
65
|
|
|
61
66
|
function removeSoftwareCursor(line: string, cursorMarker = ""): string {
|
|
62
67
|
return line.replace(/\x1b\[7m([\s\S]*?)\x1b\[0m/g, (_match, cursor: string) => {
|
|
@@ -78,10 +83,42 @@ function roundedBorder(
|
|
|
78
83
|
kind: "top" | "bottom",
|
|
79
84
|
paint: (s: string) => string,
|
|
80
85
|
sourceLine?: string,
|
|
86
|
+
indicator?: WorkingStatusIndicator,
|
|
81
87
|
): string {
|
|
82
88
|
if (width < 2) return paint(truncateToWidth(kind === "top" ? "╭╮" : "╰╯", width, ""));
|
|
83
89
|
const corners = kind === "top" ? (["╭", "╮"] as const) : (["╰", "╯"] as const);
|
|
84
90
|
|
|
91
|
+
if (kind === "top" && indicator) {
|
|
92
|
+
const plain = sourceLine ? stripAnsi(sourceLine) : "";
|
|
93
|
+
const scrollMatch = plain.match(/([↑↓]\s+\d+\s+more)/);
|
|
94
|
+
const contentWidth = width - 2;
|
|
95
|
+
let status = indicator.renderInBorder(Math.max(1, contentWidth - 5));
|
|
96
|
+
let statusWidth = visibleWidth(status);
|
|
97
|
+
if (statusWidth > 0) {
|
|
98
|
+
const overflowLabel = scrollMatch ? ` ${scrollMatch[1]} ` : undefined;
|
|
99
|
+
const overflowLabelWidth = overflowLabel ? visibleWidth(overflowLabel) : 0;
|
|
100
|
+
const overflowStart = Math.floor((contentWidth - overflowLabelWidth) / 2);
|
|
101
|
+
const canFitOverflow = () => overflowLabel !== undefined
|
|
102
|
+
&& overflowLabelWidth + 2 <= contentWidth
|
|
103
|
+
&& overflowStart - (3 + statusWidth + 1) >= 1;
|
|
104
|
+
if (overflowLabel && !canFitOverflow()) {
|
|
105
|
+
status = indicator.renderSpinnerInBorder(contentWidth);
|
|
106
|
+
statusWidth = visibleWidth(status);
|
|
107
|
+
}
|
|
108
|
+
if (canFitOverflow()) {
|
|
109
|
+
const leftBlockWidth = 3 + statusWidth + 1;
|
|
110
|
+
return `${corners[0]}${paint("── ")}${status}${paint(` ${"─".repeat(overflowStart - leftBlockWidth)}${overflowLabel}${"─".repeat(contentWidth - overflowStart - overflowLabelWidth)}`)}${corners[1]}`;
|
|
111
|
+
}
|
|
112
|
+
if (contentWidth >= statusWidth + 5) {
|
|
113
|
+
return `${corners[0]}${paint("── ")}${status}${paint(` ${"─".repeat(contentWidth - statusWidth - 4)}`)}${corners[1]}`;
|
|
114
|
+
}
|
|
115
|
+
status = indicator.renderSpinnerInBorder(contentWidth);
|
|
116
|
+
statusWidth = visibleWidth(status);
|
|
117
|
+
const prefixWidth = Math.min(3, Math.max(0, contentWidth - statusWidth));
|
|
118
|
+
return `${corners[0]}${paint("─".repeat(prefixWidth))}${status}${paint("─".repeat(Math.max(0, contentWidth - prefixWidth - statusWidth)))}${corners[1]}`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
85
122
|
if (sourceLine) {
|
|
86
123
|
const plain = stripAnsi(sourceLine);
|
|
87
124
|
const scrollMatch = plain.match(/([↑↓]\s+\d+\s+more)/);
|
|
@@ -96,8 +133,10 @@ function roundedBorder(
|
|
|
96
133
|
}
|
|
97
134
|
|
|
98
135
|
export class OpenTuiEditor extends CustomEditor {
|
|
136
|
+
readonly embedWorkingStatus = true;
|
|
99
137
|
private readonly getRail: () => string;
|
|
100
138
|
private readonly getBorder: (s: string) => string;
|
|
139
|
+
private embeddedWorkingStatusIndicator: WorkingStatusIndicator | undefined;
|
|
101
140
|
private cursorStyle: CursorStyle;
|
|
102
141
|
private previewHardwareCursor = false;
|
|
103
142
|
|
|
@@ -122,6 +161,11 @@ export class OpenTuiEditor extends CustomEditor {
|
|
|
122
161
|
super.setPaddingX(0);
|
|
123
162
|
}
|
|
124
163
|
|
|
164
|
+
setWorkingStatusIndicator(indicator: WorkingStatusIndicator | undefined): void {
|
|
165
|
+
this.embeddedWorkingStatusIndicator = indicator;
|
|
166
|
+
this.tui.requestRender();
|
|
167
|
+
}
|
|
168
|
+
|
|
125
169
|
setCursorStyle(cursorStyle: CursorStyle, blockHardwareCursor = false): void {
|
|
126
170
|
const styleChanged = cursorStyle !== this.cursorStyle;
|
|
127
171
|
this.previewHardwareCursor = cursorStyle !== "block";
|
|
@@ -163,7 +207,7 @@ export class OpenTuiEditor extends CustomEditor {
|
|
|
163
207
|
const bottomIdx = findBottomBorderIndex(baseLines);
|
|
164
208
|
|
|
165
209
|
const result: string[] = [];
|
|
166
|
-
result.push(roundedBorder(width, "top", borderPaint, baseLines[0]));
|
|
210
|
+
result.push(roundedBorder(width, "top", borderPaint, baseLines[0], this.embeddedWorkingStatusIndicator));
|
|
167
211
|
|
|
168
212
|
for (let i = 1; i < bottomIdx; i++) {
|
|
169
213
|
const line = baseLines[i] ?? "";
|
|
@@ -192,51 +236,51 @@ export function installEditor(
|
|
|
192
236
|
) {
|
|
193
237
|
let activeTui: TUI | undefined;
|
|
194
238
|
let activeEditor: OpenTuiEditor | undefined;
|
|
195
|
-
let previousHardwareCursor: boolean | undefined;
|
|
196
|
-
let currentCursorStyle = cursorStyle;
|
|
197
|
-
let currentWheelScrollLines = wheelScrollLines;
|
|
198
|
-
let hiddenThinkingTarget: HiddenThinkingLabelComponent | undefined;
|
|
199
|
-
const getActiveTui = (): TUI => {
|
|
200
|
-
if (!activeTui) throw new Error("Open TUI editor is not mounted");
|
|
201
|
-
return activeTui;
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
ctx.ui.setEditorComponent((tui, editorTheme, keybindings) => {
|
|
205
|
-
activeTui = tui;
|
|
206
|
-
hiddenThinkingTarget = undefined;
|
|
207
|
-
applyFullscreenWheelScrollLines(tui, currentWheelScrollLines);
|
|
239
|
+
let previousHardwareCursor: boolean | undefined;
|
|
240
|
+
let currentCursorStyle = cursorStyle;
|
|
241
|
+
let currentWheelScrollLines = wheelScrollLines;
|
|
242
|
+
let hiddenThinkingTarget: HiddenThinkingLabelComponent | undefined;
|
|
243
|
+
const getActiveTui = (): TUI => {
|
|
244
|
+
if (!activeTui) throw new Error("Open TUI editor is not mounted");
|
|
245
|
+
return activeTui;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
ctx.ui.setEditorComponent((tui, editorTheme, keybindings) => {
|
|
249
|
+
activeTui = tui;
|
|
250
|
+
hiddenThinkingTarget = undefined;
|
|
251
|
+
applyFullscreenWheelScrollLines(tui, currentWheelScrollLines);
|
|
208
252
|
previousHardwareCursor = tui.getShowHardwareCursor();
|
|
209
253
|
activeEditor = new OpenTuiEditor(tui, editorTheme, keybindings, currentCursorStyle);
|
|
210
254
|
return activeEditor;
|
|
211
|
-
});
|
|
212
|
-
return {
|
|
213
|
-
getViewportWidth(): number {
|
|
214
|
-
const columns = getActiveTui().terminal.columns;
|
|
215
|
-
if (typeof columns !== "number" || !Number.isFinite(columns)) {
|
|
216
|
-
throw new Error("Open TUI editor terminal has an invalid width");
|
|
217
|
-
}
|
|
218
|
-
return Math.max(1, Math.floor(columns));
|
|
219
|
-
},
|
|
220
|
-
setLatestHiddenThinkingLabel(label: string): void {
|
|
221
|
-
const tui = getActiveTui();
|
|
222
|
-
hiddenThinkingTarget ??= findLatestHiddenThinkingLabel(tui);
|
|
223
|
-
hiddenThinkingTarget.setHiddenThinkingLabel(label);
|
|
224
|
-
tui.requestRender();
|
|
225
|
-
},
|
|
226
|
-
resetHiddenThinkingLabelTarget(): void {
|
|
227
|
-
hiddenThinkingTarget = undefined;
|
|
228
|
-
},
|
|
229
|
-
setCursorStyle(nextCursorStyle: CursorStyle): void {
|
|
230
|
-
currentCursorStyle = nextCursorStyle;
|
|
231
|
-
activeEditor?.setCursorStyle(nextCursorStyle, previousHardwareCursor);
|
|
255
|
+
});
|
|
256
|
+
return {
|
|
257
|
+
getViewportWidth(): number {
|
|
258
|
+
const columns = getActiveTui().terminal.columns;
|
|
259
|
+
if (typeof columns !== "number" || !Number.isFinite(columns)) {
|
|
260
|
+
throw new Error("Open TUI editor terminal has an invalid width");
|
|
261
|
+
}
|
|
262
|
+
return Math.max(1, Math.floor(columns));
|
|
263
|
+
},
|
|
264
|
+
setLatestHiddenThinkingLabel(label: string): void {
|
|
265
|
+
const tui = getActiveTui();
|
|
266
|
+
hiddenThinkingTarget ??= findLatestHiddenThinkingLabel(tui);
|
|
267
|
+
hiddenThinkingTarget.setHiddenThinkingLabel(label);
|
|
268
|
+
tui.requestRender();
|
|
269
|
+
},
|
|
270
|
+
resetHiddenThinkingLabelTarget(): void {
|
|
271
|
+
hiddenThinkingTarget = undefined;
|
|
272
|
+
},
|
|
273
|
+
setCursorStyle(nextCursorStyle: CursorStyle): void {
|
|
274
|
+
currentCursorStyle = nextCursorStyle;
|
|
275
|
+
activeEditor?.setCursorStyle(nextCursorStyle, previousHardwareCursor);
|
|
232
276
|
},
|
|
233
277
|
setWheelScrollLines(nextWheelScrollLines: number): void {
|
|
234
278
|
currentWheelScrollLines = nextWheelScrollLines;
|
|
235
279
|
if (activeTui) applyFullscreenWheelScrollLines(activeTui, currentWheelScrollLines);
|
|
236
280
|
},
|
|
237
|
-
cleanup(): void {
|
|
238
|
-
hiddenThinkingTarget = undefined;
|
|
239
|
-
ctx.ui.setEditorComponent(undefined);
|
|
281
|
+
cleanup(): void {
|
|
282
|
+
hiddenThinkingTarget = undefined;
|
|
283
|
+
ctx.ui.setEditorComponent(undefined);
|
|
240
284
|
if (activeTui) {
|
|
241
285
|
if (currentCursorStyle !== "block") activeTui.terminal.write(DEFAULT_CURSOR_STYLE_SEQUENCE);
|
|
242
286
|
if (previousHardwareCursor !== undefined) activeTui.setShowHardwareCursor(previousHardwareCursor);
|
|
@@ -3,7 +3,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
import type { GitStatus } from "./git.ts";
|
|
4
4
|
import { emptyGitStatus } from "./git.ts";
|
|
5
5
|
import type { RuntimeInfo } from "./runtime.ts";
|
|
6
|
-
import { finiteOrZero,
|
|
6
|
+
import { finiteOrZero, formatProviderLabel } from "./utils.ts";
|
|
7
7
|
|
|
8
8
|
export interface FooterState {
|
|
9
9
|
git: GitStatus;
|
|
@@ -239,6 +239,7 @@ export function effortColor(level: ThinkingLevel | string | undefined): ThemeCol
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
export function isEditorBorderLine(line: string): boolean {
|
|
242
|
+
if (typeof line !== "string") return false;
|
|
242
243
|
const plain = stripAnsi(line);
|
|
243
244
|
if (/^─+$/.test(plain)) return true;
|
|
244
245
|
if (/^─*\s*[↑↓]\s+\d+\s+more\s*─*$/.test(plain)) return true;
|
|
@@ -247,7 +248,8 @@ export function isEditorBorderLine(line: string): boolean {
|
|
|
247
248
|
|
|
248
249
|
export function findBottomBorderIndex(lines: string[]): number {
|
|
249
250
|
for (let i = lines.length - 1; i >= 1; i--) {
|
|
250
|
-
|
|
251
|
+
const line = lines[i];
|
|
252
|
+
if (line !== undefined && isEditorBorderLine(line)) return i;
|
|
251
253
|
}
|
|
252
254
|
return Math.max(0, lines.length - 1);
|
|
253
255
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-open-tui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
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": {
|