@d3ara1n/pi-editor-shell 0.8.2 → 0.9.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 +4 -2
- package/package.json +1 -1
- package/src/index.ts +91 -5
package/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# pi-editor-shell
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@d3ara1n/pi-editor-shell) [](https://www.npmjs.com/package/@d3ara1n/pi-editor-shell) [](https://www.npmjs.com/package/@d3ara1n/pi-editor-shell)
|
|
4
|
+
|
|
3
5
|
Replaces pi's default editor and status bar with a unified rounded-corner shell drawn with box-drawing glyphs (`╭╮││╰╯`), with status info embedded in the border. The frame and spinner use only standard Unicode; the six border icons are Nerd Font glyphs (overridable — see [Configuration](#configuration)).
|
|
4
6
|
|
|
5
7
|
## What shows up where
|
|
6
8
|
|
|
7
9
|
- **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 *4) ` (right, shows git branch plus staged, unstaged, and untracked file counts when in a repo). Session hit rate via `/editor-shell:status`.
|
|
10
|
+
- **Bottom border** — ` ctx NN%/NNk|N.NM · ⚡ cacheRead (total) hitRate% · NN.N t/s · $N.NNN ` (left) + ` ~/Projects (main +2 ~1 *4) ` (right, shows git branch plus staged, unstaged, and untracked file counts when in a repo). TPS is measured for the latest completed response from its first streamed content delta, excluding time-to-first-token. Session cost includes assistant, tool, compaction, and branch-summary usage; the dollar segment is hidden when the provider reports no priced usage. Session hit rate via `/editor-shell:status`.
|
|
9
11
|
- **Below shell** — Auto-wrapping extension status line (all `setStatus` entries not pinned to the top)
|
|
10
12
|
- **Border color** follows pi's thinking-level / bash-mode indicator automatically.
|
|
11
13
|
|
|
@@ -61,7 +63,7 @@ How the model is labeled in the top-left border (`"name"` by default):
|
|
|
61
63
|
|
|
62
64
|
| Command | Description |
|
|
63
65
|
|---------|-------------|
|
|
64
|
-
| `/editor-shell:status` | Show debug info: pinned config, all extension statuses with their keys, cache totals |
|
|
66
|
+
| `/editor-shell:status` | Show debug info: pinned config, all extension statuses with their keys, cache totals, latest response TPS, and session cost |
|
|
65
67
|
|
|
66
68
|
## How it works
|
|
67
69
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { visibleWidth } from "@earendil-works/pi-tui";
|
|
|
3
3
|
import { spawn } from "node:child_process";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
-
import { CardEditor, type FrameProvider, type SpinnerPhase } from "./card-editor";
|
|
7
|
-
import { DEFAULT_CONFIG, loadEditorShellConfig, type EditorShellConfig, type EditorShellIcons } from "./config";
|
|
6
|
+
import { CardEditor, type FrameProvider, type SpinnerPhase } from "./card-editor.ts";
|
|
7
|
+
import { DEFAULT_CONFIG, loadEditorShellConfig, type EditorShellConfig, type EditorShellIcons } from "./config.ts";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* pi-editor-shell — Replaces pi's default editor and status bar with a
|
|
@@ -74,8 +74,12 @@ const DEFAULT_ICONS: EditorShellIcons = {
|
|
|
74
74
|
* full pi-ai message union tree. */
|
|
75
75
|
interface UsageSnap {
|
|
76
76
|
input?: number;
|
|
77
|
+
output?: number;
|
|
77
78
|
cacheRead?: number;
|
|
78
79
|
cacheWrite?: number;
|
|
80
|
+
cost?: {
|
|
81
|
+
total?: number;
|
|
82
|
+
};
|
|
79
83
|
}
|
|
80
84
|
interface MsgSnap {
|
|
81
85
|
role: string;
|
|
@@ -84,6 +88,7 @@ interface MsgSnap {
|
|
|
84
88
|
interface EntrySnap {
|
|
85
89
|
type: string;
|
|
86
90
|
message?: MsgSnap;
|
|
91
|
+
usage?: UsageSnap;
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
/** Sum cache-related usage across all assistant messages on the session.
|
|
@@ -102,6 +107,25 @@ function sumSessionUsage(ctx: { sessionManager: { getEntries(): unknown[] } }):
|
|
|
102
107
|
return { input, cacheRead, cacheWrite };
|
|
103
108
|
}
|
|
104
109
|
|
|
110
|
+
/** Sum billable usage across the full session, matching pi's built-in footer:
|
|
111
|
+
* assistant responses, usage-bearing tool results, compactions, and branch
|
|
112
|
+
* summaries. Providers without configured pricing report zero cost. */
|
|
113
|
+
function sumSessionCost(ctx: { sessionManager: { getEntries(): unknown[] } }): number {
|
|
114
|
+
let total = 0;
|
|
115
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
116
|
+
const e = entry as EntrySnap;
|
|
117
|
+
let usage: UsageSnap | undefined;
|
|
118
|
+
if (e.type === "message" && (e.message?.role === "assistant" || e.message?.role === "toolResult")) {
|
|
119
|
+
usage = e.message.usage;
|
|
120
|
+
} else if (e.type === "compaction" || e.type === "branch_summary") {
|
|
121
|
+
usage = e.usage;
|
|
122
|
+
}
|
|
123
|
+
const cost = usage?.cost?.total;
|
|
124
|
+
if (typeof cost === "number" && Number.isFinite(cost)) total += cost;
|
|
125
|
+
}
|
|
126
|
+
return total;
|
|
127
|
+
}
|
|
128
|
+
|
|
105
129
|
/** Sum cache-read tokens across all assistant messages — session total for
|
|
106
130
|
* the "(14.0M)" display in the border. Kept separate from sumSessionUsage
|
|
107
131
|
* to keep the hot path (agent_end) minimal. */
|
|
@@ -260,13 +284,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
260
284
|
// entries every frame.
|
|
261
285
|
let _cacheTotal = 0;
|
|
262
286
|
let _latestUsage: UsageSnap | undefined;
|
|
287
|
+
// Latest completed response throughput. Timing begins with the first streamed
|
|
288
|
+
// content delta, excluding time-to-first-token; it is intentionally not
|
|
289
|
+
// reconstructed for resumed sessions because persisted messages have no end time.
|
|
290
|
+
let _streamStartedAt: number | undefined;
|
|
291
|
+
let _pendingResponseElapsedMs: number | undefined;
|
|
292
|
+
let _latestTps: number | undefined;
|
|
293
|
+
// Full-session billable cost, including tool/summarization usage. Zero means the
|
|
294
|
+
// provider supplied no priced usage, so the border omits the dollar segment.
|
|
295
|
+
let _sessionCost = 0;
|
|
263
296
|
|
|
264
297
|
// ── Phase-aware spinner + lifecycle ────────────────────────────
|
|
265
298
|
// Each event asks the editor for a phase; CardEditor.setSpinner is itself
|
|
266
299
|
// a same-phase no-op, so rapid event streams never reset the animation.
|
|
300
|
+
pi.on("message_start", (event) => {
|
|
301
|
+
if (event.message.role !== "assistant") return;
|
|
302
|
+
_streamStartedAt = undefined;
|
|
303
|
+
_pendingResponseElapsedMs = undefined;
|
|
304
|
+
});
|
|
267
305
|
pi.on("turn_start", () => editor?.setSpinner("thinking"));
|
|
268
306
|
pi.on("message_update", (event) => {
|
|
269
307
|
const t = event.assistantMessageEvent.type;
|
|
308
|
+
if (
|
|
309
|
+
_streamStartedAt == null &&
|
|
310
|
+
(t === "thinking_delta" || t === "text_delta" || t === "toolcall_delta")
|
|
311
|
+
) {
|
|
312
|
+
_streamStartedAt = Date.now();
|
|
313
|
+
}
|
|
270
314
|
let next: SpinnerPhase;
|
|
271
315
|
if (t.startsWith("thinking_")) next = "thinking";
|
|
272
316
|
else if (t.startsWith("text_")) next = "outputting";
|
|
@@ -274,22 +318,48 @@ export default function (pi: ExtensionAPI) {
|
|
|
274
318
|
else return;
|
|
275
319
|
editor?.setSpinner(next);
|
|
276
320
|
});
|
|
321
|
+
pi.on("message_end", (event) => {
|
|
322
|
+
if (event.message.role !== "assistant") return;
|
|
323
|
+
const elapsedMs = _streamStartedAt == null ? 0 : Date.now() - _streamStartedAt;
|
|
324
|
+
_pendingResponseElapsedMs = elapsedMs > 0 ? elapsedMs : undefined;
|
|
325
|
+
_streamStartedAt = undefined;
|
|
326
|
+
});
|
|
277
327
|
pi.on("tool_execution_start", () => editor?.setSpinner("exec"));
|
|
278
328
|
pi.on("agent_end", (_event, ctx) => {
|
|
279
329
|
// cacheRead totals + latest usage are stable once a turn finishes —
|
|
280
330
|
// recompute here instead of on every render frame.
|
|
281
331
|
_cacheTotal = sumCacheRead(ctx);
|
|
282
332
|
_latestUsage = latestAssistantUsage(ctx);
|
|
333
|
+
_sessionCost = sumSessionCost(ctx);
|
|
283
334
|
editor?.setSpinner(null);
|
|
284
335
|
});
|
|
285
336
|
pi.on("session_shutdown", () => {
|
|
337
|
+
_streamStartedAt = undefined;
|
|
338
|
+
_pendingResponseElapsedMs = undefined;
|
|
339
|
+
_latestTps = undefined;
|
|
340
|
+
_sessionCost = 0;
|
|
286
341
|
editor?.setSpinner(null);
|
|
287
342
|
editor = undefined;
|
|
288
343
|
});
|
|
344
|
+
pi.on("session_compact", (_event, ctx) => {
|
|
345
|
+
_sessionCost = sumSessionCost(ctx);
|
|
346
|
+
editor?.requestRender();
|
|
347
|
+
});
|
|
348
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
349
|
+
_sessionCost = sumSessionCost(ctx);
|
|
350
|
+
editor?.requestRender();
|
|
351
|
+
});
|
|
289
352
|
|
|
290
353
|
// Refresh git dirty after every agent turn (tools may have changed files).
|
|
291
354
|
// Async — never blocks the event loop; re-renders once settled.
|
|
292
|
-
pi.on("turn_end", () => {
|
|
355
|
+
pi.on("turn_end", (event) => {
|
|
356
|
+
if (event.message.role === "assistant") {
|
|
357
|
+
const output = (event.message as MsgSnap).usage?.output ?? 0;
|
|
358
|
+
const elapsedMs = _pendingResponseElapsedMs ?? 0;
|
|
359
|
+
_latestTps = elapsedMs > 0 && output > 0 ? output / (elapsedMs / 1000) : undefined;
|
|
360
|
+
_pendingResponseElapsedMs = undefined;
|
|
361
|
+
editor?.requestRender();
|
|
362
|
+
}
|
|
293
363
|
if (_cwd) refreshGitDirty(_cwd, () => editor?.requestRender());
|
|
294
364
|
});
|
|
295
365
|
|
|
@@ -301,6 +371,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
301
371
|
icons = { ...DEFAULT_ICONS, ...config.icons };
|
|
302
372
|
_cacheTotal = sumCacheRead(ctx);
|
|
303
373
|
_latestUsage = latestAssistantUsage(ctx);
|
|
374
|
+
_streamStartedAt = undefined;
|
|
375
|
+
_pendingResponseElapsedMs = undefined;
|
|
376
|
+
_latestTps = undefined;
|
|
377
|
+
_sessionCost = sumSessionCost(ctx);
|
|
304
378
|
refreshGitDirty(ctx.cwd, () => editor?.requestRender());
|
|
305
379
|
|
|
306
380
|
// Fresh segments on every render — reads live ctx state, so thinking /
|
|
@@ -348,6 +422,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
348
422
|
_cacheTotal > 0
|
|
349
423
|
? `${theme.fg("dim", " · ")}${theme.fg("warning", `${icons.cache} ${formatTokens(cacheReadNow)} (${formatTokens(_cacheTotal)})${hitRate != null ? ` ${icons.hitRate} ${hitRate.toFixed(1)}%` : ""}`)}`
|
|
350
424
|
: "";
|
|
425
|
+
const tpsPart =
|
|
426
|
+
_latestTps != null
|
|
427
|
+
? `${theme.fg("dim", " · ")}${theme.fg("success", `${_latestTps.toFixed(1)} t/s`)}`
|
|
428
|
+
: "";
|
|
429
|
+
const costPart =
|
|
430
|
+
_sessionCost > 0
|
|
431
|
+
? `${theme.fg("dim", " · ")}${theme.fg("warning", `$${_sessionCost.toFixed(3)}`)}`
|
|
432
|
+
: "";
|
|
351
433
|
|
|
352
434
|
// Git branch + dirty state — pi's format: ~/Projects (main).
|
|
353
435
|
const cwdText = formatCwd(ctx.cwd);
|
|
@@ -364,7 +446,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
364
446
|
topLeft: ` ${theme.fg("accent", `${icons.model} ${model}`)}${theme.fg("dim", " · ")}${theme.fg(thinkingColor, `${icons.thinking} ${thinking}`)} `,
|
|
365
447
|
topRight: buildPinned(),
|
|
366
448
|
// Context in severity color; cwd stays muted so it never competes.
|
|
367
|
-
bottomLeft: ` ${theme.fg(contextToken(pct), `${icons.context} ${ctxText}`)}${cachePart} `,
|
|
449
|
+
bottomLeft: ` ${theme.fg(contextToken(pct), `${icons.context} ${ctxText}`)}${cachePart}${tpsPart}${costPart} `,
|
|
368
450
|
bottomRight: theme.fg("muted", ` ${cwdDisplay} `),
|
|
369
451
|
};
|
|
370
452
|
};
|
|
@@ -418,7 +500,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
418
500
|
|
|
419
501
|
// ── Debug command ──────────────────────────────────────────────
|
|
420
502
|
pi.registerCommand("editor-shell:status", {
|
|
421
|
-
description: "Show editor-shell debug state: status keys,
|
|
503
|
+
description: "Show editor-shell debug state: status keys, performance, and usage totals",
|
|
422
504
|
handler: async (_args, ctx) => {
|
|
423
505
|
// Refresh git dirty first so the status output reflects the current
|
|
424
506
|
// working tree — the event-driven cache is otherwise only updated at
|
|
@@ -459,6 +541,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
459
541
|
lines.push(` session cacheWrite: ${formatTokens(sess.cacheWrite ?? 0)}`);
|
|
460
542
|
const sRate = cacheHitRate(sess);
|
|
461
543
|
lines.push(` session hit rate: ${sRate != null ? `${sRate.toFixed(1)}%` : "n/a"}`);
|
|
544
|
+
const sessionCost = sumSessionCost(ctx);
|
|
545
|
+
_sessionCost = sessionCost;
|
|
546
|
+
lines.push(` session cost: ${sessionCost > 0 ? `$${sessionCost.toFixed(3)}` : "n/a"}`);
|
|
547
|
+
lines.push(` latest response TPS: ${_latestTps != null ? `${_latestTps.toFixed(1)} t/s` : "n/a"}`);
|
|
462
548
|
const latest = latestAssistantUsage(ctx);
|
|
463
549
|
const now = latest?.cacheRead ?? 0;
|
|
464
550
|
lines.push(` this turn cacheRead: ${formatTokens(now)}`);
|