Gadfly
Gadfly is for reviewing the Markdown plans Claude Code writes. You comment on exact passages in
VS Code, and the review goes back into the session that wrote the plan. Session transcripts can be
reviewed the same way, and a local TTS voice can read either one aloud. The
repository README explains the idea and shows what a
review looks like when it reaches Claude.
Install
Install Gadfly Reviewer from the VS Code Marketplace (code --install-extension gadfly.gadfly-reviewer).
Each GitHub release also carries gadfly.vsix, or build
it from a clone with npm install and npm run package (which writes extension/gadfly.vsix).
- Install from the Marketplace, or
code --install-extension gadfly.vsix
- Run Gadfly: Install Claude Code Hooks. This adds the PreToolUse, PostToolUse, Stop and
UserPromptSubmit hooks to
~/.claude/settings.json (a backup is kept) and installs the review
skill into ~/.claude/skills/gadfly-review/. After that, Gadfly refreshes the hooks and the skill
by itself when you update the extension.
- Quit VS Code with ⌘Q and reopen it. A window reload doesn't reliably pick up a new extension manifest.
Open a .md file with Open in Gadfly (editor title button, Explorer context menu, or Reopen
With…). Gadfly: Make Gadfly the Default for Markdown makes it the default; Gadfly: Stop Using
Gadfly as the Default for Markdown undoes that.
Requirements
- VS Code 1.100 or later, and Claude Code (Gadfly is built against its current release).
- The player only: an Apple Silicon Mac (Kokoro-82M runs on the GPU through MLX) and
uv,
auto-detected at /opt/homebrew/bin/uv, ~/.local/bin/uv or on PATH, or set gadfly.uvPath.
The model (gadfly.model, default mlx-community/Kokoro-82M-8bit) comes from the Hugging Face
cache or is reachable through HF_ENDPOINT/HF_TOKEN. On first play the daemon installs its
Python dependencies, which takes a minute once.
The TTS daemon starts on the first play, so everything except listening works without uv or the model.
Layout
Everything lives in one tab: Contents · Source · reader · Notes · Review. Toggle each
from the top bar, or click Zen mode to hide them all and read the document alone (click again to
bring them back). Drag the dividers to resize, drag a pane past its minimum width to hide it, and
double-click a divider to reset it. Clicking a heading in Contents scrolls the reader, and the source
if it's open.
The scroll track on the right marks the bookmark (red ribbon) and comment threads (violet; click one
to open the thread). Clicking highlighted text in the reader also opens its thread.
The source pane is Gadfly's own editor on the same document. VS Code handles undo (⌘Z), dirty
state and save (⌘S); the pane scrolls in step with the reader and is read-only only while Claude
applies a completed review. Gadfly: Open Source in Text Editor opens VS Code's own editor if you
prefer it. gadfly.readerFont switches the reader between Literata and your UI font.
Review workflow
- Select text and click Comment (or click + in the review panel: it comments on the
selection, or adds a document-wide general note when nothing is selected). The first
comment starts a review: Claude's Write/Edit is blocked by the hook until the review is
complete. You can keep editing the file yourself (see Drafts below).
- The review panel is one list, grouped by top-level section (General first; click a group to
fold it). Each thread shows the number Claude cites (
4, G1), the sub-section when it is
deeper than the group (hover for the full path), and pending (not sent yet, or new
messages since the last Send) or sent. Status and Section filters combine; open
threads show by default. Collapse all, Resolve all and Delete pending act
on the threads shown and ask first.
- Send to Claude posts a compact digest (
mode=DISCUSS) to the linked session's inbox.
The link comes from the PostToolUse hook when Claude writes the file. With no live link,
the digest goes to the clipboard.
- Claude answers in chat. Once every thread sent so far is settled, Claude asks you once (Post
and resolve, Post only, or Don't post), then posts summary replies in one batch
(
gadfly review reply <file> --batch [--resolve]), coloured by the session that wrote them. A
reply is unread (blue dot) until you open or click its thread; N unread at the top
jumps to the next one. Resolve is one click, and you can resolve, reopen and reply
yourself. A thread's resolution is Claude's latest reply (plus anything you added after it);
a thread with no reply from Claude means no change. Claude never clicks Review complete.
- References in comments (
[§5 Infra "we already run Postgres 16"], or type # to pick a
section) are links: click one to scroll the document (and the source) to the quote. A red
link means the quote isn't in this version; it still jumps to the section if that exists.
- References follow your edits like comment quotes do: each one remembers the text it was made
on and where its words were in it, and when you edit that text the quote shown in the comment
follows (a comment already sent becomes pending again, and the next Send shows
reference edited: [§… "old"] → [§… "new"]).
- When all of the referenced text is gone, the reference is outdated: red, struck through,
"Referenced text removed", and nothing asks you anything. It comes back by itself if the text
returns (undo, switching branches back). Claude's digest shows it as
[§… "quote" (referenced text removed)].
- In the document, text that a comment references has a thin dotted underline in the link
colour; click it to open the thread (a comment on the same text wins).
- Connected to @name under the title shows the linked session: the one that last wrote or
replied on the file. Its dot turns amber when that session isn't running (Send then copies
to the clipboard). Click it to pick another running session; from inside a session,
gadfly review link <file> does the same.
- Review complete (enabled once every thread is resolved) asks who makes the next version:
- Claude applies the resolutions saves the file, sends the
mode=APPLY digest (with a note
to keep your manual edits, if you made any) and lets Claude write. Meanwhile the file is
read-only for you (the top bar says "Claude is applying the review"); Take back ends
that. Claude's update becomes the next version and ends the hand-over; Verify shows the
diff next to your resolutions.
- I'll write the next version sends nothing. Claude stays blocked while you edit; Save as
new version ends it.
- Drafts. You can always edit (except while Claude applies a review). Edits that make the
file differ from the latest version are a draft ("Draft on v9" in the top bar, "v9
(current) · draft" in the version picker). Saving to disk (
⌘S, autosave) never makes a
version; undoing back to the latest version ends the draft. While a draft exists Claude's
Write/Edit is blocked ("the user has unsaved draft edits"), so its work can't clash with
yours. Save as new version (top bar, or Gadfly: Save as New Version) saves the file,
records it as the next version and ends the draft. It is disabled during a review until
every thread is resolved and you click Review complete. You can comment while you have a
draft: the review is on the version the draft is based on. Restoring an older version while
you have a draft asks before discarding it.
- Comments follow the text, the way GitHub and Gerrit carry review comments to a newer
patch: without asking, and without saving anything per edit. A comment remembers the text it
was made on (kept once, in the review folder) and where its words were in it. Where it sits now
is that position carried through a diff of the two texts, so Gadfly's source pane, VS Code's
text editor, a
git pull or branch switch, and edits made while Gadfly was closed all give
the same answer, and undo, or switching away and back, restores positions exactly. Text typed
inside a quote extends it; deleted text shrinks it. At an edge, only the word the quote ends
(or starts) in is followed: "tenant" becomes "tenants" when you add an "s" and "rewrite" when
you add "re", but " (draft)", a full stop or a new word stay outside. Words keep - and '
when they sit between letters ("per-tenant", "tenant's"), and a word you edit inside the quote
("tenant" to "tenancy") is followed. If the words moved elsewhere, the exact quote is found
again when it occurs once.
- When the whole quote is gone the thread stays, marked outdated ("outdated · won't be
sent" while it has never been sent): its quote is dimmed and struck through, and it has no
highlight or scroll-bar mark. It returns by itself when the text does. An unsent outdated
thread can be deleted; a sent one is resolved as usual.
- Outdated comments are not sent. A thread that was never sent and is outdated is left out
of a Send and is not counted in "Send to Claude (N)"; when its text returns it is an
ordinary pending thread. A thread Claude already has that becomes outdated turns pending
once, and the next Send carries one line,
#4 dropped: quoted text removed by the user. If
the text comes back after that, the thread is pending again as an ordinary follow-up. A sent
thread whose quote text changed shows quote edited: "old" → "new". Resolved threads add
nothing to a Send. In Review complete, a resolved thread whose text is gone is skipped.
- Renamed or moved files (
git mv): opening a Markdown file Gadfly doesn't track, whose
text equals the latest version (or draft) of a tracked file that no longer exists, offers
"Continue the review of old/path.md here?". Continue moves the whole review.
- A draft that comes from the file changing under the editor (not from typing) is labelled
"v9 · draft (includes outside changes)" in the version picker.
- Compare (the compare button by the version picker, or Gadfly: Compare Versions) shows
the line diff between any two versions, or between a version and your unsaved draft. Pick
From and To in the header, swap them, and click a "… N unchanged lines" gap to expand it.
It shows text only, not review threads.
- One version per Claude turn. Claude's edits within one turn amend a single version
(marked "writing…" in the picker) instead of making one each. It is sealed when Claude's turn
ends (the
Stop hook that Install Claude Code Hooks adds), when you start a draft, comment,
save a version or restore, when another session writes, or after 5 minutes without a write.
gadfly versions squash <file> merges older runs of one session's consecutive versions (never
across comments or a draft); it also runs once for every tracked file after upgrading.
The CLI is there for Claude and for recovery. It changes the store only by posting Claude's
replies, linking a session and recording versions:
~/.gadfly/bin/gadfly review pending <file> # reprint the digest
~/.gadfly/bin/gadfly review status <file>
~/.gadfly/bin/gadfly review reply <file> <id> # one reply, body on stdin
~/.gadfly/bin/gadfly review reply <file> --batch [--resolve]
~/.gadfly/bin/gadfly review link <file> # make this session the linked one
~/.gadfly/bin/gadfly versions squash <file>
~/.gadfly/bin/gadfly install-hooks
~/.gadfly/bin/gadfly doctor
Notes
A private notepad next to the document: Notes in the top bar, or ⌥N. Nothing in it is ever
sent to Claude: it is not in digests, references or the CLI's output, and it is not read aloud.
- Notes belong to the Claude session, not to a document. A session window shows its own
notes; a document shows the notes of the session it is connected to ("Connected to @name"), so
the notes follow you from the conversation to the plan it produced and back, and through
--resume. With no connected session the pane says "Notes belong to a Claude session. Connect
one to take notes." with a Connect a session button. The header reads "Notes · @name ·
private, never sent to Claude".
- A light list editor. Bullets (
- ), numbers (1. ), checkboxes (- [ ] ) and quotes
(> ), with bold, italic, code and links styled where they stand (there is no preview
mode; headings only show as bold lines, tables and HTML as plain text). What is stored is a small
Markdown subset, so the file stays readable anywhere.
- Enter continues the list (a new checkbox starts unchecked, numbers count on); on an empty
item it ends the list. Backspace right after a marker removes the marker.
- Tab / Shift+Tab indent and outdent an item by two spaces.
- ⌘Enter toggles the checkbox on the line (an item without one gets one; a plain line becomes
a checkbox item). Clicking a box toggles it too.
- Undo inside the pane (
⌘Z) is the pane's own.
- Autosave about 400 ms after you stop typing, and when the pane hides or the tab closes.
- Add to notes on the selection bar appends the selected text as a
> quote and puts the cursor
after it.
- Open in editor (pane header) opens the raw
.md in a normal VS Code tab beside; saving there
updates the open panes.
- Everywhere at once. All panels on one session's note, in this window or in other VS Code
windows, show the same text: a change arrives when you are not typing in that pane.
- Notes from before 0.3.2 were keyed by document and are not migrated; their files stay in the
store until retention removes them.
Saved sessions and expired sessions
Claude Code deletes transcripts after a while (cleanupPeriodDays, 30 days by default). So Gadfly
keeps its own copy of every session you open in it: the messages it shows (you, Claude, subagent
results, Gadfly digests, compaction summaries and rules) plus the session's name, folder, branch,
model and times. The copy is written when you open the session and follows it live.
When Claude Code's transcript is gone, the session still opens from the copy, under Gadfly: Open
Session… in an Expired in Claude Code group, with the banner "Session expired in Claude
Code · read-only". Everything that would change it is read-only: new comments, replies, edits,
resolve, reopen, delete, Send, and its notes (you can still read and copy them; Open in editor and
Add to notes are off). gadfly session reply refuses too. You can still read it, listen to it,
select and copy references to it, and gadfly show ref=s:… still resolves its messages. A session
that is merely not running keeps its full editing; only a missing transcript expires it.
Retention
gadfly.retentionDays (default 90; 0 keeps everything) deletes what Gadfly stores for a
document or a session that has not been opened in Gadfly for that many days, counted from when it
was last opened or closed in any window.
- A session is one unit: its saved copy, comments, link, details and notes go together. A
document's whole review folder (versions, comments) goes together too. Leftover notes (keyed by a
document, from 0.3.1) are judged by their file's modification time.
- Never deleted: a running session, anything open in a window, and a document review that is
under review or has a draft.
- It runs once a day across all windows (30 seconds after a window starts, if no window has run it
in the last 24 hours). There is no warning: a session never opened in Gadfly has nothing of
Gadfly's to lose, and one that was opened already has its copy.
Session review
The conversation before the plan gets the same loop as the plan: read it, listen to it, comment
on it, and send the comments back to the session. Gadfly: Open Session… lists the running
Claude Code sessions of this window's folders first ("Show sessions in other folders" for the
rest), then past sessions, then those that have Expired in Claude Code (see above). The review panel's connection chip has an Open transcript button
for the session a document is linked to. A session opens once per window; opening it again
focuses its tab.
- What you see. Your messages, Claude's text, subagent results, Gadfly digests and
compaction summaries (both folded, readable, not commentable), and a "Compacted · time" rule
at each compaction. Tool calls, thinking and harness noise are dropped. Each message has a
heading (
Claude · 14:02 · msg 12); Contents lists the turns. The transcript is followed live:
new messages append in place without moving your scroll position or the playhead, and
Claude's message that is still being written is marked "writing…" until it ends. Sessions have
no versions, drafts, Compare, Source pane or Review complete.
- Listening. Read aloud chips (You · Claude · Subagents · Gadfly · Summaries, all on)
choose what is spoken; a short cue ("Claude.") is spoken when the speaker changes. The chips
are kept in the tab's state only. Playback follows live: when it has reached the end, new
messages play next; when you are behind, they wait and Catch up shows "N new".
- Comments. Select text in your message, Claude's or a subagent's and comment as in a
document. Threads are numbered
S1, S2… and grouped by turn ("msg 12 · Claude"). Messages
never change, so nothing goes outdated. Threads are stored by the session's root id
(<store>/sessions/<id>/), so they follow a session through --resume.
- Send carries only this session's pending threads and goes only to this session's inbox. It
is disabled while the session isn't running ("@name isn't running — resume it to send");
sessions never fall back to the clipboard. Claude answers in plain chat first; once everything
sent is settled it asks once and posts the replies in one batch
(
~/.gadfly/bin/gadfly session reply --batch [--resolve]), optionally resolving them.
- References both ways. Select text in a document or a session (running, stopped or expired)
and press Copy reference in the selection bar (or run Gadfly: Copy Reference to Selection). It
copies
[@name › msg 12 "quote"] ref=s:… or [file.md › §Section "quote"] ref=d:…, an id that
resolves in any VS Code window. Pasted into a Gadfly comment (new, reply or edit), it becomes a live
reference: a chip that opens the session at that message or the document at that text, followed
through later edits of a document (it goes outdated when the text is removed). Pasted anywhere else,
such as Claude's prompt, it stays readable and gadfly show ref=… resolves it. An id this machine
can't resolve stays plain text. Digests expand references with the quote, a sentence either side and
the id, for gadfly show <ref> [--context N].
- Plan mode. With open session threads, a session can't enter plan mode
(
gadfly.sessions.blockPlanOnOpenThreads, default on).
CLI for Claude: gadfly session pending [<id>], session threads [--open],
session reply --batch [--resolve] (blocks headed === S4), show <ref>.
Player
⌥Space play/pause · ⌥←/→ skip 10 words (gadfly.skipWords) · ⌥⇧←/→ previous/next
sentence · ⌥↑/↓ speed ±0.25 · ⌥C comment on the selection or current sentence ·
⌥B go to bookmark (click the red ribbon on the right edge to go to, play from, or remove
it; or run Gadfly: Remove Bookmark). Player keys pause while you type in the source pane
or a comment box. While playing, click a word to jump; ⌥-click plays from anywhere.
At rest the player is a small pill: back, play, forward and speed, under a thin progress line.
Hover it (or Tab into it) and it opens in place, with Play staying where it is: the progress
line grows into the time bar, follow and focus slide out on the left, and settings and minimise
on the right. The voice is picked from settings. On the time bar, pointing near a section mark
snaps to it, so a click starts that section. Minimise leaves just play, settings and expand in the bottom-right corner; below 400 px of reader width
the player uses that size by itself. Gadfly remembers the size. The gear opens the settings you
change while listening; the rest are in VS Code's Settings. Voice engine progress and playback
errors appear in VS Code's status bar (click for the log); the play button only gets a red dot.
Speed runs 0.5–3.0× in 0.1 steps. In hybrid mode (gadfly.speedMode) Kokoro synthesizes
at 0.5/0.75/1/1.25/1.5 and pitch-preserving playback rate covers the rest.
Where things live
~/.gadfly/ (override: GADFLY_HOME):
config.json points the CLI and hooks at the store (storePath) and mirrors two settings for
them (blockPlanOnOpenThreads, retentionDays), plus the time of the last retention run.
bin/gadfly (a wrapper) and bin/gadfly.mjs: the CLI copy the hooks call, so hooks survive
extension updates; bin/SKILL.md is the skill it installs.
gadflyd.sock, gadflyd.lock, gadflyd.log: the shared TTS daemon.
The store is VS Code's global storage for this extension, normally
~/Library/Application Support/Code/User/globalStorage/gadfly.gadfly-reviewer/ (override: GADFLY_STORE,
for the CLI and tests). It is never inside a repo:
tracked-index.json tracked documents (locked? why? open threads?) and open session
threads: what the hooks read to decide, fast
recent-writers.json .md files Claude wrote before they were tracked
reviews/<sha1 of path>/ one folder per document opened in Gadfly
meta.json link.json versions.json owner.json (only while a window has it open)
blobs/<sha1>.md every version's text (and comment base texts), stored once
pins/<sha1> blobs a copied d: reference names, never dropped
threads/<n>.json the comments made on version n
sessions/<root session id>/ one folder per Claude session reviewed or opened in Gadfly
meta.json link.json session details, and how to reach the running session
owner.json (only while a window has it open)
threads.json the comments (S1, S2…)
transcript.jsonl the saved copy (one message per line)
info.json name, folder, branch, model, times, resume chain
notes/s-<root session id>.md the private note of that session
audio/ synthesized sentences (<key>.wav + <key>.json), capped by gadfly.cacheMB
Sessions are stored by their root id (the id of a conversation's first transcript entry), so
comments and notes follow a session through --resume. If the extension id ever changes, copy
this folder across (see Extension id and the store location).
Settings
| Setting |
Default |
What it does |
gadfly.openMarkdownByDefault |
false |
Open Markdown files in Gadfly instead of the text editor. |
gadfly.readerFont |
serif |
serif is Literata; sans is your VS Code interface font. |
gadfly.retentionDays |
90 |
See Retention above. 0 keeps everything. |
gadfly.sessions.blockPlanOnOpenThreads |
true |
Block plan mode in a session that has open Gadfly threads. |
gadfly.voice |
af_heart |
TTS voice. |
gadfly.speed |
1 |
Playback speed, 0.5 to 3. |
gadfly.speedMode |
hybrid |
hybrid (see Player above), model (all speed from Kokoro) or playback (all from playback rate). |
gadfly.skipWords |
10 |
Words to skip with ⌥←/→. |
gadfly.readCode |
false |
Read code blocks aloud instead of announcing and skipping them. |
gadfly.followResumeSeconds |
8 |
Resume follow mode this many seconds after a manual scroll. |
gadfly.uvPath |
empty |
Path to uv; empty means auto-detect. |
gadfly.model |
mlx-community/Kokoro-82M-8bit |
Kokoro model: a Hugging Face repo id or a local folder. |
gadfly.daemonIdleMinutes |
15 |
The shared TTS daemon exits after this many idle minutes. |
gadfly.cacheMB |
300 |
Size limit of the audio cache. |
When something is wrong
Gadfly: Doctor writes an environment report to Gadfly's log (gadfly doctor prints the same
report in a terminal), and Gadfly: Show Log opens that log. Gadfly: Restart TTS Engine
restarts the voice daemon, whose own log is ~/.gadfly/gadflyd.log.
Bugs go to the issue tracker. To work on Gadfly itself,
see CONTRIBUTING.md.
| |