pi-open-tui 0.2.1 → 0.2.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.
|
@@ -142,8 +142,11 @@ function renderStatsBlock(
|
|
|
142
142
|
if (segments.tokens) {
|
|
143
143
|
stats.push(theme.fg("accent", `${glyphs.input} ${fmtTokens(totals.input)}`));
|
|
144
144
|
stats.push(theme.fg("success", `${glyphs.output} ${fmtTokens(totals.output)}`));
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
// ponytail: hide cache-hit rate when the provider never reported cache
|
|
146
|
+
// tokens — avoids a misleading "0%" on providers without prompt caching.
|
|
147
|
+
const hasCacheTokens = totals.cacheRead > 0 || totals.cacheWrite > 0;
|
|
148
|
+
if (hasCacheTokens && totals.latestCacheHitRate !== undefined) {
|
|
149
|
+
stats.push(theme.fg(cacheHitColor(totals.latestCacheHitRate), `${glyphs.cacheHit} ${totals.latestCacheHitRate.toFixed(1)}%`));
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
152
|
if (segments.cost) {
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import { VERSION, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
center,
|
|
5
|
+
collectPiCommandNames,
|
|
6
|
+
formatCwd,
|
|
7
|
+
formatModelLabel,
|
|
8
|
+
formatThinkingLabel,
|
|
9
|
+
headerColumnWidths,
|
|
10
|
+
padRight,
|
|
11
|
+
pickSlashCommandTips,
|
|
12
|
+
truncateToWidth,
|
|
13
|
+
visibleWidth,
|
|
14
|
+
} from "./utils.ts";
|
|
4
15
|
|
|
5
16
|
const LOGO_CELL = "███";
|
|
6
|
-
const LOGO_ANIMATION_MS = 80;
|
|
7
17
|
|
|
8
18
|
type LogoColor = "panel" | "cyan" | "red" | "green" | "orange" | "white" | "flash" | "brand";
|
|
9
19
|
type LogoFrame = { phase: number; active: "left" | "top" | "right" | "none"; ax: number; ay: number; flash: boolean; white: boolean };
|
|
@@ -116,50 +126,107 @@ function renderLogo(frameIndex: number, paintBrand: (text: string) => string): s
|
|
|
116
126
|
});
|
|
117
127
|
}
|
|
118
128
|
|
|
129
|
+
function borderLine(
|
|
130
|
+
left: string,
|
|
131
|
+
label: string,
|
|
132
|
+
right: string,
|
|
133
|
+
width: number,
|
|
134
|
+
paint: (text: string) => string,
|
|
135
|
+
): string {
|
|
136
|
+
if (width <= 1) return "";
|
|
137
|
+
if (width < 8 || label.length === 0) {
|
|
138
|
+
return paint(truncateToWidth(left + "─".repeat(Math.max(0, width - 2)) + right, width, ""));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const before = "─── ";
|
|
142
|
+
const after = " ─────";
|
|
143
|
+
const fixedWidth = visibleWidth(before) + visibleWidth(label) + visibleWidth(after);
|
|
144
|
+
const fill = Math.max(0, width - 2 - fixedWidth);
|
|
145
|
+
return `${paint(left)}${paint(before)}${label}${paint(after)}${paint("─".repeat(fill))}${paint(right)}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function boxedLine(content: string, width: number, paint: (text: string) => string): string {
|
|
149
|
+
if (width <= 2) return truncateToWidth(content, width, "");
|
|
150
|
+
return `${paint("│")}${padRight(content, width - 2)}${paint("│")}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function twoColumn(
|
|
154
|
+
left: string,
|
|
155
|
+
right: string,
|
|
156
|
+
leftWidth: number,
|
|
157
|
+
rightWidth: number,
|
|
158
|
+
paint: (text: string) => string,
|
|
159
|
+
): string {
|
|
160
|
+
return `${padRight(left, leftWidth)} ${paint("│")} ${padRight(right, rightWidth, "…")}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
119
163
|
export class OpenTuiHeader implements Component {
|
|
120
|
-
private
|
|
121
|
-
private readonly timer: ReturnType<typeof setInterval>;
|
|
164
|
+
private readonly pi: ExtensionAPI;
|
|
122
165
|
private readonly ctx: ExtensionContext;
|
|
166
|
+
private readonly frame = LOGO_FRAMES.length - 1;
|
|
167
|
+
private readonly tipCommands: string[];
|
|
123
168
|
|
|
124
|
-
constructor(
|
|
169
|
+
constructor(pi: ExtensionAPI, ctx: ExtensionContext, _tui: TUI) {
|
|
170
|
+
this.pi = pi;
|
|
125
171
|
this.ctx = ctx;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
clearInterval(this.timer);
|
|
132
|
-
}
|
|
133
|
-
}, LOGO_ANIMATION_MS);
|
|
134
|
-
this.timer.unref?.();
|
|
172
|
+
const pool = collectPiCommandNames(pi.getCommands());
|
|
173
|
+
this.tipCommands = pickSlashCommandTips(pool, {
|
|
174
|
+
fixed: ["open-tui"],
|
|
175
|
+
count: 3,
|
|
176
|
+
});
|
|
135
177
|
}
|
|
136
178
|
|
|
137
179
|
render(width: number): string[] {
|
|
138
180
|
const theme = this.ctx.ui.theme;
|
|
139
181
|
const paint = (s: string) => theme.fg("accent", s);
|
|
140
182
|
const muted = (s: string) => theme.fg("muted", s);
|
|
183
|
+
const dim = (s: string) => theme.fg("dim", s);
|
|
141
184
|
const bold = (s: string) => theme.bold(s);
|
|
142
185
|
|
|
143
186
|
if (width < 24) return [paint(`Pi v${VERSION}`)];
|
|
144
187
|
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
188
|
+
const innerWidth = width - 2;
|
|
189
|
+
const { leftWidth, rightWidth, useTips } = headerColumnWidths(innerWidth);
|
|
190
|
+
const model = formatModelLabel(this.ctx.model);
|
|
191
|
+
const effort = formatThinkingLabel(this.pi.getThinkingLevel());
|
|
192
|
+
const cwd = formatCwd(this.ctx.cwd);
|
|
193
|
+
|
|
194
|
+
const leftLines = [
|
|
195
|
+
...renderLogo(this.frame, paint).map((line) => center(line, leftWidth)),
|
|
196
|
+
center(bold("Let's build something great"), leftWidth),
|
|
197
|
+
center(muted(`${model} · ${effort}`), leftWidth),
|
|
198
|
+
center(dim(cwd), leftWidth),
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
const tipDivider = paint("─".repeat(Math.max(8, Math.min(rightWidth, 22))));
|
|
202
|
+
const [cmd0 = "", cmd1 = "", cmd2 = "", cmd3 = ""] = this.tipCommands;
|
|
203
|
+
const tipLines = [
|
|
204
|
+
"",
|
|
205
|
+
paint(bold("Welcome")),
|
|
206
|
+
muted("Ask Pi anything"),
|
|
207
|
+
tipDivider,
|
|
208
|
+
paint(bold("Commands")),
|
|
209
|
+
muted(cmd0),
|
|
210
|
+
muted(cmd1),
|
|
211
|
+
muted(cmd2),
|
|
212
|
+
muted(cmd3),
|
|
213
|
+
"",
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
const lines = [borderLine("╭", `${paint("Pi")} v${VERSION}`, "╮", width, paint)];
|
|
217
|
+
for (let i = 0; i < leftLines.length; i++) {
|
|
218
|
+
const content = useTips
|
|
219
|
+
? twoColumn(leftLines[i] ?? "", tipLines[i] ?? "", leftWidth, rightWidth, paint)
|
|
220
|
+
: padRight(leftLines[i] ?? "", leftWidth);
|
|
221
|
+
lines.push(boxedLine(content, width, paint));
|
|
151
222
|
}
|
|
152
|
-
|
|
153
|
-
lines.push(center(bold("Let's build something great"), width));
|
|
154
|
-
|
|
223
|
+
lines.push(borderLine("╰", "", "╯", width, paint));
|
|
155
224
|
return lines.map((line) => truncateToWidth(line, width, ""));
|
|
156
225
|
}
|
|
157
226
|
|
|
158
227
|
invalidate(): void {}
|
|
159
228
|
|
|
160
|
-
dispose(): void {
|
|
161
|
-
clearInterval(this.timer);
|
|
162
|
-
}
|
|
229
|
+
dispose(): void {}
|
|
163
230
|
}
|
|
164
231
|
|
|
165
232
|
export function installHeader(pi: ExtensionAPI, ctx: ExtensionContext): () => void {
|
|
@@ -155,3 +155,110 @@ export function sanitizeStatus(text: string): string {
|
|
|
155
155
|
.replace(/ +/g, " ")
|
|
156
156
|
.trim();
|
|
157
157
|
}
|
|
158
|
+
|
|
159
|
+
export function formatThinkingLabel(level: string): string {
|
|
160
|
+
if (level === "off") return "thinking off";
|
|
161
|
+
return `${level} effort`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const PI_BUILTIN_SLASH_COMMAND_NAMES = [
|
|
165
|
+
"settings",
|
|
166
|
+
"model",
|
|
167
|
+
"scoped-models",
|
|
168
|
+
"export",
|
|
169
|
+
"import",
|
|
170
|
+
"share",
|
|
171
|
+
"copy",
|
|
172
|
+
"name",
|
|
173
|
+
"session",
|
|
174
|
+
"changelog",
|
|
175
|
+
"hotkeys",
|
|
176
|
+
"fork",
|
|
177
|
+
"clone",
|
|
178
|
+
"tree",
|
|
179
|
+
"trust",
|
|
180
|
+
"login",
|
|
181
|
+
"logout",
|
|
182
|
+
"new",
|
|
183
|
+
"compact",
|
|
184
|
+
"resume",
|
|
185
|
+
"reload",
|
|
186
|
+
"quit",
|
|
187
|
+
] as const;
|
|
188
|
+
|
|
189
|
+
export function collectPiCommandNames(sessionCommands: readonly { name: string }[]): string[] {
|
|
190
|
+
const names = new Set<string>(PI_BUILTIN_SLASH_COMMAND_NAMES);
|
|
191
|
+
for (const command of sessionCommands) {
|
|
192
|
+
if (command.name) names.add(command.name);
|
|
193
|
+
}
|
|
194
|
+
return [...names];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function pickSlashCommandTips(
|
|
198
|
+
availableNames: readonly string[],
|
|
199
|
+
options: {
|
|
200
|
+
fixed?: readonly string[];
|
|
201
|
+
count?: number;
|
|
202
|
+
exclude?: readonly string[];
|
|
203
|
+
random?: () => number;
|
|
204
|
+
} = {},
|
|
205
|
+
): string[] {
|
|
206
|
+
const fixed = [...(options.fixed ?? [])];
|
|
207
|
+
const count = options.count ?? 3;
|
|
208
|
+
const exclude = new Set<string>([...(options.exclude ?? []), ...fixed]);
|
|
209
|
+
const random = options.random ?? Math.random;
|
|
210
|
+
|
|
211
|
+
const pool = [...new Set(availableNames.map((n) => n.trim()).filter(Boolean))].filter(
|
|
212
|
+
(name) => !exclude.has(name),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
for (let i = pool.length - 1; i > 0; i--) {
|
|
216
|
+
const j = Math.floor(random() * (i + 1));
|
|
217
|
+
const tmp = pool[i]!;
|
|
218
|
+
pool[i] = pool[j]!;
|
|
219
|
+
pool[j] = tmp;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const picked = pool.slice(0, Math.max(0, count));
|
|
223
|
+
return [...fixed, ...picked].map((name) => (name.startsWith("/") ? name : `/${name}`));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export const MIN_LEFT_WIDTH = 28;
|
|
227
|
+
export const MIN_TIPS_WIDTH = 16;
|
|
228
|
+
export const MAX_TIPS_WIDTH = 28;
|
|
229
|
+
const COLUMN_GAP = 3;
|
|
230
|
+
|
|
231
|
+
export function headerColumnWidths(
|
|
232
|
+
innerWidth: number,
|
|
233
|
+
minTipsWidth = MIN_TIPS_WIDTH,
|
|
234
|
+
maxTipsWidth = MAX_TIPS_WIDTH,
|
|
235
|
+
minLeftWidth = MIN_LEFT_WIDTH,
|
|
236
|
+
): { leftWidth: number; rightWidth: number; useTips: boolean } {
|
|
237
|
+
if (innerWidth <= 0) {
|
|
238
|
+
return { leftWidth: 0, rightWidth: 0, useTips: false };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const gap = COLUMN_GAP;
|
|
242
|
+
if (innerWidth < minLeftWidth + gap + minTipsWidth) {
|
|
243
|
+
return { leftWidth: innerWidth, rightWidth: 0, useTips: false };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
let rightWidth = Math.min(maxTipsWidth, Math.max(minTipsWidth, Math.round(innerWidth * 0.28)));
|
|
247
|
+
let leftWidth = innerWidth - gap - rightWidth;
|
|
248
|
+
|
|
249
|
+
if (leftWidth < minLeftWidth) {
|
|
250
|
+
leftWidth = minLeftWidth;
|
|
251
|
+
rightWidth = innerWidth - gap - leftWidth;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (leftWidth <= rightWidth) {
|
|
255
|
+
leftWidth = Math.ceil((innerWidth - gap) * 0.65);
|
|
256
|
+
rightWidth = innerWidth - gap - leftWidth;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (rightWidth < minTipsWidth || leftWidth < minLeftWidth) {
|
|
260
|
+
return { leftWidth: innerWidth, rightWidth: 0, useTips: false };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return { leftWidth, rightWidth, useTips: true };
|
|
264
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-open-tui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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. Combines the best of pi-haiku, pi-claude-code-tui, and pi-zentui.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -55,6 +55,8 @@
|
|
|
55
55
|
"allowScripts": {
|
|
56
56
|
"protobufjs@7.6.5": true,
|
|
57
57
|
"protobufjs@7.6.4": true,
|
|
58
|
-
"@google/genai@1.52.0": true
|
|
58
|
+
"@google/genai@1.52.0": true,
|
|
59
|
+
"koffi@2.16.2": true,
|
|
60
|
+
"tree-sitter-bash@0.25.1": true
|
|
59
61
|
}
|
|
60
62
|
}
|