From eca9353382f803960ef5940fe28db62decdbcbf2 Mon Sep 17 00:00:00 2001 From: Iuliia Ivashko Date: Fri, 28 Aug 2026 19:14:02 +0300 Subject: [PATCH] fix(composer): keep the caret inside the normalized document CodeMirror collapses a CRLF pair into one line break, so the document is shorter than the string it was given. The composer derived the caret from the JS string length, which put it past the end of the document and made dispatch throw `RangeError: Selection points outside of document`. Because the exception fires before the transaction applies, the document never updates, the un-normalized text stays in React state, and the draft persists as-is: every later visit to the session restores it and crashes again, with no way out from the UI. Derive the caret from the change set instead, in the controlled writeback and in the imperative insert/replace handles. fixes #3013 # Conflicts: # CHANGELOG.md # packages/vscode/CHANGELOG.md --- CHANGELOG.md | 1 + .../components/chat/composer/DOCUMENTATION.md | 9 +++ .../chat/composer/editor/ComposerEditor.tsx | 30 +++++----- .../editor/__tests__/documentEdits.test.ts | 58 +++++++++++++++++++ .../writebackCompositionGuard.test.ts | 2 +- .../chat/composer/editor/documentEdits.ts | 33 +++++++++++ packages/vscode/CHANGELOG.md | 1 + 7 files changed, 117 insertions(+), 17 deletions(-) create mode 100644 packages/ui/src/components/chat/composer/editor/__tests__/documentEdits.test.ts create mode 100644 packages/ui/src/components/chat/composer/editor/documentEdits.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e38ff2ab..a501e816 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file. - Files: opening a file over 5,000 lines is no longer blocked — the line-count guard now allows up to 20,000 lines, letting large files reach the virtualized full-file preview instead of being rejected at the open step (thanks @gaojunran). - Usage: GitHub Copilot now shows a single AI Credits window, matching Copilot's token-based quota, in place of the old Chat Requests and Completions windows (thanks to @jakoss). - Updates: "Update OpenCode" no longer fails with a bare "Bad Request". OpenChamber now names the release to install, which recent OpenCode versions require, and when an update is refused the reason from OpenCode is shown instead of the HTTP status. This affects setups where OpenChamber runs an OpenCode you installed yourself; the desktop app bundles OpenCode and never offered the button. +- Chat: a saved draft or recalled message containing Windows line endings no longer replaces the chat with a "Selection points outside of document" error — text like this reached the input from reverted messages, message history and plugin output, and once it was saved as a draft the error came back on every visit to that session. - Settings: fixed the Cloudflare Tunnel download link shown when cloudflared is not installed (thanks to @AyoubAchour). - Git: picking a remote branch such as `origin/main` in the branch selector now switches you to that branch instead of leaving the repository on a detached `HEAD` with no branch name. - Desktop: "Restart to Update" no longer looks dead when the update cannot be installed — the update window now shows the reason, including when the running copy was not installed from an official signed release, and the button stays available to retry. diff --git a/packages/ui/src/components/chat/composer/DOCUMENTATION.md b/packages/ui/src/components/chat/composer/DOCUMENTATION.md index a3d0854e..2302e3a0 100644 --- a/packages/ui/src/components/chat/composer/DOCUMENTATION.md +++ b/packages/ui/src/components/chat/composer/DOCUMENTATION.md @@ -60,6 +60,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 `