2025-12-27 18:14:10 -08:00
import React , { useState , useEffect , useMemo , useRef } from 'react' ;
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-27 05:26:06 +01:00
import { parseMarkdownToBlocks , exportAnnotations , exportLinkedDocAnnotations , extractFrontmatter , Frontmatter } from '@plannotator/ui/utils/parser' ;
2025-12-27 21:24:05 -08:00
import { Viewer , ViewerHandle } from '@plannotator/ui/components/Viewer' ;
import { AnnotationPanel } from '@plannotator/ui/components/AnnotationPanel' ;
import { ExportModal } from '@plannotator/ui/components/ExportModal' ;
2026-02-09 20:37:33 +00:00
import { ImportModal } from '@plannotator/ui/components/ImportModal' ;
2026-01-06 18:26:11 -08:00
import { ConfirmDialog } from '@plannotator/ui/components/ConfirmDialog' ;
feat: named image references and annotate command (#147)
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:23:49 -08:00
import { Annotation , Block , EditorMode , type ImageAttachment } from '@plannotator/ui/types' ;
2025-12-27 21:24:05 -08:00
import { ThemeProvider } from '@plannotator/ui/components/ThemeProvider' ;
import { ModeToggle } from '@plannotator/ui/components/ModeToggle' ;
import { ModeSwitcher } from '@plannotator/ui/components/ModeSwitcher' ;
import { TaterSpriteRunning } from '@plannotator/ui/components/TaterSpriteRunning' ;
import { TaterSpritePullup } from '@plannotator/ui/components/TaterSpritePullup' ;
import { Settings } from '@plannotator/ui/components/Settings' ;
import { useSharing } from '@plannotator/ui/hooks/useSharing' ;
2026-01-19 11:07:22 -08:00
import { useAgents } from '@plannotator/ui/hooks/useAgents' ;
feat: TOC sidebar, sticky actions, and settings redesign (#122)
* feat: Add Table of Contents sidebar with sticky action buttons
- Add hierarchical Table of Contents component with clickable navigation
- Implement useActiveSection hook for real-time section highlighting
- Add annotationHelpers utility for block identification
- Make Images, Global comment, and Copy plan buttons sticky during scroll
- Fix navigation scrolling to work within scrollable main container
* fix: Correct typo in opencode.json (CALUDE -> CLAUDE)
* docs: Update testing documentation - separate UI tests from integration/utility tests
* fix(ui): prevent code block annotation from breaking syntax highlighting
- Replace surroundContents() with plain text wrapper approach
- Add syntax highlighting restoration in removeHighlight()
- Fixes issue reported by kkharji in PR #122
The old approach used range.surroundContents() which wrapped syntax-highlighted
<span> elements, creating nested structure that broke the layout. The new approach
replaces code block innerHTML with plain text wrapped in <mark>, then restores
syntax highlighting when the annotation is removed.
Test coverage: 15 tests pass with edge cases for empty blocks, special chars,
large blocks (10k), unicode, and multiple annotation cycles.
* test(ui): add comprehensive code block annotation regression tests
- Add 15 test cases covering code block annotation behavior
- Test plain text wrapper approach vs nested span approach
- Verify syntax highlighting restoration on annotation removal
- Cover edge cases: empty blocks, special chars, large blocks, unicode
- Add happy-dom dev dependency for DOM testing
* feat: update opencode.json to include additional documentation instructions and configure Playwright MCP
* chore: stop tracking docs directory and opencode.json
* chore: move UI-TESTING.md to tests directory
* docs: extract UI testing checklist into separate file
- Move comprehensive feature checklists to UI-TESTING-CHECKLIST.md
- Keep main UI-TESTING.md focused on development workflow and setup
- Add reference link to checklist file for easy navigation
* docs: update UI testing documentation
* fix(ui): fix TOC active section tracking
IntersectionObserver was using root: null (viewport) instead of the
actual scroll container (<main>), and the effect only ran on mount
before headings were rendered. Pass the container as root and re-run
the observer when heading count changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* revert: remove Playwright MCP from marketplace.json and stale README links
Playwright MCP is dev tooling, not for end users. README referenced
docs/UI-TESTING.md and docs/CODE-STYLE.md which don't exist at those paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style(ui): compact TOC styling and fix layout shifts
- Smaller text (text-xs) and tighter padding for compact feel
- Remove border-l-2 and font-medium from active state to prevent
layout shifts that pushed text into multiline
- Annotation count badges are now perfect circles (w-5 h-5)
- Add left padding to root-level heading
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(ui): add optional TOC and sticky actions settings
- New uiPreferences.ts utility for cookie-based persistence
- TOC toggle: conditionally renders sidebar in App.tsx
- Sticky Actions toggle: conditionally applies sticky positioning in Viewer.tsx
- Both default to enabled, persisted via cookies
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(ui): redesign Settings dialog with sidebar navigation
Replace flat settings list with sidebar + content panel layout:
- General tab: Identity, Permission Mode, Agent Switching
- Display tab: TOC, Sticky Actions, Tater Mode
- Saving tab: Plan Saving, Obsidian, Bear Notes
- Scrollable content area (max-h-[70vh])
- Sidebar hidden in review mode (single tab)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(ui): fix sticky action bar positioning and add scroll-aware card background
Match main's button positioning across all breakpoints with responsive
margins that account for article padding. Use IntersectionObserver sentinel
to detect when the bar is stuck, revealing the card background only on scroll.
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-09 17:28:09 +00:00
import { useActiveSection } from '@plannotator/ui/hooks/useActiveSection' ;
2026-02-20 02:13:37 +09:00
import { storage } from '@plannotator/ui/utils/storage' ;
import { CompletionOverlay } from '@plannotator/ui/components/CompletionOverlay' ;
2025-12-28 17:42:08 -08:00
import { UpdateBanner } from '@plannotator/ui/components/UpdateBanner' ;
2026-02-09 11:28:09 -08:00
import { getObsidianSettings , getEffectiveVaultPath , isObsidianConfigured , CUSTOM_PATH_SENTINEL } from '@plannotator/ui/utils/obsidian' ;
2026-01-04 10:14:36 -08:00
import { getBearSettings } from '@plannotator/ui/utils/bear' ;
2026-02-09 11:28:09 -08:00
import { getDefaultNotesApp } from '@plannotator/ui/utils/defaultNotesApp' ;
2026-01-09 10:32:36 -08:00
import { getAgentSwitchSettings , getEffectiveAgentName } from '@plannotator/ui/utils/agentSwitch' ;
2026-01-07 14:17:56 -08:00
import { getPlanSaveSettings } from '@plannotator/ui/utils/planSave' ;
2026-02-09 11:52:36 -08:00
import { getUIPreferences , needsUIFeaturesSetup , type UIPreferences } from '@plannotator/ui/utils/uiPreferences' ;
2026-02-01 20:18:39 -06:00
import { getEditorMode , saveEditorMode } from '@plannotator/ui/utils/editorMode' ;
2026-02-13 04:19:14 +09:00
import { useResizablePanel } from '@plannotator/ui/hooks/useResizablePanel' ;
import { ResizeHandle } from '@plannotator/ui/components/ResizeHandle' ;
2026-01-13 20:31:23 -08:00
import {
getPermissionModeSettings ,
needsPermissionModeSetup ,
type PermissionMode ,
} from '@plannotator/ui/utils/permissionMode' ;
import { PermissionModeSetup } from '@plannotator/ui/components/PermissionModeSetup' ;
2026-02-09 11:52:36 -08:00
import { UIFeaturesSetup } from '@plannotator/ui/components/UIFeaturesSetup' ;
2026-02-23 00:24:25 -08:00
import { PlanDiffMarketing } from '@plannotator/ui/components/plan-diff/PlanDiffMarketing' ;
import { needsPlanDiffMarketingDialog } from '@plannotator/ui/utils/planDiffMarketing' ;
2026-01-05 17:06:43 -08:00
import { ImageAnnotator } from '@plannotator/ui/components/ImageAnnotator' ;
feat: named image references and annotate command (#147)
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:23:49 -08:00
import { deriveImageName } from '@plannotator/ui/components/AttachmentsButton' ;
feat: plan diff UI with sidebar and dual view modes (#176)
* feat: add plan diff UI with sidebar, badge, and dual view modes
Shows what changed between plan iterations when Claude revises after
feedback. Adds a +N/-M badge below repo info that toggles the diff view,
a shared left sidebar with TOC and Version Browser tabs, and two diff
modes: rendered (color-coded borders) and raw markdown (+/- lines).
Closes #138, closes #111
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update CLAUDE.md project structure and align first-run dialog labels
- Add plan-diff/ and sidebar/ component subdirectories to CLAUDE.md
- Add new hooks and utils to CLAUDE.md project structure
- Rename "Table of Contents" to "Auto-open Sidebar" in UIFeaturesSetup
to match Settings.tsx label
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address code review findings for plan diff UX
- Fix badge stats mixing block counts with line counts (modifications
now fold into additions/deletions)
- Gate hasPreviousVersion on diffBasePlan being loaded to prevent
"Show Changes" no-op and ModeSwitcher disappearing
- Make sidebar reactive to Settings toggle (useEffect on tocEnabled)
- Match PlanDiffViewer badge layout to Viewer (flex-col) so badge
doesn't jump position on toggle
- Add "Exit Diff" label to the close button in diff view
- Remove dead CSS (plan-diff-removed-marker, plan-diff-modified)
- Clean up stale header comment and unused lines prop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: second-round review cleanup for plan diff UX
- Fix stale "amber border" JSDoc in PlanCleanDiffView (actually green)
- Rename sidebar tab from "diff" to "versions" for clarity
- Gate VersionBrowser fetch on versionInfo being available
- Move .sidebar-tab-flag CSS into its own Sidebar section
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add loading state for version selection in sidebar
Add isSelectingVersion to selectBaseVersion, mirroring the existing
isLoadingVersions pattern. Shows "Loading..." on the selected version
button while the fetch is in progress.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address third-round code review findings
- Fix duplicate border/backdrop on TOC inside sidebar (className override)
- Fix loading indicator targeting wrong version button (fetchingVersion state)
- Fix "Show Changes" button silent no-op (gate on hasPreviousVersion)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move date to slug suffix, improve Other Plans UX
- Slug format changed from YYYY-MM-DD-{heading} to {heading}-YYYY-MM-DD
- Other Plans: single "coming soon" banner instead of per-item labels
- Strip date suffix from plan names in sidebar for readability
- Remove cursor-not-allowed from Other Plans items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add Plan Diff section to CLAUDE.md, alert on version fetch failure
- Document plan diff feature: engine, view modes, state management, sidebar
- Update slug format documentation to {heading}-YYYY-MM-DD
- Show native alert when version fetch fails instead of silent swallow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add table rendering to clean diff view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:59:44 -08:00
import { useSidebar } from '@plannotator/ui/hooks/useSidebar' ;
import { usePlanDiff , type VersionInfo } from '@plannotator/ui/hooks/usePlanDiff' ;
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-27 05:26:06 +01:00
import { useLinkedDoc } from '@plannotator/ui/hooks/useLinkedDoc' ;
2026-03-03 15:32:59 -08:00
import { useVaultBrowser } from '@plannotator/ui/hooks/useVaultBrowser' ;
import { isVaultBrowserEnabled } from '@plannotator/ui/utils/obsidian' ;
feat: plan diff UI with sidebar and dual view modes (#176)
* feat: add plan diff UI with sidebar, badge, and dual view modes
Shows what changed between plan iterations when Claude revises after
feedback. Adds a +N/-M badge below repo info that toggles the diff view,
a shared left sidebar with TOC and Version Browser tabs, and two diff
modes: rendered (color-coded borders) and raw markdown (+/- lines).
Closes #138, closes #111
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update CLAUDE.md project structure and align first-run dialog labels
- Add plan-diff/ and sidebar/ component subdirectories to CLAUDE.md
- Add new hooks and utils to CLAUDE.md project structure
- Rename "Table of Contents" to "Auto-open Sidebar" in UIFeaturesSetup
to match Settings.tsx label
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address code review findings for plan diff UX
- Fix badge stats mixing block counts with line counts (modifications
now fold into additions/deletions)
- Gate hasPreviousVersion on diffBasePlan being loaded to prevent
"Show Changes" no-op and ModeSwitcher disappearing
- Make sidebar reactive to Settings toggle (useEffect on tocEnabled)
- Match PlanDiffViewer badge layout to Viewer (flex-col) so badge
doesn't jump position on toggle
- Add "Exit Diff" label to the close button in diff view
- Remove dead CSS (plan-diff-removed-marker, plan-diff-modified)
- Clean up stale header comment and unused lines prop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: second-round review cleanup for plan diff UX
- Fix stale "amber border" JSDoc in PlanCleanDiffView (actually green)
- Rename sidebar tab from "diff" to "versions" for clarity
- Gate VersionBrowser fetch on versionInfo being available
- Move .sidebar-tab-flag CSS into its own Sidebar section
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add loading state for version selection in sidebar
Add isSelectingVersion to selectBaseVersion, mirroring the existing
isLoadingVersions pattern. Shows "Loading..." on the selected version
button while the fetch is in progress.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address third-round code review findings
- Fix duplicate border/backdrop on TOC inside sidebar (className override)
- Fix loading indicator targeting wrong version button (fetchingVersion state)
- Fix "Show Changes" button silent no-op (gate on hasPreviousVersion)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move date to slug suffix, improve Other Plans UX
- Slug format changed from YYYY-MM-DD-{heading} to {heading}-YYYY-MM-DD
- Other Plans: single "coming soon" banner instead of per-item labels
- Strip date suffix from plan names in sidebar for readability
- Remove cursor-not-allowed from Other Plans items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add Plan Diff section to CLAUDE.md, alert on version fetch failure
- Document plan diff feature: engine, view modes, state management, sidebar
- Update slug format documentation to {heading}-YYYY-MM-DD
- Show native alert when version fetch fails instead of silent swallow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add table rendering to clean diff view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:59:44 -08:00
import { SidebarTabs } from '@plannotator/ui/components/sidebar/SidebarTabs' ;
import { SidebarContainer } from '@plannotator/ui/components/sidebar/SidebarContainer' ;
import { PlanDiffViewer } from '@plannotator/ui/components/plan-diff/PlanDiffViewer' ;
import type { PlanDiffMode } from '@plannotator/ui/components/plan-diff/PlanDiffModeSwitcher' ;
2025-12-27 18:14:10 -08:00
const PLAN_CONTENT = ` # Implementation Plan: Real-time Collaboration
## Overview
Add real-time collaboration features to the editor using WebSocket connections and operational transforms.
2026-01-28 09:50:20 -08:00
### Architecture
\` \` \` mermaid
flowchart LR
subgraph Client["Client Browser"]
UI[React UI] --> OT[OT Engine]
OT <--> WS[WebSocket Client]
end
subgraph Server["Backend"]
WSS[WebSocket Server] <--> OTS[OT Transform]
OTS <--> DB[(PostgreSQL)]
end
WS <--> WSS
\` \` \`
2025-12-27 18:14:10 -08:00
## Phase 1: Infrastructure
### WebSocket Server
Set up a WebSocket server to handle concurrent connections:
\` \` \` typescript
const server = new WebSocketServer({ port: 8080 });
server.on('connection', (socket, request) => {
const sessionId = generateSessionId();
sessions.set(sessionId, socket);
socket.on('message', (data) => {
broadcast(sessionId, data);
});
});
\` \` \`
### Client Connection
- Establish persistent connection on document load
2026-01-02 08:57:01 -08:00
- Initialize WebSocket with authentication token
- Set up heartbeat ping/pong every 30 seconds
- Handle connection state changes (connecting, open, closing, closed)
2025-12-27 18:14:10 -08:00
- Implement reconnection logic with exponential backoff
2026-01-02 08:57:01 -08:00
- Start with 1 second delay
- Double delay on each retry (max 30 seconds)
- Reset delay on successful connection
2025-12-27 18:14:10 -08:00
- Handle offline state gracefully
2026-01-02 08:57:01 -08:00
- Queue local changes in IndexedDB
- Show offline indicator in UI
- Sync queued changes on reconnect
2025-12-27 18:14:10 -08:00
### Database Schema
\` \` \` sql
CREATE TABLE documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
content JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE collaborators (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
document_id UUID REFERENCES documents(id) ON DELETE CASCADE,
user_id UUID NOT NULL,
role VARCHAR(50) DEFAULT 'editor',
cursor_position JSONB,
last_seen_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_collaborators_document ON collaborators(document_id);
\` \` \`
## Phase 2: Operational Transforms
> The key insight is that we need to transform operations against concurrent operations to maintain consistency.
Key requirements:
- Transform insert against insert
2026-01-02 08:57:01 -08:00
- Same position: use user ID for deterministic ordering
- Different positions: adjust offset of later operation
2025-12-27 18:14:10 -08:00
- Transform insert against delete
2026-01-02 08:57:01 -08:00
- Insert before delete: no change needed
- Insert inside deleted range: special handling required
- Option A: Move insert to delete start position
- Option B: Discard the insert entirely
- Insert after delete: adjust insert position
2025-12-27 18:14:10 -08:00
- Transform delete against delete
2026-01-02 08:57:01 -08:00
- Non-overlapping: adjust positions
- Overlapping: merge or split operations
2025-12-27 18:14:10 -08:00
- Maintain cursor positions across transforms
2026-01-02 08:57:01 -08:00
- Track cursor as a zero-width insert operation
- Update cursor position after each transform
2025-12-27 18:14:10 -08:00
### Transform Implementation
\` \` \` typescript
interface Operation {
type: 'insert' | 'delete';
position: number;
content?: string;
length?: number;
userId: string;
timestamp: number;
}
class OperationalTransform {
private pendingOps: Operation[] = [];
private history: Operation[] = [];
transform(op1: Operation, op2: Operation): [Operation, Operation] {
if (op1.type === 'insert' && op2.type === 'insert') {
if (op1.position <= op2.position) {
return [op1, { ...op2, position: op2.position + (op1.content?.length || 0) }];
} else {
return [{ ...op1, position: op1.position + (op2.content?.length || 0) }, op2];
}
}
if (op1.type === 'delete' && op2.type === 'delete') {
// Complex delete vs delete transformation
const op1End = op1.position + (op1.length || 0);
const op2End = op2.position + (op2.length || 0);
if (op1End <= op2.position) {
return [op1, { ...op2, position: op2.position - (op1.length || 0) }];
}
// ... more cases
}
return [op1, op2];
}
apply(doc: string, op: Operation): string {
if (op.type === 'insert') {
return doc.slice(0, op.position) + op.content + doc.slice(op.position);
} else {
return doc.slice(0, op.position) + doc.slice(op.position + (op.length || 0));
}
}
}
\` \` \`
## Phase 3: UI Updates
1. Show collaborator cursors in real-time
2026-01-02 08:57:01 -08:00
- Render cursor as colored vertical line
- Add name label above cursor
- Animate cursor movement smoothly
2025-12-27 18:14:10 -08:00
2. Display presence indicators
2026-01-02 08:57:01 -08:00
- Avatar stack in header
- Dropdown with full collaborator list
- Show online/away status
- Display last activity time
- Allow @mentioning collaborators
2025-12-27 18:14:10 -08:00
3. Add conflict resolution UI
2026-01-02 08:57:01 -08:00
- Highlight conflicting regions
- Show diff comparison panel
- Provide merge options:
- Accept mine
- Accept theirs
- Manual merge
2025-12-27 18:14:10 -08:00
4. Implement undo/redo stack per user
2026-01-02 08:57:01 -08:00
- Track operations by user ID
- Allow undoing only own changes
- Show undo history in sidebar
2025-12-27 18:14:10 -08:00
### React Component for Cursors
\` \` \` tsx
import React, { useEffect, useState } from 'react';
import { useCollaboration } from '../hooks/useCollaboration';
interface CursorOverlayProps {
documentId: string;
containerRef: React.RefObject<HTMLDivElement>;
}
export const CursorOverlay: React.FC<CursorOverlayProps> = ({
documentId,
containerRef
}) => {
const { collaborators, currentUser } = useCollaboration(documentId);
const [positions, setPositions] = useState<Map<string, DOMRect>>(new Map());
useEffect(() => {
const updatePositions = () => {
const newPositions = new Map<string, DOMRect>();
collaborators.forEach(collab => {
if (collab.userId !== currentUser.id && collab.cursorPosition) {
const rect = getCursorRect(containerRef.current, collab.cursorPosition);
if (rect) newPositions.set(collab.userId, rect);
}
});
setPositions(newPositions);
};
const interval = setInterval(updatePositions, 50);
return () => clearInterval(interval);
}, [collaborators, currentUser, containerRef]);
return (
<>
{Array.from(positions.entries()).map(([userId, rect]) => (
<div
key={userId}
className="absolute pointer-events-none transition-all duration-75"
style={{
left: rect.left,
top: rect.top,
height: rect.height,
}}
>
<div className="w-0.5 h-full bg-blue-500 animate-pulse" />
<div className="absolute -top-5 left-0 px-1.5 py-0.5 bg-blue-500
text-white text-xs rounded whitespace-nowrap">
{collaborators.find(c => c.userId === userId)?.userName}
</div>
</div>
))}
</>
);
};
\` \` \`
### Configuration
\` \` \` json
{
"collaboration": {
"enabled": true,
"maxCollaborators": 10,
"cursorColors": ["#3B82F6", "#10B981", "#F59E0B", "#EF4444", "#8B5CF6"],
"syncInterval": 100,
"reconnect": {
"maxAttempts": 5,
"backoffMultiplier": 1.5,
"initialDelay": 1000
}
}
}
\` \` \`
---
2026-01-02 08:57:01 -08:00
## Pre-launch Checklist
- [ ] Infrastructure ready
- [x] WebSocket server deployed
- [x] Database migrations applied
- [ ] Load balancer configured
- [ ] SSL certificates installed
- [ ] Health checks enabled
- [ ] /health endpoint returns 200
- [ ] /ready endpoint checks DB connection
- [ ] Primary database
- [ ] Read replicas
- [ ] us-east-1 replica
- [ ] eu-west-1 replica
- [ ] Security audit complete
- [x] Authentication flow reviewed
- [ ] Rate limiting implemented
- [x] 100 req/min for anonymous users
- [ ] 1000 req/min for authenticated users
- [ ] Input sanitization verified
- [x] Documentation updated
- [x] API reference generated
- [x] Integration guide written
- [ ] Video tutorials recorded
### Mixed List Styles
* Asterisk item at level 0
- Dash item at level 1
* Asterisk at level 2
- Dash at level 3
* Asterisk at level 4
- Maximum reasonable depth
1. Numbered item
- Sub-bullet under numbered
- Another sub-bullet
1. Nested numbered list
2. Second nested number
---
2025-12-27 18:14:10 -08:00
**Target:** Ship MVP in next sprint
` ;
const App : React.FC = ( ) = > {
const [ markdown , setMarkdown ] = useState ( PLAN_CONTENT ) ;
const [ annotations , setAnnotations ] = useState < Annotation [ ] > ( [ ] ) ;
const [ selectedAnnotationId , setSelectedAnnotationId ] = useState < string | null > ( null ) ;
const [ blocks , setBlocks ] = useState < Block [ ] > ( [ ] ) ;
2026-01-09 09:23:58 -08:00
const [ frontmatter , setFrontmatter ] = useState < Frontmatter | null > ( null ) ;
2025-12-27 18:14:10 -08:00
const [ showExport , setShowExport ] = useState ( false ) ;
2026-02-09 20:37:33 +00:00
const [ showImport , setShowImport ] = useState ( false ) ;
2025-12-27 18:14:10 -08:00
const [ showFeedbackPrompt , setShowFeedbackPrompt ] = useState ( false ) ;
2026-01-06 18:26:11 -08:00
const [ showClaudeCodeWarning , setShowClaudeCodeWarning ] = useState ( false ) ;
2026-01-19 11:07:22 -08:00
const [ showAgentWarning , setShowAgentWarning ] = useState ( false ) ;
const [ agentWarningMessage , setAgentWarningMessage ] = useState ( '' ) ;
2025-12-27 18:14:10 -08:00
const [ isPanelOpen , setIsPanelOpen ] = useState ( true ) ;
2026-02-01 20:18:39 -06:00
const [ editorMode , setEditorMode ] = useState < EditorMode > ( getEditorMode ) ;
2025-12-27 18:14:10 -08:00
const [ taterMode , setTaterMode ] = useState ( ( ) = > {
2025-12-27 21:24:05 -08:00
const stored = storage . getItem ( 'plannotator-tater-mode' ) ;
2025-12-27 18:14:10 -08:00
return stored === 'true' ;
} ) ;
feat: TOC sidebar, sticky actions, and settings redesign (#122)
* feat: Add Table of Contents sidebar with sticky action buttons
- Add hierarchical Table of Contents component with clickable navigation
- Implement useActiveSection hook for real-time section highlighting
- Add annotationHelpers utility for block identification
- Make Images, Global comment, and Copy plan buttons sticky during scroll
- Fix navigation scrolling to work within scrollable main container
* fix: Correct typo in opencode.json (CALUDE -> CLAUDE)
* docs: Update testing documentation - separate UI tests from integration/utility tests
* fix(ui): prevent code block annotation from breaking syntax highlighting
- Replace surroundContents() with plain text wrapper approach
- Add syntax highlighting restoration in removeHighlight()
- Fixes issue reported by kkharji in PR #122
The old approach used range.surroundContents() which wrapped syntax-highlighted
<span> elements, creating nested structure that broke the layout. The new approach
replaces code block innerHTML with plain text wrapped in <mark>, then restores
syntax highlighting when the annotation is removed.
Test coverage: 15 tests pass with edge cases for empty blocks, special chars,
large blocks (10k), unicode, and multiple annotation cycles.
* test(ui): add comprehensive code block annotation regression tests
- Add 15 test cases covering code block annotation behavior
- Test plain text wrapper approach vs nested span approach
- Verify syntax highlighting restoration on annotation removal
- Cover edge cases: empty blocks, special chars, large blocks, unicode
- Add happy-dom dev dependency for DOM testing
* feat: update opencode.json to include additional documentation instructions and configure Playwright MCP
* chore: stop tracking docs directory and opencode.json
* chore: move UI-TESTING.md to tests directory
* docs: extract UI testing checklist into separate file
- Move comprehensive feature checklists to UI-TESTING-CHECKLIST.md
- Keep main UI-TESTING.md focused on development workflow and setup
- Add reference link to checklist file for easy navigation
* docs: update UI testing documentation
* fix(ui): fix TOC active section tracking
IntersectionObserver was using root: null (viewport) instead of the
actual scroll container (<main>), and the effect only ran on mount
before headings were rendered. Pass the container as root and re-run
the observer when heading count changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* revert: remove Playwright MCP from marketplace.json and stale README links
Playwright MCP is dev tooling, not for end users. README referenced
docs/UI-TESTING.md and docs/CODE-STYLE.md which don't exist at those paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style(ui): compact TOC styling and fix layout shifts
- Smaller text (text-xs) and tighter padding for compact feel
- Remove border-l-2 and font-medium from active state to prevent
layout shifts that pushed text into multiline
- Annotation count badges are now perfect circles (w-5 h-5)
- Add left padding to root-level heading
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(ui): add optional TOC and sticky actions settings
- New uiPreferences.ts utility for cookie-based persistence
- TOC toggle: conditionally renders sidebar in App.tsx
- Sticky Actions toggle: conditionally applies sticky positioning in Viewer.tsx
- Both default to enabled, persisted via cookies
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(ui): redesign Settings dialog with sidebar navigation
Replace flat settings list with sidebar + content panel layout:
- General tab: Identity, Permission Mode, Agent Switching
- Display tab: TOC, Sticky Actions, Tater Mode
- Saving tab: Plan Saving, Obsidian, Bear Notes
- Scrollable content area (max-h-[70vh])
- Sidebar hidden in review mode (single tab)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(ui): fix sticky action bar positioning and add scroll-aware card background
Match main's button positioning across all breakpoints with responsive
margins that account for article padding. Use IntersectionObserver sentinel
to detect when the bar is stuck, revealing the card background only on scroll.
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-09 17:28:09 +00:00
const [ uiPrefs , setUiPrefs ] = useState ( ( ) = > getUIPreferences ( ) ) ;
2025-12-27 18:14:10 -08:00
const [ isApiMode , setIsApiMode ] = useState ( false ) ;
2026-02-23 00:24:25 -08:00
const [ origin , setOrigin ] = useState < 'claude-code' | 'opencode' | 'pi' | null > ( null ) ;
feat: named image references and annotate command (#147)
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:23:49 -08:00
const [ globalAttachments , setGlobalAttachments ] = useState < ImageAttachment [ ] > ( [ ] ) ;
const [ annotateMode , setAnnotateMode ] = useState ( false ) ;
2025-12-27 18:14:10 -08:00
const [ isLoading , setIsLoading ] = useState ( true ) ;
const [ isSubmitting , setIsSubmitting ] = useState ( false ) ;
const [ submitted , setSubmitted ] = useState < 'approved' | 'denied' | null > ( null ) ;
feat: named image references and annotate command (#147)
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:23:49 -08:00
const [ pendingPasteImage , setPendingPasteImage ] = useState < { file : File ; blobUrl : string ; initialName : string } | null > ( null ) ;
2026-01-13 20:31:23 -08:00
const [ showPermissionModeSetup , setShowPermissionModeSetup ] = useState ( false ) ;
2026-02-09 11:52:36 -08:00
const [ showUIFeaturesSetup , setShowUIFeaturesSetup ] = useState ( false ) ;
2026-02-23 00:24:25 -08:00
const [ showPlanDiffMarketing , setShowPlanDiffMarketing ] = useState ( false ) ;
2026-01-13 20:31:23 -08:00
const [ permissionMode , setPermissionMode ] = useState < PermissionMode > ( 'bypassPermissions' ) ;
2026-01-14 13:12:37 -08:00
const [ sharingEnabled , setSharingEnabled ] = useState ( true ) ;
2026-02-12 15:07:43 -08:00
const [ shareBaseUrl , setShareBaseUrl ] = useState < string | undefined > ( undefined ) ;
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-03-01 12:25:55 +05:30
const [ pasteApiUrl , setPasteApiUrl ] = useState < string | undefined > ( undefined ) ;
2026-01-28 10:29:52 -08:00
const [ repoInfo , setRepoInfo ] = useState < { display : string ; branch? : string } | null > ( null ) ;
2026-02-09 11:28:09 -08:00
const [ showExportDropdown , setShowExportDropdown ] = useState ( false ) ;
2026-02-22 09:11:25 -08:00
const [ initialExportTab , setInitialExportTab ] = useState < 'share' | 'annotations' | 'notes' > ( ) ;
2026-02-09 11:28:09 -08:00
const [ noteSaveToast , setNoteSaveToast ] = useState < { type : 'success' | 'error' ; message : string } | null > ( null ) ;
feat: plan diff UI with sidebar and dual view modes (#176)
* feat: add plan diff UI with sidebar, badge, and dual view modes
Shows what changed between plan iterations when Claude revises after
feedback. Adds a +N/-M badge below repo info that toggles the diff view,
a shared left sidebar with TOC and Version Browser tabs, and two diff
modes: rendered (color-coded borders) and raw markdown (+/- lines).
Closes #138, closes #111
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update CLAUDE.md project structure and align first-run dialog labels
- Add plan-diff/ and sidebar/ component subdirectories to CLAUDE.md
- Add new hooks and utils to CLAUDE.md project structure
- Rename "Table of Contents" to "Auto-open Sidebar" in UIFeaturesSetup
to match Settings.tsx label
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address code review findings for plan diff UX
- Fix badge stats mixing block counts with line counts (modifications
now fold into additions/deletions)
- Gate hasPreviousVersion on diffBasePlan being loaded to prevent
"Show Changes" no-op and ModeSwitcher disappearing
- Make sidebar reactive to Settings toggle (useEffect on tocEnabled)
- Match PlanDiffViewer badge layout to Viewer (flex-col) so badge
doesn't jump position on toggle
- Add "Exit Diff" label to the close button in diff view
- Remove dead CSS (plan-diff-removed-marker, plan-diff-modified)
- Clean up stale header comment and unused lines prop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: second-round review cleanup for plan diff UX
- Fix stale "amber border" JSDoc in PlanCleanDiffView (actually green)
- Rename sidebar tab from "diff" to "versions" for clarity
- Gate VersionBrowser fetch on versionInfo being available
- Move .sidebar-tab-flag CSS into its own Sidebar section
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add loading state for version selection in sidebar
Add isSelectingVersion to selectBaseVersion, mirroring the existing
isLoadingVersions pattern. Shows "Loading..." on the selected version
button while the fetch is in progress.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address third-round code review findings
- Fix duplicate border/backdrop on TOC inside sidebar (className override)
- Fix loading indicator targeting wrong version button (fetchingVersion state)
- Fix "Show Changes" button silent no-op (gate on hasPreviousVersion)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move date to slug suffix, improve Other Plans UX
- Slug format changed from YYYY-MM-DD-{heading} to {heading}-YYYY-MM-DD
- Other Plans: single "coming soon" banner instead of per-item labels
- Strip date suffix from plan names in sidebar for readability
- Remove cursor-not-allowed from Other Plans items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add Plan Diff section to CLAUDE.md, alert on version fetch failure
- Document plan diff feature: engine, view modes, state management, sidebar
- Update slug format documentation to {heading}-YYYY-MM-DD
- Show native alert when version fetch fails instead of silent swallow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add table rendering to clean diff view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:59:44 -08:00
// Plan diff state
const [ isPlanDiffActive , setIsPlanDiffActive ] = useState ( false ) ;
const [ planDiffMode , setPlanDiffMode ] = useState < PlanDiffMode > ( 'clean' ) ;
const [ previousPlan , setPreviousPlan ] = useState < string | null > ( null ) ;
const [ versionInfo , setVersionInfo ] = useState < VersionInfo | null > ( null ) ;
2025-12-27 18:14:10 -08:00
const viewerRef = useRef < ViewerHandle > ( null ) ;
feat: TOC sidebar, sticky actions, and settings redesign (#122)
* feat: Add Table of Contents sidebar with sticky action buttons
- Add hierarchical Table of Contents component with clickable navigation
- Implement useActiveSection hook for real-time section highlighting
- Add annotationHelpers utility for block identification
- Make Images, Global comment, and Copy plan buttons sticky during scroll
- Fix navigation scrolling to work within scrollable main container
* fix: Correct typo in opencode.json (CALUDE -> CLAUDE)
* docs: Update testing documentation - separate UI tests from integration/utility tests
* fix(ui): prevent code block annotation from breaking syntax highlighting
- Replace surroundContents() with plain text wrapper approach
- Add syntax highlighting restoration in removeHighlight()
- Fixes issue reported by kkharji in PR #122
The old approach used range.surroundContents() which wrapped syntax-highlighted
<span> elements, creating nested structure that broke the layout. The new approach
replaces code block innerHTML with plain text wrapped in <mark>, then restores
syntax highlighting when the annotation is removed.
Test coverage: 15 tests pass with edge cases for empty blocks, special chars,
large blocks (10k), unicode, and multiple annotation cycles.
* test(ui): add comprehensive code block annotation regression tests
- Add 15 test cases covering code block annotation behavior
- Test plain text wrapper approach vs nested span approach
- Verify syntax highlighting restoration on annotation removal
- Cover edge cases: empty blocks, special chars, large blocks, unicode
- Add happy-dom dev dependency for DOM testing
* feat: update opencode.json to include additional documentation instructions and configure Playwright MCP
* chore: stop tracking docs directory and opencode.json
* chore: move UI-TESTING.md to tests directory
* docs: extract UI testing checklist into separate file
- Move comprehensive feature checklists to UI-TESTING-CHECKLIST.md
- Keep main UI-TESTING.md focused on development workflow and setup
- Add reference link to checklist file for easy navigation
* docs: update UI testing documentation
* fix(ui): fix TOC active section tracking
IntersectionObserver was using root: null (viewport) instead of the
actual scroll container (<main>), and the effect only ran on mount
before headings were rendered. Pass the container as root and re-run
the observer when heading count changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* revert: remove Playwright MCP from marketplace.json and stale README links
Playwright MCP is dev tooling, not for end users. README referenced
docs/UI-TESTING.md and docs/CODE-STYLE.md which don't exist at those paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style(ui): compact TOC styling and fix layout shifts
- Smaller text (text-xs) and tighter padding for compact feel
- Remove border-l-2 and font-medium from active state to prevent
layout shifts that pushed text into multiline
- Annotation count badges are now perfect circles (w-5 h-5)
- Add left padding to root-level heading
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(ui): add optional TOC and sticky actions settings
- New uiPreferences.ts utility for cookie-based persistence
- TOC toggle: conditionally renders sidebar in App.tsx
- Sticky Actions toggle: conditionally applies sticky positioning in Viewer.tsx
- Both default to enabled, persisted via cookies
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(ui): redesign Settings dialog with sidebar navigation
Replace flat settings list with sidebar + content panel layout:
- General tab: Identity, Permission Mode, Agent Switching
- Display tab: TOC, Sticky Actions, Tater Mode
- Saving tab: Plan Saving, Obsidian, Bear Notes
- Scrollable content area (max-h-[70vh])
- Sidebar hidden in review mode (single tab)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(ui): fix sticky action bar positioning and add scroll-aware card background
Match main's button positioning across all breakpoints with responsive
margins that account for article padding. Use IntersectionObserver sentinel
to detect when the bar is stuck, revealing the card background only on scroll.
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-09 17:28:09 +00:00
const containerRef = useRef < HTMLElement > ( null ) ;
2026-02-13 04:19:14 +09:00
// Resizable panels
const panelResize = useResizablePanel ( { storageKey : 'plannotator-panel-width' } ) ;
const tocResize = useResizablePanel ( {
storageKey : 'plannotator-toc-width' ,
defaultWidth : 240 , minWidth : 160 , maxWidth : 400 , side : 'left' ,
} ) ;
const isResizing = panelResize . isDragging || tocResize . isDragging ;
feat: plan diff UI with sidebar and dual view modes (#176)
* feat: add plan diff UI with sidebar, badge, and dual view modes
Shows what changed between plan iterations when Claude revises after
feedback. Adds a +N/-M badge below repo info that toggles the diff view,
a shared left sidebar with TOC and Version Browser tabs, and two diff
modes: rendered (color-coded borders) and raw markdown (+/- lines).
Closes #138, closes #111
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update CLAUDE.md project structure and align first-run dialog labels
- Add plan-diff/ and sidebar/ component subdirectories to CLAUDE.md
- Add new hooks and utils to CLAUDE.md project structure
- Rename "Table of Contents" to "Auto-open Sidebar" in UIFeaturesSetup
to match Settings.tsx label
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address code review findings for plan diff UX
- Fix badge stats mixing block counts with line counts (modifications
now fold into additions/deletions)
- Gate hasPreviousVersion on diffBasePlan being loaded to prevent
"Show Changes" no-op and ModeSwitcher disappearing
- Make sidebar reactive to Settings toggle (useEffect on tocEnabled)
- Match PlanDiffViewer badge layout to Viewer (flex-col) so badge
doesn't jump position on toggle
- Add "Exit Diff" label to the close button in diff view
- Remove dead CSS (plan-diff-removed-marker, plan-diff-modified)
- Clean up stale header comment and unused lines prop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: second-round review cleanup for plan diff UX
- Fix stale "amber border" JSDoc in PlanCleanDiffView (actually green)
- Rename sidebar tab from "diff" to "versions" for clarity
- Gate VersionBrowser fetch on versionInfo being available
- Move .sidebar-tab-flag CSS into its own Sidebar section
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add loading state for version selection in sidebar
Add isSelectingVersion to selectBaseVersion, mirroring the existing
isLoadingVersions pattern. Shows "Loading..." on the selected version
button while the fetch is in progress.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address third-round code review findings
- Fix duplicate border/backdrop on TOC inside sidebar (className override)
- Fix loading indicator targeting wrong version button (fetchingVersion state)
- Fix "Show Changes" button silent no-op (gate on hasPreviousVersion)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move date to slug suffix, improve Other Plans UX
- Slug format changed from YYYY-MM-DD-{heading} to {heading}-YYYY-MM-DD
- Other Plans: single "coming soon" banner instead of per-item labels
- Strip date suffix from plan names in sidebar for readability
- Remove cursor-not-allowed from Other Plans items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add Plan Diff section to CLAUDE.md, alert on version fetch failure
- Document plan diff feature: engine, view modes, state management, sidebar
- Update slug format documentation to {heading}-YYYY-MM-DD
- Show native alert when version fetch fails instead of silent swallow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add table rendering to clean diff view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:59:44 -08:00
// Sidebar (shared TOC + Version Browser)
const sidebar = useSidebar ( getUIPreferences ( ) . tocEnabled ) ;
// Sync sidebar open state when preference changes in Settings
useEffect ( ( ) = > {
if ( uiPrefs . tocEnabled ) {
sidebar . open ( 'toc' ) ;
} else {
sidebar . close ( ) ;
}
} , [ uiPrefs . tocEnabled ] ) ;
2026-03-02 15:11:13 +09:00
// Clear diff view when switching away from versions tab
useEffect ( ( ) = > {
if ( sidebar . activeTab === 'toc' && isPlanDiffActive ) {
setIsPlanDiffActive ( false ) ;
}
} , [ sidebar . activeTab ] ) ;
// Clear diff view on Escape key
useEffect ( ( ) = > {
if ( ! isPlanDiffActive ) return ;
const handleKeyDown = ( e : KeyboardEvent ) = > {
if ( e . key === 'Escape' ) {
setIsPlanDiffActive ( false ) ;
}
} ;
document . addEventListener ( 'keydown' , handleKeyDown ) ;
return ( ) = > document . removeEventListener ( 'keydown' , handleKeyDown ) ;
} , [ isPlanDiffActive ] ) ;
feat: plan diff UI with sidebar and dual view modes (#176)
* feat: add plan diff UI with sidebar, badge, and dual view modes
Shows what changed between plan iterations when Claude revises after
feedback. Adds a +N/-M badge below repo info that toggles the diff view,
a shared left sidebar with TOC and Version Browser tabs, and two diff
modes: rendered (color-coded borders) and raw markdown (+/- lines).
Closes #138, closes #111
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update CLAUDE.md project structure and align first-run dialog labels
- Add plan-diff/ and sidebar/ component subdirectories to CLAUDE.md
- Add new hooks and utils to CLAUDE.md project structure
- Rename "Table of Contents" to "Auto-open Sidebar" in UIFeaturesSetup
to match Settings.tsx label
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address code review findings for plan diff UX
- Fix badge stats mixing block counts with line counts (modifications
now fold into additions/deletions)
- Gate hasPreviousVersion on diffBasePlan being loaded to prevent
"Show Changes" no-op and ModeSwitcher disappearing
- Make sidebar reactive to Settings toggle (useEffect on tocEnabled)
- Match PlanDiffViewer badge layout to Viewer (flex-col) so badge
doesn't jump position on toggle
- Add "Exit Diff" label to the close button in diff view
- Remove dead CSS (plan-diff-removed-marker, plan-diff-modified)
- Clean up stale header comment and unused lines prop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: second-round review cleanup for plan diff UX
- Fix stale "amber border" JSDoc in PlanCleanDiffView (actually green)
- Rename sidebar tab from "diff" to "versions" for clarity
- Gate VersionBrowser fetch on versionInfo being available
- Move .sidebar-tab-flag CSS into its own Sidebar section
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add loading state for version selection in sidebar
Add isSelectingVersion to selectBaseVersion, mirroring the existing
isLoadingVersions pattern. Shows "Loading..." on the selected version
button while the fetch is in progress.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address third-round code review findings
- Fix duplicate border/backdrop on TOC inside sidebar (className override)
- Fix loading indicator targeting wrong version button (fetchingVersion state)
- Fix "Show Changes" button silent no-op (gate on hasPreviousVersion)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move date to slug suffix, improve Other Plans UX
- Slug format changed from YYYY-MM-DD-{heading} to {heading}-YYYY-MM-DD
- Other Plans: single "coming soon" banner instead of per-item labels
- Strip date suffix from plan names in sidebar for readability
- Remove cursor-not-allowed from Other Plans items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add Plan Diff section to CLAUDE.md, alert on version fetch failure
- Document plan diff feature: engine, view modes, state management, sidebar
- Update slug format documentation to {heading}-YYYY-MM-DD
- Show native alert when version fetch fails instead of silent swallow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add table rendering to clean diff view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:59:44 -08:00
// Plan diff computation
const planDiff = usePlanDiff ( markdown , previousPlan , versionInfo ) ;
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-27 05:26:06 +01:00
// Linked document navigation
const linkedDocHook = useLinkedDoc ( {
markdown , annotations , selectedAnnotationId , globalAttachments ,
setMarkdown , setAnnotations , setSelectedAnnotationId , setGlobalAttachments ,
viewerRef , sidebar ,
} ) ;
2026-03-03 15:32:59 -08:00
// Obsidian vault browser
const vaultBrowser = useVaultBrowser ( ) ;
const showVaultTab = useMemo ( ( ) = > isVaultBrowserEnabled ( ) , [ uiPrefs ] ) ;
const vaultPath = useMemo ( ( ) = > {
if ( ! showVaultTab ) return '' ;
const settings = getObsidianSettings ( ) ;
return getEffectiveVaultPath ( settings ) ;
} , [ showVaultTab , uiPrefs ] ) ;
// Clear active file when vault browser is disabled
useEffect ( ( ) = > {
if ( ! showVaultTab ) vaultBrowser . setActiveFile ( null ) ;
} , [ showVaultTab ] ) ;
// Auto-fetch vault tree when vault tab is first opened
useEffect ( ( ) = > {
if ( sidebar . activeTab === 'vault' && showVaultTab && vaultPath && vaultBrowser . tree . length === 0 && ! vaultBrowser . isLoading ) {
vaultBrowser . fetchTree ( vaultPath ) ;
}
} , [ sidebar . activeTab , showVaultTab , vaultPath ] ) ;
const buildVaultDocUrl = React . useCallback (
( vp : string ) = > ( path : string ) = >
` /api/reference/obsidian/doc?vaultPath= ${ encodeURIComponent ( vp ) } &path= ${ encodeURIComponent ( path ) } ` ,
[ ]
) ;
// Vault file selection: open via linked doc system with vault endpoint
const handleVaultFileSelect = React . useCallback ( ( relativePath : string ) = > {
linkedDocHook . open ( relativePath , buildVaultDocUrl ( vaultPath ) ) ;
vaultBrowser . setActiveFile ( relativePath ) ;
} , [ vaultPath , linkedDocHook , vaultBrowser , buildVaultDocUrl ] ) ;
// Route linked doc opens through vault endpoint when viewing a vault file
const handleOpenLinkedDoc = React . useCallback ( ( docPath : string ) = > {
if ( vaultBrowser . activeFile && vaultPath ) {
linkedDocHook . open ( docPath , buildVaultDocUrl ( vaultPath ) ) ;
} else {
linkedDocHook . open ( docPath ) ;
}
} , [ vaultBrowser . activeFile , vaultPath , linkedDocHook , buildVaultDocUrl ] ) ;
// Wrap linked doc back to also clear vault active file
const handleLinkedDocBack = React . useCallback ( ( ) = > {
linkedDocHook . back ( ) ;
vaultBrowser . setActiveFile ( null ) ;
} , [ linkedDocHook , vaultBrowser ] ) ;
const handleVaultFetchTree = React . useCallback ( ( ) = > {
vaultBrowser . fetchTree ( vaultPath ) ;
} , [ vaultBrowser , vaultPath ] ) ;
feat: TOC sidebar, sticky actions, and settings redesign (#122)
* feat: Add Table of Contents sidebar with sticky action buttons
- Add hierarchical Table of Contents component with clickable navigation
- Implement useActiveSection hook for real-time section highlighting
- Add annotationHelpers utility for block identification
- Make Images, Global comment, and Copy plan buttons sticky during scroll
- Fix navigation scrolling to work within scrollable main container
* fix: Correct typo in opencode.json (CALUDE -> CLAUDE)
* docs: Update testing documentation - separate UI tests from integration/utility tests
* fix(ui): prevent code block annotation from breaking syntax highlighting
- Replace surroundContents() with plain text wrapper approach
- Add syntax highlighting restoration in removeHighlight()
- Fixes issue reported by kkharji in PR #122
The old approach used range.surroundContents() which wrapped syntax-highlighted
<span> elements, creating nested structure that broke the layout. The new approach
replaces code block innerHTML with plain text wrapped in <mark>, then restores
syntax highlighting when the annotation is removed.
Test coverage: 15 tests pass with edge cases for empty blocks, special chars,
large blocks (10k), unicode, and multiple annotation cycles.
* test(ui): add comprehensive code block annotation regression tests
- Add 15 test cases covering code block annotation behavior
- Test plain text wrapper approach vs nested span approach
- Verify syntax highlighting restoration on annotation removal
- Cover edge cases: empty blocks, special chars, large blocks, unicode
- Add happy-dom dev dependency for DOM testing
* feat: update opencode.json to include additional documentation instructions and configure Playwright MCP
* chore: stop tracking docs directory and opencode.json
* chore: move UI-TESTING.md to tests directory
* docs: extract UI testing checklist into separate file
- Move comprehensive feature checklists to UI-TESTING-CHECKLIST.md
- Keep main UI-TESTING.md focused on development workflow and setup
- Add reference link to checklist file for easy navigation
* docs: update UI testing documentation
* fix(ui): fix TOC active section tracking
IntersectionObserver was using root: null (viewport) instead of the
actual scroll container (<main>), and the effect only ran on mount
before headings were rendered. Pass the container as root and re-run
the observer when heading count changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* revert: remove Playwright MCP from marketplace.json and stale README links
Playwright MCP is dev tooling, not for end users. README referenced
docs/UI-TESTING.md and docs/CODE-STYLE.md which don't exist at those paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style(ui): compact TOC styling and fix layout shifts
- Smaller text (text-xs) and tighter padding for compact feel
- Remove border-l-2 and font-medium from active state to prevent
layout shifts that pushed text into multiline
- Annotation count badges are now perfect circles (w-5 h-5)
- Add left padding to root-level heading
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(ui): add optional TOC and sticky actions settings
- New uiPreferences.ts utility for cookie-based persistence
- TOC toggle: conditionally renders sidebar in App.tsx
- Sticky Actions toggle: conditionally applies sticky positioning in Viewer.tsx
- Both default to enabled, persisted via cookies
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(ui): redesign Settings dialog with sidebar navigation
Replace flat settings list with sidebar + content panel layout:
- General tab: Identity, Permission Mode, Agent Switching
- Display tab: TOC, Sticky Actions, Tater Mode
- Saving tab: Plan Saving, Obsidian, Bear Notes
- Scrollable content area (max-h-[70vh])
- Sidebar hidden in review mode (single tab)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(ui): fix sticky action bar positioning and add scroll-aware card background
Match main's button positioning across all breakpoints with responsive
margins that account for article padding. Use IntersectionObserver sentinel
to detect when the bar is stuck, revealing the card background only on scroll.
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-09 17:28:09 +00:00
// Track active section for TOC highlighting
const headingCount = useMemo ( ( ) = > blocks . filter ( b = > b . type === 'heading' ) . length , [ blocks ] ) ;
const activeSection = useActiveSection ( containerRef , headingCount ) ;
2025-12-27 18:14:10 -08:00
// URL-based sharing
const {
isSharedSession ,
isLoadingShared ,
shareUrl ,
shareUrlSize ,
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-03-01 12:25:55 +05:30
shortShareUrl ,
isGeneratingShortUrl ,
shortUrlError ,
2025-12-27 18:14:10 -08:00
pendingSharedAnnotations ,
2026-01-05 17:06:43 -08:00
sharedGlobalAttachments ,
2025-12-27 18:14:10 -08:00
clearPendingSharedAnnotations ,
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-03-01 12:25:55 +05:30
generateShortUrl ,
2026-02-09 20:37:33 +00:00
importFromShareUrl ,
2026-03-02 16:42:56 -08:00
shareLoadError ,
clearShareLoadError ,
2025-12-27 18:14:10 -08:00
} = useSharing (
markdown ,
annotations ,
2026-01-05 17:06:43 -08:00
globalAttachments ,
2025-12-27 18:14:10 -08:00
setMarkdown ,
setAnnotations ,
2026-01-05 17:06:43 -08:00
setGlobalAttachments ,
2025-12-27 18:14:10 -08:00
( ) = > {
2025-12-28 14:44:54 -08:00
// When loaded from share, mark as loaded
2025-12-27 18:14:10 -08:00
setIsLoading ( false ) ;
2026-02-12 15:07:43 -08:00
} ,
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-03-01 12:25:55 +05:30
shareBaseUrl ,
pasteApiUrl
2025-12-27 18:14:10 -08:00
) ;
2026-01-19 11:07:22 -08:00
// Fetch available agents for OpenCode (for validation on approve)
const { agents : availableAgents , validateAgent , getAgentWarning } = useAgents ( origin ) ;
2025-12-27 18:14:10 -08:00
// Apply shared annotations to DOM after they're loaded
useEffect ( ( ) = > {
if ( pendingSharedAnnotations && pendingSharedAnnotations . length > 0 ) {
// Small delay to ensure DOM is rendered
const timer = setTimeout ( ( ) = > {
// Clear existing highlights first (important when loading new share URL)
viewerRef . current ? . clearAllHighlights ( ) ;
viewerRef . current ? . applySharedAnnotations ( pendingSharedAnnotations ) ;
clearPendingSharedAnnotations ( ) ;
} , 100 ) ;
return ( ) = > clearTimeout ( timer ) ;
}
} , [ pendingSharedAnnotations , clearPendingSharedAnnotations ] ) ;
const handleTaterModeChange = ( enabled : boolean ) = > {
setTaterMode ( enabled ) ;
2025-12-27 21:24:05 -08:00
storage . setItem ( 'plannotator-tater-mode' , String ( enabled ) ) ;
2025-12-27 18:14:10 -08:00
} ;
2026-02-01 20:18:39 -06:00
const handleEditorModeChange = ( mode : EditorMode ) = > {
setEditorMode ( mode ) ;
saveEditorMode ( mode ) ;
} ;
2025-12-27 18:14:10 -08:00
// Check if we're in API mode (served from Bun hook server)
// Skip if we loaded from a shared URL
useEffect ( ( ) = > {
if ( isLoadingShared ) return ; // Wait for share check to complete
if ( isSharedSession ) return ; // Already loaded from share
fetch ( '/api/plan' )
. then ( res = > {
if ( ! res . ok ) throw new Error ( 'Not in API mode' ) ;
return res . json ( ) ;
} )
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-03-01 12:25:55 +05:30
. then ( ( data : { plan : string ; origin ? : 'claude-code' | 'opencode' | 'pi' ; mode ? : 'annotate' ; sharingEnabled? : boolean ; shareBaseUrl? : string ; pasteApiUrl? : string ; repoInfo ? : { display : string ; branch? : string } ; previousPlan? : string | null ; versionInfo ? : { version : number ; totalVersions : number ; project : string } } ) = > {
2026-03-02 15:11:13 +09:00
if ( data . plan ) setMarkdown ( data . plan ) ;
2025-12-27 18:14:10 -08:00
setIsApiMode ( true ) ;
feat: named image references and annotate command (#147)
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:23:49 -08:00
if ( data . mode === 'annotate' ) {
setAnnotateMode ( true ) ;
}
2026-01-14 13:12:37 -08:00
if ( data . sharingEnabled !== undefined ) {
setSharingEnabled ( data . sharingEnabled ) ;
}
2026-02-12 15:07:43 -08:00
if ( data . shareBaseUrl ) {
setShareBaseUrl ( data . shareBaseUrl ) ;
}
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-03-01 12:25:55 +05:30
if ( data . pasteApiUrl ) {
setPasteApiUrl ( data . pasteApiUrl ) ;
}
2026-01-28 10:29:52 -08:00
if ( data . repoInfo ) {
setRepoInfo ( data . repoInfo ) ;
}
feat: plan diff UI with sidebar and dual view modes (#176)
* feat: add plan diff UI with sidebar, badge, and dual view modes
Shows what changed between plan iterations when Claude revises after
feedback. Adds a +N/-M badge below repo info that toggles the diff view,
a shared left sidebar with TOC and Version Browser tabs, and two diff
modes: rendered (color-coded borders) and raw markdown (+/- lines).
Closes #138, closes #111
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update CLAUDE.md project structure and align first-run dialog labels
- Add plan-diff/ and sidebar/ component subdirectories to CLAUDE.md
- Add new hooks and utils to CLAUDE.md project structure
- Rename "Table of Contents" to "Auto-open Sidebar" in UIFeaturesSetup
to match Settings.tsx label
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address code review findings for plan diff UX
- Fix badge stats mixing block counts with line counts (modifications
now fold into additions/deletions)
- Gate hasPreviousVersion on diffBasePlan being loaded to prevent
"Show Changes" no-op and ModeSwitcher disappearing
- Make sidebar reactive to Settings toggle (useEffect on tocEnabled)
- Match PlanDiffViewer badge layout to Viewer (flex-col) so badge
doesn't jump position on toggle
- Add "Exit Diff" label to the close button in diff view
- Remove dead CSS (plan-diff-removed-marker, plan-diff-modified)
- Clean up stale header comment and unused lines prop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: second-round review cleanup for plan diff UX
- Fix stale "amber border" JSDoc in PlanCleanDiffView (actually green)
- Rename sidebar tab from "diff" to "versions" for clarity
- Gate VersionBrowser fetch on versionInfo being available
- Move .sidebar-tab-flag CSS into its own Sidebar section
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add loading state for version selection in sidebar
Add isSelectingVersion to selectBaseVersion, mirroring the existing
isLoadingVersions pattern. Shows "Loading..." on the selected version
button while the fetch is in progress.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address third-round code review findings
- Fix duplicate border/backdrop on TOC inside sidebar (className override)
- Fix loading indicator targeting wrong version button (fetchingVersion state)
- Fix "Show Changes" button silent no-op (gate on hasPreviousVersion)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move date to slug suffix, improve Other Plans UX
- Slug format changed from YYYY-MM-DD-{heading} to {heading}-YYYY-MM-DD
- Other Plans: single "coming soon" banner instead of per-item labels
- Strip date suffix from plan names in sidebar for readability
- Remove cursor-not-allowed from Other Plans items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add Plan Diff section to CLAUDE.md, alert on version fetch failure
- Document plan diff feature: engine, view modes, state management, sidebar
- Update slug format documentation to {heading}-YYYY-MM-DD
- Show native alert when version fetch fails instead of silent swallow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add table rendering to clean diff view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:59:44 -08:00
// Capture plan version history data
if ( data . previousPlan !== undefined ) {
setPreviousPlan ( data . previousPlan ) ;
}
if ( data . versionInfo ) {
setVersionInfo ( data . versionInfo ) ;
}
2026-01-13 20:31:23 -08:00
if ( data . origin ) {
setOrigin ( data . origin ) ;
// For Claude Code, check if user needs to configure permission mode
if ( data . origin === 'claude-code' && needsPermissionModeSetup ( ) ) {
setShowPermissionModeSetup ( true ) ;
2026-02-09 11:52:36 -08:00
} else if ( needsUIFeaturesSetup ( ) ) {
setShowUIFeaturesSetup ( true ) ;
2026-02-23 00:24:25 -08:00
} else if ( needsPlanDiffMarketingDialog ( ) ) {
setShowPlanDiffMarketing ( true ) ;
2026-01-13 20:31:23 -08:00
}
// Load saved permission mode preference
setPermissionMode ( getPermissionModeSettings ( ) . mode ) ;
}
2025-12-27 18:14:10 -08:00
} )
. catch ( ( ) = > {
// Not in API mode - use default content
setIsApiMode ( false ) ;
} )
. finally ( ( ) = > setIsLoading ( false ) ) ;
} , [ isLoadingShared , isSharedSession ] ) ;
useEffect ( ( ) = > {
2026-01-09 09:23:58 -08:00
const { frontmatter : fm } = extractFrontmatter ( markdown ) ;
setFrontmatter ( fm ) ;
2025-12-27 18:14:10 -08:00
setBlocks ( parseMarkdownToBlocks ( markdown ) ) ;
} , [ markdown ] ) ;
2026-01-05 17:06:43 -08:00
// Global paste listener for image attachments
useEffect ( ( ) = > {
const handlePaste = ( e : ClipboardEvent ) = > {
const items = e . clipboardData ? . items ;
if ( ! items ) return ;
for ( const item of items ) {
if ( item . type . startsWith ( 'image/' ) ) {
e . preventDefault ( ) ;
const file = item . getAsFile ( ) ;
if ( file ) {
feat: named image references and annotate command (#147)
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:23:49 -08:00
// Derive name before showing annotator so user sees it immediately
const initialName = deriveImageName ( file . name , globalAttachments . map ( g = > g . name ) ) ;
2026-01-05 17:06:43 -08:00
const blobUrl = URL . createObjectURL ( file ) ;
feat: named image references and annotate command (#147)
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:23:49 -08:00
setPendingPasteImage ( { file , blobUrl , initialName } ) ;
2026-01-05 17:06:43 -08:00
}
break ;
}
}
} ;
document . addEventListener ( 'paste' , handlePaste ) ;
return ( ) = > document . removeEventListener ( 'paste' , handlePaste ) ;
feat: named image references and annotate command (#147)
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:23:49 -08:00
} , [ globalAttachments ] ) ;
2026-01-05 17:06:43 -08:00
feat: named image references and annotate command (#147)
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:23:49 -08:00
// Handle paste annotator accept — name comes from ImageAnnotator
const handlePasteAnnotatorAccept = async ( blob : Blob , hasDrawings : boolean , name : string ) = > {
2026-01-05 17:06:43 -08:00
if ( ! pendingPasteImage ) return ;
try {
const formData = new FormData ( ) ;
const fileToUpload = hasDrawings
? new File ( [ blob ] , 'annotated.png' , { type : 'image/png' } )
: pendingPasteImage . file ;
formData . append ( 'file' , fileToUpload ) ;
const res = await fetch ( '/api/upload' , { method : 'POST' , body : formData } ) ;
if ( res . ok ) {
feat: named image references and annotate command (#147)
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:23:49 -08:00
const data = await res . json ( ) ;
setGlobalAttachments ( prev = > [ . . . prev , { path : data.path , name } ] ) ;
2026-01-05 17:06:43 -08:00
}
} catch {
// Upload failed silently
} finally {
URL . revokeObjectURL ( pendingPasteImage . blobUrl ) ;
setPendingPasteImage ( null ) ;
}
} ;
const handlePasteAnnotatorClose = ( ) = > {
if ( pendingPasteImage ) {
URL . revokeObjectURL ( pendingPasteImage . blobUrl ) ;
setPendingPasteImage ( null ) ;
}
} ;
2025-12-27 18:14:10 -08:00
// API mode handlers
const handleApprove = async ( ) = > {
setIsSubmitting ( true ) ;
try {
2026-01-02 11:08:19 -08:00
const obsidianSettings = getObsidianSettings ( ) ;
2026-01-04 10:14:36 -08:00
const bearSettings = getBearSettings ( ) ;
2026-01-07 14:17:56 -08:00
const agentSwitchSettings = getAgentSwitchSettings ( ) ;
const planSaveSettings = getPlanSaveSettings ( ) ;
2026-01-02 11:08:19 -08:00
2026-01-04 10:14:36 -08:00
// Build request body - include integrations if enabled
2026-01-13 20:31:23 -08:00
const body : { obsidian? : object ; bear? : object ; feedback? : string ; agentSwitch? : string ; planSave ? : { enabled : boolean ; customPath? : string } ; permissionMode? : string } = { } ;
// Include permission mode for Claude Code
if ( origin === 'claude-code' ) {
body . permissionMode = permissionMode ;
}
2026-01-07 14:17:56 -08:00
2026-01-09 10:32:36 -08:00
// Include agent switch setting for OpenCode (effective name handles custom agents)
const effectiveAgent = getEffectiveAgentName ( agentSwitchSettings ) ;
if ( effectiveAgent ) {
body . agentSwitch = effectiveAgent ;
}
2026-01-07 14:17:56 -08:00
// Include plan save settings
body . planSave = {
enabled : planSaveSettings.enabled ,
. . . ( planSaveSettings . customPath && { customPath : planSaveSettings.customPath } ) ,
} ;
2026-01-04 10:14:36 -08:00
2026-01-14 14:22:04 -08:00
const effectiveVaultPath = getEffectiveVaultPath ( obsidianSettings ) ;
if ( obsidianSettings . enabled && effectiveVaultPath ) {
2026-01-04 10:14:36 -08:00
body . obsidian = {
2026-01-14 14:22:04 -08:00
vaultPath : effectiveVaultPath ,
2026-01-04 10:14:36 -08:00
folder : obsidianSettings.folder || 'plannotator' ,
plan : markdown ,
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
. . . ( obsidianSettings . filenameFormat && { filenameFormat : obsidianSettings.filenameFormat } ) ,
2026-01-04 10:14:36 -08:00
} ;
}
if ( bearSettings . enabled ) {
body . bear = { plan : markdown } ;
}
2026-01-02 11:08:19 -08:00
2026-01-06 18:26:11 -08:00
// Include annotations as feedback if any exist (for OpenCode "approve with notes")
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-27 05:26:06 +01:00
const hasDocAnnotations = Array . from ( linkedDocHook . getDocAnnotations ( ) . values ( ) ) . some (
( d ) = > d . annotations . length > 0 || d . globalAttachments . length > 0
) ;
if ( annotations . length > 0 || globalAttachments . length > 0 || hasDocAnnotations ) {
2026-02-22 09:11:25 -08:00
body . feedback = annotationsOutput ;
2026-01-06 18:26:11 -08:00
}
2026-01-02 11:08:19 -08:00
await fetch ( '/api/approve' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON.stringify ( body ) ,
} ) ;
2025-12-27 18:14:10 -08:00
setSubmitted ( 'approved' ) ;
} catch {
setIsSubmitting ( false ) ;
}
} ;
const handleDeny = async ( ) = > {
setIsSubmitting ( true ) ;
try {
2026-01-07 14:17:56 -08:00
const planSaveSettings = getPlanSaveSettings ( ) ;
2025-12-27 18:14:10 -08:00
await fetch ( '/api/deny' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
2026-01-07 14:17:56 -08:00
body : JSON.stringify ( {
2026-02-22 09:11:25 -08:00
feedback : annotationsOutput ,
2026-01-07 14:17:56 -08:00
planSave : {
enabled : planSaveSettings.enabled ,
. . . ( planSaveSettings . customPath && { customPath : planSaveSettings.customPath } ) ,
} ,
} )
2025-12-27 18:14:10 -08:00
} ) ;
setSubmitted ( 'denied' ) ;
} catch {
setIsSubmitting ( false ) ;
}
} ;
feat: named image references and annotate command (#147)
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:23:49 -08:00
// Annotate mode handler — sends feedback via /api/feedback
const handleAnnotateFeedback = async ( ) = > {
setIsSubmitting ( true ) ;
try {
await fetch ( '/api/feedback' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON.stringify ( {
2026-02-22 09:11:25 -08:00
feedback : annotationsOutput ,
feat: named image references and annotate command (#147)
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:23:49 -08:00
annotations ,
} ) ,
} ) ;
setSubmitted ( 'denied' ) ; // reuse 'denied' state for "feedback sent" overlay
} catch {
setIsSubmitting ( false ) ;
}
} ;
2026-01-15 20:51:57 -08:00
// Global keyboard shortcuts (Cmd/Ctrl+Enter to submit)
useEffect ( ( ) = > {
const handleKeyDown = ( e : KeyboardEvent ) = > {
// Only handle Cmd/Ctrl+Enter
if ( e . key !== 'Enter' || ! ( e . metaKey || e . ctrlKey ) ) return ;
// Don't intercept if typing in an input/textarea
const tag = ( e . target as HTMLElement ) ? . tagName ;
if ( tag === 'INPUT' || tag === 'TEXTAREA' ) return ;
// Don't intercept if any modal is open
2026-02-09 20:37:33 +00:00
if ( showExport || showImport || showFeedbackPrompt || showClaudeCodeWarning ||
2026-02-23 00:24:25 -08:00
showAgentWarning || showPermissionModeSetup || showUIFeaturesSetup || showPlanDiffMarketing || pendingPasteImage ) return ;
2026-01-15 20:51:57 -08:00
// Don't intercept if already submitted or submitting
if ( submitted || isSubmitting ) return ;
// Don't intercept in demo/share mode (no API)
if ( ! isApiMode ) return ;
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-27 05:26:06 +01:00
// Don't submit while viewing a linked doc
if ( linkedDocHook . isActive ) return ;
2026-01-15 20:51:57 -08:00
e . preventDefault ( ) ;
feat: named image references and annotate command (#147)
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:23:49 -08:00
// Annotate mode: always send feedback
if ( annotateMode ) {
if ( annotations . length === 0 ) {
setShowFeedbackPrompt ( true ) ;
} else {
handleAnnotateFeedback ( ) ;
}
return ;
}
2026-01-15 20:51:57 -08:00
// No annotations → Approve, otherwise → Send Feedback
if ( annotations . length === 0 ) {
2026-01-19 11:07:22 -08:00
// Check if agent exists for OpenCode users
if ( origin === 'opencode' ) {
const warning = getAgentWarning ( ) ;
if ( warning ) {
setAgentWarningMessage ( warning ) ;
setShowAgentWarning ( true ) ;
return ;
}
}
2026-01-15 20:51:57 -08:00
handleApprove ( ) ;
} else {
handleDeny ( ) ;
}
} ;
window . addEventListener ( 'keydown' , handleKeyDown ) ;
return ( ) = > window . removeEventListener ( 'keydown' , handleKeyDown ) ;
} , [
2026-02-09 20:37:33 +00:00
showExport , showImport , showFeedbackPrompt , showClaudeCodeWarning , showAgentWarning ,
2026-02-23 00:24:25 -08:00
showPermissionModeSetup , showUIFeaturesSetup , showPlanDiffMarketing , pendingPasteImage ,
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-27 05:26:06 +01:00
submitted , isSubmitting , isApiMode , linkedDocHook . isActive , annotations . length , annotateMode ,
2026-01-19 11:07:22 -08:00
origin , getAgentWarning ,
2026-01-15 20:51:57 -08:00
] ) ;
2025-12-27 18:14:10 -08:00
const handleAddAnnotation = ( ann : Annotation ) = > {
setAnnotations ( prev = > [ . . . prev , ann ] ) ;
setSelectedAnnotationId ( ann . id ) ;
setIsPanelOpen ( true ) ;
} ;
const handleDeleteAnnotation = ( id : string ) = > {
viewerRef . current ? . removeHighlight ( id ) ;
setAnnotations ( prev = > prev . filter ( a = > a . id !== id ) ) ;
if ( selectedAnnotationId === id ) setSelectedAnnotationId ( null ) ;
} ;
2026-01-13 23:29:47 -05:00
const handleEditAnnotation = ( id : string , updates : Partial < Annotation > ) = > {
setAnnotations ( prev = > prev . map ( ann = >
ann . id === id ? { . . . ann , . . . updates } : ann
) ) ;
} ;
2025-12-27 18:14:10 -08:00
const handleIdentityChange = ( oldIdentity : string , newIdentity : string ) = > {
setAnnotations ( prev = > prev . map ( ann = >
ann . author === oldIdentity ? { . . . ann , author : newIdentity } : ann
) ) ;
} ;
feat: named image references and annotate command (#147)
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:23:49 -08:00
const handleAddGlobalAttachment = ( image : ImageAttachment ) = > {
setGlobalAttachments ( prev = > [ . . . prev , image ] ) ;
2026-01-05 17:06:43 -08:00
} ;
const handleRemoveGlobalAttachment = ( path : string ) = > {
feat: named image references and annotate command (#147)
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:23:49 -08:00
setGlobalAttachments ( prev = > prev . filter ( p = > p . path !== path ) ) ;
2026-01-05 17:06:43 -08:00
} ;
feat: named image references and annotate command (#147)
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:23:49 -08:00
feat: TOC sidebar, sticky actions, and settings redesign (#122)
* feat: Add Table of Contents sidebar with sticky action buttons
- Add hierarchical Table of Contents component with clickable navigation
- Implement useActiveSection hook for real-time section highlighting
- Add annotationHelpers utility for block identification
- Make Images, Global comment, and Copy plan buttons sticky during scroll
- Fix navigation scrolling to work within scrollable main container
* fix: Correct typo in opencode.json (CALUDE -> CLAUDE)
* docs: Update testing documentation - separate UI tests from integration/utility tests
* fix(ui): prevent code block annotation from breaking syntax highlighting
- Replace surroundContents() with plain text wrapper approach
- Add syntax highlighting restoration in removeHighlight()
- Fixes issue reported by kkharji in PR #122
The old approach used range.surroundContents() which wrapped syntax-highlighted
<span> elements, creating nested structure that broke the layout. The new approach
replaces code block innerHTML with plain text wrapped in <mark>, then restores
syntax highlighting when the annotation is removed.
Test coverage: 15 tests pass with edge cases for empty blocks, special chars,
large blocks (10k), unicode, and multiple annotation cycles.
* test(ui): add comprehensive code block annotation regression tests
- Add 15 test cases covering code block annotation behavior
- Test plain text wrapper approach vs nested span approach
- Verify syntax highlighting restoration on annotation removal
- Cover edge cases: empty blocks, special chars, large blocks, unicode
- Add happy-dom dev dependency for DOM testing
* feat: update opencode.json to include additional documentation instructions and configure Playwright MCP
* chore: stop tracking docs directory and opencode.json
* chore: move UI-TESTING.md to tests directory
* docs: extract UI testing checklist into separate file
- Move comprehensive feature checklists to UI-TESTING-CHECKLIST.md
- Keep main UI-TESTING.md focused on development workflow and setup
- Add reference link to checklist file for easy navigation
* docs: update UI testing documentation
* fix(ui): fix TOC active section tracking
IntersectionObserver was using root: null (viewport) instead of the
actual scroll container (<main>), and the effect only ran on mount
before headings were rendered. Pass the container as root and re-run
the observer when heading count changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* revert: remove Playwright MCP from marketplace.json and stale README links
Playwright MCP is dev tooling, not for end users. README referenced
docs/UI-TESTING.md and docs/CODE-STYLE.md which don't exist at those paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style(ui): compact TOC styling and fix layout shifts
- Smaller text (text-xs) and tighter padding for compact feel
- Remove border-l-2 and font-medium from active state to prevent
layout shifts that pushed text into multiline
- Annotation count badges are now perfect circles (w-5 h-5)
- Add left padding to root-level heading
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(ui): add optional TOC and sticky actions settings
- New uiPreferences.ts utility for cookie-based persistence
- TOC toggle: conditionally renders sidebar in App.tsx
- Sticky Actions toggle: conditionally applies sticky positioning in Viewer.tsx
- Both default to enabled, persisted via cookies
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(ui): redesign Settings dialog with sidebar navigation
Replace flat settings list with sidebar + content panel layout:
- General tab: Identity, Permission Mode, Agent Switching
- Display tab: TOC, Sticky Actions, Tater Mode
- Saving tab: Plan Saving, Obsidian, Bear Notes
- Scrollable content area (max-h-[70vh])
- Sidebar hidden in review mode (single tab)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(ui): fix sticky action bar positioning and add scroll-aware card background
Match main's button positioning across all breakpoints with responsive
margins that account for article padding. Use IntersectionObserver sentinel
to detect when the bar is stuck, revealing the card background only on scroll.
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-09 17:28:09 +00:00
const handleTocNavigate = ( blockId : string ) = > {
// Navigation handled by TableOfContents component
// This is just a placeholder for future custom logic
} ;
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-27 05:26:06 +01:00
const annotationsOutput = useMemo ( ( ) = > {
const docAnnotations = linkedDocHook . getDocAnnotations ( ) ;
const hasDocAnnotations = Array . from ( docAnnotations . values ( ) ) . some (
( d ) = > d . annotations . length > 0 || d . globalAttachments . length > 0
) ;
const hasPlanAnnotations = annotations . length > 0 || globalAttachments . length > 0 ;
if ( ! hasPlanAnnotations && ! hasDocAnnotations ) {
return 'No changes detected.' ;
}
let output = hasPlanAnnotations
? exportAnnotations ( blocks , annotations , globalAttachments )
: '' ;
if ( hasDocAnnotations ) {
output += exportLinkedDocAnnotations ( docAnnotations ) ;
}
return output ;
} , [ blocks , annotations , globalAttachments , linkedDocHook . getDocAnnotations ] ) ;
2025-12-27 18:14:10 -08:00
2026-02-09 11:28:09 -08:00
// Quick-save handlers for export dropdown and keyboard shortcut
2026-02-22 09:11:25 -08:00
const handleDownloadAnnotations = ( ) = > {
2026-02-09 11:28:09 -08:00
setShowExportDropdown ( false ) ;
2026-02-22 09:11:25 -08:00
const blob = new Blob ( [ annotationsOutput ] , { type : 'text/plain' } ) ;
2026-02-09 11:28:09 -08:00
const url = URL . createObjectURL ( blob ) ;
const a = document . createElement ( 'a' ) ;
a . href = url ;
2026-02-22 09:11:25 -08:00
a . download = 'annotations.md' ;
2026-02-09 11:28:09 -08:00
a . click ( ) ;
URL . revokeObjectURL ( url ) ;
2026-02-22 09:11:25 -08:00
setNoteSaveToast ( { type : 'success' , message : 'Downloaded annotations' } ) ;
2026-02-09 11:28:09 -08:00
setTimeout ( ( ) = > setNoteSaveToast ( null ) , 3000 ) ;
} ;
const handleQuickSaveToNotes = async ( target : 'obsidian' | 'bear' ) = > {
setShowExportDropdown ( false ) ;
const body : { obsidian? : object ; bear? : object } = { } ;
if ( target === 'obsidian' ) {
const s = getObsidianSettings ( ) ;
const vaultPath = getEffectiveVaultPath ( s ) ;
if ( vaultPath ) {
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
body . obsidian = {
vaultPath ,
folder : s.folder || 'plannotator' ,
plan : markdown ,
. . . ( s . filenameFormat && { filenameFormat : s.filenameFormat } ) ,
} ;
2026-02-09 11:28:09 -08:00
}
}
if ( target === 'bear' ) {
body . bear = { plan : markdown } ;
}
try {
const res = await fetch ( '/api/save-notes' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' } ,
body : JSON.stringify ( body ) ,
} ) ;
const data = await res . json ( ) ;
const result = data . results ? . [ target ] ;
if ( result ? . success ) {
setNoteSaveToast ( { type : 'success' , message : ` Saved to ${ target === 'obsidian' ? 'Obsidian' : 'Bear' } ` } ) ;
} else {
setNoteSaveToast ( { type : 'error' , message : result?.error || 'Save failed' } ) ;
}
} catch {
setNoteSaveToast ( { type : 'error' , message : 'Save failed' } ) ;
}
setTimeout ( ( ) = > setNoteSaveToast ( null ) , 3000 ) ;
} ;
// Cmd/Ctrl+S keyboard shortcut — save to default notes app
useEffect ( ( ) = > {
const handleSaveShortcut = ( e : KeyboardEvent ) = > {
if ( e . key !== 's' || ! ( e . metaKey || e . ctrlKey ) ) return ;
const tag = ( e . target as HTMLElement ) ? . tagName ;
if ( tag === 'INPUT' || tag === 'TEXTAREA' ) return ;
if ( showExport || showFeedbackPrompt || showClaudeCodeWarning ||
2026-02-23 00:24:25 -08:00
showAgentWarning || showPermissionModeSetup || showUIFeaturesSetup || showPlanDiffMarketing || pendingPasteImage ) return ;
2026-02-09 11:28:09 -08:00
if ( submitted || ! isApiMode ) return ;
e . preventDefault ( ) ;
const defaultApp = getDefaultNotesApp ( ) ;
const obsOk = isObsidianConfigured ( ) ;
const bearOk = getBearSettings ( ) . enabled ;
if ( defaultApp === 'download' ) {
2026-02-22 09:11:25 -08:00
handleDownloadAnnotations ( ) ;
2026-02-09 11:28:09 -08:00
} else if ( defaultApp === 'obsidian' && obsOk ) {
handleQuickSaveToNotes ( 'obsidian' ) ;
} else if ( defaultApp === 'bear' && bearOk ) {
handleQuickSaveToNotes ( 'bear' ) ;
} else {
setInitialExportTab ( 'notes' ) ;
setShowExport ( true ) ;
}
} ;
window . addEventListener ( 'keydown' , handleSaveShortcut ) ;
return ( ) = > window . removeEventListener ( 'keydown' , handleSaveShortcut ) ;
} , [
showExport , showFeedbackPrompt , showClaudeCodeWarning , showAgentWarning ,
2026-02-23 00:24:25 -08:00
showPermissionModeSetup , showUIFeaturesSetup , showPlanDiffMarketing , pendingPasteImage ,
2026-02-22 09:11:25 -08:00
submitted , isApiMode , markdown , annotationsOutput ,
2026-02-09 11:28:09 -08:00
] ) ;
// Close export dropdown on click outside
useEffect ( ( ) = > {
if ( ! showExportDropdown ) return ;
const handleClickOutside = ( e : MouseEvent ) = > {
const target = e . target as HTMLElement ;
if ( ! target . closest ( '[data-export-dropdown]' ) ) {
setShowExportDropdown ( false ) ;
}
} ;
document . addEventListener ( 'mousedown' , handleClickOutside ) ;
return ( ) = > document . removeEventListener ( 'mousedown' , handleClickOutside ) ;
} , [ showExportDropdown ] ) ;
2026-01-10 00:33:07 +07:00
const agentName = useMemo ( ( ) = > {
if ( origin === 'opencode' ) return 'OpenCode' ;
if ( origin === 'claude-code' ) return 'Claude Code' ;
2026-02-23 00:24:25 -08:00
if ( origin === 'pi' ) return 'Pi' ;
2026-01-10 00:33:07 +07:00
return 'Coding Agent' ;
} , [ origin ] ) ;
2025-12-27 18:14:10 -08:00
return (
< ThemeProvider defaultTheme = "dark" >
< div className = "h-screen flex flex-col bg-background overflow-hidden" >
{ /* Tater sprites */ }
{ taterMode && < TaterSpriteRunning / > }
{ /* Minimal Header */ }
feat: TOC sidebar, sticky actions, and settings redesign (#122)
* feat: Add Table of Contents sidebar with sticky action buttons
- Add hierarchical Table of Contents component with clickable navigation
- Implement useActiveSection hook for real-time section highlighting
- Add annotationHelpers utility for block identification
- Make Images, Global comment, and Copy plan buttons sticky during scroll
- Fix navigation scrolling to work within scrollable main container
* fix: Correct typo in opencode.json (CALUDE -> CLAUDE)
* docs: Update testing documentation - separate UI tests from integration/utility tests
* fix(ui): prevent code block annotation from breaking syntax highlighting
- Replace surroundContents() with plain text wrapper approach
- Add syntax highlighting restoration in removeHighlight()
- Fixes issue reported by kkharji in PR #122
The old approach used range.surroundContents() which wrapped syntax-highlighted
<span> elements, creating nested structure that broke the layout. The new approach
replaces code block innerHTML with plain text wrapped in <mark>, then restores
syntax highlighting when the annotation is removed.
Test coverage: 15 tests pass with edge cases for empty blocks, special chars,
large blocks (10k), unicode, and multiple annotation cycles.
* test(ui): add comprehensive code block annotation regression tests
- Add 15 test cases covering code block annotation behavior
- Test plain text wrapper approach vs nested span approach
- Verify syntax highlighting restoration on annotation removal
- Cover edge cases: empty blocks, special chars, large blocks, unicode
- Add happy-dom dev dependency for DOM testing
* feat: update opencode.json to include additional documentation instructions and configure Playwright MCP
* chore: stop tracking docs directory and opencode.json
* chore: move UI-TESTING.md to tests directory
* docs: extract UI testing checklist into separate file
- Move comprehensive feature checklists to UI-TESTING-CHECKLIST.md
- Keep main UI-TESTING.md focused on development workflow and setup
- Add reference link to checklist file for easy navigation
* docs: update UI testing documentation
* fix(ui): fix TOC active section tracking
IntersectionObserver was using root: null (viewport) instead of the
actual scroll container (<main>), and the effect only ran on mount
before headings were rendered. Pass the container as root and re-run
the observer when heading count changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* revert: remove Playwright MCP from marketplace.json and stale README links
Playwright MCP is dev tooling, not for end users. README referenced
docs/UI-TESTING.md and docs/CODE-STYLE.md which don't exist at those paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style(ui): compact TOC styling and fix layout shifts
- Smaller text (text-xs) and tighter padding for compact feel
- Remove border-l-2 and font-medium from active state to prevent
layout shifts that pushed text into multiline
- Annotation count badges are now perfect circles (w-5 h-5)
- Add left padding to root-level heading
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(ui): add optional TOC and sticky actions settings
- New uiPreferences.ts utility for cookie-based persistence
- TOC toggle: conditionally renders sidebar in App.tsx
- Sticky Actions toggle: conditionally applies sticky positioning in Viewer.tsx
- Both default to enabled, persisted via cookies
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(ui): redesign Settings dialog with sidebar navigation
Replace flat settings list with sidebar + content panel layout:
- General tab: Identity, Permission Mode, Agent Switching
- Display tab: TOC, Sticky Actions, Tater Mode
- Saving tab: Plan Saving, Obsidian, Bear Notes
- Scrollable content area (max-h-[70vh])
- Sidebar hidden in review mode (single tab)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(ui): fix sticky action bar positioning and add scroll-aware card background
Match main's button positioning across all breakpoints with responsive
margins that account for article padding. Use IntersectionObserver sentinel
to detect when the bar is stuck, revealing the card background only on scroll.
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-09 17:28:09 +00:00
< header className = "h-12 flex items-center justify-between px-2 md:px-4 border-b border-border/50 bg-card/50 backdrop-blur-xl sticky top-0 z-20" >
2025-12-27 18:14:10 -08:00
< div className = "flex items-center gap-2 md:gap-3" >
2025-12-28 14:44:54 -08:00
< a
href = "https://plannotator.ai"
target = "_blank"
rel = "noopener noreferrer"
2025-12-27 18:14:10 -08:00
className = "flex items-center gap-1.5 md:gap-2 hover:opacity-80 transition-opacity"
>
2025-12-28 13:17:15 -08:00
< span className = "text-sm font-semibold tracking-tight" > Plannotator < / span >
2025-12-28 14:44:54 -08:00
< / a >
2026-01-05 18:12:07 -08:00
< a
href = "https://github.com/backnotprop/plannotator/releases"
target = "_blank"
rel = "noopener noreferrer"
className = "text-xs text-muted-foreground font-mono opacity-60 hidden md:inline hover:opacity-100 transition-opacity"
>
v { typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '0.0.0' }
< / a >
2026-01-05 14:16:35 -08:00
{ origin && (
< span className = { ` text-[10px] px-1.5 py-0.5 rounded font-medium hidden md:inline ${
origin === 'claude-code'
? 'bg-orange-500/15 text-orange-400'
2026-02-23 00:24:25 -08:00
: origin === 'pi'
? 'bg-violet-500/15 text-violet-400'
: 'bg-zinc-500/20 text-zinc-400'
2026-01-05 14:16:35 -08:00
} ` } >
2026-01-10 00:33:07 +07:00
{ agentName }
2026-01-05 14:16:35 -08:00
< / span >
) }
2025-12-27 18:14:10 -08:00
< / div >
< div className = "flex items-center gap-1 md:gap-2" >
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-27 05:26:06 +01:00
{ isApiMode && ! linkedDocHook . isActive && (
2025-12-27 18:14:10 -08:00
< >
< button
onClick = { ( ) = > {
if ( annotations . length === 0 ) {
setShowFeedbackPrompt ( true ) ;
feat: named image references and annotate command (#147)
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:23:49 -08:00
} else if ( annotateMode ) {
handleAnnotateFeedback ( ) ;
2025-12-27 18:14:10 -08:00
} else {
handleDeny ( ) ;
}
} }
disabled = { isSubmitting }
className = { ` p-1.5 md:px-2.5 md:py-1 rounded-md text-xs font-medium transition-all ${
isSubmitting
? 'opacity-50 cursor-not-allowed bg-muted text-muted-foreground'
: 'bg-accent/15 text-accent hover:bg-accent/25 border border-accent/30'
} ` }
2026-01-12 19:36:09 -08:00
title = "Send Feedback"
2025-12-27 18:14:10 -08:00
>
< svg className = "w-4 h-4 md:hidden" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
< path strokeLinecap = "round" strokeLinejoin = "round" d = "M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" / >
< / svg >
feat: named image references and annotate command (#147)
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:23:49 -08:00
< span className = "hidden md:inline" > { isSubmitting ? 'Sending...' : annotateMode ? 'Send Annotations' : 'Send Feedback' } < / span >
2025-12-27 18:14:10 -08:00
< / button >
feat: named image references and annotate command (#147)
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:23:49 -08:00
{ ! annotateMode && < div className = "relative group/approve" >
2026-01-05 17:58:19 -08:00
< button
2026-01-06 18:26:11 -08:00
onClick = { ( ) = > {
// Show warning for Claude Code users with annotations
if ( origin === 'claude-code' && annotations . length > 0 ) {
setShowClaudeCodeWarning ( true ) ;
2026-01-19 11:07:22 -08:00
return ;
}
// Check if agent exists for OpenCode users
if ( origin === 'opencode' ) {
const warning = getAgentWarning ( ) ;
if ( warning ) {
setAgentWarningMessage ( warning ) ;
setShowAgentWarning ( true ) ;
return ;
}
2026-01-06 18:26:11 -08:00
}
2026-01-19 11:07:22 -08:00
handleApprove ( ) ;
2026-01-06 18:26:11 -08:00
} }
2026-01-05 17:58:19 -08:00
disabled = { isSubmitting }
className = { ` px-2 py-1 md:px-2.5 rounded-md text-xs font-medium transition-all ${
isSubmitting
? 'opacity-50 cursor-not-allowed bg-muted text-muted-foreground'
: origin === 'claude-code' && annotations . length > 0
2026-01-11 09:37:42 -08:00
? 'bg-success/50 text-success-foreground/70 hover:bg-success hover:text-success-foreground'
: 'bg-success text-success-foreground hover:opacity-90'
2026-01-05 17:58:19 -08:00
} ` }
>
< span className = "md:hidden" > { isSubmitting ? '...' : 'OK' } < / span >
< span className = "hidden md:inline" > { isSubmitting ? 'Approving...' : 'Approve' } < / span >
< / button >
{ origin === 'claude-code' && annotations . length > 0 && (
< div className = "absolute top-full right-0 mt-2 px-3 py-2 bg-popover border border-border rounded-lg shadow-xl text-xs text-foreground w-56 text-center opacity-0 invisible group-hover/approve:opacity-100 group-hover/approve:visible transition-all pointer-events-none z-50" >
< div className = "absolute bottom-full right-4 border-4 border-transparent border-b-border" / >
< div className = "absolute bottom-full right-4 mt-px border-4 border-transparent border-b-popover" / >
2026-01-10 00:33:07 +07:00
{ agentName } doesn 't support feedback on approval. Your annotations won' t be seen .
2026-01-05 17:58:19 -08:00
< / div >
) }
feat: named image references and annotate command (#147)
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:23:49 -08:00
< / div > }
2025-12-27 18:14:10 -08:00
< div className = "w-px h-5 bg-border/50 mx-1 hidden md:block" / >
< / >
) }
< ModeToggle / >
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-27 05:26:06 +01:00
{ ! linkedDocHook . isActive && < Settings taterMode = { taterMode } onTaterModeChange = { handleTaterModeChange } onIdentityChange = { handleIdentityChange } origin = { origin } onUIPreferencesChange = { setUiPrefs } / > }
2025-12-27 18:14:10 -08:00
< button
onClick = { ( ) = > setIsPanelOpen ( ! isPanelOpen ) }
className = { ` p-1.5 rounded-md text-xs font-medium transition-all ${
isPanelOpen
? 'bg-primary/15 text-primary'
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
} ` }
>
< svg className = "w-4 h-4" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
< path strokeLinecap = "round" strokeLinejoin = "round" d = "M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" / >
< / svg >
< / button >
2026-02-09 11:28:09 -08:00
< div className = "relative flex" data - export - dropdown >
< button
onClick = { ( ) = > { setInitialExportTab ( undefined ) ; setShowExport ( true ) ; } }
className = "p-1.5 md:px-2.5 md:py-1 rounded-l-md text-xs font-medium bg-muted hover:bg-muted/80 transition-colors"
title = "Export"
>
< svg className = "w-4 h-4 md:hidden" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
< path strokeLinecap = "round" strokeLinejoin = "round" d = "M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" / >
< / svg >
< span className = "hidden md:inline" > Export < / span >
< / button >
< button
onClick = { ( ) = > setShowExportDropdown ( prev = > ! prev ) }
className = "px-1 md:px-1.5 rounded-r-md text-xs bg-muted hover:bg-muted/80 border-l border-border/50 transition-colors flex items-center"
title = "Quick save options"
>
< svg className = "w-3 h-3" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
< path strokeLinecap = "round" strokeLinejoin = "round" d = "M19 9l-7 7-7-7" / >
< / svg >
< / button >
{ showExportDropdown && (
< div className = "absolute top-full right-0 mt-1 w-48 bg-popover border border-border rounded-lg shadow-xl z-50 py-1" >
{ sharingEnabled && (
< button
onClick = { async ( ) = > {
setShowExportDropdown ( false ) ;
try {
await navigator . clipboard . writeText ( shareUrl ) ;
setNoteSaveToast ( { type : 'success' , message : 'Share link copied' } ) ;
} catch {
setNoteSaveToast ( { type : 'error' , message : 'Failed to copy' } ) ;
}
setTimeout ( ( ) = > setNoteSaveToast ( null ) , 3000 ) ;
} }
className = "w-full text-left px-3 py-1.5 text-xs hover:bg-muted transition-colors flex items-center gap-2"
>
< svg className = "w-3.5 h-3.5 text-muted-foreground" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
< path strokeLinecap = "round" strokeLinejoin = "round" d = "M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" / >
< / svg >
Copy Share Link
< / button >
) }
< button
2026-02-22 09:11:25 -08:00
onClick = { handleDownloadAnnotations }
2026-02-09 11:28:09 -08:00
className = "w-full text-left px-3 py-1.5 text-xs hover:bg-muted transition-colors flex items-center gap-2"
>
< svg className = "w-3.5 h-3.5 text-muted-foreground" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
< path strokeLinecap = "round" strokeLinejoin = "round" d = "M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" / >
< / svg >
2026-02-22 09:11:25 -08:00
Download Annotations
2026-02-09 11:28:09 -08:00
< / button >
{ isApiMode && isObsidianConfigured ( ) && (
< button
onClick = { ( ) = > handleQuickSaveToNotes ( 'obsidian' ) }
className = "w-full text-left px-3 py-1.5 text-xs hover:bg-muted transition-colors flex items-center gap-2"
>
< svg className = "w-3.5 h-3.5 text-muted-foreground" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
< path strokeLinecap = "round" strokeLinejoin = "round" d = "M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" / >
< / svg >
Save to Obsidian
< / button >
) }
{ isApiMode && getBearSettings ( ) . enabled && (
< button
onClick = { ( ) = > handleQuickSaveToNotes ( 'bear' ) }
className = "w-full text-left px-3 py-1.5 text-xs hover:bg-muted transition-colors flex items-center gap-2"
>
< svg className = "w-3.5 h-3.5 text-muted-foreground" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
< path strokeLinecap = "round" strokeLinejoin = "round" d = "M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" / >
< / svg >
Save to Bear
< / button >
) }
{ isApiMode && ! isObsidianConfigured ( ) && ! getBearSettings ( ) . enabled && (
< div className = "px-3 py-2 text-[10px] text-muted-foreground" >
No notes apps configured .
< / div >
) }
2026-02-09 20:37:33 +00:00
{ sharingEnabled && (
< >
< div className = "my-1 border-t border-border" / >
< button
onClick = { ( ) = > {
setShowExportDropdown ( false ) ;
setShowImport ( true ) ;
} }
className = "w-full text-left px-3 py-1.5 text-xs hover:bg-muted transition-colors flex items-center gap-2"
>
< svg className = "w-3.5 h-3.5 text-muted-foreground" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
< path strokeLinecap = "round" strokeLinejoin = "round" d = "M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m4-4l-4 4m0 0l-4-4m4 4V4" / >
< / svg >
Import Review
< / button >
< / >
) }
2026-02-09 11:28:09 -08:00
< / div >
) }
< / div >
2025-12-27 18:14:10 -08:00
< / div >
< / header >
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-27 05:26:06 +01:00
{ /* Linked document error banner */ }
{ linkedDocHook . error && (
< div className = "bg-destructive/10 border-b border-destructive/20 px-4 py-2 flex items-center gap-2 flex-shrink-0" >
< span className = "text-xs text-destructive" > { linkedDocHook . error } < / span >
< button
onClick = { linkedDocHook . dismissError }
className = "ml-auto text-xs text-destructive/60 hover:text-destructive"
>
dismiss
< / button >
< / div >
) }
2025-12-27 18:14:10 -08:00
{ /* Main Content */ }
2026-02-13 04:19:14 +09:00
< div className = { ` flex-1 flex overflow-hidden ${ isResizing ? 'select-none' : '' } ` } >
feat: plan diff UI with sidebar and dual view modes (#176)
* feat: add plan diff UI with sidebar, badge, and dual view modes
Shows what changed between plan iterations when Claude revises after
feedback. Adds a +N/-M badge below repo info that toggles the diff view,
a shared left sidebar with TOC and Version Browser tabs, and two diff
modes: rendered (color-coded borders) and raw markdown (+/- lines).
Closes #138, closes #111
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update CLAUDE.md project structure and align first-run dialog labels
- Add plan-diff/ and sidebar/ component subdirectories to CLAUDE.md
- Add new hooks and utils to CLAUDE.md project structure
- Rename "Table of Contents" to "Auto-open Sidebar" in UIFeaturesSetup
to match Settings.tsx label
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address code review findings for plan diff UX
- Fix badge stats mixing block counts with line counts (modifications
now fold into additions/deletions)
- Gate hasPreviousVersion on diffBasePlan being loaded to prevent
"Show Changes" no-op and ModeSwitcher disappearing
- Make sidebar reactive to Settings toggle (useEffect on tocEnabled)
- Match PlanDiffViewer badge layout to Viewer (flex-col) so badge
doesn't jump position on toggle
- Add "Exit Diff" label to the close button in diff view
- Remove dead CSS (plan-diff-removed-marker, plan-diff-modified)
- Clean up stale header comment and unused lines prop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: second-round review cleanup for plan diff UX
- Fix stale "amber border" JSDoc in PlanCleanDiffView (actually green)
- Rename sidebar tab from "diff" to "versions" for clarity
- Gate VersionBrowser fetch on versionInfo being available
- Move .sidebar-tab-flag CSS into its own Sidebar section
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add loading state for version selection in sidebar
Add isSelectingVersion to selectBaseVersion, mirroring the existing
isLoadingVersions pattern. Shows "Loading..." on the selected version
button while the fetch is in progress.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address third-round code review findings
- Fix duplicate border/backdrop on TOC inside sidebar (className override)
- Fix loading indicator targeting wrong version button (fetchingVersion state)
- Fix "Show Changes" button silent no-op (gate on hasPreviousVersion)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move date to slug suffix, improve Other Plans UX
- Slug format changed from YYYY-MM-DD-{heading} to {heading}-YYYY-MM-DD
- Other Plans: single "coming soon" banner instead of per-item labels
- Strip date suffix from plan names in sidebar for readability
- Remove cursor-not-allowed from Other Plans items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add Plan Diff section to CLAUDE.md, alert on version fetch failure
- Document plan diff feature: engine, view modes, state management, sidebar
- Update slug format documentation to {heading}-YYYY-MM-DD
- Show native alert when version fetch fails instead of silent swallow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add table rendering to clean diff view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:59:44 -08:00
{ /* Left Sidebar: collapsed tab flags (when sidebar is closed) */ }
{ ! sidebar . isOpen && (
< SidebarTabs
activeTab = { sidebar . activeTab }
onToggleTab = { sidebar . toggleTab }
hasDiff = { planDiff . hasPreviousVersion }
2026-03-03 15:32:59 -08:00
showVaultTab = { showVaultTab }
feat: plan diff UI with sidebar and dual view modes (#176)
* feat: add plan diff UI with sidebar, badge, and dual view modes
Shows what changed between plan iterations when Claude revises after
feedback. Adds a +N/-M badge below repo info that toggles the diff view,
a shared left sidebar with TOC and Version Browser tabs, and two diff
modes: rendered (color-coded borders) and raw markdown (+/- lines).
Closes #138, closes #111
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update CLAUDE.md project structure and align first-run dialog labels
- Add plan-diff/ and sidebar/ component subdirectories to CLAUDE.md
- Add new hooks and utils to CLAUDE.md project structure
- Rename "Table of Contents" to "Auto-open Sidebar" in UIFeaturesSetup
to match Settings.tsx label
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address code review findings for plan diff UX
- Fix badge stats mixing block counts with line counts (modifications
now fold into additions/deletions)
- Gate hasPreviousVersion on diffBasePlan being loaded to prevent
"Show Changes" no-op and ModeSwitcher disappearing
- Make sidebar reactive to Settings toggle (useEffect on tocEnabled)
- Match PlanDiffViewer badge layout to Viewer (flex-col) so badge
doesn't jump position on toggle
- Add "Exit Diff" label to the close button in diff view
- Remove dead CSS (plan-diff-removed-marker, plan-diff-modified)
- Clean up stale header comment and unused lines prop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: second-round review cleanup for plan diff UX
- Fix stale "amber border" JSDoc in PlanCleanDiffView (actually green)
- Rename sidebar tab from "diff" to "versions" for clarity
- Gate VersionBrowser fetch on versionInfo being available
- Move .sidebar-tab-flag CSS into its own Sidebar section
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add loading state for version selection in sidebar
Add isSelectingVersion to selectBaseVersion, mirroring the existing
isLoadingVersions pattern. Shows "Loading..." on the selected version
button while the fetch is in progress.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address third-round code review findings
- Fix duplicate border/backdrop on TOC inside sidebar (className override)
- Fix loading indicator targeting wrong version button (fetchingVersion state)
- Fix "Show Changes" button silent no-op (gate on hasPreviousVersion)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move date to slug suffix, improve Other Plans UX
- Slug format changed from YYYY-MM-DD-{heading} to {heading}-YYYY-MM-DD
- Other Plans: single "coming soon" banner instead of per-item labels
- Strip date suffix from plan names in sidebar for readability
- Remove cursor-not-allowed from Other Plans items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add Plan Diff section to CLAUDE.md, alert on version fetch failure
- Document plan diff feature: engine, view modes, state management, sidebar
- Update slug format documentation to {heading}-YYYY-MM-DD
- Show native alert when version fetch fails instead of silent swallow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add table rendering to clean diff view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:59:44 -08:00
className = "hidden lg:flex"
/ >
) }
{ /* Left Sidebar: open state (TOC or Version Browser) */ }
{ sidebar . isOpen && (
2026-02-13 04:19:14 +09:00
< >
feat: plan diff UI with sidebar and dual view modes (#176)
* feat: add plan diff UI with sidebar, badge, and dual view modes
Shows what changed between plan iterations when Claude revises after
feedback. Adds a +N/-M badge below repo info that toggles the diff view,
a shared left sidebar with TOC and Version Browser tabs, and two diff
modes: rendered (color-coded borders) and raw markdown (+/- lines).
Closes #138, closes #111
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update CLAUDE.md project structure and align first-run dialog labels
- Add plan-diff/ and sidebar/ component subdirectories to CLAUDE.md
- Add new hooks and utils to CLAUDE.md project structure
- Rename "Table of Contents" to "Auto-open Sidebar" in UIFeaturesSetup
to match Settings.tsx label
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address code review findings for plan diff UX
- Fix badge stats mixing block counts with line counts (modifications
now fold into additions/deletions)
- Gate hasPreviousVersion on diffBasePlan being loaded to prevent
"Show Changes" no-op and ModeSwitcher disappearing
- Make sidebar reactive to Settings toggle (useEffect on tocEnabled)
- Match PlanDiffViewer badge layout to Viewer (flex-col) so badge
doesn't jump position on toggle
- Add "Exit Diff" label to the close button in diff view
- Remove dead CSS (plan-diff-removed-marker, plan-diff-modified)
- Clean up stale header comment and unused lines prop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: second-round review cleanup for plan diff UX
- Fix stale "amber border" JSDoc in PlanCleanDiffView (actually green)
- Rename sidebar tab from "diff" to "versions" for clarity
- Gate VersionBrowser fetch on versionInfo being available
- Move .sidebar-tab-flag CSS into its own Sidebar section
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add loading state for version selection in sidebar
Add isSelectingVersion to selectBaseVersion, mirroring the existing
isLoadingVersions pattern. Shows "Loading..." on the selected version
button while the fetch is in progress.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address third-round code review findings
- Fix duplicate border/backdrop on TOC inside sidebar (className override)
- Fix loading indicator targeting wrong version button (fetchingVersion state)
- Fix "Show Changes" button silent no-op (gate on hasPreviousVersion)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move date to slug suffix, improve Other Plans UX
- Slug format changed from YYYY-MM-DD-{heading} to {heading}-YYYY-MM-DD
- Other Plans: single "coming soon" banner instead of per-item labels
- Strip date suffix from plan names in sidebar for readability
- Remove cursor-not-allowed from Other Plans items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add Plan Diff section to CLAUDE.md, alert on version fetch failure
- Document plan diff feature: engine, view modes, state management, sidebar
- Update slug format documentation to {heading}-YYYY-MM-DD
- Show native alert when version fetch fails instead of silent swallow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add table rendering to clean diff view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:59:44 -08:00
< SidebarContainer
activeTab = { sidebar . activeTab }
onTabChange = { sidebar . toggleTab }
onClose = { sidebar . close }
width = { tocResize . width }
2026-02-13 04:19:14 +09:00
blocks = { blocks }
annotations = { annotations }
feat: plan diff UI with sidebar and dual view modes (#176)
* feat: add plan diff UI with sidebar, badge, and dual view modes
Shows what changed between plan iterations when Claude revises after
feedback. Adds a +N/-M badge below repo info that toggles the diff view,
a shared left sidebar with TOC and Version Browser tabs, and two diff
modes: rendered (color-coded borders) and raw markdown (+/- lines).
Closes #138, closes #111
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update CLAUDE.md project structure and align first-run dialog labels
- Add plan-diff/ and sidebar/ component subdirectories to CLAUDE.md
- Add new hooks and utils to CLAUDE.md project structure
- Rename "Table of Contents" to "Auto-open Sidebar" in UIFeaturesSetup
to match Settings.tsx label
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address code review findings for plan diff UX
- Fix badge stats mixing block counts with line counts (modifications
now fold into additions/deletions)
- Gate hasPreviousVersion on diffBasePlan being loaded to prevent
"Show Changes" no-op and ModeSwitcher disappearing
- Make sidebar reactive to Settings toggle (useEffect on tocEnabled)
- Match PlanDiffViewer badge layout to Viewer (flex-col) so badge
doesn't jump position on toggle
- Add "Exit Diff" label to the close button in diff view
- Remove dead CSS (plan-diff-removed-marker, plan-diff-modified)
- Clean up stale header comment and unused lines prop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: second-round review cleanup for plan diff UX
- Fix stale "amber border" JSDoc in PlanCleanDiffView (actually green)
- Rename sidebar tab from "diff" to "versions" for clarity
- Gate VersionBrowser fetch on versionInfo being available
- Move .sidebar-tab-flag CSS into its own Sidebar section
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add loading state for version selection in sidebar
Add isSelectingVersion to selectBaseVersion, mirroring the existing
isLoadingVersions pattern. Shows "Loading..." on the selected version
button while the fetch is in progress.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address third-round code review findings
- Fix duplicate border/backdrop on TOC inside sidebar (className override)
- Fix loading indicator targeting wrong version button (fetchingVersion state)
- Fix "Show Changes" button silent no-op (gate on hasPreviousVersion)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move date to slug suffix, improve Other Plans UX
- Slug format changed from YYYY-MM-DD-{heading} to {heading}-YYYY-MM-DD
- Other Plans: single "coming soon" banner instead of per-item labels
- Strip date suffix from plan names in sidebar for readability
- Remove cursor-not-allowed from Other Plans items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add Plan Diff section to CLAUDE.md, alert on version fetch failure
- Document plan diff feature: engine, view modes, state management, sidebar
- Update slug format documentation to {heading}-YYYY-MM-DD
- Show native alert when version fetch fails instead of silent swallow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add table rendering to clean diff view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:59:44 -08:00
activeSection = { activeSection }
onTocNavigate = { handleTocNavigate }
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-27 05:26:06 +01:00
linkedDocFilepath = { linkedDocHook . filepath }
2026-03-03 15:32:59 -08:00
onLinkedDocBack = { linkedDocHook . isActive ? handleLinkedDocBack : undefined }
showVaultTab = { showVaultTab }
vaultPath = { vaultPath }
vaultBrowser = { vaultBrowser }
onVaultSelectFile = { handleVaultFileSelect }
onVaultFetchTree = { handleVaultFetchTree }
feat: plan diff UI with sidebar and dual view modes (#176)
* feat: add plan diff UI with sidebar, badge, and dual view modes
Shows what changed between plan iterations when Claude revises after
feedback. Adds a +N/-M badge below repo info that toggles the diff view,
a shared left sidebar with TOC and Version Browser tabs, and two diff
modes: rendered (color-coded borders) and raw markdown (+/- lines).
Closes #138, closes #111
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update CLAUDE.md project structure and align first-run dialog labels
- Add plan-diff/ and sidebar/ component subdirectories to CLAUDE.md
- Add new hooks and utils to CLAUDE.md project structure
- Rename "Table of Contents" to "Auto-open Sidebar" in UIFeaturesSetup
to match Settings.tsx label
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address code review findings for plan diff UX
- Fix badge stats mixing block counts with line counts (modifications
now fold into additions/deletions)
- Gate hasPreviousVersion on diffBasePlan being loaded to prevent
"Show Changes" no-op and ModeSwitcher disappearing
- Make sidebar reactive to Settings toggle (useEffect on tocEnabled)
- Match PlanDiffViewer badge layout to Viewer (flex-col) so badge
doesn't jump position on toggle
- Add "Exit Diff" label to the close button in diff view
- Remove dead CSS (plan-diff-removed-marker, plan-diff-modified)
- Clean up stale header comment and unused lines prop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: second-round review cleanup for plan diff UX
- Fix stale "amber border" JSDoc in PlanCleanDiffView (actually green)
- Rename sidebar tab from "diff" to "versions" for clarity
- Gate VersionBrowser fetch on versionInfo being available
- Move .sidebar-tab-flag CSS into its own Sidebar section
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add loading state for version selection in sidebar
Add isSelectingVersion to selectBaseVersion, mirroring the existing
isLoadingVersions pattern. Shows "Loading..." on the selected version
button while the fetch is in progress.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address third-round code review findings
- Fix duplicate border/backdrop on TOC inside sidebar (className override)
- Fix loading indicator targeting wrong version button (fetchingVersion state)
- Fix "Show Changes" button silent no-op (gate on hasPreviousVersion)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move date to slug suffix, improve Other Plans UX
- Slug format changed from YYYY-MM-DD-{heading} to {heading}-YYYY-MM-DD
- Other Plans: single "coming soon" banner instead of per-item labels
- Strip date suffix from plan names in sidebar for readability
- Remove cursor-not-allowed from Other Plans items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add Plan Diff section to CLAUDE.md, alert on version fetch failure
- Document plan diff feature: engine, view modes, state management, sidebar
- Update slug format documentation to {heading}-YYYY-MM-DD
- Show native alert when version fetch fails instead of silent swallow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add table rendering to clean diff view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:59:44 -08:00
versionInfo = { versionInfo }
versions = { planDiff . versions }
projectPlans = { planDiff . projectPlans }
selectedBaseVersion = { planDiff . diffBaseVersion }
onSelectBaseVersion = { planDiff . selectBaseVersion }
isPlanDiffActive = { isPlanDiffActive }
hasPreviousVersion = { planDiff . hasPreviousVersion }
onActivatePlanDiff = { ( ) = > setIsPlanDiffActive ( true ) }
isLoadingVersions = { planDiff . isLoadingVersions }
isSelectingVersion = { planDiff . isSelectingVersion }
fetchingVersion = { planDiff . fetchingVersion }
onFetchVersions = { planDiff . fetchVersions }
onFetchProjectPlans = { planDiff . fetchProjectPlans }
2026-02-13 04:19:14 +09:00
/ >
< ResizeHandle { ...tocResize.handleProps } className = "hidden lg:block" / >
< / >
feat: TOC sidebar, sticky actions, and settings redesign (#122)
* feat: Add Table of Contents sidebar with sticky action buttons
- Add hierarchical Table of Contents component with clickable navigation
- Implement useActiveSection hook for real-time section highlighting
- Add annotationHelpers utility for block identification
- Make Images, Global comment, and Copy plan buttons sticky during scroll
- Fix navigation scrolling to work within scrollable main container
* fix: Correct typo in opencode.json (CALUDE -> CLAUDE)
* docs: Update testing documentation - separate UI tests from integration/utility tests
* fix(ui): prevent code block annotation from breaking syntax highlighting
- Replace surroundContents() with plain text wrapper approach
- Add syntax highlighting restoration in removeHighlight()
- Fixes issue reported by kkharji in PR #122
The old approach used range.surroundContents() which wrapped syntax-highlighted
<span> elements, creating nested structure that broke the layout. The new approach
replaces code block innerHTML with plain text wrapped in <mark>, then restores
syntax highlighting when the annotation is removed.
Test coverage: 15 tests pass with edge cases for empty blocks, special chars,
large blocks (10k), unicode, and multiple annotation cycles.
* test(ui): add comprehensive code block annotation regression tests
- Add 15 test cases covering code block annotation behavior
- Test plain text wrapper approach vs nested span approach
- Verify syntax highlighting restoration on annotation removal
- Cover edge cases: empty blocks, special chars, large blocks, unicode
- Add happy-dom dev dependency for DOM testing
* feat: update opencode.json to include additional documentation instructions and configure Playwright MCP
* chore: stop tracking docs directory and opencode.json
* chore: move UI-TESTING.md to tests directory
* docs: extract UI testing checklist into separate file
- Move comprehensive feature checklists to UI-TESTING-CHECKLIST.md
- Keep main UI-TESTING.md focused on development workflow and setup
- Add reference link to checklist file for easy navigation
* docs: update UI testing documentation
* fix(ui): fix TOC active section tracking
IntersectionObserver was using root: null (viewport) instead of the
actual scroll container (<main>), and the effect only ran on mount
before headings were rendered. Pass the container as root and re-run
the observer when heading count changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* revert: remove Playwright MCP from marketplace.json and stale README links
Playwright MCP is dev tooling, not for end users. README referenced
docs/UI-TESTING.md and docs/CODE-STYLE.md which don't exist at those paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style(ui): compact TOC styling and fix layout shifts
- Smaller text (text-xs) and tighter padding for compact feel
- Remove border-l-2 and font-medium from active state to prevent
layout shifts that pushed text into multiline
- Annotation count badges are now perfect circles (w-5 h-5)
- Add left padding to root-level heading
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(ui): add optional TOC and sticky actions settings
- New uiPreferences.ts utility for cookie-based persistence
- TOC toggle: conditionally renders sidebar in App.tsx
- Sticky Actions toggle: conditionally applies sticky positioning in Viewer.tsx
- Both default to enabled, persisted via cookies
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(ui): redesign Settings dialog with sidebar navigation
Replace flat settings list with sidebar + content panel layout:
- General tab: Identity, Permission Mode, Agent Switching
- Display tab: TOC, Sticky Actions, Tater Mode
- Saving tab: Plan Saving, Obsidian, Bear Notes
- Scrollable content area (max-h-[70vh])
- Sidebar hidden in review mode (single tab)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(ui): fix sticky action bar positioning and add scroll-aware card background
Match main's button positioning across all breakpoints with responsive
margins that account for article padding. Use IntersectionObserver sentinel
to detect when the bar is stuck, revealing the card background only on scroll.
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-09 17:28:09 +00:00
) }
2025-12-27 18:14:10 -08:00
{ /* Document Area */ }
2026-02-13 04:19:14 +09:00
< main ref = { containerRef } className = "flex-1 min-w-0 overflow-y-auto bg-grid" >
2026-01-06 14:33:03 -07:00
< div className = "min-h-full flex flex-col items-center px-4 py-3 md:px-10 md:py-8 xl:px-16" >
feat: plan diff UI with sidebar and dual view modes (#176)
* feat: add plan diff UI with sidebar, badge, and dual view modes
Shows what changed between plan iterations when Claude revises after
feedback. Adds a +N/-M badge below repo info that toggles the diff view,
a shared left sidebar with TOC and Version Browser tabs, and two diff
modes: rendered (color-coded borders) and raw markdown (+/- lines).
Closes #138, closes #111
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update CLAUDE.md project structure and align first-run dialog labels
- Add plan-diff/ and sidebar/ component subdirectories to CLAUDE.md
- Add new hooks and utils to CLAUDE.md project structure
- Rename "Table of Contents" to "Auto-open Sidebar" in UIFeaturesSetup
to match Settings.tsx label
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address code review findings for plan diff UX
- Fix badge stats mixing block counts with line counts (modifications
now fold into additions/deletions)
- Gate hasPreviousVersion on diffBasePlan being loaded to prevent
"Show Changes" no-op and ModeSwitcher disappearing
- Make sidebar reactive to Settings toggle (useEffect on tocEnabled)
- Match PlanDiffViewer badge layout to Viewer (flex-col) so badge
doesn't jump position on toggle
- Add "Exit Diff" label to the close button in diff view
- Remove dead CSS (plan-diff-removed-marker, plan-diff-modified)
- Clean up stale header comment and unused lines prop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: second-round review cleanup for plan diff UX
- Fix stale "amber border" JSDoc in PlanCleanDiffView (actually green)
- Rename sidebar tab from "diff" to "versions" for clarity
- Gate VersionBrowser fetch on versionInfo being available
- Move .sidebar-tab-flag CSS into its own Sidebar section
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add loading state for version selection in sidebar
Add isSelectingVersion to selectBaseVersion, mirroring the existing
isLoadingVersions pattern. Shows "Loading..." on the selected version
button while the fetch is in progress.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address third-round code review findings
- Fix duplicate border/backdrop on TOC inside sidebar (className override)
- Fix loading indicator targeting wrong version button (fetchingVersion state)
- Fix "Show Changes" button silent no-op (gate on hasPreviousVersion)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move date to slug suffix, improve Other Plans UX
- Slug format changed from YYYY-MM-DD-{heading} to {heading}-YYYY-MM-DD
- Other Plans: single "coming soon" banner instead of per-item labels
- Strip date suffix from plan names in sidebar for readability
- Remove cursor-not-allowed from Other Plans items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add Plan Diff section to CLAUDE.md, alert on version fetch failure
- Document plan diff feature: engine, view modes, state management, sidebar
- Update slug format documentation to {heading}-YYYY-MM-DD
- Show native alert when version fetch fails instead of silent swallow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add table rendering to clean diff view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:59:44 -08:00
{ /* Mode Switcher (hidden during plan diff) */ }
{ ! isPlanDiffActive && (
< div className = "w-full max-w-[832px] 2xl:max-w-5xl mb-3 md:mb-4 flex justify-start" >
< ModeSwitcher mode = { editorMode } onChange = { handleEditorModeChange } taterMode = { taterMode } / >
< / div >
) }
2025-12-27 18:14:10 -08:00
feat: plan diff UI with sidebar and dual view modes (#176)
* feat: add plan diff UI with sidebar, badge, and dual view modes
Shows what changed between plan iterations when Claude revises after
feedback. Adds a +N/-M badge below repo info that toggles the diff view,
a shared left sidebar with TOC and Version Browser tabs, and two diff
modes: rendered (color-coded borders) and raw markdown (+/- lines).
Closes #138, closes #111
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update CLAUDE.md project structure and align first-run dialog labels
- Add plan-diff/ and sidebar/ component subdirectories to CLAUDE.md
- Add new hooks and utils to CLAUDE.md project structure
- Rename "Table of Contents" to "Auto-open Sidebar" in UIFeaturesSetup
to match Settings.tsx label
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address code review findings for plan diff UX
- Fix badge stats mixing block counts with line counts (modifications
now fold into additions/deletions)
- Gate hasPreviousVersion on diffBasePlan being loaded to prevent
"Show Changes" no-op and ModeSwitcher disappearing
- Make sidebar reactive to Settings toggle (useEffect on tocEnabled)
- Match PlanDiffViewer badge layout to Viewer (flex-col) so badge
doesn't jump position on toggle
- Add "Exit Diff" label to the close button in diff view
- Remove dead CSS (plan-diff-removed-marker, plan-diff-modified)
- Clean up stale header comment and unused lines prop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: second-round review cleanup for plan diff UX
- Fix stale "amber border" JSDoc in PlanCleanDiffView (actually green)
- Rename sidebar tab from "diff" to "versions" for clarity
- Gate VersionBrowser fetch on versionInfo being available
- Move .sidebar-tab-flag CSS into its own Sidebar section
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add loading state for version selection in sidebar
Add isSelectingVersion to selectBaseVersion, mirroring the existing
isLoadingVersions pattern. Shows "Loading..." on the selected version
button while the fetch is in progress.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address third-round code review findings
- Fix duplicate border/backdrop on TOC inside sidebar (className override)
- Fix loading indicator targeting wrong version button (fetchingVersion state)
- Fix "Show Changes" button silent no-op (gate on hasPreviousVersion)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move date to slug suffix, improve Other Plans UX
- Slug format changed from YYYY-MM-DD-{heading} to {heading}-YYYY-MM-DD
- Other Plans: single "coming soon" banner instead of per-item labels
- Strip date suffix from plan names in sidebar for readability
- Remove cursor-not-allowed from Other Plans items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add Plan Diff section to CLAUDE.md, alert on version fetch failure
- Document plan diff feature: engine, view modes, state management, sidebar
- Update slug format documentation to {heading}-YYYY-MM-DD
- Show native alert when version fetch fails instead of silent swallow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add table rendering to clean diff view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:59:44 -08:00
{ /* Plan Diff View or Normal Plan View */ }
{ isPlanDiffActive && planDiff . diffBlocks && planDiff . diffStats ? (
< PlanDiffViewer
diffBlocks = { planDiff . diffBlocks }
diffStats = { planDiff . diffStats }
diffMode = { planDiffMode }
onDiffModeChange = { setPlanDiffMode }
onPlanDiffToggle = { ( ) = > setIsPlanDiffActive ( false ) }
repoInfo = { repoInfo }
baseVersionLabel = { planDiff . diffBaseVersion != null ? ` v ${ planDiff . diffBaseVersion } ` : undefined }
2026-02-26 13:25:08 -08:00
baseVersion = { planDiff . diffBaseVersion ? ? undefined }
feat: plan diff UI with sidebar and dual view modes (#176)
* feat: add plan diff UI with sidebar, badge, and dual view modes
Shows what changed between plan iterations when Claude revises after
feedback. Adds a +N/-M badge below repo info that toggles the diff view,
a shared left sidebar with TOC and Version Browser tabs, and two diff
modes: rendered (color-coded borders) and raw markdown (+/- lines).
Closes #138, closes #111
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update CLAUDE.md project structure and align first-run dialog labels
- Add plan-diff/ and sidebar/ component subdirectories to CLAUDE.md
- Add new hooks and utils to CLAUDE.md project structure
- Rename "Table of Contents" to "Auto-open Sidebar" in UIFeaturesSetup
to match Settings.tsx label
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address code review findings for plan diff UX
- Fix badge stats mixing block counts with line counts (modifications
now fold into additions/deletions)
- Gate hasPreviousVersion on diffBasePlan being loaded to prevent
"Show Changes" no-op and ModeSwitcher disappearing
- Make sidebar reactive to Settings toggle (useEffect on tocEnabled)
- Match PlanDiffViewer badge layout to Viewer (flex-col) so badge
doesn't jump position on toggle
- Add "Exit Diff" label to the close button in diff view
- Remove dead CSS (plan-diff-removed-marker, plan-diff-modified)
- Clean up stale header comment and unused lines prop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: second-round review cleanup for plan diff UX
- Fix stale "amber border" JSDoc in PlanCleanDiffView (actually green)
- Rename sidebar tab from "diff" to "versions" for clarity
- Gate VersionBrowser fetch on versionInfo being available
- Move .sidebar-tab-flag CSS into its own Sidebar section
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add loading state for version selection in sidebar
Add isSelectingVersion to selectBaseVersion, mirroring the existing
isLoadingVersions pattern. Shows "Loading..." on the selected version
button while the fetch is in progress.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address third-round code review findings
- Fix duplicate border/backdrop on TOC inside sidebar (className override)
- Fix loading indicator targeting wrong version button (fetchingVersion state)
- Fix "Show Changes" button silent no-op (gate on hasPreviousVersion)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move date to slug suffix, improve Other Plans UX
- Slug format changed from YYYY-MM-DD-{heading} to {heading}-YYYY-MM-DD
- Other Plans: single "coming soon" banner instead of per-item labels
- Strip date suffix from plan names in sidebar for readability
- Remove cursor-not-allowed from Other Plans items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add Plan Diff section to CLAUDE.md, alert on version fetch failure
- Document plan diff feature: engine, view modes, state management, sidebar
- Update slug format documentation to {heading}-YYYY-MM-DD
- Show native alert when version fetch fails instead of silent swallow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add table rendering to clean diff view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:59:44 -08:00
/ >
) : (
< Viewer
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-27 05:26:06 +01:00
key = { linkedDocHook . isActive ? ` doc: ${ linkedDocHook . filepath } ` : 'plan' }
feat: plan diff UI with sidebar and dual view modes (#176)
* feat: add plan diff UI with sidebar, badge, and dual view modes
Shows what changed between plan iterations when Claude revises after
feedback. Adds a +N/-M badge below repo info that toggles the diff view,
a shared left sidebar with TOC and Version Browser tabs, and two diff
modes: rendered (color-coded borders) and raw markdown (+/- lines).
Closes #138, closes #111
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update CLAUDE.md project structure and align first-run dialog labels
- Add plan-diff/ and sidebar/ component subdirectories to CLAUDE.md
- Add new hooks and utils to CLAUDE.md project structure
- Rename "Table of Contents" to "Auto-open Sidebar" in UIFeaturesSetup
to match Settings.tsx label
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address code review findings for plan diff UX
- Fix badge stats mixing block counts with line counts (modifications
now fold into additions/deletions)
- Gate hasPreviousVersion on diffBasePlan being loaded to prevent
"Show Changes" no-op and ModeSwitcher disappearing
- Make sidebar reactive to Settings toggle (useEffect on tocEnabled)
- Match PlanDiffViewer badge layout to Viewer (flex-col) so badge
doesn't jump position on toggle
- Add "Exit Diff" label to the close button in diff view
- Remove dead CSS (plan-diff-removed-marker, plan-diff-modified)
- Clean up stale header comment and unused lines prop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: second-round review cleanup for plan diff UX
- Fix stale "amber border" JSDoc in PlanCleanDiffView (actually green)
- Rename sidebar tab from "diff" to "versions" for clarity
- Gate VersionBrowser fetch on versionInfo being available
- Move .sidebar-tab-flag CSS into its own Sidebar section
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add loading state for version selection in sidebar
Add isSelectingVersion to selectBaseVersion, mirroring the existing
isLoadingVersions pattern. Shows "Loading..." on the selected version
button while the fetch is in progress.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address third-round code review findings
- Fix duplicate border/backdrop on TOC inside sidebar (className override)
- Fix loading indicator targeting wrong version button (fetchingVersion state)
- Fix "Show Changes" button silent no-op (gate on hasPreviousVersion)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move date to slug suffix, improve Other Plans UX
- Slug format changed from YYYY-MM-DD-{heading} to {heading}-YYYY-MM-DD
- Other Plans: single "coming soon" banner instead of per-item labels
- Strip date suffix from plan names in sidebar for readability
- Remove cursor-not-allowed from Other Plans items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add Plan Diff section to CLAUDE.md, alert on version fetch failure
- Document plan diff feature: engine, view modes, state management, sidebar
- Update slug format documentation to {heading}-YYYY-MM-DD
- Show native alert when version fetch fails instead of silent swallow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add table rendering to clean diff view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:59:44 -08:00
ref = { viewerRef }
blocks = { blocks }
markdown = { markdown }
frontmatter = { frontmatter }
annotations = { annotations }
onAddAnnotation = { handleAddAnnotation }
onSelectAnnotation = { setSelectedAnnotationId }
selectedAnnotationId = { selectedAnnotationId }
mode = { editorMode }
taterMode = { taterMode }
globalAttachments = { globalAttachments }
onAddGlobalAttachment = { handleAddGlobalAttachment }
onRemoveGlobalAttachment = { handleRemoveGlobalAttachment }
repoInfo = { repoInfo }
stickyActions = { uiPrefs . stickyActionsEnabled }
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-27 05:26:06 +01:00
planDiffStats = { linkedDocHook . isActive ? null : planDiff . diffStats }
feat: plan diff UI with sidebar and dual view modes (#176)
* feat: add plan diff UI with sidebar, badge, and dual view modes
Shows what changed between plan iterations when Claude revises after
feedback. Adds a +N/-M badge below repo info that toggles the diff view,
a shared left sidebar with TOC and Version Browser tabs, and two diff
modes: rendered (color-coded borders) and raw markdown (+/- lines).
Closes #138, closes #111
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update CLAUDE.md project structure and align first-run dialog labels
- Add plan-diff/ and sidebar/ component subdirectories to CLAUDE.md
- Add new hooks and utils to CLAUDE.md project structure
- Rename "Table of Contents" to "Auto-open Sidebar" in UIFeaturesSetup
to match Settings.tsx label
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address code review findings for plan diff UX
- Fix badge stats mixing block counts with line counts (modifications
now fold into additions/deletions)
- Gate hasPreviousVersion on diffBasePlan being loaded to prevent
"Show Changes" no-op and ModeSwitcher disappearing
- Make sidebar reactive to Settings toggle (useEffect on tocEnabled)
- Match PlanDiffViewer badge layout to Viewer (flex-col) so badge
doesn't jump position on toggle
- Add "Exit Diff" label to the close button in diff view
- Remove dead CSS (plan-diff-removed-marker, plan-diff-modified)
- Clean up stale header comment and unused lines prop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: second-round review cleanup for plan diff UX
- Fix stale "amber border" JSDoc in PlanCleanDiffView (actually green)
- Rename sidebar tab from "diff" to "versions" for clarity
- Gate VersionBrowser fetch on versionInfo being available
- Move .sidebar-tab-flag CSS into its own Sidebar section
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add loading state for version selection in sidebar
Add isSelectingVersion to selectBaseVersion, mirroring the existing
isLoadingVersions pattern. Shows "Loading..." on the selected version
button while the fetch is in progress.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address third-round code review findings
- Fix duplicate border/backdrop on TOC inside sidebar (className override)
- Fix loading indicator targeting wrong version button (fetchingVersion state)
- Fix "Show Changes" button silent no-op (gate on hasPreviousVersion)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move date to slug suffix, improve Other Plans UX
- Slug format changed from YYYY-MM-DD-{heading} to {heading}-YYYY-MM-DD
- Other Plans: single "coming soon" banner instead of per-item labels
- Strip date suffix from plan names in sidebar for readability
- Remove cursor-not-allowed from Other Plans items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add Plan Diff section to CLAUDE.md, alert on version fetch failure
- Document plan diff feature: engine, view modes, state management, sidebar
- Update slug format documentation to {heading}-YYYY-MM-DD
- Show native alert when version fetch fails instead of silent swallow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add table rendering to clean diff view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:59:44 -08:00
isPlanDiffActive = { isPlanDiffActive }
onPlanDiffToggle = { ( ) = > setIsPlanDiffActive ( ! isPlanDiffActive ) }
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-27 05:26:06 +01:00
hasPreviousVersion = { ! linkedDocHook . isActive && planDiff . hasPreviousVersion }
2026-03-02 16:42:56 -08:00
showDemoBadge = { ! isApiMode && ! isLoadingShared && ! isSharedSession }
2026-03-03 15:32:59 -08:00
onOpenLinkedDoc = { handleOpenLinkedDoc }
linkedDocInfo = { linkedDocHook . isActive ? { filepath : linkedDocHook.filepath ! , onBack : handleLinkedDocBack , label : vaultBrowser.activeFile ? 'Vault File' : undefined } : null }
feat: plan diff UI with sidebar and dual view modes (#176)
* feat: add plan diff UI with sidebar, badge, and dual view modes
Shows what changed between plan iterations when Claude revises after
feedback. Adds a +N/-M badge below repo info that toggles the diff view,
a shared left sidebar with TOC and Version Browser tabs, and two diff
modes: rendered (color-coded borders) and raw markdown (+/- lines).
Closes #138, closes #111
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: update CLAUDE.md project structure and align first-run dialog labels
- Add plan-diff/ and sidebar/ component subdirectories to CLAUDE.md
- Add new hooks and utils to CLAUDE.md project structure
- Rename "Table of Contents" to "Auto-open Sidebar" in UIFeaturesSetup
to match Settings.tsx label
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address code review findings for plan diff UX
- Fix badge stats mixing block counts with line counts (modifications
now fold into additions/deletions)
- Gate hasPreviousVersion on diffBasePlan being loaded to prevent
"Show Changes" no-op and ModeSwitcher disappearing
- Make sidebar reactive to Settings toggle (useEffect on tocEnabled)
- Match PlanDiffViewer badge layout to Viewer (flex-col) so badge
doesn't jump position on toggle
- Add "Exit Diff" label to the close button in diff view
- Remove dead CSS (plan-diff-removed-marker, plan-diff-modified)
- Clean up stale header comment and unused lines prop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: second-round review cleanup for plan diff UX
- Fix stale "amber border" JSDoc in PlanCleanDiffView (actually green)
- Rename sidebar tab from "diff" to "versions" for clarity
- Gate VersionBrowser fetch on versionInfo being available
- Move .sidebar-tab-flag CSS into its own Sidebar section
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add loading state for version selection in sidebar
Add isSelectingVersion to selectBaseVersion, mirroring the existing
isLoadingVersions pattern. Shows "Loading..." on the selected version
button while the fetch is in progress.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address third-round code review findings
- Fix duplicate border/backdrop on TOC inside sidebar (className override)
- Fix loading indicator targeting wrong version button (fetchingVersion state)
- Fix "Show Changes" button silent no-op (gate on hasPreviousVersion)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move date to slug suffix, improve Other Plans UX
- Slug format changed from YYYY-MM-DD-{heading} to {heading}-YYYY-MM-DD
- Other Plans: single "coming soon" banner instead of per-item labels
- Strip date suffix from plan names in sidebar for readability
- Remove cursor-not-allowed from Other Plans items
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add Plan Diff section to CLAUDE.md, alert on version fetch failure
- Document plan diff feature: engine, view modes, state management, sidebar
- Update slug format documentation to {heading}-YYYY-MM-DD
- Show native alert when version fetch fails instead of silent swallow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add table rendering to clean diff view
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:59:44 -08:00
/ >
) }
2025-12-27 18:14:10 -08:00
< / div >
< / main >
2026-02-13 04:19:14 +09:00
{ /* Resize Handle */ }
{ isPanelOpen && < ResizeHandle { ...panelResize.handleProps } / > }
2025-12-27 18:14:10 -08:00
{ /* Annotation Panel */ }
< AnnotationPanel
isOpen = { isPanelOpen }
blocks = { blocks }
annotations = { annotations }
selectedId = { selectedAnnotationId }
onSelect = { setSelectedAnnotationId }
onDelete = { handleDeleteAnnotation }
2026-01-13 23:29:47 -05:00
onEdit = { handleEditAnnotation }
2025-12-27 18:14:10 -08:00
shareUrl = { shareUrl }
2026-01-14 13:12:37 -08:00
sharingEnabled = { sharingEnabled }
2026-02-13 04:19:14 +09:00
width = { panelResize . width }
2025-12-27 18:14:10 -08:00
/ >
< / div >
{ /* Export Modal */ }
< ExportModal
isOpen = { showExport }
2026-02-09 11:28:09 -08:00
onClose = { ( ) = > { setShowExport ( false ) ; setInitialExportTab ( undefined ) ; } }
2025-12-27 18:14:10 -08:00
shareUrl = { shareUrl }
shareUrlSize = { shareUrlSize }
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-03-01 12:25:55 +05:30
shortShareUrl = { shortShareUrl }
isGeneratingShortUrl = { isGeneratingShortUrl }
shortUrlError = { shortUrlError }
onGenerateShortUrl = { generateShortUrl }
2026-02-22 09:11:25 -08:00
annotationsOutput = { annotationsOutput }
2025-12-27 18:14:10 -08:00
annotationCount = { annotations . length }
taterSprite = { taterMode ? < TaterSpritePullup / > : undefined }
2026-01-14 13:12:37 -08:00
sharingEnabled = { sharingEnabled }
2026-02-09 11:28:09 -08:00
markdown = { markdown }
isApiMode = { isApiMode }
initialTab = { initialExportTab }
2025-12-27 18:14:10 -08:00
/ >
2026-02-09 20:37:33 +00:00
{ /* Import Modal */ }
< ImportModal
isOpen = { showImport }
onClose = { ( ) = > setShowImport ( false ) }
onImport = { importFromShareUrl }
2026-02-12 15:07:43 -08:00
shareBaseUrl = { shareBaseUrl }
2026-02-09 20:37:33 +00:00
/ >
2025-12-27 18:14:10 -08:00
{ /* Feedback prompt dialog */ }
2026-01-06 18:26:11 -08:00
< ConfirmDialog
isOpen = { showFeedbackPrompt }
onClose = { ( ) = > setShowFeedbackPrompt ( false ) }
title = "Add Annotations First"
2026-01-10 00:33:07 +07:00
message = { ` To provide feedback, select text in the plan and add annotations. ${ agentName } will use your annotations to revise the plan. ` }
2026-01-06 18:26:11 -08:00
variant = "info"
/ >
{ /* Claude Code annotation warning dialog */ }
< ConfirmDialog
isOpen = { showClaudeCodeWarning }
onClose = { ( ) = > setShowClaudeCodeWarning ( false ) }
onConfirm = { ( ) = > {
setShowClaudeCodeWarning ( false ) ;
handleApprove ( ) ;
} }
title = "Annotations Won't Be Sent"
2026-01-10 00:33:07 +07:00
message = { < > { agentName } doesn 't yet support feedback on approval. Your {annotations.length} annotation{annotations.length !== 1 ? ' s ' : ' '} will be lost.</>}
2026-01-06 18:26:11 -08:00
subMessage={
<>
2026-01-12 19:36:09 -08:00
To send feedback, use <strong>Send Feedback</strong> instead.
2026-01-06 18:26:11 -08:00
<br /><br />
Want this feature? Upvote these issues:
<br />
<a href="https://github.com/anthropics/claude-code/issues/16001" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">#16001</a>
{' · '}
<a href="https://github.com/anthropics/claude-code/issues/15755" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">#15755</a>
</>
}
confirmText="Approve Anyway"
cancelText="Cancel"
variant="warning"
showCancel
/>
2025-12-27 18:14:10 -08:00
2026-01-19 11:07:22 -08:00
{/* OpenCode agent not found warning dialog */}
<ConfirmDialog
isOpen={showAgentWarning}
onClose={() => setShowAgentWarning(false)}
onConfirm={() => {
setShowAgentWarning(false);
handleApprove();
}}
title="Agent Not Found"
message={agentWarningMessage}
subMessage={
<>
You can change the agent in <strong>Settings</strong>, or approve anyway and OpenCode will use the default agent.
</>
}
confirmText="Approve Anyway"
cancelText="Cancel"
variant="warning"
showCancel
/>
2026-03-02 16:42:56 -08:00
{/* Shared URL load failure warning */}
<ConfirmDialog
isOpen={!!shareLoadError && !isApiMode}
onClose={clearShareLoadError}
title="Shared Plan Could Not Be Loaded"
message={shareLoadError}
subMessage="You are viewing a demo plan. This is sample content — it is not your data or anyone else' s . "
variant=" warning "
/ >
2026-02-09 11:28:09 -08:00
{ /* Save-to-notes toast */ }
{ noteSaveToast && (
< div className = { ` fixed top-16 right-4 z-50 px-3 py-2 rounded-lg text-xs font-medium shadow-lg transition-opacity ${
noteSaveToast . type === 'success'
? 'bg-success/15 text-success border border-success/30'
: 'bg-destructive/15 text-destructive border border-destructive/30'
} ` } >
{ noteSaveToast . message }
< / div >
) }
2025-12-27 18:14:10 -08:00
{ /* Completion overlay - shown after approve/deny */ }
2026-02-20 02:13:37 +09:00
< CompletionOverlay
submitted = { submitted }
title = { submitted === 'approved' ? 'Plan Approved' : annotateMode ? 'Annotations Sent' : 'Feedback Sent' }
subtitle = {
submitted === 'approved'
? ` ${ agentName } will proceed with the implementation. `
: annotateMode
? ` ${ agentName } will address your annotations on the file. `
: ` ${ agentName } will revise the plan based on your annotations. `
}
agentLabel = { agentName }
/ >
2025-12-28 17:42:08 -08:00
{ /* Update notification */ }
2026-01-12 19:36:09 -08:00
< UpdateBanner origin = { origin } / >
2026-01-05 17:06:43 -08:00
{ /* Image Annotator for pasted images */ }
< ImageAnnotator
isOpen = { ! ! pendingPasteImage }
imageSrc = { pendingPasteImage ? . blobUrl ? ? '' }
feat: named image references and annotate command (#147)
* feat: named image references and annotate command (#67, #109)
Add human-readable names to image attachments throughout the annotation
pipeline, and add a new `plannotator annotate <file.md>` command for
annotating arbitrary markdown files.
Image names: ImageAttachment type replaces plain string paths, upload
endpoints return originalName, editable name inputs under thumbnails,
[name] path format in exported feedback, backward-compatible sharing.
Annotate command: new server module reusing plan editor HTML with
mode:"annotate", CLI subcommand, slash commands for Claude Code and
OpenCode, annotate mode UI (hides Approve, shows Send Annotations).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: move image name input to ImageAnnotator screen
The name input now appears on the full-screen annotator modal that opens
immediately when uploading/pasting an image, pre-populated from the
filename. Removes the disruptive inline name editing from thumbnails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: stale closure in paste handler, update CLAUDE.md for new features
Fix race condition where globalAttachments was captured as empty array
in the paste event listener (missing dependency). Also update CLAUDE.md
to document ImageAttachment type, annotate server/flow, updated sharing
format with image support, and new slash commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:23:49 -08:00
initialName = { pendingPasteImage ? . initialName }
2026-01-05 17:06:43 -08:00
onAccept = { handlePasteAnnotatorAccept }
onClose = { handlePasteAnnotatorClose }
/ >
2026-01-13 20:31:23 -08:00
{ /* Permission Mode Setup (Claude Code first-time) */ }
< PermissionModeSetup
isOpen = { showPermissionModeSetup }
onComplete = { ( mode ) = > {
setPermissionMode ( mode ) ;
setShowPermissionModeSetup ( false ) ;
2026-02-09 11:52:36 -08:00
if ( needsUIFeaturesSetup ( ) ) {
setShowUIFeaturesSetup ( true ) ;
2026-02-23 00:24:25 -08:00
} else if ( needsPlanDiffMarketingDialog ( ) ) {
setShowPlanDiffMarketing ( true ) ;
2026-02-09 11:52:36 -08:00
}
} }
/ >
{ /* UI Features Setup (TOC & Sticky Actions) */ }
< UIFeaturesSetup
isOpen = { showUIFeaturesSetup }
onComplete = { ( prefs ) = > {
setUiPrefs ( prefs ) ;
setShowUIFeaturesSetup ( false ) ;
2026-02-23 00:24:25 -08:00
if ( needsPlanDiffMarketingDialog ( ) ) {
setShowPlanDiffMarketing ( true ) ;
}
} }
/ >
{ /* Plan Diff Marketing (feature announcement) */ }
< PlanDiffMarketing
isOpen = { showPlanDiffMarketing }
origin = { origin }
onComplete = { ( ) = > {
setShowPlanDiffMarketing ( false ) ;
2026-01-13 20:31:23 -08:00
} }
/ >
2025-12-27 18:14:10 -08:00
< / div >
< / ThemeProvider >
) ;
} ;
export default App ;