@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/README.md +22 -8
- package/assets/preview.png +0 -0
- package/package.json +2 -2
- package/src/data/git.ts +12 -9
- package/src/data/github.ts +141 -0
- package/src/data/runner.ts +25 -0
- package/src/data/snapshot.ts +6 -3
- package/src/data/start-notes.ts +108 -0
- package/src/data/telemetry.ts +6 -1
- package/src/data/totals.ts +12 -11
- package/src/data/visits.ts +38 -0
- package/src/index.ts +147 -71
- package/src/render/fit.ts +2 -1
- package/src/render/format.ts +9 -12
- package/src/render/glyphs.ts +14 -0
- package/src/render/segments.ts +30 -31
- package/src/render/streaming.ts +217 -0
- package/src/render/tools.ts +76 -12
- package/src/settings.ts +61 -210
- package/src/ui/editor.ts +8 -2
- package/src/ui/footer.ts +64 -5
- package/src/ui/header.ts +175 -71
- package/src/ui/outputs.ts +173 -0
- package/src/ui/pi-entries.ts +185 -0
- package/src/ui/previews.ts +76 -0
- package/src/ui/queue.ts +75 -0
- package/src/ui/screen.ts +8 -0
- package/src/ui/tool-frame.ts +118 -39
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
|
|
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 {
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
101
|
+
const glyphs = glyphsOf(() => icons.get(scope));
|
|
102
|
+
const screen = createScreen(scope, live, reader, telemetry, glyphs);
|
|
96
103
|
registerToolFrames(scope);
|
|
97
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
216
|
+
setActivity(live, { kind: "thinking" });
|
|
176
217
|
} else if (update.type === "text_delta" || update.type === "text_start") {
|
|
177
|
-
setActivity(live,
|
|
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,
|
|
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,
|
|
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: "
|
|
255
|
+
section: "Appearance",
|
|
215
256
|
label: "What is on screen",
|
|
216
|
-
description: "Every piece the
|
|
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
|
-
|
|
238
|
-
|
|
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",
|
|
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
|
-
|
|
272
|
-
|
|
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
|
|
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: () =>
|
|
343
|
+
separator: () => SEPARATOR,
|
|
306
344
|
options: () => ({
|
|
307
|
-
pathLength
|
|
308
|
-
gaugeCells
|
|
309
|
-
labels
|
|
310
|
-
claimed: claimedStatuses(Object.values(
|
|
345
|
+
pathLength,
|
|
346
|
+
gaugeCells,
|
|
347
|
+
labels,
|
|
348
|
+
claimed: claimedStatuses(Object.values(SLOTS)),
|
|
311
349
|
}),
|
|
312
|
-
slot: (name) =>
|
|
350
|
+
slot: (name) => SLOTS[name],
|
|
313
351
|
frameStyle: () => frame.get(scope),
|
|
314
|
-
cursor: () => cursor
|
|
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
|
|
386
|
+
applyCursor(tui, cursor);
|
|
349
387
|
return new LookEditor(tui, theme, keybindings, screen);
|
|
350
388
|
});
|
|
351
389
|
}
|
|
352
390
|
|
|
353
|
-
function installHeader(
|
|
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(
|
|
362
|
-
|
|
363
|
-
|
|
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,
|
|
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
|
|
package/src/render/format.ts
CHANGED
|
@@ -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
|
-
|
|
21
|
-
|
|
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
|
|
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 =
|
package/src/render/glyphs.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/render/segments.ts
CHANGED
|
@@ -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
|
|
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
|
-
"
|
|
348
|
-
render: ({ snapshot, glyphs, paint
|
|
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
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return { text: `${icon}${
|
|
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
|
-
|
|
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}${
|
|
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 =
|
|
457
|
-
const got =
|
|
458
|
-
|
|
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: `${
|
|
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
|
-
|
|
480
|
-
|
|
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,
|
|
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)
|
|
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
|
-
}
|