@d3ara1n/pi-editor-shell 0.6.0 → 0.7.1
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 -4
- package/package.json +1 -1
- package/src/card-editor.ts +2 -2
- package/src/config.ts +17 -0
- package/src/index.ts +59 -15
package/README.md
CHANGED
|
@@ -4,8 +4,8 @@ Replaces pi's default editor and status bar with a unified rounded-corner shell
|
|
|
4
4
|
|
|
5
5
|
## What shows up where
|
|
6
6
|
|
|
7
|
-
- **Top border** — `
|
|
8
|
-
- **Bottom border** — ` ctx NN%/NNk|N.NM · ⚡ cacheRead (total) hitRate% ` (left) + ` ~/Projects (main +2 ~1) ` (right, shows git branch + dirty state when in a repo)
|
|
7
|
+
- **Top border** — ` model · thinking-level ` (left) + pinned extension statuses (right, via `pinnedStatus` config)
|
|
8
|
+
- **Bottom border** — ` ctx NN%/NNk|N.NM · ⚡ cacheRead (total) hitRate% ` (left) + ` ~/Projects (main +2 ~1) ` (right, shows git branch + dirty state when in a repo). Session hit rate via `/editor-shell:status`.
|
|
9
9
|
- **Below shell** — Auto-wrapping extension status line (all `setStatus` entries not pinned to the top)
|
|
10
10
|
- **Border color** follows pi's thinking-level / bash-mode indicator automatically.
|
|
11
11
|
|
|
@@ -38,6 +38,25 @@ In `~/.pi/agent/settings.json` under the `editorShell` key:
|
|
|
38
38
|
| `hitRate` | `` | fa-bullseye |
|
|
39
39
|
| `folder` | `` | fa-folder_open |
|
|
40
40
|
|
|
41
|
+
### Model display
|
|
42
|
+
|
|
43
|
+
How the model is labeled in the top-left border (`"name"` by default):
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"editorShell": {
|
|
48
|
+
"modelDisplay": "name"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
| Value | Example |
|
|
54
|
+
|-------|---------|
|
|
55
|
+
| `"name"` (default) | `Claude Opus 4.8 (Yanproxy)` |
|
|
56
|
+
| `"provider-id"` | `yanproxy/anthropic/claude-opus-4-8` |
|
|
57
|
+
|
|
58
|
+
`"name"` uses `model.name`; a model with no name falls back to its id, so the slot never goes blank.
|
|
59
|
+
|
|
41
60
|
## Commands
|
|
42
61
|
|
|
43
62
|
| Command | Description |
|
|
@@ -74,5 +93,4 @@ Or add to `~/.pi/agent/settings.json`:
|
|
|
74
93
|
|
|
75
94
|
## Dependencies
|
|
76
95
|
|
|
77
|
-
|
|
78
|
-
- [`@earendil-works/pi-tui`](https://www.npmjs.com/package/@earendil-works/pi-tui)
|
|
96
|
+
None.
|
package/package.json
CHANGED
package/src/card-editor.ts
CHANGED
|
@@ -34,12 +34,12 @@ export type SpinnerPhase = "thinking" | "outputting" | "toolcall" | "exec";
|
|
|
34
34
|
/** Spinner frames keyed by agent phase, chosen for semantic fit:
|
|
35
35
|
* thinking — ●/○ pulse, doubled frames to halve the tempo
|
|
36
36
|
* outputting — braille sand-pile (dots accumulate bottom-up, then clear)
|
|
37
|
-
* toolcall —
|
|
37
|
+
* toolcall — shade ramp breathing (░▒▓█▓▒, symmetric incl. full block)
|
|
38
38
|
* exec — ◜◝◞◟ arc rotation (tool spinning, mechanical) */
|
|
39
39
|
const SPINNERS: Record<SpinnerPhase, readonly string[]> = {
|
|
40
40
|
thinking: ["●", "●", "○", "○"],
|
|
41
41
|
outputting: ["⡀", "⣀", "⣄", "⣤", "⣦", "⣶", "⣷", "⣿"],
|
|
42
|
-
toolcall: ["
|
|
42
|
+
toolcall: ["░", "▒", "▓", "█", "▓", "▒"],
|
|
43
43
|
exec: ["◜", "◝", "◞", "◟"],
|
|
44
44
|
};
|
|
45
45
|
|
package/src/config.ts
CHANGED
|
@@ -22,6 +22,9 @@ export interface EditorShellIcons {
|
|
|
22
22
|
folder: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/** How the model is shown in the top-left border slot. */
|
|
26
|
+
export type ModelDisplay = "name" | "provider-id";
|
|
27
|
+
|
|
25
28
|
export interface EditorShellConfig {
|
|
26
29
|
/**
|
|
27
30
|
* Status keys to pin to the shell's top-right corner.
|
|
@@ -34,6 +37,13 @@ export interface EditorShellConfig {
|
|
|
34
37
|
* for a Nerd Font glyph, or `"🤖"` for an emoji, etc.
|
|
35
38
|
*/
|
|
36
39
|
icons: Partial<EditorShellIcons>;
|
|
40
|
+
/**
|
|
41
|
+
* What to show as the model label in the top-left border.
|
|
42
|
+
* - `"name"` — `model.name` (friendlier; falls back to the id when a model
|
|
43
|
+
* has no name, so it never goes blank).
|
|
44
|
+
* - `"provider-id"` — `provider/id`.
|
|
45
|
+
*/
|
|
46
|
+
modelDisplay: ModelDisplay;
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
const ICON_KEYS: ReadonlyArray<keyof EditorShellIcons> = [
|
|
@@ -59,6 +69,7 @@ function filterIcons(obj: Record<string, unknown>): Partial<EditorShellIcons> {
|
|
|
59
69
|
export const DEFAULT_CONFIG: EditorShellConfig = {
|
|
60
70
|
pinnedStatus: [],
|
|
61
71
|
icons: {},
|
|
72
|
+
modelDisplay: "name",
|
|
62
73
|
};
|
|
63
74
|
|
|
64
75
|
function getAgentDir(): string {
|
|
@@ -91,6 +102,11 @@ export function loadEditorShellConfig(cwd?: string): EditorShellConfig {
|
|
|
91
102
|
|
|
92
103
|
const pinned = raw.pinnedStatus;
|
|
93
104
|
const iconsRaw = raw.icons;
|
|
105
|
+
const modelDisplayRaw = raw.modelDisplay;
|
|
106
|
+
const modelDisplay =
|
|
107
|
+
modelDisplayRaw === "name" || modelDisplayRaw === "provider-id"
|
|
108
|
+
? modelDisplayRaw
|
|
109
|
+
: DEFAULT_CONFIG.modelDisplay;
|
|
94
110
|
return {
|
|
95
111
|
pinnedStatus: Array.isArray(pinned)
|
|
96
112
|
? pinned.filter((k): k is string => typeof k === "string")
|
|
@@ -99,5 +115,6 @@ export function loadEditorShellConfig(cwd?: string): EditorShellConfig {
|
|
|
99
115
|
iconsRaw && typeof iconsRaw === "object"
|
|
100
116
|
? filterIcons(iconsRaw as Record<string, unknown>)
|
|
101
117
|
: {},
|
|
118
|
+
modelDisplay,
|
|
102
119
|
};
|
|
103
120
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { spawn } from "node:child_process";
|
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { CardEditor, type FrameProvider, type SpinnerPhase } from "./card-editor";
|
|
7
|
-
import { loadEditorShellConfig, type EditorShellConfig, type EditorShellIcons } from "./config";
|
|
7
|
+
import { DEFAULT_CONFIG, loadEditorShellConfig, type EditorShellConfig, type EditorShellIcons } from "./config";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* pi-editor-shell — Replaces pi's default editor and status bar with a
|
|
@@ -86,8 +86,25 @@ interface EntrySnap {
|
|
|
86
86
|
message?: MsgSnap;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/** Sum cache-
|
|
90
|
-
*
|
|
89
|
+
/** Sum cache-related usage across all assistant messages on the session.
|
|
90
|
+
* Returns the same shape as UsageSnap so we can reuse cacheHitRate(). */
|
|
91
|
+
function sumSessionUsage(ctx: { sessionManager: { getEntries(): unknown[] } }): UsageSnap {
|
|
92
|
+
let input = 0;
|
|
93
|
+
let cacheRead = 0;
|
|
94
|
+
let cacheWrite = 0;
|
|
95
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
96
|
+
const e = entry as EntrySnap;
|
|
97
|
+
if (e.type !== "message" || e.message?.role !== "assistant" || !e.message.usage) continue;
|
|
98
|
+
input += e.message.usage.input ?? 0;
|
|
99
|
+
cacheRead += e.message.usage.cacheRead ?? 0;
|
|
100
|
+
cacheWrite += e.message.usage.cacheWrite ?? 0;
|
|
101
|
+
}
|
|
102
|
+
return { input, cacheRead, cacheWrite };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Sum cache-read tokens across all assistant messages — session total for
|
|
106
|
+
* the "(14.0M)" display in the border. Kept separate from sumSessionUsage
|
|
107
|
+
* to keep the hot path (agent_end) minimal. */
|
|
91
108
|
function sumCacheRead(ctx: { sessionManager: { getEntries(): unknown[] } }): number {
|
|
92
109
|
let total = 0;
|
|
93
110
|
for (const entry of ctx.sessionManager.getEntries()) {
|
|
@@ -110,14 +127,31 @@ function latestAssistantUsage(ctx: { sessionManager: { getEntries(): unknown[] }
|
|
|
110
127
|
return latest;
|
|
111
128
|
}
|
|
112
129
|
|
|
113
|
-
/** Cache hit rate for a single turn
|
|
114
|
-
*
|
|
115
|
-
*
|
|
130
|
+
/** Cache hit rate for a single turn.
|
|
131
|
+
*
|
|
132
|
+
* The `Usage.input` field has different semantics depending on the provider:
|
|
133
|
+
* - OpenAI/DeepSeek: `input` = fresh non-cached tokens (promptTokens −
|
|
134
|
+
* cacheRead − cacheWrite). Denominator = input + cacheRead + cacheWrite.
|
|
135
|
+
* - Anthropic: `input` = total input_tokens (already includes cacheRead +
|
|
136
|
+
* cacheWrite). Denominator = input alone (otherwise cache is counted twice).
|
|
137
|
+
*
|
|
138
|
+
* We detect the convention: if `input` can account for both cacheRead and
|
|
139
|
+
* cacheWrite (input >= cacheRead + cacheWrite), assume it is the total-input
|
|
140
|
+
* convention (Anthropic). Otherwise assume the fresh-only convention
|
|
141
|
+
* (OpenAI). Heuristic, not perfect, but correct for both conventions in
|
|
142
|
+
* practice; the real fix belongs in pi-ai where Usage is populated.
|
|
143
|
+
*
|
|
144
|
+
* Returns undefined when there is no usage or no prompt tokens. */
|
|
116
145
|
function cacheHitRate(u: UsageSnap | undefined): number | undefined {
|
|
117
146
|
if (!u) return undefined;
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
147
|
+
const nonCached = u.input ?? 0;
|
|
148
|
+
const cached = (u.cacheRead ?? 0) + (u.cacheWrite ?? 0);
|
|
149
|
+
// Anthropic convention: input already includes cacheRead + cacheWrite.
|
|
150
|
+
const totalPrompt = nonCached >= cached && cached > 0
|
|
151
|
+
? nonCached
|
|
152
|
+
: nonCached + cached;
|
|
153
|
+
if (totalPrompt <= 0) return undefined;
|
|
154
|
+
return ((u.cacheRead ?? 0) / totalPrompt) * 100;
|
|
121
155
|
}
|
|
122
156
|
|
|
123
157
|
/** Format a token count for display: 14000000 → "14.0M", 132000 → "132.0k". */
|
|
@@ -205,7 +239,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
205
239
|
// The factory may run again when pi rebuilds the editor (model switch,
|
|
206
240
|
// reload, …), so always drive whichever instance is current.
|
|
207
241
|
let editor: CardEditor | undefined;
|
|
208
|
-
let config: EditorShellConfig = {
|
|
242
|
+
let config: EditorShellConfig = { ...DEFAULT_CONFIG };
|
|
209
243
|
// Resolved icons for the current session: built-in defaults merged with
|
|
210
244
|
// the user's overrides. Re-computed at session_start.
|
|
211
245
|
let icons: EditorShellIcons = { ...DEFAULT_ICONS };
|
|
@@ -281,7 +315,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
281
315
|
return ` ${texts.map((s) => theme.fg("muted", s)).join(theme.fg("dim", " · "))} `;
|
|
282
316
|
};
|
|
283
317
|
|
|
284
|
-
const model = ctx.model
|
|
318
|
+
const model = ctx.model
|
|
319
|
+
? config.modelDisplay === "name"
|
|
320
|
+
? ctx.model.name
|
|
321
|
+
: `${ctx.model.provider}/${ctx.model.id}`
|
|
322
|
+
: "no model";
|
|
285
323
|
const thinking = pi.getThinkingLevel();
|
|
286
324
|
const thinkingColor = THINKING_TOKEN[thinking] ?? "muted";
|
|
287
325
|
|
|
@@ -296,6 +334,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
296
334
|
// Cache-read tokens — per-turn figure first, session total in parens,
|
|
297
335
|
// then hit rate (pi's "CHxx%" formula). All refreshed at agent_end and
|
|
298
336
|
// read from cache off the hot path.
|
|
337
|
+
// Session hit rate is available via /editor-shell:status.
|
|
299
338
|
const cacheReadNow = _latestUsage?.cacheRead ?? 0;
|
|
300
339
|
const hitRate = cacheHitRate(_latestUsage);
|
|
301
340
|
const cachePart =
|
|
@@ -378,6 +417,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
378
417
|
|
|
379
418
|
lines.push("[editor-shell config]");
|
|
380
419
|
lines.push(` pinnedStatus: [${config.pinnedStatus.join(", ")}]`);
|
|
420
|
+
lines.push(` modelDisplay: ${config.modelDisplay}`);
|
|
381
421
|
|
|
382
422
|
lines.push("");
|
|
383
423
|
lines.push("[extension statuses]");
|
|
@@ -402,13 +442,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
402
442
|
|
|
403
443
|
lines.push("");
|
|
404
444
|
lines.push("[cache totals]");
|
|
405
|
-
const
|
|
406
|
-
lines.push(`
|
|
445
|
+
const sess = sumSessionUsage(ctx);
|
|
446
|
+
lines.push(` session input: ${formatTokens(sess.input ?? 0)}`);
|
|
447
|
+
lines.push(` session cacheRead: ${formatTokens(sess.cacheRead ?? 0)}`);
|
|
448
|
+
lines.push(` session cacheWrite: ${formatTokens(sess.cacheWrite ?? 0)}`);
|
|
449
|
+
const sRate = cacheHitRate(sess);
|
|
450
|
+
lines.push(` session hit rate: ${sRate != null ? `${sRate.toFixed(1)}%` : "n/a"}`);
|
|
407
451
|
const latest = latestAssistantUsage(ctx);
|
|
408
452
|
const now = latest?.cacheRead ?? 0;
|
|
409
|
-
lines.push(`
|
|
453
|
+
lines.push(` this turn cacheRead: ${formatTokens(now)}`);
|
|
410
454
|
const hr = cacheHitRate(latest);
|
|
411
|
-
lines.push(` hit rate: ${hr != null ? `${hr.toFixed(1)}%` : "n/a"}`);
|
|
455
|
+
lines.push(` this turn hit rate: ${hr != null ? `${hr.toFixed(1)}%` : "n/a"}`);
|
|
412
456
|
|
|
413
457
|
lines.push("");
|
|
414
458
|
lines.push(`[context] cwd: ${ctx.cwd}`);
|