@stemtrooper/learningcode 0.4.9 → 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.
- package/assets/cat.webp +0 -0
- package/bin/learningcode.mjs +1 -0
- package/extensions/banner.ts +52 -4
- package/extensions/orange-cat.ts +131 -0
- package/package.json +1 -1
package/assets/cat.webp
ADDED
|
Binary file
|
package/bin/learningcode.mjs
CHANGED
|
@@ -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
|
}
|
package/extensions/banner.ts
CHANGED
|
@@ -152,9 +152,57 @@ export function createBanner(theme: ThemeLike): HeaderComponent {
|
|
|
152
152
|
* right moment: extensions are loaded in print and JSON modes too, where there is
|
|
153
153
|
* no header to replace, and starting work in the factory would run for those.
|
|
154
154
|
*/
|
|
155
|
+
|
|
156
|
+
/** Terminal tab title. Just enough of the event context to set it. */
|
|
157
|
+
const TITLE = "learningcode by the learning curve";
|
|
158
|
+
type TitleContext = {
|
|
159
|
+
hasUI: boolean;
|
|
160
|
+
mode: string;
|
|
161
|
+
ui: { setTitle(title: string): void };
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Pi rewrites its own π title after extensions bind and again after its async
|
|
166
|
+
* startup package check on Windows, so one write is never enough. The title is
|
|
167
|
+
* set on every event where Pi rewrites its own (startup, renames, new turns),
|
|
168
|
+
* plus once deferred past the startup check. Each write is last-writer-wins in
|
|
169
|
+
* our favour on at least one of those paths.
|
|
170
|
+
*/
|
|
171
|
+
const TITLE_REASSERT_MS = 2500;
|
|
172
|
+
|
|
155
173
|
export default function learningcodeBanner(pi: ExtensionAPI) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
174
|
+
let titleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
175
|
+
|
|
176
|
+
const applyTitle = (ctx: TitleContext) => {
|
|
177
|
+
try {
|
|
178
|
+
ctx.ui.setTitle(TITLE);
|
|
179
|
+
} catch {
|
|
180
|
+
// Cosmetic only: a title failure must never break startup.
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const titleHook =
|
|
185
|
+
(defer: boolean) => async (_event: unknown, ctx: TitleContext) => {
|
|
186
|
+
if (!ctx.hasUI || ctx.mode !== "tui") return;
|
|
187
|
+
applyTitle(ctx);
|
|
188
|
+
if (!defer) return;
|
|
189
|
+
if (titleTimer) clearTimeout(titleTimer);
|
|
190
|
+
titleTimer = setTimeout(() => {
|
|
191
|
+
titleTimer = undefined;
|
|
192
|
+
applyTitle(ctx);
|
|
193
|
+
}, TITLE_REASSERT_MS);
|
|
194
|
+
titleTimer.unref?.();
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
198
|
+
if (!ctx.hasUI || ctx.mode !== "tui") return;
|
|
199
|
+
ctx.ui.setHeader((_tui, theme) => createBanner(theme as ThemeLike));
|
|
200
|
+
await titleHook(true)(_event, ctx);
|
|
201
|
+
});
|
|
202
|
+
pi.on("session_info_changed", titleHook(false));
|
|
203
|
+
pi.on("agent_start", titleHook(false));
|
|
204
|
+
pi.on("session_shutdown", async () => {
|
|
205
|
+
if (titleTimer) clearTimeout(titleTimer);
|
|
206
|
+
titleTimer = undefined;
|
|
207
|
+
});
|
|
160
208
|
}
|
|
@@ -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
|
+
["[38;2;255;165;0m⠓⣄⣠⠚[39m"],
|
|
29
|
+
["[38;2;255;165;0m⠓⣄⣠⠚[39m"],
|
|
30
|
+
["[38;2;255;165;0m⠓⣤⣤⠚[39m"],
|
|
31
|
+
["[38;2;255;165;0m⠓⣄⣠⠚[39m"],
|
|
32
|
+
["[38;2;255;165;0m⠒⣄⣠⠒[39m"],
|
|
33
|
+
["[38;2;255;165;0m⠓⣄⣠⠚[39m"]
|
|
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 "[38;2;255;165;0m" + out + "[39m";
|
|
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.
|
|
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",
|