* fix: don't ask agent to address feedback on LGTM approval (#284)
When the reviewer approves with no annotations, send a neutral
"Code review completed — no changes requested." message instead of
the contradictory "LGTM" + "Please address this feedback."
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use explicit approved flag instead of annotations.length heuristic
The previous fix inferred LGTM from an empty annotations array, but
VS Code editor annotations are carried in feedbackMarkdown without
populating the annotations array — causing real review comments to be
misclassified as approvals. Thread an explicit `approved` boolean from
the UI through the review server to all three integrations.
Closes#284
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: append assertive instruction to review feedback output
When the reviewer submits actual feedback, append "The reviewer has
identified issues above. You must address all of them." so the agent
treats annotations with urgency rather than soft-acknowledging them.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: annotate unused LGTM feedback string
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The annotate server was missing the /api/doc endpoint, so clicking
.md links (e.g., [see other](sibling.md)) in annotated files showed
"Failed to connect to server" — the request fell through to the SPA
catch-all and returned HTML instead of JSON.
Three changes:
1. annotate.ts — Add /api/doc route, auto-injecting the source
file's parent directory as ?base= so relative links resolve
from the correct location.
2. reference-handlers.ts — handleDoc() now accepts an optional
?base= query param and tries resolving relative paths against
it before falling back to the project-root glob search.
3. App.tsx — handleOpenLinkedDoc passes the current file's
directory as ?base= when navigating linked-doc → linked-doc,
so chained relative links resolve correctly.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Adds full inline image rendering to the Viewer for both annotate and
plan review modes. Previously,  syntax was treated as plain
text.
## What changed
### Viewer.tsx
- Added `` regex to InlineMarkdown before the link pattern.
Images starting with `!` were silently falling through to plain text
because `!` was not in the special-character scan set — added it.
- Added `imageBaseDir` prop (flows Viewer → BlockRenderer → InlineMarkdown)
so relative image paths in markdown resolve against the annotated file's
directory rather than the server's cwd.
- Added `onImageClick` callback (same prop chain) to open lightbox on click.
- Added `ImageLightbox` component: full-screen dark backdrop, image scaled
to 90vw/85vh with object-contain, alt text caption, closes on Escape or
backdrop click. Portaled to document.body.
- Images render with `cursor-zoom-in` to hint they're clickable.
### App.tsx
- Captures `filePath` from `/api/plan` response (already sent by annotate
server) and derives `imageBaseDir` (directory of the markdown file).
- Passes `imageBaseDir` to `<Viewer>`.
### ImageThumbnail.tsx
- `getImageSrc()` now accepts an optional `base` parameter. For relative
local paths, appends `&base=<dir>` to the `/api/image` query string.
### shared-handlers.ts
- `handleImage` falls back to resolving the path relative to a `base`
query param when the primary path doesn't exist and the path is relative.
Covers the common case of `` in a markdown
file opened from an arbitrary directory.
## Behavior
- Remote URLs (https://...) render directly, no proxy.
- Absolute local paths route through /api/image as before.
- Relative paths resolve against the markdown file's directory via ?base=.
- Click any image → lightbox. Escape or click outside → dismiss.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
* fix(annotate): support Windows markdown paths in CLI annotate flow
* fix tmp path
* fix resolve-file.ts
* Update command to use EXE_PATH variable
* Update command path for plugin hooks in install.ps1
* test: add core test suite for path resolution, storage, remote detection, and install scripts
- resolve-file: absolute paths, relative paths, case-insensitive search, ignored dirs, extension filtering, ambiguity, Windows separators
- storage: slug generation, tilde expansion, version history, deduplication
- remote: env var detection (PLANNOTATOR_REMOTE, SSH_TTY), port config and validation
- image: tmpdir usage, extension validation
- install scripts: JSON structure validation, checksum verification, arch detection, full exe path in hooks
79 tests, 35ms
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* ci: add test workflow and gate release builds on tests
- New test.yml: runs `bun test` on PRs and pushes to main
- release.yml: tests must pass before build job runs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve pre-existing test failures for CI compatibility
- project.test.ts: make repo name assertion portable (works in CI where
checkout dir differs from local dev)
- vscode mock: add missing APIs needed by editor-annotations.ts
(comments, languages, Range, CodeActionKind, decorations)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: remove Viewer.test.tsx and happy-dom dependency
Tests didn't exercise any application code — they manually constructed
DOM elements inline and verified DOM API behavior, not Viewer.tsx logic.
The mock highlighter didn't simulate real hljs, and one test could never
fail due to its try/catch structure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use targeted markdown glob and restore clean lockfile
- Replace **/* glob with **/*.[mM][dD]{,[xX]} to only scan markdown
files during case-insensitive search (avoids iterating every file)
- Restore package.json key ordering from main, only removing happy-dom
- Regenerate bun.lock from main's base to eliminate version drift
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: drop redundant test and fix weak assertion
- Remove redundant PLANNOTATOR_REMOTE=TRUE test (already covered by true)
- Fix UPLOAD_DIR assertion that would pass even with hardcoded /tmp
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add Pi origin support to code review UI and update banner
Pi users were seeing "OpenCode" badge and getting the wrong update
command. Adds 'pi' to origin types in review/annotate servers, shows
violet "Pi" badge in code review header, and copies the correct
`pi install` command in the update banner.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: update Pi extension from install scripts when pi is available
The install scripts now check if `pi` is on PATH and run
`pi install npm:@plannotator/pi-extension` to update the extension.
Silent for users without Pi installed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add per-file git add from code review UI (#254)
Adds the ability to `git add` individual files directly from the code
review interface. Users can stage approved files then switch to the new
"Unstaged changes" diff view to see only remaining work.
- POST /api/git-add endpoint with worktree support
- gitAddFile/gitResetFile utilities in packages/server/git.ts
- "Staged changes" and "Unstaged changes" added to diff type dropdown
- useGitAdd hook encapsulating all staging state and API logic
- "Git Add" / "Added" toggle button in FileHeader (next to Viewed)
- Visual indicator (left border) for staged files in sidebar FileTree
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: use green background tint for staged files instead of left border
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: support staged/unstaged diffs in worktrees and display stage errors
Adds "staged" and "unstaged" to worktree sub-type allowlists (server parser,
client parser, and runGitDiff switch), displays stageError in FileHeader,
and documents /api/git-add in CLAUDE.md.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: clean up git add feature — fix stale closure, timeout leak, dedup validation
- useGitAdd: use ref for stagedFiles to stabilize stageFile callback,
clear error timeout on reset, drop unnecessary useMemo
- App.tsx: wrap onFileViewed in useCallback
- FileTree: merge duplicate staged count conditionals
- review.ts: reuse exported validateFilePath for /api/file-content
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add capture-phase paste listener to AttachmentsButton so users can
Cmd+V / Ctrl+V images directly into per-annotation attachments when
the popover is open. Uses stopPropagation to prevent the global paste
handler in App.tsx from also processing the event. Adds a platform-aware
keyboard shortcut hint in the drop zone.
Also fixes a regression from #237 where resolveMarkdownFile rejected
absolute paths outside the project root. The original behavior allowed
any absolute path the user explicitly provided; the security boundary
should only apply to relative path / bare filename search.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: split worktree/diff-type into separate controls
The single dropdown that mixed diff type selection (uncommitted, last-commit,
branch) with worktree switching was confusing — entering a worktree silently
replaced all dropdown options with no persistent indicator of which worktree
was active.
Split into two controls:
- Context dropdown: switches between main repo and worktrees (only shown
when worktrees exist, highlighted when a worktree is active)
- View dropdown: always shows the same diff type options regardless of context
Server changes:
- GitContext now exposes worktrees as a separate field
- Removed getWorktreeDiffOptions() and back-to-main sentinel handling
- Simplified /api/diff/switch (no more diffOptions replacement)
Client changes:
- New activeWorktreePath state + handleWorktreeSwitch callback
- handleDiffSwitch composes worktree prefix from context automatically
- Derived activeDiffBase strips worktree prefix for dropdown display
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: derive activeWorktreePath from diffType instead of separate state
Eliminates redundant state — activeWorktreePath and activeDiffBase are
now both derived in a single useMemo from the composite diffType string.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move DiffOption, WorktreeInfo, GitContext to @plannotator/shared
Eliminates duplicate interface definitions across server and client.
Types now live in packages/shared/types.ts and are imported by both
packages/server/git.ts and packages/review-editor/.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: expandable diff context in code review (closes#243)
Switch from PatchDiff to FileDiff component from @pierre/diffs to enable
GitHub-style "show more lines" buttons between hunks. The library handles
all expansion UI when provided full file contents via oldLines/newLines.
- Add /api/file-content endpoint to serve old/new file content per diff type
- Add getFileContentsForDiff() helper with ref mapping for all diff types
- Parse patch client-side via getSingularPatch(), augment with file contents
- Update @pierre/diffs from 1.0.4 to 1.0.11 (scroll sync fix, no breaking changes)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: upgrade @pierre/diffs to beta, re-parse patch with full file contents
The previous approach spread file content arrays onto a partial-mode
FileDiffMetadata, causing hunk index mismatches and Shiki decoration
errors. Now uses processFile() to re-parse the patch with oldFile/newFile
so hunk indices are computed against the full file (isPartial: false),
which is required for expansion to work correctly.
Also fixes a flash error on file switch by tagging fileContents with the
filePath they were fetched for, preventing stale contents from being
paired with the wrong patch during the render before useEffect fires.
- Upgrade @pierre/diffs from ^1.0.x to ^1.1.0-beta.19 in all workspaces
- Add test fixtures for disjoint hunks, deleted/renamed/new files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: abort stale file-content fetches, validate path params
- Add AbortController to file-content fetch effect to cancel in-flight
requests when switching files
- Reject path traversal (.. or absolute paths) on /api/file-content
- Document /api/file-content endpoint in CLAUDE.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add shared EditorAnnotation type
Single source of truth for the editor annotation interface,
imported by both @plannotator/server and @plannotator/ui.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: editor annotation server endpoints
In-memory store with POST/GET/DELETE endpoints for editor annotations.
The array lives in the handler closure and dies with the server session.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: editor annotation UI — polling hook, card, panel, and export
- useEditorAnnotations hook with auto-disable polling (500ms interval)
- EditorAnnotationCard with file path, code preview, and comment
- AnnotationPanel conditionally renders editor annotation section
- exportEditorAnnotations formats annotations for Claude feedback
- App.tsx wires hook, includes in output memo and send feedback gate
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: VS Code extension editor annotation command
Cmd+Shift+. or right-click to capture selected text as an annotation.
POSTs through the cookie proxy to the plannotator server. Adds amber
left-border decorations on annotated lines, cleared on panel close.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: inline comment threads, lightbulb menu, and stronger decorations
Replace showInputBox with VS Code CommentController for inline annotation
threads anchored to selected code. Add CodeActionProvider for lightbulb
discoverability. Stronger decoration styling with gutter icon.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: VS Code theme integration for webview
Bridge VS Code CSS variables to Plannotator's CSS variable system via
postMessage between wrapper page and proxied iframe. Automatically adopts
the active VS Code color theme (dark/light/custom) without touching any
UI components or CSS files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: gate editor annotation polling behind VS Code detection
Only poll /api/editor-annotations when running inside a VS Code webview
(window.__PLANNOTATOR_VSCODE). Browser and shared URL users now have
zero network cost from the editor annotations feature. Also fixes poll
interval from 500ms to 2000ms.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: faster polling, prevent unnecessary re-renders, unify HTTP helper
- Poll interval 2s → 500ms for snappier annotation pickup
- Shallow equality check on annotation IDs prevents React re-renders
when poll data is unchanged
- Merge postToProxy/deleteFromProxy into single requestProxy function
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: editor annotations support in code review
Wire existing editor annotation infrastructure into the review flow
so VS Code users can annotate files outside the diff during code review.
- Add editor annotation endpoints to review server (same 3-line pattern)
- Call useEditorAnnotations hook in review App.tsx
- Display editor annotations in ReviewPanel with "Editor" divider
- Include editor annotations in feedback export and gating logic
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
feat: support git worktrees in code review (#196)
Adds full worktree support to the code review flow:
- Detects worktrees via `git worktree list --porcelain` and surfaces them in the diff dropdown
- Selecting a worktree enters "worktree mode" with scoped diff options (uncommitted, last commit, vs main)
- "Back to main repo" restores the original dropdown
- Handles initial commits gracefully (no HEAD~1 crash)
Closes#196
* feat: add --browser CLI flag and session discovery (#135)
- Add `--browser <name>` global flag to override which browser opens
(sets PLANNOTATOR_BROWSER, works with existing openBrowser() logic)
- Add `plannotator sessions` subcommand to list active server sessions
with `--open [N]` to reopen in browser and `--clean` to prune stale entries
- Track active sessions in ~/.plannotator/sessions/<pid>.json with
automatic stale cleanup via PID liveness checks
- Register/unregister sessions in all three server modes (plan, review, annotate)
- Export ./sessions and ./project from @plannotator/server package
Closes#135
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add integration test for session discovery
Non-interactive test script that validates:
- Session file creation/cleanup lifecycle
- Session file content (all fields)
- `plannotator sessions` listing, --open, --clean
- Stale PID auto-cleanup
Also syncs bun.lock versions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add --browser flag and session discovery to configuration page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add troubleshooting page
Covers lost tabs (sessions --open), data storage locations,
browser issues, and hook not firing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: harden title sanitization for Obsidian filenames
Strip parentheses, brackets, curly braces, hash, tilde, and backtick
from plan titles when generating Obsidian filenames. Re-trim after
stripping to handle edge cases where stripped chars leave leading/trailing
whitespace. Closes part of #235.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: filename separator + auto-save on arrival for Obsidian
Filename separator: adds a dropdown in Obsidian settings to replace
spaces with dashes or underscores in generated filenames. Fixes issues
with CLI tools fumbling space-escaped paths inside Obsidian vaults.
The setting flows through the save request to generateFilename() as a
post-processing step. Live preview reflects the choice.
Auto-save on arrival: adds an opt-in toggle that automatically saves
plans to Obsidian the moment they load in the browser, before the user
approves or denies. This fixes the workflow where approving from the
Claude Code CLI kills the hook server before the user can Cmd+S.
Guarded by a ref to fire only once per session.
Closes#235, closes#228.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: pass filenameSeparator in ExportModal Obsidian save path
The ExportModal's save-to-Obsidian callsite was missing the
filenameSeparator config, causing the separator preference to be
silently ignored when saving via the Export Modal.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add case-insensitive file resolution so users can open markdown files
without typing exact paths. Applies to both the CLI (`plannotator
annotate`) and the `/api/doc` endpoint for linked documents.
Resolution strategies (tried in order):
1. Exact path — absolute or relative to cwd (existing behavior)
2. Case-insensitive relative path — `docs/setup.md` matches `docs/SETUP.md`
3. Bare filename search — `setup.md` searches the entire project tree
Ambiguity handling:
- 1 match → opens the file, prints "Resolved: /full/path"
- 0 matches → "File not found: <input>"
- 2+ matches → "Ambiguous filename: found N matches" with full paths
Skips node_modules, .git, dist, build, .next, __pycache__, .obsidian,
.trash during search. Restricts results to .md/.mdx/.markdown files
and enforces project root boundary (no path traversal).
Extracted shared resolveMarkdownFile() into packages/server/resolve-file.ts
and refactored /api/doc handler to use it, removing ~40 lines of inline
resolution logic.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
* feat: integrate VS Code extension from community (7tg/plannotator-vscode)
Opens Plannotator plan reviews, code reviews, and annotations inside VS Code
tabs instead of an external browser. Intercepts PLANNOTATOR_BROWSER via env
var injection in integrated terminals, with cookie persistence and auto-close.
Original repository: https://github.com/7tg/plannotator-vscodeCloses#91
Co-Authored-By: Barbaros Gören <tayyipgoren@gmail.com>
* chore: adapt VS Code extension for monorepo
- Update package.json: publisher → backnotprop, repo → plannotator, add private
- Replace node -e URL encoding with curl --data-urlencode in router script
- Simplify panel-manager tests to keep only behavioral tests
- Add dev:vscode, build:vscode, package:vscode scripts to root
- Add *.vsix to .gitignore
- Update bun.lock with new workspace member
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address code review and duplicate panel issue
- Fix macOS browser: detect script paths in PLANNOTATOR_BROWSER and
invoke directly instead of via 'open -a' (which only accepts app
names/.app bundles)
- Update CLAUDE.md: add vscode-extension to project structure,
development, and build sections
- Fix command title: 'Simple Browser' -> 'Open URL in Editor'
- Add 1s debounce to panel opens: Claude Code fires ExitPlanMode hook
twice, spawning two plannotator processes on different ports that
both hit the IPC server simultaneously
- Add .vscode/launch.json + tasks.json for F5 debugging
---------
Co-authored-by: Barbaros Gören <tayyipgoren@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: auto-save annotation drafts to survive server crashes
Adds server-side draft persistence so annotations are not lost when the
server process dies. Drafts are saved to ~/.plannotator/drafts/ keyed by
a SHA-256 content hash, debounced at 500ms. On reload, a dialog prompts
the user to restore or dismiss. Drafts are auto-deleted on approve/deny/feedback.
Closes#212
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: remove duplicate imports and update CLAUDE.md for draft feature
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: wrap handleRestoreDraft in useCallback and update shared-handlers comment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract shared route handlers and modularize plan server
Pull duplicated /api/image, /api/upload, /api/agents handlers and
handleServerReady into shared-handlers.ts. Move /api/doc and Obsidian
reference endpoints into reference-handlers.ts. Eliminates ~120 lines
of verbatim duplication across index.ts, review.ts, and annotate.ts.
Closes#210
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: deduplicate OpencodeClient type and fix stale module comment
Export OpencodeClient interface from shared-handlers.ts and import it
in index.ts and review.ts instead of redefining inline. Also corrects
the module doc comment about /api/agents usage scope.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add Obsidian vault browser sidebar tab (#207)
Add a third sidebar tab that lets users browse their Obsidian vault's
file tree, click any markdown file to open and annotate it as part of
plan feedback. Includes wikilink support for navigating between vault
files.
- New /api/reference/obsidian/files and /api/reference/obsidian/doc endpoints
- useVaultBrowser hook and VaultBrowser sidebar component
- Wikilink [[filename]] and [[filename|display]] detection in Viewer
- "Vault Browser" toggle in Settings > Obsidian
- Bare filename search for wikilink resolution within vault
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update docs and clear vault state on disable
- Add new reference endpoints to CLAUDE.md API table
- Update stale doc comments referencing "two tabs" in sidebar files
- Clear vaultPath and activeFile when vault browser is disabled
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: simplify vault browser prop passing and derived state
- Move VaultNode to packages/ui/types.ts (shared type location)
- Pass useVaultBrowser return as single object prop through SidebarContainer
- Replace showVaultTab/vaultPath useState with useMemo derivations
- Extract buildVaultDocUrl helper to deduplicate URL template
- Wrap onVaultFetchTree in useCallback
- Fix toggle switch size to match existing toggles (h-6 w-11)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add customizable filename format for Obsidian export (#167)
Add a filename format setting to the Obsidian integration that supports
template variables ({title}, {YYYY}, {MM}, {DD}, {Mon}, {D}, {HH}, {h},
{hh}, {mm}, {ss}, {ampm}). Default format preserves existing behavior.
Settings UI shows a live preview of the resulting filename.
https://claude.ai/code/session_01QycvXHHvAy38N36PVP3jFF
* fix: guard against double .md extension in custom filename format
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* 🐛 fix: force standard a/b diff prefix to handle mnemonic prefix config
When diff.mnemonicPrefix is enabled in git config, `git diff` uses
context-dependent prefixes (c/ for commit, w/ for worktree, i/ for
index) instead of the standard a/b. Both the internal parseDiffToFiles
and @pierre/diffs library expect a/b prefixes, causing silent parse
failures that result in an empty file list.
Add --src-prefix=a/ --dst-prefix=b/ to all git diff invocations to
ensure consistent output regardless of user git configuration.
* ✨ feat: propagate git diff errors to review UI
Previously, git diff errors were silently caught and returned as empty
patches. The UI showed "No changes" with no indication of failure.
- Add error field to DiffResult and propagate through ReviewServerOptions
- Include error in /api/diff and /api/diff/switch responses
- Show distinct error state in review UI (red icon + error message)
- Clear/set error state on diff type switch
* Fix mnemonic prefix handling in pi-extension runGitDiff
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Add short URL sharing via paste service to fix URL truncation on Slack/messaging apps
Share URLs for large plans can be 10-40KB+ because the entire plan + annotations
are compressed into the URL hash. Services like Slack, WhatsApp, and Twitter
truncate these URLs, making shared plans unviewable (related: #142).
This adds an optional paste-service backend that stores compressed payloads and
returns short ~60-char URLs (e.g. share.plannotator.ai/p/aBcDeFgH).
Changes:
- Add Cloudflare Worker paste service (apps/paste-worker/) with KV storage
and 90-day TTL for stored plans
- Add createShortShareUrl() and loadFromPasteId() to sharing utils
- Update useSharing hook to auto-generate short URLs with 1s debounce
- Update ExportModal to show short URL as primary copy target with
full hash URL as backup
- Portal automatically supports /p/<id> paths via useSharing hook
- Fully backward compatible: hash-based URLs continue to work unchanged
- Graceful degradation: falls back to hash URLs if paste service is unavailable
* Fix critical bugs found during code review of short URL feature
- Worker: return only { id } so client constructs URL with its own
shareBaseUrl (fixes self-hosted deployments)
- importFromShareUrl: handle /p/<id> short URLs in addition to hash
URLs (fixes teammate import via short links)
- Anchor /p/<id> regex with ^ to prevent false matches on nested paths
- Pass shareBaseUrl to loadFromPasteId (was always defaulting)
- replaceState preserves base path instead of hardcoding /
- Clear stale shortShareUrl immediately on debounce to prevent showing
outdated link during the 1s delay
- Add shareBaseUrl to dependency arrays for loadFromHash and
importFromShareUrl callbacks
- Remove unused url field fallback from paste API response parsing
* Fix pasteApiUrl/shareBaseUrl confusion and add remote session share URLs
Bug fix (addresses @backnotprop's code review):
- useSharing hook now accepts separate `pasteApiUrl` parameter instead of
incorrectly passing `shareBaseUrl` to `loadFromPasteId`. These are
different domains (share portal vs paste backend). When omitted, the
default `https://paste.plannotator.ai` is used correctly.
- Fixed in all three call sites: loadFromHash, importFromShareUrl, and
generateShortUrl (via createShortShareUrl options).
Remote session share URLs (implements #192):
- When running on a remote instance (SSH, devcontainer), Plannotator now
generates a share.plannotator.ai URL and prints it to stderr so the
user can open the plan review in their local browser.
- Works for all three modes: plan review, code review, and annotate.
- Uses the same deflate-raw + base64url encoding as the client.
- Fails silently if URL generation fails — port forwarding still works.
- New server utility: packages/server/share-url.ts
* Address code review findings: fix spread limit, CORS, deps array
- share-url.ts: Replace btoa(String.fromCharCode(...compressed)) with
a loop to avoid RangeError on plans >65K compressed bytes
- share-url.ts: Add cross-reference comment noting the server-side
SharePayload is an intentional subset of the canonical UI type
- paste-worker: Only return CORS headers for allowed origins; disallowed
origins get no CORS headers instead of misleading partial headers
- useSharing.ts: Remove unused shareBaseUrl from loadFromHash dependency
array (loadFromHash only uses pasteApiUrl, not shareBaseUrl)
* Restructure paste service with pluggable storage, fix consent flow and security issues
Replaces monolithic paste-worker with multi-target paste-service architecture:
- Core logic (handler, storage interface, CORS) separated from deployment targets
- Filesystem store (self-hosted), KV store (Cloudflare), S3 stub (future)
- Bun binary target + Cloudflare Worker target
- Fix auto-upload removed: short URLs only created on explicit user click
- Clear stale short URL state when content changes
- Fix require('fs') in ESM module, add path-traversal guard in FsPasteStore
- Return 400 (not 500) for malformed JSON in both targets
- Fix compress() spread limit for large plans
- Updated docs: self-hosting guide, sharing guide, env vars, API endpoints
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Simplify paste service: extract shared router, remove TOCTOU patterns
- Extract duplicated HTTP routing from bun.ts and cloudflare.ts into
handleRequest() in core/handler.ts — targets are now thin wrappers
- Move ID_PATTERN regex to core (was duplicated in both targets)
- Remove existsSync guard before mkdirSync({recursive:true}) in fs.ts
- Remove file.exists() check before file.json() in fs.ts get() — the
try/catch already handles missing files
- Extract DEFAULT_SHARE_BASE constant in sharing.ts (was hardcoded twice)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Deduplicate compression, image parsing, and remote share URL logic
- Create packages/shared/ with compress/decompress (single source of truth)
— both @plannotator/server and @plannotator/ui import from here
- Export parseShareableImages from sharing.ts, remove duplicate
parseGlobalAttachments from useSharing.ts
- Extract writeRemoteShareLink() helper in share-url.ts, replace 3
near-identical blocks in apps/hook/server/index.ts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix modulo bias in paste ID generation and remove nonexistent doc artifacts
- Use rejection sampling in generateId() to eliminate modulo bias
(bytes >= 248 discarded, uniform distribution over 62 chars)
- Remove references to install-paste.sh and Docker image that don't
exist yet; point self-hosting docs to GitHub Releases binaries
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: open linked .md files in read-only tabs with TOC navigation
When a plan references local .md files via markdown links, they now open
in a new browser tab using the full Plannotator viewer in read-only mode.
Changes:
- server: add /api/doc?path= endpoint with 4-strategy path resolution
(absolute, relative to project root, bare filename search). Returns 400
with match list on ambiguous filenames, 404 when not found, 403 on
path traversal. Only resolves files within process.cwd().
- Viewer: detect local .md links in InlineMarkdown and route them to
/?doc=<path>&readonly=true with a new-tab icon. Add isReadOnly prop
that guards the web-highlighter init (prevents phantom highlights) and
suppresses annotation toolbars.
- App: parse ?doc= URL param on load and fetch from /api/doc instead of
/api/plan. Set isReadOnly mode which shows a read-only banner, hides
approve/deny/feedback buttons, ModeSwitcher, Settings, AnnotationPanel,
and annotation panel toggle. Shows an error view (not a silent fallback)
when the document cannot be found or is ambiguous. Opens the sidebar to
the TOC tab automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: upgrade linked docs to same-view annotatable navigation with aggregated feedback
Replace the new-tab read-only approach with same-view navigation: clicking a
.md link swaps the plan content in-place, preserving all annotations via a
docCache. Users can annotate linked docs and all feedback (plan + linked docs)
is aggregated into the deny/approve payload sent to Claude.
Key changes:
- New useLinkedDoc hook for state swapping, caching, and highlight restoration
- React key on Viewer forces clean unmount/remount (fixes web-highlighter DOM crash)
- exportLinkedDocAnnotations() aggregates linked doc feedback with filepath context
- UI: 2px primary border, "Linked File" badge, "Copy file" button, TOC filepath
indicator with back-to-plan button
- Fix Tailwind v4 cascade: move * { border-color } to @layer base so utility
classes like border-primary actually override it
- Compact TOC spacing (padding, gaps, line-height) without affecting Version Browser
- Exclude node_modules/.git from /api/doc bare filename glob
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: harden /api/doc endpoint and align linked doc annotation output
Restrict /api/doc to .md/.mdx files only, fix heading hierarchy and
sorting in exportLinkedDocAnnotations, and update CLAUDE.md docs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: complete linked doc annotations in getDocAnnotations and fix useMemo deps
- getDocAnnotations() now includes the active linked doc's live
annotations alongside the cache, removing the implicit requirement
to call back() before reading
- Remove unstable linkedDocHook object from annotationsOutput useMemo
dependency array (was defeating memoization every render)
- Guard Cmd+Enter shortcut against linkedDocHook.isActive to prevent
accidental approve/deny while viewing a linked doc
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: include linked doc annotations in approve feedback gate
The handleApprove gate only checked plan-level annotations, silently
dropping linked-doc-only feedback on "approve with notes" (OpenCode).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add getDocAnnotations to annotationsOutput useMemo deps
Ensures memo recomputes when navigating to/from linked docs, preventing
stale annotation output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
* Add feature plan for VS Code diff viewer button
Scoping document for a new PlanDiffViewer button that opens
the plan diff in VS Code's native diff viewer via `code --diff`.
https://claude.ai/code/session_01FYcJkGsWfuiGy53x3pwCSW
* feat: add "Open in VS Code" button to plan diff viewer
Add a button in the PlanDiffViewer toolbar that opens the current
plan diff in VS Code's native side-by-side diff viewer. The server
writes both versions to temp files and spawns `code --diff`.
- New POST /api/plan/vscode-diff endpoint in packages/server/index.ts
- VS Code button with loading/error states in PlanDiffViewer.tsx
- Wire currentPlan/basePlan/baseVersion props from App.tsx
https://claude.ai/code/session_01FYcJkGsWfuiGy53x3pwCSW
* chore: update bun.lock after install
https://claude.ai/code/session_01FYcJkGsWfuiGy53x3pwCSW
* refactor: server reads plan versions directly instead of receiving from client
The server already has the current plan in its closure and can read
any version from disk via getPlanVersion(). No need to send plan
text from the UI — just send baseVersion.
https://claude.ai/code/session_01FYcJkGsWfuiGy53x3pwCSW
* chore: remove plan.md scoping document from repo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: extract editor diff logic into shared packages/server/editor.ts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: use official VS Code SVG icon in plan diff viewer button
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: use history file paths directly for VS Code diff instead of writing temp files
Both plan versions already exist on disk in ~/.plannotator/history/.
Eliminates redundant file writes, fixes UPLOAD_DIR semantic misuse,
and corrects HTTP status codes for editor errors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: rename editor.ts to ide.ts to avoid confusion with plan editor UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add ide.ts and vscode-diff endpoint to CLAUDE.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add plan diff UI with sidebar, badge, and dual view modes
Shows what changed between plan iterations when Claude revises after
feedback. Adds a +N/-M badge below repo info that toggles the diff view,
a shared left sidebar with TOC and Version Browser tabs, and two diff
modes: rendered (color-coded borders) and raw markdown (+/- lines).
Closes#138, closes#111
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update CLAUDE.md project structure and align first-run dialog labels
- Add plan-diff/ and sidebar/ component subdirectories to CLAUDE.md
- Add new hooks and utils to CLAUDE.md project structure
- Rename "Table of Contents" to "Auto-open Sidebar" in UIFeaturesSetup
to match Settings.tsx label
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address code review findings for plan diff UX
- Fix badge stats mixing block counts with line counts (modifications
now fold into additions/deletions)
- Gate hasPreviousVersion on diffBasePlan being loaded to prevent
"Show Changes" no-op and ModeSwitcher disappearing
- Make sidebar reactive to Settings toggle (useEffect on tocEnabled)
- Match PlanDiffViewer badge layout to Viewer (flex-col) so badge
doesn't jump position on toggle
- Add "Exit Diff" label to the close button in diff view
- Remove dead CSS (plan-diff-removed-marker, plan-diff-modified)
- Clean up stale header comment and unused lines prop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: second-round review cleanup for plan diff UX
- Fix stale "amber border" JSDoc in PlanCleanDiffView (actually green)
- Rename sidebar tab from "diff" to "versions" for clarity
- Gate VersionBrowser fetch on versionInfo being available
- Move .sidebar-tab-flag CSS into its own Sidebar section
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add loading state for version selection in sidebar
Add isSelectingVersion to selectBaseVersion, mirroring the existing
isLoadingVersions pattern. Shows "Loading..." on the selected version
button while the fetch is in progress.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address third-round code review findings
- Fix duplicate border/backdrop on TOC inside sidebar (className override)
- Fix loading indicator targeting wrong version button (fetchingVersion state)
- Fix "Show Changes" button silent no-op (gate on hasPreviousVersion)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move date to slug suffix, improve Other Plans UX
- Slug format changed from YYYY-MM-DD-{heading} to {heading}-YYYY-MM-DD
- Other Plans: single "coming soon" banner instead of per-item labels
- Strip date suffix from plan names in sidebar for readability
- Remove cursor-not-allowed from Other Plans items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add Plan Diff section to CLAUDE.md, alert on version fetch failure
- Document plan diff feature: engine, view modes, state management, sidebar
- Update slug format documentation to {heading}-YYYY-MM-DD
- Show native alert when version fetch fails instead of silent swallow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add table rendering to clean diff view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add plan version history (backend + API)
Save plan versions to ~/.plannotator/history/{project}/{slug}/ on every
plan arrival. Serves previousPlan and versionInfo via /api/plan, and adds
GET /api/plan/version?v=N for fetching any historical version. Enables
future diff UI. Deduplicates identical resubmissions.
Closes#138, closes#111
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add version listing and project history APIs
Add GET /api/plan/versions to list all versions of the current plan with
timestamps, and GET /api/plan/history to list all plans in the current
project. Both endpoints available in shared server and pi extension.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add version history section and API endpoints to CLAUDE.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: rename misleading "diff" terminology to "annotations" in plan review context
The plan review flow used "diff" naming (exportDiff, diffOutput, .diff.md,
"Raw Diff", "Download .diff") for what is actually user annotations/feedback
on a plan. This was confusing since the code review flow legitimately uses
"diff" for actual git diffs. Renames all plan-review-context "diff" references
to "annotations" across code, UI labels, file extensions, and docs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update stale exportDiff references in ANNOTATE.md
Missed during the diff→annotations rename. Updates two references
to exportDiff() → exportAnnotations() in the annotate flow docs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: rename remaining "Download Diff" label and tab type in App.tsx
The quick-save dropdown button still showed "Download Diff" and the
initialExportTab state type still used 'diff' instead of 'annotations'.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The /api/image endpoint serves files from the local filesystem based on
a user-provided path parameter with no validation. This allows any
process on localhost to read arbitrary files (e.g., /etc/passwd,
~/.ssh/id_rsa) by requesting /api/image?path=<any-path>.
This commit adds:
- Path validation requiring image file extensions (png, jpg, gif, etc.)
- Path resolution via path.resolve() to normalize traversal sequences
- Upload extension validation against the same image allowlist
- Shared image.ts module used by all three servers (plan, review, annotate)
[agent]
* fix(browser): use open -a only for PLANNOTATOR_BROWSER on macOS
BROWSER env var follows Unix convention (executable + URL as arg),
so BROWSER=open correctly resolves to `open <url>` instead of
the broken `open -a open <url>`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(browser): use cmd.exe only for PLANNOTATOR_BROWSER on WSL
Same fix as the macOS open -a change: when only BROWSER is set (e.g.
VS Code devcontainer helper script), execute it directly instead of
routing through cmd.exe /c start, which can't run Linux paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
* feat: use BROWSER env var as fallback for opening UI
Closes#119
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add missing reference docs (were gitignored by reference/ pattern)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>