refactor(chat-ui): the composer owns its editor, chips, attachments and submit
What & why
In the New Workspace screen the mic sat to the right of submit. The screen drew its own submit button (hideSubmit + a button in toolbarEnd), and dictation was added assuming the built-in one is last. That was one symptom of a wider pattern: the screen routed around the shared composer in several places, through its own submit, attachment list and pills, a header slot, and link buttons, and each mismatch became a prop.
This PR makes the composer own its editor, chips, attachments and submit. Both screens fill two slots (mode picker on the left, model picker on the right) and set a couple of options.
Proposal with before/after tables and mocks: https://app.superset.sh/page/tightening-the-shared-composer-mh4hrw
Four commits:
- Delete the v1
NewWorkspaceModal. It only rendered whenuseIsV2CloudEnabled()was false, and that returnstrue. - Move
TiptapPromptEditorunder the terminal rich input. That input is its only user and is legacy; it is not ported onto the shared composer. updateQueuedPrompt(protocol → runtime → router → client → hook) rewrites a queued prompt in place.- Composer rework:
- Built-in submit gains
disabled/pendingand shows Queue while a turn runs. Removed:hideSubmit,clearOnSubmit,submitWhileStreaming,header,onAddFiles,placement,onMentionHighlight,onChipClick. - New Workspace renders the chat's
ModePicker/ModelPicker, fed fromagent-models. "No agent" moves to the row below the composer. - Linked tasks, Linear issues, GitHub issues and PRs are
@-mention chips that carry their data. A PR chip still drives "based off PR #N". Pasted Linear / GitHub URLs resolve to the item. Menu rows and chips show the file, tracker and PR-state icons. - The draft is
ComposerDraft { segments, attachments }, replacingdefaultValue/onChange/findChips/appendText. A failed send or a failed answer to an input request comes back with its chips and attachments. Text-only drafts from older builds still load. - Pastes of 5,000+ characters become text attachments with "Show in text field", like Codex. The paste-card hook and the
<pasted_text>writer are gone; the parser stays so older messages still render. - Queued rows edit in place. A bare
@opens with Add → Attach files. - Rebased onto #8585 / #8682: the
/mcppicker no longer needsfindChips, and the ref keepsinsertChip/openPanelfor it.
- Built-in submit gains
PromptInput goes from 27 props to 19. New Workspace passes 11, none of them workarounds.
How I tested it
Drove the dev app over CDP with real pointer and keyboard input, on the rebased build:
- New Workspace: footer order
⊕ · mode · model · mic · send; model menu with agent tabs and the effort slider; No-agent toggle; bare@with Add first and recents in each section; task / PR chips, with "based off PR #N" and the checkout locked;@<PR URL>resolves to that PR; a 6,500-char paste becomes a card, and Show in text field puts it back. - Chat pane (Haiku): Stop with an empty editor; "Queue message" once typed; a follow-up queues behind a running turn, its row edits in place, and Stop pauses the queue;
/mcpopens its picker; "Edit message" opens the sent text. - Not driven: getting a failed send back in the composer (no way to force an outbox failure from the UI). It's covered by
useComposerDraftand draft round-trip tests. bun run lint,check:i18n,check:pluginsand sherif are clean. Typecheck is clean apart from the better-auth errors that are also onmain. Tests: chat-ui 21, chat 82, chat-runtime 302, i18n 164 pass. Desktop is 8,468 pass / 37 fail, and all 37 are main-process git tests (workspaces/utils/git.test.tsand similar) in files this PR doesn't touch.
Known issue, also on main, not fixed here: a chat started from New Workspace ignores the picked model. The screen sends the CLI id (haiku), and acpAdapter.applyStartSelections only applies an exact ACP option id. Follow-up PR.
Checklist
- PR title follows conventional commits (
type(scope): subject) -
bun run lintandbun run typecheckpass (CI fails on lint warnings too) - "Allow edits from maintainers" is checked on fork PRs (n/a, not a fork)
Summary by cubic
The shared composer now owns its editor, chips, attachments, and submit; the New Workspace screen fills two slots (mode and model pickers) instead of wiring its own submit button, header, and link buttons around the composer.
Composer
- The built-in submit gains
disabled/pendingand labels itself Queue while a turn runs;hideSubmit,clearOnSubmit,submitWhileStreaming,header,onAddFiles,placement,onMentionHighlight, andonChipClickare removed. - The draft is
ComposerDraft { segments, attachments }, so a failed send returns with its chips and attachments; text-only drafts from older builds still load. - Tasks, Linear/GitHub issues, and PRs are
@-mention chips that carry their data, and pasted Linear/GitHub URLs resolve to the item. - Pastes over 5,000 characters become text attachments with "Show in text field"; the
<pasted_text>writer is gone but the parser stays so older messages still render. - Queued rows edit in place;
updateQueuedPromptrewrites a prompt without removing and re-sending it. - New Workspace renders the chat's
ModePicker/ModelPicker, and "No agent" moves to the row below the composer.
Cleanup
- Deleted the unreachable v1
NewWorkspaceModaland movedTiptapPromptEditorunder the terminal rich input, its only user. PromptInputprops drop from 27 to 19; New Workspace passes 11, none of them workarounds.
Known issue, not fixed here: a chat started from New Workspace sends the CLI id, so the picked model is ignored.