Skip to content

feat: on-device grammar check (Harper) in both composers#4

Open
theetherGit wants to merge 1 commit into
mainfrom
feat/harper-grammar
Open

feat: on-device grammar check (Harper) in both composers#4
theetherGit wants to merge 1 commit into
mainfrom
feat/harper-grammar

Conversation

@theetherGit

Copy link
Copy Markdown
Contributor

On-device grammar & spell check (Harper)

Adds grammar/spell checking to the message body in both composers (new message + reply) using harper.js — a WASM engine that runs entirely client-side. Draft text never leaves the device (fits the encrypted-mail model; no Grammarly/LanguageTool cloud round-trip).

How it works

  • harper-extension.ts — a TipTap/ProseMirror extension. Flattens the doc to plaintext with a char→PM-position map, lints (debounced) in a shared WorkerLinter, and paints wavy-underline decorations (red = spelling, amber = grammar/style).
  • Click a word → suggestion menu (ranked) → pick a fix → the correction flashes green. A separate decoration layer keeps the flash alive across re-linting.
  • tiptap-editor.svelte — wires the extension, the suggestion menu (portaled to <body> so it isn't clipped by the transformed compose drawer), a status pill (only shown when there's content), and a bodyClass prop for an inline max-height cap. Native spellcheck is turned off so Harper owns the squiggles.
  • reply-composer.svelte — switched from the execCommand rich-editor to the shared TiptapEditor, so grammar check works in replies too.

Performance

  • Lazy: the ~17 MB WASM is dynamically imported — never on SSR or the cold inbox, only when an editor mounts. Rollup emits it as a hashed, browser-cached asset.
  • Off the main thread: linting runs in a Web Worker (blob URL, no bundler worker config).
  • Warm-on-mount: getLinter() fires at requestIdleCallback so the download/compile happens in the worker before the first word is typed; one memoized linter is shared across every editor.
  • vite.config: excludes harper.js from dep pre-bundling (dev-only) so its new URL(..., import.meta.url) WASM path resolves from node_modules.

Verification

  • svelte-check: 0 errors (only the pre-existing vite.config.ts vite7/8 type-mismatch noise)
  • vitest: 158/158
  • pnpm build: passes (wasm emitted as a lazy hashed asset)
  • Svelte MCP autofixer: clean

🤖 Generated with Claude Code

Add spelling/grammar checking to the message body using harper.js — a
WASM engine that runs entirely client-side, so draft text never leaves
the device (fits the encrypted-mail model; no Grammarly/LanguageTool
cloud round-trip).

Implementation
- harper-extension.ts: a TipTap/ProseMirror extension. Flattens the doc
  to plaintext with a char->PM-position map, lints (debounced) in a
  shared WorkerLinter, and paints wavy-underline decorations. Clicking an
  underline opens a suggestion menu; picking one applies the fix and
  flashes the span green (a separate decoration layer so re-linting
  doesn't wipe it).
- tiptap-editor.svelte: wires the extension, the click menu (portaled to
  <body> so it isn't clipped by the transformed compose drawer), a status
  pill, and a bodyClass prop for an inline max-height cap. Native
  spellcheck is turned off so Harper owns the squiggles.
- reply-composer.svelte: switch from the execCommand rich-editor to the
  shared TiptapEditor so grammar check works in replies too.

Performance
- Lazy: the ~17 MB WASM is dynamically imported, never on SSR or the cold
  inbox — only when an editor mounts. Rollup emits it as a hashed,
  browser-cached asset.
- Off the main thread: linting runs in a Web Worker (blob URL, no bundler
  worker config needed).
- Warm-on-mount: getLinter() is kicked off at requestIdleCallback so the
  download/compile happens in the worker before the first word is typed;
  one memoized linter is shared across every editor.
- The status pill only shows once the body has content.
- vite.config: exclude harper.js from dep pre-bundling (dev-only) so its
  `new URL(..., import.meta.url)` WASM path resolves from node_modules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: theetherGit <meenashivam9650@gmail.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
doota-mail-jobs 308627a Jul 24 2026, 04:07 PM

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
doota 308627a Commit Preview URL

Branch Preview URL
Jul 24 2026, 04:08 PM

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.

1 participant