@stemtrooper/learningcode 0.4.8 → 0.4.10
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/banner.ts +52 -4
- package/extensions/footer.ts +46 -1
- package/package.json +1 -1
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
|
}
|
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
|
|
|
@@ -109,7 +135,17 @@ export default function sparkFooter(pi: ExtensionAPI) {
|
|
|
109
135
|
// and the footer is the one line that is always on screen.
|
|
110
136
|
// (ctrl+c clears the editor; pressed twice, or on an empty editor
|
|
111
137
|
// via ctrl+d, it exits. These are Pi's default bindings.)
|
|
112
|
-
|
|
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;
|
|
113
149
|
|
|
114
150
|
// Nothing to say off Spark, or no token: say so rather than draw a bar
|
|
115
151
|
// full of empties that reads as "you have spent nothing".
|
|
@@ -166,10 +202,19 @@ export default function sparkFooter(pi: ExtensionAPI) {
|
|
|
166
202
|
});
|
|
167
203
|
|
|
168
204
|
pi.on("session_shutdown", async () => {
|
|
205
|
+
setWorking(false);
|
|
169
206
|
if (timer) clearInterval(timer);
|
|
170
207
|
timer = undefined;
|
|
171
208
|
tui = undefined;
|
|
172
209
|
});
|
|
210
|
+
|
|
211
|
+
pi.on("agent_start", async () => {
|
|
212
|
+
setWorking(true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
pi.on("agent_end", async () => {
|
|
216
|
+
setWorking(false);
|
|
217
|
+
});
|
|
173
218
|
}
|
|
174
219
|
|
|
175
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.10",
|
|
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",
|