@adeildo/pi-look 4.0.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/README.md
CHANGED
|
@@ -20,24 +20,24 @@ It also comes in [`@adeildo/pi-harness`](../harness), with the rest of the piece
|
|
|
20
20
|
|
|
21
21
|
**The strip above the editor** is calm on purpose: nothing in it moves while the model streams except the stopwatch.
|
|
22
22
|
|
|
23
|
-
| Piece
|
|
24
|
-
|
|
|
25
|
-
| `0:12
|
|
26
|
-
| `last call 1.7s wait
|
|
23
|
+
| Piece | Means |
|
|
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, never a partial count |
|
|
27
27
|
|
|
28
|
-
The
|
|
28
|
+
The wait runs from the moment the request leaves, which pi reports on `before_provider_request`, to the first token of any kind. `thought` is the part of that wait spent thinking before the first word, and only appears when the model thought. The pieces that split it further, `server` (the request leaving to the response headers, which is the provider's own latency) and `prefill` (the headers to the first token, the model reading the prompt), go in any slot. So do the live pieces, which move with every token: `speed`, `wait`, `request` and `costRate`.
|
|
29
29
|
|
|
30
|
-
The arrows are from where you sit: `↑ in` is what goes to the model, `↓ out` is what comes back.
|
|
30
|
+
The arrows are from where you sit: `↑ in` is what goes to the model, `↓ out` is what comes back. `What is on screen`, in the Slots section of the settings (`Alt+S`), lists every piece with its current value and what it means.
|
|
31
31
|
|
|
32
32
|
**The frame** has four slots. By default: the branch with its distance and changes on the top left, after the spinner; the folder and the machine on the top right; the permission mode, the model and the effort meter on the bottom left; the context on the bottom right. The border is drawn in pi's own effort colour, so it warms up as the model thinks harder, and turns to the bash colour in shell mode.
|
|
33
33
|
|
|
34
|
-
**The spinner**
|
|
34
|
+
**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. The thinking and the call are already in the transcript, so the spinner never repeats them.
|
|
35
35
|
|
|
36
36
|
**The footer** is the session: the cost (`sub` when the figure is what a subscription would have paid), tokens sent and received, where sent counts the cached part too, the cache hit of the last prompt, the average speeds of the whole session, and last, what other packages report, because it changes the most.
|
|
37
37
|
|
|
38
38
|
### It holds still
|
|
39
39
|
|
|
40
|
-
Numbers change on every frame, and the screen must not move with them. Every number sits in a cell of fixed width (` 17` and `2.8k` take the same room), a value that is not known yet shows as `–` in its cell instead of appearing later in the middle of the line, the working time is a stopwatch (`0:59`, `1:00`) instead of `59s` then `1m 0s`, and an estimate is drawn quieter instead of gaining a `~`. The spinner's message
|
|
40
|
+
Numbers change on every frame, and the screen must not move with them. Every number sits in a cell of fixed width (` 17` and `2.8k` take the same room), a value that is not known yet shows as `–` in its cell instead of appearing later in the middle of the line, the working time is a stopwatch (`0:59`, `1:00`) instead of `59s` then `1m 0s`, and an estimate is drawn quieter instead of gaining a `~`. The spinner's message comes after the branch and only takes the room the slots leave. The strip and the footer keep their line from the start, so the editor does not jump when the first answer arrives.
|
|
41
41
|
|
|
42
42
|
Every finished answer leaves a small entry in the session with its time to first token and writing time, because pi records usage without a duration. That is what keeps the averages right after a resume.
|
|
43
43
|
|
|
@@ -70,10 +70,7 @@ If you run [DankMaterialShell](https://github.com/AvengeMedia/DankMaterialShell)
|
|
|
70
70
|
|
|
71
71
|
Pi's own `system` theme builds its colours from the terminal's palette. If your terminal already follows matugen, `system` gets close without this, with fewer colours to work from: the sixteen terminal colours instead of the Material roles.
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
/look explain what every piece on screen is, with its current value
|
|
75
|
-
/look theme write the desktop theme and switch to it
|
|
76
|
-
```
|
|
73
|
+
`Switch to it now`, in the Desktop theme section of the settings (`Alt+S`), writes the theme and makes it pi's.
|
|
77
74
|
|
|
78
75
|
The mapping, from the Material roles and the terminal colours:
|
|
79
76
|
|
|
@@ -97,25 +94,25 @@ Another palette source is a new file next to `src/desktop/palette.ts`: a parser
|
|
|
97
94
|
|
|
98
95
|
They live in the file every piece shares, `~/.pi/agent/extensions/pi-harness/settings.json`, under `look.`. The ones marked _project_ are also read from a trusted project.
|
|
99
96
|
|
|
100
|
-
| Key | Default
|
|
101
|
-
| ------------------- |
|
|
102
|
-
| `strip` | `["elapsed", "last"]`
|
|
103
|
-
| `frame.topLeft` | `["branch", "session"]`
|
|
104
|
-
| `frame.topRight` | `["path", "host"]`
|
|
105
|
-
| `frame.bottomLeft` | `["status:pi-ask-permission:mode", "model", "effort"]` | _project_ |
|
|
106
|
-
| `frame.bottomRight` | `["context"]`
|
|
107
|
-
| `below` | `["cost", "tokens", "cache", "average", "statuses"]`
|
|
108
|
-
| `labels` | `true`
|
|
109
|
-
| `frame.style` | `"rounded"`
|
|
110
|
-
| `frame.cursor` | `"bar"`
|
|
111
|
-
| `header` | `"card"`
|
|
112
|
-
| `icons` | `"auto"`
|
|
113
|
-
| `separator` | `"
|
|
114
|
-
| `pathLength` | `40`
|
|
115
|
-
| `gaugeCells` | `8`
|
|
116
|
-
| `peek` | `true`
|
|
117
|
-
| `desktop.theme` | `true`
|
|
118
|
-
| `desktop.source` | `"~/.cache/DankMaterialShell/dms-colors.json"`
|
|
97
|
+
| Key | Default | Does |
|
|
98
|
+
| ------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
99
|
+
| `strip` | `["elapsed", "last"]` | The line above the editor. _project_ |
|
|
100
|
+
| `frame.topLeft` | `["branch", "session"]` | After the spinner. _project_ |
|
|
101
|
+
| `frame.topRight` | `["path", "host"]` | _project_ |
|
|
102
|
+
| `frame.bottomLeft` | `["status:pi-ask-permission:mode", "model", "status:pi-providers:account", "effort"]` | _project_ |
|
|
103
|
+
| `frame.bottomRight` | `["context"]` | _project_ |
|
|
104
|
+
| `below` | `["cost", "tokens", "cache", "average", "statuses"]` | The line below the editor. _project_ |
|
|
105
|
+
| `labels` | `true` | A short word before each number: `out`, `in`, `ttft`, `wait`, `thought`, `server`, `time`, `ctx`, `cache`. Off is denser, with only the glyphs. _project_ |
|
|
106
|
+
| `frame.style` | `"rounded"` | `rounded`, `square`, `heavy`, `line` (pi's two rules, written into), or `off`, which leaves pi's editor alone and moves the frame slots to the footer. _project_ |
|
|
107
|
+
| `frame.cursor` | `"bar"` | `block`, `bar` or `underline`. The last two use the terminal's own cursor |
|
|
108
|
+
| `header` | `"card"` | `card`, `compact` (two lines) or `off` (pi's own). _project_ |
|
|
109
|
+
| `icons` | `"auto"` | `nerd`, `unicode` or `ascii`. Auto uses Nerd Font glyphs locally and plain Unicode over SSH, where the font lives on the other machine |
|
|
110
|
+
| `separator` | `"space"` | `dot`, `bar`, `slash` or `space`. _project_ |
|
|
111
|
+
| `pathLength` | `40` | Longest the folder may be before it loses folders from the left. _project_ |
|
|
112
|
+
| `gaugeCells` | `8` | Cells in the context gauge. `0` hides it. _project_ |
|
|
113
|
+
| `peek` | `true` | The spinner names the state. Off, it says what pi says |
|
|
114
|
+
| `desktop.theme` | `true` | Keep the desktop theme written. Nothing happens without the palette file |
|
|
115
|
+
| `desktop.source` | `"~/.cache/DankMaterialShell/dms-colors.json"` | Where the palette is |
|
|
119
116
|
|
|
120
117
|
### Segments
|
|
121
118
|
|
|
@@ -135,8 +132,9 @@ Any segment goes in any slot, in the order the slot lists them.
|
|
|
135
132
|
| `context` | Context used, the gauge, tokens over the window |
|
|
136
133
|
| `speed` | This answer's speeds, received `↓` and sent `↑` |
|
|
137
134
|
| `wait` | Time to the first token |
|
|
135
|
+
| `server` | The last call's time from the request leaving to the response headers |
|
|
138
136
|
| `elapsed` | The agent's working time, as a stopwatch, and its calls |
|
|
139
|
-
| `last` | The last finished call: wait
|
|
137
|
+
| `last` | The last finished call: its wait, its thought time and its writing speed |
|
|
140
138
|
| `request` | This answer's tokens, with the cache |
|
|
141
139
|
| `costRate` | This answer's cost per million tokens |
|
|
142
140
|
| `cost` | The session's cost |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adeildo/pi-look",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.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": "
|
|
41
|
+
"@adeildo/pi-kit": "5.0.0"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"@earendil-works/pi-ai": "*",
|
package/src/data/git.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
// The working tree, in one call: `git status --porcelain=v2 --branch --show-stash` says how far the
|
|
2
|
-
// branch is from its upstream and what changed, file by file. The parse is pure so it can be tested
|
|
3
|
-
// against captured output.
|
|
4
1
|
import { execFile } from "node:child_process";
|
|
5
2
|
|
|
6
3
|
export interface GitState {
|
|
7
4
|
ahead: number;
|
|
8
5
|
behind: number;
|
|
9
|
-
/** In the index, ready to commit. */
|
|
10
6
|
staged: number;
|
|
11
|
-
/** Changed in the tree and not staged. */
|
|
12
7
|
modified: number;
|
|
13
8
|
untracked: number;
|
|
14
9
|
conflicted: number;
|
|
@@ -45,7 +40,6 @@ export function parseStatus(output: string): GitState {
|
|
|
45
40
|
return state;
|
|
46
41
|
}
|
|
47
42
|
|
|
48
|
-
/** Undefined outside a repository, or when git is missing or slow. */
|
|
49
43
|
export function readGit(cwd: string): Promise<GitState | undefined> {
|
|
50
44
|
return new Promise((resolve) => {
|
|
51
45
|
execFile(
|
package/src/data/snapshot.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// Everything the look can show, read once per frame. Segments are pure functions of this, so the
|
|
2
|
-
// whole screen can be drawn and tested without a terminal or a session.
|
|
3
1
|
import { hostname } from "node:os";
|
|
4
2
|
|
|
5
3
|
import { VERSION, type ExtensionContext, type SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
@@ -18,7 +16,6 @@ import {
|
|
|
18
16
|
|
|
19
17
|
export interface ModelInfo {
|
|
20
18
|
name: string;
|
|
21
|
-
/** As the provider wants to be written, not the id pi routes by. */
|
|
22
19
|
provider: string;
|
|
23
20
|
reasoning: boolean;
|
|
24
21
|
}
|
|
@@ -34,7 +31,6 @@ export interface Snapshot {
|
|
|
34
31
|
cwd: string;
|
|
35
32
|
home: string | undefined;
|
|
36
33
|
branch: string | null;
|
|
37
|
-
/** Undefined outside a repository or before the first probe. */
|
|
38
34
|
git: GitState | undefined;
|
|
39
35
|
host: string | undefined;
|
|
40
36
|
sessionName: string | undefined;
|
|
@@ -44,16 +40,12 @@ export interface Snapshot {
|
|
|
44
40
|
context: ContextInfo | undefined;
|
|
45
41
|
totals: Totals;
|
|
46
42
|
cacheHit: number | undefined;
|
|
47
|
-
/** The last prompt was written to the cache and nothing was read: the first call, not a miss. */
|
|
48
43
|
cacheWarming: boolean;
|
|
49
|
-
/** The dollar figure is what the tokens would have cost, not what was billed. */
|
|
50
44
|
subscription: boolean;
|
|
51
45
|
request: RequestView | undefined;
|
|
52
|
-
/** The last request that finished: its numbers are final. */
|
|
53
46
|
last: RequestView | undefined;
|
|
54
47
|
run: RunView | undefined;
|
|
55
48
|
averages: Averages;
|
|
56
|
-
/** What other packages report through `setStatus`, by key, in key order. */
|
|
57
49
|
statuses: ReadonlyMap<string, string>;
|
|
58
50
|
}
|
|
59
51
|
|
|
@@ -98,10 +90,6 @@ interface Derived {
|
|
|
98
90
|
averages: Averages;
|
|
99
91
|
}
|
|
100
92
|
|
|
101
|
-
/**
|
|
102
|
-
* Reads the session once per frame. The sums walk the whole branch, so they are kept until the leaf
|
|
103
|
-
* moves.
|
|
104
|
-
*/
|
|
105
93
|
export class SnapshotReader {
|
|
106
94
|
#key: string | undefined;
|
|
107
95
|
#derived: Derived | undefined;
|
|
@@ -167,7 +155,6 @@ function contextOf(ctx: ExtensionContext): ContextInfo | undefined {
|
|
|
167
155
|
return { percent: usage.percent, tokens: usage.tokens, window: usage.contextWindow };
|
|
168
156
|
}
|
|
169
157
|
|
|
170
|
-
/** A status is one line: newlines and tabs from another package would break the frame. */
|
|
171
158
|
function sanitize(text: string): string {
|
|
172
159
|
return text.replace(/[\r\n\t]+/g, " ").trim();
|
|
173
160
|
}
|
package/src/data/telemetry.ts
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
// The clock behind the speeds. A run is everything between the prompt and the agent settling; a
|
|
2
|
-
// request is one call to the model inside it. Both are measured from the message events, because pi
|
|
3
|
-
// records usage without a duration.
|
|
4
|
-
//
|
|
5
|
-
// Two speeds per request, one for each direction:
|
|
6
|
-
// ↑ prefill: prompt tokens over the wait for the first token, how fast what we sent was taken in
|
|
7
|
-
// ↓ decode: output tokens over the time spent writing, how fast the answer comes back
|
|
8
1
|
import {
|
|
9
2
|
emptyTotals,
|
|
10
3
|
perSecond,
|
|
@@ -14,42 +7,48 @@ import {
|
|
|
14
7
|
usageTotals,
|
|
15
8
|
} from "./totals.ts";
|
|
16
9
|
|
|
17
|
-
/** A decode speed over less than this is noise, so it waits. */
|
|
18
10
|
const MIN_SAMPLE_MS = 250;
|
|
19
|
-
/**
|
|
20
|
-
const
|
|
11
|
+
/** Measured over 16,889 assistant messages of one machine: 2.88 characters per output token. */
|
|
12
|
+
const DEFAULT_CHARS_PER_TOKEN = 2.9;
|
|
13
|
+
|
|
14
|
+
/** What a delta carried, so the wait for the first thought can be told from the first word. */
|
|
15
|
+
export type DeltaKind = "thinking" | "writing";
|
|
21
16
|
|
|
22
17
|
export interface RequestView {
|
|
23
|
-
/** The answer is still arriving. */
|
|
24
18
|
streaming: boolean;
|
|
25
|
-
/** Waiting for the first token right now. */
|
|
26
19
|
waiting: boolean;
|
|
27
|
-
/** Since the request left, frozen when it ends. */
|
|
28
20
|
elapsedMs: number;
|
|
29
|
-
|
|
30
|
-
|
|
21
|
+
/** From the request leaving to the first token of any kind: what you waited for the model. */
|
|
22
|
+
waitMs: number | undefined;
|
|
23
|
+
/** The same wait while it is still running, so the number counts from the moment it left. */
|
|
24
|
+
waitingMs: number;
|
|
25
|
+
/** From the request leaving to the response headers: the provider's own latency. */
|
|
26
|
+
serverMs: number | undefined;
|
|
27
|
+
/** From the headers to the first token: the model reading the prompt. */
|
|
28
|
+
prefillMs: number | undefined;
|
|
29
|
+
/** From the first thought to the first word written. Unset when it wrote without thinking. */
|
|
30
|
+
thoughtMs: number | undefined;
|
|
31
31
|
decode: number | undefined;
|
|
32
|
-
/** Prompt tokens per second of waiting, once the prompt size is known. */
|
|
33
32
|
prefill: number | undefined;
|
|
34
|
-
/** What this request used so far. */
|
|
35
33
|
usage: Totals;
|
|
36
|
-
/** The output count is an estimate until the provider reports it. */
|
|
37
34
|
estimated: boolean;
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
export interface RunView {
|
|
41
38
|
running: boolean;
|
|
42
39
|
elapsedMs: number;
|
|
43
|
-
/** Requests made in this run. */
|
|
44
40
|
requests: number;
|
|
45
41
|
}
|
|
46
42
|
|
|
47
43
|
interface Request {
|
|
48
44
|
startedAt: number;
|
|
45
|
+
/** When the request left, from pi's own `before_provider_request`. */
|
|
46
|
+
sentAt: number | undefined;
|
|
47
|
+
headersAt: number | undefined;
|
|
49
48
|
firstTokenAt: number | undefined;
|
|
49
|
+
firstWritingAt: number | undefined;
|
|
50
50
|
endedAt: number | undefined;
|
|
51
51
|
usage: Totals;
|
|
52
|
-
/** Characters seen, for a live rate before the provider says how many tokens they were. */
|
|
53
52
|
characters: number;
|
|
54
53
|
}
|
|
55
54
|
|
|
@@ -67,12 +66,20 @@ interface UsageLike {
|
|
|
67
66
|
cost: { total: number };
|
|
68
67
|
}
|
|
69
68
|
|
|
69
|
+
function span(from: number | undefined, to: number | undefined): number | undefined {
|
|
70
|
+
return from === undefined || to === undefined ? undefined : to - from;
|
|
71
|
+
}
|
|
72
|
+
|
|
70
73
|
export class Telemetry {
|
|
71
74
|
readonly #now: () => number;
|
|
72
75
|
#run: Run | undefined;
|
|
73
76
|
#request: Request | undefined;
|
|
74
|
-
/** The last request that finished, frozen, so a line can show numbers that no longer move. */
|
|
75
77
|
#last: RequestView | undefined;
|
|
78
|
+
/** The marks of the request in flight, which arrive before the pi message it belongs to. */
|
|
79
|
+
#sentAt: number | undefined;
|
|
80
|
+
#headersAt: number | undefined;
|
|
81
|
+
/** Characters and tokens of the answers that finished, for the live estimate to use a real ratio. */
|
|
82
|
+
#sampled = { characters: 0, output: 0 };
|
|
76
83
|
|
|
77
84
|
constructor(now: () => number = Date.now) {
|
|
78
85
|
this.#now = now;
|
|
@@ -86,35 +93,43 @@ export class Telemetry {
|
|
|
86
93
|
if (this.#run !== undefined && this.#run.endedAt === undefined) this.#run.endedAt = this.#now();
|
|
87
94
|
}
|
|
88
95
|
|
|
89
|
-
/**
|
|
96
|
+
/** Fires before every call to the model, so it is the start of the wait. */
|
|
97
|
+
providerRequestSent(): void {
|
|
98
|
+
this.#sentAt = this.#now();
|
|
99
|
+
this.#headersAt = undefined;
|
|
100
|
+
this.#attach();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Fires when the response arrives, before its stream is read. */
|
|
104
|
+
providerResponseArrived(): void {
|
|
105
|
+
this.#headersAt = this.#now();
|
|
106
|
+
this.#attach();
|
|
107
|
+
}
|
|
108
|
+
|
|
90
109
|
requestStarted(): void {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
usage: emptyTotals(),
|
|
96
|
-
characters: 0,
|
|
97
|
-
};
|
|
110
|
+
// A turn begins, and nothing has left for it yet.
|
|
111
|
+
this.#sentAt = undefined;
|
|
112
|
+
this.#headersAt = undefined;
|
|
113
|
+
this.#begin();
|
|
98
114
|
if (this.#run === undefined || this.#run.endedAt !== undefined) this.runStarted();
|
|
99
115
|
if (this.#run !== undefined) this.#run.requests++;
|
|
100
116
|
}
|
|
101
117
|
|
|
102
|
-
|
|
118
|
+
// Later requests of a turn: the model answered, a tool ran, and the next call is already on its way.
|
|
103
119
|
answerStarted(): void {
|
|
104
|
-
if (this.#request === undefined || this.#request.endedAt !== undefined) this
|
|
120
|
+
if (this.#request === undefined || this.#request.endedAt !== undefined) this.#begin();
|
|
105
121
|
}
|
|
106
122
|
|
|
107
|
-
|
|
108
|
-
answerGrew(delta: string, usage: UsageLike): void {
|
|
123
|
+
answerGrew(delta: string, usage: UsageLike, kind: DeltaKind): void {
|
|
109
124
|
const request = this.#request;
|
|
110
125
|
if (request === undefined || request.endedAt !== undefined) return;
|
|
111
126
|
request.usage = usageTotals(usage);
|
|
112
127
|
if (delta.length === 0) return;
|
|
113
128
|
request.characters += delta.length;
|
|
114
129
|
request.firstTokenAt ??= this.#now();
|
|
130
|
+
if (kind === "writing") request.firstWritingAt ??= this.#now();
|
|
115
131
|
}
|
|
116
132
|
|
|
117
|
-
/** The answer is complete. Returns what the session keeps, or nothing when there was no answer. */
|
|
118
133
|
answerEnded(usage: UsageLike): RequestRecord | undefined {
|
|
119
134
|
const request = this.#request;
|
|
120
135
|
if (request === undefined || request.endedAt !== undefined) return undefined;
|
|
@@ -124,51 +139,31 @@ export class Telemetry {
|
|
|
124
139
|
request.usage = usageTotals(usage);
|
|
125
140
|
if (request.usage.output === 0 && request.firstTokenAt === undefined) return undefined;
|
|
126
141
|
|
|
142
|
+
if (request.characters > 0 && request.usage.output > 0) {
|
|
143
|
+
this.#sampled.characters += request.characters;
|
|
144
|
+
this.#sampled.output += request.usage.output;
|
|
145
|
+
}
|
|
146
|
+
|
|
127
147
|
const firstTokenAt = request.firstTokenAt ?? now;
|
|
128
|
-
|
|
148
|
+
const from = request.sentAt ?? request.startedAt;
|
|
149
|
+
this.#last = this.#view(request);
|
|
129
150
|
return {
|
|
130
151
|
output: request.usage.output,
|
|
131
152
|
prompt: promptOf(request.usage),
|
|
132
|
-
firstTokenMs: firstTokenAt -
|
|
153
|
+
firstTokenMs: firstTokenAt - from,
|
|
133
154
|
generationMs: now - firstTokenAt,
|
|
134
155
|
};
|
|
135
156
|
}
|
|
136
157
|
|
|
137
|
-
/** The request running now, or the last one. */
|
|
138
158
|
request(): RequestView | undefined {
|
|
139
159
|
const request = this.#request;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const now = this.#now();
|
|
143
|
-
const streaming = request.endedAt === undefined;
|
|
144
|
-
const firstTokenMs =
|
|
145
|
-
request.firstTokenAt === undefined ? undefined : request.firstTokenAt - request.startedAt;
|
|
146
|
-
const estimated = streaming && request.usage.output === 0;
|
|
147
|
-
const output = estimated
|
|
148
|
-
? Math.round(request.characters / CHARS_PER_TOKEN)
|
|
149
|
-
: request.usage.output;
|
|
150
|
-
const writingMs =
|
|
151
|
-
request.firstTokenAt === undefined ? 0 : (request.endedAt ?? now) - request.firstTokenAt;
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
streaming,
|
|
155
|
-
waiting: streaming && request.firstTokenAt === undefined,
|
|
156
|
-
elapsedMs: (request.endedAt ?? now) - request.startedAt,
|
|
157
|
-
firstTokenMs,
|
|
158
|
-
decode: writingMs < MIN_SAMPLE_MS && streaming ? undefined : perSecond(output, writingMs),
|
|
159
|
-
prefill:
|
|
160
|
-
firstTokenMs === undefined ? undefined : perSecond(promptOf(request.usage), firstTokenMs),
|
|
161
|
-
usage: { ...request.usage, output },
|
|
162
|
-
estimated,
|
|
163
|
-
};
|
|
160
|
+
return request === undefined ? undefined : this.#view(request);
|
|
164
161
|
}
|
|
165
162
|
|
|
166
|
-
/** The last request that finished. Undefined until one has. */
|
|
167
163
|
last(): RequestView | undefined {
|
|
168
164
|
return this.#last;
|
|
169
165
|
}
|
|
170
166
|
|
|
171
|
-
/** The run going now, or the last one. */
|
|
172
167
|
run(): RunView | undefined {
|
|
173
168
|
const run = this.#run;
|
|
174
169
|
if (run === undefined) return undefined;
|
|
@@ -184,5 +179,63 @@ export class Telemetry {
|
|
|
184
179
|
this.#run = undefined;
|
|
185
180
|
this.#request = undefined;
|
|
186
181
|
this.#last = undefined;
|
|
182
|
+
this.#sentAt = undefined;
|
|
183
|
+
this.#headersAt = undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
#begin(): void {
|
|
187
|
+
this.#request = {
|
|
188
|
+
startedAt: this.#now(),
|
|
189
|
+
sentAt: this.#sentAt,
|
|
190
|
+
headersAt: this.#headersAt,
|
|
191
|
+
firstTokenAt: undefined,
|
|
192
|
+
firstWritingAt: undefined,
|
|
193
|
+
endedAt: undefined,
|
|
194
|
+
usage: emptyTotals(),
|
|
195
|
+
characters: 0,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The marks belong to a request that may not exist yet: pi sends before it opens the message. */
|
|
200
|
+
#attach(): void {
|
|
201
|
+
const request = this.#request;
|
|
202
|
+
if (request === undefined || request.endedAt !== undefined) return;
|
|
203
|
+
request.sentAt = this.#sentAt;
|
|
204
|
+
request.headersAt = this.#headersAt;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#view(request: Request): RequestView {
|
|
208
|
+
const now = this.#now();
|
|
209
|
+
const streaming = request.endedAt === undefined;
|
|
210
|
+
const from = request.sentAt ?? request.startedAt;
|
|
211
|
+
const waitMs = span(from, request.firstTokenAt);
|
|
212
|
+
const estimated = streaming && request.usage.output === 0;
|
|
213
|
+
const output = estimated
|
|
214
|
+
? Math.round(request.characters / this.#charsPerToken())
|
|
215
|
+
: request.usage.output;
|
|
216
|
+
const writingMs =
|
|
217
|
+
request.firstTokenAt === undefined ? 0 : (request.endedAt ?? now) - request.firstTokenAt;
|
|
218
|
+
const thoughtMs = span(request.firstTokenAt, request.firstWritingAt);
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
streaming,
|
|
222
|
+
waiting: streaming && request.firstTokenAt === undefined,
|
|
223
|
+
elapsedMs: (request.endedAt ?? now) - request.startedAt,
|
|
224
|
+
waitMs,
|
|
225
|
+
waitingMs: (request.endedAt ?? now) - from,
|
|
226
|
+
serverMs: span(request.sentAt, request.headersAt),
|
|
227
|
+
prefillMs: span(request.headersAt ?? request.sentAt, request.firstTokenAt),
|
|
228
|
+
thoughtMs: thoughtMs !== undefined && thoughtMs > 0 ? thoughtMs : undefined,
|
|
229
|
+
decode: writingMs < MIN_SAMPLE_MS && streaming ? undefined : perSecond(output, writingMs),
|
|
230
|
+
prefill: waitMs === undefined ? undefined : perSecond(promptOf(request.usage), waitMs),
|
|
231
|
+
usage: { ...request.usage, output },
|
|
232
|
+
estimated,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The measured mix of this session, or the one measured over a machine's worth of answers. */
|
|
237
|
+
#charsPerToken(): number {
|
|
238
|
+
const { characters, output } = this.#sampled;
|
|
239
|
+
return output > 0 ? characters / output : DEFAULT_CHARS_PER_TOKEN;
|
|
187
240
|
}
|
|
188
241
|
}
|
package/src/data/totals.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// What a session spent and how fast it went, added up from its own entries. Pi keeps the same totals
|
|
2
|
-
// for its footer but does not export the helper, so this is the one place that sums them.
|
|
3
1
|
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
4
2
|
|
|
5
3
|
export interface Totals {
|
|
@@ -18,18 +16,10 @@ interface UsageLike {
|
|
|
18
16
|
cost: { total: number };
|
|
19
17
|
}
|
|
20
18
|
|
|
21
|
-
/**
|
|
22
|
-
* What one request to the model left behind. Pi records usage without a duration, so this entry is
|
|
23
|
-
* where the session speeds come from after a resume.
|
|
24
|
-
*/
|
|
25
19
|
export interface RequestRecord {
|
|
26
|
-
/** Tokens the model wrote. */
|
|
27
20
|
output: number;
|
|
28
|
-
/** Tokens sent: fresh input plus what was read from and written to the cache. */
|
|
29
21
|
prompt: number;
|
|
30
|
-
/** From the request leaving to the first piece of the answer. */
|
|
31
22
|
firstTokenMs: number;
|
|
32
|
-
/** From the first piece of the answer to the last. */
|
|
33
23
|
generationMs: number;
|
|
34
24
|
}
|
|
35
25
|
|
|
@@ -38,9 +28,7 @@ export const REQUEST_ENTRY = "pi-look:request";
|
|
|
38
28
|
const LEGACY_ENTRY = "pi-statusline:turn";
|
|
39
29
|
|
|
40
30
|
export interface Averages {
|
|
41
|
-
/** Output tokens per second of writing, across the branch. */
|
|
42
31
|
decode: number | undefined;
|
|
43
|
-
/** Prompt tokens per second of waiting for the first token, across the branch. */
|
|
44
32
|
prefill: number | undefined;
|
|
45
33
|
}
|
|
46
34
|
|
|
@@ -62,7 +50,6 @@ export function usageTotals(usage: UsageLike): Totals {
|
|
|
62
50
|
return totals;
|
|
63
51
|
}
|
|
64
52
|
|
|
65
|
-
/** The whole branch, not just the messages after the last compaction. */
|
|
66
53
|
export function totalsOf(entries: readonly SessionEntry[]): Totals {
|
|
67
54
|
const totals = emptyTotals();
|
|
68
55
|
|
|
@@ -79,7 +66,6 @@ export function totalsOf(entries: readonly SessionEntry[]): Totals {
|
|
|
79
66
|
return totals;
|
|
80
67
|
}
|
|
81
68
|
|
|
82
|
-
/** How much of the last prompt came from the cache, the way pi's own footer reports it. */
|
|
83
69
|
export function cacheHitPercent(entries: readonly SessionEntry[]): number | undefined {
|
|
84
70
|
for (let index = entries.length - 1; index >= 0; index--) {
|
|
85
71
|
const entry = entries[index];
|
package/src/desktop/palette.ts
CHANGED
|
@@ -1,21 +1,12 @@
|
|
|
1
|
-
// The desktop palette as a pi theme. DankMaterialShell runs matugen over the wallpaper and keeps the
|
|
2
|
-
// result in one file: the Material roles for dark and light, and sixteen terminal colours. Pi wants
|
|
3
|
-
// 51 tokens. This maps one onto the other, so the terminal, the shell and pi share one palette.
|
|
4
|
-
//
|
|
5
|
-
// Every value comes from the file. Where Material has no role, like success or a diff line, the
|
|
6
|
-
// terminal colour of that hue stands in, and tinted backgrounds are a surface mixed with that hue.
|
|
7
1
|
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
8
2
|
|
|
9
3
|
export const DESKTOP_THEME = "desktop";
|
|
10
4
|
|
|
11
5
|
export type Mode = "dark" | "light";
|
|
12
6
|
|
|
13
|
-
/** The parts of `dms-colors.json` the theme reads. */
|
|
14
7
|
export interface DesktopColors {
|
|
15
8
|
mode: Mode;
|
|
16
|
-
/** Material roles, like `primary` and `surface_container_high`. */
|
|
17
9
|
roles: Readonly<Record<string, string>>;
|
|
18
|
-
/** The sixteen terminal colours, `color0` to `color15`, for this mode. */
|
|
19
10
|
terminal: Readonly<Record<string, string>>;
|
|
20
11
|
}
|
|
21
12
|
|
|
@@ -29,7 +20,6 @@ export interface ThemeJson {
|
|
|
29
20
|
|
|
30
21
|
const HEX = /^#[0-9a-f]{6}$/i;
|
|
31
22
|
|
|
32
|
-
/** Reads the file's JSON. Undefined when it is not the shape DankMaterialShell writes. */
|
|
33
23
|
export function parseDesktopColors(json: unknown, mode?: Mode): DesktopColors | undefined {
|
|
34
24
|
if (!isRecord(json) || !isRecord(json.colors) || !isRecord(json.dank16)) return undefined;
|
|
35
25
|
const chosen: Mode = mode ?? (json.mode === "light" ? "light" : "dark");
|
|
@@ -49,7 +39,6 @@ export function parseDesktopColors(json: unknown, mode?: Mode): DesktopColors |
|
|
|
49
39
|
return { mode: chosen, roles: material, terminal };
|
|
50
40
|
}
|
|
51
41
|
|
|
52
|
-
/** The roles and colours the mapping needs. A file missing any of them does not become a theme. */
|
|
53
42
|
const REQUIRED_ROLES = [
|
|
54
43
|
"primary",
|
|
55
44
|
"secondary",
|
|
@@ -157,8 +146,6 @@ export function desktopTheme(colors: DesktopColors): ThemeJson {
|
|
|
157
146
|
syntaxOperator: "subtext",
|
|
158
147
|
syntaxPunctuation: "subtext",
|
|
159
148
|
|
|
160
|
-
// The effort ramp runs from the quiet outline through the palette's own hues to the terminal
|
|
161
|
-
// reds, which keep their meaning whatever the wallpaper, so the frame warms up with the effort.
|
|
162
149
|
thinkingOff: "outlineVariant",
|
|
163
150
|
thinkingMinimal: "outline",
|
|
164
151
|
thinkingLow: "secondary",
|
|
@@ -189,7 +176,6 @@ type BackgroundToken =
|
|
|
189
176
|
| "toolSuccessBg"
|
|
190
177
|
| "toolErrorBg";
|
|
191
178
|
|
|
192
|
-
/** `base` moved `amount` of the way towards `tint`, in sRGB, which is enough for a faint wash. */
|
|
193
179
|
export function mix(base: string, tint: string, amount: number): string {
|
|
194
180
|
const a = channels(base);
|
|
195
181
|
const b = channels(tint);
|
package/src/desktop/sync.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
// Keeps `~/.pi/agent/themes/desktop.json` equal to the desktop palette. Pi reloads the active theme
|
|
2
|
-
// from that folder by itself, so changing the wallpaper recolours a running pi with no restart.
|
|
3
1
|
import { type FSWatcher, watch } from "node:fs";
|
|
4
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
3
|
import { homedir } from "node:os";
|
|
@@ -9,7 +7,6 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
9
7
|
|
|
10
8
|
import { DESKTOP_THEME, desktopTheme, missingColors, parseDesktopColors } from "./palette.ts";
|
|
11
9
|
|
|
12
|
-
/** matugen writes a few files in a burst; one rewrite per burst is enough. */
|
|
13
10
|
const SETTLE_MS = 250;
|
|
14
11
|
|
|
15
12
|
export type SyncResult =
|
|
@@ -26,7 +23,6 @@ export function expandHome(path: string): string {
|
|
|
26
23
|
return path === "~" || path.startsWith("~/") ? join(homedir(), path.slice(1)) : path;
|
|
27
24
|
}
|
|
28
25
|
|
|
29
|
-
/** Writes the theme when the palette differs from what is on disk. */
|
|
30
26
|
export async function syncDesktopTheme(source: string): Promise<SyncResult> {
|
|
31
27
|
let raw: string;
|
|
32
28
|
try {
|
|
@@ -57,10 +53,7 @@ export async function syncDesktopTheme(source: string): Promise<SyncResult> {
|
|
|
57
53
|
return { kind: "written", path };
|
|
58
54
|
}
|
|
59
55
|
|
|
60
|
-
/**
|
|
61
|
-
* Watches the folder rather than the file: tools that write atomically replace the file, and a
|
|
62
|
-
* watch on the old one goes quiet.
|
|
63
|
-
*/
|
|
56
|
+
/** Watches the folder: an atomic write replaces the file, and a watch on the old one goes quiet. */
|
|
64
57
|
export function watchDesktopPalette(source: string, changed: () => void): () => void {
|
|
65
58
|
const path = expandHome(source);
|
|
66
59
|
let timer: ReturnType<typeof setTimeout> | undefined;
|