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