@adeildo/pi-look 5.1.0 → 5.3.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
@@ -1,64 +1,74 @@
1
- # @adeildo/pi-look
1
+ <h1 align="center">@adeildo/pi-look</h1>
2
2
 
3
- [![npm](https://img.shields.io/npm/v/@adeildo/pi-look)](https://www.npmjs.com/package/@adeildo/pi-look)
3
+ <p align="center">
4
+ <a href="https://www.npmjs.com/package/@adeildo/pi-look"><img src="https://img.shields.io/npm/v/@adeildo/pi-look" alt="npm"></a>
5
+ <a href="https://pi.dev"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Ffelipeadeildo%2Fpi-harness%2Fmain%2Fpackage.json&query=%24.devDependencies%5B%22%40earendil-works%2Fpi-coding-agent%22%5D&label=pi%20SDK&color=6E56CF" alt="pi SDK"></a>
6
+ </p>
4
7
 
5
- [Pi](https://pi.dev)'s start screen, editor, footer and working line, redrawn around the numbers that matter while the agent works. It paints only with the colours of the theme you run, and the screen holds still while the numbers change.
8
+ <p align="center">
9
+ <strong>Pi's screen, drawn around the numbers that matter while the agent works.</strong><br>
10
+ The start card, the editor framed with the branch and the model, a box around every command, and a footer with what the session costs.
11
+ </p>
6
12
 
7
- ```bash
8
- pi install npm:@adeildo/pi-look
9
- ```
13
+ <p align="center"><code>pi install npm:@adeildo/pi-look</code></p>
10
14
 
11
- It also comes in [`@adeildo/pi-harness`](../harness), with the rest of the pieces.
15
+ <!-- docs:look/preview -->
12
16
 
13
17
  <p align="center">
14
- <img src="https://raw.githubusercontent.com/felipeadeildo/pi-harness/main/packages/look/assets/preview.png" alt="The start card with the model, the folder, the branch and the keys, and below it the framed editor with the branch and folder in its top border and the model and context in its bottom border." width="860">
18
+ <img src="https://raw.githubusercontent.com/felipeadeildo/pi-harness/main/packages/look/assets/preview.png" alt="Pi with the look: the start card, then the strip with the stopwatch and the last call, the framed editor with the branch, the model and the context, and below it the cost, the tokens, the cache and the average speeds." width="860">
19
+ <br>
20
+ <em>The start card, then a prompt halfway through its answer.</em>
15
21
  </p>
16
22
 
17
- ## What is on the screen
23
+ <!-- /docs -->
18
24
 
19
- **The start card** says what the session starts with: the model and its effort, the folder and the state of the tree, the machine, how many tools, skills, prompts and extensions loaded, and the keys worth knowing, read from your keybindings.
25
+ ## The screen
20
26
 
21
- **The strip above the editor** is calm on purpose. Nothing in it moves while the model streams except the stopwatch.
27
+ A finished answer takes a box of its own, so you can see what the model wants to run before it runs. The mark of the tool and its name sit on the top line, the command under them, and the reason it was allowed under that, when another model answered it. A line cuts the command off from what it printed, and the bottom line says how it went: a check, a cross, and how long it took. A command still running says `running` with the time so far. A tool from an MCP server shows its server and tool instead of `mcp__server__tool`. A tool with no drawing of its own, like the memory calls, gets the same box with its name and arguments on top and its text under the cut.
22
28
 
23
- | Piece | Means |
24
- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
25
- | `0:12 3 calls` | How long the agent has worked on this prompt, and how many calls to the model it made |
26
- | `last call 1.7s wait 900ms thought ↓906 tok/s` | The last call that finished: how long the model took to start, how long it thought before writing, and how fast it wrote. Final numbers |
29
+ <!-- docs:look/call -->
27
30
 
28
- **The frame** around the editor carries the branch with its distance and changes, the folder, the permission mode, the model with an effort meter, and the context. The border takes pi's own effort colour, so it warms up as the model thinks harder and turns to the bash colour in shell mode.
31
+ <p align="center">
32
+ <img src="https://raw.githubusercontent.com/felipeadeildo/pi-harness/main/packages/look/assets/call.png" alt="Three commands, each in its own box: a bash command with the reason it was allowed and the time it took, a file read, and a command still running." width="860">
33
+ <br>
34
+ <em>One command that ran, one a rule let through, and one still going.</em>
35
+ </p>
29
36
 
30
- **The spinner** names the state instead of saying "Working": `waiting` for the first token, `thinking`, `writing`, `drafting bash` while the model writes the call, `running bash` while it runs.
37
+ <!-- /docs -->
31
38
 
32
- **The footer** is the session: the cost, tokens sent and received, the cache hit of the last prompt, the average speeds, and last what other packages report, because it changes the most.
39
+ The strip above the editor is calm on purpose. Nothing moves while the model streams except the stopwatch.
33
40
 
34
- ## It holds still
41
+ | Piece | Means |
42
+ | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
43
+ | `0:12 3 calls` | How long the agent has worked on this prompt, and how many calls to the model it made |
44
+ | `last call 1.7s wait 900ms thought ↓906 tok/s` | The last call: how long the model took to start, how long it thought then wrote, and how fast it writes |
35
45
 
36
- Numbers change on every frame and the screen must not move with them.
46
+ The border around the editor carries the branch with its distance and changes, the folder, the permission mode, the model with an effort meter, and the context. It takes pi's own effort colour, so it warms up as the model thinks harder and turns to the shell colour in bash mode.
37
47
 
38
- - Every number sits in a cell of fixed width, so ` 17` and `2.8k` take the same room.
39
- - A value not known yet shows as `–` in its cell and does not appear later in the middle of the line.
40
- - The working time is a stopwatch, `0:59` then `1:00`, instead of `59s` then `1m 0s`.
41
- - An estimate is drawn quieter and does not gain a `~`.
42
- - The strip and the footer keep their line from the start, so the editor does not jump when the first answer arrives.
48
+ The spinner names the state instead of saying "Working": `waiting` for the first token, `thinking`, `writing`, `drafting bash` while the model writes the call, `running bash` while it runs.
43
49
 
44
- Every finished answer leaves a small entry in the session with its time to first token and writing time, because pi records usage without a duration. That keeps the averages right after a resume.
50
+ The footer is the session: the cost, the tokens sent and received, the cache of the last prompt, the average speeds, and last what other packages report, because that changes the most.
45
51
 
46
- ## Your theme, not ours
52
+ ## It holds still
47
53
 
48
- The look never names a colour. Every piece names a **theme token**, so it follows whatever theme pi runs: `system`, `dark`, `light`, a JSON of yours, or the desktop theme below. Change the theme and every piece moves with it. The [reference](docs/reference.md#colours) maps each piece to its token.
54
+ Numbers change on every frame, and the screen must not move with them.
49
55
 
50
- ### The desktop theme
56
+ - Every number sits in a cell of fixed width, so ` 17` and `2.8k` take the same room.
57
+ - A value nobody knows yet shows as `–` and does not appear later in the middle of the line.
58
+ - The working time counts as a stopwatch, `0:59` then `1:00`.
59
+ - An estimate is drawn quieter, so you know it is one.
60
+ - The strip and the footer hold their line from the start, so the editor does not jump when the first answer arrives.
51
61
 
52
- If you run [DankMaterialShell](https://github.com/AvengeMedia/DankMaterialShell), matugen already made a palette from your wallpaper. The look turns it into the pi theme `desktop`, writes it to `~/.pi/agent/themes/desktop.json` and rewrites it when the palette changes. Pi reloads the active theme from that folder by itself, so a new wallpaper recolours a running pi. `Switch to it now`, in the Desktop theme section of the settings (`Alt+S`), writes the theme and makes it pi's.
62
+ Pi records usage without a duration, so every finished answer leaves one small entry in the session with its time to the first token and its writing time. That is what keeps the averages right after a resume.
53
63
 
54
- ## Settings
64
+ ## Your theme
55
65
 
56
- `Alt+S` opens them, on the Look tab. You can reorder and move every piece of the strip, the frame and the footer, pick the frame style, the icons and the separator, and switch off what you do not want. The [reference](docs/reference.md#settings) lists every key and every segment.
66
+ The look never names a colour. Every piece names a theme token, so it follows whatever theme pi runs, and it moves with it when you change themes. The [reference](docs/reference.md#colours) maps each piece to its token.
57
67
 
58
- ## Credit
68
+ ## Settings
59
69
 
60
- The framed editor and the idea of writing into its borders come from [pi-open-tui](https://github.com/OldSuns/pi-open-tui), which this replaces. The segments with priorities, and the status line in the editor border, come from [oh-my-pi](https://github.com/can1357/oh-my-pi).
70
+ `Alt+S` opens them on the Look tab. You can reorder every piece of the strip, the frame and the footer, pick the frame style, the icons and the separator, and switch off what you do not want. The [reference](docs/reference.md#settings) lists every key and every segment.
61
71
 
62
- ## License
72
+ The framed editor, and the idea of writing into its borders, come from [pi-open-tui](https://github.com/OldSuns/pi-open-tui), which this replaces. The segments with priorities come from [oh-my-pi](https://github.com/can1357/oh-my-pi).
63
73
 
64
- [MIT](LICENSE)
74
+ Part of [pi-harness](https://github.com/felipeadeildo/pi-harness), which brings every piece in one install.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adeildo/pi-look",
3
- "version": "5.1.0",
3
+ "version": "5.3.0",
4
4
  "description": "Pi's start screen, a framed editor and the footer, redrawn around the data that matters and painted only with the theme's colours.",
5
5
  "keywords": [
6
6
  "pi",
@@ -38,7 +38,7 @@
38
38
  "access": "public"
39
39
  },
40
40
  "dependencies": {
41
- "@adeildo/pi-kit": "5.1.0"
41
+ "@adeildo/pi-kit": "5.3.0"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "@earendil-works/pi-ai": "*",
@@ -1,5 +1,6 @@
1
1
  import { hostname } from "node:os";
2
2
 
3
+ import type { AccountState } from "@adeildo/pi-kit";
3
4
  import { VERSION, type ExtensionContext, type SessionEntry } from "@earendil-works/pi-coding-agent";
4
5
 
5
6
  import type { GitState } from "./git.ts";
@@ -36,6 +37,7 @@ export interface Snapshot {
36
37
  sessionName: string | undefined;
37
38
  version: string;
38
39
  model: ModelInfo | undefined;
40
+ account: AccountState | undefined;
39
41
  thinking: string | undefined;
40
42
  context: ContextInfo | undefined;
41
43
  totals: Totals;
@@ -60,6 +62,7 @@ export function emptySnapshot(): Snapshot {
60
62
  sessionName: undefined,
61
63
  version: VERSION,
62
64
  model: undefined,
65
+ account: undefined,
63
66
  thinking: undefined,
64
67
  context: undefined,
65
68
  totals: emptyTotals(),
@@ -77,6 +80,7 @@ export function emptySnapshot(): Snapshot {
77
80
  export interface LiveInputs {
78
81
  branch: string | null;
79
82
  git: GitState | undefined;
83
+ account?: AccountState | undefined;
80
84
  request: RequestView | undefined;
81
85
  last: RequestView | undefined;
82
86
  run: RunView | undefined;
@@ -117,6 +121,7 @@ export class SnapshotReader {
117
121
  reasoning: model.reasoning,
118
122
  },
119
123
  thinking: ctx.thinkingLevel,
124
+ account: live.account,
120
125
  context: contextOf(ctx),
121
126
  ...derived,
122
127
  subscription: model === undefined ? false : ctx.modelRegistry.isUsingOAuth(model),
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { createApp, defineFeature, type FeatureScope } from "@adeildo/pi-kit";
1
+ import { accountStateOf, createApp, defineFeature, type FeatureScope } from "@adeildo/pi-kit";
2
2
  import type {
3
3
  ExtensionAPI,
4
4
  ExtensionContext,
@@ -11,18 +11,15 @@ import { emptySnapshot, type Snapshot, SnapshotReader } from "./data/snapshot.ts
11
11
  import { Telemetry } from "./data/telemetry.ts";
12
12
  import type { DeltaKind } from "./data/telemetry.ts";
13
13
  import { REQUEST_ENTRY, type RequestRecord } from "./data/totals.ts";
14
- import { DESKTOP_THEME } from "./desktop/palette.ts";
15
- import { syncDesktopTheme, watchDesktopPalette } from "./desktop/sync.ts";
16
14
  import { resolveIcons } from "./render/glyphs.ts";
17
15
  import { PLAIN, themePaint } from "./render/paint.ts";
18
16
  import { claimedStatuses, renderSegments, SEGMENTS } from "./render/segments.ts";
19
17
  import {
20
18
  below,
19
+ belowRight,
21
20
  bottomLeft,
22
21
  bottomRight,
23
22
  cursor,
24
- desktop,
25
- desktopSource,
26
23
  frame,
27
24
  gaugeCells,
28
25
  header,
@@ -42,11 +39,11 @@ import { applyCursor, LookEditor } from "./ui/editor.ts";
42
39
  import { FooterComponent, StripComponent } from "./ui/footer.ts";
43
40
  import { HeaderComponent, type LoadedCounts } from "./ui/header.ts";
44
41
  import type { Screen, SlotName } from "./ui/screen.ts";
42
+ import { registerToolFrames } from "./ui/tool-frame.ts";
45
43
  import { type Activity, describe, draftedTool } from "./ui/working.ts";
46
44
 
47
45
  export { SEGMENT_IDS, SEGMENTS, type SegmentId } from "./render/segments.ts";
48
46
  export { ROLE_TOKENS } from "./render/paint.ts";
49
- export { desktopTheme, parseDesktopColors } from "./desktop/palette.ts";
50
47
 
51
48
  const STRIP_WIDGET = "pi-look:strip";
52
49
  const TICK_MS = 500;
@@ -59,7 +56,6 @@ interface Live {
59
56
  git: GitState | undefined;
60
57
  probing: boolean;
61
58
  ticker: ReturnType<typeof setInterval> | undefined;
62
- stopWatching: (() => void) | undefined;
63
59
  frame: Snapshot | undefined;
64
60
  activity: string | undefined;
65
61
  }
@@ -81,12 +77,12 @@ export const look = defineFeature({
81
77
  git: undefined,
82
78
  probing: false,
83
79
  ticker: undefined,
84
- stopWatching: undefined,
85
80
  frame: undefined,
86
81
  activity: undefined,
87
82
  };
88
83
 
89
84
  const screen = createScreen(scope, live, reader, telemetry);
85
+ registerToolFrames(scope);
90
86
  const repaint = () => live.repaint?.();
91
87
 
92
88
  for (const entry of LOOK_SETTINGS) entry.listen(scope, repaint);
@@ -95,7 +91,7 @@ export const look = defineFeature({
95
91
  cursor.listen(scope, (style) => live.tui && applyCursor(live.tui, style));
96
92
  peek.listen(scope, (on) => !on && live.ctx?.ui.setWorkingMessage());
97
93
 
98
- scope.onSessionStart(async (ctx) => {
94
+ scope.onSessionStart((ctx) => {
99
95
  live.ctx = ctx;
100
96
  telemetry.reset();
101
97
  void probeGit(live);
@@ -119,13 +115,10 @@ export const look = defineFeature({
119
115
  });
120
116
  installEditor(ctx, screen, live, scope);
121
117
  installHeader(ctx, screen, scope);
122
- if (desktop.get(scope)) await startDesktopSync(ctx, scope, live);
123
118
  });
124
119
 
125
120
  scope.onShutdown(() => {
126
121
  stopTicking(live);
127
- live.stopWatching?.();
128
- live.stopWatching = undefined;
129
122
  const ctx = live.ctx;
130
123
  if (ctx?.mode === "tui") {
131
124
  ctx.ui.setFooter(undefined);
@@ -212,28 +205,9 @@ export const look = defineFeature({
212
205
  description: "Every piece the slots show right now, and what it means.",
213
206
  run: () => explain(screen),
214
207
  });
215
-
216
- scope.screen.action({
217
- id: "desktop.apply",
218
- section: "Desktop theme",
219
- label: "Switch to it now",
220
- description: "Builds the desktop theme from the palette file and makes it pi's theme.",
221
- run: (ctx) => switchToDesktopTheme(ctx, scope),
222
- });
223
208
  },
224
209
  });
225
210
 
226
- async function switchToDesktopTheme(ctx: ExtensionContext, scope: FeatureScope): Promise<string> {
227
- const source = desktopSource.get(scope);
228
- const result = await syncDesktopTheme(source);
229
- if (result.kind === "missing") throw new Error(`no palette at ${source}`);
230
- if (result.kind === "invalid") throw new Error(`cannot build the theme: ${result.reason}`);
231
- const switched = ctx.ui.setTheme(DESKTOP_THEME);
232
- if (!switched.success)
233
- throw new Error(`wrote ${result.path}, but pi did not load it: ${switched.error ?? "unknown"}`);
234
- return `Now on the ${DESKTOP_THEME} theme, built from ${source}.`;
235
- }
236
-
237
211
  const SLOT_TITLES: Record<SlotName, string> = {
238
212
  strip: "Above the editor, this answer",
239
213
  topLeft: "Frame, top left",
@@ -241,6 +215,7 @@ const SLOT_TITLES: Record<SlotName, string> = {
241
215
  bottomLeft: "Frame, bottom left",
242
216
  bottomRight: "Frame, bottom right",
243
217
  below: "Below the editor, the session",
218
+ belowRight: "Below the editor, on the right",
244
219
  };
245
220
 
246
221
  function explain(screen: Screen): string {
@@ -288,6 +263,7 @@ function createScreen(
288
263
  bottomLeft,
289
264
  bottomRight,
290
265
  below,
266
+ belowRight,
291
267
  };
292
268
 
293
269
  return {
@@ -298,6 +274,7 @@ function createScreen(
298
274
  const snapshot = reader.read(ctx, {
299
275
  branch: live.footerData?.getGitBranch() ?? null,
300
276
  git: live.git,
277
+ account: accountStateOf(scope.events, ctx.model?.provider),
301
278
  request: telemetry.request(),
302
279
  last: telemetry.last(),
303
280
  run: telemetry.run(),
@@ -375,23 +352,6 @@ function loadedCounts(pi: ExtensionAPI): LoadedCounts {
375
352
  };
376
353
  }
377
354
 
378
- async function startDesktopSync(ctx: ExtensionContext, scope: FeatureScope, live: Live) {
379
- const source = desktopSource.get(scope);
380
- const sync = async () => {
381
- const result = await syncDesktopTheme(source);
382
- if (result.kind === "invalid") scope.warn(`desktop theme: ${result.reason}`);
383
- return result;
384
- };
385
-
386
- const first = await sync();
387
- if (first.kind === "missing") return;
388
- if (first.kind === "written" && ctx.ui.theme.name !== DESKTOP_THEME) {
389
- ctx.ui.notify(`look: wrote the ${DESKTOP_THEME} theme. /look theme switches to it.`, "info");
390
- }
391
- live.stopWatching?.();
392
- live.stopWatching = watchDesktopPalette(source, () => void sync());
393
- }
394
-
395
355
  function setActivity(live: Live, scope: FeatureScope, activity: Activity): void {
396
356
  if (!peek.get(scope)) return;
397
357
  const message = describe(activity);
@@ -56,6 +56,7 @@ export function latency(milliseconds: number): string {
56
56
  }
57
57
 
58
58
  export function money(amount: number): string {
59
+ if (amount === 0) return "$0.00";
59
60
  return amount >= 10 ? `$${amount.toFixed(2)}` : `$${amount.toFixed(3)}`;
60
61
  }
61
62
 
@@ -1,3 +1,5 @@
1
+ import type { AccountWindow } from "@adeildo/pi-kit";
2
+
1
3
  import type { Snapshot } from "../data/snapshot.ts";
2
4
  import { promptOf, tokensOf, type Totals } from "../data/totals.ts";
3
5
  import type { Piece } from "./fit.ts";
@@ -38,6 +40,7 @@ export const SEGMENT_IDS = [
38
40
  "request",
39
41
  "costRate",
40
42
  "cost",
43
+ "quota",
41
44
  "tokens",
42
45
  "cache",
43
46
  "average",
@@ -58,11 +61,19 @@ export function isSegmentId(value: string): value is SegmentId {
58
61
  );
59
62
  }
60
63
 
64
+ /** Status keys a builtin segment already draws, so the statuses segment skips them. */
65
+ const CLAIMED_BY_SEGMENT: Partial<Record<BuiltinSegment, string>> = {
66
+ model: "pi-providers:account",
67
+ };
68
+
61
69
  export function claimedStatuses(slots: readonly (readonly SegmentId[])[]): Set<string> {
62
70
  const claimed = new Set<string>();
63
71
  for (const ids of slots) {
64
- for (const id of ids)
72
+ for (const id of ids) {
65
73
  if (id.startsWith(STATUS_PREFIX)) claimed.add(id.slice(STATUS_PREFIX.length));
74
+ const bySegment = CLAIMED_BY_SEGMENT[id as BuiltinSegment];
75
+ if (bySegment !== undefined) claimed.add(bySegment);
76
+ }
66
77
  }
67
78
  return claimed;
68
79
  }
@@ -180,13 +191,32 @@ export const SEGMENTS: Record<BuiltinSegment, Segment> = {
180
191
  },
181
192
  model: {
182
193
  priority: 100,
183
- describe: "the model doing the work, after its provider",
194
+ describe: "the model doing the work, after its provider and the account it runs on",
184
195
  render: ({ snapshot, glyphs, paint }) => {
185
196
  const icon = mark(glyphs.model, paint, "provider");
186
197
  if (snapshot.model === undefined) return { text: `${icon}${paint.dim("no model")}` };
187
198
  const name = paint.bold(paint.role("model", snapshot.model.name));
188
- const provider = `${paint.role("provider", snapshot.model.provider)}${paint.dim("/")}`;
189
- return { text: `${icon}${provider}${name}`, compact: `${icon}${name}` };
199
+ const account =
200
+ snapshot.account === undefined
201
+ ? ""
202
+ : `${paint.dim("(")}${paint.role("provider", snapshot.account.label)}${paint.dim(")")}`;
203
+ const provider = `${paint.role("provider", snapshot.model.provider)}${account}${paint.dim("/")}`;
204
+ return {
205
+ text: `${icon}${provider}${name}`,
206
+ compact: account === "" ? `${icon}${name}` : `${icon}${name} ${account}`,
207
+ };
208
+ },
209
+ },
210
+ quota: {
211
+ priority: 58,
212
+ describe: "what each window of the plan spent, and when it resets",
213
+ render: ({ snapshot, paint }) => {
214
+ const account = snapshot.account;
215
+ if (account === undefined) return undefined;
216
+ if (account.needsLogin === true) return { text: paint.stress(100, "sign in again") };
217
+ if (account.windows.length === 0) return undefined;
218
+ const text = account.windows.map((window) => quotaWindow(paint, window)).join(" ");
219
+ return { text };
190
220
  },
191
221
  },
192
222
  effort: {
@@ -350,6 +380,7 @@ export const SEGMENTS: Record<BuiltinSegment, Segment> = {
350
380
  "what the session cost; `sub` means a subscription, so it is what the tokens would have cost",
351
381
  render: ({ snapshot, glyphs, paint }) => {
352
382
  const amount = snapshot.totals.cost;
383
+ // A session that spent nothing has nothing to say yet.
353
384
  if (amount === 0) return undefined;
354
385
  const value = paint.money(amount, money(amount));
355
386
  const sub = snapshot.subscription ? paint.dim(" sub") : "";
@@ -445,6 +476,14 @@ export function renderSegments(ids: readonly SegmentId[], input: SegmentInput):
445
476
  });
446
477
  }
447
478
 
479
+ /** One plan window, like `5h 62% resets in 2h`. The percent carries the color. */
480
+ function quotaWindow(paint: Paint, window: AccountWindow): string {
481
+ const used = paint.stress(window.used, `${window.used}%`);
482
+ const reset =
483
+ window.resetsIn === undefined ? "" : ` ${paint.muted(`resets in ${window.resetsIn}`)}`;
484
+ return `${paint.dim(window.name)} ${used}${reset}`;
485
+ }
486
+
448
487
  function mark(glyph: string, paint: Paint, role: Role): string {
449
488
  return glyph === "" ? "" : `${paint.role(role, glyph)} `;
450
489
  }
@@ -0,0 +1,105 @@
1
+ // The mark and the name a tool call is drawn with. The mark follows the icon set in the look's
2
+ // settings, like every other glyph on the screen.
3
+ import type { IconSet } from "./glyphs.ts";
4
+
5
+ type ToolKind = "shell" | "read" | "search" | "edit" | "web" | "agent" | "mcp" | "other";
6
+
7
+ const ICONS: Record<IconSet, Record<ToolKind, string>> = {
8
+ nerd: {
9
+ shell: "\u{f120}", // nf-fa-terminal
10
+ read: "\u{f15c}", // nf-fa-file_lines
11
+ search: "\u{f002}", // nf-fa-search
12
+ edit: "\u{f044}", // nf-fa-pencil
13
+ web: "\u{f0ac}", // nf-fa-globe
14
+ agent: "\u{f06a9}", // nf-md-robot
15
+ mcp: "\u{f1e6}", // nf-fa-plug
16
+ other: "\u{f013}", // nf-fa-gear
17
+ },
18
+ unicode: {
19
+ shell: "\u276f",
20
+ read: "\u25a4",
21
+ search: "\u2315",
22
+ edit: "\u270e",
23
+ web: "\u25cd",
24
+ agent: "\u25c6",
25
+ mcp: "\u226b",
26
+ other: "\u25cb",
27
+ },
28
+ ascii: {
29
+ shell: "$",
30
+ read: "=",
31
+ search: "?",
32
+ edit: "+",
33
+ web: "@",
34
+ agent: "*",
35
+ mcp: ">",
36
+ other: "-",
37
+ },
38
+ };
39
+
40
+ export type CallPhase = "writing" | "waiting" | "running" | "done" | "failed";
41
+
42
+ const MARKS: Record<IconSet, Record<CallPhase, string>> = {
43
+ nerd: {
44
+ writing: "\u{f044}", // nf-fa-pencil
45
+ waiting: "\u{f252}", // nf-fa-hourglass_half
46
+ running: "\u{f017}", // nf-fa-clock_o
47
+ done: "\u{f00c}", // nf-fa-check
48
+ failed: "\u{f00d}", // nf-fa-times
49
+ },
50
+ unicode: {
51
+ writing: "\u270e",
52
+ waiting: "\u29d6",
53
+ running: "\u25f7",
54
+ done: "\u2713",
55
+ failed: "\u2717",
56
+ },
57
+ ascii: { writing: "~", waiting: ":", running: "...", done: "ok", failed: "!" },
58
+ };
59
+
60
+ export function stateMark(phase: CallPhase, set: IconSet): string {
61
+ return MARKS[set][phase];
62
+ }
63
+
64
+ export function toolIcon(toolName: string, set: IconSet): string {
65
+ return ICONS[set][toolKind(toolName)];
66
+ }
67
+
68
+ /** The name as it reads on the frame: an MCP tool says its server rather than `mcp__server__tool`. */
69
+ export function toolLabel(toolName: string): string {
70
+ const mcp = toolName.match(/^mcp__([^_]+)__(.+)$/u);
71
+ return mcp === null ? toolName : `${mcp[1]}:${mcp[2]}`;
72
+ }
73
+
74
+ function toolKind(toolName: string): ToolKind {
75
+ if (toolName.startsWith("mcp__")) return "mcp";
76
+ switch (toolName) {
77
+ case "bash":
78
+ case "powershell":
79
+ case "shell":
80
+ return "shell";
81
+ case "read":
82
+ case "ls":
83
+ case "find":
84
+ return "read";
85
+ case "grep":
86
+ case "glob":
87
+ case "tool_search":
88
+ return "search";
89
+ case "edit":
90
+ case "write":
91
+ case "apply_patch":
92
+ return "edit";
93
+ case "web_search":
94
+ case "fetch":
95
+ case "webfetch":
96
+ case "web_fetch":
97
+ return "web";
98
+ case "task":
99
+ case "agent":
100
+ case "subagent":
101
+ return "agent";
102
+ default:
103
+ return "other";
104
+ }
105
+ }
package/src/settings.ts CHANGED
@@ -7,7 +7,6 @@ import {
7
7
  pass,
8
8
  problem,
9
9
  setting,
10
- string,
11
10
  } from "@adeildo/pi-kit";
12
11
 
13
12
  import type { FrameStyle } from "./render/frame.ts";
@@ -15,21 +14,12 @@ import type { IconMode } from "./render/glyphs.ts";
15
14
  import { isSegmentId, SEGMENT_IDS, SEGMENTS, type SegmentId } from "./render/segments.ts";
16
15
 
17
16
  const PERMISSION_STATUS: SegmentId = "status:pi-ask-permission:mode";
18
- const ACCOUNT_STATUS: SegmentId = "status:pi-providers:account";
19
17
 
20
18
  const STATUS_LABELS: Partial<Record<SegmentId, string>> = {
21
19
  [PERMISSION_STATUS]: "permission mode",
22
- [ACCOUNT_STATUS]: "account",
23
20
  };
24
21
 
25
- export const SECTIONS = [
26
- "Editor",
27
- "Slots",
28
- "Footer",
29
- "Start screen",
30
- "Working line",
31
- "Desktop theme",
32
- ];
22
+ export const SECTIONS = ["Editor", "Slots", "Footer", "Start screen", "Working line"];
33
23
 
34
24
  export type HeaderStyle = "card" | "compact" | "off";
35
25
  export type SeparatorStyle = "dot" | "bar" | "slash" | "space";
@@ -65,11 +55,7 @@ function segments(fallback: readonly SegmentId[]): Decoder<SegmentId[]> {
65
55
 
66
56
  // Any other package's status can have a slot of its own, so the ones already placed are offered too.
67
57
  function slotControl(fallback: readonly SegmentId[]): Control {
68
- const statuses = [
69
- PERMISSION_STATUS,
70
- ACCOUNT_STATUS,
71
- ...fallback.filter((id) => id.startsWith("status:")),
72
- ];
58
+ const statuses = [PERMISSION_STATUS, ...fallback.filter((id) => id.startsWith("status:"))];
73
59
  return {
74
60
  type: "list",
75
61
  options: [
@@ -113,7 +99,7 @@ export const bottomLeft = slot(
113
99
  "frame.bottomLeft",
114
100
  "Frame, bottom left",
115
101
  "How what you type is handled, and who does the work.",
116
- [PERMISSION_MODE, "model", ACCOUNT_STATUS, "effort"],
102
+ [PERMISSION_MODE, "model", "effort"],
117
103
  );
118
104
  export const bottomRight = slot("frame.bottomRight", "Frame, bottom right", "How full it is.", [
119
105
  "context",
@@ -124,6 +110,12 @@ export const below = slot(
124
110
  "The session: cost, tokens, cache, average speeds, and what other packages report, last because it changes the most.",
125
111
  ["cost", "tokens", "cache", "average", "statuses"],
126
112
  );
113
+ export const belowRight = slot(
114
+ "belowRight",
115
+ "Below the editor, on the right",
116
+ "The plan's quota: what each window spent, and when it resets.",
117
+ ["quota"],
118
+ );
127
119
 
128
120
  export const frame = setting<FrameStyle | "off">({
129
121
  id: "look.frame.style",
@@ -225,29 +217,6 @@ export const peek = setting({
225
217
  },
226
218
  });
227
219
 
228
- export const desktop = setting({
229
- id: "look.desktop.theme",
230
- default: true,
231
- decoder: boolean,
232
- ui: {
233
- section: "Desktop theme",
234
- label: "Desktop theme",
235
- description:
236
- "Write the matugen palette from DankMaterialShell as the pi theme `desktop`, and keep it in sync.",
237
- },
238
- });
239
-
240
- export const desktopSource = setting({
241
- id: "look.desktop.source",
242
- default: "~/.cache/DankMaterialShell/dms-colors.json",
243
- decoder: string,
244
- ui: {
245
- section: "Desktop theme",
246
- label: "Desktop palette",
247
- description: "The DankMaterialShell colour file the desktop theme is made from.",
248
- },
249
- });
250
-
251
220
  export const LOOK_SETTINGS = [
252
221
  strip,
253
222
  topLeft,
@@ -255,6 +224,7 @@ export const LOOK_SETTINGS = [
255
224
  bottomLeft,
256
225
  bottomRight,
257
226
  below,
227
+ belowRight,
258
228
  frame,
259
229
  cursor,
260
230
  header,
@@ -264,6 +234,4 @@ export const LOOK_SETTINGS = [
264
234
  gaugeCells,
265
235
  labels,
266
236
  peek,
267
- desktop,
268
- desktopSource,
269
237
  ];
package/src/ui/footer.ts CHANGED
@@ -57,7 +57,7 @@ export class FooterComponent implements Component {
57
57
  lines.push(spread(screen, "topLeft", "topRight", room));
58
58
  lines.push(spread(screen, "bottomLeft", "bottomRight", room));
59
59
  }
60
- lines.push(renderSlot(screen, "below", room));
60
+ lines.push(spread(screen, "below", "belowRight", room));
61
61
 
62
62
  const kept = lines.filter((line) => line !== "").map((line) => `${pad}${line}`);
63
63
  return kept.length === 0 ? [""] : kept;