pi-magi-theme 0.3.0 → 0.3.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 CHANGED
@@ -36,8 +36,8 @@ Clone the repo and point pi at it instead (edits in the repo are live on the nex
36
36
 
37
37
  | Command | What it does |
38
38
  |---------|--------------|
39
- | `/magi <question>` | the council answers a question (recent conversation as context) |
40
- | `/magi review [focus]` | the council reviews your pending changes (`git diff HEAD` plus untracked file names) before you commit |
39
+ | `/magi council <question>` | the council answers a question (recent conversation as context) |
40
+ | `/magi council review [focus]` | the council reviews your pending changes (`git diff HEAD` plus untracked file names) before you commit |
41
41
  | `/magi config` | pick a model for each MAGI |
42
42
  | `/magi mecha` | MECHA SELECT: pick the llama-swap model to activate, each shown as a mecha head lit by its real state |
43
43
  | `/magi compact` | toggle the compact side panel (basic info and animations only); remembered across sessions |
@@ -120,7 +120,7 @@ When the limit is reached llama.cpp does not abort the reply: it inserts the clo
120
120
 
121
121
  ## The council
122
122
 
123
- `/magi <question>` asks three models in parallel, each with its own nature, then shows the votes and a majority verdict:
123
+ `/magi council <question>` asks three models in parallel, each with its own nature, then shows the votes and a majority verdict:
124
124
 
125
125
  | Unit | Nature | Looks at |
126
126
  |------|--------|----------|
@@ -130,7 +130,7 @@ When the limit is reached llama.cpp does not abort the reply: it inserts the clo
130
130
 
131
131
  Each nature is a lens, not a specialty, so the council answers any question, not only software ones. Every MAGI first answers the question, then judges it through its lens, naming concrete tools, numbers and scenarios from your question instead of generic advice. Votes: **APPROVE** = go ahead or clear recommendation; **CONDITIONAL** = only if the named conditions hold, or when information is missing (it says what it needs); **REJECT** = a concrete problem, with what to do instead. A MAGI never rejects because a topic is outside its nature. Answers come back in the language of your question.
132
132
 
133
- `/magi <question>` gives the MAGI the recent conversation as context; `/magi review` gives them the pending diff (truncated at 24k characters). Full opinions are added to the chat (not sent to the agent), and the last verdict stays under the MAGI in the side panel.
133
+ `/magi council <question>` gives the MAGI the recent conversation as context; `/magi council review` gives them the pending diff (truncated at 24k characters). Full opinions are added to the chat (not sent to the agent), and the last verdict stays under the MAGI in the side panel.
134
134
 
135
135
  ## Configuration
136
136
 
@@ -166,6 +166,8 @@ No token: npmjs is configured to trust this repository's `publish.yml` (npm trus
166
166
 
167
167
  ## llama-swap
168
168
 
169
+ **Thinking levels.** [pi-llama-swap](https://www.npmjs.com/package/@danielmeneses/pi-llama-swap) registers every model with reasoning off, so `/thinking` only offers `off`. At session start MAGI reads the reasoning levels llama-swap publishes in `/v1/models` (`meta.llamaswap.reasoning.levels`) and re-registers those models with thinking on: `/thinking` then offers exactly those levels, sent as `chat_template_kwargs` (`enable_thinking`, `reasoning_effort`). Aliases (e.g. the Instruct twin of a Thinking model) stay off, and image input follows `architecture.input_modalities`. A new or renamed model works without a `modelOverrides` entry in `models.json`; entries you keep there still apply on top (e.g. `samplingParams`). The starting level is pi's usual one for a model switch: the level saved for that model (`Ctrl+S` in `/thinking`), else `defaultThinkingLevel`.
170
+
169
171
  When the session model uses the `llama-swap` provider, the side panel:
170
172
 
171
173
  - loads nothing at startup: a new session opens MECHA SELECT, a resumed one shows whether its model is already in VRAM;
@@ -907,6 +907,56 @@ async function swapAliases(): Promise<Map<string, string>> {
907
907
  return aliases;
908
908
  }
909
909
 
910
+ const PI_THINKING_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"];
911
+
912
+ /**
913
+ * pi-llama-swap registers every model with reasoning off, so /thinking only offers "off".
914
+ * llama-swap publishes each model's reasoning levels in /v1/models: re-register the provider with them,
915
+ * sent as chat_template_kwargs (enable_thinking + reasoning_effort). Aliases (the Instruct twins) stay off.
916
+ * models.json modelOverrides still apply on top.
917
+ */
918
+ async function enableSwapReasoning(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
919
+ const config = ctx.modelRegistry.getRegisteredProviderConfig("llama-swap") as any;
920
+ if (!config?.models?.length || !config.baseUrl) return;
921
+ try {
922
+ const headers: Record<string, string> = config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {};
923
+ const res = await fetch(`${config.baseUrl.replace(/\/$/, "")}/models`, { headers, signal: AbortSignal.timeout(5000) });
924
+ const { data } = (await res.json()) as { data?: any[] };
925
+ const meta = new Map((data ?? []).map((m) => [m.id, m]));
926
+ let changed = false;
927
+ const models = config.models.map((m: any) => {
928
+ const entry = meta.get(m.id);
929
+ const levels: string[] | undefined = entry?.meta?.llamaswap?.reasoning?.levels;
930
+ const input = entry?.architecture?.input_modalities?.includes("image") ? ["text", "image"] : ["text"];
931
+ if (!levels?.length || entry.meta.llamaswap.type === "alias") return { ...m, input };
932
+ changed = true;
933
+ return {
934
+ ...m,
935
+ input,
936
+ reasoning: true,
937
+ thinkingLevelMap: Object.fromEntries(PI_THINKING_LEVELS.map((l) => [l, levels.includes(l) ? l : null])),
938
+ compat: {
939
+ ...m.compat,
940
+ thinkingFormat: "chat-template",
941
+ chatTemplateKwargs: {
942
+ enable_thinking: { $var: "thinking.enabled" },
943
+ preserve_thinking: true,
944
+ reasoning_effort: { $var: "thinking.effort", omitWhenOff: true },
945
+ },
946
+ },
947
+ };
948
+ });
949
+ // ponytail: a /llama-swap refresh re-registers the plain models until the next session start
950
+ if (!changed) return;
951
+ ctx.modelRegistry.registerProvider("llama-swap", { ...config, models });
952
+ // the session already holds the old model object: swap in the new one
953
+ const fresh = ctx.model?.provider === "llama-swap" ? ctx.modelRegistry.find("llama-swap", ctx.model.id) : undefined;
954
+ if (fresh?.reasoning) await pi.setModel(fresh);
955
+ } catch {
956
+ // server unreachable: models stay as pi-llama-swap registered them
957
+ }
958
+ }
959
+
910
960
  /** Models llama-swap keeps in memory: real id → "ready" | "starting" | … (empty when the server is unreachable). */
911
961
  async function swapRunning(): Promise<Map<string, string>> {
912
962
  try {
@@ -1545,10 +1595,11 @@ type MagiConfig = Partial<Record<MagiUnit, MagiUnitConfig>> & {
1545
1595
 
1546
1596
  const MAGI_CONFIG_PATH = join(homedir(), ".pi", "agent", "magi.json");
1547
1597
  /** /magi arguments that manage the theme instead of asking the council. */
1548
- const UI_ARGS = /^(on|off|panel|compact|status|cost)$|^(hygiene|budget)(\s|$)/i; // anything else is a question
1598
+ const UI_ARGS = /^(on|off|panel|compact|status|cost)$|^(hygiene|budget)(\s|$)/i;
1549
1599
  /** /magi arguments offered by autocomplete: the full argument, and what it does. */
1550
1600
  const MAGI_ARGS: [string, string][] = [
1551
- ["review", "the council reviews your pending changes before you commit"],
1601
+ ["council", "ask the three MAGI a question (recent conversation as context)"],
1602
+ ["council review", "the council reviews your pending changes before you commit"],
1552
1603
  ["config", "pick a model for each MAGI"],
1553
1604
  ["mecha", "MECHA SELECT: pick the llama-swap model to activate"],
1554
1605
  ["status", "llama-swap report: speed, tokens, cache hits, errors per model"],
@@ -1628,7 +1679,7 @@ function conversationExcerpt(ctx: ExtensionContext, maxChars = 6000): string {
1628
1679
 
1629
1680
  const REVIEW_MAX_CHARS = 24_000;
1630
1681
 
1631
- /** The pending changes for /magi review: tracked changes against HEAD plus the names of untracked files. */
1682
+ /** The pending changes for /magi council review: tracked changes against HEAD plus the names of untracked files. */
1632
1683
  async function pendingChanges(cwd: string): Promise<{ diff: string; untracked: string[] }> {
1633
1684
  const git = (args: string[]) => promisify(execFile)("git", args, { cwd, maxBuffer: 32 * 1024 * 1024 }).then((r) => r.stdout);
1634
1685
  let diff: string;
@@ -2271,6 +2322,7 @@ export default function (pi: ExtensionAPI) {
2271
2322
  fixedBudget = { planning: tb.planning ?? BUDGET_DEFAULTS.planning, acting: tb.acting ?? BUDGET_DEFAULTS.acting };
2272
2323
  budgetMessage = tb.message ?? true;
2273
2324
  learned = tb.learned ?? {};
2325
+ await enableSwapReasoning(pi, ctx);
2274
2326
  // the local-model rules live next to AGENTS.md, created once so the user can edit them
2275
2327
  const rules = join(ctx.cwd, "MAGI.md");
2276
2328
  if (!existsSync(rules)) {
@@ -2600,8 +2652,12 @@ export default function (pi: ExtensionAPI) {
2600
2652
  return pickModel(ctx);
2601
2653
  }
2602
2654
 
2603
- if (arg === "review" || arg.startsWith("review ")) {
2604
- const focus = arg.slice("review".length).trim();
2655
+ if (arg !== "council" && !arg.startsWith("council ")) {
2656
+ return ctx.ui.notify(arg ? `Unknown /magi command "${arg}": to ask the MAGI, /magi council <question>` : "Ask the MAGI with /magi council <question>; type /magi and a space to see every command", arg ? "warning" : "info");
2657
+ }
2658
+ const ask = arg.slice("council".length).trim();
2659
+ if (ask === "review" || ask.startsWith("review ")) {
2660
+ const focus = ask.slice("review".length).trim();
2605
2661
  let changes: { diff: string; untracked: string[] };
2606
2662
  try {
2607
2663
  changes = await pendingChanges(ctx.cwd);
@@ -2624,7 +2680,7 @@ export default function (pi: ExtensionAPI) {
2624
2680
  return runCouncil(ctx, question, "```diff\n" + diff + "\n```" + untracked, "Pending changes (git diff HEAD)");
2625
2681
  }
2626
2682
 
2627
- const question = arg || (await ctx.ui.input("Question for the MAGI:", "should we …?"))?.trim() || "";
2683
+ const question = ask || (await ctx.ui.input("Question for the MAGI:", "should we …?"))?.trim() || "";
2628
2684
  if (!question) return;
2629
2685
  return runCouncil(ctx, question, conversationExcerpt(ctx));
2630
2686
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-magi-theme",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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",