Studio copilot
An in-Studio chat panel that drives the framework’s existing gated loop as tools. Describe → the copilot reads routes, sources, lint and schema, previews buffers — and, only when you hold an edit role, saves drafts. Nothing it does is served until you review and apply the draft in the editor’s diff-confirm UI, and every copilot-saved draft lands in the audit trail under your name.
TesseraQL ships no model and stores no key in app source. The operator points the panel at any OpenAI-compatible chat-completions endpoint:
tesseraql: http: outbound: allowedHosts: - api.example.com # the copilot endpoint must be allow-listed copilot: enabled: true endpoint: https://api.example.com/v1/chat/completions # vLLM, Ollama, a gateway, ... model: your-model-name apiKey: ${secret.env.COPILOT_API_KEY} # resolves at call time via the secret provider maxTurns: 6 # bounded tool loop per messageThe key is read lazily per request through the config placeholder chain, so a
${secret.*} reference never resolves at startup and never lands in a file. Without
enabled: true the panel renders a setup hint and the chat endpoints (send, stream,
reset) answer TQL-STUDIO-4235.
What leaves the server. Every turn POSTs the whole conversation to the configured
endpoint: a system prompt naming the app, your messages, the model’s replies, and the
result of every tool the model called — which can include entire source files it read
(route YAML, 2-way SQL, templates), the route inventory, lint findings, and introspected
table and column names. No table data is sent — the schema tool returns names only —
and the API key rides as a bearer header. The
outbound egress allow-list applies: the endpoint’s host must be in
tesseraql.http.outbound.allowedHosts — egress is deny by default, exactly as for an
http: step — and a configured copilot whose host is not allow-listed fails the
boot with TQL-SEC-4085, an error that names the host and the YAML to add. Configuring
tesseraql.copilot.endpoint alone is deliberately not enough: allow-listing the host is
the second, explicit authorization to ship your app’s source there.
What the copilot may do
Section titled “What the copilot may do”| Tool | Gate |
|---|---|
list_routes, read_source, lint, schema_tables, preview_draft |
Studio browser session |
save_draft |
your Studio edit role — offered to the model only when you hold one |
| apply | never — a human applies every draft in the editor |
The studio_copilot MCP prompt (MCP dev tools) remains the way to drive the
same loop from an external agent; the panel is the zero-setup, in-Studio path.
Streaming replies
Section titled “Streaming replies”The panel streams the assistant’s reply as it is produced instead of blocking on the whole
tool loop. It adopts two Hypermedia Components kit recipes (hc 0.1.9) verbatim — chat-messages (the transcript is an
hc-chat log whose composer posts over htmx) and streaming-response (the assistant
placeholder owns its own SSE connection) — on htmx’s bundled sse extension. No new
dependency: the extension ships inside the htmx WebJar and is served version-less like
every other vendor asset.
The turn contract
Section titled “The turn contract”- POST
…/copilot/send(htmx): registers a single-use pending turn — actor-bound, UUID id, short TTL — and answers200with the chat-messages fragments: the user message item, an assistant placeholder (aria-busy="true",sse-connectto the stream URL), and an out-of-band composer re-render that clears the input. The model has not been called yet. - GET
…/copilot/stream?turn=<id>: authenticates the browser session, consumes the turn (an expired, foreign, or already-used id is a 404), and runs the same bounded tool loop — now streaming. Content deltas arrive aschunkevents (server-escaped HTML text), each tool execution emits a small marker chunk, and the turn ends withdonecarrying the complete final message markup (orerror). Swapping the placeholder away closes the EventSource; there is no reconnect. - No JS: the same send URL without
HX-Requestruns the blocking loop and answers303back to the page — exactly the pre-streaming behavior, now the fallback.
The SSE transport
Section titled “The SSE transport”How streaming behaves, in short: the reply rides server-sent events over the framework’s
shared streaming transport — the same one that pushes the inbox bell badge.
The stream authenticates the browser session exactly like auth: browser routes, and a
refusal before the stream opens renders the framework’s normal JSON error envelope with
its mapped status. Upstream, the chat-completions call streams too; an endpoint that
cannot stream surfaces as the stream’s error event. Without JavaScript the same send
URL runs the blocking loop and answers 303 back to the page, so the panel works —
rendered identically — with no stream at all.
Security
Section titled “Security”- The stream is a GET riding the session cookie — no CSRF needed; the turn id is the capability: UUID, single-use, actor-bound, short TTL.
- Every chunk is server-escaped before framing; the client appends, never interprets.
- CSP unchanged: EventSource connects same-origin under
default-src 'self'.