daemon: integrate QoL slices 1-5 (model/rename/catalog, history deletes, stats, repo prepare+imageUsed+built) — 95.3% cover green

This commit is contained in:
Raphael Westphal
2026-08-19 10:07:52 +02:00
parent 7287b831e4
commit 698b742cb3
16 changed files with 1506 additions and 36 deletions
+92
View File
@@ -556,6 +556,8 @@ export default function (pi: ExtensionAPI): void {
if (f.type === "welcome") onWelcome(f as Record<string, unknown>);
else if (f.type === "prompt") deliverPrompt(f);
else if (f.type === "abort") doAbort();
else if (f.type === "set_model") applySetModel(f);
else if (f.type === "rename") applyRename(f);
// unknown types are ignored (forward compatibility)
}
@@ -587,6 +589,96 @@ export default function (pi: ExtensionAPI): void {
}
}
/** Daemon-requested model switch. Success is mirrored by the resulting
* model_select event (which emits session_info); every failure path
* emits error_notice so the web sees why nothing changed. */
function applySetModel(frame: {
modelId?: unknown;
provider?: unknown;
sessionId?: unknown;
}): void {
const modelId = frame.modelId;
const provider = frame.provider;
if (
typeof frame.sessionId === "string" &&
frame.sessionId !== currentSessionId
)
return;
if (typeof modelId !== "string" || modelId.length === 0) {
emit("error_notice", { reason: "set_model without modelId" });
return;
}
if (typeof provider !== "string" || provider.length === 0) {
emit("error_notice", { reason: "set_model without provider" });
return;
}
const registry = lastCtx?.modelRegistry as
| { find?: (provider: string, modelId: string) => unknown }
| undefined;
if (registry === undefined || typeof registry.find !== "function") {
emit("error_notice", { reason: "model registry unavailable" });
return;
}
let model: unknown;
try {
model = registry.find(provider, modelId);
} catch (err) {
log(`model lookup failed: ${err}`);
emit("error_notice", {
reason: `model lookup failed: ${String(err)}`,
});
return;
}
if (model === undefined || model === null) {
emit("error_notice", {
reason: `model not found: ${provider}/${modelId}`,
});
return;
}
const maybeSet = (pi as unknown as { setModel?: (m: unknown) => unknown })
.setModel;
if (typeof maybeSet !== "function") {
emit("error_notice", { reason: "pi.setModel unavailable" });
return;
}
try {
Promise.resolve(maybeSet.call(pi, model)).then(
(ok: unknown) => {
if (ok !== true)
emit("error_notice", {
reason: `setModel rejected ${provider}/${modelId} (no API key?)`,
});
},
(err: unknown) => {
log(`setModel failed: ${err}`);
emit("error_notice", {
reason: `setModel failed: ${String(err)}`,
});
},
);
} catch (err) {
log(`setModel threw: ${err}`);
emit("error_notice", { reason: `setModel failed: ${String(err)}` });
}
}
/** Daemon-requested rename. pi has no extension rename API: only the
* mirrored snapshot changes (PROTOCOL.md); the TUI session keeps its
* own name. session_info propagates the change to the daemon/web. */
function applyRename(frame: { name?: unknown; sessionId?: unknown }): void {
const name = frame.name;
if (typeof name !== "string" || name.length === 0) return;
if (
typeof frame.sessionId === "string" &&
frame.sessionId !== currentSessionId
)
return;
if (snapshot === null) return;
if (name === snapshot.name) return;
snapshot.name = name;
emit("session_info", { session: snapshot });
}
function buildSnapshot(
ctx: ExtensionContext,
sessionId: string,
+111
View File
@@ -37,9 +37,12 @@ interface FakePi {
handlers: Map<string, (event: unknown, ctx: unknown) => unknown>;
sentMessages: Array<{ message: string; options: unknown }>;
aborted: number;
setModelCalls: unknown[];
setModelResult: boolean;
on(event: string, handler: (event: unknown, ctx: unknown) => unknown): void;
sendUserMessage(message: string, options?: unknown): void;
abort(): void;
setModel(model: unknown): Promise<boolean>;
}
function makeFakePi(): FakePi {
@@ -47,6 +50,8 @@ function makeFakePi(): FakePi {
handlers: new Map(),
sentMessages: [],
aborted: 0,
setModelCalls: [],
setModelResult: true,
on(event, handler) {
this.handlers.set(event, handler);
},
@@ -56,6 +61,10 @@ function makeFakePi(): FakePi {
abort() {
this.aborted++;
},
setModel(model) {
this.setModelCalls.push(model);
return Promise.resolve(this.setModelResult);
},
};
}
@@ -63,6 +72,10 @@ function makeFakeCtx(): unknown {
return {
cwd: "/work/repo",
model: { id: "glm-5.3", provider: "zai-renaud" },
modelRegistry: {
find: (provider: string, id: string): unknown =>
id === "glm-5.3" ? { id, provider } : undefined,
},
sessionManager: {
getSessionId: () => SESSION_ID,
getSessionName: () => undefined,
@@ -342,6 +355,104 @@ async function main(): Promise<void> {
await waitFor(() => pi.aborted > 0, 2000);
check("3 abort calls pi.abort", pi.aborted === 1);
// --- Scenario 7: set_model / rename from the daemon ----------------
const pushControl = (type: string, extra: Record<string, unknown>): void => {
daemon.pushAll(
JSON.stringify({
v: 1,
type,
sessionId: SESSION_ID,
seq: 0,
ts: Date.now(),
...extra,
}),
);
};
pushControl("set_model", {
sessionId: "other-session",
provider: "zai-renaud",
modelId: "glm-5.3",
});
pushControl("set_model", {
provider: "zai-renaud",
modelId: "no-such-model",
});
const unknownNotice = await waitFor(
() =>
daemon.frames.some(
(f) =>
f.type === "error_notice" &&
(f.reason as string)?.includes("model not found"),
),
2000,
);
check(
"7 unknown model emits error_notice, foreign session ignored",
unknownNotice && pi.setModelCalls.length === 0,
JSON.stringify(daemon.frames.filter((f) => f.type === "error_notice")),
);
pushControl("set_model", { modelId: "glm-5.3" });
const noProvider = await waitFor(
() =>
daemon.frames.some(
(f) =>
f.type === "error_notice" &&
(f.reason as string) === "set_model without provider",
),
2000,
);
check("7 missing provider rejected", noProvider);
pushControl("set_model", { provider: "zai-renaud", modelId: "glm-5.3" });
await waitFor(() => pi.setModelCalls.length === 1, 2000);
check(
"7 set_model resolves via registry and calls pi.setModel",
pi.setModelCalls.length === 1 &&
JSON.stringify(pi.setModelCalls[0]) ===
JSON.stringify({ id: "glm-5.3", provider: "zai-renaud" }),
JSON.stringify(pi.setModelCalls),
);
pi.setModelResult = false;
pushControl("set_model", { provider: "zai-renaud", modelId: "glm-5.3" });
const rejected = await waitFor(
() =>
daemon.frames.some(
(f) =>
f.type === "error_notice" &&
(f.reason as string)?.startsWith("setModel rejected"),
),
2000,
);
check("7 setModel false mirrors error_notice", rejected);
pi.setModelResult = true;
fire("model_select", { model: { id: "glm-5.4", provider: "zai-renaud" } });
const infoSeen = await waitFor(
() =>
daemon.frames.some(
(f) =>
f.type === "session_info" &&
(f.session as { model?: string })?.model === "glm-5.4",
),
2000,
);
check("7 model_select emits session_info", infoSeen);
pushControl("rename", { name: "renamed-from-daemon" });
const renameSeen = await waitFor(
() =>
daemon.frames.some(
(f) =>
f.type === "session_info" &&
(f.session as { name?: string })?.name === "renamed-from-daemon",
),
2000,
);
check("7 rename updates snapshot + session_info", renameSeen);
// --- Scenario 4: drop + reconnect, replay, overflow ----------------
const seqBeforeDrop = Math.max(...daemon.frames.map((f) => f.seq));
welcomeLastSeq = seqBeforeDrop; // daemon has everything up to the drop