Verba Cross-Platform Strategy
Status: Proposal / Planning Date: 2026-07-02 Decision: macOS-first (shared core + system-wide macOS app), iOS/iPadOS as a later phase Scope: Make Verba available beyond VS Code — primary target is the Apple ecosystem (macOS, iPhone, iPad)
1. Motivation
Today Verba is a VS Code extension and can only be used inside VS Code. The goal is to make the dictation experience universally available, with the Apple ecosystem as the primary target: macOS (daily driver), plus iPhone and iPad.
A key constraint drives the whole strategy: VS Code does not run on iPhone/iPad. The current product therefore cannot run there at all — those devices require a genuinely native app. On macOS, the bigger opportunity is not "port the extension" but system-wide dictation — dictate into any application, not just VS Code.
2. Codebase analysis: what is coupled to VS Code today?
The encouraging finding: Verba is already architecturally well separated. The actual value — prompt engineering, cleanup, transcription orchestration — barely depends on VS Code. Two existing patterns make this possible:
- Services define their own
SecretStorageinterface instead of importingvscode.SecretStoragedirectly. vscodeis only lazy-loaded (require('vscode')) where unavoidable.
2.1 Portable core (already largely decoupled)
| Module | Coupling to VS Code | Portability |
|---|---|---|
pipeline.ts |
None (pure TS) | ✅ 100 % reusable |
cleanupService.ts |
One lazy require('vscode') for a warning; uses SecretStorage interface |
✅ Nearly complete |
transcriptionService.ts (Deepgram path) |
fs.readFileSync + SecretStorage interface |
✅ with an adapter |
| Prompt engineering (course correction, voice commands, glossary, expansions) | None | ✅ This is the actual IP |
costTracker.ts, historyManager.ts (logic) |
globalState interface |
✅ with an adapter |
2.2 Not portable (platform glue — rebuilt per target)
recorder.ts/continuousRecorder.ts— Nodechild_process+ the ffmpeg binary. Not available on iOS/iPadOS or in the browser. Audio capture must be native per platform (AVAudioEngine on Apple,getUserMediaon the web).insertText.ts— VS Code editor/terminal API. System-wide on macOS or on iOS this needs a different output path (Accessibility paste, keyboard extension, clipboard).extension.ts— ~61 KB, ~179vscodereferences: the pure orchestration/UI layer.- Semantic code search (
indexer,embeddingService,vectorStore) — only meaningful in an editor context; irrelevant for mobile.
2.3 Transcription & post-processing on non-Node platforms
Both providers expose plain REST APIs:
- Deepgram Nova-3 — pre-recorded REST + real-time WebSocket. Callable from
Swift via
URLSession/URLSessionWebSocketTask, no SDK required. - Anthropic Claude — REST API, callable from Swift via
URLSession.
On mobile there is also an on-device option: Apple's SFSpeechRecognizer
(Speech framework) or a whisper.cpp build compiled for iOS — attractive for
privacy and cost, mirroring the existing local-whisper provider on desktop.
3. Target architecture: @verba/core + platform shells
The pattern is one shared logic core, several thin platform shells that plug in through adapter interfaces.
┌─────────────────────── @verba/core (pure TypeScript) ───────────────────────┐
│ pipeline · cleanupService · transcription(REST) · prompts · glossary · │
│ expansions · costTracker · history │
│ Adapter interfaces: AudioCapture · SecretStore · TextSink · Config │
└─────────────────────────────────────────────────────────────────────────────┘
▲ ▲ ▲
VS Code Ext macOS menu-bar app iOS/iPadOS app
(existing, refactored) (system-wide, hotkey) (Swift + keyboard ext.)
3.1 Adapter interfaces (the seams)
| Interface | Responsibility | VS Code impl | macOS impl | iOS impl |
|---|---|---|---|---|
AudioCapture |
Produce audio (WAV/PCM) or a live stream | ffmpeg child process | native mic / ffmpeg sidecar | AVAudioEngine |
SecretStore |
Store/retrieve API keys | vscode.SecretStorage |
Keychain | Keychain (+ App Group) |
TextSink |
Deliver the final text | editor/terminal edit | Accessibility paste / CGEvent | keyboard extension / pasteboard |
Config |
Templates, glossary, language, etc. | workspace.getConfiguration |
app settings | app settings |
The core already effectively has SecretStore (as SecretStorage). The work is
to formalize the other three and remove the single lazy require('vscode') in
cleanupService.ts (replace with an injected notify callback).
4. Roadmap (sorted by ROI)
Phase 0 — Extract the core (small, low-risk, immediately useful) ✅ done
Carve @verba/core out of the extension. Because the services already used
interfaces rather than vscode, this was mostly decoupling + relocating:
- ✅ Formalized the adapter interfaces in
packages/core/src/adapters.ts(SecretStore,Notifier,KeyValueStore,AudioBytesReader, plus forward-lookingAudioCapture,TextSink,ConfigProvider). - ✅ Removed the lazy
require('vscode')fromcleanupService,costTracker, andhistoryManagerin favor of an injectedNotifier. - ✅ Put audio reads behind an injected
AudioBytesReader(defaults tofsin the host) so core never importsfs. - ✅ Relocated the portable modules into
packages/core/src/(pipeline,cleanupService,transcriptioncontracts,deepgramProvider) — a self-contained boundary: novscode, no Node built-ins, no../imports. - ✅ Split transcription into a portable Deepgram provider (core) and a
desktop-only whisper.cpp backend (host) behind a shared
TranscriptionBackend. - ✅ Promoted
@verba/coreto a real npm-workspaces package underpackages/core/, with its ownpackage.json/tsconfig.jsonthat builds independently (npm run compile:core), consumed by the extension via@verba/coreand bundled by esbuild intodist/extension.js. The boundary is checked by a regression test that scans the compiled output for strayvscode/fs/child_processrequires.
Outcome: cleaner extension, better testability, and the foundation for every platform that follows. No user-visible change.
Phase 1 — macOS system-wide (biggest ROI) ⭐ chosen starting point
A menu-bar app with a global hotkey that dictates into any app — not just VS Code.
- Framework: Tauri (decided). A TypeScript/web frontend consumes
@verba/core1:1, while a Rust backend handles the native concerns (global hotkey, microphone capture, Accessibility paste). Chosen over Electron for its much smaller bundle, lower memory footprint, and native Rust access to system APIs — and because we already have good experience with Tauri from the HP41C project. - Flow: global hotkey → native mic capture → pipeline → insert text into the
frontmost app via the Accessibility API /
CGEvent(paste), with clipboard fallback. - Reuse: ~all core logic, all prompt engineering, cost tracking, history.
- This is the real leap from "a VS Code tool" to "a dictation tool for the whole Mac."
Open design points: microphone permission & entitlements, Accessibility permission for system-wide paste, code signing / notarization, distribution (direct download vs. Mac App Store — note App Store sandbox limits on global input injection).
Phase 2 — iOS / iPadOS (native app)
TypeScript can no longer run natively in a convenient way. Two honest options:
- React Native → maximum reuse of
@verba/core, but keyboard extensions in RN are awkward and memory-constrained. - Native Swift/SwiftUI (one Xcode project for iPhone + iPad) → best UX, App
Store friendly, optional on-device
SFSpeechRecognizer. Reuse here is not the code but the prompt contracts and API contracts (port once, keep in sync).
Critical iOS constraint to design around early: a custom keyboard extension cannot access the microphone directly. The standard pattern is: the container app records (shared via an App Group), and the keyboard only inserts the finished text. This shapes the entire UX.
5. Decision & recommendation
- Chosen path: macOS-first. Do Phase 0 + Phase 1 first — the shared core plus a system-wide macOS app deliver by far the most value per unit of effort and cover the primary daily-use device.
- iOS/iPadOS (Phase 2) follows once the core is stable and the adapter/prompt contracts have settled, reducing rework.
Suggested immediate next step
Begin Phase 0: extract @verba/core from the extension and formalize the
adapter seams. It is low-risk, ships no user-visible change, and unblocks the
macOS app.
6. Open questions / risks
- macOS distribution: direct + notarization vs. Mac App Store (sandbox restricts global input injection).
- iOS transcription: cloud Deepgram vs. on-device Speech framework/whisper — trade-off between accuracy/cost and privacy/offline.
- Keeping prompt logic in sync between the TS core and a potential native Swift port (single source of truth for prompt strings).
- API-key handling across devices (per-device Keychain vs. optional iCloud Keychain sync) while preserving the Bring-Your-Own-Key model.