From fb7eb3105e1fbbdefdad9348f9132815b5bbe52d Mon Sep 17 00:00:00 2001 From: Adam Malczewski Date: Thu, 20 Aug 2026 19:36:12 +0900 Subject: feat(named-agents): sidebar view, synthetic provider, auto-save, snapshot semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the frontend of the Named Agents feature (plan §5-§9): - New features/named-agents module: pure logic (types re-exported from @dispatch/wire + transport-contract, validation, provider/model options, snapshot helpers, network normalizers, auto-save status model) + an injected auto-save controller (name debounce, immediate provider/model save, monotonic stale-response guard) + the collapsible sidebar card view. - Store: /named-agents CRUD + boot seed; selectModel(model, snapshot?) persists the raw model WITH the namedAgent snapshot; draft promotion carries it; refreshModel restores ModelResponse.namedAgent. - Chat store setNamedAgent (sends namedAgent on chat.send); Tab.snapshot + setSnapshot (retained across rename/remove, persisted with tabs). - Model selector: synthetic Named Agents provider ALWAYS first; selecting one resolves to the raw model + records a snapshot; a retained snapshot of a renamed/removed agent still displays as unavailable (plan §7). - Composition root wires the view (viewKinds + loaded-modules manifest) + feeds the selector. Tests: logic + controller + selector + app-store CRUD (1246 pass), typecheck 0, biome clean (only pre-existing config infos pending a future migrate), build OK. --- src/features/named-agents/logic/normalize.test.ts | 78 +++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/features/named-agents/logic/normalize.test.ts (limited to 'src/features/named-agents/logic/normalize.test.ts') diff --git a/src/features/named-agents/logic/normalize.test.ts b/src/features/named-agents/logic/normalize.test.ts new file mode 100644 index 0000000..5009194 --- /dev/null +++ b/src/features/named-agents/logic/normalize.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from "vitest"; +import { + normalizeNamedAgent, + normalizeNamedAgentRecord, + normalizeNamedAgents, + toNamedAgentInput, +} from "./normalize"; +import type { NamedAgent } from "./types"; + +describe("normalizeNamedAgent", () => { + it("coerces a well-formed record", () => { + expect(normalizeNamedAgent({ id: "x", name: "Coding", model: "opencode-1/claude" })).toEqual({ + id: "x", + name: "Coding", + model: "opencode-1/claude", + }); + }); + + it("rejects a record missing any field", () => { + expect(normalizeNamedAgent({ id: "x", name: "Coding" })).toBeNull(); + expect(normalizeNamedAgent({ name: "Coding", model: "m" })).toBeNull(); + expect(normalizeNamedAgent(null)).toBeNull(); + }); +}); + +describe("normalizeNamedAgents", () => { + it("coerces a list body", () => { + const agents: readonly NamedAgent[] = normalizeNamedAgents({ + namedAgents: [ + { id: "a", name: "Coding", model: "opencode-1/claude" }, + { id: "b", name: "Research", model: "umans/glm" }, + ], + }); + expect(agents).toHaveLength(2); + expect(agents[0]?.name).toBe("Coding"); + }); + + it("drops malformed entries and survives a missing list key", () => { + expect( + normalizeNamedAgents({ + namedAgents: [ + { id: "a", name: "x" }, + { id: "b", name: "y", model: "m" }, + ], + }).map((a) => a.id), + ).toEqual(["b"]); + expect(normalizeNamedAgents({})).toEqual([]); + expect(normalizeNamedAgents(null)).toEqual([]); + }); +}); + +describe("normalizeNamedAgentRecord", () => { + it("reads the canonical saved record", () => { + expect( + normalizeNamedAgentRecord({ + namedAgent: { id: "a", name: "Coding", model: "opencode-1/claude" }, + })?.id, + ).toBe("a"); + }); + + it("returns null for a malformed record", () => { + expect(normalizeNamedAgentRecord({ namedAgent: {} })).toBeNull(); + expect(normalizeNamedAgentRecord({})).toBeNull(); + }); +}); + +describe("toNamedAgentInput", () => { + it("trims a name and keeps the model", () => { + expect(toNamedAgentInput(" Coding ", "opencode-1/claude")).toEqual({ + name: "Coding", + model: "opencode-1/claude", + }); + }); + + it("defaults non-strings to empty", () => { + expect(toNamedAgentInput(null, 42)).toEqual({ name: "", model: "" }); + }); +}); -- cgit v1.2.3