Merge remote-tracking branch 'origin/main' into port-2619

# Conflicts:
#	packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx
This commit is contained in:
Bohdan Triapitsyn
2026-08-29 00:26:49 +03:00
1318 changed files with 118125 additions and 30528 deletions
@@ -7,6 +7,16 @@ everything between typing and sending.
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 |
@@ -62,6 +72,15 @@ copy.
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 `<textarea>` over a mirror
`<div>`. That restricted highlighting to styles which do not change glyph
advance width — colour, background, underline — because anything else made the
@@ -73,20 +92,54 @@ question of design, not of feasibility.
Selection rendering: every device runs CodeMirror's `drawSelection()` — it
keeps typing on the drawn-selection code path, and removing it makes
CodeMirror enforce cursor association on the native selection, which iOS
answers with severe input lag. Every device also layers
`composerNativeSelectionExtension` (`editor/theme.ts`) on top: it re-shows
answers with severe input lag. **That much is not platform-specific and must
not be undone.** What differs is who paints the selection, and
`composerSelectionExtension` (`editor/theme.ts`) picks that per platform.
When CodeMirror 6.43.9's iOS predicate does not match,
`composerNativeSelectionExtension` layers over `drawSelection()`: it re-shows
the native selection, and — only while a range is selected — the native caret,
hiding the painted layers those replace. The native selection is the one that
shows for two reasons: the painted layer sits behind the content, so tokens
with their own background (inline code, fences) cover it completely; and
iOS's selection drag handles attach to the visible native selection and take
their colour from the caret, so a transparent caret means invisible handles.
The range-only caret scoping is load-bearing — a native caret visible while
typing makes WebKit re-render its caret UI after every keystroke, felt as
severe input lag. The selection tint comes from `--primary`, not the selection
token:
themes define `--interactive-selection` with its own alpha, so a translucent
mix of it is nearly invisible.
with their own background (inline code, fences) cover it completely; and the
platform's selection drag handles attach to the visible native selection and
take their colour from the caret, so a transparent caret means invisible
handles. The range-only caret scoping is load-bearing — a native caret visible
while typing makes the browser re-render its caret UI after every keystroke,
felt as severe input lag.
When CodeMirror 6.43.9's exact iOS predicate matches,
`composerIOSSelectionExtension` leaves selection-handle geometry and appearance
to CodeMirror. CodeMirror puts the handles in `.cm-selectionLayer`, normally at
`z-index: -1`; the extension raises that layer above the content so opaque
token backgrounds cannot cover them, and leaves it transparent to touch.
The handle dots extend 8px past their range; matching scroller padding and
negative margin expand the clip area without moving the text or changing the
composer height. iOS still paints its taller system selection overlay even
when CSS makes `::selection` transparent. The extension therefore suppresses
CodeMirror's synthetic selection rectangles on iOS while leaving its handles,
cursor path and `nativeSelectionHidden` facet active. Otherwise the grey system
highlight and themed rectangle overlap with visibly different heights.
Do not add a second custom layer or custom handles here: overlapping translucent
rectangles make selection darker at their seams and imitated handles drift from
the geometry WebKit actually manipulates. What iOS avoids is installing the
native-selection workaround above: explicitly restoring native paint and caret
makes WebKit re-measure them after every decoration redraw, and the composer
rebuilds every decoration on every keystroke. That cost is felt worst during
IME composition.
The non-iOS native selection tint comes from `--primary`, not the selection
token: themes define `--interactive-selection` with its own alpha, so mixing it
with transparent again is nearly invisible. The iOS system overlay owns its
visible selection fill.
The content element keeps the existing correction policy: on in the mobile UI,
off elsewhere. CodeMirror also reads the attribute and reverts Apple and
Android's insert-period-on-double-space only when its value is exactly `off`.
`editor/autocorrect.ts` uses the HTML standard's
[ASCII case-insensitive `autocorrect` keywords](https://html.spec.whatwg.org/multipage/interaction.html#attr-autocorrect)
to keep desktop word correction off while avoiding that CodeMirror-only
revert. Its platform checks deliberately match CodeMirror's own browser flags.
`composerLanguage.ts` retokenizes the whole document on every change. The
composer holds a prompt, not a source file: it is short enough that a full pass
@@ -100,10 +153,14 @@ and the send path reading the same grammar.
drawn caret through a class it only writes while applying an update, so the
selection has to be the update that follows the focus.
- `submit/buildOutgoingMessage.ts` flattens queued messages, the composer text,
inline comments and context into OpenCode's one-primary-plus-parts shape. The
oldest queued message becomes primary; **inline comments attach to the last
body the user authored** rather than becoming their own part; PR instructions
precede the PR diff.
context drafts and linked references into OpenCode's one-primary-plus-parts
shape. The oldest queued message becomes primary. **Every attached context
item (inline comments, terminal selections, browser annotations, PR context,
linked issue/PR) becomes its own synthetic text part carrying structured
metadata** built by `lib/messages/contextParts.ts`; the timeline reads that
metadata back to render context blocks. PR instructions precede the PR diff.
Queueing a message leaves context drafts in their store on purpose — the send
that later delivers the queue consumes them.
- `state/useComposerDraft.ts` — a draft belongs to a (runtime, directory,
session) identity. Writes are debounced while typing but forced at every edge
where the page may stop running, because a pending timer is not a saved
@@ -113,6 +170,9 @@ and the send path reading the same grammar.
- `state/useDraftTarget.ts` — the draft can target a directory that does not
exist yet (a worktree being created). It must survive not appearing in the
branch list, or the selector snaps back to the project root mid-creation.
- `ui/DraftTargetSelectors.tsx` owns the controlled project/worktree picker
state and registers its application shortcuts locally. The selectors only
consume their shared prefix while the draft target UI is mounted.
## Mobile