summaryrefslogtreecommitdiffhomepage
path: root/src/features/named-agents/logic/validate.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-08-20 19:36:12 +0900
committerAdam Malczewski <[email protected]>2026-08-20 19:36:12 +0900
commitfb7eb3105e1fbbdefdad9348f9132815b5bbe52d (patch)
treedeb95e97a917424ea67470b168b91349a41d1c08 /src/features/named-agents/logic/validate.ts
parent63588c3c69b32d180dc78a2cc187cc8aa22ad7e0 (diff)
downloaddispatch-web-fb7eb3105e1fbbdefdad9348f9132815b5bbe52d.tar.gz
dispatch-web-fb7eb3105e1fbbdefdad9348f9132815b5bbe52d.zip
feat(named-agents): sidebar view, synthetic provider, auto-save, snapshot semantics
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.
Diffstat (limited to 'src/features/named-agents/logic/validate.ts')
-rw-r--r--src/features/named-agents/logic/validate.ts47
1 files changed, 47 insertions, 0 deletions
diff --git a/src/features/named-agents/logic/validate.ts b/src/features/named-agents/logic/validate.ts
new file mode 100644
index 0000000..c25524a
--- /dev/null
+++ b/src/features/named-agents/logic/validate.ts
@@ -0,0 +1,47 @@
+/**
+ * Pure validation for Named Agent form fields — zero DOM, zero effects.
+ *
+ * A Named Agent is usable only when (plan §5.4): the name is nonempty + unique
+ * case-insensitively, and a provider + model are selected. These pure helpers
+ * drive the card UI's validity gating + inline messages; the backend remains
+ * the authority (it 400s/conflicts on a duplicate or empty name / malformed
+ * model identifier).
+ */
+
+/** Trim + collapse runs of whitespace inside a name (survey-style display). */
+export function normalizeName(raw: string): string {
+ return raw.trim().replace(/\s+/g, " ");
+}
+
+/**
+ * A name-validation error message, or `null` when the name is valid. `otherNames`
+ * are the OTHER agents' names for the case-insensitive uniqueness check (the
+ * caller EXCLUDES the card's own last-persisted name / the card being edited).
+ */
+export function nameError(name: string, otherNames: readonly string[]): string | null {
+ const normalized = normalizeName(name);
+ if (normalized.length === 0) return "Name is required.";
+ const lower = normalized.toLowerCase();
+ for (const other of otherNames) {
+ if (other.trim().toLowerCase() === lower) return "A named agent with this name already exists.";
+ }
+ return null;
+}
+
+/** True iff `name` is a unique, nonempty agent name among `otherNames`. */
+export function isUsableName(name: string, otherNames: readonly string[]): boolean {
+ return nameError(name, otherNames) === null;
+}
+
+/**
+ * Whether a full model identifier is syntactically valid — a canonical
+ * `<provider>/<model>` with a nonempty provider (the part before the first "/")
+ * AND a nonempty model (the part after). A slashless or provider-only value is
+ * invalid for a Named Agent (it must resolve to a concrete model).
+ */
+export function isUsableModel(full: string): boolean {
+ const i = full.indexOf("/");
+ if (i <= 0) return false; // no slash, or empty provider
+ const model = full.slice(i + 1);
+ return model.length > 0;
+}