# Composer The chat composer: the prompt language, the editor that renders it, and everything between typing and sending. `ChatInput.tsx` (one directory up) is the orchestrator. It holds the composer's own state and wires these modules together; it should not grow logic that belongs to one of them. `ChatContainer.tsx` keeps one `ChatInput` mounted while a new-session draft becomes its first session. Draft-only UI first fades for 120ms while the editor stays in place. The parent then moves the editor to its final session position with a 180ms transform-only FLIP animation. Reduced-motion mode skips these transitions. `session-ui-store.ts` marks sessions materialized from a submitted draft, so selecting an existing session while a draft is open switches without animation. Do not restore separate draft and session composer branches: remounting the editor loses focus and interrupts the transition. Keep the existing mobile fixed-position rules unchanged. ## Layers | Directory | Owns | |---|---| | `language/` | What the text *means*: `@` references, `/` and `#` tokens, markdown, and which picker a caret asks for | | `editor/` | The CodeMirror view that renders the language and owns the caret | | `state/` | Composer-local lifecycle state: ArrowUp/ArrowDown browsing, draft stash/restore, mobile shell, popup placement, draft targeting | | `submit/` | Turning what the user has into what gets sent | | `attachments/` | Files: paths, drop payloads | | `ui/` | Presentation | | `text.ts` | How inserted text meets the text already there | | `largeTextPaste.ts` | Detect large plain-text pastes and build virtual `.txt` files | | `largeTextPasteOffer.ts` | Ask-toast offer id begin/resolve (supersede + double-apply guards) | `ChatInput.handlePaste` owns paste orchestration: URL-over-selection markdown links, clipboard images (attach + citation), and large plain-text pastes. Large pastes (about 2,000 characters or 25 lines) follow the composer setting `largeTextPasteBehavior` (`ask` / `attach` / `inline`). Attaching creates an in-memory `text/plain` file named `pasted-context-N.txt`, inserts a bracket citation, and sends it through the same attachment pipeline as a manually picked `.txt` file. Ask-toast actions read live composer/attachment state so typing or other attaches between paste and choice stay consistent. Short text, images, and URL wraps keep their existing paths. ## The prompt language `language/` is the single source of truth for composer syntax. Everything that needs to know what a token means — highlighting, send-time resolution, and the autocomplete triggers — goes through it. **This is the invariant that matters most in this module.** Before it existed, the `@` rule was written four times with divergent cleanup and the `/` rule three times with different valid character sets, so a token could be painted as a reference and then not resolve as one. Adding a construct meant finding every copy. - `mentions.ts` — `@` references. The `start..end` span is the reference itself and is what gets highlighted; in `see @a/b.ts,` the comma is sentence punctuation, not part of the file being referenced. Mentions are plain editable text: deleting a character edits the token and reopens the mention picker, the same way `/skill` tokens behave — not an atomic delete. - `prefixTokens.ts` — `/command`, `/skill`, `#snippet`. Scanning is deliberately generous; **membership in the command, skill or snippet registry is the authority**, not the pattern. An unknown `/token` stays plain prose. - `triggers.ts` — which picker a caret position asks for. Exactly one can be active, with precedence `command > skill > snippet > mention`. - `tokenize.ts` — one pass producing every highlight range. Adding a construct to the language means adding it here, once. ## The editor `editor/` wraps CodeMirror. The document is a plain string: `getValue()` is exactly what gets sent, so nothing downstream serializes a rich document model back into a prompt. The document is not, however, the string it was given: CodeMirror normalizes line endings, so a `\r\n` pair becomes one break and the document ends up shorter than the inserted string. **Never derive a caret position from the length of text you are inserting** — a caret past the end makes `dispatch` throw, the transaction never applies, and the un-normalized text stays in React state to crash again on the next restore. Every edit that moves the caret goes through `replaceWithCaret` (`editor/documentEdits.ts`), which measures the change instead of the string. The composer previously painted a transparent `