@stemtrooper/learningcode 0.4.10 → 0.4.11

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.
Binary file
@@ -265,6 +265,7 @@ async function main() {
265
265
  forced.push("--extension", join(here, "..", "extensions", "spark-quota.ts"));
266
266
  forced.push("--extension", join(here, "..", "extensions", "footer.ts"));
267
267
  forced.push("--extension", join(here, "..", "extensions", "banner.ts"));
268
+ forced.push("--extension", join(here, "..", "extensions", "orange-cat.ts"));
268
269
  if (process.env.LEARNINGCODE_PI_FLAGS) {
269
270
  forced.push(...process.env.LEARNINGCODE_PI_FLAGS.split(/\s+/).filter(Boolean));
270
271
  }
@@ -0,0 +1,131 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { Image, allocateImageId, getCapabilities } from "@earendil-works/pi-tui";
3
+ import type { Component, TUI, Theme } from "@earendil-works/pi-tui";
4
+
5
+ /**
6
+ * Micro orange cat (opencode-style footprint: 5 cells, 1 row).
7
+ * Sitting front-facing cat: orange ears, blinking eyes, flicking tail.
8
+ * True-pixel PNG (30x12px) where image protocols exist, half-blocks elsewhere.
9
+ * The loader-row spinner is also swapped to a small orange rotating square.
10
+ */
11
+
12
+ const FRAMES = [
13
+ "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAMCAYAAAB1Lg0yAAAARElEQVR4nGNkwAH+L2X4j02cMZqBkRrqmRgGCDCNWjzsg5oRl4SIiAjWVPo6rwmretFJdVjF37x5M5qqwWA0OzHQCwAACZMNF3vfv8oAAAAASUVORK5CYII=",
14
+ "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAMCAYAAAB1Lg0yAAAARElEQVR4nGNkwAH+L2X4j02cMZqBkRrqmRgGCDCNWjzsg5oRl4SIiAjWVPo6rwmretFJdVjF37x5M5qqwWA0OzHQCwAACZMNF3vfv8oAAAAASUVORK5CYII=",
15
+ "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAMCAYAAAB1Lg0yAAAAQ0lEQVR4nGNkwAH+L2X4j02cMZqBkRrqmRgGCDCNWjzsg5oFl8TJk9jF/zdNw5p6T57MIsliJoYBAkyjFtMLDFhQAwCZmQy+VnpibQAAAABJRU5ErkJggg==",
16
+ "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAMCAYAAAB1Lg0yAAAARElEQVR4nGNkwAH+L2X4j02cMZqBkRrqmRgGCDCNWjzsg5oRl4SIiAjWVPo6rwmretFJdVjF37x5M5qqwWA0OzHQCwAACZMNF3vfv8oAAAAASUVORK5CYII=",
17
+ "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAMCAYAAAB1Lg0yAAAAT0lEQVR4nGM8kcfwnwELsJjEwIhN/P9S7OoZo0lTz8QwQIBp1OJhH9SMuCRERESwpsbXeU1Y1YtOqsMq/ubNG6x2MDEMEGAatZheYMCCGgCBNA+6BCtJTQAAAABJRU5ErkJggg==",
18
+ "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAMCAYAAAB1Lg0yAAAARElEQVR4nGNkwAH+L2X4j02cMZqBkRrqmRgGCDCNWjzsg5oRl4SIiAjWVPo6rwmretFJdVjF37x5M5qqwWA0OzHQCwAACZMNF3vfv8oAAAAASUVORK5CYII=",
19
+ "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAMCAYAAAB1Lg0yAAAATUlEQVR4nGNkwAH+L2X4j02cMZqBkRT1J09iN5+JYYAA06jFwz6oGXFJiIiIYE2lr/OasKoXnVSHVfzNmzdY7WBiGCDANGoxvcCABTUAbQkOTKbjrEAAAAAASUVORK5CYII=",
20
+ "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAMCAYAAAB1Lg0yAAAARElEQVR4nGNkwAH+L2X4j02cMZqBkRrqmRgGCDCNWjzsg5oRl4SIiAjWVPo6rwmretFJdVjF37x5M5qqwWA0OzHQCwAACZMNF3vfv8oAAAAASUVORK5CYII=",
21
+ ];
22
+
23
+ const SPIN = ["◰", "◳", "◲", "◱"].map((g) => "\x1b[38;2;255;165;0m" + g + "\x1b[39m");
24
+
25
+ // Braille fallback: 12x4 cells = 24x16 dots (4x half-block density).
26
+ // Orange silhouette of assets/cat.webp; eyes carved (blink), tail shifts.
27
+ const BRAILLE_SEQ: string[][] = [
28
+ ["⠓⣄⣠⠚"],
29
+ ["⠓⣄⣠⠚"],
30
+ ["⠓⣤⣤⠚"],
31
+ ["⠓⣄⣠⠚"],
32
+ ["⠒⣄⣠⠒"],
33
+ ["⠓⣄⣠⠚"]
34
+ ];
35
+ const FB_TRAVEL = 4;
36
+
37
+ function mirrorBraille(line: string): string {
38
+ // strip ANSI, mirror dot bits left-right, re-wrap
39
+ const core = line.replace(/\[[0-9;]*m/g, "");
40
+ let out = "";
41
+ for (let i = core.length - 1; i >= 0; i--) {
42
+ const b = core.charCodeAt(i)! - 0x2800;
43
+ const m =
44
+ ((b & 0x01) << 3) | ((b & 0x08) >> 3) |
45
+ ((b & 0x02) << 3) | ((b & 0x10) >> 3) |
46
+ ((b & 0x04) << 3) | ((b & 0x20) >> 3) |
47
+ ((b & 0x40) << 1) | ((b & 0x80) >> 1);
48
+ out += String.fromCharCode(0x2800 + m);
49
+ }
50
+ return "" + out + "";
51
+ }
52
+
53
+ function fallbackFrames(): string[][] {
54
+ const frames: string[][] = [];
55
+ for (let i = 0; i <= FB_TRAVEL; i++) {
56
+ const s = BRAILLE_SEQ[i % BRAILLE_SEQ.length]!;
57
+ frames.push(s.map((l) => " ".repeat(i) + l));
58
+ }
59
+ for (let i = FB_TRAVEL; i >= 0; i--) {
60
+ const s = BRAILLE_SEQ[i % BRAILLE_SEQ.length]!;
61
+ frames.push(s.map((l) => " ".repeat(i) + mirrorBraille(l)));
62
+ }
63
+ return frames;
64
+ }
65
+
66
+ class CatWidget implements Component {
67
+ private idx = 0;
68
+ private imageId = allocateImageId();
69
+ private fallback = fallbackFrames();
70
+ private images: Image[] = [];
71
+ private tui!: TUI;
72
+ constructor(tui: TUI, theme: Theme) {
73
+ this.tui = tui;
74
+ const fallbackColor = (s: string) => s;
75
+ for (const f of FRAMES) {
76
+ this.images.push(new Image(f, "image/png", { fallbackColor }, { imageId: this.imageId }));
77
+ }
78
+ void theme;
79
+ }
80
+ advance() {
81
+ this.idx = (this.idx + 1) % FRAMES.length;
82
+ this.invalidate();
83
+ this.tui.requestRender?.();
84
+ }
85
+ invalidate() {
86
+ for (const im of this.images) im.invalidate();
87
+ }
88
+ render(width: number): string[] {
89
+ if (!getCapabilities().images) return this.fallback[this.idx % this.fallback.length]!;
90
+ return this.images[this.idx % this.images.length]!.render(width);
91
+ }
92
+ }
93
+
94
+ export default function (pi: ExtensionAPI) {
95
+ pi.registerCommand("cat", {
96
+ description: "Show which cat renderer is active (pixel PNG or block fallback).",
97
+ handler: async (_args, ctx) => {
98
+ const caps = getCapabilities();
99
+ ctx.ui.notify(
100
+ "cat renderer: " + (caps.images ? "true-pixel PNG (" + caps.images + ")" : "block fallback (no image protocol)"),
101
+ "info",
102
+ );
103
+ },
104
+ });
105
+ let widget: CatWidget | null = null;
106
+ let timer: ReturnType<typeof setInterval> | null = null;
107
+
108
+ pi.on("session_start", async (_e, ctx: ExtensionContext) => {
109
+ ctx.ui.setWorkingIndicator({ frames: SPIN, intervalMs: 100 });
110
+ });
111
+
112
+ pi.on("agent_start", async (_e, ctx: ExtensionContext) => {
113
+ if (!ctx.hasUI || ctx.mode !== "tui") return;
114
+ if (timer) clearInterval(timer);
115
+ widget = null;
116
+ ctx.ui.setWidget("tiny-cat", (tui, theme) => {
117
+ widget = new CatWidget(tui, theme as unknown as Theme);
118
+ return widget;
119
+ });
120
+ timer = setInterval(() => widget?.advance(), 220);
121
+ });
122
+
123
+ const stop = async (_e: unknown, ctx: ExtensionContext) => {
124
+ if (timer) clearInterval(timer);
125
+ timer = null;
126
+ widget = null;
127
+ ctx.ui.setWidget("tiny-cat", undefined);
128
+ };
129
+ pi.on("agent_settled", stop as never);
130
+ pi.on("session_shutdown", stop as never);
131
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stemtrooper/learningcode",
3
- "version": "0.4.10",
3
+ "version": "0.4.11",
4
4
  "description": "TLC Spark coding agent for students: Pi wired to the Spark OpenAI-compatible endpoint with per-student tokens, quota and seat-queue awareness.",
5
5
  "type": "module",
6
6
  "license": "MIT",