From 7d19228dd16f270006333a595f8f164b80c54de6 Mon Sep 17 00:00:00 2001 From: Darrell Grissen Date: Fri, 17 Apr 2026 15:44:34 +0200 Subject: [PATCH 1/4] Add annotate wide mode --- apps/hook/vite.config.ts | 1 + apps/review/vite.config.ts | 1 + docs/adversarial_rubric.md | 61 +++++++ packages/editor/App.tsx | 160 +++++++++++++++--- packages/editor/wideMode.test.ts | 91 ++++++++++ packages/editor/wideMode.ts | 48 ++++++ .../ui/components/AnnotationToolstrip.tsx | 29 ++++ packages/ui/components/StickyHeaderLane.tsx | 13 +- packages/ui/components/Viewer.tsx | 6 +- 9 files changed, 382 insertions(+), 28 deletions(-) create mode 100644 docs/adversarial_rubric.md create mode 100644 packages/editor/wideMode.test.ts create mode 100644 packages/editor/wideMode.ts diff --git a/apps/hook/vite.config.ts b/apps/hook/vite.config.ts index 5577f88e..f9bcbb2e 100644 --- a/apps/hook/vite.config.ts +++ b/apps/hook/vite.config.ts @@ -18,6 +18,7 @@ export default defineConfig({ resolve: { alias: { '@': path.resolve(__dirname, '.'), + '@plannotator/shared': path.resolve(__dirname, '../../packages/shared'), '@plannotator/ui': path.resolve(__dirname, '../../packages/ui'), '@plannotator/editor/styles': path.resolve(__dirname, '../../packages/editor/index.css'), '@plannotator/editor': path.resolve(__dirname, '../../packages/editor/App.tsx'), diff --git a/apps/review/vite.config.ts b/apps/review/vite.config.ts index 72bedcb6..24d90a6b 100644 --- a/apps/review/vite.config.ts +++ b/apps/review/vite.config.ts @@ -17,6 +17,7 @@ export default defineConfig({ resolve: { alias: { '@': path.resolve(__dirname, '.'), + '@plannotator/shared': path.resolve(__dirname, '../../packages/shared'), '@plannotator/ui': path.resolve(__dirname, '../../packages/ui'), '@plannotator/review-editor/styles': path.resolve(__dirname, '../../packages/review-editor/index.css'), '@plannotator/review-editor': path.resolve(__dirname, '../../packages/review-editor/App.tsx'), diff --git a/docs/adversarial_rubric.md b/docs/adversarial_rubric.md new file mode 100644 index 00000000..1dd4fd6a --- /dev/null +++ b/docs/adversarial_rubric.md @@ -0,0 +1,61 @@ +# Adversarial Rubric + +Last Updated: 2026-04-17 + +This rubric captures the main adversarial and drift vectors for Plannotator's review and annotation surfaces. It is intended for milestone reviews, especially for UI state changes that can unintentionally cross plan, annotate, archive, and review modes. + +## Data Boundaries + +| Boundary | Format | Validation | Failure Mode | +| --- | --- | --- | --- | +| `/api/plan`, `/api/feedback`, `/api/draft`, `/api/upload` between browser and Bun server | JSON, multipart form data, markdown text | Per-endpoint parsing in `packages/server/index.ts`, `packages/server/annotate.ts`, `packages/server/shared-handlers.ts` | Invalid payloads can silently fall back to demo/empty state or reject late in the flow | +| Linked-doc file resolution via `/api/doc` and Obsidian doc endpoints | Relative/absolute markdown paths | `packages/server/reference-handlers.ts`, `packages/shared/resolve-file.ts` normalize and constrain paths | Path confusion can open the wrong file or expose unintended content if guards drift | +| Share/import URLs and paste payloads | URL hash, compressed JSON, encrypted blobs | `packages/ui/utils/sharing.ts` parses, decompresses, decrypts, and reconstructs annotations | Malformed share payloads can break annotation restore or produce partial state | +| External annotations stream and snapshot APIs | SSE + JSON annotations | `packages/server/external-annotations.ts`, shared annotation types in `packages/shared/external-annotation.ts` | Unsanitized/invalid annotation payloads can corrupt UI state or highlight bookkeeping | +| Cookie-backed UI preferences | Strings in `document.cookie` | `packages/ui/utils/storage.ts`, `packages/ui/utils/uiPreferences.ts`, `packages/ui/config/settings.ts` coerce to enums/bools | Invalid cookie values can create inconsistent mode/layout defaults across sessions | + +## Type Coercion Vectors + +| Coercion | Location | Risk | Test Exists? | +| --- | --- | --- | --- | +| Cookie string → boolean / enum | `packages/ui/utils/uiPreferences.ts`, `packages/ui/config/settings.ts` | Invalid values can silently select unsafe defaults or inconsistent layout state | Partial | +| URL hash / paste payload → structured annotations | `packages/ui/utils/sharing.ts` | Malformed arrays or unexpected tuple shapes can restore incomplete/shifted annotations | Partial | +| Query/path input → resolved markdown path | `packages/shared/resolve-file.ts` | Separator normalization and basename fallback can drift from intended trust boundary | Yes | +| External annotation JSON → internal annotation model | `packages/shared/external-annotation.ts` | Missing/extra fields can degrade rendering or selection restoration | Partial | +| Resize/cap values → persisted panel widths | `packages/ui/hooks/useResizablePanel.ts` | Invalid saved widths can distort layout or hide controls | No | + +## Trust Assumptions + +| Assumption | What Breaks | Severity | Test Exists? | +| --- | --- | --- | --- | +| Annotate-only UI changes will not leak into plan/review/archive modes | Hidden controls or layout regressions in other surfaces | HIGH | No | +| Session-scoped UI modes restore the user’s prior layout exactly | Users lose sidebar/panel context or hidden state drifts | HIGH | No | +| Shared workspace aliases stay aligned across app Vite configs | Local builds fail even though workspace packages compile | MEDIUM | No | +| Linked-doc navigation only needs the sidebar capabilities it declares | Runtime mismatches if hook expectations drift | MEDIUM | No | +| Cookie defaults are benign when malformed or missing | Surprising startup state, especially around sidebar and panel behavior | LOW | Partial | + +## Cascade Risks + +| Cascade Point | Blast Radius | Isolation | Test Exists? | +| --- | --- | --- | --- | +| Viewer/layout mode toggles in `packages/editor/App.tsx` | Can affect annotate, plan, linked-doc, archive, and sticky-header behavior at once | Manual branching by `annotateMode`, `archiveMode`, `isPlanDiffActive` | No | +| Sticky header lane width calculations | Reader chrome can diverge from document width and overlay controls incorrectly | Separate `StickyHeaderLane` component with measured widths | No | +| Linked-doc state swap and cached annotations | Annotation state can leak between source doc and linked doc | `useLinkedDoc` caches/restores per file | No | +| External annotation highlight replay | DOM highlights can desync when switching linked docs or diff mode | `useExternalAnnotationHighlights` and explicit reset hooks | Partial | + +## Registry Drift Risks + +| Registry | Code Location | Drift Detection | Last Verified | +| --- | --- | --- | --- | +| Hook/review app workspace aliases | `apps/hook/vite.config.ts`, `apps/review/vite.config.ts` | Manual build of both apps | 2026-04-17 | +| Public API endpoint docs vs runtime endpoints | `AGENTS.md`, marketing docs, `packages/server/*.ts` | Manual review + endpoint additions in PR review | 2026-04-17 | +| Shared package exports vs app imports | `packages/shared/package.json` and app/package imports | Typecheck/build | 2026-04-17 | +| UI preference keys vs Settings UI | `packages/ui/utils/uiPreferences.ts`, `packages/ui/components/Settings.tsx` | Manual review | 2026-04-17 | + +## Learned Vectors + +| Vector | Source Milestone | Category | Recurrence | +| --- | --- | --- | --- | +| Session-scoped layout modes can mutate hidden panel state unless every reopen path exits the mode first | `feat/annotate-wide-mode` | Cascade / Trust Assumption | Likely | +| Annotate-only controls must be explicitly gated to avoid leaking into plan/review surfaces through shared components | `feat/annotate-wide-mode` | Trust Assumption | Likely | +| Build-time alias drift can look like a feature regression even when the code change is correct | `feat/annotate-wide-mode` | Registry Drift | Likely | diff --git a/packages/editor/App.tsx b/packages/editor/App.tsx index fdead657..3f63562a 100644 --- a/packages/editor/App.tsx +++ b/packages/editor/App.tsx @@ -38,6 +38,7 @@ import { ResizeHandle } from '@plannotator/ui/components/ResizeHandle'; import { OverlayScrollArea } from '@plannotator/ui/components/OverlayScrollArea'; import { ScrollViewportContext } from '@plannotator/ui/hooks/useScrollViewport'; import { useOverlayViewport } from '@plannotator/ui/hooks/useOverlayViewport'; +import { useIsMobile } from '@plannotator/ui/hooks/useIsMobile'; import { PlanHeaderMenu } from '@plannotator/ui/components/PlanHeaderMenu'; import { getPermissionModeSettings, @@ -47,7 +48,7 @@ import { import { PermissionModeSetup } from '@plannotator/ui/components/PermissionModeSetup'; import { ImageAnnotator } from '@plannotator/ui/components/ImageAnnotator'; import { deriveImageName } from '@plannotator/ui/components/AttachmentsButton'; -import { useSidebar } from '@plannotator/ui/hooks/useSidebar'; +import { useSidebar, type SidebarTab } from '@plannotator/ui/hooks/useSidebar'; import { usePlanDiff, type VersionInfo } from '@plannotator/ui/hooks/usePlanDiff'; import { useLinkedDoc } from '@plannotator/ui/hooks/useLinkedDoc'; import { useAnnotationDraft } from '@plannotator/ui/hooks/useAnnotationDraft'; @@ -71,6 +72,7 @@ import type { PlanDiffMode } from '@plannotator/ui/components/plan-diff/PlanDiff // same env var on the server side so V2/V3 stay paired. import { DEMO_PLAN_CONTENT as DEFAULT_DEMO_PLAN_CONTENT } from './demoPlan'; import { DIFF_DEMO_PLAN_CONTENT } from './demoPlanDiffDemo'; +import { canUseAnnotateWideMode, resolveWideModeExitLayout, type WideModeLayoutSnapshot } from './wideMode'; const USE_DIFF_DEMO = import.meta.env.VITE_DIFF_DEMO === '1' || import.meta.env.VITE_DIFF_DEMO === 'true'; @@ -160,6 +162,9 @@ const App: React.FC = () => { const [pasteApiUrl, setPasteApiUrl] = useState(undefined); const [repoInfo, setRepoInfo] = useState<{ display: string; branch?: string } | null>(null); const [projectRoot, setProjectRoot] = useState(null); + const [isWideMode, setIsWideMode] = useState(false); + const wideModeSnapshotRef = useRef(null); + const lastAppliedTocEnabledRef = useRef(uiPrefs.tocEnabled); useEffect(() => { document.title = repoInfo ? `${repoInfo.display} · Plannotator` : "Plannotator"; @@ -171,6 +176,7 @@ const App: React.FC = () => { const [planDiffMode, setPlanDiffMode] = useState('clean'); const [previousPlan, setPreviousPlan] = useState(null); const [versionInfo, setVersionInfo] = useState(null); + const isMobile = useIsMobile(); const viewerRef = useRef(null); // containerRef + scrollViewport both point at the OverlayScrollbars @@ -196,14 +202,67 @@ const App: React.FC = () => { // 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'); + const exitWideMode = useCallback((options?: { + restore?: boolean; + sidebarTab?: SidebarTab; + panelOpen?: boolean; + }) => { + if (!isWideMode) { + if (options?.sidebarTab) sidebar.open(options.sidebarTab); + if (options?.panelOpen === true) setIsPanelOpen(true); + else if (options?.panelOpen === false) setIsPanelOpen(false); + return; + } + + const snapshot = wideModeSnapshotRef.current; + const layout = resolveWideModeExitLayout(snapshot, options); + + setIsWideMode(false); + wideModeSnapshotRef.current = null; + + if (layout.sidebarOpen && layout.sidebarTab) { + sidebar.open(layout.sidebarTab); } else { sidebar.close(); } - }, [uiPrefs.tocEnabled]); + + if (layout.panelOpen !== undefined) { + setIsPanelOpen(layout.panelOpen); + } + }, [isWideMode, sidebar.close, sidebar.open]); + + const openSidebarTab = useCallback((tab: SidebarTab) => { + if (isWideMode) { + exitWideMode({ restore: false, sidebarTab: tab, panelOpen: false }); + return; + } + sidebar.open(tab); + }, [exitWideMode, isWideMode, sidebar.open]); + + const toggleSidebarTab = useCallback((tab: SidebarTab) => { + if (isWideMode) { + exitWideMode({ restore: false, sidebarTab: tab, panelOpen: false }); + return; + } + sidebar.toggleTab(tab); + }, [exitWideMode, isWideMode, sidebar.toggleTab]); + + const handleAnnotationPanelToggle = useCallback(() => { + if (isWideMode) { + exitWideMode({ restore: false, panelOpen: true }); + return; + } + setIsPanelOpen(prev => !prev); + }, [exitWideMode, isWideMode]); + + // Sync sidebar open state when preference changes in Settings + useEffect(() => { + if (isWideMode) return; + if (lastAppliedTocEnabledRef.current === uiPrefs.tocEnabled) return; + lastAppliedTocEnabledRef.current = uiPrefs.tocEnabled; + if (uiPrefs.tocEnabled) sidebar.open('toc'); + else sidebar.close(); + }, [isWideMode, sidebar.close, sidebar.open, uiPrefs.tocEnabled]); // Clear diff view when switching away from versions tab useEffect(() => { @@ -227,11 +286,23 @@ const App: React.FC = () => { // Plan diff computation const planDiff = usePlanDiff(markdown, previousPlan, versionInfo); + const linkedDocSidebar = useMemo(() => ({ + ...sidebar, + open: openSidebarTab, + toggleTab: toggleSidebarTab, + }), [ + openSidebarTab, + sidebar.activeTab, + sidebar.close, + sidebar.isOpen, + toggleSidebarTab, + ]); + // Linked document navigation const linkedDocHook = useLinkedDoc({ markdown, annotations, selectedAnnotationId, globalAttachments, setMarkdown, setAnnotations, setSelectedAnnotationId, setGlobalAttachments, - viewerRef, sidebar, sourceFilePath, + viewerRef, sidebar: linkedDocSidebar, sourceFilePath, }); // Archive browser @@ -240,6 +311,40 @@ const App: React.FC = () => { setMarkdown, setAnnotations, setSelectedAnnotationId, setSubmitted, }); + const canUseWideMode = useMemo(() => canUseAnnotateWideMode({ + annotateMode, + archiveMode: archive.archiveMode, + isPlanDiffActive, + }), [annotateMode, archive.archiveMode, isPlanDiffActive]); + + const enterWideMode = useCallback(() => { + if (!canUseWideMode || isWideMode) return; + + wideModeSnapshotRef.current = { + sidebarIsOpen: sidebar.isOpen, + sidebarTab: sidebar.activeTab, + panelOpen: isPanelOpen, + }; + + setIsWideMode(true); + sidebar.close(); + setIsPanelOpen(false); + }, [canUseWideMode, isPanelOpen, isWideMode, sidebar.activeTab, sidebar.close, sidebar.isOpen]); + + const toggleWideMode = useCallback(() => { + if (isWideMode) { + exitWideMode(); + } else { + enterWideMode(); + } + }, [enterWideMode, exitWideMode, isWideMode]); + + useEffect(() => { + if (!canUseWideMode && isWideMode) { + exitWideMode(); + } + }, [canUseWideMode, exitWideMode, isWideMode]); + // Markdown file browser (also handles vault dirs via isVault flag) const fileBrowser = useFileBrowser(); const vaultPath = useMemo(() => { @@ -378,7 +483,7 @@ const App: React.FC = () => { if (filePaths.size === 0) return; // Open sidebar to the files tab so the flash is visible if (!sidebar.isOpen || sidebar.activeTab !== 'files') { - sidebar.open('files'); + openSidebarTab('files'); } // Cancel any pending clear from a previous flash if (flashTimerRef.current) clearTimeout(flashTimerRef.current); @@ -388,7 +493,7 @@ const App: React.FC = () => { setHighlightedFiles(filePaths); flashTimerRef.current = setTimeout(() => setHighlightedFiles(undefined), 1200); }); - }, [allAnnotationCounts, sidebar, hasFileAnnotations]); + }, [allAnnotationCounts, openSidebarTab, sidebar, hasFileAnnotations]); // Context-aware back label for linked doc navigation const backLabel = annotateSource === 'folder' ? 'file list' @@ -963,14 +1068,16 @@ const App: React.FC = () => { const handleAddAnnotation = (ann: Annotation) => { setAnnotations(prev => [...prev, ann]); setSelectedAnnotationId(ann.id); - setIsPanelOpen(true); + if (!isWideMode) { + setIsPanelOpen(true); + } }; - // Stable reference — the Viewer's highlighter useEffect depends on this + // Keep selection behavior explicit across mobile/wide-mode transitions. const handleSelectAnnotation = React.useCallback((id: string | null) => { setSelectedAnnotationId(id); - if (id && window.innerWidth < 768) setIsPanelOpen(true); - }, []); + if (id && isMobile && !isWideMode) setIsPanelOpen(true); + }, [isMobile, isWideMode]); // Core annotation removal — highlight cleanup + state filter + selection clear const removeAnnotation = (id: string) => { @@ -1250,6 +1357,7 @@ const App: React.FC = () => { const widths: Record = { compact: 832, default: 1040, wide: 1280 }; return widths[uiPrefs.planWidth] ?? 832; }, [uiPrefs.planWidth]); + const annotateReaderMaxWidth = canUseWideMode && isWideMode ? null : planMaxWidth; return ( @@ -1387,7 +1495,7 @@ const App: React.FC = () => { {/* Annotations panel toggle — top-level header button */} + + + ))} + + + )} { {/* Resize Handle */} - {isPanelOpen && !isWideMode && } + {isPanelOpen && wideModeType === null && } {/* Annotation Panel */} { }} /> + ); }; diff --git a/packages/editor/wideMode.ts b/packages/editor/wideMode.ts index ee149d3b..d38b0ea6 100644 --- a/packages/editor/wideMode.ts +++ b/packages/editor/wideMode.ts @@ -1,4 +1,5 @@ import type { SidebarTab } from '@plannotator/ui/hooks/useSidebar'; +export type { WideModeType } from '@plannotator/ui/types'; export type WideModeLayoutSnapshot = { sidebarIsOpen: boolean; @@ -23,7 +24,7 @@ export function canUseAnnotateWideMode(options: { archiveMode: boolean; isPlanDiffActive: boolean; }): boolean { - return options.annotateMode && !options.archiveMode && !options.isPlanDiffActive; + return !options.archiveMode && !options.isPlanDiffActive; } export function resolveWideModeExitLayout( diff --git a/packages/ui/components/AnnotationToolstrip.tsx b/packages/ui/components/AnnotationToolstrip.tsx index c95e2221..bebd4e00 100644 --- a/packages/ui/components/AnnotationToolstrip.tsx +++ b/packages/ui/components/AnnotationToolstrip.tsx @@ -8,9 +8,6 @@ interface AnnotationToolstripProps { mode: EditorMode; onModeChange: (mode: EditorMode) => void; taterMode?: boolean; - showWideMode?: boolean; - wideMode?: boolean; - onWideModeToggle?: () => void; /** * Compact mode: used inside the sticky header lane. Buttons only expand for * the active mode (no hover expansion), gap is tightened, and the help link @@ -31,9 +28,6 @@ export const AnnotationToolstrip: React.FC = ({ mode, onModeChange, taterMode, - showWideMode = false, - wideMode = false, - onWideModeToggle, compact = false, iconOnly = false, }) => { @@ -148,29 +142,6 @@ export const AnnotationToolstrip: React.FC = ({ } /> - {showWideMode && onWideModeToggle && ( - - - - - - - - - - - } - /> - )} {/* Help */} diff --git a/packages/ui/components/StickyHeaderLane.tsx b/packages/ui/components/StickyHeaderLane.tsx index 3d3063f0..2088af69 100644 --- a/packages/ui/components/StickyHeaderLane.tsx +++ b/packages/ui/components/StickyHeaderLane.tsx @@ -62,9 +62,6 @@ interface StickyHeaderLaneProps { mode: EditorMode; onModeChange: (mode: EditorMode) => void; taterMode?: boolean; - showWideMode?: boolean; - wideMode?: boolean; - onWideModeToggle?: () => void; // Badge state repoInfo?: { display: string; branch?: string } | null; @@ -92,9 +89,6 @@ export const StickyHeaderLane: React.FC = ({ mode, onModeChange, taterMode, - showWideMode = false, - wideMode = false, - onWideModeToggle, repoInfo, planDiffStats, isPlanDiffActive, @@ -245,9 +239,6 @@ export const StickyHeaderLane: React.FC = ({ mode={mode} onModeChange={onModeChange} taterMode={taterMode} - showWideMode={showWideMode} - wideMode={wideMode} - onWideModeToggle={onWideModeToggle} compact iconOnly={isNarrow || isToolstripIconOnly} /> diff --git a/packages/ui/components/Tooltip.tsx b/packages/ui/components/Tooltip.tsx new file mode 100644 index 00000000..782fbf2b --- /dev/null +++ b/packages/ui/components/Tooltip.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import * as RadixTooltip from '@radix-ui/react-tooltip'; + +export const TooltipProvider = RadixTooltip.Provider; + +interface TooltipProps { + content: React.ReactNode; + children: React.ReactNode; + side?: 'top' | 'right' | 'bottom' | 'left'; + align?: 'start' | 'center' | 'end'; + delayDuration?: number; + sideOffset?: number; +} + +export const Tooltip: React.FC = ({ + content, + children, + side = 'top', + align = 'center', + delayDuration, + sideOffset = 8, +}) => ( + + {children} + + + {content} + + + +); diff --git a/packages/ui/package.json b/packages/ui/package.json index 3d9f6815..f7f83331 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -16,6 +16,7 @@ "dependencies": { "@plannotator/shared": "workspace:*", "@plannotator/web-highlighter": "^0.8.1", + "@radix-ui/react-tooltip": "^1.2.8", "@viz-js/viz": "^3.25.0", "diff": "^8.0.3", "highlight.js": "^11.11.1", diff --git a/packages/ui/types.ts b/packages/ui/types.ts index 5bf81515..99a7f3d9 100644 --- a/packages/ui/types.ts +++ b/packages/ui/types.ts @@ -18,6 +18,8 @@ export type InputMethod = 'drag' | 'pinpoint'; */ export type ActionsLabelMode = 'full' | 'short' | 'icon'; +export type WideModeType = 'wide' | 'focus'; + export interface ImageAttachment { path: string; name: string; From c1d91633897137de819e68425e4ddcf2c5b650ca Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Sat, 18 Apr 2026 23:09:48 -0700 Subject: [PATCH 3/4] Update wideMode test to cover both annotate and plan review modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The annotateMode gate was removed — update the test matrix to assert wide mode is enabled in annotate AND plan review, and disabled for archive/diff across both mode values. For provenance purposes, this commit was AI assisted. --- packages/editor/wideMode.test.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/editor/wideMode.test.ts b/packages/editor/wideMode.test.ts index 6e7983b0..c5dc5805 100644 --- a/packages/editor/wideMode.test.ts +++ b/packages/editor/wideMode.test.ts @@ -12,7 +12,7 @@ const snapshot: WideModeLayoutSnapshot = { }; describe('canUseAnnotateWideMode', () => { - test('only enables wide mode in annotate mode outside archive and diff', () => { + test('enables wide mode in both annotate and plan review, excluding archive and diff', () => { expect(canUseAnnotateWideMode({ annotateMode: true, archiveMode: false, @@ -23,7 +23,7 @@ describe('canUseAnnotateWideMode', () => { annotateMode: false, archiveMode: false, isPlanDiffActive: false, - })).toBe(false); + })).toBe(true); expect(canUseAnnotateWideMode({ annotateMode: true, @@ -31,11 +31,23 @@ describe('canUseAnnotateWideMode', () => { isPlanDiffActive: false, })).toBe(false); + expect(canUseAnnotateWideMode({ + annotateMode: false, + archiveMode: true, + isPlanDiffActive: false, + })).toBe(false); + expect(canUseAnnotateWideMode({ annotateMode: true, archiveMode: false, isPlanDiffActive: true, })).toBe(false); + + expect(canUseAnnotateWideMode({ + annotateMode: false, + archiveMode: false, + isPlanDiffActive: true, + })).toBe(false); }); }); From 187c1c770bd1b0101e2eb9ea76a7f34a44e18f17 Mon Sep 17 00:00:00 2001 From: Michael Ramos Date: Sat, 18 Apr 2026 23:17:15 -0700 Subject: [PATCH 4/4] Drop unused annotateMode parameter from canUseAnnotateWideMode The parameter was ignored after the annotate-only gate was removed. Prune it from the signature, the App.tsx call site and deps, and the test matrix to match the actual behavior (disabled only for archive and diff). For provenance purposes, this commit was AI assisted. --- packages/editor/App.tsx | 3 +-- packages/editor/wideMode.test.ts | 20 ++------------------ packages/editor/wideMode.ts | 1 - 3 files changed, 3 insertions(+), 21 deletions(-) diff --git a/packages/editor/App.tsx b/packages/editor/App.tsx index 24df027b..f20c3dba 100644 --- a/packages/editor/App.tsx +++ b/packages/editor/App.tsx @@ -313,10 +313,9 @@ const App: React.FC = () => { }); const canUseWideMode = useMemo(() => canUseAnnotateWideMode({ - annotateMode, archiveMode: archive.archiveMode, isPlanDiffActive, - }), [annotateMode, archive.archiveMode, isPlanDiffActive]); + }), [archive.archiveMode, isPlanDiffActive]); const enterViewMode = useCallback((type: WideModeType) => { if (!canUseWideMode) return; diff --git a/packages/editor/wideMode.test.ts b/packages/editor/wideMode.test.ts index c5dc5805..4e8a56bc 100644 --- a/packages/editor/wideMode.test.ts +++ b/packages/editor/wideMode.test.ts @@ -12,40 +12,24 @@ const snapshot: WideModeLayoutSnapshot = { }; describe('canUseAnnotateWideMode', () => { - test('enables wide mode in both annotate and plan review, excluding archive and diff', () => { + test('enables wide mode outside archive and diff', () => { expect(canUseAnnotateWideMode({ - annotateMode: true, archiveMode: false, isPlanDiffActive: false, })).toBe(true); expect(canUseAnnotateWideMode({ - annotateMode: false, - archiveMode: false, - isPlanDiffActive: false, - })).toBe(true); - - expect(canUseAnnotateWideMode({ - annotateMode: true, - archiveMode: true, - isPlanDiffActive: false, - })).toBe(false); - - expect(canUseAnnotateWideMode({ - annotateMode: false, archiveMode: true, isPlanDiffActive: false, })).toBe(false); expect(canUseAnnotateWideMode({ - annotateMode: true, archiveMode: false, isPlanDiffActive: true, })).toBe(false); expect(canUseAnnotateWideMode({ - annotateMode: false, - archiveMode: false, + archiveMode: true, isPlanDiffActive: true, })).toBe(false); }); diff --git a/packages/editor/wideMode.ts b/packages/editor/wideMode.ts index d38b0ea6..6c3d7402 100644 --- a/packages/editor/wideMode.ts +++ b/packages/editor/wideMode.ts @@ -20,7 +20,6 @@ export type WideModeExitLayout = { }; export function canUseAnnotateWideMode(options: { - annotateMode: boolean; archiveMode: boolean; isPlanDiffActive: boolean; }): boolean {