feat(desktop): New coding agent in the + opens a draft agent whose first message is its task
Summary
Dean: "I also want to be able to create a new coding agent from the + button." And, clarified: it "must NOT send the plan". It opens a new coding agent tab with an empty transcript and the composer focused. The agent isn't started on the service until the developer sends the first message, so an abandoned empty tab leaves nothing behind. That first message starts the agent with the message as its task, with the same repo, model picker, and preconditions as Start. Start and its behaviour stay exactly as they are.
This is the desktop half of the agreed split. It builds on #1958 (merged: the service starts an agent on a task instead of the plan) and #1952 (merged: agent/subagent tabs from the "+").
-
"New coding agent" row at the top of the "+" menu's Coding agents section, with the panel's own plus glyph and no shortcut. It is gated exactly as Start is: while Start can't run, the row is dimmed and shows Start's own reason ("Choose a repository first."). That reason is set exactly while Start can't run on the open plan. While a Start is out, the row reads "Starting…". With that row always listed, the "+" always opens, so its "Every tab is open" disabled state is gone. Choosing the row when the open plan already has a draft just switches to it, so there is one draft per plan.
-
Draft agent tab (
planning/draft-agent-tab.tsx), named "New agent", stands after the plan's agents:- Where the transcript will be, one line: "Describe a task. The agent starts when you send it."
- Below it, the agent composer (the same component), focused as the tab opens, with the placeholder "Describe a task for the agent…".
- The model chip shows the account's default (read as the tab opens) and uses the same model menu. A choice made there goes with the start, and the service then keeps it as the default, exactly as Start's menu does.
- Nothing is sent until the first message. Closing the tab (× or Delete) discards the draft, and since nothing was started, nothing is left on the service. The next "New coding agent" is a fresh draft.
- The first message calls
CODING_AGENTS_STARTwith{ planId, idempotencyKey: <draft's key>, task, model?/effort? }(#1958). The agent is handed those words, never the plan, and works in the plan's repository. - If the send doesn't start the agent, the words stay in the box, with the reason and Retry underneath. Retry asks again under the draft's key, so a lost answer can't start two agents.
- Once it starts, the agent joins the list and its own tab opens chosen in place of the draft, through the same
onStartedpath Start uses.
-
Task agents draw their task as the developer's message.
AgentTranscriptViewreadsagent.brieffrom #1958: a task agent's first message is a normal user bubble, and only a plan agent's is the "Plan" card. (This answers Devin's note on #1958.) -
Panel state: a
{ draft }tab kind inuse-side-panel.ts. It is shown only while the open plan holds that draft, and is never persisted: a chosen draft tab is saved as no tab, the same way subagent tabs are handled. -
One neighbour rule for every closed tab. Closing the chosen tab picks:
- the next tab of its own run (fixed tabs, open agents, the draft, the plan's subagent tabs);
- else the previous tab in that run;
- else the tab before it in the strip;
- else the tab after it.
This one helper (
neighbourOf) replaces four per-kind functions that spelled out the same rule. The behaviour for fixed and subagent tabs is unchanged, and every existing test still holds. A draft's tab closes to the open agent before it, else the last fixed tab. With no other agent open, an agent's tab closes to the tab before it. -
Control:
useCodingAgentsholds drafts in a map keyed by plan id. Each draft holds its words and its chip's choice (AgentDraft { id, task, choice }), so both survive the tab being hidden and visits to other plans. Each plan keeps its own draft. The hook gains four functions:onNewDraft(returns the plan's draft, or mints one whose key becomes its idempotency key).onDraftChange.onDiscardDraft.startTask, which shares one start path with Start's own press (askStart/landed).- Start's press, key reuse, and toolbar note are unchanged.
-
Nothing new beside Start, and a test asserts the toolbar has no New coding agent button.
-
Docs:
docs/PLANNING.md("Agent tabs").PRIVACY.md: the agents the Mac app lists are those started from Start "or from New coding agent in its side panel".
Bundle budget (re-recorded, approved by Dean)
apps/desktop/bundle-budget.json: the renderer.js baseline is re-recorded in this PR with LUKE_UPDATE_BUNDLE_BUDGET=1, which Dean approved. This is the only PR that touches the file. whiteboard.js and the 5% slack are unchanged.
Why: the baseline was last recorded in #1861 (2026-10-09). Deliberate feature growth since then has used up the 5% slack: the side panel's tabs, the menus, light mode, and the agent tabs. Three finished PRs were each over the ceiling: #1962 (this one), #1956, and #1967. This is growth, not drift.
| renderer/renderer.js (gzip -9) | bytes |
|---|---|
| Old baseline (#1861) | 664,696 |
| Old ceiling (+5%) | 697,930 |
main at 85d15634 | 697,713 (217 bytes of headroom) |
| This PR | 698,487 (557 over the old ceiling) |
| New baseline | 698,487 |
| New ceiling (+5%) | 733,411 |
renderer/whiteboard.js stays at 467,697. The build re-measured it at 468,579 and that value was put back untouched.
Evidence
- Platform-independent checks:
./scripts/check.shexits 0. - macOS Electron verification (
./scripts/verify.sh):not run. CI could not verify this UI change. CI builds nothing for the Mac and this was done on a Linux VM; LUKE-159 is the manual Mac pass.
Visual evidence
Headless Chrome render of the real DesktopPlans page and side panel (stub controls, the renderer's built stylesheet).
The "+" with New coding agent at the top of Coding agents:
| Dark | Light |
|---|---|
![]() | ![]() |
The draft tab: nothing said yet, the composer focused, and the model chip on the account's default:
| Dark | Light |
|---|---|
![]() | ![]() |
A plan with no repository: the row is dimmed and shows Start's reason:
| Dark | Light |
|---|---|
![]() | ![]() |
Physical-device evidence
- Screenshot or screen recording:
not attached(no Mac in this workspace)
Tests
Each new test fails without this change:
desktop-plans.test.tsdrives the real page with the realuseSidePaneland the realuseCodingAgents, wired as the Plans tab wires them, over a scripted service:- New coding agent opens the "New agent" tab chosen, shows the draft line, and focuses the box. No Start has been asked yet. Typed words survive showing Board and coming back. The first send is unanswered: the words stay, with "The agent could not be started. Try again." and Retry. Retry starts the agent with
{ planId, idempotencyKey: <draft key>, task }, the same key both times, and no plan. The draft tab goes and the agent's own tab is chosen. - Closing the draft's tab with typed words asks for no Start, and the next New coding agent opens an empty draft.
- With no repository, the row is disabled, shows
Choose a repository first., and opens nothing. The toolbar has no New coding agent button.
- New coding agent opens the "New agent" tab chosen, shows the draft line, and focuses the box. No Start has been asked yet. Typed words survive showing Board and coming back. The first send is unanswered: the words stay, with "The agent could not be started. Try again." and Retry. Retry starts the agent with
use-coding-agents.test.ts:- A draft asks nothing of the service, and the plan's one draft is answered again. Its words and chip choice are held. Another plan opens a draft of its own without erasing the first, and both are there on return. After the first starts, the other plan's draft remains.
startTasksends the task under the draft's key, with the choice when one was made. A failure keeps the draft and returns the note without touching Start's toolbar note. Success opens the agent's tab and discards the draft.- A discarded draft is gone and the next is new. A plan without a repository starts nothing.
agent-tab.test.ts: brieftaskdraws no plan card and a user bubble, and briefplanstill draws the card.use-side-panel.test.ts: a draft tab shows while its plan holds it and returns when you come back to that plan. A launch reads a chosen draft tab as the first open tab. Closing it picks the open agent before it, and an agent's tab closes to the draft after it.- Updated: #1950/#1952's "+" tests now include the New coding agent row. The "Every tab is open" block is gone, since the "+" always opens now.
Test count: +7.
Invariants
-
./scripts/check.shgreen;verify.shnot run (Linux VM; see above). - JSON Schema goldens unchanged (#1958 carried the wire changes).
- Every behavior change has, in this PR, the test that fails without it; no change-detector test added.
- Gateway envelope goldens unchanged.
- No new
astype assertion outside the allowlisted files (the one cast in the test's scripted acts carries itsSAFETY:note, as inuse-coding-agents.test.ts). - No
Effect.runPromise/runSync/runForkoutside the runtime edges. - No new
setTimeout/setInterval/new Promise/AbortController/fs.watch. - No Effect, Stream, or Layer written as a statement and dropped.
- Test count delta listed above.
- No hand-rolled file replaced.
- Docs naming the changed parts edited (
docs/PLANNING.md);AGENTS.md/CLAUDE.mdname none of them. -
PRIVACY.mdchanged: the agents the Mac app lists now include those started from New coding agent in the side panel. - No dependency changes. The renderer bundle baseline is re-recorded (see "Bundle budget" above; approved by Dean).
- No barrel/door change.
🤖 Generated with Claude Code





