Skip to content
Open
Show file tree
Hide file tree
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
Jul 4, 2026
154b777
feat(scribe): add Scribe to the pro catalog + nav
Jul 4, 2026
bcb69a0
test(scribe): pro E2E — squiggle on typo, valid words untouched
Jul 4, 2026
4608108
docs(scribe): research findings + desktop implementation plan
Jul 4, 2026
39681b1
test(scribe): exercise the document review panel
Jul 4, 2026
ff09cd1
test(scribe): refresh squiggle screenshot for the two-column layout
Jul 4, 2026
d804c24
docs(scribe): roadmap — import→transform→edit, section feedback, lear…
Jul 4, 2026
f74d40e
test(scribe): settings expose style guides + learning loop
Jul 4, 2026
7526470
test(scribe): assert the import menu + refresh aligned-squiggle scree…
Jul 4, 2026
6481d8e
test(scribe): refresh review + settings screenshots
Jul 4, 2026
57cc8c5
feat(scribe): seed the chat composer from Scribe (continue-in-chat)
Jul 4, 2026
9480f78
test(scribe): refresh screenshots (readability report + hotkey settings)
Jul 4, 2026
e9719b4
feat(crm): preload bindings for create-entity + link-observation
Jul 4, 2026
71c76c4
test: fix stale clipboard placeholder assertion
Jul 4, 2026
0f18656
test(scribe): refresh screenshots
Jul 4, 2026
d0a315b
feat(stt): Parakeet transcription engine (additive, beside whisper)
Jul 4, 2026
1b89e7c
feat(stt): engine selector with whisper fallback
Jul 4, 2026
0e930a3
build(stt): CI staging for the Parakeet runtime
Jul 4, 2026
9b638d2
feat(models): Parakeet STT models in the catalog (downloadable)
Jul 4, 2026
d16b95f
feat(stt): use downloaded Parakeet models; keep whisper isolated
Jul 4, 2026
c011389
test(stt): e2e for the Parakeet model picker in Voice settings
Jul 4, 2026
3f15803
fix(stt): honor active whisper model by catalog id; de-em-dash catalo…
Jul 5, 2026
c42de2e
test(stt): point the model-picker e2e at the generalized component te…
Jul 5, 2026
ca7aa35
fix(stt): match active Parakeet model by catalog id; refresh screenshots
Jul 5, 2026
52e33dc
build(stt): stage Parakeet engine dylibs with the 0.0.28 gates
Jul 5, 2026
4b32e86
feat(onboarding): showcase Scribe in the Pro step
Jul 5, 2026
5417f05
fix(stt): correct sherpa-onnx flags + slim/robust staging (verified o…
Jul 5, 2026
fb1a0fd
test(stt): verify parser on real Parakeet output; gitignore the runtime
Jul 5, 2026
9c7c400
refactor(open-core): drop crm preload bindings for pro entity create/…
Jul 5, 2026
c71a807
fix(stt): honor Parakeet model pick when active slot stores a filename
Jul 5, 2026
705a1c5
fix(stt): route core transcription paths through the active engine
Jul 5, 2026
05576ea
fix(build): verify + harden Parakeet tarball staging (supply chain)
Jul 5, 2026
5e7a2ac
feat(stt): effectiveEngine() — single source of truth for engine labe…
Jul 5, 2026
034021d
fix(build): archive traversal check no longer fails open under pipefail
Jul 5, 2026
7076858
docs(scribe): system-wide inline overlay plan + AX feasibility probe
Jul 5, 2026
ed56b9b
feat(scribe): P1 overlay checkpoint — squiggles over native apps (ver…
Jul 5, 2026
e41d184
fix(scribe): overlay viewport culling + squiggle cap
Jul 5, 2026
708ac89
feat(scribe): overlay binary IPC mode (mechanism, engine-driven)
Jul 5, 2026
8533d51
build(scribe): compile + stage the overlay helper in CI
Jul 5, 2026
44e8d4c
perf(scribe): cache overlay bounds + gate selection measurement to ty…
Jul 5, 2026
035e986
feat(scribe): hover-to-fix — native menu at squiggles + AX apply
Jul 5, 2026
a31a271
docs(scribe): overlay build status (v1 implemented, on-device verific…
Jul 5, 2026
cff760a
feat(scribe): branded hover card (replaces the native NSMenu)
Jul 5, 2026
e9d040f
fix(scribe): correction card renders above the squiggle overlay
Jul 5, 2026
02f0f09
feat(scribe): respect macOS learned words (coexist, don't suppress)
Jul 5, 2026
c6214d2
fix(scribe): stop cursor disruption in Electron/browser + brand card …
Jul 5, 2026
c1811fc
fix(scribe): card clicks apply the fix + design-token card colors
Jul 5, 2026
ccd1944
feat(scribe): card category badge + settings gear
Jul 5, 2026
5dd4d20
feat(scribe): system-wide rewrite menu (Improve/Shorten/Tone/Translate)
Jul 5, 2026
258e446
feat(scribe): actionable AI rewrite on the card + on-brand styling
Jul 5, 2026
d07ae72
feat(scribe): theme-aware card + fix Rephrase not replacing text
Jul 5, 2026
481702d
fix(scribe): Rephrase rewrites the whole sentence (verified on-device)
Jul 5, 2026
8c8275c
fix(scribe): make Slack/Electron work + conservative live-underline +…
Jul 6, 2026
937d42c
build(scribe): compile the overlay binary automatically on npm run dev
Jul 6, 2026
732d041
docs(scribe): start gaps backlog (design-audit + functional + verific…
Jul 6, 2026
d764277
docs(scribe): mark G-1 (light-mode hex) resolved
Jul 6, 2026
92c1d98
feat(scribe): overlay squiggles use brand colors (red correctness / e…
Jul 6, 2026
c2bff00
docs(scribe): mark G-2 (color-coding) resolved - brand-aligned
Jul 6, 2026
c86aca2
feat(scribe): selection affordance — select a passage in any app → Re…
Jul 6, 2026
5014b68
fix(scribe): anchor the Rephrase pill to the selection, not the mouse
Jul 6, 2026
30cff1a
feat(a11y): honor prefers-reduced-motion app-wide (G-4)
Jul 6, 2026
b7e005f
docs(scribe): provit E2E findings writeup (G-9) + backlog G-3/G-4/G-9…
Jul 6, 2026
7ea9e85
docs(scribe): log G-11/12/13 (spelling false-positive fixes) in backlog
Jul 6, 2026
28dd46b
fix(scribe): don't offer Rephrase / squiggles on URL bars & non-prose
Jul 7, 2026
b24e31d
fix(scribe): style the markdown preview (Tailwind Preflight stripped …
Jul 7, 2026
6efe70f
feat(scribe): add TipTap for real rich-text mode + editor CSS
Jul 7, 2026
1f8ffd4
docs(scribe): log editor/rich-text items (G-14..G-17)
Jul 7, 2026
b3b0200
fix(scribe-overlay): never offer rephrase in the address bar
Jul 7, 2026
e3565ac
chore(scribe): bump pro (SOLID/DRY audit sweep) + log G-17..G-19
Jul 7, 2026
5a3aa2b
fix: remove posthog-js telemetry dep; fix onboarding Pro grid overflow
Jul 7, 2026
c977ec3
test: add jsdom component-integration test env for Scribe editors
Jul 7, 2026
01f43b3
docs: inherit shared design philosophy + add multi-agent operating model
Jul 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,25 @@ jobs:
mkdir -p resources/bin
cp scripts/dictation-hotkey/dictation-hotkey resources/bin/dictation-hotkey
chmod +x resources/bin/dictation-hotkey
# Compile the Scribe system-wide overlay helper (AX read/measure + squiggle window)
# and stage it into resources/bin. Self-contained, deployment target pinned. If it ever
# fails to ship, Scribe simply falls back to the select-and-hotkey flow (the overlay
# service no-ops when the binary is absent), so it can't break a release.
- name: Build Scribe overlay helper
run: |
bash scripts/build-scribe-overlay.sh
mkdir -p resources/bin
cp scripts/scribe-overlay/scribe-overlay resources/bin/scribe-overlay
chmod +x resources/bin/scribe-overlay
# Stage the Parakeet STT runtime (sherpa-onnx CLI + ONNX model) into
# resources/bin/parakeet. Additive: with SHERPA_ONNX_URL / PARAKEET_MODEL_URL
# unset this is a no-op and transcription stays on whisper, so it can't break a
# release. Set those vars once the exact sherpa-onnx build + Parakeet export are pinned.
- name: Stage Parakeet STT runtime (optional)
env:
SHERPA_ONNX_URL: ${{ vars.SHERPA_ONNX_URL }}
PARAKEET_MODEL_URL: ${{ vars.PARAKEET_MODEL_URL }}
run: bash scripts/fetch-parakeet.sh
# Private pro source into pro/ → __OFFGRID_PRO__ true. The checkout action
# injects the token as an http.extraheader (never in a clone URL) and scrubs
# it afterwards — safer than embedding ${TOKEN} in a git URL.
Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ electron/accessibility/ocr
# them. Only the heavy optional runtimes we don't ship are excluded.
resources/bin/coreml-sd/
resources/bin/mflux/
# Parakeet STT runtime — CI-staged via scripts/fetch-parakeet.sh (like mflux/coreml-sd),
# not committed. Devs stage it locally with the same script.
resources/bin/parakeet/
# Gateway probe scratch output
.gateway-probe/
# TS build cache
Expand All @@ -50,3 +53,7 @@ scripts/dictation-hotkey/dictation-hotkey

# Local demo profile — synthetic-data run target for `npm run demo` (never real userData)
.demo-profile/

# local PR-campaign target list (not for commit)
pr-targets.local.md
scripts/scribe-overlay/scribe-overlay
167 changes: 167 additions & 0 deletions docs/SCRIBE_SYSTEMWIDE_OVERLAY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Scribe system-wide inline overlay — implementation plan

Copy link
Copy Markdown

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
-# Scribe system-wide inline overlay — implementation plan
+# Scribe system-wide inline overlay: implementation plan
-   - 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).**
+   - 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).**
-shown only when a squiggle is engaged — one small window, not a full-screen overlay.
+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
+**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
-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 →
+apps (fallback: native card). 3. Per-app quirks are the real cost ("98% of effort is macOS
+integration," per TextWarden). 4. `AXEnhancedUserInterface` window side effect on browsers (set →

Also applies to: 30-31, 75-77, 164-165

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/SCRIBE_SYSTEMWIDE_OVERLAY.md` at line 1, The document still uses em
dashes in the title and several sections, which violates the brand voice rule
for markdown content. Update the text in this doc to remove all em dashes and
replace them with periods, commas, or parentheses, especially in the title, the
AX-enable notes, the Swift squiggles rationale, the rejected-alternatives list,
and the risks section. Use the existing section text and headings in Scribe
system-wide overlay as the anchor points while keeping the meaning unchanged.

Source: Coding guidelines


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.
Loading
Loading