Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ plannotator/
│ │ └── review-editor.html # Built code review app
│ ├── marketing/ # Marketing site, docs, and blog (plannotator.ai)
│ │ └── astro.config.mjs # Astro 5 static site with content collections
│ ├── vscode-extension/ # VS Code extension for inline annotations
│ │ ├── src/extension.ts # Add/export annotation commands
│ │ └── package.json # Extension manifest
│ └── review/ # Standalone review server (for development)
│ ├── index.html
│ ├── index.tsx
Expand Down Expand Up @@ -324,6 +327,7 @@ bun run dev:hook # Hook server (plan review)
bun run dev:review # Review editor (code review)
bun run dev:portal # Portal editor
bun run dev:marketing # Marketing site
bun run dev:vscode # VS Code extension (watch mode)
```

## Build
Expand All @@ -334,6 +338,7 @@ bun run build:review # Code review editor
bun run build:opencode # OpenCode plugin (copies HTML from hook + review)
bun run build:portal # Static build for share.plannotator.ai
bun run build:marketing # Static build for plannotator.ai
bun run build:vscode # VS Code extension
bun run build # Build hook + opencode (main targets)
```

Expand Down
13 changes: 13 additions & 0 deletions apps/vscode-extension/.vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "build-extension"
}
]
}
12 changes: 12 additions & 0 deletions apps/vscode-extension/.vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build-extension",
"type": "shell",
"command": "node esbuild.config.mjs",
"group": "build",
"problemMatcher": ["$tsc"]
}
]
}
5 changes: 5 additions & 0 deletions apps/vscode-extension/.vscodeignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
src/
node_modules/
tsconfig.json
esbuild.config.mjs
.vscode/
23 changes: 23 additions & 0 deletions apps/vscode-extension/esbuild.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as esbuild from "esbuild";

const watch = process.argv.includes("--watch");

const config = {
entryPoints: ["src/extension.ts"],
bundle: true,
outfile: "dist/extension.js",
external: ["vscode"],
format: "cjs",
platform: "node",
target: "node18",
sourcemap: true,
minify: !watch,
};

if (watch) {
const ctx = await esbuild.context(config);
await ctx.watch();
console.log("Watching for changes...");
} else {
await esbuild.build(config);
}
72 changes: 72 additions & 0 deletions apps/vscode-extension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"name": "plannotator",
"displayName": "Plannotator",
"description": "Add inline annotation markers to any file for plan review and code feedback",
"version": "0.0.1",
"publisher": "backnotprop",
"license": "MIT OR Apache-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/backnotprop/plannotator.git",
"directory": "apps/vscode-extension"
},
"engines": {
"vscode": "^1.80.0"
},
"categories": ["Other"],
"keywords": ["annotation", "code-review", "plannotator"],
"main": "./dist/extension.js",
"activationEvents": [],
"contributes": {
"commands": [
{
"command": "plannotator.addAnnotation",
"title": "Plannotator: Add Annotation"
},
{
"command": "plannotator.exportAnnotations",
"title": "Plannotator: Export Annotations"
}
],
"menus": {
"editor/context": [
{
"command": "plannotator.addAnnotation",
"group": "1_modification@99"
},
{
"command": "plannotator.exportAnnotations",
"group": "1_modification@100"
}
]
},
"keybindings": [
{
"command": "plannotator.addAnnotation",
"key": "ctrl+alt+p",
"mac": "cmd+alt+p",
"when": "editorTextFocus"
}
],
"configuration": {
"title": "Plannotator",
"properties": {
"plannotator.annotationPrefix": {
"type": "string",
"default": "@plannotator",
"description": "Marker prefix for annotation comments"
}
}
}
},
"scripts": {
"build": "node esbuild.config.mjs",
"watch": "node esbuild.config.mjs --watch",
"package": "npx @vscode/vsce package --no-dependencies"
},
"devDependencies": {
"@types/vscode": "^1.80.0",
"esbuild": "^0.20.0",
"typescript": "~5.8.2"
}
}
168 changes: 168 additions & 0 deletions apps/vscode-extension/src/extension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import * as vscode from "vscode";

export function activate(context: vscode.ExtensionContext) {
const addAnnotation = vscode.commands.registerTextEditorCommand(
"plannotator.addAnnotation",
async (editor) => {
const config = vscode.workspace.getConfiguration("plannotator");
const prefix = config.get<string>("annotationPrefix", "@plannotator");
const closingTag = prefix.startsWith("@")
? "/" + prefix.slice(1)
: "/" + prefix;

const doc = editor.document;
const selection = editor.selection;
const startLine = selection.start.line;
const endLine = selection.end.line;
const multiLine = startLine !== endLine;
const indent = doc.lineAt(startLine).text.match(/^\s*/)![0];

// Auto-increment ID by scanning existing annotations
const idPattern = new RegExp(`${escapeRegex(prefix)}\\s+(\\d+)`);
let maxId = 0;
for (let i = 0; i < doc.lineCount; i++) {
const match = doc.lineAt(i).text.match(idPattern);
if (match) {
maxId = Math.max(maxId, parseInt(match[1]));
}
}
const nextId = String(maxId + 1).padStart(4, "0");

const startMarker = `${indent}<!-- ${prefix} ${nextId}: -->\n`;

const inserted = await editor.edit((eb) => {
if (multiLine) {
// Insert end marker first so startLine doesn't shift
const endInsertLine = endLine + 1;
const endMarker = `${indent}<!-- ${closingTag} ${nextId} -->\n`;
eb.insert(new vscode.Position(endInsertLine, 0), endMarker);
}
eb.insert(new vscode.Position(startLine, 0), startMarker);
});

if (inserted) {
const cursorCol =
indent.length + `<!-- ${prefix} ${nextId}: `.length;
const pos = new vscode.Position(startLine, cursorCol);
editor.selection = new vscode.Selection(pos, pos);
}
}
);

const exportAnnotations = vscode.commands.registerTextEditorCommand(
"plannotator.exportAnnotations",
async (editor) => {
const config = vscode.workspace.getConfiguration("plannotator");
const prefix = config.get<string>("annotationPrefix", "@plannotator");
const closingTag = prefix.startsWith("@")
? "/" + prefix.slice(1)
: "/" + prefix;

const doc = editor.document;
const escapedPrefix = escapeRegex(prefix);
const escapedClosing = escapeRegex(closingTag);

const startPattern = new RegExp(
`^\\s*<!--\\s*${escapedPrefix}\\s+(\\d{4}):\\s*(.*?)\\s*-->`,
);
const endPattern = new RegExp(
`^\\s*<!--\\s*${escapedClosing}\\s+(\\d{4})\\s*-->`,
);
const anyMarkerPattern = new RegExp(
`^\\s*<!--\\s*(?:${escapedPrefix}|${escapedClosing})\\s`,
);

// Collect all start markers
const starts: { id: string; text: string; lineIndex: number }[] = [];
for (let i = 0; i < doc.lineCount; i++) {
const match = doc.lineAt(i).text.match(startPattern);
if (match && match[2]) {
starts.push({ id: match[1], text: match[2], lineIndex: i });
}
}

if (starts.length === 0) {
vscode.window.showInformationMessage(
"No annotations found in this file.",
);
return;
}

// Resolve context for each annotation
const annotations: { text: string; context: string; multiLine: boolean }[] = [];

for (const start of starts) {
// Search downward for matching end marker
let endLineIndex = -1;
for (let j = start.lineIndex + 1; j < doc.lineCount; j++) {
const endMatch = doc.lineAt(j).text.match(endPattern);
if (endMatch && endMatch[1] === start.id) {
endLineIndex = j;
break;
}
}

if (endLineIndex > start.lineIndex + 1) {
// Multi-line: capture lines between markers, strip inner markers
const lines: string[] = [];
for (let j = start.lineIndex + 1; j < endLineIndex; j++) {
const line = doc.lineAt(j).text;
if (!anyMarkerPattern.test(line)) {
lines.push(line);
}
}
annotations.push({
text: start.text,
context: lines.join("\n"),
multiLine: true,
});
} else {
// Single-line: next non-empty, non-marker line
let context = "";
for (let j = start.lineIndex + 1; j < doc.lineCount; j++) {
const line = doc.lineAt(j).text;
if (line.trim() === "") continue;
if (anyMarkerPattern.test(line)) continue;
context = line.trim();
break;
}
annotations.push({
text: start.text,
context: context || "(end of file)",
multiLine: false,
});
}
}

// Format output
let output = "# Plan Feedback\n\n";
output += `I've reviewed this plan and have ${annotations.length} piece${annotations.length > 1 ? "s" : ""} of feedback:\n\n`;

annotations.forEach((ann, i) => {
if (ann.multiLine) {
output += `## ${i + 1}. Feedback on:\n`;
output += "```\n" + ann.context + "\n```\n";
output += `> ${ann.text}\n\n`;
} else {
output += `## ${i + 1}. Feedback on: "${ann.context}"\n`;
output += `> ${ann.text}\n\n`;
}
});

output += "---\n";

await vscode.env.clipboard.writeText(output);
vscode.window.showInformationMessage(
`Exported ${annotations.length} annotation${annotations.length > 1 ? "s" : ""} to clipboard.`,
);
}
);

context.subscriptions.push(addAnnotation, exportAnnotations);
}

function escapeRegex(str: string): string {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

export function deactivate() {}
16 changes: 16 additions & 0 deletions apps/vscode-extension/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "node",
"sourceMap": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
Loading