* feat: plan archive browser with in-session sidebar tab (#362)
Add `plannotator archive` subcommand and archive sidebar tab for browsing
saved plan decisions from ~/.plannotator/plans/. Plans show approved/denied
badges and timestamps. In-session mode uses the linked doc overlay pattern
so users can reference old plans while reviewing a current one.
- New archive server (packages/server/archive.ts) following annotate pattern
- New ArchiveBrowser sidebar component, reusable in both contexts
- Archive listing/parsing functions in storage.ts (reads decision snapshots)
- Archive endpoints on plan server for in-session use (/api/archive/plans)
- Remove dead "Other Plans" UI, projectPlans state, /api/plan/history endpoint
- Fix resize handle touch area covering scrollbars in sidebar/main content
- Fix sidebar tab bar overflow when narrow
For provenance purposes, this commit was AI assisted.
* fix: code quality sweep for plan archive
- Remove `as any` cast: add "archive" to SessionInfo.mode union
- Replace inline import() type with proper import for ArchivedPlan
- Replace any[] with ArchivedPlan[] in fetch response types
- Fix infinite re-fetch when archive is empty (use hasFetched ref)
- Cache archive plan list in plan server (avoid re-scanning filesystem)
- Document ResizeHandle side prop behavior
- Remove redundant comment on Viewer archiveInfo prop
For provenance purposes, this commit was AI assisted.
* chore: remove dead marketing components
Step.astro and Landing.astro are unused — landing page inlines
step markup and pages use Base.astro directly.
For provenance purposes, this commit was AI assisted.
* fix: address code review findings for plan archive
- Path traversal: use resolve() + trailing separator guard (matches reference-handlers.ts)
- Thread customPath into in-session archive endpoints via query param
- Sort same-day archive entries by mtime instead of title
- Clear selectedArchiveFile on linked doc back to prevent badge leak
- Hide archive tab in annotate mode (server doesn't serve those endpoints)
- Add targetTab param to useLinkedDoc.open() to preserve calling sidebar tab
- Replace mutable render variable with index-based date grouping
For provenance purposes, this commit was AI assisted.
* refactor: collapse standalone archive server into plan server
Delete packages/server/archive.ts (187 lines) — nearly all duplicated
from the plan server. Add mode:"archive" option to startPlannotatorServer
instead. Fixes two bugs from code review:
- handleArchiveCopy now splits on "# Plan Feedback" marker instead of
bare "---", preventing truncation at horizontal rules in plan content
- customPath support works in standalone archive mode (was only working
in-session because the standalone server never received it)
For provenance purposes, this commit was AI assisted.
* refactor: extract useArchive hook from App.tsx
Move archive state (archiveMode, plans, selectedFile, isLoading) and
handlers (select, fetchPlans, done, copy) into a dedicated useArchive
hook. Reduces App.tsx by ~75 lines and makes the archive feature
self-contained.
For provenance purposes, this commit was AI assisted.
* feat: Pi archive parity + eliminate server duplication
Move runtime-agnostic storage, draft, and project functions from
packages/server/ to packages/shared/ — eliminating ~250 lines of
duplicated code in Pi's server.ts. Server package becomes thin
re-exports, preserving all existing import paths.
Add archive mode to Pi's plan review server (mode, routes, waitForDone)
and register /plannotator-archive command in the Pi extension. Consolidate
ArchivedPlan type to single definition in shared/storage.ts.
Simplify archive copy to include full content with feedback.
For provenance purposes, this commit was AI assisted.
* fix: drop -core suffix from Pi shared copies
The -core suffix broke cross-file imports — storage.ts imports from
./project which didn't resolve to project-core.ts. Using the original
filenames (no collision) lets relative imports work naturally.
For provenance purposes, this commit was AI assisted.
* fix: archive custom path bugs, disable sharing, update docs
- Normalize planDir via resolve() in getPlanDir() to handle relative
paths and trailing slashes in the path traversal guard
- Re-fetch archive plans client-side with cookie-backed customPath
so standalone archive respects the user's configured save location
- Disable sharing in archive mode (read-only viewer, no need)
- Remove dead /api/plan/history endpoint and listProjectPlans import
from Pi extension
- Remove dead /api/plan/history mock from dev-mock-api
- Update CLAUDE.md and AGENTS.md: add archive flow, archive API
endpoints, shared package structure, correct storage location,
sidebar tab count, remove stale /api/plan/history references
- Update hook server docstring from four to five modes
For provenance purposes, this commit was AI assisted.
* fix: empty archive shows demo content, stale viewer after customPath fetch
- Clear demo markdown when archive opens with no plans (plan: "" was falsy,
so setMarkdown was never called)
- Remove redundant fetchPlans() from archive init — server already sends
archivePlans in initial response
- After fetchPlans() resolves with customPath results, auto-select and load
the first plan into the viewer
- Remove dead listProjectPlans re-export from server barrel
For provenance purposes, this commit was AI assisted.
* refactor: gitignore Pi shared copies, add @generated headers
Pi's copied .ts files (storage, draft, project, feedback-templates,
review-core) are build artifacts generated from packages/shared/. They
looked like editable source files, leading to confusion about which file
to edit. Now gitignored like the HTML copies, with @generated headers
prepended by the build script.
For provenance purposes, this commit was AI assisted.
* fix: generate Pi shared copies in CI before tests
The Pi .ts copies are now gitignored build artifacts. CI needs to
generate them before running tests since server.test.ts transitively
imports them via server.ts.
For provenance purposes, this commit was AI assisted.
* fix: generate Pi shared copies in release pipeline test job
Same fix as test.yml — the Pi .ts copies are gitignored, so the test
job in the release pipeline also needs to generate them before bun test.
For provenance purposes, this commit was AI assisted.
* fix: use block scalar in CI workflow to avoid YAML parse error
The inline `run:` had a colon in the printf string that YAML
interpreted as a mapping key. Switch to `run: |` block scalar.
For provenance purposes, this commit was AI assisted.
* feat: GitLab merge request review support
Add full GitLab MR review parity with existing GitHub PR review:
- Auto-detect platform from URL (github.com vs any GitLab host)
- Extract GitHub logic into pr-github.ts, new pr-gitlab.ts implementation
- Widen PRRef/PRMetadata to discriminated unions for type safety
- Dispatch functions route to correct platform implementation
- Platform-aware UI labels (PR/MR, #/!, GitHub/GitLab icons)
- Self-hosted GitLab support via --hostname flag
- Normalize glab diff output to standard git format
- Handle glab CLI differences (no --jq, Content-Type header for --input)
- Defensive JSON parsing for GitLab context API responses
Tested against gitlab.com with inline comments, multi-line ranges,
approval, and PR context tabs (summary, comments, checks).
For provenance purposes, this commit was AI assisted.
* fix: correct GitLab enum mappings and add shared file path encoding
- Map GitLab job statuses to UI-expected enums (failed→FAILURE, canceled→NEUTRAL)
- Map GitLab detailed_merge_status to CLEAN/BLOCKED/BEHIND/DIRTY/UNKNOWN
- Fix false approval state on repos without required approvers
- Add shared encodeApiFilePath helper used by both GitHub and GitLab
For provenance purposes, this commit was AI assisted.
* fix: align panel headers and refine file tree selection style
- Use shared --panel-header-h CSS variable for consistent header heights
across file tree search, file header, and annotations panel
- Update GitLab icon to use official tanuki SVG paths with currentColor
- Replace solid primary fill on active file tree items with 30% tinted
background for better readability and semantic color preservation
For provenance purposes, this commit was AI assisted.
* fix: annotate-last resolves wrong session after cd
When the user cd's during a Claude Code session, /plannotator-last
picks up a stale session log from a different project directory because
findSessionLogsForCwd() uses the current shell CWD, not the session's
original project directory.
Add three-tier session resolution:
1. PPID metadata (~/.claude/sessions/<ppid>.json) for deterministic O(1) lookup
2. CWD slug match (existing behavior, unchanged)
3. Ancestor directory walk as fallback
Also: getLastRenderedMessage() now catches read errors instead of
throwing, and extracted tryLogCandidates() to deduplicate the
candidate iteration loop.
* fix: use case-insensitive slug lookup in PPID resolver
resolveSessionLogByPpid() was constructing the log path directly,
bypassing the case-insensitive directory scan that findSessionLogsForCwd()
already provides for Windows. Reuse that function instead.
For provenance purposes, this commit was AI assisted.
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
* Approve and review PR to github
* fix: GitHub review improvements — multi-line ranges, own-PR detection, error handling
- Add start_line/start_side to review payload for multi-line annotations
- Detect current GitHub user and disable Approve on own PRs with tooltip
- Fix loading state: approve button no longer shows "Approving..." during feedback
- Fix empty submit: block Post Comments when no annotations and no comment
- Fix Cmd+Enter in GitHub comment dialog
- Fix completion screen text for GitHub mode ("submitted to GitHub")
- Fix agent label fallback to "Your agent" instead of "OpenCode"
- Improve gh-not-installed error message with install URL
- Fix stale useCallback dependency array on handleGitHubAction
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(review): destination dropdown, icon components, and UI polish
Replace Agent/GitHub toggle with a compact destination dropdown.
Buttons stay consistent ("Send Feedback"/"Post Comments" + "Approve")
with behavior routed by destination. Double-tap Option to toggle.
- Add GitHubIcon, RepoIcon, PullRequestIcon components (currentColor)
- Add repo/PR icons to editor header
- Add keyboard hint in destination dropdown
- Add double-Option shortcut to settings
- Hide duplicate filename from Pierre diff header
- Refine FileHeader typography (text-xs font-semibold, no mono)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: include VS Code editor annotations in GitHub PR reviews
Editor annotations were silently dropped when submitting to GitHub.
Now converts them to inline PR comments with selected text as context.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: rename visibility, own-PR keyboard guard, and editor annotation filtering
- Keep data-prev-name visible in Pierre header for rename diffs
- Block Cmd+Enter approve path for own PRs (was only blocked via mouse)
- Filter editor annotations to diff files only to prevent GitHub API rejection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Claude Code replaces all non-[a-zA-Z0-9-] characters with dashes when
deriving project slugs, but we only replaced forward slashes. This broke
annotate-last on Windows and with non-ASCII (e.g. Cyrillic) paths.
Also adds a case-insensitive fallback for directory lookup to handle
Windows drive letter casing differences (C: vs c:).
Closes#339
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(review): add runtime-agnostic PR provider
Introduces `packages/shared/pr-provider.ts` with a `PRRuntime` interface
(same pattern as ReviewGitRuntime in review-core.ts) and GitHub PR
operations: URL parsing, auth check, diff/metadata fetching, and file
content retrieval via `gh` CLI.
`packages/server/pr.ts` is the Bun wrapper that pre-binds the runtime,
matching the git.ts pattern.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(review): add PR mode to review server
When `prMetadata` is provided to `startReviewServer`, the server enters
PR mode: `/api/diff` includes PR metadata and omits gitContext,
`/api/diff/switch` and `/api/git-add` return 400 (not applicable),
and `/api/file-content` fetches from GitHub API using base/head SHAs
instead of local git.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(review): PR review flow for Claude Code and OpenCode
Detects URL argument in `/plannotator-review` command. When a GitHub PR
URL is provided, fetches diff and metadata via `gh` CLI and starts the
review server in PR mode. Local review mode is unchanged when no URL
is passed.
Updates slash command definitions to pass $ARGUMENTS through.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(review): PR metadata display in review UI
Shows "PR Review" badge, PR title with link, and owner/repo in the
header when reviewing a pull request. Diff switcher and staging
controls auto-hide since gitContext is omitted in PR mode.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(review): contextualized PR feedback for agent
In PR mode, the feedback markdown now includes PR metadata (repo,
number, title, branches, URL) so the agent has full context about
the remote PR being reviewed. Removes the aggressive "address all
of them" instruction in PR mode since the content is self-explanatory.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(review): extract exportReviewFeedback + add tests
Moves the pure feedback construction function from App.tsx to
utils/exportFeedback.ts so it can be unit tested. Drops the unused
`files` parameter. 11 tests covering local/PR headers, annotation
grouping, sorting, file-scope ordering, and suggested code rendering.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(review): strengthen PR/local boundary tests
Replaces shallow header checks with comprehensive boundary assertions:
local mode must never contain PR-specific content (repo, URL, branches),
PR mode must include all context fields and exclude the generic header.
Covers null/undefined prMetadata edge cases.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(review): remove redundant empty-annotations test
Covered by the "no annotations: returns generic empty regardless of
prMetadata" test which checks all three cases (no arg, null, PR mode).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(review): guard fetchPR, fix UTF-8 decoding, correct useCallback deps
- Wrap fetchPR() in try/catch in both hook and OpenCode entry points
so network/auth errors show a clean message instead of a stack trace
- Replace atob() with Buffer.from() for UTF-8 correct base64 decoding
of PR file content from GitHub API
- Fix stale useCallback deps in handleCopyFeedback (files → prMetadata)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(install): add $ARGUMENTS to review command for PR URL support
Install scripts were writing the review slash command without
$ARGUMENTS, so PR URLs passed to /plannotator-review were silently
dropped. Also switches PS1 heredoc to single-quoted to prevent
$ARGUMENTS from being expanded as a PowerShell variable.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: add PR review support to docs and READMEs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: add /plannotator-last to READMEs and marketing docs
* feat: add /plannotator-last to install scripts
* fix: sync review command wording in install scripts with plugin
The plugin's plannotator-review.md was updated in #293 to handle
LGTM approvals but the install scripts still had the old wording.
* feat: add /plannotator-last command to annotate last assistant message
Adds a new slash command that extracts the last rendered assistant message
from Claude Code's session log and opens it in the annotation UI.
Session log parser (apps/hook/server/session-log.ts):
- Parses Claude Code JSONL logs at ~/.claude/projects/{slug}/*.jsonl
- Finds the last assistant message.id with text content blocks
- Skips noise entries (progress, system, file-history-snapshot, queue-operation)
- Filters system-generated user messages by prefix to avoid false turn boundaries
- Walks backward through empty turns when back-to-back user messages exist
- No anchoring — reads from end of log since <command-message> isn't written
until after the binary completes
New files:
- apps/hook/commands/plannotator-last.md — slash command definition
- apps/hook/server/session-log.ts — Claude-Code-specific log parser
- apps/hook/server/session-log.test.ts — 30 tests covering streaming chunks,
tool call turns, sub-agent noise, stop hooks, thinking blocks, and edge cases
Modified:
- apps/hook/server/index.ts — annotate-last subcommand
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: remove 3 redundant real-world scenario tests
These duplicated coverage already provided by focused unit tests:
- "full conversation" → covered by "grabs last message.id in multi-tool turn"
- "stop hook interrupted" → covered by "skips progress and system noise"
- "long tool-only sequence" → covered by "skips tool-only assistant entries"
Kept the thinking block test (unique coverage). 27 tests remain.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add /plannotator-last command to Pi extension
Uses Pi's session manager API to find the last assistant message —
walks backward through ctx.sessionManager.getEntries(), finds the
last entry with role "assistant" and text content, opens it in the
annotation UI. Reuses existing isAssistantMessage(), getTextContent(),
startAnnotateServer(), and runBrowserReview() from the extension.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add /plannotator-last to OpenCode plugin + extract command handlers
Adds annotate-last command that fetches session messages via
client.session.messages(), finds the last assistant message with text
parts, and opens it in the annotation UI.
Refactors command handling: extracts review, annotate, and annotate-last
handlers from the inline event hook into commands.ts module. Reduces
index.ts by ~120 lines and makes adding future commands cleaner.
New files:
- apps/opencode-plugin/commands.ts — extracted command handlers
- apps/opencode-plugin/commands/plannotator-last.md — command metadata
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: context-aware UI labels for annotate-last mode
Adds "annotate-last" mode to the annotate server, passed through to the
UI via /api/plan response. The editor uses this to show "Copy message"
instead of "Copy plan", and "annotations on the message" in the
completion overlay.
- packages/server/annotate.ts: new `mode` option on AnnotateServerOptions
- packages/editor/App.tsx: annotateSource state derived from mode
- packages/ui/components/Viewer.tsx: copyLabel prop for button text
- All three harnesses pass mode: "annotate-last" in their callers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add Codex support to annotate-last command
Detects Codex via CODEX_THREAD_ID env var (injected by Codex into every
spawned process). Uses the thread ID to find the rollout file in
~/.codex/sessions/, parses the Codex rollout JSONL format to extract
the last assistant message.
Also adds `plannotator last` alias for shorter usage in Codex bang
commands (!plannotator last).
New files:
- apps/hook/server/codex-session.ts — Codex rollout parser
- apps/hook/server/codex-session.test.ts — 9 tests
Modified:
- apps/hook/server/index.ts — Codex detection + `last` alias
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: context-aware feedback title + top spacing for paragraph-first content
- exportAnnotations now accepts a title param: "Message Feedback" for
annotate-last, "File Feedback" for file annotation, "Plan Feedback"
for plan review (default)
- Adds top spacer when content starts with a paragraph (not a heading)
and has no frontmatter, fixing tight spacing in annotate-last mode
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: add sandbox scripts for Pi and Codex testing
- sandbox-pi.sh: builds extension, creates temp project, installs via
`pi install`, launches Pi with sample files
- sandbox-codex.sh: compiles binary, creates temp project, launches
Codex. Test with `!plannotator last`
Both follow the same pattern as sandbox-opencode.sh.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add hook build step to opencode sandbox script
The opencode build copies HTML from hook/dist/ — without building hook
first, the sandbox could use stale HTML. Pi and Codex sandboxes already
had this step.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove command body from plannotator-last to prevent agent response
The .md body was being sent to the agent as a prompt, causing it to
respond with "Opening annotation UI..." before the event handler could
fetch messages. That response became the "last message" instead of the
actual one. Empty body = agent stays silent, event handler intercepts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use command.execute.before hook for OpenCode annotate-last
Moves plannotator-last from the passive event hook to the
command.execute.before hook. This intercepts the command before the
agent sees it, clears output.parts so the agent stays silent, fetches
session messages, opens the annotation UI, then sends feedback via
client.session.prompt() — same pattern as review/annotate.
Previously the agent would respond to the command body before the
event handler could fetch messages, polluting the session history.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add Codex to origin type and agent name mapping
Origin "codex" was falling through to the default "Coding Agent" label.
Added "codex" to the origin union type across annotate server, editor,
and removed the `as any` cast in the hook.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remote share link, plan-specific prose, and codex type unions
- Add writeRemoteShareLink to annotate-last onReady callback so remote
sessions get a reachable URL
- Add subject parameter to exportAnnotations so feedback says "message"
or "file" instead of "plan" when appropriate
- Add 'codex' to origin type unions in useAgents, Settings, UpdateBanner,
and App.tsx fetch handler
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: correct JSDoc for projectSlugFromCwd (leading dash is kept, not stripped)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: use RenderedMessage type instead of inline structural type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add favicon (option 2 — purple P with gold highlight)
Adds the "Purple P + Highlight" favicon across all three Plannotator
servers (plan, review, annotate). The icon is a rounded purple square
with a bold white "P" and a semi-transparent gold stripe through the
middle — typographic, on-brand, and legible at 16px.
Implementation:
- shared-handlers.ts: handleFavicon() serves the inline SVG with a
1-day cache header, shared by all servers
- index.ts, review.ts, annotate.ts: /favicon.svg route added before
the SPA catch-all
- apps/hook/index.html, apps/review/index.html: <link rel="icon">
added pointing to /favicon.svg
SVG favicons are the modern standard and render crisply at all sizes
without a separate .ico file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add favicon to marketing site
Use the same purple P + gold highlight SVG favicon on plannotator.ai.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
* refactor: shared feedback templates across all integrations
The deny/feedback prompts sent to LLM agents were duplicated as inline
string templates in hook, opencode-plugin, and pi-extension — each with
different tone and framing. The hook's directive style (from #224) was
the most effective at getting agents to address feedback. This extracts
all feedback text into @plannotator/shared/feedback-templates and has
every integration import from the single source of truth.
Closes#215 follow-up (propagates fix to OpenCode and Pi).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: rewrite feedback template tests around contracts not implementation
Tests now verify: cross-integration consistency, verbatim feedback
preservation, empty input handling, and that approved messages don't
contain directive language. Wording can change freely without breaking
tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: instruct agent to preserve plan title on resubmission (#296)
Version history slugs are derived from the plan's first # heading.
When the agent renames the heading after a deny, the version chain
breaks and the user loses diffs. The deny template now tells the
agent not to change the title unless explicitly asked.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: improve plan deny preamble readability
Break the dense single-paragraph preamble into structured sections:
verdict, directive, and rules list. Easier for agents to parse.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add missing @plannotator/shared workspace dependency
Hook and OpenCode plugin imported from @plannotator/shared/feedback-templates
without declaring it as a dependency. Worked locally but failed in CI.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* revert: remove code review and annotate from shared templates
Scope-crept into code review/annotate feedback which introduced a double
heading regression and dropped integration-specific strings. Reverts those
paths to their original inline strings; shared module now only covers
plan deny feedback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: restore Pi plan file hint and vendor template for source installs
Add optional planFilePath to planDenyFeedback so Pi can tell the agent
to read the plan file before editing. Check in a vendored copy of the
template so Pi source installs work without running build:pi first.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* 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>
* 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>
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>
* fix(annotate): resolve file paths correctly on Windows
- Use PLANNOTATOR_CWD env var to get original working directory
- Strip @ prefix from file paths (Claude Code reference syntax)
- Add debug logging (PLANNOTATOR_DEBUG=1)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: add missing path import for annotate subcommand
The dynamic `import("path")` was removed but `path.isAbsolute()` and
`path.join()` are still used, causing a ReferenceError at runtime.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Julien Fernandez <theflysurfer@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Michael Ramos <mdramos8@gmail.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>
* 🐛 Clear diff view when switching from Versions to Contents tab
sidebar.activeTab and isPlanDiffActive were independent states with
no synchronization. Switching to TOC tab left the diff viewer active
in the main area. Add useEffect to reset isPlanDiffActive when tab
changes to "toc".
* 🐛 Dismiss diff view on Escape key and add dev mock API for version history
- Add keydown listener to clear isPlanDiffActive on Escape
- Add Vite dev plugin (dev-mock-api.ts) serving mock /api/plan endpoints
with 3 plan versions so the Versions tab works during local development
- Guard setMarkdown against undefined plan from API response
* 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: add Plan Diff marketing dialog, Pi origin support, and docs
- Add PlanDiffMarketing first-run dialog announcing the Plan Diff feature
with per-origin video demo URLs (Claude Code, OpenCode, Pi)
- Add 'pi' as a first-class origin with display name and violet badge
- Add Plan Diff blog post (plan-diff-see-what-changed.md)
- Add brief Plan Diff mentions across READMEs and marketing docs
* polish: tighten blog post copy and reduce em dash usage
* fix: move PlanDiffMarketing to plan-diff/ and fix stale useEffect deps
* add plan diff preview screenshot for marketing dialog
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add PLANNOTATOR_SHARE_URL env var so users can point share links at
their own self-hosted portal instance instead of share.plannotator.ai.
Threads the base URL through the same path as sharingEnabled: env var →
server options → API response → editor state → useSharing hook →
generateShareUrl(). Includes self-hosting guide and documentation.
Closes#12
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>