pi-magi-theme 0.2.1 → 0.2.3
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 -4
- package/extensions/magi/index.ts +66 -65
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A three-mind council theme + extension for [pi](https://pi.dev): a MAGI SYSTEM header, the three MAGI as their control screen in a fixed side panel, live llama-swap telemetry, and `/magi`, a council of three models that votes on your engineering questions and reviews your pending changes.
|
|
4
4
|
|
|
5
|
-
Fan-art theme inspired by Neon Genesis Evangelion: the MAGI and their screen belong to their creators, all rights reserved to khara, Inc. This project is not affiliated with them. The rest of the symbolism (the Tree of Life, the
|
|
5
|
+
Fan-art theme inspired by Neon Genesis Evangelion: the MAGI and their screen belong to their creators, all rights reserved to khara, Inc. This project is not affiliated with them. The rest of the symbolism (the Tree of Life, the sephirot, the seven seals) is public domain.
|
|
6
6
|
|
|
7
7
|

|
|
8
8
|
|
|
@@ -59,9 +59,9 @@ Every symbol stands for something real the agent is doing.
|
|
|
59
59
|
| red spreading through BALTHASAR, MELCHIOR, CASPAR | an angel hacking the MAGI | the model is **being loaded into VRAM**, at the pace of its last load; CASPAR's last corner blinks once everything else has fallen | panel |
|
|
60
60
|
| blue taking the MAGI back from that corner | the attack repelled | the model is **loaded** | panel |
|
|
61
61
|
| MECHA-I · II · III · LEGION | units waiting for a pilot | the **llama-swap models**: dormant, waking while loading, eyes lit in VRAM | MECHA SELECT |
|
|
62
|
-
| the
|
|
63
|
-
| the
|
|
64
|
-
| SYNC | the
|
|
62
|
+
| the session's unit, in sync | the unit acts under its pilot | a **tool is running**, with the file or command it works on | panel + footer |
|
|
63
|
+
| the unit going berserk | the leash breaks: red eyes, jaw wide open | a **tool failed** | panel + footer |
|
|
64
|
+
| SYNC | the unit's sync ratio | **tool success rate** | panel + footer |
|
|
65
65
|
| CHESED ✓ / GEBURAH ✗ | mercy / severity | **successful / failed tools** | panel + footer |
|
|
66
66
|
| the seven seals | the end of an age | **context window usage**, one seal per seventh | panel |
|
|
67
67
|
| the sixth seal blinking | the last warning | context **close to compaction**: compact now instead of mid-task | panel + footer |
|
package/extensions/magi/index.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* to Malkuth while it answers, ascending while the model is loaded into VRAM, at rest in Malkuth when idle
|
|
8
8
|
* - the three MAGI (MELCHIOR / BALTHASAR / CASPAR) flicker while the agent works, showing what it is doing,
|
|
9
9
|
* and form the /magi council: three real models voting (on a question, or on the pending git diff)
|
|
10
|
-
* - the
|
|
10
|
+
* - the session's MECHA unit acts while tools run, showing the file or command it works on, and goes berserk when one fails;
|
|
11
11
|
* a failing tool erases the aleph and EMET becomes MET ("death"). SYNC is the tool success rate
|
|
12
12
|
* - CHESED (mercy) and GEBURAH (severity) count successful and failed tools
|
|
13
13
|
* - the SEVEN SEALS measure the context window; the sixth warns before compaction, the seventh opens when it runs
|
|
@@ -97,42 +97,10 @@ function renderPath(th: Theme, lights: NodeLight[]): string {
|
|
|
97
97
|
return out;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
/** The golem, 20×8 cells: at rest, striking (arms raised), and fallen (EMET → MET). */
|
|
101
|
-
const GOLEM_REST = [
|
|
102
|
-
" ▄██████▄ ",
|
|
103
|
-
" █ EMET █ ",
|
|
104
|
-
" ▀██████▀ ",
|
|
105
|
-
" ▄████████████▄ ",
|
|
106
|
-
" ██ ████████ ██ ",
|
|
107
|
-
" ▀▀ ████████ ▀▀ ",
|
|
108
|
-
" ███ ███ ",
|
|
109
|
-
" ▀▀▀▀ ▀▀▀▀ ",
|
|
110
|
-
];
|
|
111
|
-
const GOLEM_STRIKE = [
|
|
112
|
-
" ▄▄ ▄██████▄ ▄▄ ",
|
|
113
|
-
" ██ █ EMET █ ██ ",
|
|
114
|
-
" ██ ▀██████▀ ██ ",
|
|
115
|
-
" ▀████████████████▀ ",
|
|
116
|
-
" ████████ ",
|
|
117
|
-
" ████████ ",
|
|
118
|
-
" ███ ███ ",
|
|
119
|
-
" ▀▀▀▀ ▀▀▀▀ ",
|
|
120
|
-
];
|
|
121
|
-
const GOLEM_FALLEN = [
|
|
122
|
-
" ▄██████▄ ",
|
|
123
|
-
" █ MET █ ",
|
|
124
|
-
" ▀██████▀ ",
|
|
125
|
-
" ▄████████████▄ ",
|
|
126
|
-
" ██ ██░███░█ ██ ",
|
|
127
|
-
" ▀▀ █░████░█ ▀▀ ",
|
|
128
|
-
" ███ ███ ",
|
|
129
|
-
" ▀▀▀▀ ▀▀▀▀ ",
|
|
130
|
-
];
|
|
131
|
-
|
|
132
100
|
/* ── MECHA units: the heads of the model picker, one unit per model llama-swap runs ── */
|
|
133
101
|
|
|
134
102
|
type MechaUnit = "I" | "II" | "III" | "LEGION";
|
|
135
|
-
type MechaState = "dormant" | "waking" | "active";
|
|
103
|
+
type MechaState = "dormant" | "waking" | "active" | "berserk";
|
|
136
104
|
type Rgb = readonly [number, number, number];
|
|
137
105
|
|
|
138
106
|
/**
|
|
@@ -243,17 +211,31 @@ const MECHA: Record<MechaUnit, { name: string; armor: Rgb; accent: Rgb; eyes: Rg
|
|
|
243
211
|
},
|
|
244
212
|
};
|
|
245
213
|
|
|
214
|
+
/** Every head is drawn on the same 13×7 grid. */
|
|
215
|
+
const MECHA_W = 13;
|
|
216
|
+
|
|
246
217
|
const rgb = ([r, g, b]: Rgb, s: string) => `\x1b[38;2;${r};${g};${b}m${s}\x1b[39m`;
|
|
247
218
|
const shade = (c: Rgb, k: number): Rgb => [Math.round(c[0] * k), Math.round(c[1] * k), Math.round(c[2] * k)];
|
|
248
219
|
|
|
249
|
-
/**
|
|
220
|
+
/** The berserk eyes: the unit off its leash sees in red, whatever colour its own eyes are. */
|
|
221
|
+
const BERSERK_EYES: Rgb = [255, 40, 40];
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* One MECHA head: dormant (dark, eyes off), waking (eyes flicker, the jaw lock breaks),
|
|
225
|
+
* active (armor lit, eyes pulse), berserk (jaw wide open, red eyes, a tool just failed).
|
|
226
|
+
*/
|
|
250
227
|
function renderMechaHead(unit: MechaUnit, state: MechaState, frame: number): string[] {
|
|
251
228
|
const u = MECHA[unit];
|
|
252
|
-
const jawOpen = state === "waking" && frame % 8 < 4;
|
|
253
|
-
const eyesOn = state === "active" || (state === "waking" && flicker(frame, 7));
|
|
229
|
+
const jawOpen = state === "berserk" || (state === "waking" && frame % 8 < 4);
|
|
230
|
+
const eyesOn = state === "active" || state === "berserk" || (state === "waking" && flicker(frame, 7));
|
|
254
231
|
const armor = state === "dormant" ? shade(u.armor, 0.3) : state === "waking" ? shade(u.armor, 0.7) : u.armor;
|
|
255
232
|
const accent = state === "dormant" ? shade(u.accent, 0.3) : u.accent;
|
|
256
|
-
const eyes =
|
|
233
|
+
const eyes =
|
|
234
|
+
state === "berserk"
|
|
235
|
+
? shade(BERSERK_EYES, 0.7 + 0.3 * Math.sin(frame / 1.5)) // the roar throbs faster than the sync pulse
|
|
236
|
+
: eyesOn
|
|
237
|
+
? shade(u.eyes, state === "active" ? 0.75 + 0.25 * Math.sin(frame / 3) : 1)
|
|
238
|
+
: shade(u.eyes, 0.18);
|
|
257
239
|
return u.art.map((row, r) => {
|
|
258
240
|
const jaw = jawOpen ? u.jaw.rows[r - u.jaw.at] : undefined;
|
|
259
241
|
const art = jaw ?? row;
|
|
@@ -323,6 +305,7 @@ const state = {
|
|
|
323
305
|
turns: 0,
|
|
324
306
|
toolOk: 0, // CHESED
|
|
325
307
|
toolFail: 0, // GEBURAH
|
|
308
|
+
unit: "I" as MechaUnit, // the session model's own unit: it is the one that acts while a tool runs
|
|
326
309
|
lastFailAt: 0,
|
|
327
310
|
lastFailTool: "",
|
|
328
311
|
lastFailTarget: "",
|
|
@@ -362,7 +345,7 @@ const REBIRTH_MS = 6000;
|
|
|
362
345
|
const DONE_TITLE_AFTER_MS = 30_000;
|
|
363
346
|
const SIXTH_SEAL_PERCENT = (6 / 7) * 100;
|
|
364
347
|
|
|
365
|
-
/**
|
|
348
|
+
/** Sync ratio: share of tool calls that succeeded (null before the first tool). */
|
|
366
349
|
function syncPercent(): number | null {
|
|
367
350
|
const done = state.toolOk + state.toolFail;
|
|
368
351
|
return done ? (state.toolOk / done) * 100 : null;
|
|
@@ -1147,23 +1130,18 @@ class MagiPanel implements Component {
|
|
|
1147
1130
|
return this.frameLine(" " + l + this.theme.fg(tone, value), inner);
|
|
1148
1131
|
}
|
|
1149
1132
|
|
|
1150
|
-
/* ── the
|
|
1133
|
+
/* ── the unit at work: tools ── */
|
|
1151
1134
|
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
const
|
|
1155
|
-
const art =
|
|
1156
|
-
const
|
|
1157
|
-
const left = " ".repeat(Math.max(0, Math.floor((inner -
|
|
1158
|
-
return art.map((line) =>
|
|
1159
|
-
const word = fallen ? "MET" : "EMET";
|
|
1160
|
-
const at = line.indexOf(word);
|
|
1161
|
-
if (at < 0) return left + th.fg(tone, line);
|
|
1162
|
-
return left + th.fg(tone, line.slice(0, at)) + th.bold(th.fg(fallen ? "error" : "warning", word)) + th.fg(tone, line.slice(at + word.length));
|
|
1163
|
-
});
|
|
1135
|
+
/** The session's own unit while a tool runs; berserk (and shaking) for as long as the failure flashes. */
|
|
1136
|
+
private mechaArt(inner: number): string[] {
|
|
1137
|
+
const berserk = Date.now() - state.lastFailAt < FAIL_FLASH_MS;
|
|
1138
|
+
const art = renderMechaHead(state.unit, berserk ? "berserk" : "active", this.frame);
|
|
1139
|
+
const shake = berserk ? (this.frame % 2 ? 1 : -1) : 0;
|
|
1140
|
+
const left = " ".repeat(Math.max(0, Math.floor((inner - MECHA_W) / 2) + shake));
|
|
1141
|
+
return art.map((line) => left + line);
|
|
1164
1142
|
}
|
|
1165
1143
|
|
|
1166
|
-
/* ── top of the panel: the MAGI (flickering while working) or the
|
|
1144
|
+
/* ── top of the panel: the MAGI (flickering while working) or the unit (tools) ── */
|
|
1167
1145
|
|
|
1168
1146
|
private councilSection(inner: number): string[] {
|
|
1169
1147
|
const th = this.theme;
|
|
@@ -1180,9 +1158,10 @@ class MagiPanel implements Component {
|
|
|
1180
1158
|
let status: string;
|
|
1181
1159
|
|
|
1182
1160
|
if (state.phase === "tool" || (idle && fallen)) {
|
|
1183
|
-
|
|
1161
|
+
const name = MECHA[state.unit].name;
|
|
1162
|
+
title = fallen ? `${name} BERSERK · ${state.lastFailTool}` : `${name} · ${state.toolName || "tool"} · ${secs(state.phaseSince)}`;
|
|
1184
1163
|
titleTone = fallen ? "error" : "warning";
|
|
1185
|
-
body = this.
|
|
1164
|
+
body = this.mechaArt(inner);
|
|
1186
1165
|
const target = (fallen ? state.lastFailTarget || state.lastFailTool : state.toolTarget || state.toolName) || "tool";
|
|
1187
1166
|
status = `${pulse} ${th.fg(fallen ? "error" : "text", truncateToWidth(target, inner - 4))}`;
|
|
1188
1167
|
} else {
|
|
@@ -1275,7 +1254,7 @@ class MagiPanel implements Component {
|
|
|
1275
1254
|
if (tokens.cost) out.push(this.field("API COST", `$${tokens.cost.toFixed(3)}`, inner, "muted"));
|
|
1276
1255
|
}
|
|
1277
1256
|
|
|
1278
|
-
// SYNC: the
|
|
1257
|
+
// SYNC: the unit's sync ratio = tool success rate (CHESED ✓ / GEBURAH ✗)
|
|
1279
1258
|
const sync = syncPercent();
|
|
1280
1259
|
out.push(
|
|
1281
1260
|
this.frameLine(
|
|
@@ -1818,7 +1797,7 @@ async function configureMagi(ctx: ExtensionContext): Promise<void> {
|
|
|
1818
1797
|
/**
|
|
1819
1798
|
* The footer's left side, one animation per real state of the agent:
|
|
1820
1799
|
* - seals breaking / seventh seal opened → context compaction
|
|
1821
|
-
* -
|
|
1800
|
+
* - unit in sync / berserk → tool running (with its file or command) / tool failed
|
|
1822
1801
|
* - light pulsing in the upper triad → thinking
|
|
1823
1802
|
* - light descending to Malkuth → streaming the answer
|
|
1824
1803
|
* - light ascending from Malkuth → loading the model into VRAM
|
|
@@ -1856,13 +1835,22 @@ function footerLeft(th: Theme, now = Date.now()): string {
|
|
|
1856
1835
|
const syncText = sync === null ? "" : dim(" · sync ") + th.fg(syncTone(sync), `${sync.toFixed(0)}%`);
|
|
1857
1836
|
if (now - state.lastFailAt < FAIL_FLASH_MS) {
|
|
1858
1837
|
const target = state.lastFailTarget ? dim(" ") + th.fg("error", truncateToWidth(state.lastFailTarget, 40)) : "";
|
|
1859
|
-
return
|
|
1838
|
+
return (
|
|
1839
|
+
th.fg("error", `✗ ${MECHA[state.unit].name} · `) +
|
|
1840
|
+
th.bold(th.fg("error", "BERSERK")) +
|
|
1841
|
+
dim(" · ") +
|
|
1842
|
+
th.fg("error", `${state.lastFailTool} failed`) +
|
|
1843
|
+
target +
|
|
1844
|
+
dim(" ·") +
|
|
1845
|
+
tally +
|
|
1846
|
+
syncText
|
|
1847
|
+
);
|
|
1860
1848
|
}
|
|
1861
|
-
const
|
|
1849
|
+
const roar = ["▚", "▞"][step % 2]!;
|
|
1862
1850
|
const target = state.toolTarget ? dim(" ") + th.fg("muted", truncateToWidth(state.toolTarget, 40)) : "";
|
|
1863
1851
|
return (
|
|
1864
|
-
th.fg("accent", `${
|
|
1865
|
-
th.bold(th.fg("warning", "
|
|
1852
|
+
th.fg("accent", `${roar} ${MECHA[state.unit].name} · `) +
|
|
1853
|
+
th.bold(th.fg("warning", "SYNC")) +
|
|
1866
1854
|
dim(" · ") +
|
|
1867
1855
|
th.fg("text", state.toolName || "tool") +
|
|
1868
1856
|
target +
|
|
@@ -2073,7 +2061,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2073
2061
|
let lastPoll = 0;
|
|
2074
2062
|
let titleTimer: ReturnType<typeof setInterval> | undefined;
|
|
2075
2063
|
let liveTimer: ReturnType<typeof setInterval> | undefined;
|
|
2076
|
-
/** Tools currently executing, by call id: the
|
|
2064
|
+
/** Tools currently executing, by call id: the unit shows one of them, and leaves when none is left. */
|
|
2077
2065
|
const runningTools = new Map<string, { name: string; target: string }>();
|
|
2078
2066
|
|
|
2079
2067
|
/** The picker has the keyboard: keys pressed there must not wake the default model. */
|
|
@@ -2112,8 +2100,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
2112
2100
|
}
|
|
2113
2101
|
};
|
|
2114
2102
|
|
|
2103
|
+
/** Which unit acts in the panel: the one the picker gave this session's model. LEGION for anything not llama-swap. */
|
|
2104
|
+
const refreshUnit = async (ctx: ExtensionContext, model: Model<any> | undefined = ctx.model) => {
|
|
2105
|
+
if (!model) return;
|
|
2106
|
+
try {
|
|
2107
|
+
const aliases = await swapAliases();
|
|
2108
|
+
state.unit = mechaUnitsOf(ctx, aliases).get(aliases.get(model.id) ?? model.id) ?? "LEGION";
|
|
2109
|
+
} catch {
|
|
2110
|
+
state.unit = "LEGION"; // llama-swap unreachable: no roster, so the unit is nameless
|
|
2111
|
+
}
|
|
2112
|
+
};
|
|
2113
|
+
|
|
2115
2114
|
pi.on("session_start", async (event, ctx) => {
|
|
2116
2115
|
liveCtx = ctx;
|
|
2116
|
+
void refreshUnit(ctx);
|
|
2117
2117
|
recountSession(ctx);
|
|
2118
2118
|
state.hasSmartCompact = pi.getCommands().some((c) => c.name.replace(/^\//, "") === "smart-compact");
|
|
2119
2119
|
const cfg = loadMagiConfig();
|
|
@@ -2168,6 +2168,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2168
2168
|
|
|
2169
2169
|
pi.on("model_select", async (event, ctx) => {
|
|
2170
2170
|
liveCtx = ctx;
|
|
2171
|
+
void refreshUnit(ctx, event.model);
|
|
2171
2172
|
if (ctx.mode === "tui") void preloadModel(ctx, event.model);
|
|
2172
2173
|
});
|
|
2173
2174
|
|
|
@@ -2212,7 +2213,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2212
2213
|
if (t === "thinking_start" || t === "thinking_delta") setPhase("thinking");
|
|
2213
2214
|
else if (t === "text_start" || t === "text_delta") setPhase("responding");
|
|
2214
2215
|
else if (t === "toolcall_start" || t === "toolcall_delta") {
|
|
2215
|
-
// the model is writing a tool call: the
|
|
2216
|
+
// the model is writing a tool call: the unit shows which tool it prepares, not the previous one
|
|
2216
2217
|
setPhase("tool");
|
|
2217
2218
|
const call = e.partial?.content?.[e.contentIndex];
|
|
2218
2219
|
if (call?.type === "toolCall") {
|
|
@@ -2259,7 +2260,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2259
2260
|
repaint();
|
|
2260
2261
|
});
|
|
2261
2262
|
|
|
2262
|
-
// CHESED (mercy) counts what worked, GEBURAH (severity) what failed; a failure
|
|
2263
|
+
// CHESED (mercy) counts what worked, GEBURAH (severity) what failed; a failure throws the unit berserk.
|
|
2263
2264
|
pi.on("tool_execution_end", async (event) => {
|
|
2264
2265
|
const tool = runningTools.get(event.toolCallId);
|
|
2265
2266
|
runningTools.delete(event.toolCallId);
|
|
@@ -2273,7 +2274,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2273
2274
|
}
|
|
2274
2275
|
const next = [...runningTools.values()][0];
|
|
2275
2276
|
if (next) {
|
|
2276
|
-
// parallel tools: the
|
|
2277
|
+
// parallel tools: the unit moves on to one still running
|
|
2277
2278
|
state.toolName = next.name;
|
|
2278
2279
|
state.toolTarget = next.target;
|
|
2279
2280
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-magi-theme",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
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",
|