canvas: a chat panel on the left, for Claude Code and Codex - #96
Jing-yilin wants to merge 27 commits into
Conversation
… run Three routes under /__sp/agent/run: POST starts one `claude -p` in the user's project and answers 202 with a run id; GET :id/events?after=N is a server-sent event stream of everything after N, replayed from what the server kept and then live; POST :id/cancel stops it. Two steps rather than one streaming response because a board the agent writes reloads the page through the watcher, and a stream bound to the starting fetch would die exactly when the run succeeds. claudeStream.ts reduces the CLI's stream-json frames to text, a thinking marker, tool calls with their results, and the end; its tests run against recordings of Claude Code 2.1.274 with the flags the server spawns. agentRun.ts is the per-run event log and its attach/replay, node-free so vitest covers it. The agent's cwd is the user's project, which the server learns from PROTOTYPING_PROJECT_DIR; sp-canvas start sets it to the directory it is started from, and without it the routes answer 503 by name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ChatPanel.tsx is the inspector's mirror as a flex sibling before the editor: 360px, so tldraw's viewport shrinks by it and every screen-space calculation stays right. Floating was rejected — tldraw 5 has a scalar inset and symmetric padding, no asymmetric expression — and the comment in index.css now says so. The panel keeps its run ids in sessionStorage and reads each run again from event zero after the reload a board write causes; chatTransport.ts holds the SSE decoding, the reconnect-with-cursor loop and the fold from events into a turn, with tests. One run at a time, Send becomes Stop while it goes, and the empty state says permission prompts are off. Dev server only: App.tsx mounts it under import.meta.env.DEV. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs/2026-09-17-canvas-chat-panel.md keeps why the panel squeezes rather than floats, why the transport is two steps with a cursor, why permissions are off and where that is said, and what was left out. The prototype-canvas skill gets a section on the panel, since the agent it runs is pointed at that skill; RELEASE-NOTES gets its Unreleased line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The system prompt asks Claude Code to open its reply with the run's title as <sp-title>…</sp-title>, and titleFilter in claudeStream.ts lifts it out on the server, before anything is emitted, into a title event: the page never sees the marker as text. The marker arrives split across deltas, so text is held only while it could still be the marker, and the blank lines the model puts after it go with it whichever delta they come in — the recorded fixture had them in the closing delta, a live run had them in the next one. Every run now opens with a start event carrying the prompt, its first line as the title until the model gives one, and the time, so a replay from zero rebuilds the whole turn. GET /__sp/agent/runs lists the runs the server still holds, newest first, read off those same events by runSummary; the middleware moves up to /__sp/agent so the one 503 guard covers it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The header shows the conversation's title behind Claude's mark — the model's own once it has given one, the prompt's first line until then. A clock opens the server's recent runs as a native popover, placed under the header by the panel's fixed geometry; picking one replays it. A panel icon folds the panel to a 36px rail that is the mark itself, which opens it again; nothing unmounts, so a run keeps being followed while the panel is closed, and the state is in localStorage so a closed panel stays closed across the reload a board write causes. sessionStorage keeps only run ids now, since the start event carries the prompt. ClaudeMark.tsx is one path from lobehub/icons' Claude.Color (MIT), drawn inline like every icon here rather than pulled in as a package that is nine megabytes and an Ant Design stack. One agent, one mark, no registry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…talling The decision doc gains the title marker and why it never reaches the page, the history that lives in the server's memory and dies with it by design, collapsing as a width change, and the one mark with no registry behind it. The release notes say the panel's header changed and that sp-canvas 1.2.0 predates PROTOTYPING_PROJECT_DIR, so the toolkit must be reinstalled or every message answers 503. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude writes markdown — bold, lists, fences, GFM tables with a <br> inside a cell — and the panel drew it as source. renderMarkdown in markdown.ts is now the one path from model text to the DOM: remend closes what a delta cut open, so a streamed **bold is bold from the first paint rather than asterisks until the next delta; marked parses; DOMPurify sanitizes, because marked passes <script> through untouched. Three small libraries, 27 KB gzipped together, where Streamdown — aimed at exactly this — is 159 KB and a hundred packages behind one export. No highlighter: a fence is escaped text. Wide tables and fences scroll sideways inside their block rather than widening the 360px panel. A table exists only once its delimiter row has streamed in, and becomes one in place; nothing can know sooner. jsdom is a dev dependency for the sanitizer test alone. happy-dom was tried first and dropped: DOMPurify strips <table> and empties an <img> under it, which a browser does not, and a sanitizer test on a DOM that misbehaves holds nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The decision doc gets the measurements behind the choice, the two renderers Open Design already has and why neither is a straight copy, the sanitizer rule, and the two things streaming does that nothing fixes. The release notes say replies now render as markdown. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The panel's header carried the open canvas's slug, a few hundred pixels left of the canvas's own header carrying the same board's name. One of the two had to go and it is not the one that owns it. The slug still travels with every message; only the chip is gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Undo, redo, delete and duplicate came from tldraw's QuickActions, and the overflow of aligns, distributes and reorders from its ActionsMenu. The shapes on this canvas are boards written from files, so six buttons for nudging them sat between the page's name and the two destinations that are what the bar is for. QuickActions now renders the comment tool alone — the one mark someone makes on a canvas that is read rather than drawn on — and the actions menu renders the app's own buttons without the default menu behind them. None of the actions are gone, only their buttons: the keyboard shortcuts and the right-click menu are untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
codexEventsFromLine turns what `codex exec --json` writes into the same seven ChatEvents claudeStream.ts produces, so nothing past the parser has to know which CLI it is reading: a command as it starts and when it exits, a file change the same way, a reasoning summary as the thinking marker, the message as text, and the turn's end. The thread id, turn.started, token usage and the error items that are only warnings about the configured model are dropped. Nothing streams on this wire. Codex has suppressed its message deltas since rust-v0.8.0, so the reply arrives whole in one item.completed after every tool line; the header comment says so, so nobody goes looking for why a Codex turn seems to hang before its text. Its other wire, app-server, does stream, and is a JSON-RPC session Open Design carries a second transport for; this panel does not. A failed turn says so twice, as a bare error and then turn.failed with the same text, and only the second ends the run, since agentRun.ts refuses a second end. The message is the server's wording verbatim — on this machine the configured model is one the installed CLI is too old for, and that sentence is the whole diagnosis. The two fixtures are recordings of codex-cli 0.146.0, paths renamed; file_change has no recording here yet and takes its shape from Open Design's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
agents.ts holds one object literal per CLI — what to spawn, how the preamble and the message reach it, and which parser reads it back — and the dev server looks the spawn up there by the id the panel sends, defaulting to claude. The run, its events and the stream to the page are the same past that point. The shape is Open Design's RuntimeAgentDef, which carries twenty-eight CLIs over one engine; this carries the fields the two here genuinely differ in and is not a registry. A third agent is a third literal. Claude Code takes the preamble as --append-system-prompt and the message as one stream-json line. Codex has no system-prompt flag, so the preamble goes ahead of the message on stdin as plain text; it runs in its own workspace-write sandbox with the network on, and is told the boards folder with --add-dir, since sp-canvas --canvases can put that outside the project and the sandbox writes the working directory alone. It is also asked for a reasoning summary, without which the stream carries no reasoning item at all on a turn that reasoned; the summary is what becomes the thinking marker. GET /__sp/agent/agents says which agents are installed, by `bin --version` once each for the server's lifetime, ahead of the project check since PATH does not depend on it. The start event and RunSummary name the agent, so a run replayed from zero and the history list both know whose mark to draw. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The mark on the chat panel's header was Claude's, and said only who the panel talked to. Now it is a button: it opens a menu of the agents the server found on PATH, the chosen one's mark takes its place, and the choice is kept in localStorage and sent with every message. An agent the server could not find is in the menu greyed, with the sentence the run would have failed with under its name, so the way to install it is where the user looks for it. Which agent runs a turn is part of the turn: the start event carries it, chatTransport keeps it on the Turn, and each turn and each history row shows the mark of the agent that ran it, since one conversation can switch between messages and the header only says where the next one goes. The composer's placeholder and the title before the model gives one name the chosen agent. Codex's mark is lobehub's, inlined next to Claude's as ClaudeMark.tsx said a second agent would be; it is monochrome, so it takes the text colour where Claude's keeps its terracotta. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The design note gets the section the mark's own paragraph promised: why the spawn moved into a table of two literals rather than a registry, where Claude Code and Codex genuinely differ — the preamble on stdin, --add-dir for the boards, the reasoning summary that has to be asked for — why a Codex turn shows its commands one by one and its reply all at once, why a failed turn ends once with the server's own sentence, and the gpt-6-astra refusal that sentence will be on this machine until the CLI is updated. The left-out list now says resuming and model choice are left out for both. The release notes tell the user the mark is a switch and what to expect of Codex; the skill stops naming only claude -p. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ran both through the panel and fixed what the running showed. Codex's failure was a line of JSON: it puts an API refusal on the wire as the whole response body inside a string, so "the 'gpt-6-astra' model requires a newer version of Codex" arrived wrapped in an envelope nobody needs to read. One layer comes off, and the sentence shows. Two things the parser's notes claimed are not what codex-cli 0.146.0 sends. A turn does not save its text for the end: the recordings open with a sentence about what it is off to do, then the tool lines, then the answer. And the `file_change` shape was Open Design's recording, with a note saying no turn here had written a file; one has now, spawned the way agents.ts spawns it, and it is the fixture the test reads instead of the hand-written lines. The title filter gave up too early. A turn with work to do opens by saying what it is about to do, runs a tool, and titles the reply after it — and the filter, having released the first sentence, left the marker in the second, where the sanitizer dropped the tags and the model's title sat in the middle of the text as a stray line while the header showed the prompt. A tool call re-arms it, until a title has been lifted. A run the server has forgotten replayed as an error under an empty bubble, and came back again on the next reload, and the one after. There is no conversation left to put an error under, so the turn goes too. The agent menu was 280px for two rows that need 150; the width is the ceiling now, for the row of an agent that is missing and says how to get it, whose mark also dims with its text rather than staying in its brand colour beside it. And a history row keeps its timestamp on one line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A strip under the composer: which model, which effort, and the tokens the last message took. Both lists are data on the AgentDef, beside the argv each agent takes. Claude's is written down, since Claude Code publishes no list. Codex keeps its own at ~/.codex/models_cache.json — the server-sent presets its own picker draws, with display names, context windows and the reasoning levels each model actually takes, which differ per model — so AgentDef carries a modelsFile of a home-relative path and a pure read(json), and vite.config.ts does the readFileSync beside the PATH probe. agents.ts stays free of node. Default is the first row of both pickers and sends no flag, so config.toml and Claude's own settings keep deciding until you pick. A choice is per agent and kept; a stale one is not sent; the server checks both against the same list, since both reach a command line. Effort is not symmetric: claude takes --effort and refuses an unknown level at startup, codex takes -c model_reasoning_effort and finds out mid-turn, which is why the levels come per model. The count rides in on a new usage event. Claude's result frame sums input, both cache figures and output, and names the window in modelUsage — largest of the models listed, as a turn with a sub-agent reports the sub-agent's first. Codex reports usage and no window at all; the server stamps it from the same cache. The number is one message, not the conversation: each is still its own process. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deploying super-prototyping with
|
| Latest commit: |
66cae13
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://544fca6f.super-prototyping.pages.dev |
| Branch Preview URL: | https://canvas-agent-chat.super-prototyping.pages.dev |
The header mark is the agent the next message goes to, so a Codex conversation opened from history under a Claude mark was the header saying something untrue about what was on screen — and the model and effort under the composer were the other agent's as well. The history row already carries who ran it; picking one now selects that agent, the same way the menu does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ecd428bc15
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| abort.current.abort(); | ||
| abort.current = new AbortController(); | ||
| setTurns([turnFor(runId)]); | ||
| follow(runId); |
There was a problem hiding this comment.
Preserve the active run when selecting history
When an agent is still running and the user selects a completed history row, this aborts only the SSE follower and replaces the active turn without cancelling its child process. Because the selected turn soon has an end event, the composer permits another run while the abandoned agent is still editing the same project, allowing concurrent agents to overwrite or conflict with each other's changes. Keep the active run tracked, prevent history switching while it runs, or cancel it before replacing the turn.
Useful? React with 👍 / 👎.
| const used = | ||
| (u.input_tokens ?? 0) + | ||
| (u.cache_creation_input_tokens ?? 0) + | ||
| (u.cache_read_input_tokens ?? 0) + | ||
| (u.output_tokens ?? 0); |
There was a problem hiding this comment.
Do not divide cumulative turn usage by one context window
For a request that invokes tools or sub-agents, Claude's result-frame usage is cumulative across multiple model invocations, as the accompanying comment also acknowledges. Summing all of those inputs and outputs and pairing the result with one model's context-window size does not measure how full any individual context was and can produce an exaggerated or even greater-than-100% readout. Either derive the maximum per-invocation occupancy or present this value as aggregate token usage without a context-window denominator.
Useful? React with 👍 / 👎.
tldraw's navigation panel sits in the bottom-left corner, which is where the chat panel's composer now ends, so it was a widget in the way of the one place you type. Nothing goes with it: zooming was already the trackpad, ⌘+ and ⌘-, and ⇧1 to fit, and the minimap of a page of boards shows boards. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Typing "/" now lists what the agent can run. The commands already ran — claude -p takes one as the message text the same way the terminal does, verified through the panel's own stdin path, skills included — so what was missing was knowing the names. The list is Claude Code's own: its init frame names every command available in that project, the plugins' and the skills' among them, namespaced as it namespaces them. The dev server keeps what the frame says, so the panel never discovers commands a second way and nothing goes stale. Nothing is spawned to ask, either: the frame needs a message to work on, and a run with an empty one still bills for the system prompt, $0.017 measured — the palette is empty until the session's first answer instead. Codex defines none of this, because codex exec hands /foo to the model as the five characters it is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cf07cddb72
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (req.method === "POST" && url.pathname === "/run") { | ||
| let body = ""; | ||
| req.on("data", (chunk) => (body += chunk)); | ||
| req.on("end", () => { | ||
| try { | ||
| const { message, canvas, agent = "claude", model = "", effort = "" } = JSON.parse(body || "{}"); |
There was a problem hiding this comment.
Reject cross-origin agent launches
When the canvas is running on its default loopback port, any website visited by the user can issue a no-cors POST with a text/plain JSON body to this endpoint; because the handler validates neither Origin nor content type, it will parse that request and launch Claude with bypassPermissions in the user's project. Require a same-origin check or an unguessable per-server token before accepting agent-control requests.
Useful? React with 👍 / 👎.
| const run = Object.assign(newRun(randomUUID()), { | ||
| child: spawn(def.bin, def.args({ preamble, boards: canvasesDir, model, effort }), { | ||
| cwd: project, | ||
| env: process.env, | ||
| }), | ||
| }); |
There was a problem hiding this comment.
Serialize agent runs at the server boundary
Even if history switching is disabled, opening the canvas in a second tab or sending a second API request still reaches this unconditional spawn while another run is active, because running is only client-local React state. Both agents can then edit the same project concurrently and overwrite or conflict with each other's work, so the server should reject or queue a new run while any retained run is unfinished.
Useful? React with 👍 / 👎.
Escape closes the palette for the word being typed, and the composer's change handler forgets that as soon as the draft stops being a slash word. Sending does not go through that handler — it clears the draft directly — so a word that was escaped and then sent left the suppression latched on, and every later slash word opened nothing at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The arrow keys already moved the selection; nothing on screen moved with it. The selected row was painted in the hover grey, which is a 5% step on white, and :hover paints that same grey on whatever row the pointer is resting over — so two rows looked alike and neither looked chosen. The keyboard selection now takes the accent, and outranks :hover so the pointer cannot impersonate it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The palette was truncated to eight rows, so typing "/" on an install with 132 commands showed the alphabetical head of the list and silently hid the rest — including, for a narrow filter, the one being looked for. The list now holds all of them and scrolls, and the row the arrow keys land on scrolls itself into view. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A plus beside the history button clears the log so the next message has nothing above it. The runs themselves were already independent of each other, so this is about the view rather than the agent: a run still going keeps going and stays in the history list, since stopping one is what the Stop button under the composer is for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7e76f117f1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const run = Object.assign(newRun(randomUUID()), { | ||
| child: spawn(def.bin, def.args({ preamble, boards: canvasesDir, model, effort }), { | ||
| cwd: project, | ||
| env: process.env, | ||
| }), | ||
| }); |
There was a problem hiding this comment.
Terminate agent children when the dev server closes
When a canvas running via the non-tmux background path is stopped while an agent is active, sp-canvas stop sends SIGTERM only to the Vite PID, while this independently spawned child has no server-shutdown cleanup. The agent can therefore continue modifying the project after the UI and in-memory run registry have disappeared; register lifecycle cleanup that terminates every active agent process (and its process tree) when the server closes.
Useful? React with 👍 / 👎.
Codex runs no slash commands: `codex debug prompt-input` — the prompt the CLI would send, composed locally without spending a turn — shows "/foo bar" arriving at the model as those characters, whether or not a custom prompt of that name exists, since the TUI is what expands those. Listing its TUI commands would have offered things that do nothing here. What a slash means to codex is a skill, and skills do reach `exec`: the same probe carries a skills block naming every one, across the personal directory, the plugin caches and this project's own. So the table gains a second way of answering the question — `commands` reads a line the agent writes anyway, `commandsProbe` asks an agent that writes no such line — and codex's palette is that block, read once per server lifetime, three seconds and no network. The two are not the same mechanism and the notes say so: Claude Code executes its own command, codex is told what its skills are and decides. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b59cc3af30
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const { runId } = await res.json(); | ||
| setTurns((ts) => [...ts, { ...turnFor(runId), prompt: message, agent }]); |
There was a problem hiding this comment.
Persist the run ID before relying on React state
If the canvas reloads after the POST creates the process but before the turns effect writes RUNS_KEY—for example, a startup hook or another process rewrites a board while this request is pending—the new run ID exists only in the interrupted page. After remount, the panel does not follow or show the still-running agent and appears ready for another message. Persist the ID synchronously when it is received, or reconcile active server runs on mount, rather than waiting for the state-driven effect.
Useful? React with 👍 / 👎.
A dev server listens on a port every page in the browser can reach, and CORS withholds a cross-origin reply, not the request: a site the user visits while the canvas is open could POST to /__sp/agent/run and start an agent holding bypassPermissions in their project. The other /__sp endpoints write too — a board's status, a comment, a cloned canvas. The browser already says where a request came from, and a page cannot argue with it: Sec-Fetch-* are forbidden header names. One check in front of all of them, and absent stays allowed, because absent means the caller was not a browser and curl was never the attack. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The composer already refused a second message while a run was open, but that refusal was React state: a second tab does not share it, a reload does not restore it, and the new-session button clears the view it was read from. Two agents in one project overwrite each other's boards, so the server is where the answer belongs — 409 while it holds an unfinished run, and the panel shows the sentence it sent. `sp-canvas stop` signals the process group now instead of the pid. The server is its own group leader, so the agents its panel spawned stop with it rather than carrying on in a project whose canvas has closed. Two small ones on the way past: the slash palette matches a command written in capitals, and lists a skill reachable from two roots once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 033b80760c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (match[2] === "cancel" && req.method === "POST") { | ||
| run.child.kill(); | ||
| return send(200, ""); |
There was a problem hiding this comment.
Terminate the whole agent process tree on Stop
When Stop is pressed while the agent has a shell command or sub-agent running, run.child.kill() signals only the direct CLI process, so its descendants can remain alive and continue editing after this run emits end; the server then permits another run, allowing the orphan and the new agent to modify the project concurrently. The new process-group shutdown in sp-canvas stop only covers stopping the entire server, so cancellation needs its own process-tree termination strategy.
Useful? React with 👍 / 👎.
A bordered box with the submit inside it — the return arrow, the square while a run is going — and under it a row that reads left to right as what the agent may do and what it runs as: the permission label, then the model, the effort, what the last message cost, and a spinner while one is in flight. The textarea keeps no chrome of its own, so the box is what grows and what takes the focus ring, and the slash palette still hangs off its top edge. The placeholder says what the box can do rather than who is listening; the header mark already names the agent. Three controls of that row are not here: the microphone, because there is no voice input; the chevron that switches Edit and Bypass mode, because the mode is fixed when the process is spawned and the label states it rather than offering it; and `+`, because the prompt reaches the agent as one string on stdin and there is nothing yet to attach to it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 66cae13dc7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * and every screen-space calculation stays right (see .canvas-shell above). | ||
| */ | ||
| .sp-chat { | ||
| width: 360px; |
There was a problem hiding this comment.
Reserve the chat width when clamping the inspector
When the inspector is open, its width is still capped at window.innerWidth - 280 by usePanelWidth, without accounting for this new 360px flex sibling. At the inspector's maximum width—even on a large window—the two non-shrinking panels consume more than the viewport and collapse the tldraw editor to zero width; the collapsed 36px rail still reduces it below the intended 280px minimum. Clamp the inspector against the space remaining after the current chat width, or otherwise include both panels in the width calculation.
Useful? React with 👍 / 👎.
What this changes
The canvas grows a chat panel: you talk to Claude Code or Codex in the page, in
your own project, and watch it work. It runs from the dev server the canvas
already has, so there is no second backend.
keeps its full width. Two-step transport (
POST /__sp/agent/run→ 202{runId}, then SSE from?after=N), which is what makes a board rewrite —which reloads the whole page — pick its own run back up instead of losing it.
ChatEventwas already agent-neutral, so thesecond CLI is a data literal plus a parser:
agents.tsholds argv, stdin andthe parser per agent, and
vite.config.tsspawns from the table. The mark inthe header is the switch; agents are found with a PATH probe and the ones you
do not have say how to get them. Codex's body arrives in one piece rather
than streamed — that is
codex exec --json, not us, and the parser's headersays so.
remend→marked→DOMPurify, 27 KB gzipped with thesanitizer. Streamdown was measured (159 KB gzipped, ~110 packages) and
rejected;
remendalone is the piece that stops mid-token flicker.is read from the cache its own picker draws, so a model you gain by updating
the CLI is simply there; effort levels come per model because they differ.
Default sends no flag, leaving your
config.tomland Claude's settings incharge. The number on the right is the tokens that message used against the
model's window — that message, not the conversation, since each one is its
own process.
server's recent runs, and folds to a rail.
Decisions are in
docs/2026-09-17-canvas-chat-panel.md, including what wasdeliberately left out: no resume for either agent, no
codex app-servertransport, no settings page.
Needs the toolkit reinstalled, not only the plugin updated —
sp-canvas1.2.0predates
PROTOTYPING_PROJECT_DIRand every message answers 503 without it.Checklist
ref-*.html,assets/refs/or other third-party captures are in this PR.gen.pywas edited and re-run, and theNN-*.htmlboards were not hand-edited. (no canvas folder changed)layout.json,probes.json,crops.jsonandassets.jsonare committed alongside the boards. (n/a)README.mdand no folder has its own.gitignore. (no new folder)## UnreleasedinRELEASE-NOTES.md.canvas/changed:bun run test(126 pass) andbun run buildpass incanvas/. Note the suite isvitest run— a barebun testbypasses the Vite plugins and fails onvirtual:canvasesonmaintoo.🤖 Generated with Claude Code