Skip to content
| Marketplace
Sign in
Visual Studio Code>AI>YapNew to Visual Studio Code? Get it now.
Yap

Yap

YapAI

|
4 installs
| (0) | Free
AI wrote it. Yap reads it out loud. Read any selection aloud, or hear a plain-English summary of it.
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info
Yap icon

Yap

AI wrote it. Yap reads it out loud.

Install from the VS Code Marketplace · code --install-extension YapAI.yap

A VS Code extension that reads the selected code aloud, or summarizes it with an LLM and reads the explanation aloud — an audiobook for source files.

A note on naming. The extension is YapAI.yap on the Marketplace, but every command id and setting key keeps the codeNarrator. prefix from its original name — so codeNarrator.voice, codeNarrator.readSelection, and friends. Renaming those would silently break anyone's keybindings.json and settings.json, which is not worth a cosmetic tidy.

Yap in the editor context menu

Select code, right-click, and pick what you want to hear — or use the keyboard shortcuts and never leave the editor.

Commands

Command Keybinding Behaviour
Yap: Read Selection Aloud cmd+alt+r Speaks the selection through the OS speech engine
Yap: Summarize Selection and Read Aloud cmd+alt+e Sends the selection to an LLM, speaks the plain-English explanation
Yap: Pause or Resume cmd+alt+p Pauses at the current sentence, or resumes from it
Yap: Next Sentence cmd+alt+] Skips forward one chunk
Yap: Previous Sentence cmd+alt+[ Replays the previous chunk
Yap: Stop Speaking cmd+alt+. Abandons playback and clears the queue
Yap: Choose Voice — Picker that auditions each installed voice as you arrow through it
Yap: Speak Faster / Slower — Nudges the rate by 20 wpm, live
Yap: Save Narration to File — Renders the selection to an audio file instead of playing it
Yap: Set API Key / Clear API Key — Manages the stored key in SecretStorage

Read and summarize also appear in the editor context menu when text is selected. With an empty selection they act on the whole document. The status bar shows what is playing and the position within it (Reading 2/5), and clicking it stops.

Playback

The speaker is a chunked player, not a fire-and-forget call: the text is split into sentence-ish chunks and each one is a separate engine process. That is what makes the transport controls possible.

  • Pause kills the current chunk and remembers the cursor. Resume restarts that chunk from its beginning — chunk boundaries are the only seek granularity the OS engines give us, so a paused sentence replays rather than resuming mid-word.
  • Next / Previous move the cursor and kill the current chunk, so they take effect immediately rather than at the end of the sentence.
  • Speak Faster / Slower write to codeNarrator.wordsPerMinute. Options are re-read per chunk rather than captured once per command, so a rate change lands at the next sentence instead of the next time you press read.
  • Stop clears the queue as well as the current chunk.

Chunk size is tunable through SpeakOptions.maxChunkChars (default 350). Smaller chunks make Stop and Pause respond sooner at the cost of one process spawn each — the tradeoff matters most on Windows, where PowerShell startup is 300–600ms.

Choose Voice auditions as you move: each sample supersedes the last, using the same supersede-not-overlap path as normal playback. An unusable voice is silently not auditioned rather than erroring mid-list.

Save Narration to File writes AIFF or M4A on macOS (say -o, with the --file-format/--data-format pair added for M4A) and WAV on Windows and Linux. Verified: a short line renders to a 58 KB AIFF and a 39 KB M4A.

Platform support

Platform Engine Rate scale Status
macOS say -f <file> words/minute tested on real hardware
Windows powershell.exe + System.Speech $s.Rate, -10…10 implemented, unit-tested only
Linux espeak -f <file> (or espeak-ng) words/minute implemented, unit-tested only

Only the macOS path has been run against an actual speech device. The Windows and Linux invocations are covered by unit tests that assert the exact argv and PowerShell script produced — buildInvocation takes the platform as a parameter precisely so all three branches are testable from one host — but nobody has heard them. Treat them as unverified.

On Linux the engine is probed at first use and espeak-ng is accepted where espeak is absent. If neither is installed you get an actionable error with a Copy install command button rather than an ENOENT.

Windows rate mapping is not a pass-through: SAPI uses a -10…10 scale where 0 is roughly 200 wpm, so toWindowsRate converts.

Because audio needs a real output device, the extension declares "extensionKind": ["ui"] so it runs in the local extension host rather than the remote one under Remote-SSH, WSL, Dev Containers, or Codespaces.

Voice-name validation is macOS-only, deliberately: say exits 0 and silently substitutes the default voice for an unknown name, so it is the one platform where the name must be checked up front. Windows SelectVoice throws and espeak exits non-zero, so both already fail loudly through the normal error path.

Read modes

codeNarrator.readMode is humanized by default. Verbatim code is unlistenable — for (let i = 0; i < arr.length; i++) becomes "for left paren let i equals zero semicolon…" — so humanized mode expands operators, splits identifiers, and drops syntax noise:

Input Spoken
for (let i = 0; i < arr.length; i++) { total += arr[i]; } for let i equals 0 i less than array dot length i increment total plus equals array i
HTTPResponse parseHTTPResponse(String raw_body) HTTP Response parse HTTP Response String raw body
std::vector<int> v; auto x = obj->field / 2.0f; std scope vector less than int greater than v auto x equals object arrow field slash 2 point 0f

commentsOnly

For the audiobook feel this is often the mode people actually want: the prose the author already wrote, with none of the syntax. It is not one big regex — a naive // match happily reads the middle of https://example.com. Instead a per-language token table drives a character scanner that tracks string-literal state, so a comment marker inside a string is ignored and a quote inside a comment does not open a string.

Covers C-like, Python (# and docstrings), hash languages (shell, YAML, TOML, Ruby, Dockerfile), markup (<!-- -->), CSS, SQL, and Lua; unknown languages fall back to C-like, which is right far more often than it is wrong. A triple-quoted Python string counts as a docstring only when it opens its own line, so label = """a value""" is skipped.

Runs of adjacent line comments are merged into one paragraph before punctuation is added, so a wrapped comment is not spoken as two half-sentences. Banner rules (// ------) are dropped. If the selection has no comments you are told so rather than played silence.

Set codeNarrator.readMode to verbatim for exact text. The abbreviation table is a judgement call, not a correctness property — override or opt out of any entry via codeNarrator.abbreviations, e.g. { "id": "id" } to stop id being read as "I D".

Known rough edges: generics read badly (vector<int> → "vector less than int greater than"), and string literals are read as code rather than kept verbatim.

Summarization

Three providers, tried in order per codeNarrator.summary.provider:

  1. Copilot (auto, copilot) — the built-in VS Code language model API. No key needed; billed against the user's Copilot quota. The first request shows a consent dialog naming this extension.
  2. Claude Code CLI (auto, claudeCli) — shells out to a locally installed claude, using the credentials it already holds. No API key to manage. The selection goes in over stdin, never argv.
  3. Your API key (auto, apiKey) — an Anthropic-shaped Messages endpoint, configured by codeNarrator.apiKey.endpoint and .model (default claude-opus-5). Set it with the Yap: Set API Key command; the key lives in VS Code SecretStorage, never in settings.json, and is cleared automatically on a 401/403 so a bad key re-prompts instead of failing forever. Yap: Clear API Key removes it.

Quota exhaustion, un-granted consent, a missing model, and a missing or logged-out CLI are all treated as "try the next provider", not as crashes. The status bar names which provider actually answered.

Choosing between them

Provider Key needed Typical latency
Copilot no (needs a subscription) ~2–3s
Claude Code CLI no 6–20s
API key yes ~2–4s

The CLI is the slowest by a wide margin, and the spread is real: repeated identical calls on the same machine ranged from 6s to 20s. Almost all of it is CLI startup rather than inference, which is why --strict-mcp-config (skipping your configured MCP servers) is passed unconditionally, and why setting codeNarrator.claudeCli.model to a smaller model does not reliably speed it up — it just produces a shallower summary. Leave that setting empty.

If claude works in your terminal but the extension reports it missing, set codeNarrator.claudeCli.path. A GUI-launched VS Code frequently inherits a shorter PATH than an interactive shell.

The CLI is invoked with cwd set to the temp directory, so no project CLAUDE.md, settings file, or workspace-trust prompt is pulled into what should be one stateless call. All tools are denied and --max-turns 1 is set: nothing should turn a summary request into an agent loop over your repository.

Privacy

Summarizing sends source code off the machine, so:

  • A modal consent prompt appears before the first outbound call, naming the destination. "Allow always" is remembered in globalState.
  • codeNarrator.summary.denyList blocks files that must never be sent. Defaults cover .env, .env.*, *.pem, *.key, *.p12, secrets/**, and credentials*.
  • Reading aloud is entirely local and involves no network call.
  • A workspace cannot redirect where your code goes. codeNarrator.claudeCli.path and codeNarrator.apiKey.endpoint are declared "scope": "machine", so they can only be set in user settings. Without that, a repository could ship a .vscode/settings.json pointing the endpoint at its own server — exfiltrating both the selection and your API key — or pointing the CLI path at an arbitrary binary that Yap would then execute. Neither needs you to do anything but summarize once, and "Allow always" would suppress the consent prompt entirely.
  • A workspace can only tighten the deny list, never loosen it. The effective list is your user-level patterns plus whatever the workspace adds, so a repository cannot whitelist its own .env.
  • Summarizing is disabled in a restricted workspace (untrustedWorkspaces: limited). Reading aloud still works, because it is local.

The Claude Code CLI provider is not an offline option — claude runs locally but still sends the code to Anthropic's API. The consent dialog says so.

Development

npm install
npm test          # 102 unit tests over the pure modules, no VS Code harness needed
npm run verify    # type-check + test + bundle

Press F5 to launch the Extension Development Host with the extension loaded, then cmd+R in that window to reload after an edit.

src/
  extension.ts   # activate(), command registration, progress + consent UX
  speaker.ts     # Speaker: spawn/kill the OS TTS process, chunked playback
  humanize.ts    # code -> speakable prose
  summarize.ts   # Summarizer interface + Copilot, Claude CLI, API-key providers
  selection.ts   # the text to act on, with fallbacks
  deny.ts        # glob matching for the summarizer deny list
  comments.ts    # per-language comment/docstring scanner for commentsOnly mode
  claudecli.ts   # argv construction and output cleanup for the CLI provider
  apikey.ts      # key validation and paste normalization

speaker.ts, humanize.ts, deny.ts, comments.ts, claudecli.ts, and apikey.ts import no vscode, which is what makes them testable with plain node --test (via Node's native TypeScript type stripping — no compile step, no test dependencies).

Two invariants worth preserving when editing:

  • Text reaches the synthesizer through a temp file, never a command line. A selection can contain quotes, backticks, $, and newlines; interpolating that into a shell string is arbitrary code execution triggered by selecting text. spawn(..., { shell: false }) plus a temp file avoids the whole class.
  • Untrusted text never reaches a command line. The speech layer uses a temp file; the CLI summarizer uses stdin. A selection is arbitrary text, and arbitrary text in argv or a shell string is an injection hazard triggered by the victim selecting code.
  • stop() bumps a generation counter, so it cancels the queued chunks and not just the chunk currently playing — and a kill()-induced non-zero exit is not reported as an error. Pause and seek use the same mechanism, which is why they can interrupt a chunk without the player treating it as a failure.
  • utter() and cancelCurrent() are the process-lifecycle seam. Overriding those two is how the queue and cursor logic is tested without a speech device (src/test/player.test.ts); keep them protected and keep the state machine out of them.

Publishing

package.json declares capabilities.untrustedWorkspaces as limited and virtualWorkspaces as unsupported — the latter because Yap spawns a local speech engine and so needs a local extension host.

The three transport commands carry enablement clauses driven by two context keys, codeNarrator.playing and codeNarrator.paused, set from the player's state callback. Without them Pause and the seek commands appear in the palette while nothing is playing and silently do nothing. Those keys are also usable in your own keybindings.json when clauses.

Packaging

npm run package                          # -> yap-0.0.1.vsix
code --install-extension yap-0.0.1.vsix  # smoke-test the real artifact

Roadmap

  • Verify the Windows and Linux backends on real hardware
  • Semantic-token-based comment detection, falling back to the token table
  • Word-boundary highlighting for karaoke-style follow-along (say -o emits timing data)
  • Chapter-by-chapter narration of a whole file via the document symbol tree
  • Summary caching keyed by a hash of the selection
  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
© 2026 Microsoft