pi-magi-theme 0.2.3 → 0.2.4
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 +16 -2
- package/extensions/magi/index.ts +80 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,6 +72,8 @@ The window title is `π - Magi - <working directory>`. After a run longer than 3
|
|
|
72
72
|
|
|
73
73
|
In fullscreen mode the side panel always reaches the bottom of the terminal.
|
|
74
74
|
|
|
75
|
+
In a terminal too narrow for it, the footer scrolls as one line instead of being cut off.
|
|
76
|
+
|
|
75
77
|
## The seventh seal: smart compaction
|
|
76
78
|
|
|
77
79
|
The theme does not compact anything itself: it shows who does. For better compaction install [pi-smart-compact](https://www.npmjs.com/package/pi-smart-compact):
|
|
@@ -104,15 +106,27 @@ Each nature is a lens, not a specialty, so the council answers any question, not
|
|
|
104
106
|
{
|
|
105
107
|
"MELCHIOR": { "model": "llama-swap/Qwen3.8 27B Q4_K_M - Thinking", "thinking": "low" },
|
|
106
108
|
"ui": { "compact": false, "kwhPrice": 0.30, "currency": "EUR" },
|
|
107
|
-
"loads": { "qwen3.8-27b": 41200 }
|
|
109
|
+
"loads": { "qwen3.8-27b": 41200 },
|
|
110
|
+
"totalWh": 1843.2
|
|
108
111
|
}
|
|
109
112
|
```
|
|
110
113
|
|
|
111
114
|
- per MAGI: `model` (unset = current session model) and optional `thinking` level;
|
|
112
115
|
- `ui.compact`: start with the compact side panel;
|
|
113
|
-
- `ui.kwhPrice` and `ui.currency` (`EUR` or `USD`): the COST row multiplies the GPU energy
|
|
116
|
+
- `ui.kwhPrice` and `ui.currency` (`EUR` or `USD`): the COST row multiplies the GPU energy by this price, showing the running total of every session with the current one in brackets;
|
|
117
|
+
- `totalWh`: written by the theme, GPU energy summed over every session (delete the key to reset the COST total);
|
|
114
118
|
- `loads`: written by the theme, how long each llama-swap model took to load last time (paces the angel attack; 60s when unknown).
|
|
115
119
|
|
|
120
|
+
## Release
|
|
121
|
+
|
|
122
|
+
`.github/workflows/publish.yml` publishes to npm when a `v*` tag is pushed, and refuses if the tag does not match `package.json`:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
npm version patch && git push --follow-tags
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
No token: npmjs is configured to trust this repository's `publish.yml` (npm trusted publishing, OIDC), which also signs the provenance.
|
|
129
|
+
|
|
116
130
|
## llama-swap
|
|
117
131
|
|
|
118
132
|
When the session model uses the `llama-swap` provider, the side panel:
|
package/extensions/magi/index.ts
CHANGED
|
@@ -32,7 +32,7 @@ import type { AssistantMessage, Model } from "@earendil-works/pi-ai";
|
|
|
32
32
|
import { completeSimple } from "@earendil-works/pi-ai";
|
|
33
33
|
import type { ExtensionAPI, ExtensionContext, Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
34
34
|
import type { Component, OverlayHandle, TUI } from "@earendil-works/pi-tui";
|
|
35
|
-
import { HStack, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
35
|
+
import { HStack, matchesKey, sliceByColumn, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
36
36
|
|
|
37
37
|
/* ────────────────────────────────────────────────────────────── art ── */
|
|
38
38
|
|
|
@@ -340,10 +340,15 @@ function setPhase(p: Phase): void {
|
|
|
340
340
|
}
|
|
341
341
|
|
|
342
342
|
const ANIM_STEP_MS = 220;
|
|
343
|
+
/** How long one Sephirah stays lit in the footer: its name and meaning are text, they need time to be read. */
|
|
344
|
+
const SEPHIRAH_STEP_MS = 1400;
|
|
343
345
|
const FAIL_FLASH_MS = 2500;
|
|
344
346
|
const REBIRTH_MS = 6000;
|
|
345
347
|
const DONE_TITLE_AFTER_MS = 30_000;
|
|
346
348
|
const SIXTH_SEAL_PERCENT = (6 / 7) * 100;
|
|
349
|
+
/** Footer marquee: one cell every FOOTER_SCROLL_MS, with this gap between the loop's end and its start. */
|
|
350
|
+
const FOOTER_SCROLL_MS = 260;
|
|
351
|
+
const FOOTER_SCROLL_GAP = " · ";
|
|
347
352
|
|
|
348
353
|
/** Sync ratio: share of tool calls that succeeded (null before the first tool). */
|
|
349
354
|
function syncPercent(): number | null {
|
|
@@ -710,6 +715,8 @@ const swap = {
|
|
|
710
715
|
cacheTokens: 0,
|
|
711
716
|
inputTokens: 0,
|
|
712
717
|
energyWh: 0, // GPU energy since the session started
|
|
718
|
+
diskWh: 0, // GPU energy of all sessions, as last read from/written to magi.json
|
|
719
|
+
savedWh: 0, // the part of energyWh already added to diskWh
|
|
713
720
|
lastSampleAt: 0,
|
|
714
721
|
lastWatts: 0,
|
|
715
722
|
};
|
|
@@ -760,6 +767,34 @@ function sampleEnergy(now = Date.now()): void {
|
|
|
760
767
|
}
|
|
761
768
|
swap.lastSampleAt = now;
|
|
762
769
|
swap.lastWatts = watts;
|
|
770
|
+
persistEnergy();
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** GPU energy of every session so far: what is on disk plus this session's not-yet-written part. */
|
|
774
|
+
function totalWh(): number {
|
|
775
|
+
return swap.diskWh + swap.energyWh - swap.savedWh;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const ENERGY_SAVE_MS = 60_000;
|
|
779
|
+
let lastPersistAt = 0;
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Adds this session's new energy to the running total in magi.json, so the COST row survives restarts.
|
|
783
|
+
* ponytail: writes at most once a minute, and adds a delta so parallel sessions do not overwrite each other.
|
|
784
|
+
*/
|
|
785
|
+
function persistEnergy(force = false): void {
|
|
786
|
+
const delta = swap.energyWh - swap.savedWh;
|
|
787
|
+
if (delta <= 0 || (!force && Date.now() - lastPersistAt < ENERGY_SAVE_MS)) return;
|
|
788
|
+
lastPersistAt = Date.now();
|
|
789
|
+
const cfg = loadMagiConfig();
|
|
790
|
+
cfg.totalWh = (cfg.totalWh ?? 0) + delta;
|
|
791
|
+
try {
|
|
792
|
+
saveMagiConfig(cfg);
|
|
793
|
+
} catch {
|
|
794
|
+
return; // disk unavailable: keep the delta and retry on the next sample
|
|
795
|
+
}
|
|
796
|
+
swap.savedWh = swap.energyWh;
|
|
797
|
+
swap.diskWh = cfg.totalWh;
|
|
763
798
|
}
|
|
764
799
|
|
|
765
800
|
async function refreshSwapMetrics(): Promise<void> {
|
|
@@ -1311,11 +1346,14 @@ class MagiPanel implements Component {
|
|
|
1311
1346
|
return out;
|
|
1312
1347
|
}
|
|
1313
1348
|
|
|
1314
|
-
/** COST: GPU energy
|
|
1349
|
+
/** COST: GPU energy × price per kWh set with /magi-ui config — all sessions, with this one in brackets. */
|
|
1315
1350
|
private costRow(inner: number): string {
|
|
1316
1351
|
if (!swap.gpus.length) return this.field("COST", "—", inner, "muted");
|
|
1317
1352
|
if (ui.kwhPrice === undefined) return this.field("COST", "→ /magi-ui config", inner, "dim");
|
|
1318
|
-
|
|
1353
|
+
const th = this.theme;
|
|
1354
|
+
const price = (wh: number) => fmtMoney((wh / 1000) * ui.kwhPrice!);
|
|
1355
|
+
const label = th.fg("dim", "COST".padEnd(9));
|
|
1356
|
+
return this.frameLine(" " + label + th.fg("warning", price(totalWh())) + th.fg("dim", ` (ses ${price(swap.energyWh)})`), inner);
|
|
1319
1357
|
}
|
|
1320
1358
|
|
|
1321
1359
|
private swapRows(inner: number, compact: boolean): string[] {
|
|
@@ -1357,7 +1395,7 @@ class MagiPanel implements Component {
|
|
|
1357
1395
|
);
|
|
1358
1396
|
}
|
|
1359
1397
|
if (swap.ramTotal) out.push(this.field("RAM", `${gib(swap.ramUsed).toFixed(1)} / ${Math.round(gib(swap.ramTotal))}G`, inner, "muted"));
|
|
1360
|
-
if (swap.gpus.length) out.push(this.field("ENERGY", `${(
|
|
1398
|
+
if (swap.gpus.length) out.push(this.field("ENERGY", `${(totalWh() / 1000).toFixed(3)} kWh`, inner, "muted"));
|
|
1361
1399
|
out.push(this.costRow(inner));
|
|
1362
1400
|
return out;
|
|
1363
1401
|
}
|
|
@@ -1466,6 +1504,7 @@ interface MagiUnitConfig {
|
|
|
1466
1504
|
type MagiConfig = Partial<Record<MagiUnit, MagiUnitConfig>> & {
|
|
1467
1505
|
ui?: { compact?: boolean; kwhPrice?: number; currency?: Currency };
|
|
1468
1506
|
loads?: Record<string, number>; // real model id → ms its last load took
|
|
1507
|
+
totalWh?: number; // GPU energy summed over every session, for the COST row
|
|
1469
1508
|
};
|
|
1470
1509
|
|
|
1471
1510
|
const MAGI_CONFIG_PATH = join(homedir(), ".pi", "agent", "magi.json");
|
|
@@ -1807,6 +1846,7 @@ async function configureMagi(ctx: ExtensionContext): Promise<void> {
|
|
|
1807
1846
|
function footerLeft(th: Theme, now = Date.now()): string {
|
|
1808
1847
|
const dim = (s: string) => th.fg("dim", s);
|
|
1809
1848
|
const step = Math.floor(now / ANIM_STEP_MS);
|
|
1849
|
+
const slowStep = Math.floor(now / SEPHIRAH_STEP_MS);
|
|
1810
1850
|
const lights = (fn: (i: number) => NodeLight) => SEPHIROT.map((_, i) => fn(i));
|
|
1811
1851
|
|
|
1812
1852
|
if (state.compacting) {
|
|
@@ -1860,7 +1900,7 @@ function footerLeft(th: Theme, now = Date.now()): string {
|
|
|
1860
1900
|
);
|
|
1861
1901
|
}
|
|
1862
1902
|
if (state.phase === "thinking") {
|
|
1863
|
-
const cur =
|
|
1903
|
+
const cur = slowStep % 3; // Keter, Chokmah, Binah
|
|
1864
1904
|
const s = SEPHIROT[cur]!;
|
|
1865
1905
|
return (
|
|
1866
1906
|
th.fg("accent", "◆ ") +
|
|
@@ -1870,7 +1910,7 @@ function footerLeft(th: Theme, now = Date.now()): string {
|
|
|
1870
1910
|
);
|
|
1871
1911
|
}
|
|
1872
1912
|
if (state.phase === "responding") {
|
|
1873
|
-
const cur = 5 + (
|
|
1913
|
+
const cur = 5 + (slowStep % 5); // Tiferet → Malkuth
|
|
1874
1914
|
const s = SEPHIROT[cur]!;
|
|
1875
1915
|
const tps = liveTps();
|
|
1876
1916
|
return (
|
|
@@ -1908,9 +1948,21 @@ function footerLeft(th: Theme, now = Date.now()): string {
|
|
|
1908
1948
|
|
|
1909
1949
|
/** Footer: the animation on the left, other extensions' statuses on the right. */
|
|
1910
1950
|
function buildFooter(tui: TUI, theme: Theme, footerData: any) {
|
|
1951
|
+
let scrolling = false;
|
|
1952
|
+
let scrollOff = 0;
|
|
1953
|
+
let scrollPeriod = 1;
|
|
1954
|
+
let scrollLine = "";
|
|
1911
1955
|
const timer = setInterval(() => {
|
|
1912
1956
|
if (animating()) tui.requestRender();
|
|
1913
1957
|
}, ANIM_STEP_MS);
|
|
1958
|
+
// The marquee keeps its own cadence: one cell per tick, never derived from the clock, so it
|
|
1959
|
+
// does not stutter when the line is re-rendered at some other pace (animation, streamed tokens)
|
|
1960
|
+
// nor jump when the left side changes length (sephirah name, tok/s) and with it the loop period.
|
|
1961
|
+
const scrollTimer = setInterval(() => {
|
|
1962
|
+
if (!scrolling) return;
|
|
1963
|
+
scrollOff = (scrollOff + 1) % scrollPeriod;
|
|
1964
|
+
tui.requestRender();
|
|
1965
|
+
}, FOOTER_SCROLL_MS);
|
|
1914
1966
|
|
|
1915
1967
|
const unsub = footerData?.onBranchChange?.(() => tui.requestRender());
|
|
1916
1968
|
|
|
@@ -1919,14 +1971,30 @@ function buildFooter(tui: TUI, theme: Theme, footerData: any) {
|
|
|
1919
1971
|
const dim = (s: string) => theme.fg("dim", s);
|
|
1920
1972
|
const statuses = footerData?.getExtensionStatuses?.();
|
|
1921
1973
|
const right = statuses ? [...statuses.values()].filter(Boolean).join(dim(" │ ")) : "";
|
|
1922
|
-
const
|
|
1923
|
-
const
|
|
1924
|
-
|
|
1925
|
-
|
|
1974
|
+
const left = footerLeft(theme);
|
|
1975
|
+
const gap = width - visibleWidth(left) - visibleWidth(right);
|
|
1976
|
+
scrolling = gap < 1;
|
|
1977
|
+
if (!scrolling) {
|
|
1978
|
+
scrollLine = "";
|
|
1979
|
+
scrollOff = 0;
|
|
1980
|
+
return [left + " ".repeat(gap) + right];
|
|
1981
|
+
}
|
|
1982
|
+
// Too narrow to fit: scroll the whole line instead of cutting it off. The line is frozen
|
|
1983
|
+
// while it scrolls — live text changes width (seconds, tok/s, sephirah names) and every
|
|
1984
|
+
// change would shift it under the window. A fresh line is taken when it has the same
|
|
1985
|
+
// width, so nothing moves, otherwise at the end of the loop.
|
|
1986
|
+
const line = left + dim(FOOTER_SCROLL_GAP) + right + dim(FOOTER_SCROLL_GAP);
|
|
1987
|
+
const period = Math.max(1, visibleWidth(line));
|
|
1988
|
+
if (!scrollLine || scrollOff === 0 || period === scrollPeriod) {
|
|
1989
|
+
scrollLine = line;
|
|
1990
|
+
scrollPeriod = period;
|
|
1991
|
+
}
|
|
1992
|
+
return [sliceByColumn(scrollLine + scrollLine, scrollOff % scrollPeriod, width, true)];
|
|
1926
1993
|
},
|
|
1927
1994
|
invalidate() {},
|
|
1928
1995
|
dispose() {
|
|
1929
1996
|
clearInterval(timer);
|
|
1997
|
+
clearInterval(scrollTimer);
|
|
1930
1998
|
unsub?.();
|
|
1931
1999
|
},
|
|
1932
2000
|
};
|
|
@@ -2120,6 +2188,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2120
2188
|
ui.compact = cfg.ui?.compact ?? false;
|
|
2121
2189
|
ui.kwhPrice = cfg.ui?.kwhPrice;
|
|
2122
2190
|
ui.currency = cfg.ui?.currency === "USD" ? "USD" : "EUR";
|
|
2191
|
+
swap.diskWh = cfg.totalWh ?? 0;
|
|
2123
2192
|
if (ctx.mode !== "tui") return;
|
|
2124
2193
|
applyChrome(ctx);
|
|
2125
2194
|
// nothing is loaded at startup: a new session picks its MECHA unit, a resumed one shows whether its model is in VRAM
|
|
@@ -2173,6 +2242,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2173
2242
|
});
|
|
2174
2243
|
|
|
2175
2244
|
pi.on("session_shutdown", async () => {
|
|
2245
|
+
persistEnergy(true);
|
|
2176
2246
|
prewarm.abort?.abort();
|
|
2177
2247
|
clearInterval(metricsTimer);
|
|
2178
2248
|
metricsTimer = undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-magi-theme",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "MAGI SYSTEM theme + extension for pi (Evangelion fan art): MAGI control screen panel, three-model /magi council, MECHA SELECT model picker, angel-attack loading, seven-seal context gauge, llama-swap telemetry",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|