pi-bedrouter 0.2.1 → 0.2.2
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/index.ts +14 -13
- package/package.json +1 -1
- package/src/footer.ts +18 -4
package/extensions/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
13
13
|
import { loadSettings, saveSettings, settingsPath, agentDir, type Settings } from "../src/settings.js";
|
|
14
14
|
import * as br from "../src/bedrouter.js";
|
|
15
15
|
import { fitNotes, piModels } from "../src/models.js";
|
|
16
|
-
import { fromHeaders, statusLine, type LastDecision } from "../src/footer.js";
|
|
16
|
+
import { downLine, fromHeaders, readyLine, restartingLine, statusLine, type LastDecision, type Paint } from "../src/footer.js";
|
|
17
17
|
|
|
18
18
|
export default async function (pi: ExtensionAPI) {
|
|
19
19
|
let settings = loadSettings();
|
|
@@ -28,7 +28,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
28
28
|
|
|
29
29
|
const isOurs = (ctx: ExtensionContext) => ctx.model?.provider === settings.providerName;
|
|
30
30
|
const setStatus = (ctx: ExtensionContext, text: string | undefined) => { lastCtx = ctx; if (ctx.hasUI && settings.footer) ctx.ui.setStatus("bedrouter", text); };
|
|
31
|
-
const
|
|
31
|
+
const paint = (ctx: ExtensionContext | null): Paint => (ctx?.hasUI && ctx.ui.theme ? (c, t) => ctx.ui.theme.fg(c, t) : (_c, t) => t);
|
|
32
|
+
const readyText = (ctx: ExtensionContext | null = lastCtx) => statusLine(last, stats, paint(ctx));
|
|
32
33
|
const notify = (ctx: ExtensionContext, msg: string, type: "info" | "warning" | "error" = "info") => { if (ctx.hasUI) ctx.ui.notify(msg, type); };
|
|
33
34
|
/** Show multi-line output (doctor/report/log) as a visible message without adding it to the model's context. */
|
|
34
35
|
const show = (title: string, body: string) => pi.sendMessage({ customType: "bedrouter", content: `**${title}**\n\n\`\`\`\n${body}\n\`\`\``, display: true }, { triggerTurn: false });
|
|
@@ -112,23 +113,23 @@ export default async function (pi: ExtensionAPI) {
|
|
|
112
113
|
|
|
113
114
|
pi.on("session_start", async (ev, ctx) => {
|
|
114
115
|
lastCtx = ctx;
|
|
115
|
-
if (ev.reason !== "startup" && ev.reason !== "new") { if (isOurs(ctx)) setStatus(ctx, statusLine(last, stats)); return; }
|
|
116
|
+
if (ev.reason !== "startup" && ev.reason !== "new") { if (isOurs(ctx)) setStatus(ctx, statusLine(last, stats, paint(ctx))); return; }
|
|
116
117
|
last = null; stats = null;
|
|
117
118
|
const h = await br.health(settings);
|
|
118
119
|
serverUp = !!h?.ok;
|
|
119
120
|
if (!h?.ok && settings.autoStart) {
|
|
120
121
|
const r = await bringUp(ctx, { install: true, start: true });
|
|
121
122
|
serverUp = r.ok;
|
|
122
|
-
if (!r.ok) { notify(ctx, `bedrouter: ${r.lines[r.lines.length - 1]}`, "warning"); setStatus(ctx, "
|
|
123
|
+
if (!r.ok) { notify(ctx, `bedrouter: ${r.lines[r.lines.length - 1]}`, "warning"); setStatus(ctx, downLine("/bedrouter start", paint(ctx))); return; }
|
|
123
124
|
const created = r.lines.filter((l) => l.startsWith("created "));
|
|
124
125
|
if (created.length) notify(ctx, created.join("\n"), "warning");
|
|
125
|
-
} else if (!h?.ok) { setStatus(ctx, "
|
|
126
|
+
} else if (!h?.ok) { setStatus(ctx, downLine("/bedrouter start", paint(ctx))); return; }
|
|
126
127
|
await autoSelect(ctx);
|
|
127
|
-
if (isOurs(ctx)) setStatus(ctx,
|
|
128
|
+
if (isOurs(ctx)) setStatus(ctx, readyText(ctx));
|
|
128
129
|
});
|
|
129
130
|
|
|
130
131
|
pi.on("model_select", async (ev, ctx) => {
|
|
131
|
-
if (ev.model.provider === settings.providerName) setStatus(ctx, statusLine(last, stats));
|
|
132
|
+
if (ev.model.provider === settings.providerName) setStatus(ctx, statusLine(last, stats, paint(ctx)));
|
|
132
133
|
else setStatus(ctx, undefined);
|
|
133
134
|
});
|
|
134
135
|
|
|
@@ -140,13 +141,13 @@ export default async function (pi: ExtensionAPI) {
|
|
|
140
141
|
last = d;
|
|
141
142
|
serverUp = true;
|
|
142
143
|
if (d.conversation) ourConversations.add(d.conversation);
|
|
143
|
-
setStatus(ctx, statusLine(last, stats));
|
|
144
|
+
setStatus(ctx, statusLine(last, stats, paint(ctx)));
|
|
144
145
|
});
|
|
145
146
|
|
|
146
147
|
pi.on("agent_end", async (_ev, ctx) => {
|
|
147
148
|
if (!isOurs(ctx) || !last?.conversation) return;
|
|
148
149
|
stats = await br.conversation(settings, last.conversation);
|
|
149
|
-
setStatus(ctx, statusLine(last, stats));
|
|
150
|
+
setStatus(ctx, statusLine(last, stats, paint(ctx)));
|
|
150
151
|
});
|
|
151
152
|
|
|
152
153
|
// ---- background health poll ----------------------------------------------------------------------------------
|
|
@@ -160,14 +161,14 @@ export default async function (pi: ExtensionAPI) {
|
|
|
160
161
|
if (up === serverUp) return;
|
|
161
162
|
serverUp = up;
|
|
162
163
|
if (up) { setStatus(ctx, readyText()); if (last) notify(ctx, "bedrouter: back up"); return; }
|
|
163
|
-
setStatus(ctx,
|
|
164
|
+
setStatus(ctx, settings.autoStart ? restartingLine(paint(ctx)) : downLine("/bedrouter start", paint(ctx)));
|
|
164
165
|
if (!settings.autoStart || Date.now() - restartAttemptAt < 60_000) return;
|
|
165
166
|
restartAttemptAt = Date.now();
|
|
166
167
|
// same path as startup: locate, build/install if needed, start
|
|
167
168
|
const r = await bringUp(ctx, { install: true, start: true });
|
|
168
169
|
serverUp = r.ok;
|
|
169
170
|
if (r.ok) { setStatus(ctx, readyText()); notify(ctx, `bedrouter: restarted`); }
|
|
170
|
-
else { setStatus(ctx, "
|
|
171
|
+
else { setStatus(ctx, downLine("/bedrouter start", paint(ctx))); notify(ctx, `bedrouter: restart failed — ${r.lines[r.lines.length - 1].split("\n")[0]}`, "warning"); }
|
|
171
172
|
}
|
|
172
173
|
if (settings.healthPollS > 0) { poll = setInterval(() => void checkHealth(), settings.healthPollS * 1000); poll.unref(); }
|
|
173
174
|
|
|
@@ -212,10 +213,10 @@ export default async function (pi: ExtensionAPI) {
|
|
|
212
213
|
const r = await bringUp(ctx, { install: false, start: true });
|
|
213
214
|
show(`bedrouter ${sub}`, r.lines.join("\n"));
|
|
214
215
|
serverUp = r.ok;
|
|
215
|
-
if (r.ok) { await autoSelect(ctx); setStatus(ctx, isOurs(ctx) ? readyText() : undefined); }
|
|
216
|
+
if (r.ok) { await autoSelect(ctx); setStatus(ctx, isOurs(ctx) ? readyText(ctx) : undefined); }
|
|
216
217
|
break;
|
|
217
218
|
}
|
|
218
|
-
case "stop": notify(ctx, await br.stop(settings)); serverUp = false; setStatus(ctx, isOurs(ctx) ? "
|
|
219
|
+
case "stop": notify(ctx, await br.stop(settings)); serverUp = false; setStatus(ctx, isOurs(ctx) ? downLine("/bedrouter start", paint(ctx)) : undefined); break;
|
|
219
220
|
case "install": {
|
|
220
221
|
notify(ctx, "bedrouter: installing/building (this can take a minute)…");
|
|
221
222
|
const r = await bringUp(ctx, { install: true, start: false });
|
package/package.json
CHANGED
package/src/footer.ts
CHANGED
|
@@ -13,10 +13,24 @@ export function fromHeaders(h: Record<string, string>): LastDecision | null {
|
|
|
13
13
|
const usd = (n: number) => (n >= 1 ? `$${n.toFixed(2)}` : `$${n.toFixed(4)}`);
|
|
14
14
|
const short = (reason: string) => reason.replace(/^keyword:/, "kw:").replace(/^classifier:/, "clf:").replace(/^upgrade:keyword:/, "up:kw:").replace(/^shape:/, "");
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Pi's footer sorts extension statuses by key, joins them with ONE space, and collapses runs of ASCII spaces, so two
|
|
18
|
+
* packages' lines run into each other. Each status therefore starts with a dim segment bar and ends with non-breaking
|
|
19
|
+
* spaces (which survive the collapse) as a right margin. `paint` is ctx.ui.theme.fg when a UI is present.
|
|
20
|
+
*/
|
|
21
|
+
export type Paint = (color: "success" | "error" | "warning" | "dim" | "accent", text: string) => string;
|
|
22
|
+
const plain: Paint = (_c, t) => t;
|
|
23
|
+
const NBSP2 = "\u00a0\u00a0";
|
|
24
|
+
export const segment = (body: string, paint: Paint = plain) => `${paint("dim", "│")} ${body}${NBSP2}`;
|
|
25
|
+
|
|
26
|
+
export function readyLine(paint: Paint = plain): string { return segment(`${paint("success", "✓")} bedrouter ready`, paint); }
|
|
27
|
+
export function downLine(hint: string, paint: Paint = plain): string { return segment(`${paint("error", "✗")} bedrouter down · ${hint}`, paint); }
|
|
28
|
+
export function restartingLine(paint: Paint = plain): string { return segment(`${paint("warning", "↻")} bedrouter restarting…`, paint); }
|
|
29
|
+
|
|
30
|
+
export function statusLine(d: LastDecision | null, c: ConversationStats | null, paint: Paint = plain): string {
|
|
31
|
+
if (!d) return readyLine(paint);
|
|
18
32
|
const arrow = d.model === d.requested ? "=" : "≠";
|
|
19
|
-
let s =
|
|
33
|
+
let s = `${paint("success", "✓")} bedrouter ⇄ ${d.model} ${arrow} ${d.requested} ${d.cls}·${short(d.reason)}`;
|
|
20
34
|
if (c && c.requests > 0) {
|
|
21
35
|
const spend = c.costUsd + c.classifierCostUsd;
|
|
22
36
|
// Routing effect = model cost vs the same tokens on the asked-for model. The classifier's one-off call is overhead
|
|
@@ -30,5 +44,5 @@ export function statusLine(d: LastDecision | null, c: ConversationStats | null):
|
|
|
30
44
|
else verdict = `+${usd(-diff)} over asked-for (routed up)${clf}`;
|
|
31
45
|
s += ` ${usd(spend)} · ${verdict}${c.escalations ? ` ↑${c.escalations}` : ""}`;
|
|
32
46
|
}
|
|
33
|
-
return s;
|
|
47
|
+
return segment(s, paint);
|
|
34
48
|
}
|