Skip to content
Merged
10 changes: 8 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ plannotator/
│ │ │ ├── plan-diff/ # PlanDiffBadge, PlanDiffViewer, clean/raw diff views
│ │ │ └── sidebar/ # SidebarContainer, SidebarTabs, VersionBrowser
│ │ ├── utils/ # parser.ts, sharing.ts, storage.ts, planSave.ts, agentSwitch.ts, planDiffEngine.ts
│ │ ├── hooks/ # useSharing.ts, usePlanDiff.ts, useSidebar.ts, useLinkedDoc.ts, useAnnotationDraft.ts, useCodeAnnotationDraft.ts
│ │ ├── hooks/ # useAnnotationHighlighter.ts, useSharing.ts, usePlanDiff.ts, useSidebar.ts, useLinkedDoc.ts, useAnnotationDraft.ts, useCodeAnnotationDraft.ts
│ │ └── types.ts
│ ├── shared/ # Cross-package types (EditorAnnotation)
│ ├── editor/ # Plan review App.tsx
Expand Down Expand Up @@ -227,6 +227,10 @@ When a user denies a plan and Claude resubmits, the UI shows what changed betwee

**State** (`packages/ui/hooks/usePlanDiff.ts`): Manages base version selection, diff computation, and version fetching. The server sends `previousPlan` with the initial `/api/plan` response; the hook auto-diffs against it. Users can select any prior version from the sidebar Version Browser.

**Diff annotations:** The clean diff view supports block-level annotation — hover over added/removed/modified sections to annotate entire blocks. Annotations carry a `diffContext` field (`added`/`removed`/`modified`). Exported feedback includes `[In diff content]` labels.

**Annotation hook** (`packages/ui/hooks/useAnnotationHighlighter.ts`): Annotation infrastructure used by `Viewer.tsx`. Manages web-highlighter lifecycle, toolbar/popover state, annotation creation, text-based restoration, and scroll-to-selected. The diff view uses its own block-level hover system instead.

**Sidebar** (`packages/ui/hooks/useSidebar.ts`): Shared left sidebar with two tabs — Table of Contents and Version Browser. The "Auto-open Sidebar" setting controls whether it opens on load (TOC tab only).

## Data Types
Expand Down Expand Up @@ -258,6 +262,7 @@ interface Annotation {
createdA: number; // Timestamp
author?: string; // Tater identity
images?: ImageAttachment[]; // Attached images with names
diffContext?: 'added' | 'removed' | 'modified'; // Set when annotation created in plan diff view
startMeta?: { parentTagName; parentIndex; textOffset };
endMeta?: { parentTagName; parentIndex; textOffset };
}
Expand Down Expand Up @@ -286,7 +291,7 @@ interface Block {
- Horizontal rules (`---`)
- Paragraphs (default)

`exportAnnotations(blocks, annotations, globalAttachments)` generates human-readable feedback for Claude. Images are referenced by name: `[image-name] /tmp/path...`.
`exportAnnotations(blocks, annotations, globalAttachments)` generates human-readable feedback for Claude. Images are referenced by name: `[image-name] /tmp/path...`. Annotations with `diffContext` include `[In diff content]` labels.

## Annotation System

Expand All @@ -311,6 +316,7 @@ interface SharePayload {
p: string; // Plan markdown
a: ShareableAnnotation[]; // Compact annotations
g?: ShareableImage[]; // Global attachments
d?: (string | null)[]; // diffContext per annotation, parallel to `a`
}

type ShareableAnnotation =
Expand Down
49 changes: 30 additions & 19 deletions packages/editor/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ const App: React.FC = () => {
const [showExportDropdown, setShowExportDropdown] = useState(false);
const [initialExportTab, setInitialExportTab] = useState<'share' | 'annotations' | 'notes'>();
const [noteSaveToast, setNoteSaveToast] = useState<{ type: 'success' | 'error'; message: string } | null>(null);
// Plan diff state
// Plan diff state — memoize filtered annotation lists to avoid new references per render
const diffAnnotations = useMemo(() => annotations.filter(a => !!a.diffContext), [annotations]);
const viewerAnnotations = useMemo(() => annotations.filter(a => !a.diffContext), [annotations]);
const [isPlanDiffActive, setIsPlanDiffActive] = useState(false);
const [planDiffMode, setPlanDiffMode] = useState<PlanDiffMode>('clean');
const [previousPlan, setPreviousPlan] = useState<string | null>(null);
Expand Down Expand Up @@ -271,7 +273,7 @@ const App: React.FC = () => {
if (restoredGlobal.length > 0) setGlobalAttachments(restoredGlobal);
// Apply highlights to DOM after a tick
setTimeout(() => {
viewerRef.current?.applySharedAnnotations(restored);
viewerRef.current?.applySharedAnnotations(restored.filter(a => !a.diffContext));
}, 100);
}
}, [restoreDraft]);
Expand All @@ -286,7 +288,7 @@ const App: React.FC = () => {
const timer = setTimeout(() => {
// Clear existing highlights first (important when loading new share URL)
viewerRef.current?.clearAllHighlights();
viewerRef.current?.applySharedAnnotations(pendingSharedAnnotations);
viewerRef.current?.applySharedAnnotations(pendingSharedAnnotations.filter(a => !a.diffContext));
clearPendingSharedAnnotations();
}, 100);
return () => clearTimeout(timer);
Expand Down Expand Up @@ -1254,27 +1256,36 @@ const App: React.FC = () => {
</div>
)}

{/* 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}
baseVersion={planDiff.diffBaseVersion ?? undefined}
maxWidth={planMaxWidth}
/>
) : (
{/* Plan Diff View — rendered when diff data exists, hidden when inactive */}
{planDiff.diffBlocks && planDiff.diffStats && (
<div className="w-full flex justify-center" style={{ display: isPlanDiffActive ? undefined : 'none' }}>
<PlanDiffViewer
diffBlocks={planDiff.diffBlocks}
diffStats={planDiff.diffStats}
diffMode={planDiffMode}
onDiffModeChange={setPlanDiffMode}
onPlanDiffToggle={() => setIsPlanDiffActive(false)}
repoInfo={repoInfo}
baseVersionLabel={planDiff.diffBaseVersion != null ? `v${planDiff.diffBaseVersion}` : undefined}
baseVersion={planDiff.diffBaseVersion ?? undefined}
maxWidth={planMaxWidth}
annotations={diffAnnotations}
onAddAnnotation={handleAddAnnotation}
onSelectAnnotation={handleSelectAnnotation}
selectedAnnotationId={selectedAnnotationId}
mode={editorMode}
/>
</div>
)}
{/* Normal Plan View — always mounted, hidden during diff mode */}
<div className="w-full flex justify-center" style={{ display: isPlanDiffActive && planDiff.diffBlocks ? 'none' : undefined }}>
<Viewer
key={linkedDocHook.isActive ? `doc:${linkedDocHook.filepath}` : 'plan'}
ref={viewerRef}
blocks={blocks}
markdown={markdown}
frontmatter={frontmatter}
annotations={annotations}
annotations={viewerAnnotations}
onAddAnnotation={handleAddAnnotation}
onSelectAnnotation={handleSelectAnnotation}
selectedAnnotationId={selectedAnnotationId}
Expand All @@ -1296,7 +1307,7 @@ const App: React.FC = () => {
linkedDocInfo={linkedDocHook.isActive ? { filepath: linkedDocHook.filepath!, onBack: handleLinkedDocBack, label: vaultBrowser.activeFile ? 'Vault File' : undefined } : null}
imageBaseDir={imageBaseDir}
/>
)}
</div>
</div>
</main>

Expand Down
5 changes: 5 additions & 0 deletions packages/ui/components/AnnotationPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,11 @@ const AnnotationCard: React.FC<{
{config.label}
</span>
</div>
{annotation.diffContext && (
<span className="text-[9px] px-1.5 py-0.5 rounded font-medium bg-muted text-muted-foreground">
diff
</span>
)}
<span className="text-[10px] text-muted-foreground/50">
{formatTimestamp(annotation.createdA)}
</span>
Expand Down
Loading