Before system-wide apps like Wispr Flow and Superwhisper ate dictation, coding agents rolling their own /voice input took pains to achieve typo-free input. They did so by exploiting local context. System-wide dictation apps have more local context, not less, and can build integrations in cases where the OS itself cannot expose context. So, they should support context-aware dictation, pulling in structured high-value data instead of what they support today — selected text, clipboard content, and a hand/auto-maintained dictionary. They shouldn’t just do this in coding, where coding agents’ /voice provides reference implementations, but in Slack, email, meeting notes, and Docs — and eventually in prompts to agents that write legal contracts and fill electronic health records (EHRs).

The reference implementation: coding agents

For example, open-source coding agent Aider has a clever detail in its voice.py. Instead of simply passing in the user’s recorded speech to a speech-to-text (STT) model for transcription, Aider does “vocabulary biasing” by pasting in the past chat turns as a prompt:

transcript = litellm.transcription(
    model="whisper-1", file=raw_wav_recording, prompt=history, language=language
)

This is a trick from ye olde Whisper docs: “By submitting the prior segment’s transcript via the prompt, the Whisper model can use that context to better understand the speech and maintain a consistent writing style. However, prompts do not need to be genuine transcripts […]. Fictitious prompts can be submitted to steer the model to use particular spellings or styles.” The chat history contains identifiers and jargon to bias recognition towards the right spellings.

But Aider’s intentional minimalism (the whole voice.py is under 200 LoC) is just the simplest way to vocabulary-bias transcription with local context. A certain de-minified coding agent’s source instead seeds requests to an enterprise STT API with “keyterms” — vocabulary hints about words that are especially easy to mistranscribe — from the most error-prone bits of local context. It draws keyterms from three broad buckets:

  • Coding terminology (symlink, grep, gRPC, dotfiles).
  • The project and branch names.
  • Recent files.

The file itself looks something like this:

# Provides domain-specific vocabulary hints so the STT correctly transcribes 
# coding terminology. Skip words never spoken aloud as spelled, e.g., "stdout" 
# -> "standard out". 
GLOBAL_KEYTERMS = (
    "MCP", "symlink", "grep", "regex", "localhost", "codebase", "TypeScript", 
    "JSON", "OAuth", "webhook", "gRPC", "dotfiles", "subagent", "worktree" 
)

def split_identifier(name: str) -> list[str]:
    # Split camelCase/PascalCase/kebab-case/snakeCase/path identifiers into 
    # their spoken words:
    #   "feat/stream-grok-responses" -> ["feat", "stream", "grok", "responses"]
    # Drop fragments <= 2 chars to avoid wasting keyterm slots.
    spaced = re.sub(r"([a-z])([A-Z])", r"\1 \2", name)
    return [
        w
        for w in (part.strip() for part in re.split(r"[-_./\s]+", spaced))
        if 2 < len(w) <= 20
    ]

def file_name_words(file_path: str) -> list[str]:
    # Strip directory and extension, then split the stem:
    #   "packages/tui/src/component/dialog-retry-action.tsx" -> ["dialog", "retry", "action"] 
    stem = re.sub(r"\.[^.]+$", "", basename(file_path))
    return split_identifier(stem)

def get_voice_keyterms(
    git_branch: str | None = None,
    project_name: str | None = None,
    recent_files: dict[str, object] | None = None
) -> list[str]:
    terms: dict[str, None] = dict.fromkeys(GLOBAL_KEYTERMS)

    # Kept whole: the project name is semantically a single keyterm, and 
    # matched as one.
    if project_name and 2 < len(project_name) <= 50:
        terms[project_name] = None

    if git_branch:
        for word in split_identifier(git_branch):
            terms[word] = None

    # dicts preserve insertion order, so the top files in recent_files get 
    # preserved in the sliced terms.
    if recent_files:
        for file_path in recent_files:
            if len(terms) >= MAX_KEYTERMS:
                break
            for word in file_name_words(file_path):
                terms[word] = None

    return list(terms)[:MAX_KEYTERMS]

Other apps

The most obvious target for context-aware dictation is Slack. Extract top members, channels, and custom emoji names from the workspace, and let this cache resolve spellings (is it @Ishan or @Ishaan?). Similarly, you can ensure dictation and live transcription never get an email recipient or meeting attendee’s name wrong — keyterm all of the To: field or the attendee list.

Intelligent keyterming in an editor like Docs/Coda/Notion is more sophisticated than the static reference implementation in coding. Aider’s prompt prefixing (send the ~200 tokens before the cursor to Whisper) is a fine first pass, and keyterming the top N uncommon words in the doc is a reasonable second pass.

Legal contract drafting and EHR filling are similar to the coding reference implementation, being forms of technical writing themselves. Clinical terminology is filled with Greek/Latin constructions that STT will reliably get wrong. Legal writing is similarly stuffed with Latin phrases. Each benefits from global keyterm lists that come online when the cursor is in the right app (Epic for EHRs, MS Word for legal drafting). Each also benefits from local context. The active medication and diagnoses list in EHRs is (1) full of difficult names, and (2) structured for extraction. A legal drafting project has (1) enough parties and authorities that a mistranscription-free sentence is a surprise rather than expected, and (2) collects them all in a structured way.

(I expect dictation app integrations into many of these apps will not really live in the OS layer because of PII-handling requirements. Fine — the integration lives as a partnered relationship in Epic, but nothing about the principles of using domain context changes.)

Now, a stock response to any careful-deliberate-design proposal like this one is to gesture at Rich Sutton’s bitter lesson. Here, you’d say something like: “soon, LLMs will be strong enough and fast enough that you can run one over a broken STT transcript and fix it near-instantly.” True, but two responses. First, no amount of intelligence gets those spellings right which depend on local context — a genius would still need to ask me how my first name is spelled. The glue work of threading in local context is unavoidable if you want a pleasant dictation experience. Second, the fact is that today, dictation often gets technical terms wrong. Until GPUs are cheap enough and strong LLMs are small enough to proofread every technical STT transcription, hand-tuning a short global keyterms list is a week’s ticket.