refactor(chat-ui): the composer owns its editor, chips, attachments and submit

Draft
SA
10d ago
+2391-9369

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:

  1. Delete the v1 NewWorkspaceModal. It only rendered when useIsV2CloudEnabled() was false, and that returns true.
  2. Move TiptapPromptEditor under the terminal rich input. That input is its only user and is legacy; it is not ported onto the shared composer.
  3. updateQueuedPrompt (protocol → runtime → router → client → hook) rewrites a queued prompt in place.
  4. Composer rework:
    • Built-in submit gains disabled / pending and shows Queue while a turn runs. Removed: hideSubmit, clearOnSubmit, submitWhileStreaming, header, onAddFiles, placement, onMentionHighlight, onChipClick.
    • New Workspace renders the chat's ModePicker / ModelPicker, fed from agent-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 }, replacing defaultValue / 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 /mcp picker no longer needs findChips, and the ref keeps insertChip / openPanel for it.

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; /mcp opens 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 useComposerDraft and draft round-trip tests.
  • bun run lint, check:i18n, check:plugins and sherif are clean. Typecheck is clean apart from the better-auth errors that are also on main. 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.ts and 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 lint and bun run typecheck pass (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/pending and labels itself Queue while a turn runs; hideSubmit, clearOnSubmit, submitWhileStreaming, header, onAddFiles, placement, onMentionHighlight, and onChipClick are 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; updateQueuedPrompt rewrites 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 NewWorkspaceModal and moved TiptapPromptEditor under the terminal rich input, its only user.
  • PromptInput props 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.