@adeildo/pi-harness 4.0.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/LICENSE +21 -0
- package/README.md +24 -0
- package/docs/look.md +163 -0
- package/docs/providers.md +35 -0
- package/package.json +54 -0
- package/src/look/data/git.ts +58 -0
- package/src/look/data/snapshot.ts +173 -0
- package/src/look/data/telemetry.ts +188 -0
- package/src/look/data/totals.ts +149 -0
- package/src/look/desktop/palette.ts +207 -0
- package/src/look/desktop/sync.ts +86 -0
- package/src/look/index.ts +448 -0
- package/src/look/render/fit.ts +89 -0
- package/src/look/render/format.ts +136 -0
- package/src/look/render/frame.ts +118 -0
- package/src/look/render/glyphs.ts +155 -0
- package/src/look/render/paint.ts +137 -0
- package/src/look/render/segments.ts +514 -0
- package/src/look/settings.ts +237 -0
- package/src/look/ui/editor.ts +194 -0
- package/src/look/ui/footer.ts +103 -0
- package/src/look/ui/header.ts +212 -0
- package/src/look/ui/screen.ts +39 -0
- package/src/look/ui/working.ts +73 -0
- package/src/providers/index.ts +10 -0
- package/src/providers/subscription/billing.ts +27 -0
- package/src/providers/subscription/feature.ts +43 -0
- package/src/providers/subscription/payload.ts +87 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
// What each piece says. A segment is a pure function of the snapshot: it returns a piece, with an
|
|
2
|
+
// optional shorter form and a priority, or nothing when it has nothing to say. Where a segment goes is
|
|
3
|
+
// the layout's business, so any segment can sit in any slot.
|
|
4
|
+
import type { Snapshot } from "../data/snapshot.ts";
|
|
5
|
+
import { promptOf, tokensOf, type Totals } from "../data/totals.ts";
|
|
6
|
+
import type { Piece } from "./fit.ts";
|
|
7
|
+
import {
|
|
8
|
+
basename,
|
|
9
|
+
CELL,
|
|
10
|
+
cell,
|
|
11
|
+
count,
|
|
12
|
+
fitWidth,
|
|
13
|
+
gauge,
|
|
14
|
+
latency,
|
|
15
|
+
money,
|
|
16
|
+
percent,
|
|
17
|
+
rate,
|
|
18
|
+
shortenPath,
|
|
19
|
+
stopwatch,
|
|
20
|
+
UNKNOWN,
|
|
21
|
+
} from "./format.ts";
|
|
22
|
+
import type { Glyphs } from "./glyphs.ts";
|
|
23
|
+
import { EFFORT_LEVELS, type Paint, type Role } from "./paint.ts";
|
|
24
|
+
|
|
25
|
+
export const SEGMENT_IDS = [
|
|
26
|
+
"path",
|
|
27
|
+
"branch",
|
|
28
|
+
"host",
|
|
29
|
+
"session",
|
|
30
|
+
"version",
|
|
31
|
+
"clock",
|
|
32
|
+
"provider",
|
|
33
|
+
"model",
|
|
34
|
+
"effort",
|
|
35
|
+
"context",
|
|
36
|
+
"speed",
|
|
37
|
+
"wait",
|
|
38
|
+
"elapsed",
|
|
39
|
+
"last",
|
|
40
|
+
"request",
|
|
41
|
+
"costRate",
|
|
42
|
+
"cost",
|
|
43
|
+
"tokens",
|
|
44
|
+
"cache",
|
|
45
|
+
"average",
|
|
46
|
+
"statuses",
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
export type BuiltinSegment = (typeof SEGMENT_IDS)[number];
|
|
50
|
+
/** One package's status on its own, by the key it passes to `setStatus`. */
|
|
51
|
+
export type StatusSegment = `status:${string}`;
|
|
52
|
+
export type SegmentId = BuiltinSegment | StatusSegment;
|
|
53
|
+
|
|
54
|
+
const STATUS_PREFIX = "status:";
|
|
55
|
+
/** A status placed on its own is something its package wants seen, like the permission mode. */
|
|
56
|
+
const STATUS_PRIORITY = 95;
|
|
57
|
+
|
|
58
|
+
export function isSegmentId(value: string): value is SegmentId {
|
|
59
|
+
return (
|
|
60
|
+
(SEGMENT_IDS as readonly string[]).includes(value) ||
|
|
61
|
+
(value.startsWith(STATUS_PREFIX) && value.length > STATUS_PREFIX.length)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The status keys a layout places on their own, which the `statuses` segment then leaves out. */
|
|
66
|
+
export function claimedStatuses(slots: readonly (readonly SegmentId[])[]): Set<string> {
|
|
67
|
+
const claimed = new Set<string>();
|
|
68
|
+
for (const ids of slots) {
|
|
69
|
+
for (const id of ids)
|
|
70
|
+
if (id.startsWith(STATUS_PREFIX)) claimed.add(id.slice(STATUS_PREFIX.length));
|
|
71
|
+
}
|
|
72
|
+
return claimed;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface SegmentOptions {
|
|
76
|
+
/** Longest the folder may be before it is shortened from the left. 0 keeps all of it. */
|
|
77
|
+
pathLength: number;
|
|
78
|
+
/** Cells in the context gauge. 0 draws no gauge. */
|
|
79
|
+
gaugeCells: number;
|
|
80
|
+
/** Status keys placed on their own somewhere in the layout. */
|
|
81
|
+
claimed: ReadonlySet<string>;
|
|
82
|
+
/** A short word before each number, saying what it is. */
|
|
83
|
+
labels: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface SegmentInput {
|
|
87
|
+
snapshot: Snapshot;
|
|
88
|
+
glyphs: Glyphs;
|
|
89
|
+
paint: Paint;
|
|
90
|
+
options: SegmentOptions;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
type Rendered = Omit<Piece, "priority"> | undefined;
|
|
94
|
+
|
|
95
|
+
interface Segment {
|
|
96
|
+
/** Higher stays longer when the line runs out of room. */
|
|
97
|
+
priority: number;
|
|
98
|
+
/** One line for the docs and the settings screen. */
|
|
99
|
+
describe: string;
|
|
100
|
+
render(input: SegmentInput): Rendered;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const SEGMENTS: Record<BuiltinSegment, Segment> = {
|
|
104
|
+
path: {
|
|
105
|
+
priority: 70,
|
|
106
|
+
describe: "the working folder, with home as ~",
|
|
107
|
+
render: ({ snapshot, glyphs, paint, options }) => {
|
|
108
|
+
if (snapshot.cwd === "") return undefined;
|
|
109
|
+
const full = shortenPath(snapshot.cwd, snapshot.home, options.pathLength);
|
|
110
|
+
const icon = mark(glyphs.folder, paint, "path");
|
|
111
|
+
return {
|
|
112
|
+
text: `${icon}${paint.role("path", full)}`,
|
|
113
|
+
compact: `${icon}${paint.role("path", basename(snapshot.cwd))}`,
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
branch: {
|
|
118
|
+
priority: 82,
|
|
119
|
+
describe:
|
|
120
|
+
"the git branch: ↑ commits ahead, ↓ behind, + staged, ~ modified, ? untracked, ! conflicts, then stashes",
|
|
121
|
+
render: ({ snapshot, glyphs, paint }) => {
|
|
122
|
+
if (snapshot.branch === null) return undefined;
|
|
123
|
+
const icon = mark(glyphs.branch, paint, "branch");
|
|
124
|
+
const detached = snapshot.branch === "detached";
|
|
125
|
+
const name = detached
|
|
126
|
+
? paint.role("behind", "detached")
|
|
127
|
+
: paint.role("branch", fitWidth(snapshot.branch, 32, glyphs.ellipsis));
|
|
128
|
+
const git = snapshot.git;
|
|
129
|
+
if (git === undefined) return { text: `${icon}${name}` };
|
|
130
|
+
|
|
131
|
+
const marks = [
|
|
132
|
+
counted(git.ahead, glyphs.ahead, "ahead", paint),
|
|
133
|
+
counted(git.behind, glyphs.behind, "behind", paint),
|
|
134
|
+
counted(git.staged, glyphs.staged, "staged", paint),
|
|
135
|
+
counted(git.modified, glyphs.modified, "modified", paint),
|
|
136
|
+
counted(git.untracked, glyphs.untracked, "untracked", paint),
|
|
137
|
+
counted(git.conflicted, glyphs.conflicted, "conflicted", paint),
|
|
138
|
+
counted(git.stashed, glyphs.stashed, "untracked", paint),
|
|
139
|
+
].filter((part) => part !== "");
|
|
140
|
+
const dirty = git.staged + git.modified + git.untracked + git.conflicted > 0;
|
|
141
|
+
return {
|
|
142
|
+
text: marks.length === 0 ? `${icon}${name}` : `${icon}${name} ${marks.join(" ")}`,
|
|
143
|
+
compact: dirty ? `${icon}${name}${paint.role("modified", "*")}` : `${icon}${name}`,
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
host: {
|
|
148
|
+
priority: 20,
|
|
149
|
+
describe: "the machine, so a pasted line says where it came from",
|
|
150
|
+
render: ({ snapshot, glyphs, paint }) =>
|
|
151
|
+
snapshot.host === undefined || snapshot.host === ""
|
|
152
|
+
? undefined
|
|
153
|
+
: { text: `${mark(glyphs.host, paint, "host")}${paint.role("host", snapshot.host)}` },
|
|
154
|
+
},
|
|
155
|
+
session: {
|
|
156
|
+
priority: 45,
|
|
157
|
+
describe: "the session name, once it has one",
|
|
158
|
+
render: ({ snapshot, glyphs, paint }) => {
|
|
159
|
+
const name = snapshot.sessionName;
|
|
160
|
+
if (name === undefined || name === "") return undefined;
|
|
161
|
+
const icon = mark(glyphs.session, paint, "session");
|
|
162
|
+
return {
|
|
163
|
+
text: `${icon}${paint.role("session", fitWidth(name, 32, glyphs.ellipsis))}`,
|
|
164
|
+
compact: `${icon}${paint.role("session", fitWidth(name, 14, glyphs.ellipsis))}`,
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
version: {
|
|
169
|
+
priority: 10,
|
|
170
|
+
describe: "the pi version, so a report says which one it was",
|
|
171
|
+
render: ({ snapshot, glyphs, paint }) => ({
|
|
172
|
+
text: `${mark(glyphs.version, paint, "brand")}${paint.role("version", snapshot.version)}`,
|
|
173
|
+
}),
|
|
174
|
+
},
|
|
175
|
+
clock: {
|
|
176
|
+
priority: 15,
|
|
177
|
+
describe: "the time of day",
|
|
178
|
+
render: ({ snapshot, glyphs, paint }) => {
|
|
179
|
+
const now = new Date(snapshot.now);
|
|
180
|
+
const time = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
|
|
181
|
+
return { text: `${mark(glyphs.clock, paint, "clock")}${paint.role("clock", time)}` };
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
provider: {
|
|
185
|
+
priority: 40,
|
|
186
|
+
describe: "who serves the model, on its own",
|
|
187
|
+
render: ({ snapshot, paint }) =>
|
|
188
|
+
snapshot.model === undefined
|
|
189
|
+
? undefined
|
|
190
|
+
: { text: paint.role("provider", snapshot.model.provider) },
|
|
191
|
+
},
|
|
192
|
+
model: {
|
|
193
|
+
priority: 100,
|
|
194
|
+
describe: "the model doing the work, after its provider",
|
|
195
|
+
render: ({ snapshot, glyphs, paint }) => {
|
|
196
|
+
const icon = mark(glyphs.model, paint, "provider");
|
|
197
|
+
if (snapshot.model === undefined) return { text: `${icon}${paint.dim("no model")}` };
|
|
198
|
+
const name = paint.bold(paint.role("model", snapshot.model.name));
|
|
199
|
+
const provider = `${paint.role("provider", snapshot.model.provider)}${paint.dim("/")}`;
|
|
200
|
+
return { text: `${icon}${provider}${name}`, compact: `${icon}${name}` };
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
effort: {
|
|
204
|
+
// The frame is already painted in the effort colour, so the meter can go before the context.
|
|
205
|
+
priority: 80,
|
|
206
|
+
describe: "how hard the model thinks, as a meter that warms with the level",
|
|
207
|
+
render: ({ snapshot, glyphs, paint }) => {
|
|
208
|
+
if (snapshot.model?.reasoning !== true) return undefined;
|
|
209
|
+
const level = snapshot.thinking ?? "off";
|
|
210
|
+
const reached = EFFORT_LEVELS.indexOf(level as (typeof EFFORT_LEVELS)[number]);
|
|
211
|
+
const meter = EFFORT_LEVELS.map((step, index) => {
|
|
212
|
+
const bar = glyphs.levels[index] ?? "|";
|
|
213
|
+
return index <= reached ? paint.effort(step, bar) : paint.dim(bar);
|
|
214
|
+
}).join("");
|
|
215
|
+
const label = level === "off" ? paint.dim(level) : paint.effort(level, level);
|
|
216
|
+
return { text: `${meter} ${label}`, compact: meter };
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
context: {
|
|
220
|
+
priority: 88,
|
|
221
|
+
describe: "how full the context window is, with the tokens in it over the window",
|
|
222
|
+
render: ({ snapshot, glyphs, paint, options }) => {
|
|
223
|
+
const context = snapshot.context;
|
|
224
|
+
if (context === undefined) return undefined;
|
|
225
|
+
const glyph =
|
|
226
|
+
glyphs.context === "" ? "" : `${paint.stress(context.percent, glyphs.context)} `;
|
|
227
|
+
const icon = `${glyph}${word(options, glyphs.context === "ctx" ? "" : "ctx", paint)}`;
|
|
228
|
+
const value = paint.stress(context.percent, percent(context.percent));
|
|
229
|
+
const cells = gauge(context.percent, options.gaugeCells);
|
|
230
|
+
const bar =
|
|
231
|
+
options.gaugeCells <= 0
|
|
232
|
+
? ""
|
|
233
|
+
: ` ${paint.stress(context.percent, glyphs.fill.repeat(cells.filled))}${paint.dim(glyphs.empty.repeat(cells.empty))}`;
|
|
234
|
+
const size = paint.dim(` ${count(context.tokens)}/${count(context.window)}`);
|
|
235
|
+
return { text: `${icon}${value}${bar}${size}`, compact: `${icon}${value}` };
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
speed: {
|
|
239
|
+
priority: 60,
|
|
240
|
+
describe:
|
|
241
|
+
"how fast tokens flow in this answer: out ↓ is the model writing, in ↑ is it reading the prompt",
|
|
242
|
+
render: ({ snapshot, glyphs, paint, options }) => {
|
|
243
|
+
const request = snapshot.request;
|
|
244
|
+
if (request === undefined) return undefined;
|
|
245
|
+
// An estimate is drawn quieter until the provider says the real count; a `~` would change the width.
|
|
246
|
+
const out = speedCell(request.decode);
|
|
247
|
+
const value = request.estimated ? paint.muted(out) : paint.bold(paint.role("speed", out));
|
|
248
|
+
const down = `${flow("received", glyphs, paint, options)}${value}`;
|
|
249
|
+
const up = `${flow("sent", glyphs, paint, options)}${paint.role("sent", speedCell(request.prefill))}`;
|
|
250
|
+
const icon = mark(glyphs.speed, paint, "speed");
|
|
251
|
+
const unit = paint.dim(" tok/s");
|
|
252
|
+
return { text: `${icon}${down} ${up}${unit}`, compact: `${icon}${down}${unit}` };
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
wait: {
|
|
256
|
+
priority: 55,
|
|
257
|
+
describe: "time to first token (ttft): how long the model took to start answering",
|
|
258
|
+
render: ({ snapshot, glyphs, paint, options }) => {
|
|
259
|
+
const request = snapshot.request;
|
|
260
|
+
if (request === undefined) return undefined;
|
|
261
|
+
const ms = request.waiting ? request.elapsedMs : request.firstTokenMs;
|
|
262
|
+
const icon = glyphs.wait === "" ? "" : `${paint.role("wait", glyphs.wait)} `;
|
|
263
|
+
const value = cell(ms === undefined ? UNKNOWN : latency(ms), CELL.latency);
|
|
264
|
+
return {
|
|
265
|
+
text: `${icon}${word(options, "ttft", paint, glyphs.wait === "ttft")}${paint.role("wait", value)}`,
|
|
266
|
+
};
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
elapsed: {
|
|
270
|
+
priority: 65,
|
|
271
|
+
describe:
|
|
272
|
+
"how long the agent has worked on this prompt, and how many calls to the model it made",
|
|
273
|
+
render: ({ snapshot, glyphs, paint, options }) => {
|
|
274
|
+
const run = snapshot.run;
|
|
275
|
+
if (run === undefined) return undefined;
|
|
276
|
+
const role: Role = run.running ? "running" : "done";
|
|
277
|
+
const icon = mark(run.running ? glyphs.running : glyphs.done, paint, role);
|
|
278
|
+
// A stopwatch keeps its shape as it counts, so the line does not move while it ticks.
|
|
279
|
+
const time = paint.role(role, stopwatch(run.elapsedMs));
|
|
280
|
+
const calls =
|
|
281
|
+
run.requests <= 1
|
|
282
|
+
? ""
|
|
283
|
+
: paint.dim(options.labels ? ` · ${run.requests} calls` : ` ×${run.requests}`);
|
|
284
|
+
return { text: `${icon}${time}${calls}`, compact: `${icon}${time}` };
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
last: {
|
|
288
|
+
priority: 55,
|
|
289
|
+
describe:
|
|
290
|
+
"the last call to the model that finished: its wait for the first token and its writing speed",
|
|
291
|
+
render: ({ snapshot, glyphs, paint, options }) => {
|
|
292
|
+
const last = snapshot.last;
|
|
293
|
+
if (last === undefined) return undefined;
|
|
294
|
+
const parts: string[] = [];
|
|
295
|
+
if (last.firstTokenMs !== undefined) {
|
|
296
|
+
const wait = paint.role("wait", latency(last.firstTokenMs));
|
|
297
|
+
parts.push(
|
|
298
|
+
options.labels
|
|
299
|
+
? `${wait}${paint.dim(" wait")}`
|
|
300
|
+
: `${mark(glyphs.wait, paint, "wait")}${wait}`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
if (last.decode !== undefined) {
|
|
304
|
+
const speed = `${paint.role("received", glyphs.received)}${paint.role("speed", rate(last.decode))}`;
|
|
305
|
+
parts.push(`${speed}${paint.dim(" tok/s")}`);
|
|
306
|
+
}
|
|
307
|
+
if (parts.length === 0) return undefined;
|
|
308
|
+
const label = options.labels ? paint.dim("last call ") : "";
|
|
309
|
+
return { text: `${label}${parts.join(paint.dim(" · "))}`, compact: parts[0] };
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
request: {
|
|
313
|
+
priority: 30,
|
|
314
|
+
describe: "tokens of this answer: in ↑ is the prompt sent, out ↓ is what the model wrote",
|
|
315
|
+
render: ({ snapshot, glyphs, paint, options }) => {
|
|
316
|
+
const usage = snapshot.request?.usage;
|
|
317
|
+
if (usage === undefined) return undefined;
|
|
318
|
+
const sent = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
319
|
+
const up = `${flow("sent", glyphs, paint, options)}${tokenCell(sent)}`;
|
|
320
|
+
const down = `${flow("received", glyphs, paint, options)}${tokenCell(usage.output)}`;
|
|
321
|
+
// Both cache cells are always there, so the breakdown never appears in the middle of the line.
|
|
322
|
+
const cache = cacheParts(usage, glyphs, paint, options, true);
|
|
323
|
+
return {
|
|
324
|
+
text: `${up} ${paint.dim("(")}${cache}${paint.dim(")")} ${down}`,
|
|
325
|
+
compact: `${up} ${down}`,
|
|
326
|
+
};
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
costRate: {
|
|
330
|
+
priority: 26,
|
|
331
|
+
describe:
|
|
332
|
+
"what a million tokens cost in this answer, the number that says whether a model is cheap",
|
|
333
|
+
render: ({ snapshot, paint }) => {
|
|
334
|
+
const usage = snapshot.request?.usage;
|
|
335
|
+
if (usage === undefined) return undefined;
|
|
336
|
+
const tokens = tokensOf(usage);
|
|
337
|
+
if (usage.cost === 0 || tokens === 0) return undefined;
|
|
338
|
+
const perMillion = (usage.cost / tokens) * 1_000_000;
|
|
339
|
+
if (perMillion < 0.005) return undefined;
|
|
340
|
+
return {
|
|
341
|
+
text: `${paint.role("cost", `$${perMillion.toFixed(2)}`)}${paint.dim(" per Mtok")}`,
|
|
342
|
+
};
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
cost: {
|
|
346
|
+
priority: 75,
|
|
347
|
+
describe:
|
|
348
|
+
"what the session cost; `sub` means a subscription, so it is what the tokens would have cost",
|
|
349
|
+
render: ({ snapshot, glyphs, paint }) => {
|
|
350
|
+
const amount = snapshot.totals.cost;
|
|
351
|
+
if (amount === 0) return undefined;
|
|
352
|
+
const value = paint.money(amount, money(amount));
|
|
353
|
+
const sub = snapshot.subscription ? paint.dim(" sub") : "";
|
|
354
|
+
const icon = glyphs.cost === "$" ? "" : mark(glyphs.cost, paint, "cost");
|
|
355
|
+
return { text: `${icon}${value}${sub}`, compact: `${icon}${value}` };
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
tokens: {
|
|
359
|
+
priority: 35,
|
|
360
|
+
describe:
|
|
361
|
+
"tokens of the whole session: ↑ sent to the model, cache included, and ↓ what it wrote back",
|
|
362
|
+
render: ({ snapshot, glyphs, paint, options }) => {
|
|
363
|
+
const totals = snapshot.totals;
|
|
364
|
+
if (tokensOf(totals) === 0) return undefined;
|
|
365
|
+
// Everything sent, not just the uncached part: `66 in` next to a million cached reads is a lie.
|
|
366
|
+
const sent = `${paint.role("sent", glyphs.sent)}${paint.role("sent", count(promptOf(totals)))}`;
|
|
367
|
+
const got = `${paint.role("received", glyphs.received)}${paint.role("received", count(totals.output))}`;
|
|
368
|
+
if (!options.labels) return { text: `${sent} ${got}` };
|
|
369
|
+
return {
|
|
370
|
+
text: `${sent}${paint.dim(" in")} ${got}${paint.dim(" out")}`,
|
|
371
|
+
compact: `${sent} ${got}`,
|
|
372
|
+
};
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
cache: {
|
|
376
|
+
priority: 30,
|
|
377
|
+
describe:
|
|
378
|
+
"how much of the last prompt was read from the cache; `new` when it was only just written",
|
|
379
|
+
render: ({ snapshot, glyphs, paint, options }) => {
|
|
380
|
+
const hit = snapshot.cacheHit;
|
|
381
|
+
if (hit === undefined) return undefined;
|
|
382
|
+
const icon = mark(glyphs.cacheRead, paint, "cache");
|
|
383
|
+
// A prompt that was written to the cache and not read from it is the cache warming up, not a miss.
|
|
384
|
+
if (snapshot.cacheWarming)
|
|
385
|
+
return { text: `${icon}${paint.muted(options.labels ? "cache new" : "new")}` };
|
|
386
|
+
const value =
|
|
387
|
+
hit >= 90 ? paint.role("done", percent(hit)) : paint.stress(100 - hit, percent(hit));
|
|
388
|
+
return {
|
|
389
|
+
text: `${icon}${value}${options.labels ? paint.dim(" cached") : ""}`,
|
|
390
|
+
compact: `${icon}${value}`,
|
|
391
|
+
};
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
average: {
|
|
395
|
+
priority: 25,
|
|
396
|
+
describe:
|
|
397
|
+
"the session's average speeds: ↓ the model writing, ↑ it reading the prompt, cache included",
|
|
398
|
+
render: ({ snapshot, glyphs, paint }) => {
|
|
399
|
+
const { decode, prefill } = snapshot.averages;
|
|
400
|
+
if (decode === undefined && prefill === undefined) return undefined;
|
|
401
|
+
const down =
|
|
402
|
+
decode === undefined
|
|
403
|
+
? ""
|
|
404
|
+
: `${paint.role("received", glyphs.received)}${paint.role("speed", rate(decode))}`;
|
|
405
|
+
const up =
|
|
406
|
+
prefill === undefined
|
|
407
|
+
? ""
|
|
408
|
+
: `${paint.role("sent", glyphs.sent)}${paint.role("sent", rate(prefill))}`;
|
|
409
|
+
const label = paint.dim("avg ");
|
|
410
|
+
const unit = paint.dim(" tok/s");
|
|
411
|
+
return {
|
|
412
|
+
text: `${label}${[down, up].filter((part) => part !== "").join(" ")}${unit}`,
|
|
413
|
+
compact: down === "" ? undefined : `${label}${down}${unit}`,
|
|
414
|
+
};
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
statuses: {
|
|
418
|
+
priority: 50,
|
|
419
|
+
describe: "what other packages report through setStatus, minus the ones placed on their own",
|
|
420
|
+
render: ({ snapshot, glyphs, paint, options }) => {
|
|
421
|
+
const texts = [...snapshot.statuses]
|
|
422
|
+
.filter(([key]) => !options.claimed.has(key))
|
|
423
|
+
.map(([, text]) => text);
|
|
424
|
+
if (texts.length === 0) return undefined;
|
|
425
|
+
const icon = glyphs.plug === "" ? "" : `${paint.dim(glyphs.plug)} `;
|
|
426
|
+
return {
|
|
427
|
+
text: `${icon}${texts.join(paint.dim(" · "))}`,
|
|
428
|
+
compact: `${icon}${texts[0] ?? ""}`,
|
|
429
|
+
};
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
/** The pieces for one slot, in the order the slot lists them. */
|
|
435
|
+
export function renderSegments(ids: readonly SegmentId[], input: SegmentInput): Piece[] {
|
|
436
|
+
return ids.flatMap((id): Piece[] => {
|
|
437
|
+
if (id.startsWith(STATUS_PREFIX)) {
|
|
438
|
+
const text = input.snapshot.statuses.get(id.slice(STATUS_PREFIX.length));
|
|
439
|
+
return text === undefined ? [] : [{ text, priority: STATUS_PRIORITY }];
|
|
440
|
+
}
|
|
441
|
+
const segment = SEGMENTS[id as BuiltinSegment];
|
|
442
|
+
const rendered = segment.render(input);
|
|
443
|
+
return rendered === undefined ? [] : [{ ...rendered, priority: segment.priority }];
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function mark(glyph: string, paint: Paint, role: Role): string {
|
|
448
|
+
return glyph === "" ? "" : `${paint.role(role, glyph)} `;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function counted(value: number, glyph: string, role: Role, paint: Paint): string {
|
|
452
|
+
return value > 0 ? paint.role(role, `${glyph}${value}`) : "";
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Read from the cache, and written to it. With labels: `cache read 80k · wrote 1.3k`. `fixed` keeps both
|
|
457
|
+
* cells with a dash for zero, for a line that must not change shape.
|
|
458
|
+
*/
|
|
459
|
+
function cacheParts(
|
|
460
|
+
usage: Totals,
|
|
461
|
+
glyphs: Glyphs,
|
|
462
|
+
paint: Paint,
|
|
463
|
+
options: SegmentOptions,
|
|
464
|
+
fixed = false,
|
|
465
|
+
): string {
|
|
466
|
+
const value = (tokens: number) => (fixed ? tokenCell(tokens) : tokens > 0 ? count(tokens) : "");
|
|
467
|
+
const read = value(usage.cacheRead);
|
|
468
|
+
const write = value(usage.cacheWrite);
|
|
469
|
+
if (options.labels) {
|
|
470
|
+
const parts = [
|
|
471
|
+
read === "" ? "" : `${paint.dim("read ")}${paint.role("cache", read)}`,
|
|
472
|
+
write === "" ? "" : `${paint.dim("wrote ")}${paint.role("cache", write)}`,
|
|
473
|
+
].filter((part) => part !== "");
|
|
474
|
+
return parts.length === 0 ? "" : `${paint.dim("cache ")}${parts.join(paint.dim(" · "))}`;
|
|
475
|
+
}
|
|
476
|
+
return [
|
|
477
|
+
read === "" ? "" : `${paint.role("cache", glyphs.cacheRead)}${read}`,
|
|
478
|
+
write === "" ? "" : `${paint.role("cache", glyphs.cacheWrite)}${write}`,
|
|
479
|
+
]
|
|
480
|
+
.filter((part) => part !== "")
|
|
481
|
+
.join(" ");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* The direction of a flow of tokens, from where you sit: ↑ in, what is sent to the model, and ↓ out,
|
|
486
|
+
* what comes back. With labels the word follows the arrow.
|
|
487
|
+
*/
|
|
488
|
+
function flow(
|
|
489
|
+
role: "sent" | "received",
|
|
490
|
+
glyphs: Glyphs,
|
|
491
|
+
paint: Paint,
|
|
492
|
+
options: SegmentOptions,
|
|
493
|
+
): string {
|
|
494
|
+
const arrow = role === "sent" ? glyphs.sent : glyphs.received;
|
|
495
|
+
const name = role === "sent" ? "in" : "out";
|
|
496
|
+
return paint.role(role, options.labels ? `${arrow}${name} ` : arrow);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** A dim word in front of a value, when labels are on. `skip` when the glyph already is the word. */
|
|
500
|
+
function word(options: SegmentOptions, text: string, paint: Paint, skip = false): string {
|
|
501
|
+
return options.labels && !skip && text !== "" ? paint.dim(`${text} `) : "";
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function speedCell(value: number | undefined): string {
|
|
505
|
+
return cell(value === undefined ? UNKNOWN : rate(value), CELL.rate);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function tokenCell(value: number): string {
|
|
509
|
+
return cell(value === 0 ? UNKNOWN : count(value), CELL.count);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function pad(value: number): string {
|
|
513
|
+
return String(value).padStart(2, "0");
|
|
514
|
+
}
|