Skip to content

Commit 1eb5615

Browse files
authored
feat: standalone skills package + HTML render-annotate mode (#687)
Add --render-html flag to plannotator annotate that renders HTML files as-is in an iframe instead of converting to markdown. Includes annotation support via postMessage bridge, sharing via paste service, and theme inheritance from Plannotator's 30+ themes. New skill: plannotator-visual-explainer — wraps nicobailon/visual-explainer with Plannotator theme tokens, extended patterns (timelines, SVG diagrams, code blocks, risk tables, Pierre diffs via CDN), and plan/PR-specific guidance. All three servers (Bun, Pi, OpenCode) support the new flag.
1 parent 50191e6 commit 1eb5615

22 files changed

Lines changed: 2142 additions & 97 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,4 @@ opencode.json
5252
plannotator-local
5353
# Local research/reference docs (not for repo)
5454
/reference/
55+
*.bun-build

AGENTS.md

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,17 @@ plannotator/
2828
│ │ ├── index.html
2929
│ │ ├── index.tsx
3030
│ │ └── vite.config.ts
31-
│ └── vscode-extension/ # VS Code extension — opens plans in editor tabs
32-
│ ├── bin/ # Router scripts (open-in-vscode, xdg-open)
33-
│ ├── src/ # extension.ts, cookie-proxy.ts, ipc-server.ts, panel-manager.ts, editor-annotations.ts, vscode-theme.ts
34-
│ └── package.json # Extension manifest (publisher: backnotprop)
31+
│ ├── vscode-extension/ # VS Code extension — opens plans in editor tabs
32+
│ │ ├── bin/ # Router scripts (open-in-vscode, xdg-open)
33+
│ │ ├── src/ # extension.ts, cookie-proxy.ts, ipc-server.ts, panel-manager.ts, editor-annotations.ts, vscode-theme.ts
34+
│ │ └── package.json # Extension manifest (publisher: backnotprop)
35+
│ └── skills/ # Agent skills (agentskills.io format)
36+
│ ├── plannotator-review/ # Lightweight: opens review UI
37+
│ ├── plannotator-annotate/ # Lightweight: opens annotate UI
38+
│ ├── plannotator-last/ # Lightweight: annotates last message
39+
│ ├── plannotator-compound/ # Research analysis agent (map-reduce over denied plans)
40+
│ ├── plannotator-setup-goal/ # Goal package scaffolder for /goal workflows
41+
│ └── plannotator-visual-explainer/ # Visual HTML generator (plans, diagrams, PR explainers) with Plannotator theming
3542
├── packages/
3643
│ ├── server/ # Shared server implementation
3744
│ │ ├── index.ts # startPlannotatorServer(), handleServerReady()
@@ -176,7 +183,7 @@ OpenCode/Pi: event handler intercepts command
176183
177184
Input type detected:
178185
.md/.mdx → file read from disk
179-
.html/.htm → file read, converted to markdown via Turndown
186+
.html/.htm → file read, converted to markdown via Turndown (or rendered as-is with --render-html)
180187
https:// → fetched via Jina Reader (default) or fetch+Turndown (--no-jina)
181188
folder/ → file browser opened, files converted on demand
182189
@@ -275,7 +282,7 @@ During normal plan review, an Archive sidebar tab provides the same browsing via
275282

276283
| Endpoint | Method | Purpose |
277284
| --------------------- | ------ | ------------------------------------------ |
278-
| `/api/plan` | GET | Returns `{ plan, origin, mode: "annotate", filePath, sourceInfo?, gate }` |
285+
| `/api/plan` | GET | Returns `{ plan, origin, mode: "annotate", filePath, sourceInfo?, gate, renderAs?, rawHtml? }` |
279286
| `/api/feedback` | POST | Submit annotations (body: feedback, annotations) |
280287
| `/api/approve` | POST | Approve without feedback (review-gate UX, `--gate`) |
281288
| `/api/exit` | POST | Close session without feedback |
@@ -428,6 +435,9 @@ interface SharePayload {
428435
a: ShareableAnnotation[]; // Compact annotations
429436
g?: ShareableImage[]; // Global attachments
430437
d?: (string | null)[]; // diffContext per annotation, parallel to `a`
438+
s?: (string | undefined)[]; // source per annotation (external tool identifier), parallel to `a`
439+
h?: string; // Raw HTML content (--render-html mode)
440+
r?: 'html'; // Render mode flag (omitted = markdown)
431441
}
432442

433443
type ShareableAnnotation =

apps/hook/server/index.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@ const hookIdx = args.indexOf("--hook");
148148
const hookFlag = hookIdx !== -1;
149149
if (hookFlag) args.splice(hookIdx, 1);
150150
if (hookFlag) gateFlag = true;
151+
const renderHtmlIdx = args.indexOf("--render-html");
152+
const renderHtmlFlag = renderHtmlIdx !== -1;
153+
if (renderHtmlFlag) args.splice(renderHtmlIdx, 1);
151154

152155
// Stdout matrix for annotate / annotate-last / copilot annotate-last (#570).
153156
//
@@ -590,6 +593,7 @@ if (args[0] === "sessions") {
590593
}
591594

592595
let markdown: string;
596+
let rawHtml: string | undefined;
593597
let absolutePath: string;
594598
let folderPath: string | undefined;
595599
let annotateMode: "annotate" | "annotate-folder" = "annotate";
@@ -649,11 +653,16 @@ if (args[0] === "sessions") {
649653
process.exit(1);
650654
}
651655
const html = await htmlFile.text();
652-
markdown = htmlToMarkdown(html);
656+
if (renderHtmlFlag) {
657+
rawHtml = html;
658+
markdown = "";
659+
} else {
660+
markdown = htmlToMarkdown(html);
661+
sourceConverted = true;
662+
}
653663
absolutePath = resolvedArg;
654664
sourceInfo = path.basename(resolvedArg);
655-
sourceConverted = true;
656-
console.error(`Converted: ${absolutePath}`);
665+
console.error(`${renderHtmlFlag ? "Raw HTML" : "Converted"}: ${absolutePath}`);
657666
} else {
658667
// Single markdown file annotation mode
659668
// Strip-first with literal-@ fallback (scoped-package-style names).
@@ -696,6 +705,8 @@ if (args[0] === "sessions") {
696705
shareBaseUrl,
697706
pasteApiUrl,
698707
gate: gateFlag,
708+
rawHtml,
709+
renderHtml: renderHtmlFlag,
699710
htmlContent: planHtmlContent,
700711
onReady: async (url, isRemote, port) => {
701712
handleAnnotateServerReady(url, isRemote, port);

apps/opencode-plugin/commands.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,14 +176,15 @@ export async function handleAnnotateCommand(
176176
// --json is accepted silently (OpenCode writes to session, not stdout).
177177
// parseAnnotateArgs strips leading @ on filePath (reference-mode convention).
178178
// `rawFilePath` preserves it for the scoped-package markdown fallback.
179-
const { filePath, rawFilePath, gate } = parseAnnotateArgs(rawArgs);
179+
const { filePath, rawFilePath, gate, renderHtml: renderHtmlFlag } = parseAnnotateArgs(rawArgs);
180180

181181
if (!filePath) {
182182
client.app.log({ level: "error", message: "Usage: /plannotator-annotate <file.md | file.html | https://... | folder/> [--gate] [--json]" });
183183
return;
184184
}
185185

186186
let markdown: string;
187+
let rawHtml: string | undefined;
187188
let absolutePath: string;
188189
let folderPath: string | undefined;
189190
let annotateMode: "annotate" | "annotate-folder" = "annotate";
@@ -240,11 +241,16 @@ export async function handleAnnotateCommand(
240241
return;
241242
}
242243
const html = await Bun.file(resolvedArg).text();
243-
markdown = htmlToMarkdown(html);
244+
if (renderHtmlFlag) {
245+
rawHtml = html;
246+
markdown = "";
247+
} else {
248+
markdown = htmlToMarkdown(html);
249+
sourceConverted = true;
250+
}
244251
absolutePath = resolvedArg;
245252
sourceInfo = path.basename(resolvedArg);
246-
sourceConverted = true;
247-
client.app.log({ level: "info", message: `Converted: ${absolutePath}` });
253+
client.app.log({ level: "info", message: `${renderHtmlFlag ? "Raw HTML" : "Converted"}: ${absolutePath}` });
248254
} else {
249255
// Markdown file annotation
250256
client.app.log({ level: "info", message: `Opening annotation UI for ${filePath}...` });
@@ -280,6 +286,8 @@ export async function handleAnnotateCommand(
280286
folderPath,
281287
sourceInfo,
282288
sourceConverted,
289+
rawHtml,
290+
renderHtml: renderHtmlFlag,
283291
sharingEnabled: await getSharingEnabled(),
284292
shareBaseUrl: getShareBaseUrl(),
285293
pasteApiUrl: getPasteApiUrl(),

apps/pi-extension/index.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,7 @@ export default function plannotator(pi: ExtensionAPI): void {
493493
// accepted (Pi writes back via sendUserMessage, not stdout).
494494
// `rawFilePath` keeps any leading `@` for the literal-@ fallback
495495
// (scoped-package-style names).
496-
const { filePath, rawFilePath, gate } = parseAnnotateArgs(args ?? "");
496+
const { filePath, rawFilePath, gate, renderHtml: renderHtmlFlag } = parseAnnotateArgs(args ?? "");
497497
if (!filePath) {
498498
ctx.ui.notify("Usage: /plannotator-annotate <file.md | file.html | https://... | folder/> [--gate] [--json]", "error");
499499
return;
@@ -507,6 +507,7 @@ export default function plannotator(pi: ExtensionAPI): void {
507507
}
508508

509509
let markdown: string;
510+
let rawHtml: string | undefined;
510511
let absolutePath: string;
511512
let folderPath: string | undefined;
512513
let mode: "annotate" | "annotate-folder" | undefined;
@@ -570,9 +571,14 @@ export default function plannotator(pi: ExtensionAPI): void {
570571
return;
571572
}
572573
const html = readFileSync(absolutePath, "utf-8");
573-
markdown = htmlToMarkdown(html);
574+
if (renderHtmlFlag) {
575+
rawHtml = html;
576+
markdown = "";
577+
} else {
578+
markdown = htmlToMarkdown(html);
579+
sourceConverted = true;
580+
}
574581
sourceInfo = basename(absolutePath);
575-
sourceConverted = true;
576582
ctx.ui.notify(`Opening annotation UI for ${filePath}...`, "info");
577583
} else {
578584
markdown = readFileSync(absolutePath, "utf-8");
@@ -593,6 +599,8 @@ export default function plannotator(pi: ExtensionAPI): void {
593599
sourceInfo,
594600
sourceConverted,
595601
gate,
602+
rawHtml,
603+
renderHtmlFlag,
596604
);
597605
ctx.ui.notify("Annotation opened. You can keep chatting while it runs.", "info");
598606
void session

apps/pi-extension/plannotator-browser.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -471,13 +471,15 @@ export async function startMarkdownAnnotationSession(
471471
sourceInfo?: string,
472472
sourceConverted?: boolean,
473473
gate?: boolean,
474+
rawHtml?: string,
475+
renderHtml?: boolean,
474476
): Promise<BrowserDecisionSession<{ feedback: string; exit?: boolean; approved?: boolean }>> {
475477
if (!ctx.hasUI || !planHtmlContent) {
476478
throw new Error("Plannotator annotation browser is unavailable in this session.");
477479
}
478480

479481
let resolvedMarkdown = markdown;
480-
if (!resolvedMarkdown.trim() && existsSync(filePath)) {
482+
if (!renderHtml && !resolvedMarkdown.trim() && existsSync(filePath)) {
481483
try {
482484
const fileStat = statSync(filePath);
483485
if (!fileStat.isDirectory()) {
@@ -497,6 +499,8 @@ export async function startMarkdownAnnotationSession(
497499
sourceInfo,
498500
sourceConverted,
499501
gate,
502+
rawHtml,
503+
renderHtml,
500504
htmlContent: planHtmlContent,
501505
sharingEnabled: process.env.PLANNOTATOR_SHARE !== "disabled",
502506
shareBaseUrl: process.env.PLANNOTATOR_SHARE_URL || undefined,

apps/pi-extension/server/serverAnnotate.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export async function startAnnotateServer(options: {
4747
sourceInfo?: string;
4848
sourceConverted?: boolean;
4949
gate?: boolean;
50+
rawHtml?: string;
51+
renderHtml?: boolean;
5052
}): Promise<AnnotateServerResult> {
5153
// Side-channel pre-warm so /api/doc/exists POSTs land on warm cache.
5254
void warmFileListCache(process.cwd(), "code");
@@ -77,7 +79,7 @@ export async function startAnnotateServer(options: {
7779
const draftSource =
7880
options.mode === "annotate-folder" && options.folderPath
7981
? `folder:${resolvePath(options.folderPath)}`
80-
: options.markdown;
82+
: options.renderHtml && options.rawHtml ? options.rawHtml : options.markdown;
8183
const draftKey = contentHash(draftSource);
8284

8385
// Detect repo info (cached for this session)
@@ -99,6 +101,8 @@ export async function startAnnotateServer(options: {
99101
sourceInfo: options.sourceInfo,
100102
sourceConverted: options.sourceConverted ?? false,
101103
gate: options.gate ?? false,
104+
renderAs: options.renderHtml && options.rawHtml ? 'html' : 'markdown',
105+
...(options.renderHtml && options.rawHtml ? { rawHtml: options.rawHtml } : {}),
102106
sharingEnabled,
103107
shareBaseUrl,
104108
pasteApiUrl,
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
---
2+
name: plannotator-visual-explainer
3+
description: >
4+
Generate beautiful, self-contained HTML visualizations themed with Plannotator's design system.
5+
Wraps the visual-explainer skill by nicobailon with Plannotator theme token integration. Use for
6+
architecture diagrams, diff reviews, plan reviews, data tables, slide decks, project recaps, or
7+
any visual explanation of technical concepts — whenever you want output styled consistently with
8+
Plannotator's UI and compatible with --render-html annotation. Triggers on the same prompts as
9+
visual-explainer (diagrams, architecture overviews, visual plans, diff reviews) but produces
10+
output that uses Plannotator CSS custom properties instead of custom palettes.
11+
---
12+
13+
# Plannotator Visual Explainer
14+
15+
This skill wraps [visual-explainer](https://github.com/nicobailon/visual-explainer) by Nico Bailon with Plannotator theme integration and additional component patterns. You follow visual-explainer's workflow, references, templates, and anti-slop rules — with Plannotator's color/typography tokens and extended patterns for plans and technical documents.
16+
17+
## Setup
18+
19+
Before generating, ensure `visual-explainer` is available:
20+
21+
1. Check if the skill exists at any of these paths (in order):
22+
- `~/.claude/skills/visual-explainer/SKILL.md`
23+
- `~/.agents/skills/visual-explainer/SKILL.md`
24+
- `~/.codex/skills/visual-explainer/SKILL.md`
25+
26+
2. If not found, install it:
27+
```bash
28+
npx skills add nicobailon/visual-explainer -g --yes
29+
```
30+
31+
3. Read the visual-explainer `SKILL.md` to absorb its full workflow, diagram type routing, structure rules, and anti-slop guidelines. Read its `references/` and `templates/` as directed by its workflow.
32+
33+
## Theme Override
34+
35+
Instead of visual-explainer's custom palettes and font pairings, use Plannotator's semantic theme tokens. Read `references/theme-override.md` for the exact CSS custom properties and mapping table. Apply these **after** reading visual-explainer's references — they replace only the color and typography layer.
36+
37+
## Extended Patterns
38+
39+
Plannotator adds component patterns that complement visual-explainer's toolkit. Read `references/extended-patterns.md` for timelines, inline SVG diagrams, code blocks with syntax highlighting, risk tables, and open question callouts. Use these alongside Nico's `.ve-card`, `.kpi-card`, `.pipeline` components — they share the same theme tokens.
40+
41+
## Design Philosophy: Use the Power of HTML
42+
43+
The point of generating HTML instead of markdown is spatial layout. Don't pack every piece of information into dense cards. Let the page breathe.
44+
45+
- **Whitespace is a feature.** Generous padding, large section gaps, breathing room between cards. If a section feels cramped, it needs more space, not smaller text.
46+
- **One idea per viewport.** The reader should be able to absorb one concept at a time as they scroll. A hero section, then a diagram, then a detail grid — not all three crammed together.
47+
- **Visual weight signals importance.** Hero sections dominate (large type, accent-tinted backgrounds, more padding). Supporting details are compact and can collapse. Not everything deserves equal treatment.
48+
- **Show, don't describe.** A timeline shows sequencing. A diagram shows relationships. A before/after grid shows change. A code block shows the interface. Use the right visual element — don't describe things in prose that a component could show directly.
49+
- **Timelines show sequence without estimates.** Show the phases and their dependencies, but do not attach hour/day/week estimates. AI consistently misjudges timing. Showing phases in order communicates sequencing; attaching numbers communicates false precision.
50+
51+
## Workflow
52+
53+
1. **Read** visual-explainer's SKILL.md (full workflow, diagram types, quality checks)
54+
2. **Read** the relevant visual-explainer references and templates for your content type
55+
3. **Read** `references/theme-override.md` for Plannotator color/typography tokens
56+
4. **Read** `references/extended-patterns.md` for additional components (timelines, code blocks, risk tables, SVG diagrams)
57+
5. **Generate** following visual-explainer's structure and rules, with Plannotator tokens and extended patterns. Use Nico's component classes (`.ve-card`, `.ve-card--hero`, `.kpi-card`, `.pipeline`, etc.) for cards and layout. Use the extended patterns for timelines, code blocks, risk tables, and SVG diagrams.
58+
6. **Deliver** via Plannotator's annotation UI:
59+
60+
**If the output is a plan or proposal** (something the user should approve/deny):
61+
```bash
62+
plannotator annotate <file> --render-html --gate
63+
```
64+
65+
**If the output is a visual explainer, diagram, or informational page:**
66+
```bash
67+
plannotator annotate <file> --render-html
68+
```
69+
70+
Always use `--render-html` so the HTML renders as-is in the Plannotator UI with theme inheritance and annotation support. Do NOT use `open` or `xdg-open` directly.
71+
72+
## What visual-explainer provides (do not duplicate)
73+
74+
All of these come from visual-explainer — read them there, don't reinvent them:
75+
- Diagram type routing (architecture, flowchart, sequence, ER, state, mind map, etc.)
76+
- Mermaid integration (theming, zoom controls, scaling, layout direction)
77+
- CSS structural patterns (ve-card, grids, connectors, depth tiers, collapsibles)
78+
- Slide deck mode (viewport-snapping presentations)
79+
- Data table patterns (sticky headers, status indicators, responsive scrolling)
80+
- Anti-slop rules (forbidden fonts, colors, animations, patterns)
81+
- Quality checks (squint test, swap test, overflow protection)
82+
- Animation guidelines (staggered entrance, reduced-motion support)
83+
84+
## Plan-specific guidance
85+
86+
When the output is an implementation plan, design doc, or proposal:
87+
88+
**Adapt the visual vocabulary to the task:**
89+
- **Backend/API work**: Lead with data flow diagrams, schemas, API signatures
90+
- **Frontend/UI work**: Lead with mockups, component hierarchy, state flow
91+
- **Infrastructure/DevOps**: Lead with architecture diagrams, deployment flow
92+
- **Refactoring**: Lead with before/after diagrams showing structural change
93+
- **Cross-cutting features**: Lead with a system map showing all touchpoints
94+
95+
**Section menu — pick what fits:**
96+
Solution overview, architecture/data flow diagram, UI mockups, key code, integration points, risks & mitigations, open questions, considerations & rationale, reusability & code quality. Not every plan needs every section — choose what serves the content.
97+
98+
**What NOT to include in plans:**
99+
- Time estimates (timelines showing sequence are fine, hour/day estimates are not)
100+
- Boilerplate sections that would just say "N/A"
101+
- Exhaustive file lists — show the important files, not every file touched
102+
103+
**Quality bar for plans:** The plan should answer "what are we building, why, and how" within 30 seconds of reading.
104+
105+
## PR explainer guidance
106+
107+
When the output is a PR walkthrough, diff review, or code change explainer:
108+
109+
- **TL;DR first** — a bordered card summarizing what the PR does and why, so readers who skim get the gist
110+
- **Risk map** — visual chips showing which files need careful review vs. which are mechanical
111+
- **Inline diffs** — use the diff rendering pattern from `references/extended-patterns.md` for important hunks (not every hunk)
112+
- **File-by-file commentary** — collapsible cards per file with a "why" paragraph explaining the purpose of changes
113+
- **"Where to focus"** — numbered callouts telling reviewers exactly what to look at and why
114+
- **Before/after comparison** — two-column grid for behavior changes
115+
116+
See `references/extended-patterns.md` for diff rendering, review comment bubbles, and file badge patterns.
117+
118+
## What this skill adds
119+
120+
- Plannotator theme tokens (colors, typography, radii) — see `references/theme-override.md`
121+
- Extended component patterns (timelines, code blocks, risk tables, SVG diagrams, open questions) — see `references/extended-patterns.md`
122+
- Plan-specific guidance (section menu, adaptation by task type, quality bar)
123+
- `--render-html` delivery with annotation support and theme inheritance
124+
- Design philosophy emphasizing spatial layout, breathing room, and visual hierarchy

0 commit comments

Comments
 (0)