Commit Graph

309 Commits

Author SHA1 Message Date
Michael Ramos d4ff4a8df7 feat(ai): OpenCode provider via HTTP + SSE (#379)
* feat(ai): add OpenCode provider via HTTP + SSE

Bundle @opencode-ai/sdk to spawn `opencode serve` and communicate via
HTTP REST + SSE. Richest provider: fork, resume, streaming, tools, and
runtime permission approvals all work natively.

- OpenCodeProvider: lazy server spawn, shared HTTP client, dynamic model
  discovery via GET /provider
- OpenCodeSession: SSE event drain loop, system prompt via API field,
  permission reply (once/reject), abort via HTTP
- mapOpenCodeEvent: SSE → AIMessage mapping (text deltas, tool lifecycle,
  permissions, session status, errors)
- 11 new event mapping tests (78 total, 157 expect calls)

For provenance purposes, this commit was AI assisted.

* docs(ai): add OpenCode to AI features guide and code review docs

For provenance purposes, this commit was AI assisted.

* fix(ai): prevent double server spawn in OpenCode ensureServer()

Use a promise-mutex pattern so concurrent callers (e.g. fetchModels +
createSession) share a single server spawn instead of racing to create two.
Also remove empty finally block from SSE drain loop.

For provenance purposes, this commit was AI assisted.

* fix(ai): await SSE subscribe in OpenCode provider

The SDK's event.subscribe() is async — must be awaited before
destructuring the stream.

For provenance purposes, this commit was AI assisted.

* fix(ui): scrollable model dropdown with search filter

Cap dropdown height at min(320px, 50vh) so it doesn't overflow the
viewport when providers expose many models (e.g. OpenCode with 20+).
Add a search/filter input when there are more than 8 models.

For provenance purposes, this commit was AI assisted.

* fix(ai): close SSE stream, rename to opencode-sdk, move dep to root

- Close SSE stream in finally block to prevent connection leak on
  abort/early return
- Rename provider from opencode to opencode-sdk for consistency with
  claude-agent-sdk, codex-sdk, pi-sdk
- Move @opencode-ai/sdk dependency to root package.json alongside the
  other SDK dependencies

For provenance purposes, this commit was AI assisted.

* feat(ui): add first-run AI setup dialog for provider selection

Show a one-time modal on first review when AI providers are available.
Users pick a default provider and model from a 2-column grid, with a
brief feature intro and auth disclaimer. Persists via cookie, can be
changed later in Settings.

For provenance purposes, this commit was AI assisted.

* fix(ai): clear startPromise on failure so OpenCode provider can retry

If doStart() fails (port conflict, timeout, binary crash), the cached
rejected promise prevented all future ensureServer() calls from
retrying. Now cleared on failure and on dispose().

Also updates stale "future" comment in types.ts.

For provenance purposes, this commit was AI assisted.
2026-03-23 17:37:56 -07:00
Michael Ramos 877b0c78ac feat(ai): Pi provider via RPC subprocess (#377)
* refactor(ai): extract BaseSession and buildEffectivePrompt

Pull shared session lifecycle (~80 lines duplicated across Claude and
Codex providers) into BaseSession: query guard, abort, ID resolution,
generation counter. Extract first-query prompt prepending into
buildEffectivePrompt() in context.ts.

Both providers now extend BaseSession. Behavior-preserving — all 54
existing tests pass unchanged.

For provenance purposes, this commit was AI assisted.

* feat(ai): add Pi provider via RPC subprocess

Spawns `pi --mode rpc` and communicates via JSONL over stdio. No Pi SDK
bundled — user must have the pi CLI installed. Provider auto-discovers
available models at startup via get_available_models.

Includes: PiProcess (JSONL client), PiSDKProvider, PiSDKSession extending
BaseSession, mapPiEvent for streaming, Pi icon in provider UI, and 12
new tests (9 event mapping + 3 buildEffectivePrompt).

For provenance purposes, this commit was AI assisted.

* fix(ai): harden Pi provider error paths

- Surface startup errors when Pi process dies (bad config, missing API keys)
  instead of hanging the chat indefinitely
- Map tool-call errors as non-terminal tool_result instead of AIErrorMessage,
  so Pi can continue reasoning after a failed tool call
- Fix subprocess leak in fetchModels() by moving proc.kill() to finally block

For provenance purposes, this commit was AI assisted.

* fix(ai): detect Pi prompt rejections and process crashes

- Use sendAndWait for prompt command so RPC-level rejections (expired
  credentials, invalid session) surface as errors instead of hanging
- Emit process_exited instead of synthetic agent_end when Pi subprocess
  dies, so crashes show as errors instead of silent empty successes

For provenance purposes, this commit was AI assisted.

* docs(ai): add Pi to AI features guide, code review docs, and installation

For provenance purposes, this commit was AI assisted.

* style(ai): apply biome formatting to new files

For provenance purposes, this commit was AI assisted.
2026-03-23 15:50:30 -07:00
Michael Ramos b675e8677c feat: markdown file browser sidebar tab (#378)
* feat: markdown file browser sidebar tab (#367)

Add a Files sidebar tab that lets users browse and annotate markdown
files from their project. The project root is shown automatically —
no configuration needed. Users can add extra directories in Settings.

- Server: new /api/reference/files endpoint scans directories for .md
  files with broad exclusion list (node_modules, .git, dist, etc.)
- Server: projectRoot sent in /api/plan response (plan + annotate)
- UI: FileBrowser component with per-directory collapsible sections
- UI: useFileBrowser hook manages multi-directory tree state
- UI: Files tab in sidebar (collapsed + expanded), between Versions
  and Vault
- UI: Settings integration tab for additional directories
- Wired into linked doc system for annotation support

Closes #367

For provenance purposes, this commit was AI assisted.

* fix: simplify file browser - compute activeDirPath, fix re-fetch bug, dedupe settings logic

- Compute activeDirPath from activeFile + dirs instead of storing as
  separate state (removes state setter hack that abused setDirs as a
  read accessor)
- Fix auto-fetch not detecting directory changes: compare loaded paths
  against configured dirs instead of checking dirs.length === 0
- Extract addDirectory() helper in Settings to eliminate copy-pasted
  trim-validate-add-clear logic
- Remove "what" comments that described obvious code

For provenance purposes, this commit was AI assisted.

* fix: file browser prefix match, settings reactivity, and vault→files routing

- Add trailing slash to activeDirPath startsWith check to prevent prefix collisions
- Spread uiPrefs in handleFileBrowserChange so React sees a new reference on each settings edit
- Clear vaultBrowser.activeFile when opening a file browser doc to prevent misrouted links

For provenance purposes, this commit was AI assisted.
2026-03-23 15:47:58 -07:00
Michael Ramos 4b097197a0 feat(ai): AI backbone + inline chat for code review (#363)
* feat(ai): add provider-agnostic AI backbone for inline chat

Introduces @plannotator/ai — a pluggable provider layer that powers
AI features (inline chat, plan Q&A, code review assistance) across
all Plannotator surfaces.

- ProviderRegistry: class-based, multi-instance provider management
- SessionManager: in-memory session tracking with eviction, ID aliasing
- Context builders: system prompts and fork preambles for all three modes
- SSE streaming endpoints: session create/fork, query, abort, list
- Claude Agent SDK provider: fork from parent sessions, read-only tools,
  per-query abort, cached SDK import, concurrent query guard
- 34 passing tests covering all layers

For provenance purposes, this commit was AI assisted.

* fix(ai): address code review findings from PR #363

- Concurrent query race: reject with session_busy error instead of
  racing; add query generation counter so stale finally blocks don't
  clobber newer query state
- Text dropped before tool_use: mapSDKMessage now returns AIMessage[]
  and flushes accumulated text before each tool_use block
- defaultProvider returns instance ID: ProviderRegistry.getDefault()
  returns { id, provider } so capabilities endpoint sends the correct
  key for multi-instance setups
- Fork preamble re-sent on error: use _firstQuerySent boolean instead
  of _resolvedId to guard preamble injection
- Dead abortController params: remove unused abortController from
  CreateSessionOptions and AIProvider.resumeSession
- Unused context in memory: remove context from SessionConfig and
  baseConfig — already consumed to build systemPrompt/forkPreamble
- Fix tool_result mapping: SDK wraps tool results in SDKUserMessage
  (type "user"), not a "tool_result" type

For provenance purposes, this commit was AI assisted.

* feat(ai): inline AI chat on code review diffs

Add "Ask AI" to the code review toolbar — select lines, ask questions
about agent-written code, get streaming responses. Conversation persists
across file switches with inline markers on the diff showing where
questions were asked.

- useAIChat hook: SSE streaming, lazy session creation, multi-turn
- AskAIInput: question input with per-selection AI history
- InlineAIMarker: compact sparkle markers on diff lines via @pierre
- AITab: sidebar conversation grouped by file with collapsible sections
- SparklesIcon: shared 3-star sparkle from automations branch
- ReviewPanel: AI tab with message count badge, file group flash highlight
- Server: mount AI endpoints in review server with graceful fallback
- Support three question scopes: line, file, and general
- Toolbar ergonomics: typed text submits as AI question, no double popover

For provenance purposes, this commit was AI assisted.

* fix(ai): toolbar popover ergonomics and visual consistency

- X button closes entire toolbar in both modes (no ghost popover)
- Comment/Ask AI toggle is inline in the action row, same position
  in both modes (left side, mirrored layout)
- AI history appears below the input, not above
- File group flash highlight uses --muted to match file tree hover

For provenance purposes, this commit was AI assisted.

* fix(ai): visual consistency, shared primitives, markdown rendering

Visual consistency:
- AI tab file group headers match FileTree pattern (sizing, hover, badges)
- Q&A bubbles match annotation card padding and border-radius
- Empty state uses circle-bg icon (same as annotations)
- General input matches search input styling
- Removed redundant CSS classes, moved to Tailwind

Shared primitives:
- Extract formatTimestamp to shared utility (was duplicated)
- CountBadge component for consistent badge styling
- CopyButton component with hover-reveal + "Copied" flash

Affordances:
- Copy button on AI responses (hover-reveal)
- Send button on general input with keyboard shortcut hint
- Timestamps on Q&A pairs
- Keyboard shortcut title on Ask button

Markdown rendering:
- Add marked dependency for full block-level markdown in AI responses
- renderMarkdown utility using marked + DOMPurify
- Scoped .ai-markdown CSS for headings, lists, code blocks, blockquotes

For provenance purposes, this commit was AI assisted.

* feat(ai): permission inheritance + inline approval UI

The inline AI agent now inherits the user's existing Claude Code
permission rules via settingSources: ['user', 'project']. Tools
already approved in ~/.claude/settings.json auto-approve silently.

For tools not covered by existing rules, the SDK emits control_request
messages. These are mapped to a new AIPermissionRequestMessage type and
surfaced as inline approval cards in the AI tab — showing the tool name,
command/input, and Allow/Deny buttons.

Backend:
- Map control_request to AIPermissionRequestMessage in provider
- Store Query object on session for sending control_response back
- respondToPermission() method on AISession sends allow/deny
- POST /api/ai/permission endpoint for frontend → backend decisions
- Add settingSources to ClaudeSDKQueryOptions

Frontend:
- PermissionCard component with Allow/Deny buttons + resolved state
- useAIChat tracks permissionRequests array
- respondToPermission() POSTs decision to server
- AITab renders pending permission cards inline in conversation

For provenance purposes, this commit was AI assisted.

* fix(ai): streaming reactivity, maxTurns, popover→sidebar linking

Streaming fixes:
- State updates now create new object references via prev.map()
  instead of mutating objects inside the array — fixes React not
  re-rendering the sidebar during streaming
- Complete assistant text messages only used as fallback when no
  streaming deltas received (prevents duplicate text)
- Result text used as fallback when response is empty (agent spent
  all turns on tools)

Config:
- maxTurns raised to 99 (was 3 — agent ran out of turns reading
  files and never got to write its answer)

Popover → sidebar linking:
- Clicking AI history items in comment popover now passes questionId
  through to handleViewAIResponse, triggering the file group flash
  highlight in the sidebar (same behavior as clicking inline markers)

Debug logging (temporary):
- Server-side: [AI STREAM] logs every message the provider yields
- Client-side: [AI SSE] logs every message received in the browser

For provenance purposes, this commit was AI assisted.

* refactor(ai): simplification pass — types, perf, cleanup

From the three-agent code review analysis:

Quality:
- Rename AIMessage → AIChatEntry in review-editor to avoid collision
  with the wire-protocol AIMessage from packages/ai
- Type permissionMode as union (not string) in SessionConfig + QueryOptions
- Type _activeQuery with narrow interface instead of any
- Make toolUseId optional on AIToolResultMessage (SDK doesn't populate it)
- Remove redundant sessionIdRef double-write in useAIChat
- Fix handleAskGeneral useCallback dep: [aiChat] → [aiChat.ask]

Performance:
- Wrap QAPair in React.memo to prevent re-renders on sibling updates
- Memoize renderMarkdown output via useMemo (avoid re-parsing on every token)
- Fix auto-scroll useEffect: depend on messages.length not messages ref
  (prevents querySelectorAll on every streaming token)

Cleanup:
- Remove debug console.log from SSE endpoint and browser parser
- Extract isMac/submitHint to shared utils/platform.ts
- Extract generateId to shared utils/generateId.ts

Net: -2 lines. No behavior changes.

For provenance purposes, this commit was AI assisted.

* chore: biome lint fixes — import types, button a11y, unused import

- Use `import type` where only types are consumed (endpoints, CountBadge,
  CopyButton, renderMarkdown)
- Remove unused AIContext import from session-manager
- Template literal instead of string concat in context.ts truncate()
- Add type="button" + aria-hidden on CopyButton SVGs

For provenance purposes, this commit was AI assisted.

* feat(ai): Codex SDK provider + multi-provider backend

- Add CodexSDKProvider implementing AIProvider interface
  - Maps Codex ThreadEvents to AIMessage types
  - Delta tracking for streaming text from cumulative item.updated events
  - Supports resume, abort, sandbox modes
  - Factory self-registration pattern matching Claude provider
- Enhance capabilities endpoint with provider models + metadata
- Register both providers in review server (graceful degradation)
- Add CodexSDKConfig type, models list (GPT-5.4 family)
- Add reasoningEffort to CreateSessionOptions (Codex only)
- Extract DiffFile interface to shared types.ts
- Update CLAUDE.md with build order documentation
- 54 tests passing (20+ new for Codex event mapping)

For provenance purposes, this commit was AI assisted.

* feat(ai): provider settings UI + inline config bar

- Settings → AI tab: provider cards with per-provider model dropdowns
- Inline config bar in AI chat sidebar (provider, model, reasoning effort)
- Provider icons: Claude SVG + Codex PNG from marketing assets
- Cookie-based persistence for provider + per-provider model preferences
- useAIChat accepts providerId, model, reasoningEffort from props
- Session reset on provider/model switch (next question starts fresh)
- Reasoning effort selector (Codex only: Low/Medium/High/Max)
- Update code review docs with AI provider documentation

For provenance purposes, this commit was AI assisted.

* fix(ai): review fixes — reasoning props, input blocking, cwd resolution, codex-sdk dep

- Add missing selectedReasoningEffort + onReasoningEffortChange props to
  populated-state AIConfigBar (was only on empty-state, causing crash)
- Disable GeneralInput while streaming to prevent concurrent queries
- Thread cwd through CreateSessionOptions so AI sessions use the correct
  working directory (worktree-aware via getCwd resolver)
- Add @openai/codex-sdk as a real dependency (was only an optional peer)
- Eagerly verify codex-sdk importability at startup to avoid advertising
  a broken provider

For provenance purposes, this commit was AI assisted.

* fix(ai): polish config bar, input, and sidebar header

- Replace native <select> dropdowns with custom popover menus that show
  provider icons, model names, and checkmarks on active selection
- Remove disabled gear placeholder button
- Auto-grow textarea upward on multi-line input (capped at 6 lines)
- Push tab icons to far right in sidebar header (w-full on flex container)
- Add centered divider between file-scoped and general questions

For provenance purposes, this commit was AI assisted.

* fix(ai): honest fork capability + permissionMode doc accuracy

- Endpoint now checks capabilities.fork before calling forkSession(),
  falling back to createSession() for providers that can't truly fork
- Codex forkSession() now throws as the interface contract requires
  (was silently faking a fork with no history — misleading UX)
- Fixed permissionMode JSDoc: said "plan" but code defaults to "default"

For provenance purposes, this commit was AI assisted.

* fix(review): move search result count inside input field

Count now appears inline to the left of the X button instead of
squeezing the input from outside. Dynamic right padding accommodates
the count + clear button when a query is active.

For provenance purposes, this commit was AI assisted.

* fix(ai): use claude_code preset, pass selected code to queries

- Claude provider now uses { preset: "claude_code", append } instead of
  a raw system prompt, so inline chat inherits Claude Code's full
  built-in capabilities
- Stripped custom role/guidelines preamble from context builders — just
  emit the review context (diff, plan, document)
- handleAskAI now extracts selected code via extractLinesFromPatch and
  sends it with the query, fixing hallucinations on large PRs where the
  file's diff was truncated out of the system prompt

For provenance purposes, this commit was AI assisted.

* fix(ai): bundle both SDKs, remove peer dependencies

Both @anthropic-ai/claude-agent-sdk and @openai/codex-sdk are now
direct imports (no string indirection) so bun build --compile bundles
them into the binary. Users don't need to install SDKs separately.

Removed peer dependency declarations from packages/ai/package.json —
meaningless for a compiled binary.

For provenance purposes, this commit was AI assisted.

* refactor: canonical platform.ts, file renames, file-level comments

- Created packages/ui/utils/platform.ts as the single source for
  isMac, modKey, altKey, submitHint, isWindows
- Consolidated inline navigator.platform checks from 6 files
- Renamed renderMarkdown → renderChatMarkdown (distinct from plan editor)
- Renamed formatTimestamp → formatRelativeTime (clearer purpose)
- Added file-level comments to InlineAIMarker, PermissionCard, generateId

For provenance purposes, this commit was AI assisted.

* docs(ai): add AI features guide + link from settings

New docs page at guides/ai-features covering providers, models,
configuration, session behavior, permissions, and available settings.

AISettingsTab now links to the docs in both empty and populated states.

For provenance purposes, this commit was AI assisted.
2026-03-23 12:13:03 -07:00
Michael Ramos a278fdaf77 feat: plan archive browser (#369)
* 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.
2026-03-23 11:31:38 -07:00
Michael Ramos 40002e69dc feat: GitLab merge request review support (#364)
* 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.
2026-03-22 10:22:36 -07:00
Michael Ramos 181e23711a fix: eliminate gaps around file header in code review
Move FileHeader out of the scroll container into a flex-col wrapper so
it spans the full panel width. Make resize handles zero-width overlays
instead of layout-occupying flex children.

Closes #354

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 17:36:37 -07:00
Rock Neurotiko c352465254 feat: Approve and review PR to github (#352)
* 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>
2026-03-20 16:48:26 -07:00
Michael Ramos 33d5d6154c fix: prevent resize handle from covering scrollbar (#359)
* fix: prevent resize handle touch area from covering scrollbar (#354)

The ResizeHandle touch target extended 8px in both directions (-inset-x-2),
overlapping the 6px scrollbar on the adjacent content area. Users could see
the scrollbar but couldn't grab it because the resize handle intercepted clicks.

Add a `side` prop so the touch area only extends outward (away from the scroll
container) — left handles extend left, right handles extend right.

Closes #354

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove transform/opacity from global transition to fix choppy scroll

The global `*` rule applied 150ms transitions on transform and opacity to
every DOM element. During scroll, the compositor fights these transitions
on sticky headers, annotation highlights, and layer compositing — causing
visible choppiness.

Narrowed to color, background-color, border-color, box-shadow only. These
handle theme switching and hover states without affecting scroll performance.
Components needing transform/opacity transitions already declare their own
via Tailwind classes (205 usages across 45 files).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 16:11:06 -07:00
Michael Ramos 6b775ea1ed feat: /plannotator-last — annotate the last agent message (#325)
* 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>
2026-03-17 23:49:34 -07:00
Michael Ramos 93c0f035b3 feat: annotatable diff view with diff context in feedback
* refactor: extract useAnnotationHighlighter hook from Viewer

Move annotation plumbing (web-highlighter lifecycle, toolbar/popover
state, text-selection handlers, findTextInDOM, applyAnnotations) out
of Viewer.tsx into a dedicated hook. Pure refactor — zero behavior
change. Viewer consumes the hook and keeps its own code block, global
comment, and pinpoint-specific logic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: block-level diff annotation with diffContext support

Add annotation support to plan diff view using block-level hover.
Hovering added/removed/modified sections shows the annotation toolbar.
No web-highlighter in diff mode — annotations live in React state only.

- diffContext field on Annotation type (added/removed/modified)
- PlanCleanDiffView: hover handlers, toolbar, comment/quicklabel flows
- Annotated blocks show persistent highlight ring via content matching
- View isolation: diff annotations filtered to diff view, normal to normal
- Share/draft restore filters diff annotations from Viewer DOM
- AnnotationPanel: neutral "diff" badge for diff annotations
- Export: [In diff content] label in feedback
- Toolstrip visible during diff mode for mode switching
- CLAUDE.md updated

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: scroll-to-selected and visible highlight ring for diff annotations

Add scroll-to-selected when clicking a diff annotation in the panel —
scrolls to the block and briefly glows (same focused effect as Viewer).
Replace invisible ring-1 ring-primary/20 with ring-2 ring-accent for
annotated blocks so they're visually distinct.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* cleanup: memoize annotation filters, fix timer leaks, use blockId for highlight rings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: preserve normal annotations across diff toggle

Replace ternary rendering with display:none so the Viewer
stays mounted and web-highlighter DOM marks survive the toggle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: store full block content in diff annotations instead of truncating to 500 chars

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 23:47:35 -07:00
Michael Ramos bb4aab27c4 fix: tater sprite z-index regression from mobile compat PR (#308)
* fix: tater sprite running behind plan document instead of in front

Move TaterSpriteRunning inside the z-0 content wrapper stacking context
so it layers correctly: above sidebars (z-auto) but below the plan
document (z-10). The mobile compat PR (#260) added relative z-0 to the
content wrapper, which trapped the plan at z-0 while the sprite at z-40
floated above everything.


* fix: move z-10 from main to inner content so sprite runs above grid
2026-03-15 22:33:22 -07:00
Michael Ramos d5e0ff6fe6 feat: Octarine notes integration + auto-save for all integrations (#297)
* feat: Octarine notes integration + auto-save for all integrations

Add Octarine as a third notes app integration alongside Obsidian and Bear.
Uses the octarine:// URI scheme to create notes via deep links, following
the same x-callback-url pattern as Bear.

- New file: packages/ui/utils/octarine.ts (cookie-backed settings)
- Server: saveToOctarine() in integrations.ts, wired into /api/approve
  and /api/save-notes endpoints
- UI: Octarine tab in Settings, card in Export > Notes, dropdown button,
  Cmd+S shortcut support
- Parallelize all integration saves with Promise.allSettled (was sequential)
- Add auto-save on plan arrival toggle to Bear and Octarine (Obsidian
  already had this), consolidate into a single effect + single API call

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add YAML frontmatter properties to Octarine notes

Prepend Octarine-compatible YAML frontmatter with tags, Status, Author,
and Last Edited properties. Uses the same extractTags() as Obsidian for
auto-generated tags (project name, title words, code fence languages).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use fresh=true for Octarine saves to prevent content duplication

Octarine's create action appends by default. If auto-save fires on plan
arrival and the user then approves within the same minute, the same path
gets hit twice — doubling the content. Using fresh=true replaces instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: trim workspace and folder in saveToOctarine before building URI

The UI checks workspace.trim().length > 0 but the server used raw values.
Accidental whitespace in settings would cause Octarine saves to fail.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use isOctarineConfigured() for client-side Octarine guards

Auto-save and approve checks used raw workspace truthiness, so
whitespace-only workspace would trigger a save attempt that the server
rejects. Now uses isOctarineConfigured() which trims before checking.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: prevent Bear duplicate notes and align default-save dropdown gates

Bear creates a new note on every save, so skip it on approve when
arrival auto-save already succeeded. Gate the default-save dropdown
by actual configuration (vault path for Obsidian, workspace for
Octarine) instead of just the enabled toggle, matching the shortcut
behavior. Self-heal stale defaults back to "ask".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 21:07:01 -07:00
Michael Ramos e1bd27aae4 feat: custom theme system with 18 built-in themes (#294)
* feat: custom theme system with 15 built-in themes

Consolidate CSS theming into a single source of truth (packages/ui/theme.css)
and introduce a multi-theme architecture where each theme defines both dark
and light mode variants. Users can pick a color palette (theme) and separately
toggle dark/light mode within it.

- Extract shared color tokens, Tailwind bridge, and base styles into packages/ui/theme.css
- Replace hardcoded oklch values with token references (oklch from var syntax)
- Fix 3 light-mode bugs in review-editor diff colors
- Create 15 built-in themes: Plannotator (default), Claude+, Soft Pop, Adwaita,
  Caffeine, Cyberdyne, Cyberfunk, Doom 64, Dracula, Gruvbox, PaulMillr,
  Quantum Rose, Solar Dusk, Terminal, Tinacious
- Expand ThemeProvider to manage colorTheme + mode independently
- Add Theme tab to Settings with mode toggle, search, and swatch grid
- Dark-only themes (Dracula, Terminal, etc.) suppress light class to prevent
  broken styling; light-only themes (Tinacious) force it
- Cookie persistence: plannotator-color-theme for palette, existing key for mode
- Include theme conversion script for FinSitter theme adaptation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: refine themes, add Catppuccin/Rosé Pine/Monokai Pro/Synthwave 84/Tokyo Night

Theme curation:
- Replace Cyberdyne with Synthwave '84 (from robb0wen/synthwave-vscode)
- Replace Cyberfunk with Catppuccin (Mocha dark + Latte light), Rosé Pine
  (dark + Dawn light), Monokai Pro (dark only), Tokyo Night (Storm + Day)
- Rewrite Gruvbox from canonical source (morhetz/gruvbox)
- Rewrite Adwaita from canonical VS Code theme (piousdeer/vscode-adwaita)
- Rewrite PaulMillr from Ghostty canonical palette

Theme fixes:
- Fix faded Send Feedback button on Solar Dusk, Quantum Rose, Caffeine
  (accent colors were too dark/invisible at 15% opacity)
- Fix Dracula/Terminal/PaulMillr/Tinacious light mode breakage — dark-only
  themes now suppress .light class via modeSupport in ThemeProvider
- Fix code block backgrounds — use --code-bg token instead of --muted
- Add faint green grid overlay for Terminal theme
- Alphabetize theme registry (Plannotator first)

Code review diff theming:
- Pass theme colors into @pierre/diffs shadow DOM via unsafeCSS prop
- Dynamic themeType based on resolved mode

18 built-in themes: Plannotator, Absolutely, Adwaita, Caffeine, Catppuccin,
Doom 64, Dracula, Gruvbox, Monokai Pro, PaulMillr, Quantum Rose, Rosé Pine,
Soft Pop, Solar Dusk, Synthwave '84, Terminal, Tinacious, Tokyo Night

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: increase settings modal height for theme grid visibility

Remove 340px cap on theme grid, bump content area from 70vh to 85vh
so all themes are visible without scrolling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: memoize ThemeProvider context, deduplicate Mode type, clean up DiffViewer

From /simplify review:
- Export Mode type from ThemeProvider, import in ThemeTab (was duplicated)
- Add resolvedMode to context — consumers no longer re-query matchMedia
- Memoize context value with useMemo, setters with useCallback (prevents
  unnecessary re-renders of all useTheme consumers)
- Consolidate DiffViewer's two pierre state vars into single object
- Use resolvedMode from context in DiffViewer instead of classList check
- Format crammed single-line extended tokens in 4 theme CSS files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: prevent FOUC, fix system mode stale read, restore marketing theme class

P1: Marketing site inline script now sets theme-{name} class on <html>
    before first paint (reads plannotator-color-theme cookie). Without this,
    CSS tokens under .theme-* selectors were never active.

P2: System mode effect now re-reads matchMedia.matches immediately when
    entering system mode, not just on future changes. Fixes stale resolvedMode
    when OS preference changed while pinned to explicit dark/light.

P3: ThemeProvider applies theme class synchronously during render (not in
    a passive useEffect) to prevent flash of unstyled content on hard refresh.
    Also extracted resolveThemeClasses to module scope to avoid useCallback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add Quick Copy button, fix import icon, comment out agent badge

- Add Quick Copy button to annotation panel footer (splits horizontally
  with existing Quick Share). Copies annotations wrapped with the deny
  preamble so output is paste-ready for agent sessions.
- Export Modal annotations copy also wraps with deny preamble
- Extract wrapFeedbackForAgent() utility in parser.ts as single source
  of truth for the preamble text
- Fix desktop import icon to match mobile (arrow-into-document, not download)
- Comment out code review agent badge — unreliable across multiple harnesses

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 21:26:51 -07:00
Marcelo Terreiro Prado d7ccc55f33 feat: configurable Bear tags + fix double-title bug (#283)
* feat: configurable Bear tags + fix double-title bug

- Fix double title: strip H1 from Bear note body since `title` URL
  param already carries it
- Add custom tags setting (comma-separated, kebab-case) with
  auto-normalization; empty = auto-generated tags (existing behavior)
- Add tag position setting: prepend (after title) or append (default)
- Settings UI in Bear tab with inputs matching existing design
- Dev mock: add /api/approve + /api/save-notes handlers for Bear
  testing in dev mode

* revert: drop dev-mock-api changes

* fix: simplify H1 stripping regex — no wording assumption

* test: add Bear integration tests + restore JSDoc comments

* refactor: export Bear helpers from integrations, import in tests

* test: use dummy title in Bear tests
2026-03-12 16:30:51 -07:00
Itay Grubman 7817c46d71 fix: suppress native mobile context menu on text selection (#281) 2026-03-12 16:15:51 -07:00
Michael Ramos 7417f49076 feat: add new settings indicator for Plan Width and Quick Labels
Pulsing dot on the Settings gear button and "new" pill badges on the
Display and Labels tabs signal newly added settings. Dismissed on first
open via cookie-based version gating.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 01:04:00 -07:00
Michael Ramos fbd4ee4f06 Remove non-critical welcome dialogs (#280)
* chore: remove non-critical welcome dialogs

Remove UI Features Setup, Plan Diff Marketing, and What's New v0.11.0
dialogs from the first-run cascade. Only the Permission Mode Setup
dialog remains as it's the only one that affects agent behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: extract demo plan content to separate file

Moves the 334-line PLAN_CONTENT string from App.tsx to demoPlan.ts to reduce clutter.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 00:27:16 -07:00
Itay Grubman 9a23c249b2 feat: add quick label selection mode for one-click annotations (#272)
* feat: add quick annotation labels for one-click preset feedback

Add preset label chips (Needs tests, Security concern, Break this up, etc.)
that allow instant annotation without typing. Includes ⚡ toolbar button,
Alt+1..8 keyboard shortcuts, label customization in Settings, and label
summary in export output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add quick label selection mode for one-click annotations

* feat: redesign quick label picker UX and add label tips

- Redesign FloatingQuickLabelPicker as a vertical context-menu style list
  with cursor-anchored positioning (appears at mouseup point, not selection center)
- Unify label dropdown: toolbar and quick-label mode now share the same
  FloatingQuickLabelPicker component (removed duplicate InlineQuickLabelDropdown)
- Fix above/below flip positioning (follow CommentPopover pattern with
  conditional translateY)
- Add label tips: optional instruction text on QuickLabel that gets injected
  into agent feedback as a blockquote below the label
- Add tip editor in Settings with three visual states (empty/editing/filled)
- Add "Missing overview" default label with a tip for requesting narrative context
- Extend keyboard shortcuts from Alt+1-8 to Alt+1-9
- Suppress input method toggle (Alt) when label picker is open
- Reorder default labels: Clarify this, Needs tests, Consider edge cases,
  Missing overview, Security concern, Break this up, Wrong order, Discuss first,
  Nice approach

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: curate default labels, add cyan/amber colors, bare digit shortcuts

Finalize the 10 default quick labels based on user feedback data:
clarify, overview, verify, example, patterns, alternatives, regression,
out-of-scope, tests, nice-approach. Each label gets a unique color
(added cyan and amber to the palette). Bare digit keys (1-0) now apply
labels when the picker is open, Alt+N still works everywhere. Tip editor
cursor starts at beginning for readability.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: prevent duplicate annotation when digit key fires both toolbar and picker handlers

When the quick label picker is open from the toolbar's zap button,
let FloatingQuickLabelPicker own all keyboard input instead of both
components handling the same keypress.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
2026-03-11 23:39:46 -07:00
김영준E e51abc6fd5 feat: render diagrams by default in plan review (#279) 2026-03-11 23:13:01 -07:00
dgrissen2 2d1016a3fd feat: render markdown images with lightbox zoom (#271)
Adds full inline image rendering to the Viewer for both annotate and
plan review modes. Previously, ![alt](path) syntax was treated as plain
text.

## What changed

### Viewer.tsx
- Added `![alt](path)` 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 `![chart](./images/chart.png)` 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>
2026-03-11 18:48:05 -07:00
Itay Grubman c0076621ac feat: add quick annotation labels for one-click preset feedback (#268)
Add preset label chips (Needs tests, Security concern, Break this up, etc.)
that allow instant annotation without typing. Includes ⚡ toolbar button,
Alt+1..8 keyboard shortcuts, label customization in Settings, and label
summary in export output.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
2026-03-11 16:49:30 -07:00
Itay Grubman 5437a37da9 feat: add mobile compatibility (#260)
* feat: add mobile compatibility

- Add responsive hamburger menu with all header actions (MobileMenu)
- Annotation panel renders as full-screen overlay on mobile with backdrop and close button
- Panel starts closed on mobile (<768px)
- Touch support for resize handles, pinpoint annotations, and toolstrip buttons
- Mobile text selection creates annotations via highlighter.fromRange() bridge
- Card action buttons always visible on touch devices (hover:none media query)
- Settings modal uses horizontal tab bar on mobile
- CommentPopover width capped to viewport on small screens
- Replace mousedown with pointerdown for touch-compatible click-outside handling
- Add useIsMobile reactive hook for breakpoint detection
- Desktop layout (>=768px) unchanged

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: mobile layout polish — panel gap, button overlap, responsive labels

- AnnotationPanel: fix bottom gap on mobile overlay (inset-y-12 → top-12 bottom-0)
- Viewer: push in-plan action buttons below badges on mobile (mt-6), add
  clear-right before frontmatter, show short labels (Comment/Copy) on mobile
- App: reduce mobile horizontal padding to 8px (px-2)
- test-hook.sh: build review before hook to fix missing dist error

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add plan width display setting with compact/default/wide options

Adds a configurable plan width preference (compact 832px, default 1040px,
wide 1280px) with an abstract layout preview in Settings. Dynamic max-width
flows through to Viewer, PlanDiffViewer, and the toolstrip. Default is
compact to preserve existing behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Michael Ramos <mdramos8@gmail.com>
2026-03-11 16:25:48 -07:00
김영준E 33c7c0be00 feat: Add Graphviz diagram rendering in plan mode (#266)
* feat: add Graphviz rendering for plan mode

Support graphviz/dot/gv fenced blocks in the shared viewer and add a Graphviz sample diagram to PLAN_CONTENT for quick validation.

* fix: use imperative wheel listener with passive:false in GraphvizBlock
2026-03-11 07:40:26 -07:00
김영준E 5b28def742 feat: Improve Mermaid diagram viewing experience (#264)
* fix: stabilize Mermaid fullscreen rendering and Safari fit behavior

* fix: block browser zoom and preserve fullscreen Mermaid wheel zoom

* refactor: simplify Mermaid pinch zoom event handling
2026-03-11 07:39:15 -07:00
김영준E 41059d8e43 fix: Support Windows markdown paths in CLI annotate flow (#267)
* 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>
2026-03-10 17:47:07 -07:00
Michael Ramos 1480915f67 fix: detect OS for update banner install command (#265) (#270)
Windows users were always shown the bash install command. Now uses
navigator.platform to show the PowerShell command on Windows.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 08:53:05 -07:00
Michael Ramos b6bec6943b feat: add Pi origin support to code review UI (#263)
* 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>
2026-03-09 23:41:29 -07:00
dgrissen2 190f7ec412 feat: clipboard paste for per-annotation images & fix absolute path resolution (#255)
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>
2026-03-09 06:35:03 -07:00
Itay Grubman 2a461380b1 feat: bidirectional scroll navigation between annotations and highlights (#253)
* feat: bidirectional scroll navigation between annotations and highlights

When reviewing long plans with many annotations, it's hard to visually
connect which annotation card corresponds to which highlighted line.

- Click annotation card → scrolls content to the highlight + applies
  a bright cyan "focused" color for visibility
- Click highlighted text → scrolls the right panel to bring the
  corresponding annotation card into view
- Works with both web-highlighter and manually created (shared/imported)
  annotations
- Handles edge cases: global comments (no highlight), multi-node
  selections, already-visible elements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: scroll to center and skip focus on annotation creation

- Change scrollIntoView block from 'nearest' to 'center' so targets
  appear prominently in the middle of the viewport
- Track just-created annotation IDs to skip scroll+focus effect when
  a new annotation is added (user is already looking at it)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 06:31:33 -07:00
Michael Ramos cb4de46636 feat: VS Code editor annotations + theme integration (#239)
* 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>
2026-03-07 12:25:02 -08:00
Michael Ramos 75968541c2 feat: Obsidian saving enhancements — separator, auto-save, sanitization (#240)
* 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>
2026-03-07 09:27:11 -08:00
김영준E 2f5fd47f23 🐛 fix: render links inside bold/italic via recursive InlineMarkdown (#236)
Bold-wrapped links like **[text](url)** were rendered as plain bold text
because the bold regex matched first and treated inner content as a string.

Fix: recurse into InlineMarkdown for bold/italic inner content so nested
markdown (links, code, etc.) is properly parsed.

Affected:
- Viewer.tsx (main plan view)
- PlanCleanDiffView.tsx (diff view duplicate)
2026-03-07 07:07:37 -08:00
Michael Ramos 0d68274975 feat: tabbed help dialog with selection modes video
Adds tabs to the help dialog: "Selection Modes" (new video) and
"How Plannotator Works" (existing video). Defaults to selection modes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 00:52:53 -08:00
Michael Ramos 2739660fa6 feat: pinpoint mode for point-and-click annotations (#232)
* feat: add pinpoint mode for point-and-click annotations

Adds a new input method toggle (Drag / Pinpoint) orthogonal to the
existing annotation action modes (Selection / Comment / Redline).

In pinpoint mode, hovering over blocks and inline elements shows a
dashed outline with a label tooltip. Clicking auto-selects the
element's text via highlighter.fromRange(), firing the same CREATE
event as drag-selection — all downstream annotation flow is unchanged.

New files:
- blockTargeting.ts: resolves hover targets (inlines, cells, list items, blocks)
- usePinpoint.ts: hover/click state machine with text-range bridge
- PinpointOverlay.tsx: dashed outline + label badge
- InputMethodToggle.tsx: Drag/Pinpoint segmented toggle
- inputMethod.ts: cookie-based persistence

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: unified AnnotationToolstrip with smooth animations

Merge InputMethodToggle + ModeSwitcher into a single compact toolbar
with two pill groups: input method (Select/Pinpoint) and action mode
(Markup/Comment/Redline). Buttons expand to show labels on hover/active
with explicit pixel-width transitions. Suppress transitions on mount
to prevent initial flash.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: keyboard shortcuts tab and settings reorganization

Add Shortcuts tab to settings with OS-aware key cap styling and
organized shortcut groups. Move Obsidian and Bear integrations
to dedicated tabs under an Integrations sidebar group, leaving
compact navigation links on the Saving tab.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: Alt/Option key to switch between Select and Pinpoint

Hold Alt to temporarily switch input method (reverts on release).
Double-tap Alt to permanently toggle. Added to keyboard shortcuts
panel in settings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove md:py-8 to use consistent py-3 padding

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: toolbar polish — icon swap, full opacity active states, styling

Move text-bracket icon to Select, pencil to Markup. Active buttons
use full opacity backgrounds with contrasting foreground. Help link
lowercase with updated styling. Revert md:py-8 padding. Add gap
between buttons in pill groups.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: match toolbar button sizing to main branch

Use text-xs (12px) and px-2.5 padding to match main branch button
rendering. Active state uses bg-background with no color tint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: smarter pinpoint hover — edge-zone tables, row targeting, list groups

Table edge-zone detection: top/bottom edges target whole table, left/right
edges target the specific row, interior targets individual cells. List group
wrappers create hover zones in gaps between consecutive list items. Demo
plan now includes sample tables for testing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove unused InputMethodToggle component

Superseded by AnnotationToolstrip which unified input method + mode switching.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove unimplemented S3 paste store stub

Never used — paste service runs on Cloudflare KV and local FS.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: remove dead ModeSwitcher component

Replaced by AnnotationToolstrip which unified mode switching + input method toggle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 00:27:59 -08:00
Michael Ramos 983246a18e fix: prevent CommentPopover from closing when interacting with ImageAnnotator (#226)
The click-outside handler matched only z-[90]/z-[100] classes, missing the
ImageAnnotator portal at z-[200]. Replace fragile z-index class matching with
a data-popover-layer attribute on all overlay portals.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 16:29:35 -08:00
Michael Ramos 2cb2152918 feat: What's New v0.11.0 first-open dialog (#225)
* feat: add What's New v0.11.0 first-open dialog

One-time announcement dialog highlighting auto-save drafts, short link
sharing with E2E encryption, and Obsidian vault browser. Supersedes the
PlanDiffMarketing dialog in the chain.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: restore PlanDiffMarketing dialog in the chain

PlanDiffMarketing was incorrectly removed. It now stays in the sequence:
PermissionMode → UIFeatures → PlanDiffMarketing → WhatsNew v0.11.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: restore showPlanDiffMarketing to useEffect dependency arrays

The fix commit restored it to guard conditions but missed the dep
arrays, creating a stale closure bug for keyboard shortcuts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 15:59:22 -08:00
Michael Ramos f20bf14cef refactor: extract applyCodeBlockAnnotation helper to deduplicate code block mark-wrapping
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 13:05:24 -08:00
Michael Ramos 092538ab8f feat: CommentPopover for better comment writing UX (#220)
* feat: replace comment textarea with CommentPopover (#219)

Replace the cramped toolbar textarea and awkward global comment inline
form with a new CommentPopover component that provides a proper writing
surface for long comments.

- New CommentPopover with popover mode (w-96) and expand-to-dialog mode
- Simplified AnnotationToolbar to menu-only (removed input step)
- Global comment button now opens CommentPopover instead of inline form
- Cmd/Ctrl+Enter to submit, Enter for newline (GitHub/Linear pattern)
- Popover tracks anchor element on scroll to stay pinned to content
- Image attachments supported in both modes

Closes #219

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: dismiss CommentPopover on new selection and memoize close handler

- Clear commentPopover state in highlighter CREATE handler when a new
  selection is made (prevents popover+toolbar coexisting)
- Wrap handleCommentClose in useCallback to prevent the click-outside
  effect from re-registering on every Viewer render

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 13:00:39 -08:00
Michael Ramos aa71ede0f3 Auto-save annotation drafts to survive server crashes (#217)
* 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>
2026-03-04 10:12:58 -08:00
Michael Ramos e5f3cfffbb feat: Obsidian vault browser sidebar tab (#209)
* 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>
2026-03-03 15:32:59 -08:00
Michael Ramos 1ea075b241 fix: show error dialog on shared URL failure and fix import annotation merge (#205)
* chore: open v0.10.0 pre-release fix branch

Tracking two bugs found during pre-release testing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: show error dialog on shared URL failure and fix import annotation merge

Bug 1: When a shared URL fails to load (truncated hash, expired paste,
missing encryption key), the portal now shows a warning dialog explaining
the failure and an amber "Demo" badge so users know they're seeing sample
content — not their data.

Bug 2: Importing a teammate's share URL no longer replaces existing
annotations. Uses React functional state updaters to avoid stale closures
and passes the full merged annotation list to pendingSharedAnnotations so
DOM highlights are re-applied for all annotations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 16:42:56 -08:00
Michael Ramos d2a159ca53 Add customizable filename format for Obsidian exports (#202)
* 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>
2026-03-02 11:21:16 -08:00
Michael Ramos b1f91936d6 Deploy paste service with E2E encryption (#203)
* feat: Deploy paste service with end-to-end encryption (zero-knowledge)

Add AES-256-GCM client-side encryption to the short URL sharing flow.
Plans are encrypted in the browser before upload — the paste service
stores only ciphertext it cannot read. The decryption key lives only
in the URL fragment (#key=...) and never leaves the browser.

- New packages/shared/crypto.ts with encrypt/decrypt via Web Crypto API
- Encrypt before POST in createShortShareUrl, decrypt on load
- Parse #key= fragment in useSharing.ts for both direct and import URLs
- Deploy Cloudflare Worker with KV namespaces to workers.dev
- Add deploy-paste CI/CD job to deploy.yml
- Fix Cache-Control from public to private, no-store
- Update ExportModal strings with encryption messaging
- Update README, docs, and blog with zero-knowledge encryption details

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Address code review: fix short URL fallthrough and regex anchor

- Return false early when paste fetch fails on /p/<id> path instead of
  falling through to parseShareHash() (which can never succeed since the
  hash contains #key=, not plan data)
- Add end-anchor to importFromShareUrl regex to prevent false matches on
  URLs with extra path segments (regression from PR #188)
- Remove demo plan NOTE from README

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:21:53 -08:00
Michael Ramos bf4589e395 fix: guard Enter key handlers with isComposing for CJK IME support (#201)
CJK users press Enter to confirm IME character selection, which
was triggering form submission instead. Add !e.nativeEvent.isComposing
guard to all Enter-to-submit handlers in the plan editor UI.

Fixes #191

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 22:45:37 -08:00
Gunit Bindal c470dcebf1 Add short URL sharing via paste service to fix URL truncation (#188)
* 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>
2026-02-28 22:55:55 -08:00
dgrissen2 93e9420cb4 feat: open linked .md files in read-only tabs with TOC navigation (#184)
* 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>
2026-02-26 20:26:06 -08:00
Michael Ramos 7145f190ec Add "Open in VS Code Diff" button to plan diff viewer (#180)
* 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>
2026-02-26 13:25:08 -08:00
김영준E 39b63c99c0 fix(ui): dismiss annotation toolbars on outside click and Escape (#182)
* fix: close annotation toolbars on outside click and escape

* refactor: share dismiss logic for annotation toolbars
2026-02-25 06:38:17 -08:00
Michael Ramos 01c5b92a82 chore: bump version to 0.9.1
Fix Plan Diff marketing dialog overflow on small viewports.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 00:55:04 -08:00