pi-jar 0.1.1 → 0.1.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.
package/src/welcome.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { sliceByColumn, stripTerminalSequences, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
2
  import { FLAME_RAMP, FLAME_WIDTH, flameFrame, rgb, supportsTruecolor } from "./flame.ts";
3
3
  import { cleanText } from "./status.ts";
4
+ import { iconSet, withIcon } from "./icons.ts";
4
5
 
5
6
  export type WelcomeAction = "settings" | "refresh" | "roles" | "plan" | "goal" | `resume:${number}`;
6
7
 
@@ -89,8 +90,11 @@ const center = (line: string, width: number) => {
89
90
  const left = Math.floor(room / 2);
90
91
  return " ".repeat(left) + line + " ".repeat(room - left);
91
92
  };
93
+ // truncateToWidth re-segments every grapheme even when a line already fits; visibleWidth is
94
+ // memoized by Pi, so measure first and only truncate real overflow (same output either way).
95
+ const fitTo = (line: string, width: number) => visibleWidth(line) <= width ? line : truncateToWidth(line, width);
92
96
  const fixedCell = (line: string, width: number) => {
93
- const value = truncateToWidth(line, width);
97
+ const value = fitTo(line, width);
94
98
  return value + " ".repeat(Math.max(0, width - visibleWidth(value)));
95
99
  };
96
100
 
@@ -157,12 +161,12 @@ function card(width: number, fg: Paint, info: WelcomeInfo): string[] {
157
161
  const label = (name: string, value: string, color: WelcomeColor = "muted") =>
158
162
  fg("dim", name.padEnd(labelWidth)) + fg(color, cleanText(value, 120));
159
163
 
160
- const header = [fg("accent", "pi-jar" + (info.version ? " " + info.version : "")),
161
- info.model ? fg("muted", cleanText(info.model, 40)) : fg("dim", "no model"),
164
+ const header = [fg("accent", withIcon("pi", "pi-jar" + (info.version ? " " + info.version : ""))),
165
+ info.model ? fg("muted", withIcon("model", cleanText(info.model, 40))) : fg("dim", "no model"),
162
166
  info.effort && info.effort !== "off" ? fg("muted", info.effort) : "",
163
167
  info.activeRole ? fg("warning", "role:" + cleanText(info.activeRole, 16)) : ""].filter(Boolean).join(fg("dim", " · "));
164
168
 
165
- const git = info.branch ? "git " + cleanText(info.branch, 24) + (info.dirty ? " · dirty" : "") : "git unavailable";
169
+ const git = info.branch ? withIcon("branch", cleanText(info.branch, 24)) + (info.dirty ? " · dirty" : "") : withIcon("branch", "unavailable");
166
170
  const quota = info.quota != null ? `quota ${Math.round(info.quota)}%` : `quota ${info.quotaEnabled ? "unavailable" : "off"}`;
167
171
  const plan = info.plan?.enabled ? "◆ planning" + (info.plan.title ? " · " + info.plan.title : "")
168
172
  : info.plan?.title ? "last · " + info.plan.title + ` (${info.plan.steps} steps)` : "none · /plan <idea>";
@@ -185,7 +189,7 @@ function card(width: number, fg: Paint, info: WelcomeInfo): string[] {
185
189
  cell(center(fg("dim", "— welcome to ") + fg("accent", "pi-jar") + fg("dim", " —"), inner)),
186
190
  cell(""),
187
191
  divider,
188
- cell(label("PROJECT", (info.project || "unavailable") + " · " + git, info.dirty ? "warning" : "muted")),
192
+ cell(label("PROJECT", withIcon("folder", info.project || "unavailable") + " · " + git, info.dirty ? "warning" : "muted")),
189
193
  cell(label("SESSION", [info.context ?? "ctx ?", quota, info.cost ?? ""].filter(Boolean).join(" · "))),
190
194
  divider,
191
195
  cell(label("PLAN", plan, info.plan?.enabled ? "warning" : "muted")),
@@ -203,6 +207,16 @@ function card(width: number, fg: Paint, info: WelcomeInfo): string[] {
203
207
  ];
204
208
  }
205
209
 
210
+ const CARD_COLORS: readonly WelcomeColor[] = ["accent", "warning", "error", "muted", "dim"];
211
+ let cardCache: { key: string; lines: string[] } | undefined;
212
+
213
+ /** The card only changes with its inputs and theme; the flame beside it repaints every tick. */
214
+ function cachedCard(width: number, fg: Paint, info: WelcomeInfo): string[] {
215
+ const key = `${width}\0${supportsTruecolor()}\0${iconSet()}\0${CARD_COLORS.map((color) => fg(color, "x")).join("")}\0${JSON.stringify(info)}`;
216
+ if (cardCache?.key !== key) cardCache = { key, lines: card(width, fg, info) };
217
+ return cardCache.lines;
218
+ }
219
+
206
220
  /** Which welcome action (if any) sits under a zero-based cell in the rendered lines. */
207
221
  export function welcomeHit(lines: readonly string[], x: number, y: number): WelcomeAction | undefined {
208
222
  const line = lines[y];
@@ -227,7 +241,7 @@ export function welcomeSettingsHit(lines: readonly string[], x: number, y: numbe
227
241
 
228
242
  export function welcomeLines(width: number, frame: number, fg: Paint, info: WelcomeInfo = {}): string[] {
229
243
  if (width <= 0) return [];
230
- const fit = (line: string) => truncateToWidth(line, width);
244
+ const fit = (line: string) => fitTo(line, width);
231
245
  const seed = info.flameSeed ?? 1;
232
246
  const flame = flameFrame(frame, seed, (color, text) => fg(color, text));
233
247
  if (width < 32) {
@@ -242,19 +256,25 @@ export function welcomeLines(width: number, frame: number, fg: Paint, info: Welc
242
256
  ];
243
257
  }
244
258
  const wide = width >= 72;
245
- const flameArt = flame.map((line) => center(fixedCell(line, FLAME_WIDTH), ART_WIDTH));
259
+ // FlameSim rows are exactly FLAME_WIDTH single-cell glyphs, so center them without measuring:
260
+ // re-measuring freshly colored rows every 150 ms tick was most of the welcome's render time.
261
+ const flameLeft = " ".repeat(Math.floor((ART_WIDTH - FLAME_WIDTH) / 2));
262
+ const flameRight = " ".repeat(ART_WIDTH - FLAME_WIDTH - flameLeft.length);
263
+ const flameArt = flame.map((line) => flameLeft + line + flameRight);
246
264
  if (!wide) {
247
- const art = [...flame.slice(2).map((line) => center(fixedCell(line, FLAME_WIDTH), Math.min(width, ART_WIDTH))),
248
- ...PI_COMPACT.map((line) => fg("accent", center(line, Math.min(width, ART_WIDTH))))];
249
- return [...art.map(fit), ...card(width, fg, info).map(fit), ""];
265
+ // width >= 32 here, so ART_WIDTH-wide art rows always fit.
266
+ const art = [...flameArt.slice(2), ...PI_COMPACT.map((line) => fg("accent", center(line, ART_WIDTH)))];
267
+ return [...art, ...cachedCard(width, fg, info).map(fit), ""];
250
268
  }
251
269
  const art = [...flameArt, ...PI_LARGE.map((line) => fg("accent", center(fixedCell(line, 21), ART_WIDTH)))];
252
- const details = card(width - ART_WIDTH - 2, fg, info);
270
+ const details = cachedCard(width - ART_WIDTH - 2, fg, info);
253
271
  const topPad = Math.max(0, Math.floor((details.length - art.length) / 2));
254
272
  const artRows = [...Array.from({ length: topPad }, () => " ".repeat(ART_WIDTH)), ...art];
255
273
  const merged = Array.from({ length: Math.max(artRows.length, details.length) }, (_, index) => {
256
274
  const left = artRows[index] ?? " ".repeat(ART_WIDTH);
257
- return fit(left + " " + (details[index] ?? ""));
275
+ const right = details[index] ?? "";
276
+ // Every art row is ART_WIDTH cells; the cached card rows hit Pi's width cache.
277
+ return ART_WIDTH + 2 + visibleWidth(right) <= width ? left + " " + right : truncateToWidth(left + " " + right, width);
258
278
  });
259
279
  return [...merged, "", ""];
260
280
  }
package/src/working.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { icon } from "./icons.ts";
1
2
  import { cleanText } from "./status.ts";
2
3
 
3
4
  export type WorkingPhase = "idle" | "generating" | "tool" | "waiting";
@@ -54,7 +55,8 @@ export class WorkingState {
54
55
  const symbols = phase === "tool" ? ["◐", "◓", "◑", "◒"] : phase === "generating" ? ["✢", "✣", "✤", "✣"] : ["✢"];
55
56
  const frames = animations ? symbols.map((symbol) => fg(color, symbol)) : [fg(color, symbols[0] ?? "✢")];
56
57
  const details = phase === "idle" || this.startedAt === undefined ? "" :
57
- ` (${duration(elapsed)}${this.outputTokens ? ` · ↓ ${tokenCount(this.outputTokens)} tokens` : ""}${effort && effort !== "off" ? ` · ${effort} effort` : ""})`;
58
+ ` (${duration(elapsed)}${this.outputTokens ? ` · ↓ ${tokenCount(this.outputTokens)} tokens` : ""}${effort && effort !== "off" ? ` · ${effort} effort` : ""}`
59
+ + `${phase === "waiting" ? "" : ` · ${icon("esc") === "esc" ? "esc" : icon("esc") + " esc"} to interrupt`})`;
58
60
  return { message: word ? fg(color, word) + fg("muted", details) : undefined, frames, color };
59
61
  }
60
62
  }