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 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 golem, the seven seals) is public domain.
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
  ![pi with the MAGI theme: MAGI SYSTEM header, MECHA SELECT model picker, side panel with the MAGI screen and llama-swap telemetry](https://raw.githubusercontent.com/b-iurea/pi-magi-theme/main/docs/screenshot.png)
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 golem, EMET ("truth") | a clay servant that acts | a **tool is running**, with the file or command it works on | panel + footer |
63
- | the golem, MET ("death") | the aleph is erased | a **tool failed** | panel + footer |
64
- | SYNC | the golem's obedience | **tool success rate** | panel + footer |
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 |
@@ -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 GOLEM acts: it is animated by EMET ("truth") while tools run, showing the file or command it works on;
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
- /** One MECHA head: dormant (dark, eyes off), waking (eyes flicker, the jaw lock breaks), active (armor lit, eyes pulse). */
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 = eyesOn ? shade(u.eyes, state === "active" ? 0.75 + 0.25 * Math.sin(frame / 3) : 1) : shade(u.eyes, 0.18);
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
- /** Golem obedience: share of tool calls that succeeded (null before the first tool). */
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 golem: tools ── */
1133
+ /* ── the unit at work: tools ── */
1151
1134
 
1152
- private golemArt(inner: number): string[] {
1153
- const th = this.theme;
1154
- const fallen = Date.now() - state.lastFailAt < FAIL_FLASH_MS;
1155
- const art = fallen ? GOLEM_FALLEN : this.frame % 4 < 2 ? GOLEM_STRIKE : GOLEM_REST;
1156
- const tone: ThemeColor = fallen ? "error" : "accent";
1157
- const left = " ".repeat(Math.max(0, Math.floor((inner - 20) / 2)));
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 golem (tools) ── */
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
- title = fallen ? `GOLEM FELL · MET · ${state.lastFailTool}` : `GOLEM · ${state.toolName || "tool"} · ${secs(state.phaseSince)}`;
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.golemArt(inner);
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 golem's obedience = tool success rate (CHESED ✓ / GEBURAH ✗)
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
- * - golem EMET / MET → tool running (with its file or command) / tool failed
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 th.fg("error", "✗ GOLEM · MET") + dim(" · ") + th.fg("error", `${state.lastFailTool} failed`) + target + dim(" ·") + tally + syncText;
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 hammer = ["▚", "▞"][step % 2]!;
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", `${hammer} GOLEM · `) +
1865
- th.bold(th.fg("warning", "EMET")) +
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 golem shows one of them, and leaves when none is left. */
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 golem shows which tool it prepares, not the previous one
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 erases the golem's aleph.
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 golem moves on to one still running
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.1",
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",