-
Notifications
You must be signed in to change notification settings - Fork 16
Scribe writing companion + Parakeet STT (core seams + engine) #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
alichherawalla
wants to merge
72
commits into
main
Choose a base branch
from
feat/scribe-writing-companion
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 39 commits
Commits
Show all changes
72 commits
Select commit
Hold shift + click to select a range
76d8057
feat(scribe): core seams for a pro editor + settings sections
154b777
feat(scribe): add Scribe to the pro catalog + nav
bcb69a0
test(scribe): pro E2E — squiggle on typo, valid words untouched
4608108
docs(scribe): research findings + desktop implementation plan
39681b1
test(scribe): exercise the document review panel
ff09cd1
test(scribe): refresh squiggle screenshot for the two-column layout
d804c24
docs(scribe): roadmap — import→transform→edit, section feedback, lear…
f74d40e
test(scribe): settings expose style guides + learning loop
7526470
test(scribe): assert the import menu + refresh aligned-squiggle scree…
6481d8e
test(scribe): refresh review + settings screenshots
57cc8c5
feat(scribe): seed the chat composer from Scribe (continue-in-chat)
9480f78
test(scribe): refresh screenshots (readability report + hotkey settings)
e9719b4
feat(crm): preload bindings for create-entity + link-observation
71c76c4
test: fix stale clipboard placeholder assertion
0f18656
test(scribe): refresh screenshots
d0a315b
feat(stt): Parakeet transcription engine (additive, beside whisper)
1b89e7c
feat(stt): engine selector with whisper fallback
0e930a3
build(stt): CI staging for the Parakeet runtime
9b638d2
feat(models): Parakeet STT models in the catalog (downloadable)
d16b95f
feat(stt): use downloaded Parakeet models; keep whisper isolated
c011389
test(stt): e2e for the Parakeet model picker in Voice settings
3f15803
fix(stt): honor active whisper model by catalog id; de-em-dash catalo…
c42de2e
test(stt): point the model-picker e2e at the generalized component te…
ca7aa35
fix(stt): match active Parakeet model by catalog id; refresh screenshots
52e33dc
build(stt): stage Parakeet engine dylibs with the 0.0.28 gates
4b32e86
feat(onboarding): showcase Scribe in the Pro step
5417f05
fix(stt): correct sherpa-onnx flags + slim/robust staging (verified o…
fb1a0fd
test(stt): verify parser on real Parakeet output; gitignore the runtime
9c7c400
refactor(open-core): drop crm preload bindings for pro entity create/…
c71a807
fix(stt): honor Parakeet model pick when active slot stores a filename
705a1c5
fix(stt): route core transcription paths through the active engine
05576ea
fix(build): verify + harden Parakeet tarball staging (supply chain)
5e7a2ac
feat(stt): effectiveEngine() — single source of truth for engine labe…
034021d
fix(build): archive traversal check no longer fails open under pipefail
7076858
docs(scribe): system-wide inline overlay plan + AX feasibility probe
ed56b9b
feat(scribe): P1 overlay checkpoint — squiggles over native apps (ver…
e41d184
fix(scribe): overlay viewport culling + squiggle cap
708ac89
feat(scribe): overlay binary IPC mode (mechanism, engine-driven)
8533d51
build(scribe): compile + stage the overlay helper in CI
44e8d4c
perf(scribe): cache overlay bounds + gate selection measurement to ty…
035e986
feat(scribe): hover-to-fix — native menu at squiggles + AX apply
a31a271
docs(scribe): overlay build status (v1 implemented, on-device verific…
cff760a
feat(scribe): branded hover card (replaces the native NSMenu)
e9d040f
fix(scribe): correction card renders above the squiggle overlay
02f0f09
feat(scribe): respect macOS learned words (coexist, don't suppress)
c6214d2
fix(scribe): stop cursor disruption in Electron/browser + brand card …
c1811fc
fix(scribe): card clicks apply the fix + design-token card colors
ccd1944
feat(scribe): card category badge + settings gear
5dd4d20
feat(scribe): system-wide rewrite menu (Improve/Shorten/Tone/Translate)
258e446
feat(scribe): actionable AI rewrite on the card + on-brand styling
d07ae72
feat(scribe): theme-aware card + fix Rephrase not replacing text
481702d
fix(scribe): Rephrase rewrites the whole sentence (verified on-device)
8c8275c
fix(scribe): make Slack/Electron work + conservative live-underline +…
937d42c
build(scribe): compile the overlay binary automatically on npm run dev
732d041
docs(scribe): start gaps backlog (design-audit + functional + verific…
d764277
docs(scribe): mark G-1 (light-mode hex) resolved
92c1d98
feat(scribe): overlay squiggles use brand colors (red correctness / e…
c2bff00
docs(scribe): mark G-2 (color-coding) resolved - brand-aligned
c86aca2
feat(scribe): selection affordance — select a passage in any app → Re…
5014b68
fix(scribe): anchor the Rephrase pill to the selection, not the mouse
30cff1a
feat(a11y): honor prefers-reduced-motion app-wide (G-4)
b7e005f
docs(scribe): provit E2E findings writeup (G-9) + backlog G-3/G-4/G-9…
7ea9e85
docs(scribe): log G-11/12/13 (spelling false-positive fixes) in backlog
28dd46b
fix(scribe): don't offer Rephrase / squiggles on URL bars & non-prose
b24e31d
fix(scribe): style the markdown preview (Tailwind Preflight stripped …
6efe70f
feat(scribe): add TipTap for real rich-text mode + editor CSS
1f8ffd4
docs(scribe): log editor/rich-text items (G-14..G-17)
b3b0200
fix(scribe-overlay): never offer rephrase in the address bar
e3565ac
chore(scribe): bump pro (SOLID/DRY audit sweep) + log G-17..G-19
5a3aa2b
fix: remove posthog-js telemetry dep; fix onboarding Pro grid overflow
c977ec3
test: add jsdom component-integration test env for Scribe editors
01f43b3
docs: inherit shared design philosophy + add multi-agent operating model
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| # Scribe system-wide inline overlay — implementation plan | ||
|
|
||
| Grammarly-style **inline squiggles in any app**, no hotkey, no browser extension. This doc is | ||
| the design for review before code. It builds on the working writing engine (see | ||
| `WRITING_ASSISTANT_PLAN.md`) and the dictation AX/paste infra. | ||
|
|
||
| ## What we proved (AX probe, on-device, 2026-07-05) | ||
|
|
||
| `scripts/scribe-ax-probe/` measured, for the focused text field in the frontmost app, | ||
| whether macOS gives us a per-word bounding rectangle (the thing we need to draw a squiggle). | ||
|
|
||
| | App | Type | `AXBoundsForRange` (direct) | Selection trick | Verdict | | ||
| |---|---|---|---|---| | ||
| | Notes | native Cocoa | ✅ 12/12 | — | direct | | ||
| | iTerm2 | native | ✅ 12/12 | — | direct (neg. Y = 2nd monitor, normalizable) | | ||
| | Slack | Electron | ✗ 0/12 | ✅ 12/12 | selection trick + `AXManualAccessibility` | | ||
| | Brave | browser web field | ✗ 0/12 | ✅ 12/12 | selection trick | | ||
|
|
||
| **Conclusion: a single native overlay covers native + Electron + browser web fields.** No | ||
| browser extension. Canvas-rendered surfaces (Google Docs) are the expected gap → hotkey fallback. | ||
|
|
||
| Reference (Apache-2.0, build-our-own, do not vendor): `PhilipSchmid/textwarden` | ||
| (`Sources/Accessibility/TextMonitor.swift`, `Sources/Positioning/Strategies/ChromiumStrategy.swift`), | ||
| `Automattic/harper`. | ||
|
|
||
| ## The three AX techniques (proven) | ||
|
|
||
| 1. **Enable the AX tree.** Chromium/Electron keep accessibility off until they detect a screen | ||
| reader. | ||
| - Electron desktop (Slack/Teams/VSCode): `AXUIElementSetAttributeValue(app, "AXManualAccessibility", true)` — succeeds, no side effects. | ||
| - Browsers (Chrome/Brave): reject that (`-25205`); they use `AXEnhancedUserInterface` (VoiceOver's switch, can disturb window positioning → set, use, restore). Note: in testing the selection trick returned real bounds even when both enables were rejected, i.e. the tree was already warm — **cold-start handling is an open item (below).** | ||
| 2. **Measure bounds.** `AXBoundsForRange` returns garbage in Chromium. Instead: save cursor → | ||
| `kAXSelectedTextRangeAttribute = range` → wait ~45 ms → read `AXSelectedTextMarkerRange` → | ||
| `AXBoundsForTextMarkerRange` → poll ≤10× rejecting **stale** (unchanged vs last) and | ||
| **whole-line** (width > 80% of field) rects → restore cursor. Native apps skip all this and | ||
| use `AXBoundsForRange` directly. | ||
| 3. **Apply a fix.** Set `kAXSelectedTextRangeAttribute` to the issue span, then either set | ||
| `kAXSelectedTextAttribute` (where supported) or synthesize paste (reuse dictation's | ||
| `text-injection` paste-back). Restore cursor. | ||
|
|
||
| ## Architecture — mechanism (Swift) vs policy (our engine) | ||
|
|
||
| Clean split so no writing logic leaks into the native binary and the engine stays the single | ||
| source of truth: | ||
|
|
||
| ``` | ||
| ┌ Swift AX overlay service (mechanism only, generic) ──────────────┐ | ||
| │ - AXObserver: focus / value-changed / scroll / resize / move │ | ||
| │ - read focused text + element frame │ | ||
| │ - measure bounds for REQUESTED ranges (direct → selection trick) │ | ||
| │ - transparent, click-through NSWindow: draws squiggle underlines │ | ||
| │ - hit-test hover/click on a squiggle → emit event │ | ||
| │ - apply replacement over a span (AX set / paste) │ | ||
| │ ↕ JSON lines over stdio │ | ||
| └──────────────────────────────────────────────────────────────────┘ | ||
| ↑ text, focus, bounds, hover/click ↓ issues (spans), fix | ||
| ┌ Pro main (policy) ───────────────────────────────────────────────┐ | ||
| │ - runs the PURE rules engine (instant) + LLM on demand │ | ||
| │ - maps text → issues; asks service to measure only ISSUE spans │ | ||
| │ - owns WritingSettings (per-app enable, pause, checks) │ | ||
| └──────────────────────────────────────────────────────────────────┘ | ||
| ↓ show correction card at (x,y) | ||
| ┌ Pro renderer ────────────────────────────────────────────────────┐ | ||
| │ - correction card = reuse existing IssuePopover (React) │ | ||
| │ in a small non-activating panel window positioned by coords │ | ||
| └──────────────────────────────────────────────────────────────────┘ | ||
| ``` | ||
|
|
||
| **Why Swift draws the squiggles (not an Electron full-screen overlay):** a transparent | ||
| click-through Electron window floating over *other* apps is notoriously unreliable | ||
| (focus-stealing, multi-monitor, click-through-except-regions, repaint cost). TextWarden proves the | ||
| native NSWindow path works. Squiggles are cheap CoreGraphics lines. **The interactive correction | ||
| card** stays our React `IssuePopover` in a small on-demand panel (reuse + streaming rewrites), | ||
| shown only when a squiggle is engaged — one small window, not a full-screen overlay. | ||
|
|
||
| **Rejected alternatives:** (a) Electron full-screen click-through overlay — reliability risk above. | ||
| (b) Native Swift correction card — loses React reuse + streaming; only fall back to this if the | ||
| panel-window positioning proves unreliable over other apps. | ||
|
|
||
| ## IPC contract (Swift ⟷ Electron, JSON lines over stdio) | ||
|
|
||
| Service → main: | ||
| - `{type:"focus", app, bundleId, role, elementFrame:{x,y,w,h}, hasText:bool}` | ||
| - `{type:"text", text, charCount, selection:{loc,len}}` (debounced on value-changed) | ||
| - `{type:"bounds", ranges:[{loc,len,rect:{x,y,w,h}|null}]}` (reply to measure) | ||
| - `{type:"hover", span:{loc,len}, at:{x,y}}` / `{type:"click", span, at}` | ||
| - `{type:"scroll"}` / `{type:"blur"}` | ||
|
|
||
| Main → service: | ||
| - `{cmd:"measure", ranges:[{loc,len}]}` — measure only issue spans (not every word) | ||
| - `{cmd:"draw", underlines:[{rect,color}]}` — repaint overlay | ||
| - `{cmd:"apply", span:{loc,len}, replacement}` — perform the edit | ||
| - `{cmd:"enable", bundleId}` / `{cmd:"clear"}` | ||
|
|
||
| ## Coordinate mapping | ||
|
|
||
| AX rects: points (= DIP, so Retina is 1:1 with Electron), **top-left origin, global** across all | ||
| displays (hence iTerm's negative Y on a second monitor). For the Swift NSWindow overlay, convert to | ||
| Cocoa bottom-left: `cocoaY = totalScreenHeight - axY - axHeight`, choosing the `NSScreen` that | ||
| contains the rect. For the Electron card panel, `screen.getDisplayNearestPoint` + DIP coords map | ||
| directly. Unit-test the transform with fixtures (incl. negative-Y multi-monitor). | ||
|
|
||
| ## Per-app strategy registry | ||
|
|
||
| `AppStrategy` keyed by bundle id (mirrors TextWarden's ContentParsers), each declaring: | ||
| enable-attr (`manual` | `enhanced` | `none`), bounds-method (`direct` | `selection`), index space | ||
| (`utf16` | `grapheme`), selection offset (newline handling), and `visualUnderlines: bool` | ||
| (off for terminals / unreliable apps → card-only). Defaults: native → direct; Chromium bundle | ||
| prefixes → selection. One place; the service and settings both read it. | ||
|
|
||
| ## Cold-start AX enable (open item) | ||
|
|
||
| In testing the selection trick worked even when both enable attrs were rejected (tree already | ||
| warm). Unknown: a freshly-launched browser with cold AX. Plan: on first focus in a Chromium app, | ||
| attempt enable → poll `AXValue`/bounds for up to ~1.5 s before first measure; if still cold, show | ||
| the squiggles on the next value-changed tick. **Verify with the quit-and-relaunch probe run | ||
| before P2.** | ||
|
|
||
| ## Open-core placement | ||
|
|
||
| - **Swift service = generic mechanism, no writing logic** → `scripts/scribe-overlay/` (core), | ||
| built + staged to `resources/bin` by `release.yml`, exactly like `scripts/dictation-hotkey`. | ||
| Carries only AX + rect drawing, so it leaks no pro source. (Open decision: if we'd rather keep | ||
| even the binary out of core, move source to `pro/native/` + a pro CI build step.) | ||
| - **All policy is pro:** engine use, issue→underline mapping, correction card, settings, activation | ||
| live in `pro/main/writing/overlay/` + `pro/renderer`. Gated by `proEnabled()`; `OFFGRID_PRO=0` | ||
| disables. Degrades gracefully to the existing select-and-hotkey flow if the binary is absent. | ||
| - Entitlements/permissions: reuse dictation's Accessibility grant + `text-injection`. | ||
|
|
||
| ## Phases | ||
|
|
||
| - **P1 — Native apps, direct bounds.** Swift service (focus observer + `AXBoundsForRange`) + | ||
| NSWindow overlay drawing squiggles + coordinate transform. Prove pixel-accurate squiggles over | ||
| **Notes/Mail**, correct on scroll/resize/window-move. Correction card panel shows + applies a fix. | ||
| *Accept:* squiggles track the text in Notes through scroll/resize; apply-fix works. | ||
| - **P2 — Electron + browser, selection trick.** Add enable-attr + selection-based measurement | ||
| (cached, typing-pause gated, whole-line/stale rejection) + per-app registry + cold-start handling. | ||
| Prove **Slack + a browser web field**. *Accept:* squiggles in Slack/Brave, no cursor disruption | ||
| while typing, ≥ the probe's hit rate. | ||
| - **P3 — Interactions + settings.** Hover card (reuse IssuePopover) with apply / add-to-dictionary, | ||
| per-app enable + pause honored, learning-loop feedback wired. *Accept:* full loop in ≥3 apps | ||
| across all three buckets; per-app toggle + pause respected. | ||
| - Canvas apps (Google Docs): documented hotkey fallback, not in scope. | ||
|
|
||
| ## Performance | ||
|
|
||
| Measure only **issue** spans (few) not every word. Cache bounds by (text hash, element frame, | ||
| attributed-string hash); invalidate on value/scroll/resize. Selection measurement only during | ||
| typing pauses (~400 ms idle) to avoid cursor interference. AX messaging timeout ~1 s. Hard cap on | ||
| issues drawn (reuse engine's `maxIssues`). | ||
|
|
||
| ## Testing | ||
|
|
||
| - **Pure/unit (vitest, no Electron):** coordinate transform (incl. negative-Y multi-monitor), | ||
| per-app strategy selection, issue→underline mapping, whole-line/stale rejection predicate. | ||
| - **Swift unit:** range→bounds parsing, selection-trick state machine (mirror TextWarden's | ||
| `SlackStrategyValidationTests`), cursor save/restore. | ||
| - **Manual matrix (can't be E2E'd):** Notes, Mail, Slack, Brave, VSCode — squiggle accuracy, | ||
| scroll tracking, apply-fix, no-typing-disruption. Screenshots per app in the PR. | ||
|
|
||
| ## Risks / open questions | ||
|
|
||
| 1. Cold-start browser AX (verify with quit-relaunch probe). 2. Card panel positioning over other | ||
| apps (fallback: native card). 3. Per-app quirks are the real cost ("98% of effort is macOS | ||
| integration" — TextWarden). 4. `AXEnhancedUserInterface` window side effect on browsers (set → | ||
| use → restore; measure impact). 5. Selection-trick cursor flicker if a measure escapes the | ||
| typing-pause gate. | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Remove em dashes to comply with brand voice guideline.
Multiple em dashes ("—") appear throughout the doc: the title (Line 1), the AX-enable notes (Lines 30-31), the "why Swift draws squiggles" rationale (Line 75), the rejected-alternatives list (Lines 76-77), and the risks section (Line 165). Replace with periods, commas, or parentheses.
As per coding guidelines,
**/*.{ts,tsx,js,jsx,md}files must follow "no em dashes, no curly quotes, no exclamation marks" brand voice rules.✏️ Example fixes
Also applies to: 30-31, 75-77, 164-165
🤖 Prompt for AI Agents
Source: Coding guidelines