pi-tropos 0.7.0 → 0.9.0

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/README.md CHANGED
@@ -24,6 +24,10 @@ Personal pi customization bundle. One package, installable anywhere.
24
24
  - **Clean footer** — one line: `project branch` left; `↑in ↓out Rcache Wcache $cost` plus
25
25
  the dumbzone gauge right. Zero-valued counters hide; no full paths, no cache-hit %,
26
26
  no session titles. Auto-named sessions are sanitized (no markdown table junk).
27
+ - **`/zen` mode** — reading mode for brainstorming: collapses all tool output and shrinks
28
+ the footer to `$cost` + the ctx gauge. `/zen` again restores your previous state.
29
+ - **`/peek <path>`** — transient code viewer: syntax-highlighted, line-numbered overlay.
30
+ Scroll with ↑/↓/jk/PgUp/PgDn/g/G, Esc or q closes. Leaves no trace in the chat.
27
31
  - **Barista mascot** — the working spinner becomes a steaming cup of kaapi; every other
28
32
  brew cycle it's a boba 🧋. Hidden thinking blocks read "Steeping…".
29
33
  - **pi-smritikosh** — semantic codebase search, bundled as a dependency. One install
@@ -45,3 +49,20 @@ npm run typecheck
45
49
  ```
46
50
 
47
51
  No build step — pi loads the TypeScript directly.
52
+
53
+ ## Font (recommended)
54
+
55
+ pi inherits the terminal's font — it can't set one. In VS Code (where pi usually runs):
56
+
57
+ ```jsonc
58
+ // settings.json
59
+ "terminal.integrated.fontFamily": "JetBrains Mono",
60
+ "terminal.integrated.fontSize": 14,
61
+ "terminal.integrated.lineHeight": 1.35,
62
+ "terminal.integrated.fontLigatures.enabled": true
63
+ ```
64
+
65
+ Good code-reading fonts: **JetBrains Mono** (free, tall x-height, superb at small sizes —
66
+ `brew install --cask font-jetbrains-mono`), **SF Mono** (preinstalled on macOS),
67
+ **Monaspace Neon** (GitHub's family — `brew install --cask font-monaspace`), **Berkeley
68
+ Mono** (paid, beautiful). Size 13–14 + lineHeight 1.3–1.4 is the biggest readability win.
package/index.ts CHANGED
@@ -3,12 +3,14 @@ import smritikosh from "pi-smritikosh/index.js";
3
3
  import barista from "./src/barista.js";
4
4
  import dumbzone from "./src/dumbzone.js";
5
5
  import footer from "./src/footer.js";
6
+ import peek from "./src/peek.js";
6
7
  import sessions from "./src/sessions.js";
7
8
 
8
9
  export default function (pi: ExtensionAPI) {
9
10
  barista(pi);
10
11
  dumbzone(pi);
11
12
  footer(pi);
13
+ peek(pi);
12
14
  sessions(pi);
13
15
  smritikosh(pi);
14
16
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-tropos",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Personal pi customization bundle — context-health indicator, session handoff, session browser",
5
5
  "type": "module",
6
6
  "keywords": [
package/src/footer.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  /**
2
- * Footer — clean one-line status bar.
2
+ * Footer — clean one-line status bar + /zen mode.
3
3
  *
4
- * Left: project dir + git branch (dim)
5
- * Right: token & cost telemetry (dim, zero-values hidden) + extension
6
- * statuses (e.g. the dumbzone ctx gauge, colored by health)
4
+ * Normal: `project branch` left; `↑in ↓out Rcache Wcache $cost` + extension
5
+ * statuses (e.g. the dumbzone ctx gauge) right. Zero-valued counters hide.
7
6
  *
8
- * Same data as the default footer, minus the noise: no full path, no
9
- * cache-hit %, no window size, no session title.
7
+ * /zen: collapses all tool output and shrinks the footer to just
8
+ * `$cost` + the ctx gauge. Reading mode for brainstorming — the chat
9
+ * becomes the content. /zen again restores your previous state.
10
+ * (Zen lives here because the footer owns the chrome it minimizes.)
10
11
  */
11
12
 
12
13
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
@@ -20,38 +21,69 @@ interface Usage {
20
21
  cost: { total: number };
21
22
  }
22
23
 
24
+ interface Totals {
25
+ input: number;
26
+ output: number;
27
+ cacheRead: number;
28
+ cacheWrite: number;
29
+ cost: number;
30
+ }
31
+
23
32
  function fmt(n: number) {
24
33
  if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
25
34
  if (n >= 1_000) return `${Math.round(n / 1_000)}k`;
26
35
  return `${n}`;
27
36
  }
28
37
 
29
- function telemetry(ctx: ExtensionContext) {
30
- let input = 0,
31
- output = 0,
32
- cacheRead = 0,
33
- cacheWrite = 0,
34
- cost = 0;
38
+ function totals(ctx: ExtensionContext): Totals {
39
+ const t: Totals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
35
40
  for (const e of ctx.sessionManager.getBranch()) {
36
41
  if (e.type !== "message" || e.message.role !== "assistant") continue;
37
42
  const u = (e.message as { usage?: Usage }).usage;
38
43
  if (!u) continue;
39
- input += u.input;
40
- output += u.output;
41
- cacheRead += u.cacheRead ?? 0;
42
- cacheWrite += u.cacheWrite ?? 0;
43
- cost += u.cost?.total ?? 0;
44
+ t.input += u.input;
45
+ t.output += u.output;
46
+ t.cacheRead += u.cacheRead ?? 0;
47
+ t.cacheWrite += u.cacheWrite ?? 0;
48
+ t.cost += u.cost?.total ?? 0;
44
49
  }
45
- if (input === 0) return "";
46
- const parts = [`↑${fmt(input)}`, `↓${fmt(output)}`];
47
- if (cacheRead > 0) parts.push(`R${fmt(cacheRead)}`);
48
- if (cacheWrite > 0) parts.push(`W${fmt(cacheWrite)}`);
49
- parts.push(`$${cost.toFixed(2)}`);
50
+ return t;
51
+ }
52
+
53
+ function telemetryLine(t: Totals) {
54
+ if (t.input === 0) return "";
55
+ const parts = [`↑${fmt(t.input)}`, `↓${fmt(t.output)}`];
56
+ if (t.cacheRead > 0) parts.push(`R${fmt(t.cacheRead)}`);
57
+ if (t.cacheWrite > 0) parts.push(`W${fmt(t.cacheWrite)}`);
58
+ parts.push(`$${t.cost.toFixed(2)}`);
50
59
  return parts.join(" ");
51
60
  }
52
61
 
62
+ let zen = false;
63
+ let wasExpanded = true;
64
+
53
65
  export default function footer(pi: ExtensionAPI) {
66
+ pi.registerCommand("zen", {
67
+ description: "Toggle zen mode — collapse tool noise, minimal footer",
68
+ handler: async (_args, ctx) => {
69
+ if (ctx.mode !== "tui") {
70
+ ctx.ui.notify("/zen needs interactive mode", "error");
71
+ return;
72
+ }
73
+ zen = !zen;
74
+ if (zen) {
75
+ wasExpanded = ctx.ui.getToolsExpanded();
76
+ ctx.ui.setToolsExpanded(false);
77
+ ctx.ui.notify("Zen ☁️ — Ctrl+T hides thinking · /zen to exit", "info");
78
+ } else {
79
+ ctx.ui.setToolsExpanded(wasExpanded);
80
+ ctx.ui.notify("Zen off — full telemetry restored", "info");
81
+ }
82
+ },
83
+ });
84
+
54
85
  pi.on("session_start", (_event, ctx) => {
86
+ zen = false;
55
87
  if (!ctx.hasUI) return;
56
88
  ctx.ui.setFooter((tui, theme, footerData) => {
57
89
  const unsub = footerData.onBranchChange(() => tui.requestRender());
@@ -59,13 +91,18 @@ export default function footer(pi: ExtensionAPI) {
59
91
  dispose: unsub,
60
92
  invalidate() {},
61
93
  render(width: number): string[] {
94
+ const t = totals(ctx);
95
+ const statuses = [...footerData.getExtensionStatuses().values()].join(" ");
96
+
97
+ if (zen) {
98
+ const right = [t.cost > 0 ? `$${t.cost.toFixed(2)}` : "", statuses].filter(Boolean).join(" ");
99
+ return [truncateToWidth(theme.fg("dim", right), width)];
100
+ }
101
+
62
102
  const dir = ctx.cwd.split("/").pop() ?? ctx.cwd;
63
103
  const branch = footerData.getGitBranch();
64
104
  const left = theme.fg("dim", branch ? `${dir} ${branch}` : dir);
65
-
66
- const stats = telemetry(ctx);
67
- const metrics = stats ? theme.fg("dim", stats) : "";
68
- const statuses = [...footerData.getExtensionStatuses().values()].join(" ");
105
+ const metrics = theme.fg("dim", telemetryLine(t));
69
106
  const right = [metrics, statuses].filter(Boolean).join(" ");
70
107
 
71
108
  const pad = " ".repeat(Math.max(1, width - visibleWidth(left) - visibleWidth(right)));
package/src/peek.ts ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Peek — transient code viewer overlay. Leaves no trace in the chat.
3
+ *
4
+ * /peek src/footer.ts
5
+ *
6
+ * Opens a centered overlay with the file syntax-highlighted (kaapi
7
+ * theme colors) and line-numbered. Scroll with ↑/↓, PgUp/PgDn, g/G;
8
+ * Esc or q closes. Nothing is appended to the transcript.
9
+ */
10
+
11
+ import { readFileSync, statSync } from "node:fs";
12
+ import { isAbsolute, join } from "node:path";
13
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
14
+ import { getLanguageFromPath, highlightCode } from "@earendil-works/pi-coding-agent";
15
+ import { ScrollView, truncateToWidth } from "@earendil-works/pi-tui";
16
+
17
+ const MAX_BYTES = 200_000;
18
+
19
+ export default function peek(pi: ExtensionAPI) {
20
+ pi.registerCommand("peek", {
21
+ description: "Peek at a file in a transient overlay — /peek <path>",
22
+ handler: async (args, ctx) => {
23
+ if (ctx.mode !== "tui") {
24
+ ctx.ui.notify("/peek needs interactive mode", "error");
25
+ return;
26
+ }
27
+ const target = args.trim();
28
+ if (!target) {
29
+ ctx.ui.notify("Usage: /peek <path>", "info");
30
+ return;
31
+ }
32
+ const path = isAbsolute(target) ? target : join(ctx.cwd, target);
33
+
34
+ let content: string;
35
+ try {
36
+ const stat = statSync(path);
37
+ if (!stat.isFile()) throw new Error("not a file");
38
+ if (stat.size > MAX_BYTES) throw new Error("file too large");
39
+ content = readFileSync(path, "utf-8");
40
+ } catch (err) {
41
+ ctx.ui.notify(`Cannot peek: ${err instanceof Error ? err.message : err}`, "error");
42
+ return;
43
+ }
44
+
45
+ const lang = getLanguageFromPath(path);
46
+ const highlighted = highlightCode(content, lang);
47
+
48
+ await ctx.ui.custom(
49
+ (tui, theme, _kb, done) => {
50
+ const gutter = String(highlighted.length).length;
51
+ const numbered = highlighted.map(
52
+ (line, i) => theme.fg("dim", `${String(i + 1).padStart(gutter)} │ `) + line,
53
+ );
54
+ const inner = {
55
+ invalidate() {},
56
+ render: (width: number) => numbered.map((l) => truncateToWidth(l, width - 2)),
57
+ };
58
+ const scroll = new ScrollView(inner, { follow: "none", overscroll: "contain" });
59
+
60
+ return {
61
+ invalidate() {},
62
+ render: (width: number) => scroll.render(width),
63
+ handleInput(data: string) {
64
+ if (data === "\x1b" || data === "q") return done(undefined);
65
+ if (data === "\x1b[A" || data === "k") scroll.scrollBy(-1);
66
+ else if (data === "\x1b[B" || data === "j") scroll.scrollBy(1);
67
+ else if (data === "\x1b[5~") scroll.scrollBy(-10);
68
+ else if (data === "\x1b[6~") scroll.scrollBy(10);
69
+ else if (data === "g") scroll.scrollToStart();
70
+ else if (data === "G") scroll.scrollToEnd();
71
+ tui.requestRender();
72
+ },
73
+ };
74
+ },
75
+ {
76
+ overlay: true,
77
+ overlayOptions: { width: "85%", maxHeight: "80%", anchor: "center" },
78
+ },
79
+ );
80
+ },
81
+ });
82
+ }