feat: extractable template pattern (repo + sources) + wizard bridge - #436
Draft
abeldantas wants to merge 24 commits into
Draft
abeldantas wants to merge 24 commits into
abeldantas wants to merge 24 commits into
Conversation
Three-step flow: paste YAML or pick a file → fill a form generated from the template's params block → POST to /rooms/from-yaml and navigate to the new room. Paramless templates skip straight to creation. Agent-name params (named exactly "agent" or ending in "_agent") render as a combobox over the live agent list with an inline warning when the name doesn't match. Required fields block submission with inline errors before any server call. Server 400s are mapped back to the offending field. Registered as "Templates" in the workspace sidebar nav. One appended entry each in view-ids.ts, view-registry.ts, and workspace-nav.tsx.
The package is "type": "module" so __dirname is undefined at runtime, crashing electron-vite dev on launch. import.meta.dirname is the ESM equivalent and works in both dev and packaged builds.
Two-column layout: form on left, "What this creates" summary panel on right showing a live preview of the room name (interpolated from inputs), which agents will be added, and template source info. Field labels now use humanized param names with description as helper text underneath. Agent-name fields show an inline "exists ✓" or "not found" badge inside the input. Fields with unchanged defaults show "(default kept)". Buttons are stacked full-width: "Back to template" + "Create room". Step indicator "Step N of 2" in the header. Parser now also extracts the agents list from the template's room block for the summary panel preview.
Make the YAML paste area taller (min-h-64) and vertically resizable so large templates are comfortable to work with. Use describeFailure's detail field directly for server errors instead of the HTTP-prefixed message, so the user sees "Unknown agents: foo" rather than "Could not create the room from this template. (HTTP 400: ...)".
…rors Pull the server's detail field from the RpcError before describeFailure wraps it with an HTTP prefix, so the user sees "Unknown agents: foo" rather than "HTTP 400: Unknown agents: foo".
Server side: new GET /gateway/rooms/template-schema endpoint returning the JSON Schema for a valid room template document, generated from Pydantic's TemplateDocument model (RoomSpec + ParamSpec). Console side: fetches the schema when the Templates view loads and validates the parsed YAML against it (via ajv) at the step 1→2 transition. Schema violations surface as parse errors before the form, so the user learns their template is invalid before filling anything out. Graceful degradation: if the server doesn't support the endpoint (404), validation is skipped. ajv added as an explicit dependency (was already present as a transitive dep).
When the schema endpoint returns 404 (server lacks params support), the parser now rejects unknown top-level keys like params: at step 1 with a clear message instead of letting the user fill a form that will fail at create time. Adds drag-and-drop support to the source step textarea — drop a YAML file onto it and it reads the same way the file picker does.
When the schema endpoint is unavailable, skip validation and let the server decide on create. The previous fallback rejected params: on any server without the schema endpoint, which blocked servers that support params but predate this PR's schema endpoint.
The room view reads from a MobX store that caches the rooms list. Navigating immediately after creation showed "still loading" because the store had no entry for the new room. Call refreshSidebarRoomState(true) before navigating — same pattern as CreateRoomModal.
A template has been a YAML file passed around by hand. This is the table it lives in instead, plus the store that reads and writes it — the storage half of the registry, with the API to follow. The document is kept verbatim and never parsed. That is what makes the round-trip byte-identical, and it means a document written for a format this server does not yet understand is stored and served back intact rather than refused at the door. `kind` is free text for the same reason: room, group and agent templates should differ by a string, not by a migration. Uniqueness is (owner_id, name), so two people may each keep a "deploy-room" but neither may keep two. Scoping it to the owner rather than the server is also what lets it survive the tenant work — an owner belongs to a tenant, so the constraint narrows instead of needing to be rebuilt. Listing is server-wide: the registry is a catalogue, so ownership governs who may change a template, not who may see it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…2677)
The REST half of the registry. Upload a template, browse the catalogue, search
it by name or description, fetch one back, and delete your own.
Two doors for reading a template: the detail route returns it inside a JSON
envelope with its metadata, and `/templates/{id}/content` returns the stored
document alone, byte for byte, for anything that wants to pipe it somewhere.
Nothing on this path parses or reformats the document.
The catalogue is server-wide — every owner's templates are listed to everyone,
because a registry nobody else can see distributes nothing. Ownership decides
who may change or remove a template, via the existing require_manage, so an
admin can clean up and nobody else can touch what is not theirs. A listing
omits the document bodies; fetch one to get its content.
Uploads are bounded by TEMPLATE_MAX_BYTES, counted in encoded bytes rather
than characters so multibyte text cannot slip past, and refused whole rather
than truncated.
Note the store now reloads after an update: `updated_at` is computed by
Postgres, so the flush leaves it expired, and reading it afterwards would trip
a lazy load from synchronous code and raise MissingGreenlet rather than return
a timestamp.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…HOO-2677) A Templates tab on the Resources page: the catalogue with search, kind and owner filters, an upload dialog that can load a document from a file, and a detail page to read, edit, copy, download or delete one. Edit and delete are offered only to the owner and to an admin, matching what the server enforces. Copy and download go back to the server for the document rather than using what is on screen, so what lands in the file is what is stored even when the editor has unsaved edits in it. Deleting has its own dialog rather than reusing DeleteResourceDialog: that one warns which rooms a resource is about to be detached from, and a template is attached to nothing. Also adds the HTTP-level tests. The existing route tests call the handler coroutines directly, which skips the part the acceptance condition is about — a template has to survive being JSON-encoded onto the wire and decoded off it again. These drive the real app over an ASGI transport and assert the document comes back byte for byte, CRLFs, tabs, quotes, backslashes and all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The server reports users/refs/docs that couldn't be added in failedAttachments, but the Console discarded them. Now shows a warning toast naming each failed item so the user knows what didn't land — e.g. "abel.dantas (no account with this name is known on the bridge)".
The existing override mapped <4.3.0 to >=4.3.0, which resolved to 4.3.1 — still vulnerable to merge-key CPU exhaustion. Floor is now 4.3.2, the fixed release. Transitive only; the app's direct js-yaml (5.x) is not in the affected range.
Six real defects, all in code added by this branch. **A hostile name broke the download.** The template name went straight into `Content-Disposition`. Headers are latin-1 on the wire, so a name holding `☕` failed the whole response — a permanent 500 on that template's document, for every reader, plantable by any user. A name holding CRLF put a header of the caller's choosing into the response; h11 refuses to send that, so it was a poison pill rather than a true injection, but relying on the HTTP server to sanitise is not a defence. Now RFC 6266: an ASCII skeleton in `filename`, the real name percent-encoded in `filename*`. The download also says `nosniff`, since the bytes are whatever somebody uploaded. **The listing read every document to report its size.** `size_bytes` was computed in Python from `content`, so rendering a page of names dragged every stored document through Postgres and into memory — exactly what the schema docstring claimed the listing avoided. Measured with `octet_length` now, and the query no longer selects the document at all. **Only the document was bounded.** `name`, `description` and `kind` had no maximum against unbounded `Text` columns, and nothing else in the request path caps a field, so a 50MB name was a valid upload. **Concurrent edits could lose one silently.** Bumping a revision means reading the old one first, so two edits racing computed the same next number and the later write dropped the earlier one with both callers seeing 200. The read-modify-write now takes a row lock. **A rename that lost a race 500ed.** `update_fields` pre-checked for a clash and then flushed outside a savepoint, so losing the race raised through the router instead of answering 409 — and the comment claimed the constraint backstopped it, which nothing did. **A concurrent delete 500ed**, for want of the `ValueError` catch every sibling router has. The store now raises `TemplateNameTaken`, a ValueError subclass, so a conflict and a missing row stop being the same exception distinguished only by which call happened to run first: create answers 409 or 400, patch 409 or 404. Front end: the clipboard write was never awaited, so a rejected copy was silently indistinguishable from a successful one, and a non-secure context reported it as a failed fetch; the file picker's name autofill read a state value captured before the read, overwriting a name typed while it was in flight; the object URL was revoked in the same tick as the click; and an oversize file was read fully into memory to be refused by the server a round trip later. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`main` gained an index migration and then a merge revision to rejoin the two heads it created (#404, #426). This branch's migration still pointed at the revision that was the head when it was written, so merging main left the chain with two heads again and `test_single_head` did its job. Re-parented onto `b47e0c39a1f5` rather than adding a second merge revision: this migration has never been released, so it can simply be moved instead of leaving a permanent join in the history to record a branch that only ever existed on a feature branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tab's body, route, dialog and handler all landed; the `<Tab>` that selects it did not. So the page rendered a branch nothing could reach — it compiled, `tsc` had nothing to object to, and with no tests in this tree the only way to notice was to open the page and look, which I had not done. Rather than add the missing line, the tab bar, the URL parameter, the union and the create button's label now come from one list. Keeping them together is what stops the next tab from being half-added: a value that is not in the list cannot typecheck, and one that is gets a bar entry for free. Also names the templates filter "Kind" rather than the shared bar's default "Type", which disagreed with the column beside it. Verified in a real browser this time, not just compiled: all five tabs select, route, label their button and render; a file picked in the file dialog is stored byte-identical; editing the document bumps the revision and editing only metadata does not; and the template is findable, then gone after a delete. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The YAML provisioning path returned None when bridge was omitted, creating an internal-only room. This broke templates with users: since users live on a collaboration bridge. Now looks up the instance's default bridge (same behavior as the regular create-room endpoint).
…greying it Every template on the server is visible to everyone, so opening one you do not own is the ordinary case here — not the exception it is for the resources next door, which a reader only sees if they were given access. Disabled fields alone answer that badly: they look much like enabled ones until you try to type, and they never say whose template it is or who could change it. Non-owners now get a line saying so. Owners and admins are unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ed locally The Templates view now reads the server catalogue from the template registry (CHOO-2677) instead of a parallel stored_templates table, which this branch previously carried. The bundled Switch expert ships inside the Console as a local template - it renders in the same listing and works with no server round trip. Registry rows map owner_name to the creator label; a template's content becomes the agent's instructions on create.
Adds repo_url (Text) and sources (JSONB) columns to the registry's templates table via a new migration. The template shape is now the full extractable pattern: persona + provider (user input) + repo + sources. - Replaces the 145-line bundled persona with the full switch-expert/ AGENT.md (~340 lines of system prompt + knowledge file references) - Bundled Switch-expert template declares repo sandbox-quantum/switch and 3 Switch docs source URLs - Template cards in the listing show repo URL and source count - Gateway client types carry repoUrl and sources - Room-template wizard: "+ Create" button on not-found agent-slot chips opens add-agent modal with the name prefilled - Add-agent modal gains prefillName prop for the wizard bridge All checks green: ruff, mypy, oxfmt, oxlint, tsgo typecheck. CHOO-2665.
abeldantas
force-pushed
the
work/switch-expert-template
branch
from
September 14, 2026 11:24
64a427e to
655ff0b
Compare
abeldantas
force-pushed
the
work/switch-expert-template
branch
3 times, most recently
from
September 16, 2026 16:43
14e13dc to
3b536c2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brief
Extends the template registry with the extractable pattern — CHOO-2665. A template now declares its full dependencies as data: persona + provider (user input) + repo + sources. Switch-expert is the first instance with repo
sandbox-quantum/switchand Switch docs sources. Part of the Framework workstream (CHOO-2600).Builds on PR #409 (Console template listing + bundled Switch expert) and #421 (template registry).
Summary
repo_url+sourcescolumns on the registry'stemplatestable via a new migration (ddb436ff9c0d).repo_urlis nullable Text,sourcesis nullable JSONB (array of{url, label}).switch-expert/AGENT.md(~340 lines of system prompt + knowledge file references).sandbox-quantum/switchand 3 doc source URLs.repoUrlandsourceson both summary and detail.prefillNameprop — for the wizard bridge.Supersedes the old napoleon Gemini-pinned persona.
Test plan
Backend: ruff format/check clean, mypy clean across core/ and connectors/. Console: workspace packages build, oxfmt clean, tsgo typecheck passes across all 5 workspace projects. The extractable pattern is proven by the bundled Switch-expert template declaring repo + sources as data. Live verification (composed demo) pending Abel's validation.