Skip to content

feat(console): Create from template view (CHOO-2656) - #405

Merged
abeldantas merged 58 commits into
mainfrom
work/import-ux
Sep 15, 2026
Merged

abeldantas merged 58 commits into
mainfrom
work/import-ux

Conversation

@abeldantas

@abeldantas abeldantas commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Brief

CHOO-2656 — Console: Create from template. Part of the template import epic: today importing a template means a curl command against an endpoint. This builds the human path in the Console so a non-technical person can turn a shared template file into a working room.

Summary

  • New "Templates" nav entry in the workspace sidebar
  • paste YAML or pick a .yaml/.yml file

@abeldantas
abeldantas changed the base branch from main to work/template-params September 8, 2026 22:39
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

All contributors have signed the CLA. ✅
Posted by the CLA Assistant Lite bot.

@abeldantas

Copy link
Copy Markdown
Collaborator Author

recheck

@abeldantas

Copy link
Copy Markdown
Collaborator Author

I have read the CLA Document and I hereby sign the CLA

@abeldantas

Copy link
Copy Markdown
Collaborator Author

recheck

@abeldantas
abeldantas force-pushed the work/import-ux branch 3 times, most recently from 810313b to fbc2d4c Compare September 9, 2026 08:38
@abeldantas

Copy link
Copy Markdown
Collaborator Author

recheck

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).
@abeldantas
abeldantas changed the base branch from work/template-params to main September 9, 2026 09:56
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.
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)".
function parseYaml(yamlText: string): Record<string, unknown> {
let doc: Record<string, unknown>;
try {
const parsed = load(yamlText);

@semgrep-code-sandbox-quantum semgrep-code-sandbox-quantum Bot Sep 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: Affected versions of js-yaml are vulnerable to Inefficient Algorithmic Complexity / Uncontrolled Resource Consumption. js-yaml is vulnerable to CPU exhaustion when parsing untrusted YAML: the maxTotalMergeKeys budget is only charged for keys that a merge (<<) source actually contributes, so empty mappings cost nothing against the limit. A small document that merges a long sequence of empty mappings repeatedly forces O(N*K) work while the counter stays flat, stalling the process. Merge keys are part of the default schema for every load entrypoint (load, loadAll, and the 3.x safeLoad/safeLoadAll), and lowering maxTotalMergeKeys does not mitigate it.

Fix: Upgrade this library to at least version 4.3.2 at switch/console/pnpm-lock.yaml:3988.

Reference(s): https://euvd.enisa.europa.eu/vulnerability/EUVD-2026-69712, GHSA-2883-xcg3-v3hh, CVE-2026-84375

🍰 Fixed in commit 258ee83 🍰

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 258ee83: the pnpm override floor moves to 4.3.2 (the fixed release), so the transitive 4.3.1 is gone from the lockfile. The app's direct js-yaml is 5.x, outside the affected range.

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.
@abeldantas

abeldantas commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

Verified E2E on a local build: pasted a params template, form renders with live interpolation, room created and bridged to Slack, agent responsive in the room.

@abeldantas
abeldantas marked this pull request as ready for review September 10, 2026 14:48
… (CHOO-2719)

Add sender_kind to MessagePayload (event stream) and read_context
timeline entries so agents can see whether a message is from the
platform. Set to "platform" when the message content carries
PLATFORM_MARKER, None otherwise. This completes the end-to-end
coherence requirement: storage (content marker), event stream
(sender_kind field), and read_context (sender_kind in entries).
Route platform-marked messages through the admin_message rendering path
so they appear on Slack/Mattermost/etc as the Switch app identity, not
as a raw "admin" sender. Uses the same rendering as admin system
messages but without suppressing agent addressing.
Room templates can now send a message after provisioning via a new
kickoff field: message, targets (default: all agents), and sender
("platform" default, or "creator" for explicit impersonation).

The default path sends as the platform through send_platform_message,
subject to normal addressing policy checks. Creator impersonation
is opt-in and visible via the on_behalf_of metadata.
… (CHOO-2719)

The existing test used a SimpleNamespace without a type field, which
broke when resolve_sender started checking client.type == "admin".
Updated the mock to include type="bridge" (non-admin) and added a
new test verifying admin clients resolve to platform principal.
on_behalf_of was passing the raw user_id (a UUID), making creator
impersonation invisible to humans and agents reading the marker.
Now resolves to User.name via UserStore, falling back to the UUID
only if the user row is missing.
The paste step now uses Monaco Editor with YAML syntax highlighting,
line numbers, code folding, and auto-expanding height (grows with
content, min 256px, max 600px). Drag-and-drop still works on the
surrounding div.

Monaco is already a dependency (@monaco-editor/react + monaco-editor).
The gateway has a similar YAML editor (CodeMirror-based) — this uses
the same concept but with the Console's existing Monaco integration.

NOT tested: visual rendering (no display on this VM). Typecheck, lint,
format all green.
…2656)

A template param could only be a string, number, boolean or enum, so a
param meant to name an agent was a free-text string and the Console
guessed from the param's name whether to offer an agent picker. Nothing
checked the value until provisioning failed part-way through.

A param can now be typed `agent`, `bridge`, `room` or `user`. The value
is still the entity's name, but the server checks it exists before
creating anything (RoomYamlService.check_entity_params, called from the
from-yaml route between parse and provision), failing with a message
that names the param so the form can point at the field. Users resolve
on the bridge the room will land on, through the same lookup room
creation uses (RoomService.resolve_bridge_users).

parse_template returns the declared params and their resolved values
alongside the spec; parse keeps its old shape for existing callers.

Claude-Session: https://claude.ai/code/session_01Ghh4wimzE1eqWQeKjMzPBv
… components

The new-room dialog, the add-agents-to-room modal and the add-agent-to-
rooms modal each carried their own copy of the same search box, chosen
tile and bridge tile. The template wizard needs the same controls, so
this moves them to renderer/lib/components (PickerCombobox, ChosenTile
and the agent, room and bridge flavours on top of it) and points the
three modals at them. No visual change to the modals.

bridgeUnusableReason takes a needsChannelCreation flag because a
surface that only names a bridge does not care whether it can create
channels, only whether it is running.

Claude-Session: https://claude.ai/code/session_01Ghh4wimzE1eqWQeKjMzPBv
… wizard (CHOO-2656)

The wizard offered an agent picker only when a param was named `agent`
or ended in `_agent`, and that picker was a text box with a datalist.
Bridges and rooms had no picker at all, and the template's fixed
members were a read-only list you could only shrink.

The parser now carries the param's declared type through instead of
guessing from its name. Entity-typed params render the same controls
the new-room dialog uses: a search box that turns into a tile for
agents, rooms and users, and the messaging-app tile grid for bridges.
Users come from the server's known users, a live directory search, and
the typed name as a last resort, since the server looks unseen names up
on create. The fixed agents and users lists become the same tile grid
with a search box to add more, not only remove.

A bridge param that has been set drives the summary panel, so the
creator identity and "channel on X" line follow the pick rather than
going blank as they did for an interpolated bridge.

Claude-Session: https://claude.ai/code/session_01Ghh4wimzE1eqWQeKjMzPBv
…m sender kind meets the template wizard

Both kickoff paths are kept for now, the top-level kickoff posted through
the creator's puppet and the room-level kickoff posted as the platform;
the follow-up collapses them onto the platform sender.

Claude-Session: https://claude.ai/code/session_01Ghh4wimzE1eqWQeKjMzPBv
…OO-2719)

The kickoff a template posts had two senders on this branch: the creator's
puppet, which impersonates a person, and 437's platform sender, which
agents deny by default so every template was dead until each agent's
policy was edited. Neither is what we want.

Now there is one path. The platform posts the kickoff through the admin
client, and the platform marker names the creator. When an agent receives
it, the resolver judges the message as that person: an open policy admits
them, an owner rule admits them when they own the agent, a users list
admits them through a claimed account (allows_on_behalf_of). The
platform's own messages, with nobody behind them, keep 437's opt-in. The
authority is per event and nothing is written to any policy. Only the
admin client's marker is read, so a copy of it on a human's or agent's
event borrows nothing.

The room sees who it came from: bridges render it as the Switch app
prefixed "On behalf of <name>", and the agent payload and read_context
carry sender_kind and on_behalf_of.

Also fixed from 437's review: the open-policy fast path in permitted()
let a bare platform message through; the admin client returning None on
a failed send was reported as success; and the kickoff fired before the
agents had joined, which drops the event, so provisioning now waits for
the sender and the template's agents and names any that were late.

Gone: post_as_user and the Switch-originated relay marker in bridge_core,
post_kickoff in room_service, the room-level kickoff mapping with its
targets and sender fields. The top-level kickoff string stays.
…he gateway (CHOO-2719)

Both addressing policy editors rebuilt each rule from the four dimensions
they know, so saving any policy dropped the platform flag 437 added, and
the gateway one dropped owner and owner_agents too. Each now carries the
fields through and offers a "Switch itself" checkbox for the platform's
own messages. The agent runtime's MessagePayload mirror gains sender_kind
and on_behalf_of.
…2656)

The summary said the kickoff was posted as you, and the identity warning
said it could not be posted without a linked account. Neither is true now
that the platform posts it: the copy names Switch as the sender and the
warning narrows to the channel invite, which still needs the account.
… import them

The wizard imported a function from the main-process parser, and the
renderer's Vite build refuses that at load time, so the Templates view
failed to open on the dev build. The types and the entity-type check now
live under shared/, and both the parser and the wizard read them there.

Claude-Session: https://claude.ai/code/session_01BA2uVSApHWFpzPg8F49mhU
Two sessions on one machine each drove "the" dev Console today and kept
killing each other's, because the dev build has one userData dir, one
renderer port and one debug port. Two dev-only env overrides fix that:
SWITCH_CONSOLE_USER_DATA_DIR picks the userData dir and
SWITCH_CONSOLE_RENDERER_PORT the Vite port; the debug port was already a
flag. A packaged app ignores both.

Claude-Session: https://claude.ai/code/session_01BA2uVSApHWFpzPg8F49mhU
…ount on (CHOO-2656)

A template with {$creator} in users: on a bridged room used to fall back
to the gateway account name when the creator had no linked identity
there. That name is not a platform handle, so the invite failed quietly
and the creator got a private channel they could not enter. The create
is now refused with a message naming the app to link.

The kickoff also names the creator the way the bridge knows them: the
platform marker carries the linked handle, the bridge renders "On behalf
of @handle", and an agent answers that person rather than "@admin", the
admin client that carried the message.

Claude-Session: https://claude.ai/code/session_01BA2uVSApHWFpzPg8F49mhU
… hand-offs in one click (CHOO-2656)

Two things the first real run showed. The wizard let you create a room
that invites you as {$creator} while the server had no account of yours
on the bridge, with only an amber note; now Create is disabled and the
note carries a Link-your-account button that opens the existing claim
flow and unlocks the form. And a coder/reviewer template bounced its
hand-off, because agents created from the Console answer only their
owner: the wizard now checks the picked agents' policies against each
other and offers "Let them hear each other", which adds the owner's
agents (or the named agent) to the blocked ones' rules.

Claude-Session: https://claude.ai/code/session_01BA2uVSApHWFpzPg8F49mhU
@abeldantas

abeldantas commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator Author

Inputs step with typed agent pickers

Agent picker open

Create blocked until the account is linked

Link your account from the wizard

Hand-off warning

After the one-click fix

Room created

…CHOO-2656)

The kickoff took half a screen at the top of every channel: the
attribution line, the mentions, and the per-agent instructions, followed
by each agent's session notice and plan at the same level. Now the
channel gets one line, "Template kickoff on behalf of @handle", and the
kickoff text goes as a reply in that message's thread. The agents are
addressed by the threaded message, so their notices, plans and hand-offs
land in the thread too.

The bridge no longer prefixes a threaded platform message, nor one whose
body already names the person, so the headline is not attributed twice.

Claude-Session: https://claude.ai/code/session_01BA2uVSApHWFpzPg8F49mhU
…its thread (CHOO-2656)

The threaded kickoff pulled the whole coder/reviewer conversation into
its thread, since an agent answers where the message that addressed it
lives. The kickoff body now carries a reply_in_channel flag in the
platform marker; the agent client reads a flagged message as top-level,
so plans, hand-offs and session notices land in the channel and the
headline keeps exactly one reply, the kickoff text.

Claude-Session: https://claude.ai/code/session_01BA2uVSApHWFpzPg8F49mhU
Flagging the kickoff body moved the agents' notices to the channel but
not their work: an agent that starts a session on the notification
catches up through read_context, and there the body still hung under
the headline, so the plan and everything after it followed it into the
thread. The history now reads a reply_in_channel platform message as
its own root.

Claude-Session: https://claude.ai/code/session_01BA2uVSApHWFpzPg8F49mhU
…e why

A comment that cites a ticket or narrates the run that prompted it goes
stale in weeks. The comments added on this branch now say what a reader
needs a year from now and nothing else: why the platform sender exists,
why a kickoff needs the creator on the bridge, why owner-only agents
cannot hear each other. Em dashes and arrows are gone from prose, test
names and user-facing strings alike, per the house style.

Claude-Session: https://claude.ai/code/session_01BA2uVSApHWFpzPg8F49mhU
@abeldantas
abeldantas merged commit 2e0706f into main Sep 15, 2026
10 checks passed
abeldantas added a commit that referenced this pull request Sep 16, 2026
…s for (CHOO-2665)

The room-template wizard that landed with #405 picks agents from what the
server has, so a slot naming an agent that does not exist no longer
happens and the "+ Create" offer on it has nothing to attach to; the code
behind it goes. The identity warning in the template dialog now says what
the link is for on this server: the template puts you in the room as
{$creator}, which is filled only from a claimed identity.

Claude-Session: https://claude.ai/code/session_01GpmTz1MFhce8ekJoPZgqBn
abeldantas added a commit that referenced this pull request Sep 16, 2026
Folds the core half of work/group-templates (#407) onto the template
format as it stands after #405, so one YAML file can declare a room group,
several rooms under it, and directed links between them.

Before: a template made exactly one room. #407 added the group shape but
sat on a base from before parse_template, builtins and kickoff landed, and
could no longer merge.

Now: parse_template tells the shapes apart by their top-level key. A
`room:` document works as before, kickoff at the top level. A `group:` +
`rooms:` (+ `links:`) document gives a GroupSpec; each room carries its own
`kickoff:`, and a top-level one is refused with a message saying where it
goes. Params and server builtins interpolate over the whole document,
mapping keys included, so an alias map written `"{bot}": helper` resolves.
Rooms accept `aliases:` and export them back. provision_group creates the
group row, each room in order (filed under the group, its kickoff posted
as the creator), then the links; a room that fails is reported in
`errors` and the rest still go ahead. POST /rooms/from-yaml returns
whichever result matches the document, entity params are checked across
every room, and /rooms/template-schema describes both shapes. Agents get
the same through a `create_room_from_yaml` operation, acting as their
owner.

The 45 Console files and two migrations in #407's diff were drift from an
older base and are already on main; none of them are touched here.

Claude-Session: https://claude.ai/code/session_01GpmTz1MFhce8ekJoPZgqBn
abeldantas added a commit that referenced this pull request Sep 16, 2026
…s for (CHOO-2665)

The room-template wizard that landed with #405 picks agents from what the
server has, so a slot naming an agent that does not exist no longer
happens and the "+ Create" offer on it has nothing to attach to; the code
behind it goes. The identity warning in the template dialog now says what
the link is for on this server: the template puts you in the room as
{$creator}, which is filled only from a claimed identity.
abeldantas added a commit that referenced this pull request Sep 16, 2026
Folds the core half of work/group-templates (#407) onto the template
format as it stands after #405, so one YAML file can declare a room group,
several rooms under it, and directed links between them.

Before: a template made exactly one room. #407 added the group shape but
sat on a base from before parse_template, builtins and kickoff landed, and
could no longer merge.

Now: parse_template tells the shapes apart by their top-level key. A
`room:` document works as before, kickoff at the top level. A `group:` +
`rooms:` (+ `links:`) document gives a GroupSpec; each room carries its own
`kickoff:`, and a top-level one is refused with a message saying where it
goes. Params and server builtins interpolate over the whole document,
mapping keys included, so an alias map written `"{bot}": helper` resolves.
Rooms accept `aliases:` and export them back. provision_group creates the
group row, each room in order (filed under the group, its kickoff posted
as the creator), then the links; a room that fails is reported in
`errors` and the rest still go ahead. POST /rooms/from-yaml returns
whichever result matches the document, entity params are checked across
every room, and /rooms/template-schema describes both shapes. Agents get
the same through a `create_room_from_yaml` operation, acting as their
owner.

The 45 Console files and two migrations in #407's diff were drift from an
older base and are already on main; none of them are touched here.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants