@adeildo/pi-look 5.9.2 → 5.10.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/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { dirname, join } from "node:path";
2
+
1
3
  import {
2
4
  accountStateOf,
3
5
  createApp,
@@ -6,6 +8,7 @@ import {
6
8
  defineFeature,
7
9
  drawSkillRefs,
8
10
  type FeatureScope,
11
+ hideImagesUnderOverlays,
9
12
  SKILL_REF,
10
13
  skillNames,
11
14
  } from "@adeildo/pi-kit";
@@ -14,21 +17,20 @@ import type {
14
17
  ExtensionContext,
15
18
  ReadonlyFooterDataProvider,
16
19
  } from "@earendil-works/pi-coding-agent";
17
- import type { TUI } from "@earendil-works/pi-tui";
20
+ import { Key, type TUI } from "@earendil-works/pi-tui";
18
21
 
19
22
  import { type GitState, readGit } from "./data/git.ts";
23
+ import { type GithubState, readGithub } from "./data/github.ts";
20
24
  import { emptySnapshot, type LiveInputs, type Snapshot, SnapshotReader } from "./data/snapshot.ts";
25
+ import type { LastVisit } from "./data/start-notes.ts";
21
26
  import { Telemetry } from "./data/telemetry.ts";
22
27
  import type { DeltaKind } from "./data/telemetry.ts";
23
28
  import { REQUEST_ENTRY, type RequestRecord } from "./data/totals.ts";
24
- import { resolveIcons } from "./render/glyphs.ts";
29
+ import { visit } from "./data/visits.ts";
30
+ import { type Glyphs, glyphsOf } from "./render/glyphs.ts";
25
31
  import { PLAIN, themePaint } from "./render/paint.ts";
26
32
  import { claimedStatuses, renderSegments, SEGMENTS } from "./render/segments.ts";
27
33
  import {
28
- below,
29
- belowRight,
30
- bottomLeft,
31
- bottomRight,
32
34
  cursor,
33
35
  frame,
34
36
  gaugeCells,
@@ -37,27 +39,29 @@ import {
37
39
  labels,
38
40
  LOOK_SETTINGS,
39
41
  pathLength,
40
- peek,
41
- separator,
42
42
  SECTIONS,
43
- SEPARATORS,
44
- strip,
45
- stripRight,
46
- topLeft,
47
- topRight,
43
+ SEPARATOR,
44
+ SLOTS,
45
+ userFrame,
48
46
  } from "./settings.ts";
49
47
  import { applyCursor, LookEditor } from "./ui/editor.ts";
50
48
  import { FooterComponent, StripComponent } from "./ui/footer.ts";
51
- import { HeaderComponent, type LoadedCounts } from "./ui/header.ts";
49
+ import { HARNESS_VERSION, HeaderComponent, type LoadedCounts } from "./ui/header.ts";
50
+ import { OutputView, outputsOf } from "./ui/outputs.ts";
51
+ import { framePiEntries } from "./ui/pi-entries.ts";
52
+ import { frameQueue } from "./ui/queue.ts";
52
53
  import type { Screen, SlotName } from "./ui/screen.ts";
53
54
  import { registerToolFrames } from "./ui/tool-frame.ts";
54
55
  import { type Activity, describe, draftedTool } from "./ui/working.ts";
55
56
 
56
57
  export { SEGMENT_IDS, SEGMENTS, type SegmentId } from "./render/segments.ts";
58
+ import type { SegmentId } from "./render/segments.ts";
57
59
  export { ROLE_TOKENS } from "./render/paint.ts";
58
60
 
59
61
  const STRIP_WIDGET = "pi-look:strip";
60
62
  const TICK_MS = 500;
63
+ /** GitHub is asked once per process, however many sessions it opens. */
64
+ let probedGithub = false;
61
65
 
62
66
  interface Live {
63
67
  ctx: ExtensionContext | undefined;
@@ -65,6 +69,7 @@ interface Live {
65
69
  tui: TUI | undefined;
66
70
  repaint: (() => void) | undefined;
67
71
  git: GitState | undefined;
72
+ github: GithubState | undefined;
68
73
  probing: boolean;
69
74
  ticker: ReturnType<typeof setInterval> | undefined;
70
75
  frame: Snapshot | undefined;
@@ -86,21 +91,36 @@ export const look = defineFeature({
86
91
  tui: undefined,
87
92
  repaint: undefined,
88
93
  git: undefined,
94
+ github: undefined,
89
95
  probing: false,
90
96
  ticker: undefined,
91
97
  frame: undefined,
92
98
  activity: undefined,
93
99
  };
94
100
 
95
- const screen = createScreen(scope, live, reader, telemetry);
101
+ const glyphs = glyphsOf(() => icons.get(scope));
102
+ const screen = createScreen(scope, live, reader, telemetry, glyphs);
96
103
  registerToolFrames(scope);
97
- const repaint = () => live.repaint?.();
104
+ framePiEntries(() => {
105
+ if (live.ctx === undefined) return undefined;
106
+ return { theme: live.ctx.ui.theme, set: glyphs().set, you: userFrame.get(scope) };
107
+ });
108
+ frameQueue(() => live.ctx?.ui.theme);
109
+ const repaint = () => {
110
+ screen.bump();
111
+ live.repaint?.();
112
+ };
98
113
 
99
114
  for (const entry of LOOK_SETTINGS) entry.listen(scope, repaint);
100
115
  frame.listen(scope, () => live.ctx && installEditor(live.ctx, screen, live, scope));
101
- header.listen(scope, () => live.ctx && installHeader(live.ctx, screen, scope));
102
- cursor.listen(scope, (style) => live.tui && applyCursor(live.tui, style));
103
- peek.listen(scope, (on) => !on && live.ctx?.ui.setWorkingMessage());
116
+ const visitsFile = join(dirname(scope.settings.globalPath), "look-visits.json");
117
+ let visited = false;
118
+ let lastVisit: LastVisit | undefined;
119
+
120
+ scope.registerShortcut(Key.alt("o"), {
121
+ description: "A tool call's whole output, to scroll; the arrows move between calls",
122
+ handler: (ctx) => openOutputs(ctx),
123
+ });
104
124
 
105
125
  scope.onSessionStart((ctx) => {
106
126
  live.ctx = ctx;
@@ -113,7 +133,12 @@ export const look = defineFeature({
113
133
  new FooterComponent(tui, data, screen, {
114
134
  attach: (footerData, paint) => {
115
135
  live.footerData = footerData;
116
- live.repaint = paint;
136
+ // Everything that asks for a redraw says what the screen draws may have changed, which is
137
+ // what a component that keeps its drawing for a moment compares.
138
+ live.repaint = () => {
139
+ screen.bump();
140
+ paint();
141
+ };
117
142
  },
118
143
  detach: () => {
119
144
  live.footerData = undefined;
@@ -125,7 +150,23 @@ export const look = defineFeature({
125
150
  placement: "aboveEditor",
126
151
  });
127
152
  installEditor(ctx, screen, live, scope);
128
- installHeader(ctx, screen, scope);
153
+ // Once per launch: a new session in the same pi is not a return to the folder.
154
+ if (!visited) {
155
+ visited = true;
156
+ lastVisit = visit(visitsFile, ctx.cwd, HARNESS_VERSION, Date.now());
157
+ }
158
+ // Once per launch, off the critical path: the card shows these lines when the answers arrive.
159
+ if (!probedGithub) {
160
+ probedGithub = true;
161
+ void probeGithub(live, lastVisit?.at);
162
+ }
163
+ installHeader(
164
+ ctx,
165
+ screen,
166
+ scope,
167
+ () => lastVisit,
168
+ () => live.github,
169
+ );
129
170
  });
130
171
 
131
172
  scope.onShutdown(() => {
@@ -150,13 +191,13 @@ export const look = defineFeature({
150
191
 
151
192
  scope.on("agent_start", () => {
152
193
  telemetry.runStarted();
153
- setActivity(live, scope, { kind: "waiting" });
194
+ setActivity(live, { kind: "waiting" });
154
195
  startTicking(live);
155
196
  });
156
197
 
157
198
  scope.on("turn_start", () => {
158
199
  telemetry.requestStarted();
159
- setActivity(live, scope, { kind: "waiting" });
200
+ setActivity(live, { kind: "waiting" });
160
201
  });
161
202
 
162
203
  scope.on("message_start", (event) => {
@@ -172,12 +213,12 @@ export const look = defineFeature({
172
213
  telemetry.answerGrew(delta, message.usage, kind);
173
214
 
174
215
  if (update.type === "thinking_start" || update.type === "thinking_delta") {
175
- setActivity(live, scope, { kind: "thinking" });
216
+ setActivity(live, { kind: "thinking" });
176
217
  } else if (update.type === "text_delta" || update.type === "text_start") {
177
- setActivity(live, scope, { kind: "writing" });
218
+ setActivity(live, { kind: "writing" });
178
219
  } else if (update.type === "toolcall_start" || update.type === "toolcall_delta") {
179
220
  const tool = draftedTool(message.content);
180
- if (tool !== undefined) setActivity(live, scope, { kind: "drafting", tool });
221
+ if (tool !== undefined) setActivity(live, { kind: "drafting", tool });
181
222
  }
182
223
  });
183
224
 
@@ -190,7 +231,7 @@ export const look = defineFeature({
190
231
  });
191
232
 
192
233
  scope.on("tool_execution_start", (event) => {
193
- setActivity(live, scope, { kind: "running", tool: event.toolName });
234
+ setActivity(live, { kind: "running", tool: event.toolName });
194
235
  });
195
236
 
196
237
  scope.on("tool_execution_end", () => void probeGit(live));
@@ -211,9 +252,9 @@ export const look = defineFeature({
211
252
 
212
253
  scope.screen.action({
213
254
  id: "explain",
214
- section: "Slots",
255
+ section: "Appearance",
215
256
  label: "What is on screen",
216
- description: "Every piece the slots show right now, and what it means.",
257
+ description: "Every piece around the editor right now, and what it means.",
217
258
  run: () => explain(screen),
218
259
  });
219
260
  },
@@ -234,10 +275,17 @@ function explain(screen: Screen): string {
234
275
  const snapshot = screen.snapshot();
235
276
  const paint = screen.paint();
236
277
  const lines: string[] = [paint.bold("What is on screen")];
237
- for (const name of Object.keys(SLOT_TITLES) as SlotName[]) {
238
- const ids = screen.slot(name);
278
+ const groups: [string, readonly SegmentId[]][] = [
279
+ ...(Object.keys(SLOT_TITLES) as SlotName[]).map((name): [string, readonly SegmentId[]] => [
280
+ SLOT_TITLES[name],
281
+ screen.slot(name),
282
+ ]),
283
+ // Not on screen: the timings of the last call, kept for when an answer feels slow.
284
+ ["The last call to the model", ["last", "wait", "speed", "server", "request"]],
285
+ ];
286
+ for (const [title, ids] of groups) {
239
287
  if (ids.length === 0) continue;
240
- lines.push("", paint.role("brand", SLOT_TITLES[name]));
288
+ lines.push("", paint.role("brand", title));
241
289
  for (const id of ids) {
242
290
  const [piece] = renderSegments([id], {
243
291
  snapshot,
@@ -252,13 +300,7 @@ function explain(screen: Screen): string {
252
300
  lines.push(` ${shown}`, ` ${paint.dim(`${id}: ${meaning}`)}`);
253
301
  }
254
302
  }
255
- lines.push(
256
- "",
257
- paint.dim("Arrows are from where you sit: ↑ in is sent to the model, ↓ out comes back."),
258
- paint.dim(
259
- "Slots and labels live under look. in ~/.pi/agent/extensions/pi-harness/settings.json.",
260
- ),
261
- );
303
+ lines.push("", paint.dim("In is what was sent to the model, out is what came back."));
262
304
  return lines.join("\n");
263
305
  }
264
306
 
@@ -267,18 +309,10 @@ function createScreen(
267
309
  live: Live,
268
310
  reader: SnapshotReader,
269
311
  telemetry: Telemetry,
312
+ glyphs: () => Glyphs,
270
313
  ): Screen {
271
- const slots: Record<SlotName, typeof strip> = {
272
- strip,
273
- stripRight,
274
- topLeft,
275
- topRight,
276
- bottomLeft,
277
- bottomRight,
278
- below,
279
- belowRight,
280
- };
281
-
314
+ // What the components that keep a drawing for a moment compare: an event, a probe, a new branch.
315
+ let revision = 0;
282
316
  return {
283
317
  snapshot() {
284
318
  const ctx = live.ctx;
@@ -300,18 +334,22 @@ function createScreen(
300
334
  });
301
335
  return snapshot;
302
336
  },
303
- glyphs: () => resolveIcons(icons.get(scope)),
337
+ glyphs,
338
+ revision: () => revision,
339
+ bump: () => {
340
+ revision++;
341
+ },
304
342
  paint: (border) => (live.ctx === undefined ? PLAIN : themePaint(live.ctx.ui.theme, border)),
305
- separator: () => SEPARATORS[separator.get(scope)],
343
+ separator: () => SEPARATOR,
306
344
  options: () => ({
307
- pathLength: pathLength.get(scope),
308
- gaugeCells: gaugeCells.get(scope),
309
- labels: labels.get(scope),
310
- claimed: claimedStatuses(Object.values(slots).map((entry) => entry.get(scope))),
345
+ pathLength,
346
+ gaugeCells,
347
+ labels,
348
+ claimed: claimedStatuses(Object.values(SLOTS)),
311
349
  }),
312
- slot: (name) => slots[name].get(scope),
350
+ slot: (name) => SLOTS[name],
313
351
  frameStyle: () => frame.get(scope),
314
- cursor: () => cursor.get(scope),
352
+ cursor: () => cursor,
315
353
  decorate: (line) => {
316
354
  const theme = live.ctx?.ui.theme;
317
355
  if (theme === undefined) return line;
@@ -345,23 +383,31 @@ function installEditor(
345
383
  }
346
384
  ctx.ui.setEditorComponent((tui, theme, keybindings) => {
347
385
  live.tui = tui;
348
- applyCursor(tui, cursor.get(scope));
386
+ applyCursor(tui, cursor);
349
387
  return new LookEditor(tui, theme, keybindings, screen);
350
388
  });
351
389
  }
352
390
 
353
- function installHeader(ctx: ExtensionContext, screen: Screen, scope: FeatureScope): void {
391
+ function installHeader(
392
+ ctx: ExtensionContext,
393
+ screen: Screen,
394
+ scope: FeatureScope,
395
+ lastVisit: () => LastVisit | undefined,
396
+ github: () => GithubState | undefined,
397
+ ): void {
354
398
  if (ctx.mode !== "tui") return;
355
- if (header.get(scope) === "off") {
356
- ctx.ui.setHeader(undefined);
357
- return;
358
- }
359
399
  ctx.ui.setHeader(
360
- () =>
361
- new HeaderComponent(screen, {
362
- style: () => header.get(scope),
363
- counts: () => loadedCounts(scope),
364
- }),
400
+ (tui) =>
401
+ new HeaderComponent(
402
+ screen,
403
+ {
404
+ style: () => header,
405
+ counts: () => loadedCounts(scope),
406
+ lastVisit,
407
+ github,
408
+ },
409
+ tui,
410
+ ),
365
411
  );
366
412
  }
367
413
 
@@ -383,14 +429,23 @@ function loadedCounts(pi: ExtensionAPI): LoadedCounts {
383
429
  };
384
430
  }
385
431
 
386
- function setActivity(live: Live, scope: FeatureScope, activity: Activity): void {
387
- if (!peek.get(scope)) return;
432
+ function setActivity(live: Live, activity: Activity): void {
388
433
  const message = describe(activity);
389
434
  if (message === live.activity) return;
390
435
  live.activity = message;
391
436
  live.ctx?.ui.setWorkingMessage(message);
392
437
  }
393
438
 
439
+ async function probeGithub(live: Live, since: number | undefined): Promise<void> {
440
+ const ctx = live.ctx;
441
+ if (ctx === undefined) return;
442
+ const github = await readGithub(ctx.cwd, since);
443
+ if (live.ctx === undefined) return;
444
+ live.github = github;
445
+ // The card keeps its drawing for a second; this is the redraw that makes the new lines appear.
446
+ live.repaint?.();
447
+ }
448
+
394
449
  async function probeGit(live: Live): Promise<void> {
395
450
  const ctx = live.ctx;
396
451
  if (ctx === undefined || live.probing) return;
@@ -418,3 +473,24 @@ function stopTicking(live: Live): void {
418
473
  export default function piLook(pi: ExtensionAPI): void {
419
474
  createApp(pi, { name: "pi-look" }).use(look).build();
420
475
  }
476
+
477
+ /** Alt+O: the session's tool output, one call at a time, newest first. */
478
+ async function openOutputs(ctx: ExtensionContext): Promise<void> {
479
+ if (ctx.mode !== "tui") return;
480
+ const outputs = outputsOf(ctx.sessionManager.getBranch());
481
+ await ctx.ui.custom<void>(
482
+ (tui, theme, _keybindings, done) => {
483
+ hideImagesUnderOverlays(tui);
484
+ return new OutputView({
485
+ theme,
486
+ outputs,
487
+ rows: () => Math.floor(tui.terminal.rows * 0.85),
488
+ close: () => done(),
489
+ });
490
+ },
491
+ {
492
+ overlay: true,
493
+ overlayOptions: { anchor: "center", width: "90%", minWidth: 60, maxHeight: "85%" },
494
+ },
495
+ );
496
+ }
package/src/render/fit.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { CUT } from "@adeildo/pi-kit";
1
2
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
3
 
3
4
  export interface Piece {
@@ -55,7 +56,7 @@ export function fitRegions(
55
56
  return joined.map((text) =>
56
57
  text === ""
57
58
  ? ""
58
- : truncateToWidth(text, Math.max(0, width - overhead), options.ellipsis ?? "..."),
59
+ : truncateToWidth(text, Math.max(0, width - overhead), options.ellipsis ?? CUT),
59
60
  );
60
61
  }
61
62
 
@@ -1,5 +1,6 @@
1
1
  import { isAbsolute, relative, resolve, sep } from "node:path";
2
2
 
3
+ import { formatCost, formatTokens } from "@adeildo/pi-kit";
3
4
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
4
5
 
5
6
  export const CELL = {
@@ -17,14 +18,8 @@ export function cell(text: string, width: number): string {
17
18
 
18
19
  export const UNKNOWN = "–";
19
20
 
20
- export function count(value: number): string {
21
- const n = Math.max(0, Math.round(value));
22
- if (n < 1000) return String(n);
23
- if (n < 10_000) return `${trim((n / 1000).toFixed(1))}k`;
24
- if (n < 1_000_000) return `${Math.round(n / 1000)}k`;
25
- if (n < 10_000_000) return `${trim((n / 1_000_000).toFixed(1))}M`;
26
- return `${Math.round(n / 1_000_000)}M`;
27
- }
21
+ /** The look's word for the kit's format, which is where the shape was decided. */
22
+ export const count = formatTokens;
28
23
 
29
24
  export function rate(perSecond: number): string {
30
25
  if (perSecond < 10) return trim(perSecond.toFixed(1));
@@ -55,10 +50,7 @@ export function latency(milliseconds: number): string {
55
50
  return duration(milliseconds);
56
51
  }
57
52
 
58
- export function money(amount: number): string {
59
- if (amount === 0) return "$0.00";
60
- return amount >= 10 ? `$${amount.toFixed(2)}` : `$${amount.toFixed(3)}`;
61
- }
53
+ export const money = formatCost;
62
54
 
63
55
  export function percent(value: number): string {
64
56
  return value >= 10 || value === 0 ? `${Math.round(value)}%` : `${value.toFixed(1)}%`;
@@ -107,6 +99,11 @@ export function fitWidth(text: string, width: number, ellipsis = "..."): string
107
99
  return truncateToWidth(text, Math.max(0, width), ellipsis);
108
100
  }
109
101
 
102
+ /** Tabs become three spaces: a tab is as wide as the terminal says, and these lines have a width. */
103
+ export function tabs(text: string): string {
104
+ return text.replace(/\t/g, " ");
105
+ }
106
+
110
107
  function underHome(cwd: string, home: string): string {
111
108
  const rest = relative(resolve(home), resolve(cwd));
112
109
  const inside =
@@ -160,3 +160,17 @@ export function resolveIcons(mode: IconMode, env: NodeJS.ProcessEnv = process.en
160
160
  if (env.SSH_TTY !== undefined || env.SSH_CONNECTION !== undefined) return UNICODE;
161
161
  return NERD;
162
162
  }
163
+
164
+ /**
165
+ * The glyphs for a scope, resolved once and again only when the setting behind them changes. Every
166
+ * frame, rule and slot of the screen asks for them on each drawing, and resolving each time was a
167
+ * fifth of what typing cost.
168
+ */
169
+ export function glyphsOf(modeOf: () => IconMode): () => Glyphs {
170
+ let known: { mode: IconMode; glyphs: Glyphs } | undefined;
171
+ return () => {
172
+ const mode = modeOf();
173
+ if (known?.mode !== mode) known = { mode, glyphs: resolveIcons(mode) };
174
+ return known.glyphs;
175
+ };
176
+ }
@@ -1,4 +1,4 @@
1
- import { type AccountWindow, type PeerMark, tasksWith } from "@adeildo/pi-kit";
1
+ import { type AccountWindow, formatClock, type PeerMark, tasksWith } from "@adeildo/pi-kit";
2
2
  import { visibleWidth } from "@earendil-works/pi-tui";
3
3
 
4
4
  import type { Snapshot } from "../data/snapshot.ts";
@@ -233,8 +233,7 @@ export const SEGMENTS: Record<BuiltinSegment, Segment> = {
233
233
  priority: 15,
234
234
  describe: "the time of day",
235
235
  render: ({ snapshot, glyphs, paint }) => {
236
- const now = new Date(snapshot.now);
237
- const time = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
236
+ const time = formatClock(snapshot.now);
238
237
  return { text: `${mark(glyphs.clock, paint, "clock")}${paint.role("clock", time)}` };
239
238
  },
240
239
  },
@@ -344,18 +343,18 @@ export const SEGMENTS: Record<BuiltinSegment, Segment> = {
344
343
  elapsed: {
345
344
  priority: 65,
346
345
  describe:
347
- "how long the agent has worked on this prompt, and how many calls to the model it made",
348
- render: ({ snapshot, glyphs, paint, options }) => {
346
+ "the run: while the agent works, how long and how many calls to the model; once it answered, the same and what it cost",
347
+ render: ({ snapshot, glyphs, paint }) => {
349
348
  const run = snapshot.run;
350
349
  if (run === undefined) return undefined;
351
350
  const role: Role = run.running ? "running" : "done";
352
351
  const icon = mark(run.running ? glyphs.running : glyphs.done, paint, role);
353
352
  const time = paint.role(role, stopwatch(run.elapsedMs));
354
- const calls =
355
- run.requests <= 1
356
- ? ""
357
- : paint.dim(options.labels ? ` ${run.requests} calls` : ` ×${run.requests}`);
358
- return { text: `${icon}${time}${calls}`, compact: `${icon}${time}` };
353
+ const lead = run.running ? time : `${paint.dim("answered in ")}${time}`;
354
+ const calls = run.requests <= 1 ? "" : paint.dim(` ${run.requests} calls`);
355
+ const cost =
356
+ run.running || run.cost === 0 ? "" : ` ${paint.money(run.cost, money(run.cost))}`;
357
+ return { text: `${icon}${lead}${calls}${cost}`, compact: `${icon}${time}` };
359
358
  },
360
359
  },
361
360
  last: {
@@ -440,9 +439,10 @@ export const SEGMENTS: Record<BuiltinSegment, Segment> = {
440
439
  // A session that spent nothing has nothing to say yet.
441
440
  if (amount === 0) return undefined;
442
441
  const value = paint.money(amount, money(amount));
443
- const sub = snapshot.subscription ? paint.dim(" sub") : "";
442
+ // On a plan nothing is billed per token: the amount is what the tokens would cost.
443
+ const what = paint.dim(snapshot.subscription ? " at API prices" : " this session");
444
444
  const icon = glyphs.cost === "$" ? "" : mark(glyphs.cost, paint, "cost");
445
- return { text: `${icon}${value}${sub}`, compact: `${icon}${value}` };
445
+ return { text: `${icon}${value}${what}`, compact: `${icon}${value}` };
446
446
  },
447
447
  },
448
448
  tokens: {
@@ -453,12 +453,13 @@ export const SEGMENTS: Record<BuiltinSegment, Segment> = {
453
453
  const totals = snapshot.totals;
454
454
  if (tokensOf(totals) === 0) return undefined;
455
455
  // Everything sent, not just the uncached part: `66 in` next to a million cached reads is a lie.
456
- const sent = `${paint.role("sent", glyphs.sent)}${paint.role("sent", count(promptOf(totals)))}`;
457
- const got = `${paint.role("received", glyphs.received)}${paint.role("received", count(totals.output))}`;
458
- if (!options.labels) return { text: `${sent} ${got}` };
456
+ const sent = paint.role("sent", count(promptOf(totals)));
457
+ const got = paint.role("received", count(totals.output));
458
+ const arrows = `${paint.role("sent", glyphs.sent)}${sent} ${paint.role("received", glyphs.received)}${got}`;
459
+ if (!options.labels) return { text: arrows };
459
460
  return {
460
- text: `${sent}${paint.dim(" in")} ${got}${paint.dim(" out")}`,
461
- compact: `${sent} ${got}`,
461
+ text: `${paint.dim("in ")}${sent} ${paint.dim("out ")}${got}`,
462
+ compact: `${paint.dim("in ")}${sent} ${paint.dim("out ")}${got}`,
462
463
  };
463
464
  },
464
465
  },
@@ -476,8 +477,11 @@ export const SEGMENTS: Record<BuiltinSegment, Segment> = {
476
477
  const value =
477
478
  hit >= 90 ? paint.role("done", percent(hit)) : paint.stress(100 - hit, percent(hit));
478
479
  return {
479
- text: `${icon}${value}${options.labels ? paint.dim(" cached") : ""}`,
480
- compact: `${icon}${value}`,
480
+ // The numbers around it are the session's, so the label says this one is the last prompt's.
481
+ text: options.labels
482
+ ? `${paint.dim("last prompt ")}${value}${paint.dim(" cached")}`
483
+ : `${icon}${value}`,
484
+ compact: options.labels ? `${paint.dim("cache ")}${value}` : `${icon}${value}`,
481
485
  };
482
486
  },
483
487
  },
@@ -485,17 +489,13 @@ export const SEGMENTS: Record<BuiltinSegment, Segment> = {
485
489
  priority: 25,
486
490
  describe:
487
491
  "the session's average speeds: ↓ the model writing, ↑ it reading the prompt, cache included",
488
- render: ({ snapshot, glyphs, paint }) => {
492
+ render: ({ snapshot, paint }) => {
489
493
  const { decode, prefill } = snapshot.averages;
490
494
  if (decode === undefined && prefill === undefined) return undefined;
491
495
  const down =
492
- decode === undefined
493
- ? ""
494
- : `${paint.role("received", glyphs.received)}${paint.role("speed", rate(decode))}`;
496
+ decode === undefined ? "" : `${paint.dim("out ")}${paint.role("speed", rate(decode))}`;
495
497
  const up =
496
- prefill === undefined
497
- ? ""
498
- : `${paint.role("sent", glyphs.sent)}${paint.role("sent", rate(prefill))}`;
498
+ prefill === undefined ? "" : `${paint.dim("in ")}${paint.role("sent", rate(prefill))}`;
499
499
  const label = paint.dim("avg ");
500
500
  const unit = paint.dim(" tok/s");
501
501
  return {
@@ -572,7 +572,10 @@ function cacheParts(
572
572
  options: SegmentOptions,
573
573
  fixed = false,
574
574
  ): string {
575
- const value = (tokens: number) => (fixed ? tokenCell(tokens) : tokens > 0 ? count(tokens) : "");
575
+ const value = (tokens: number): string => {
576
+ if (fixed) return tokenCell(tokens);
577
+ return tokens > 0 ? count(tokens) : "";
578
+ };
576
579
  const read = value(usage.cacheRead);
577
580
  const write = value(usage.cacheWrite);
578
581
  if (options.labels) {
@@ -612,7 +615,3 @@ function speedCell(value: number | undefined): string {
612
615
  function tokenCell(value: number): string {
613
616
  return cell(value === 0 ? UNKNOWN : count(value), CELL.count);
614
617
  }
615
-
616
- function pad(value: number): string {
617
- return String(value).padStart(2, "0");
618
- }