@adeildo/pi-look 4.1.0 → 5.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/README.md +30 -32
- package/package.json +2 -2
- package/src/data/git.ts +0 -6
- package/src/data/snapshot.ts +0 -13
- package/src/data/telemetry.ts +116 -63
- package/src/data/totals.ts +0 -14
- package/src/desktop/palette.ts +0 -14
- package/src/desktop/sync.ts +1 -8
- package/src/index.ts +42 -61
- package/src/render/fit.ts +0 -10
- package/src/render/format.ts +0 -17
- package/src/render/frame.ts +0 -12
- package/src/render/glyphs.ts +0 -9
- package/src/render/paint.ts +0 -16
- package/src/render/segments.ts +27 -35
- package/src/settings.ts +54 -22
- package/src/ui/editor.ts +2 -15
- package/src/ui/footer.ts +0 -9
- package/src/ui/header.ts +0 -8
- package/src/ui/screen.ts +0 -5
- package/src/ui/working.ts +9 -57
package/src/index.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// The look: pi's start screen, editor, footer and working line, redrawn around the data I want in
|
|
2
|
-
// front of me, in the colours of whatever theme is on. One feature, because the pieces share one clock
|
|
3
|
-
// and one git probe; each piece has its own switch in the settings.
|
|
4
1
|
import { createApp, defineFeature, type FeatureScope } from "@adeildo/pi-kit";
|
|
5
2
|
import type {
|
|
6
3
|
ExtensionAPI,
|
|
@@ -12,6 +9,7 @@ import type { TUI } from "@earendil-works/pi-tui";
|
|
|
12
9
|
import { type GitState, readGit } from "./data/git.ts";
|
|
13
10
|
import { emptySnapshot, type Snapshot, SnapshotReader } from "./data/snapshot.ts";
|
|
14
11
|
import { Telemetry } from "./data/telemetry.ts";
|
|
12
|
+
import type { DeltaKind } from "./data/telemetry.ts";
|
|
15
13
|
import { REQUEST_ENTRY, type RequestRecord } from "./data/totals.ts";
|
|
16
14
|
import { DESKTOP_THEME } from "./desktop/palette.ts";
|
|
17
15
|
import { syncDesktopTheme, watchDesktopPalette } from "./desktop/sync.ts";
|
|
@@ -34,6 +32,7 @@ import {
|
|
|
34
32
|
pathLength,
|
|
35
33
|
peek,
|
|
36
34
|
separator,
|
|
35
|
+
SECTIONS,
|
|
37
36
|
SEPARATORS,
|
|
38
37
|
strip,
|
|
39
38
|
topLeft,
|
|
@@ -43,7 +42,7 @@ import { applyCursor, LookEditor } from "./ui/editor.ts";
|
|
|
43
42
|
import { FooterComponent, StripComponent } from "./ui/footer.ts";
|
|
44
43
|
import { HeaderComponent, type LoadedCounts } from "./ui/header.ts";
|
|
45
44
|
import type { Screen, SlotName } from "./ui/screen.ts";
|
|
46
|
-
import { type Activity,
|
|
45
|
+
import { type Activity, describe, draftedTool } from "./ui/working.ts";
|
|
47
46
|
|
|
48
47
|
export { SEGMENT_IDS, SEGMENTS, type SegmentId } from "./render/segments.ts";
|
|
49
48
|
export { ROLE_TOKENS } from "./render/paint.ts";
|
|
@@ -51,8 +50,6 @@ export { desktopTheme, parseDesktopColors } from "./desktop/palette.ts";
|
|
|
51
50
|
|
|
52
51
|
const STRIP_WIDGET = "pi-look:strip";
|
|
53
52
|
const TICK_MS = 500;
|
|
54
|
-
/** The thinking tail changes on every token; the spinner line does not need to. */
|
|
55
|
-
const PEEK_MS = 120;
|
|
56
53
|
|
|
57
54
|
interface Live {
|
|
58
55
|
ctx: ExtensionContext | undefined;
|
|
@@ -63,16 +60,15 @@ interface Live {
|
|
|
63
60
|
probing: boolean;
|
|
64
61
|
ticker: ReturnType<typeof setInterval> | undefined;
|
|
65
62
|
stopWatching: (() => void) | undefined;
|
|
66
|
-
/** The frame's snapshot, shared by every component drawn in the same frame. */
|
|
67
63
|
frame: Snapshot | undefined;
|
|
68
64
|
activity: string | undefined;
|
|
69
|
-
peekAt: number;
|
|
70
65
|
}
|
|
71
66
|
|
|
72
67
|
export const look = defineFeature({
|
|
73
68
|
id: "look",
|
|
74
69
|
description:
|
|
75
70
|
"The start screen, a framed editor, the footer and a working line that says what runs",
|
|
71
|
+
sections: SECTIONS,
|
|
76
72
|
settings: LOOK_SETTINGS,
|
|
77
73
|
setup(scope) {
|
|
78
74
|
const telemetry = new Telemetry();
|
|
@@ -88,7 +84,6 @@ export const look = defineFeature({
|
|
|
88
84
|
stopWatching: undefined,
|
|
89
85
|
frame: undefined,
|
|
90
86
|
activity: undefined,
|
|
91
|
-
peekAt: 0,
|
|
92
87
|
};
|
|
93
88
|
|
|
94
89
|
const screen = createScreen(scope, live, reader, telemetry);
|
|
@@ -144,6 +139,11 @@ export const look = defineFeature({
|
|
|
144
139
|
live.tui = undefined;
|
|
145
140
|
});
|
|
146
141
|
|
|
142
|
+
// These two fire for the calls the agent makes, and not for pi's cache warming, which calls the
|
|
143
|
+
// provider on its own: the marks always belong to the answer being measured.
|
|
144
|
+
scope.on("before_provider_request", () => telemetry.providerRequestSent());
|
|
145
|
+
scope.on("after_provider_response", () => telemetry.providerResponseArrived());
|
|
146
|
+
|
|
147
147
|
scope.on("agent_start", () => {
|
|
148
148
|
telemetry.runStarted();
|
|
149
149
|
setActivity(live, scope, { kind: "waiting" });
|
|
@@ -164,39 +164,31 @@ export const look = defineFeature({
|
|
|
164
164
|
if (message.role !== "assistant") return;
|
|
165
165
|
const update = event.assistantMessageEvent;
|
|
166
166
|
const delta = "delta" in update ? update.delta : "";
|
|
167
|
-
|
|
167
|
+
const kind: DeltaKind = update.type.startsWith("thinking") ? "thinking" : "writing";
|
|
168
|
+
telemetry.answerGrew(delta, message.usage, kind);
|
|
168
169
|
|
|
169
|
-
if (update.type === "thinking_delta") {
|
|
170
|
-
|
|
171
|
-
if (now - live.peekAt < PEEK_MS) return;
|
|
172
|
-
live.peekAt = now;
|
|
173
|
-
setActivity(live, scope, { kind: "thinking", tail: thinkingTail(message.content) });
|
|
170
|
+
if (update.type === "thinking_start" || update.type === "thinking_delta") {
|
|
171
|
+
setActivity(live, scope, { kind: "thinking" });
|
|
174
172
|
} else if (update.type === "text_delta" || update.type === "text_start") {
|
|
175
173
|
setActivity(live, scope, { kind: "writing" });
|
|
176
174
|
} else if (update.type === "toolcall_start" || update.type === "toolcall_delta") {
|
|
177
|
-
const tool =
|
|
178
|
-
if (tool !== undefined) setActivity(live, scope, { kind: "
|
|
175
|
+
const tool = draftedTool(message.content);
|
|
176
|
+
if (tool !== undefined) setActivity(live, scope, { kind: "drafting", tool });
|
|
179
177
|
}
|
|
180
178
|
});
|
|
181
179
|
|
|
182
180
|
scope.on("message_end", (event) => {
|
|
183
181
|
if (event.message.role !== "assistant") return;
|
|
184
182
|
const record = telemetry.answerEnded(event.message.usage);
|
|
185
|
-
//
|
|
186
|
-
// duration, which pi does not record.
|
|
183
|
+
// Pi records usage without a duration, so the session keeps it for the speeds after a resume.
|
|
187
184
|
if (record !== undefined) scope.appendEntry<RequestRecord>(REQUEST_ENTRY, record);
|
|
188
185
|
repaint();
|
|
189
186
|
});
|
|
190
187
|
|
|
191
188
|
scope.on("tool_execution_start", (event) => {
|
|
192
|
-
setActivity(live, scope, {
|
|
193
|
-
kind: "running",
|
|
194
|
-
tool: event.toolName,
|
|
195
|
-
detail: toolDetail(event.args),
|
|
196
|
-
});
|
|
189
|
+
setActivity(live, scope, { kind: "running", tool: event.toolName });
|
|
197
190
|
});
|
|
198
191
|
|
|
199
|
-
// A command may have moved the branch or touched the tree.
|
|
200
192
|
scope.on("tool_execution_end", () => void probeGit(live));
|
|
201
193
|
|
|
202
194
|
scope.on("agent_end", () => {
|
|
@@ -213,41 +205,35 @@ export const look = defineFeature({
|
|
|
213
205
|
scope.on("session_compact", repaint);
|
|
214
206
|
scope.on("session_tree", repaint);
|
|
215
207
|
|
|
216
|
-
scope.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
ctx.ui.notify("usage: /look explain | /look theme", "info");
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
const result = await syncDesktopTheme(desktopSource.get(scope));
|
|
234
|
-
if (result.kind === "missing" || result.kind === "invalid") {
|
|
235
|
-
const why = result.kind === "missing" ? "no palette file" : result.reason;
|
|
236
|
-
ctx.ui.notify(`look: cannot build the desktop theme: ${why}`, "warning");
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
const switched = ctx.ui.setTheme(DESKTOP_THEME);
|
|
240
|
-
ctx.ui.notify(
|
|
241
|
-
switched.success
|
|
242
|
-
? `look: now on the ${DESKTOP_THEME} theme, from ${desktopSource.get(scope)}`
|
|
243
|
-
: `look: wrote ${result.path}, but pi did not load it: ${switched.error ?? "unknown"}`,
|
|
244
|
-
switched.success ? "info" : "warning",
|
|
245
|
-
);
|
|
246
|
-
},
|
|
208
|
+
scope.screen.action({
|
|
209
|
+
id: "explain",
|
|
210
|
+
section: "Slots",
|
|
211
|
+
label: "What is on screen",
|
|
212
|
+
description: "Every piece the slots show right now, and what it means.",
|
|
213
|
+
run: () => explain(screen),
|
|
214
|
+
});
|
|
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),
|
|
247
222
|
});
|
|
248
223
|
},
|
|
249
224
|
});
|
|
250
225
|
|
|
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
|
+
|
|
251
237
|
const SLOT_TITLES: Record<SlotName, string> = {
|
|
252
238
|
strip: "Above the editor, this answer",
|
|
253
239
|
topLeft: "Frame, top left",
|
|
@@ -257,7 +243,6 @@ const SLOT_TITLES: Record<SlotName, string> = {
|
|
|
257
243
|
below: "Below the editor, the session",
|
|
258
244
|
};
|
|
259
245
|
|
|
260
|
-
/** Every piece on screen, where it is, what it shows now and what it means. */
|
|
261
246
|
function explain(screen: Screen): string {
|
|
262
247
|
const snapshot = screen.snapshot();
|
|
263
248
|
const paint = screen.paint();
|
|
@@ -309,7 +294,6 @@ function createScreen(
|
|
|
309
294
|
snapshot() {
|
|
310
295
|
const ctx = live.ctx;
|
|
311
296
|
if (ctx === undefined) return emptySnapshot();
|
|
312
|
-
// Every component of one frame renders in the same tick, so they share one read.
|
|
313
297
|
if (live.frame !== undefined) return live.frame;
|
|
314
298
|
const snapshot = reader.read(ctx, {
|
|
315
299
|
branch: live.footerData?.getGitBranch() ?? null,
|
|
@@ -373,7 +357,6 @@ function installHeader(ctx: ExtensionContext, screen: Screen, scope: FeatureScop
|
|
|
373
357
|
);
|
|
374
358
|
}
|
|
375
359
|
|
|
376
|
-
/** What loaded, from pi's own lists: active tools, skill and prompt commands, extension files. */
|
|
377
360
|
function loadedCounts(pi: ExtensionAPI): LoadedCounts {
|
|
378
361
|
const commands = pi.getCommands();
|
|
379
362
|
const extensions = new Set<string>();
|
|
@@ -417,7 +400,6 @@ function setActivity(live: Live, scope: FeatureScope, activity: Activity): void
|
|
|
417
400
|
live.ctx?.ui.setWorkingMessage(message);
|
|
418
401
|
}
|
|
419
402
|
|
|
420
|
-
/** Only one at a time, and never while a render is waiting on it. */
|
|
421
403
|
async function probeGit(live: Live): Promise<void> {
|
|
422
404
|
const ctx = live.ctx;
|
|
423
405
|
if (ctx === undefined || live.probing) return;
|
|
@@ -430,7 +412,6 @@ async function probeGit(live: Live): Promise<void> {
|
|
|
430
412
|
}
|
|
431
413
|
}
|
|
432
414
|
|
|
433
|
-
/** The clocks have to move on their own while the model thinks and nothing else happens. */
|
|
434
415
|
function startTicking(live: Live): void {
|
|
435
416
|
stopTicking(live);
|
|
436
417
|
live.ticker = setInterval(() => live.repaint?.(), TICK_MS);
|
package/src/render/fit.ts
CHANGED
|
@@ -1,20 +1,13 @@
|
|
|
1
|
-
// Making pieces fit. Every piece has a priority and may have a shorter form. When a line is too wide,
|
|
2
|
-
// the least important pieces shorten first, then leave, one at a time, until it fits. Regions that
|
|
3
|
-
// share a line (the left and right of a border) compete for the same width, so an unimportant piece on
|
|
4
|
-
// the right leaves before an important one on the left.
|
|
5
1
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
6
2
|
|
|
7
3
|
export interface Piece {
|
|
8
4
|
text: string;
|
|
9
|
-
/** A shorter form, used before the piece is dropped. */
|
|
10
5
|
compact?: string | undefined;
|
|
11
|
-
/** Higher stays longer. */
|
|
12
6
|
priority: number;
|
|
13
7
|
}
|
|
14
8
|
|
|
15
9
|
export interface FitOptions {
|
|
16
10
|
separator: string;
|
|
17
|
-
/** Width a region costs on top of its pieces when it has any, like the padding around it. */
|
|
18
11
|
regionOverhead?: number;
|
|
19
12
|
ellipsis?: string;
|
|
20
13
|
}
|
|
@@ -27,7 +20,6 @@ interface Slot {
|
|
|
27
20
|
dropped: boolean;
|
|
28
21
|
}
|
|
29
22
|
|
|
30
|
-
/** One string per region, each the kept pieces joined by the separator. */
|
|
31
23
|
export function fitRegions(
|
|
32
24
|
regions: readonly (readonly Piece[])[],
|
|
33
25
|
width: number,
|
|
@@ -53,7 +45,6 @@ export function fitRegions(
|
|
|
53
45
|
const joined = join(regions.length, slots, options.separator);
|
|
54
46
|
if (measure() <= width) return joined;
|
|
55
47
|
|
|
56
|
-
// One piece left and still too wide: cut it, keeping the overhead of its region.
|
|
57
48
|
const overhead = options.regionOverhead ?? 0;
|
|
58
49
|
return joined.map((text) =>
|
|
59
50
|
text === ""
|
|
@@ -62,7 +53,6 @@ export function fitRegions(
|
|
|
62
53
|
);
|
|
63
54
|
}
|
|
64
55
|
|
|
65
|
-
/** A single run of pieces, as one string. */
|
|
66
56
|
export function fitLine(pieces: readonly Piece[], width: number, options: FitOptions): string {
|
|
67
57
|
return fitRegions([pieces], width, options)[0] ?? "";
|
|
68
58
|
}
|
package/src/render/format.ts
CHANGED
|
@@ -1,31 +1,22 @@
|
|
|
1
|
-
// Numbers as a person reads them at a glance. Pure, so every rule here has a test.
|
|
2
1
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
2
|
|
|
4
3
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
5
4
|
|
|
6
|
-
/**
|
|
7
|
-
* The widest each kind of number gets in normal use. A value is padded to its cell, so a number that
|
|
8
|
-
* ticks from `9.8` to `114` to `2.8k` does not push the rest of the line around.
|
|
9
|
-
*/
|
|
10
5
|
export const CELL = {
|
|
11
6
|
count: 4,
|
|
12
7
|
rate: 4,
|
|
13
8
|
latency: 5,
|
|
14
|
-
/** `0:08` to `9:59`, then one more for `59:59`. */
|
|
15
9
|
clock: 4,
|
|
16
10
|
percent: 4,
|
|
17
11
|
money: 6,
|
|
18
12
|
} as const;
|
|
19
13
|
|
|
20
|
-
/** Right-aligned in a cell of `width` columns. Longer values keep their length. */
|
|
21
14
|
export function cell(text: string, width: number): string {
|
|
22
15
|
return " ".repeat(Math.max(0, width - visibleWidth(text))) + text;
|
|
23
16
|
}
|
|
24
17
|
|
|
25
|
-
/** What a cell shows before its value is known, so the cell is there from the start. */
|
|
26
18
|
export const UNKNOWN = "–";
|
|
27
19
|
|
|
28
|
-
/** `1.2k`, `43k`, `1.2M`, `22M`, the way pi writes token counts. */
|
|
29
20
|
export function count(value: number): string {
|
|
30
21
|
const n = Math.max(0, Math.round(value));
|
|
31
22
|
if (n < 1000) return String(n);
|
|
@@ -35,13 +26,11 @@ export function count(value: number): string {
|
|
|
35
26
|
return `${Math.round(n / 1_000_000)}M`;
|
|
36
27
|
}
|
|
37
28
|
|
|
38
|
-
/** A speed: `42`, `906`, `1.2k`, `48k` tokens per second. */
|
|
39
29
|
export function rate(perSecond: number): string {
|
|
40
30
|
if (perSecond < 10) return trim(perSecond.toFixed(1));
|
|
41
31
|
return count(perSecond);
|
|
42
32
|
}
|
|
43
33
|
|
|
44
|
-
/** `2m 17s`, `1h 4m`, for how long something has been running. */
|
|
45
34
|
export function duration(milliseconds: number): string {
|
|
46
35
|
const total = Math.max(0, Math.floor(milliseconds / 1000));
|
|
47
36
|
const secs = total % 60;
|
|
@@ -52,7 +41,6 @@ export function duration(milliseconds: number): string {
|
|
|
52
41
|
return `${secs}s`;
|
|
53
42
|
}
|
|
54
43
|
|
|
55
|
-
/** `0:08`, `2:17`, `1:04:09`: a stopwatch, which keeps its shape as it counts. */
|
|
56
44
|
export function stopwatch(milliseconds: number): string {
|
|
57
45
|
const total = Math.max(0, Math.floor(milliseconds / 1000));
|
|
58
46
|
const secs = String(total % 60).padStart(2, "0");
|
|
@@ -61,14 +49,12 @@ export function stopwatch(milliseconds: number): string {
|
|
|
61
49
|
return hours > 0 ? `${hours}:${String(minutes).padStart(2, "0")}:${secs}` : `${minutes}:${secs}`;
|
|
62
50
|
}
|
|
63
51
|
|
|
64
|
-
/** `320ms`, `1.7s`, `12s`, for latencies, where the fraction matters under ten seconds. */
|
|
65
52
|
export function latency(milliseconds: number): string {
|
|
66
53
|
if (milliseconds < 1000) return `${Math.round(milliseconds)}ms`;
|
|
67
54
|
if (milliseconds < 10_000) return `${(milliseconds / 1000).toFixed(1)}s`;
|
|
68
55
|
return duration(milliseconds);
|
|
69
56
|
}
|
|
70
57
|
|
|
71
|
-
/** Three decimals while it is cents, two once it is dollars. */
|
|
72
58
|
export function money(amount: number): string {
|
|
73
59
|
return amount >= 10 ? `$${amount.toFixed(2)}` : `$${amount.toFixed(3)}`;
|
|
74
60
|
}
|
|
@@ -77,7 +63,6 @@ export function percent(value: number): string {
|
|
|
77
63
|
return value >= 10 || value === 0 ? `${Math.round(value)}%` : `${value.toFixed(1)}%`;
|
|
78
64
|
}
|
|
79
65
|
|
|
80
|
-
/** `~` for the home folder, and the tail kept when it is long, since the tail says where you are. */
|
|
81
66
|
export function shortenPath(cwd: string, home: string | undefined, maxWidth: number): string {
|
|
82
67
|
const full = home === undefined ? cwd : underHome(cwd, home);
|
|
83
68
|
if (maxWidth <= 0 || visibleWidth(full) <= maxWidth) return full;
|
|
@@ -97,13 +82,11 @@ export function basename(cwd: string): string {
|
|
|
97
82
|
return cwd.split(/[\\/]/).findLast(Boolean) ?? cwd;
|
|
98
83
|
}
|
|
99
84
|
|
|
100
|
-
/** A gauge of `cells`, filled to `value` out of 100. */
|
|
101
85
|
export function gauge(value: number, cells: number): { filled: number; empty: number } {
|
|
102
86
|
const filled = Math.max(0, Math.min(cells, Math.round((value / 100) * cells)));
|
|
103
87
|
return { filled, empty: cells - filled };
|
|
104
88
|
}
|
|
105
89
|
|
|
106
|
-
/** Clip to the start, keeping the end: for the tail of a thought, which is the newest part. */
|
|
107
90
|
export function truncateTail(text: string, maxWidth: number): string {
|
|
108
91
|
if (maxWidth <= 0) return "";
|
|
109
92
|
if (visibleWidth(text) <= maxWidth) return text;
|
package/src/render/frame.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// Borders with something written in them: `╭─ left ────── right ─╮`. The frame is the one place
|
|
2
|
-
// that knows the box characters, so a new style is one entry here.
|
|
3
1
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
4
2
|
|
|
5
3
|
import { type FitOptions, fitRegions, type Piece } from "./fit.ts";
|
|
@@ -40,7 +38,6 @@ export const BOXES: Record<FrameStyle, BoxChars> = {
|
|
|
40
38
|
horizontal: "━",
|
|
41
39
|
vertical: "┃",
|
|
42
40
|
},
|
|
43
|
-
// Only the horizontal rules, the way pi draws its editor, with the data written into them.
|
|
44
41
|
line: {
|
|
45
42
|
topLeft: "─",
|
|
46
43
|
topRight: "─",
|
|
@@ -54,23 +51,14 @@ export const BOXES: Record<FrameStyle, BoxChars> = {
|
|
|
54
51
|
export interface BorderOptions extends Pick<FitOptions, "separator" | "ellipsis"> {
|
|
55
52
|
box: BoxChars;
|
|
56
53
|
edge: "top" | "bottom";
|
|
57
|
-
/** Paints the rule and the corners. */
|
|
58
54
|
paint: (text: string) => string;
|
|
59
|
-
/**
|
|
60
|
-
* Text that follows the left pieces and takes whatever room is left, like the working spinner.
|
|
61
|
-
* It goes after them because it changes width all the time, and the pieces must not move.
|
|
62
|
-
*/
|
|
63
55
|
lead?: ((budget: number) => string | undefined) | undefined;
|
|
64
|
-
/** Room kept for the lead, so the pieces never squeeze it out. */
|
|
65
56
|
leadMin?: number;
|
|
66
57
|
}
|
|
67
58
|
|
|
68
|
-
/** Corners, one rule cell each side, and one rule cell between the two regions at least. */
|
|
69
59
|
const FIXED = 5;
|
|
70
|
-
/** A space on each side of a region's text. */
|
|
71
60
|
const PADDING = 2;
|
|
72
61
|
|
|
73
|
-
/** One border line of exactly `width` columns, with both regions fitted into it. */
|
|
74
62
|
export function border(
|
|
75
63
|
width: number,
|
|
76
64
|
left: readonly Piece[],
|
package/src/render/glyphs.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// One glyph per kind of data, in three sets. Nerd Font glyphs live in the private use area, so they
|
|
2
|
-
// are written as escapes: a font without them shows a box, and a reviewer sees which code point it is.
|
|
3
1
|
export type IconMode = "auto" | "nerd" | "unicode" | "ascii";
|
|
4
2
|
export type IconSet = Exclude<IconMode, "auto">;
|
|
5
3
|
|
|
@@ -31,10 +29,8 @@ export interface Glyphs {
|
|
|
31
29
|
untracked: string;
|
|
32
30
|
conflicted: string;
|
|
33
31
|
stashed: string;
|
|
34
|
-
/** Full and empty cells of a gauge. Blocks, so the gauge never reads as part of a border rule. */
|
|
35
32
|
fill: string;
|
|
36
33
|
empty: string;
|
|
37
|
-
/** Six rising bars, one per effort level from minimal to max. */
|
|
38
34
|
levels: readonly string[];
|
|
39
35
|
ellipsis: string;
|
|
40
36
|
}
|
|
@@ -77,7 +73,6 @@ export const NERD: Glyphs = {
|
|
|
77
73
|
ellipsis: "…",
|
|
78
74
|
};
|
|
79
75
|
|
|
80
|
-
/** Symbols every monospace font with decent Unicode coverage has, all one column wide. */
|
|
81
76
|
export const UNICODE: Glyphs = {
|
|
82
77
|
...COMMON,
|
|
83
78
|
set: "unicode",
|
|
@@ -141,10 +136,6 @@ export const ASCII: Glyphs = {
|
|
|
141
136
|
|
|
142
137
|
const SETS: Record<IconSet, Glyphs> = { nerd: NERD, unicode: UNICODE, ascii: ASCII };
|
|
143
138
|
|
|
144
|
-
/**
|
|
145
|
-
* Auto trusts a local UTF-8 terminal to have a Nerd Font. Over SSH the font lives on the other
|
|
146
|
-
* machine, so it drops to plain Unicode, and a terminal that does not speak UTF-8 gets ASCII.
|
|
147
|
-
*/
|
|
148
139
|
export function resolveIcons(mode: IconMode, env: NodeJS.ProcessEnv = process.env): Glyphs {
|
|
149
140
|
if (mode !== "auto") return SETS[mode];
|
|
150
141
|
if (env.TERM === "dumb") return ASCII;
|
package/src/render/paint.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
// Colour, without a single colour value. Every role names a token of the active pi theme, so the look
|
|
2
|
-
// follows whatever theme is on: the built-in ones, a custom JSON, or the desktop palette this package
|
|
3
|
-
// writes from matugen. Change the theme and every piece moves with it.
|
|
4
1
|
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
5
2
|
|
|
6
|
-
/** What a piece of text is. The table below says which theme token paints it. */
|
|
7
3
|
export type Role =
|
|
8
4
|
| "path"
|
|
9
5
|
| "branch"
|
|
@@ -29,10 +25,6 @@ export type Role =
|
|
|
29
25
|
| "clock"
|
|
30
26
|
| "brand";
|
|
31
27
|
|
|
32
|
-
/**
|
|
33
|
-
* The palette, as theme tokens. Syntax tokens are borrowed on purpose: a theme spends its hues there,
|
|
34
|
-
* so they give the line variety that stays in the theme's family.
|
|
35
|
-
*/
|
|
36
28
|
export const ROLE_TOKENS: Record<Role, ThemeColor> = {
|
|
37
29
|
path: "accent",
|
|
38
30
|
branch: "syntaxFunction",
|
|
@@ -59,7 +51,6 @@ export const ROLE_TOKENS: Record<Role, ThemeColor> = {
|
|
|
59
51
|
brand: "accent",
|
|
60
52
|
};
|
|
61
53
|
|
|
62
|
-
/** Pi gives every effort level its own token, which a theme draws as a ramp from cool to hot. */
|
|
63
54
|
const EFFORT_TOKENS: Record<string, ThemeColor> = {
|
|
64
55
|
off: "thinkingOff",
|
|
65
56
|
minimal: "thinkingMinimal",
|
|
@@ -73,21 +64,15 @@ const EFFORT_TOKENS: Record<string, ThemeColor> = {
|
|
|
73
64
|
export const EFFORT_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
74
65
|
|
|
75
66
|
export interface Paint {
|
|
76
|
-
/** Separators, units and labels sit back, so the data reads first. */
|
|
77
67
|
dim(text: string): string;
|
|
78
|
-
/** Quieter data. */
|
|
79
68
|
muted(text: string): string;
|
|
80
69
|
text(text: string): string;
|
|
81
70
|
bold(text: string): string;
|
|
82
71
|
italic(text: string): string;
|
|
83
72
|
role(role: Role, text: string): string;
|
|
84
|
-
/** The colour of one effort level. */
|
|
85
73
|
effort(level: string, text: string): string;
|
|
86
|
-
/** Calm under half, warm past 70%, alarming past 90%. */
|
|
87
74
|
stress(percent: number, text: string): string;
|
|
88
|
-
/** Money reads calm until it adds up. */
|
|
89
75
|
money(amount: number, text: string): string;
|
|
90
|
-
/** The editor frame, which pi recolours with the effort level and for bash mode. */
|
|
91
76
|
frame(text: string): string;
|
|
92
77
|
}
|
|
93
78
|
|
|
@@ -118,7 +103,6 @@ function moneyToken(amount: number): ThemeColor {
|
|
|
118
103
|
return ROLE_TOKENS.cost;
|
|
119
104
|
}
|
|
120
105
|
|
|
121
|
-
/** Used by tests and by anything drawing without a theme. */
|
|
122
106
|
export const PLAIN: Paint = {
|
|
123
107
|
dim: identity,
|
|
124
108
|
muted: identity,
|