Skip to content

Type dictated words at the note's cursor instead of the bottom - #192

Merged
davior merged 1 commit into
mainfrom
claude/dazzling-lamport-gh4vx0
Sep 26, 2026
Merged

davior merged 1 commit into
mainfrom
claude/dazzling-lamport-gh4vx0

Conversation

@davior

@davior davior commented Sep 26, 2026

Copy link
Copy Markdown
Owner

Problem

When dictating into a note, the words always appeared at the bottom of the note, even if the cursor had been placed somewhere in it.

Root cause: clicking the mic button moves focus off the editor on mousedown, before onClick runs. The onMouseDown={e => e.preventDefault()} that used to prevent this was removed so the mic button could keep focus for Enter/Space toggling. As a result handleDictationToggle's editor.isFocused() check was always false, the cursor anchor was always null, and every chunk fell through to "append at end of note". And when the anchor was captured, it inserted a new paragraph after the cursor's block rather than typing at the cursor.

Fix

ProseMirror keeps its selection when the editor blurs, so dictation now types straight into that selection. This doesn't depend on which element has focus.

  • New utils/dictationInsert.ts: insertDictationAtSelection(tr, text) inserts at the selection like typing does:
    • Adds spaces around the words where needed (none after whitespace or opening brackets, none before punctuation).
    • Picks up the marks at that position (e.g. bold).
    • Collapses a range selection to its end, so selected text is never overwritten.
    • Leaves the caret after the words, so each chunk continues in order.
    • Returns false when the selection can't hold text (image block, select-all), so the caller can fall back.
  • EditorView.tsx:
    • editorHasCursorRef tracks whether the user has put the cursor in the note. It's set from the editor wrapper's onFocus and reset when a note is loaded into the editor.
    • With a cursor, dictation types there. If the user clicks elsewhere mid-session, dictation follows the cursor.
    • With no cursor, the words go at the bottom as before. They now fill the trailing empty line instead of leaving a blank line above, and the rest of the session continues on that line.
    • Removed the old session-block and anchor refs, handleDictationToggle, and the dictation-only anchorBlockId parameter of insertBlocksAtCursor.
    • The mic button still takes focus so Enter/Space toggle it.
  • prosemirror-state and prosemirror-model are declared as direct dependencies at the versions already installed through BlockNote (1.4.4 / 1.25.9). The lockfile resolution is unchanged and vite.config.ts already dedupes them.

Testing

  • npx tsc --noEmit ✅, npm run build ✅, npx vitest run: 155 passed, including 12 new unit tests for the insertion helper.
  • End-to-end in Chromium (Playwright, Vite dev server + real backend, Web Speech API mocked):
Scenario Before After
Click between "Hello " and "world", click mic, dictate "there" / "my good friend" appended at bottom as a new line Hello there my good friend world
Cursor placed with arrow keys, then mic clicked appended at bottom Hello big wide world, caret left before world
No cursor placed in the note bottom, after a blank line bottom, on the trailing empty line
User clicks another line mid-session both chunks at bottom each chunk lands at the cursor at that moment
Mic keeps focus; Enter stops dictation ✅ ✅

Also checked that the dictated text autosaves: the server copy contained Hello world and goodbye.

🤖 Generated with Claude Code

https://claude.ai/code/session_017Go397yGvJqhvMQhc53Q46


Generated by Claude Code

Clicking the mic moves focus off the editor before its click handler
runs, so the "is the editor focused?" check used to capture the cursor
always failed and every dictated chunk was appended to the end of the
note. Even when it did catch the cursor, it started a new paragraph
after the cursor's block rather than typing at the cursor.

ProseMirror keeps its selection when the editor blurs, so dictation now
types straight into that selection (collapsing a range to its end, with
spacing to neighbouring words), leaving the caret after the words so
each chunk continues in order. This works however focus moves, so the
mic button keeps focus for Enter/Space toggling, and it follows the
cursor if the user clicks elsewhere mid-session.

With no cursor placed in the note (tracked via the editor wrapper's
focus, reset when a note is loaded) the words go at the bottom as
before, now filling the trailing empty line instead of leaving a gap.

prosemirror-state/-model are declared as direct dependencies at the
versions already installed through BlockNote.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Go397yGvJqhvMQhc53Q46
@davior
davior marked this pull request as ready for review September 26, 2026 18:47
@davior
davior merged commit 0f2e83c into main Sep 26, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants