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
- if (totals.latestCacheHitRate !== undefined) {
146
- stats.push(theme.fg(cacheHitColor(totals.latestCacheHitRate), `${glyphs.cacheHit} ${totals.latestCacheHitRate.toFixed(0)}%`));
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 { center, truncateToWidth } from "./utils.ts";
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 frame = 0;
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(_pi: ExtensionAPI, ctx: ExtensionContext, tui: TUI) {
169
+ constructor(pi: ExtensionAPI, ctx: ExtensionContext, _tui: TUI) {
170
+ this.pi = pi;
125
171
  this.ctx = ctx;
126
- this.timer = setInterval(() => {
127
- if (this.frame < LOGO_FRAMES.length - 1) {
128
- this.frame++;
129
- tui.requestRender();
130
- } else {
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 lines: string[] = [];
146
- lines.push(bold(theme.fg("accent", "pi")) + " " + muted(`v${VERSION}`));
147
- lines.push("");
148
-
149
- for (const logoLine of renderLogo(this.frame, paint)) {
150
- lines.push(center(logoLine, width));
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.1",
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
  }