Files
Michael Ramos 819ba11f77 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

103 lines
3.2 KiB
TypeScript

/**
* PlanRawDiffView — Raw markdown diff mode (P2 style)
*
* Shows the raw markdown source with +/- prefixed lines,
* like a traditional code diff. Monospace font, line numbers,
* colored backgrounds.
*/
import React, { useMemo } from "react";
import type { PlanDiffBlock } from "../../utils/planDiffEngine";
interface PlanRawDiffViewProps {
blocks: PlanDiffBlock[];
}
interface DiffLine {
type: "added" | "removed" | "unchanged";
content: string;
lineNumber: number | null;
}
export const PlanRawDiffView: React.FC<PlanRawDiffViewProps> = ({ blocks }) => {
const lines = useMemo(() => {
const result: DiffLine[] = [];
let lineNum = 1;
for (const block of blocks) {
const rawLines = block.content.split("\n");
// Remove trailing empty string from split
if (rawLines.length > 0 && rawLines[rawLines.length - 1] === "") {
rawLines.pop();
}
if (block.type === "modified" && block.oldContent) {
// Show old content as removed
const oldLines = block.oldContent.split("\n");
if (oldLines.length > 0 && oldLines[oldLines.length - 1] === "") {
oldLines.pop();
}
for (const line of oldLines) {
result.push({ type: "removed", content: line, lineNumber: null });
}
// Show new content as added
for (const line of rawLines) {
result.push({ type: "added", content: line, lineNumber: lineNum++ });
}
} else if (block.type === "added") {
for (const line of rawLines) {
result.push({ type: "added", content: line, lineNumber: lineNum++ });
}
} else if (block.type === "removed") {
for (const line of rawLines) {
result.push({ type: "removed", content: line, lineNumber: null });
}
} else {
for (const line of rawLines) {
result.push({
type: "unchanged",
content: line,
lineNumber: lineNum++,
});
}
}
}
return result;
}, [blocks]);
return (
<div className="font-mono text-[13px] leading-relaxed bg-muted/30 rounded-lg border border-border/30 overflow-x-auto">
<div>
{lines.map((line, index) => (
<div
key={index}
className={`flex px-4 py-0.5 ${
line.type === "added"
? "plan-diff-line-added"
: line.type === "removed"
? "plan-diff-line-removed"
: "hover:bg-muted/30"
}`}
>
{/* Gutter: +/- prefix */}
<div className="w-5 flex-shrink-0 select-none opacity-60 text-right pr-2">
{line.type === "added"
? "+"
: line.type === "removed"
? "-"
: " "}
</div>
{/* Line number */}
<div className="w-8 flex-shrink-0 select-none text-muted-foreground/40 text-right pr-3 text-[11px]">
{line.lineNumber ?? ""}
</div>
{/* Content */}
<div className="whitespace-pre-wrap break-words min-w-0">{line.content || " "}</div>
</div>
))}
</div>
</div>
);
};