Searchlight review content is persisted as plain JSON under a workspace's .vscode/searchlight-reviews/
directory. There is no review database and no network state. The schema source of truth is
src/reviewModel.ts (types, parse, migration — deliberately vscode-free) and src/reviewStore.ts
(serialize, load, mutate).
<workspaceFolder>/.vscode/searchlight-reviews/
├── registry.json # list of known reviews + activeReviewId
└── <compare>_<base>/ # one folder per comparison; branch '/' -> '-'
└── comments.json # the review: threads, comments, reviewedFiles
- Glob (activation + scan):
.vscode/searchlight-reviews/**/comments.json(REVIEWS_GLOBin reviewStore.ts). Deliberately not the original extension's.vscode/local-reviews/— the two coexist. - Folder name:
<compare>_<base>wherecompare= SOURCE branch andbase= TARGET branch, each run throughsanitizeBranch(every/→-). Example: comparefeature/mul, basemain→feature-mul_main/. - Activation events:
onStartupFinishedandworkspaceContains:.vscode/searchlight-reviews/**/comments.json, plus implicit view/command activation. Inline comments and local usage observation start without opening the sidebar; heavy comparison initialization remains on demand.
VS Code workspace state stores the selected target per source branch and an optional full baseline
commit SHA per target/source pair. These are local UI preferences, not review-schema fields.
Changing the inferred baseline or a pin does not rename review folders or rewrite comments.
targetBranch/targetCommit continue to describe the selected target, not the effective baseline.
Pins are revalidated on refresh and can be cleared with Auto in the Comparison pane.
| On-disk source | Read by | Produces |
|---|---|---|
registry.json |
reviewStore | reviews list + activeReviewId |
<compare>_<base>/comments.json |
parseReview (reviewModel) |
a Review (threads, comments, reviewedFiles, seqCounter) |
historical git blobs (via ReviewDiffContentProvider) |
commits/files diff views | left/right sides of a diff |
saved review threads (via ConversationDocumentProvider) |
read-only conversation tabs | full replies and recorded anchor context, even when the code file is gone |
Conversation transcripts do not add a new on-disk review format. They read comments.json and
never rewrite it just to display a discussion. anchorText, when present, is only the captured
trimmed anchor line; it cannot reconstruct an entire deleted uncommitted file.
Review-wide conversations use the same v2 thread/comment schema. They may have an additive
title string, but omit filePath, startLine, endLine and anchorText. They participate in the
same durable seqCounter, state and reply relationships as inline threads. Older files without
title remain valid. Merely opening a draft does not create comments.json; posting its first
message creates the review directory/file as needed.
| Type | Fields |
|---|---|
Review |
version, sourceBranch?, targetBranch?, sourceCommit?, targetCommit?, threads[], reviewedFiles?[], seqCounter?, sourceFile (in-memory only) |
ReviewThread |
id?, filePath?, startLine?, endLine?, state? (unresolved|resolved), seq? (1-based display id), tags[], comments[] |
ReviewComment |
id?, body, timestamp?, replyTo?, author? |
ReviewAuthor |
kind (human|agent|unknown), name, model?, reasoning?, version?, sessionId? (agent-only) |
- Always writes v2.
serializeReviewemitsversion: 2regardless of what was read. - Diff-friendly. 2-space indent + trailing newline.
- Sparse.
reviewedFilesandseqCounterare only written when present/non-empty;authorsub-fields (model/reasoning/version/sessionId) only when set. - No uuid dependency. Ids come from
newId(prefix)=<prefix>-<Date.now(base36)>-<rand6>.
parseReview is tolerant so files written by the original extension (or an older Searchlight) still
load:
| v1 shape | Normalized to |
|---|---|
missing version |
treated as version: 1 |
author is a bare string (e.g. "timolee") |
{ kind: "unknown", name: "timolee" } via parseAuthor |
missing tags |
[] |
missing state |
unresolved |
missing seq / seqCounter |
see durable-seq migration below |
Each thread has a 1-based seq rendered as #01, #02, … seqCounter is a monotonic
high-water mark stored on the Review; it is never decremented.
- New thread:
addThreaddoesseq = ++seqCounter— it never scansMath.max(threads), so a transiently-empty in-memory review can't reset the counter and reuse a display id. - Legacy migration (
normalizeSeq): runs once whenseqCounter === undefined. It renumbers existing threads1..Nby array order and setsseqCounter = N. WhenseqCounteris already present it only backfills any missingseqand keeps the counter>= max(seq).
This fixed a real bug where a third thread was labeled #01 (reused) because the counter was derived
from a momentarily-empty thread set.
- Core set (default):
idea question bug change todo nit praise(configurable viasearchlight.tags). - Entered as
/tagtokens in the comment input (seetagCompletion.ts); merged into the thread'stags[]on submit.
reviewedFiles is an array of repo-relative paths the reviewer has checked off in the Changed Files
view. Toggling a file's checkbox mutates the array and calls saveReview (which lazily creates
comments.json if it doesn't exist yet).
| Function | Purpose |
|---|---|
parseReview(data, sourceFile) |
tolerant v1/v2 parse → Review |
parseAuthor(raw) |
string | object → ReviewAuthor |
normalizeSeq(review) |
one-time legacy seq/seqCounter migration |
newId(prefix) |
uuid-free id generator |
formatTimestamp(iso) |
friendly local time, e.g. Jul 4, 7:12 PM (falls back to raw) |
authorDisplay(author) |
{ name, iconId } — hubot for agents, account for human/unknown (callers wrap iconId with vscode.ThemeIcon) |
{ "version": 2, // serializer ALWAYS writes 2; reader tolerates 1 "sourceBranch": "feature/mul", // = compare "targetBranch": "main", // = base "sourceCommit": "…", // resolved shas (optional) "targetCommit": "…", "reviewedFiles": ["src/app.js"], // emitted only when non-empty "seqCounter": 4, // durable high-water mark; emitted only when present "threads": [ { "id": "thr-…", "filePath": "src/app.js", // repo-relative, forward slashes "startLine": 5, "endLine": 5, "state": "unresolved", // unresolved | resolved "seq": 1, // 1-based DISPLAY id -> shown as #01 "tags": ["bug", "question"], "comments": [ { "id": "cmt-…", "body": "why here?", "timestamp": "2026-07-04T…", "author": { "kind": "human", "name": "timolee" } }, { "id": "cmt-…", "body": "Because X. ~Written by 🤖 Copilot", "timestamp": "2026-07-04T…", "replyTo": "cmt-…", // threading: parent comment id "author": { "kind": "agent", "name": "Copilot", "model": "Claude Opus 4.8", "reasoning": "high", "version": "1.0.69", "sessionId": "f7dffc32-…" } } // agent-only Copilot session UUID ] } ] }