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:
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user