@stemtrooper/learningcode 0.4.7 → 0.4.9
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/extensions/footer.ts +58 -5
- package/package.json +1 -1
package/extensions/footer.ts
CHANGED
|
@@ -24,6 +24,10 @@ const TOKEN = process.env.SPARK_API_KEY || "";
|
|
|
24
24
|
/** Two minutes. Enough to stay current, rarely enough to be invisible. */
|
|
25
25
|
const POLL_MS = 120_000;
|
|
26
26
|
|
|
27
|
+
/** One braille frame per tick while the agent works. */
|
|
28
|
+
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
29
|
+
const SPIN_MS = 120;
|
|
30
|
+
|
|
27
31
|
type Quota = {
|
|
28
32
|
tokensUsed: number;
|
|
29
33
|
/** null means no daily cap, which is not the same as a cap of zero. */
|
|
@@ -88,8 +92,30 @@ async function fetchQuota(): Promise<Quota | null> {
|
|
|
88
92
|
export default function sparkFooter(pi: ExtensionAPI) {
|
|
89
93
|
let quota: Quota | null = null;
|
|
90
94
|
let timer: ReturnType<typeof setInterval> | undefined;
|
|
95
|
+
let spinTimer: ReturnType<typeof setInterval> | undefined;
|
|
96
|
+
let working = false;
|
|
97
|
+
let frame = 0;
|
|
91
98
|
let tui: TuiLike | undefined;
|
|
92
99
|
|
|
100
|
+
const repaint = () => tui?.requestRender?.();
|
|
101
|
+
|
|
102
|
+
const setWorking = (on: boolean) => {
|
|
103
|
+
working = on;
|
|
104
|
+
if (on) {
|
|
105
|
+
if (!spinTimer) {
|
|
106
|
+
spinTimer = setInterval(() => {
|
|
107
|
+
frame = (frame + 1) % SPINNER.length;
|
|
108
|
+
repaint();
|
|
109
|
+
}, SPIN_MS);
|
|
110
|
+
spinTimer.unref?.();
|
|
111
|
+
}
|
|
112
|
+
} else if (spinTimer) {
|
|
113
|
+
clearInterval(spinTimer);
|
|
114
|
+
spinTimer = undefined;
|
|
115
|
+
}
|
|
116
|
+
repaint();
|
|
117
|
+
};
|
|
118
|
+
|
|
93
119
|
pi.on("session_start", async (_event, ctx) => {
|
|
94
120
|
if (!ctx.hasUI || ctx.mode !== "tui") return;
|
|
95
121
|
|
|
@@ -105,15 +131,31 @@ export default function sparkFooter(pi: ExtensionAPI) {
|
|
|
105
131
|
tui = footerTui;
|
|
106
132
|
return {
|
|
107
133
|
render(width: number): string[] {
|
|
134
|
+
// Every path ends with the exit hint. Students asked how to quit,
|
|
135
|
+
// and the footer is the one line that is always on screen.
|
|
136
|
+
// (ctrl+c clears the editor; pressed twice, or on an empty editor
|
|
137
|
+
// via ctrl+d, it exits. These are Pi's default bindings.)
|
|
138
|
+
//
|
|
139
|
+
// While the agent works the hint line becomes the working
|
|
140
|
+
// indicator instead: an animated frame plus the interrupt key.
|
|
141
|
+
// Pi has its own status spinner, but the footer is the TLC-owned
|
|
142
|
+
// surface, so the motion lives here where it cannot be restyled
|
|
143
|
+
// away from the brand.
|
|
144
|
+
const idleHints = theme.fg("dim", " ctrl+c exit · esc interrupt");
|
|
145
|
+
const hints = working
|
|
146
|
+
? theme.fg("accent", ` ${SPINNER[frame]} working…`) +
|
|
147
|
+
theme.fg("muted", " · esc to interrupt")
|
|
148
|
+
: idleHints;
|
|
149
|
+
|
|
108
150
|
// Nothing to say off Spark, or no token: say so rather than draw a bar
|
|
109
151
|
// full of empties that reads as "you have spent nothing".
|
|
110
152
|
if (!TOKEN) {
|
|
111
|
-
return [theme.fg("dim", " not on Spark — /login, or use --model opencode-go/…")];
|
|
153
|
+
return [theme.fg("dim", " not on Spark — /login, or use --model opencode-go/…"), hints];
|
|
112
154
|
}
|
|
113
|
-
if (!quota) return [theme.fg("dim", " Spark quota unavailable")];
|
|
155
|
+
if (!quota) return [theme.fg("dim", " Spark quota unavailable"), hints];
|
|
114
156
|
|
|
115
157
|
if (quota.aiEnabled === false) {
|
|
116
|
-
return [theme.fg("error", " AI disabled for your account — ask your teacher")];
|
|
158
|
+
return [theme.fg("error", " AI disabled for your account — ask your teacher"), hints];
|
|
117
159
|
}
|
|
118
160
|
|
|
119
161
|
// No ceiling: no meter and no percentage, because a full bar implies a
|
|
@@ -122,6 +164,7 @@ export default function sparkFooter(pi: ExtensionAPI) {
|
|
|
122
164
|
return [
|
|
123
165
|
theme.fg("dim", ` ${Math.round(quota.tokensUsed / 1000)}k used today`) +
|
|
124
166
|
theme.fg("muted", " · unlimited"),
|
|
167
|
+
hints,
|
|
125
168
|
];
|
|
126
169
|
}
|
|
127
170
|
|
|
@@ -131,6 +174,7 @@ export default function sparkFooter(pi: ExtensionAPI) {
|
|
|
131
174
|
return [
|
|
132
175
|
theme.fg("dim", ` ${Math.round(quota.tokensUsed / 1000)}k used today`) +
|
|
133
176
|
theme.fg("muted", " · no daily token cap"),
|
|
177
|
+
hints,
|
|
134
178
|
];
|
|
135
179
|
}
|
|
136
180
|
|
|
@@ -141,13 +185,13 @@ export default function sparkFooter(pi: ExtensionAPI) {
|
|
|
141
185
|
|
|
142
186
|
// Narrow terminals keep the bar and the percentage only; the absolute
|
|
143
187
|
// token counts are the part that can be dropped.
|
|
144
|
-
if (width < 52) return [` ${bar} ${percent}
|
|
188
|
+
if (width < 52) return [` ${bar} ${percent}`, hints];
|
|
145
189
|
|
|
146
190
|
const numbers = theme.fg(
|
|
147
191
|
"dim",
|
|
148
192
|
`${Math.round(tokensUsed / 1000)}k / ${Math.round(limit / 1000)}k today`,
|
|
149
193
|
);
|
|
150
|
-
return [` ${bar} ${percent} ${numbers}
|
|
194
|
+
return [` ${bar} ${percent} ${numbers}`, hints];
|
|
151
195
|
},
|
|
152
196
|
};
|
|
153
197
|
});
|
|
@@ -158,10 +202,19 @@ export default function sparkFooter(pi: ExtensionAPI) {
|
|
|
158
202
|
});
|
|
159
203
|
|
|
160
204
|
pi.on("session_shutdown", async () => {
|
|
205
|
+
setWorking(false);
|
|
161
206
|
if (timer) clearInterval(timer);
|
|
162
207
|
timer = undefined;
|
|
163
208
|
tui = undefined;
|
|
164
209
|
});
|
|
210
|
+
|
|
211
|
+
pi.on("agent_start", async () => {
|
|
212
|
+
setWorking(true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
pi.on("agent_end", async () => {
|
|
216
|
+
setWorking(false);
|
|
217
|
+
});
|
|
165
218
|
}
|
|
166
219
|
|
|
167
220
|
// Exported for tests.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stemtrooper/learningcode",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9",
|
|
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",
|