From fee218aded9d290dc39bab92e0a7ab2f49ae4b5b Mon Sep 17 00:00:00 2001 From: Yuki Hayashi Date: Fri, 10 Jul 2026 14:43:51 +0000 Subject: [PATCH 1/2] docs: design for VS Code/Cursor preview extension Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-07-10-vscode-extension-design.md | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 docs/specs/2026-07-10-vscode-extension-design.md diff --git a/docs/specs/2026-07-10-vscode-extension-design.md b/docs/specs/2026-07-10-vscode-extension-design.md new file mode 100644 index 0000000..bd44262 --- /dev/null +++ b/docs/specs/2026-07-10-vscode-extension-design.md @@ -0,0 +1,129 @@ +# reefdoc VS Code / Cursor Extension — Design + +**Date:** 2026-07-10 +**Status:** Design approved; not yet implemented. + +## Summary + +An editor extension that brings reefdoc's existing preview UI *inside* VS Code +and Cursor, so you no longer switch to a separate browser tab. The extension is +a thin **launcher + embedder**: it spawns the bundled `reefdoc` binary pointed +at the workspace folder, then displays that running server's localhost UI in a +VS Code webview panel. The Go backend and vanilla-JS frontend are reused +verbatim — no reimplementation of rendering, watching, or live reload. + +Cursor is a VS Code fork, so a single extension covers both editors. + +## Goals + +- Show the full reefdoc app (file tree, tabs, TOC, live reload, change + highlighting) in an editor tab instead of a browser tab. +- Reuse the existing binary and frontend with zero changes to rendering logic. +- One command to open the preview; single-instance per window. +- Bundle per-platform binaries so users need no separate install. +- Work identically in VS Code and Cursor. + +## Non-goals (YAGNI) + +- **Marketplace publishing.** Distribution is out of scope; the build produces a + local `.vsix` installed via "Install from VSIX." This is for in-editor use, + not public distribution. +- **Active-editor sync / scroll-sync / click-to-source.** The panel shows + reefdoc's own navigation (its file tree + tabs), not the currently focused + editor file. Deeper editor integration is a possible future project, not this + one. +- **Remote / Codespaces support.** Targets local desktop use. `asExternalUri` + port-forwarding is noted as a future hook but not implemented. +- **Reimplementing the frontend against VS Code APIs.** Explicitly rejected — it + throws away the whole point of bundling the binary. + +## Binary flag: already present + +The binary already accepts `--addr` (`main.go:23`, default `127.0.0.1:8080`), +so the extension can direct it to any port with no change to `main.go`. The +extension selects a free port itself (bind an ephemeral socket, read the port, +release it, pass it as `--addr 127.0.0.1:`) rather than relying on the +server to report a `:0`-assigned port. + +## Architecture & components + +The extension lives in `editor/vscode/` inside this repo — versioned alongside +the binary, sharing the release cross-compile that produces per-platform +binaries. + +- **`extension.ts` — activation & command.** Registers one command, + `reefdoc.openPreview` ("reefdoc: Open Preview"). On invoke: resolve the + workspace root, pick a free localhost port, start the server, then open the + webview panel. Holds the single-instance reference to the live panel. +- **`server.ts` — binary lifecycle.** Resolves the correct bundled binary for + the current `process.platform` / `process.arch`, spawns + `reefdoc --addr 127.0.0.1: `, polls `GET /` until the + port answers (with a timeout), and exposes `stop()` to kill the child + process. One server process per panel. +- **`panel.ts` — the webview.** Creates a `WebviewPanel` titled "reefdoc" with a + reefdoc icon. Its HTML is a full-bleed ` + +`; +} + +export function createReefdocPanel(url: string): vscode.WebviewPanel { + const panel = vscode.window.createWebviewPanel( + 'reefdoc.preview', + 'reefdoc', + vscode.ViewColumn.Beside, + { enableScripts: true, retainContextWhenHidden: true }, + ); + panel.webview.html = webviewHtml(url); + return panel; +} +``` + +- [ ] **Step 2: Compile to catch type errors** + +Run: `cd editor/vscode && npm run build` +Expected: `tsc` succeeds, `out/panel.js` produced. + +- [ ] **Step 3: Manual verification (deferred until Task 5 wires it)** + +`createReefdocPanel` has no standalone UI trigger yet — it is exercised by the command wired in Task 5. No separate manual step here; verification happens in Task 5, Step 4. + +- [ ] **Step 4: Commit** + +```bash +git add editor/vscode/src/panel.ts +git commit -m "feat: webview panel that frames the reefdoc server" +``` + +--- + +### Task 5: Wire the command — single instance, spawn, frame, cleanup, errors + +**Files:** +- Modify: `editor/vscode/src/extension.ts` + +**Interfaces:** +- Consumes: `resolveBinaryPath`, `findFreePort`, `startServer`, `ReefdocServer` from `server.ts`; `createReefdocPanel` from `panel.ts`. +- Produces: fully-wired `reefdoc.openPreview` command; `activate`/`deactivate` manage a single live `{ panel, server }`. + +This task imports `vscode`; verify manually via the Extension Development Host (Step 4). + +- [ ] **Step 1: Replace `editor/vscode/src/extension.ts` with the wired implementation** + +```ts +import * as vscode from 'vscode'; +import { + resolveBinaryPath, + findFreePort, + startServer, + ReefdocServer, +} from './server'; +import { createReefdocPanel } from './panel'; + +interface Live { + panel: vscode.WebviewPanel; + server: ReefdocServer; +} + +let current: Live | undefined; + +async function openPreview(context: vscode.ExtensionContext): Promise { + if (current) { + current.panel.reveal(vscode.ViewColumn.Beside); + return; + } + + const folder = vscode.workspace.workspaceFolders?.[0]; + if (!folder) { + vscode.window.showErrorMessage('reefdoc: Open a folder to preview.'); + return; + } + const root = folder.uri.fsPath; + + const cfg = vscode.workspace.getConfiguration('reefdoc'); + const host = cfg.get('host', '127.0.0.1'); + const overridePath = cfg.get('binaryPath', ''); + + let binaryPath: string; + try { + binaryPath = overridePath || resolveBinaryPath(context.extensionPath); + } catch (err) { + vscode.window.showErrorMessage(`reefdoc: ${(err as Error).message}`); + return; + } + + let server: ReefdocServer; + try { + const port = await findFreePort(); + server = await startServer({ binaryPath, root, host, port }); + } catch (err) { + vscode.window.showErrorMessage(`reefdoc: failed to start — ${(err as Error).message}`); + return; + } + + const panel = createReefdocPanel(server.url); + current = { panel, server }; + panel.onDidDispose(() => { + server.stop(); + current = undefined; + }); +} + +export function activate(context: vscode.ExtensionContext): void { + const disposable = vscode.commands.registerCommand('reefdoc.openPreview', () => + openPreview(context), + ); + context.subscriptions.push(disposable); +} + +export function deactivate(): void { + current?.server.stop(); + current = undefined; +} +``` + +- [ ] **Step 2: Compile** + +Run: `cd editor/vscode && npm run build` +Expected: `tsc` succeeds with no errors. + +- [ ] **Step 3: Run the full unit/smoke test suite** + +Run: `cd editor/vscode && npm test` +Expected: PASS — Task 2 and Task 3 tests still green (extension.ts is not unit-tested but must not break the build the test loader shares). + +- [ ] **Step 4: Manual verification in the Extension Development Host** + +Because the extension has no bundled binary yet (Task 6), temporarily set `reefdoc.binaryPath` to the repo-root binary for this check: +1. Build the binary: `go build -o "$PWD/reefdoc" .` from the repo root. +2. Open `editor/vscode/` in VS Code, press F5 to launch the Extension Development Host. +3. In the dev-host window, open a folder that contains markdown, set `reefdoc.binaryPath` to the absolute path of the binary from step 1. +4. Run **reefdoc: Open Preview** from the command palette. Confirm: a panel titled "reefdoc" opens beside the editor, the file tree renders, opening a doc works, editing a file on disk live-reloads the panel. +5. Close the panel; confirm the `reefdoc` process is gone (`pgrep reefdoc` returns nothing). Re-run the command; confirm only one panel/process exists. + +Record the result of step 4–5 in the commit message body. + +- [ ] **Step 5: Commit** + +```bash +git add editor/vscode/src/extension.ts +git commit -m "feat: wire reefdoc.openPreview command with lifecycle and errors" +``` + +--- + +### Task 6: Bundle per-platform binaries and package the .vsix + +**Files:** +- Create: `editor/vscode/scripts/bundle-binaries.mjs` +- Create: `editor/vscode/.vscodeignore` +- Create: `editor/vscode/README.md` +- Modify: `editor/vscode/package.json` (add `bundle` and `package` scripts) +- Modify: `editor/vscode/.gitignore` (ignore `bin/`) + +**Interfaces:** +- Consumes: the platform keys used by `resolveBinaryPath` (`darwin-arm64`, `darwin-x64`, `linux-arm64`, `linux-x64`, `win32-x64`). +- Produces: `bin/-/reefdoc[.exe]` for each key, and a `reefdoc-0.1.0.vsix` from `npm run package`. + +- [ ] **Step 1: Create the cross-compile script `editor/vscode/scripts/bundle-binaries.mjs`** + +```js +import { execFileSync } from 'node:child_process'; +import { mkdirSync, rmSync } from 'node:fs'; +import * as path from 'node:path'; + +// Maps our bin/ directories to Go GOOS/GOARCH values. +const TARGETS = [ + { key: 'darwin-arm64', goos: 'darwin', goarch: 'arm64' }, + { key: 'darwin-x64', goos: 'darwin', goarch: 'amd64' }, + { key: 'linux-arm64', goos: 'linux', goarch: 'arm64' }, + { key: 'linux-x64', goos: 'linux', goarch: 'amd64' }, + { key: 'win32-x64', goos: 'windows', goarch: 'amd64' }, +]; + +const extDir = path.resolve(import.meta.dirname, '..'); +const repoRoot = path.resolve(extDir, '..', '..'); +const binRoot = path.join(extDir, 'bin'); + +rmSync(binRoot, { recursive: true, force: true }); + +for (const t of TARGETS) { + const outDir = path.join(binRoot, t.key); + mkdirSync(outDir, { recursive: true }); + const exe = t.goos === 'windows' ? 'reefdoc.exe' : 'reefdoc'; + const outFile = path.join(outDir, exe); + console.log(`building ${t.key} -> ${outFile}`); + execFileSync('go', ['build', '-o', outFile, '.'], { + cwd: repoRoot, + env: { ...process.env, GOOS: t.goos, GOARCH: t.goarch, CGO_ENABLED: '0' }, + stdio: 'inherit', + }); +} +console.log('done'); +``` + +- [ ] **Step 2: Create `editor/vscode/.vscodeignore`** + +``` +src/** +scripts/** +tsconfig.json +**/*.test.ts +**/*.map +.gitignore +node_modules/** +``` + +Note: `out/**` and `bin/**` are intentionally NOT ignored — they must ship in the `.vsix`. + +- [ ] **Step 3: Create `editor/vscode/README.md`** + +```markdown +# reefdoc for VS Code / Cursor + +Opens the [reefdoc](https://github.com/exilis/reefdoc) markdown / mermaid / +allium preview inside an editor panel. Bundles the reefdoc binary — no separate +install needed. + +## Usage + +Open a folder, then run **reefdoc: Open Preview** from the command palette. A +panel opens beside your editor showing reefdoc's file tree, tabs, and live +reload. Close the panel to stop the server. + +## Settings + +- `reefdoc.binaryPath` — use a custom binary instead of the bundled one. +- `reefdoc.host` — listen host (default `127.0.0.1`). + +## Build & package + +```bash +npm install +npm run bundle # cross-compiles binaries into bin/ (needs Go on PATH) +npm run build # compiles TypeScript into out/ +npm run package # produces reefdoc-.vsix +``` + +Install the `.vsix` via the Extensions view → "Install from VSIX…". +``` + +- [ ] **Step 4: Add `bundle` and `package` scripts to `editor/vscode/package.json`** + +In the `"scripts"` block, add: + +```json + "bundle": "node scripts/bundle-binaries.mjs", + "package": "npm run bundle && npm run build && vsce package --no-dependencies" +``` + +- [ ] **Step 5: Ignore the generated `bin/` directory in `editor/vscode/.gitignore`** + +Append to `editor/vscode/.gitignore`: + +``` +bin/ +``` + +- [ ] **Step 6: Run the bundle and package end-to-end** + +Run: `cd editor/vscode && npm install && npm run package` +Expected: `bin//reefdoc[.exe]` created for all five targets; `tsc` compiles; `vsce package` emits `reefdoc-0.1.0.vsix`. (Requires Go on PATH for cross-compilation.) + +- [ ] **Step 7: Verify the packaged extension installs and runs** + +1. In VS Code: Extensions view → "…" menu → "Install from VSIX…" → select `reefdoc-0.1.0.vsix`. +2. Open a folder with markdown, run **reefdoc: Open Preview** with NO `reefdoc.binaryPath` set (so the bundled binary is used). +3. Confirm the panel opens and renders, and closing it stops the process. + +- [ ] **Step 8: Commit** + +```bash +git add editor/vscode/scripts/bundle-binaries.mjs editor/vscode/.vscodeignore editor/vscode/README.md editor/vscode/package.json editor/vscode/.gitignore +git commit -m "chore: bundle per-platform binaries and package the extension" +``` + +--- + +## Self-Review + +**Spec coverage:** +- In-editor preview via bundled binary → Tasks 3–6. ✓ +- Single command `reefdoc.openPreview` → Task 1 (contribution) + Task 5 (behaviour). ✓ +- `server.ts` free of `vscode`, unit-testable → Tasks 2–3. ✓ +- Single-instance per window; kill child on panel close / deactivate → Task 5. ✓ +- Per-platform binary resolution + bundling → Task 2 (`resolveBinaryPath`) + Task 6 (bundle script). ✓ +- Config `reefdoc.binaryPath` / `reefdoc.host` → Task 1 (contribution) + Task 5 (consumption). ✓ +- Error notifications (no binary for platform, start failure, no workspace) → Task 5. ✓ +- `--addr` already present, `main.go` untouched → Global Constraints; used in Task 3. ✓ +- Smoke test drives the real binary → Task 3. ✓ +- No marketplace; local `.vsix` → Task 6. ✓ +- Cursor + VS Code from one extension → Global Constraints (no editor-specific code). ✓ + +**Placeholder scan:** No TBD/TODO; every code and command step is concrete. + +**Type consistency:** `resolveBinaryPath`, `findFreePort`, `startServer`, `ReefdocServer`, `createReefdocPanel` names/signatures match across Tasks 2–5. Platform keys in `SUPPORTED` (Task 2) match `TARGETS` keys in the bundle script (Task 6): `darwin-arm64`, `darwin-x64`, `linux-arm64`, `linux-x64`, `win32-x64`. ✓