diff --git a/apps/hook/server/index.ts b/apps/hook/server/index.ts index 265ba0ff..7572d43c 100644 --- a/apps/hook/server/index.ts +++ b/apps/hook/server/index.ts @@ -64,6 +64,7 @@ import { handleAnnotateServerReady, } from "@plannotator/server/annotate"; import { type DiffType, getVcsContext, runVcsDiff, gitRuntime } from "@plannotator/server/vcs"; +import { loadConfig, resolveDefaultDiffType } from "@plannotator/shared/config"; import { fetchRef, createWorktree, removeWorktree, ensureObjectAvailable } from "@plannotator/shared/worktree"; import { parsePRUrl, checkPRAuth, fetchPR, getCliName, getCliInstallUrl, getMRLabel, getMRNumberLabel, getDisplayRepo } from "@plannotator/server/pr"; import { writeRemoteShareLink } from "@plannotator/server/share-url"; @@ -379,7 +380,7 @@ if (args[0] === "sessions") { } else { // --- Local Review Mode --- gitContext = await getVcsContext(); - initialDiffType = gitContext.vcsType === "p4" ? "p4-default" : "uncommitted"; + initialDiffType = gitContext.vcsType === "p4" ? "p4-default" : resolveDefaultDiffType(loadConfig()); const diffResult = await runVcsDiff(initialDiffType, gitContext.defaultBranch); rawPatch = diffResult.patch; gitRef = diffResult.label; @@ -394,7 +395,7 @@ if (args[0] === "sessions") { gitRef, error: diffError, origin: detectedOrigin, - diffType: gitContext ? (initialDiffType ?? "uncommitted") : undefined, + diffType: gitContext ? (initialDiffType ?? "unstaged") : undefined, gitContext, prMetadata, agentCwd, diff --git a/apps/opencode-plugin/commands.ts b/apps/opencode-plugin/commands.ts index 2218c271..523f4723 100644 --- a/apps/opencode-plugin/commands.ts +++ b/apps/opencode-plugin/commands.ts @@ -20,6 +20,7 @@ import { } from "@plannotator/server/annotate"; import { getGitContext, runGitDiffWithContext } from "@plannotator/server/git"; import { parsePRUrl, checkPRAuth, fetchPR, getCliName, getMRLabel, getMRNumberLabel, getDisplayRepo } from "@plannotator/server/pr"; +import { loadConfig, resolveDefaultDiffType } from "@plannotator/shared/config"; import { resolveMarkdownFile } from "@plannotator/shared/resolve-file"; /** Shared dependencies injected by the plugin */ @@ -45,6 +46,7 @@ export async function handleReviewCommand( let rawPatch: string; let gitRef: string; let diffError: string | undefined; + let userDiffType: import("@plannotator/shared/config").DefaultDiffType | undefined; let gitContext: Awaited> | undefined; let prMetadata: Awaited>["metadata"] | undefined; @@ -78,7 +80,8 @@ export async function handleReviewCommand( client.app.log({ level: "info", message: "Opening code review UI..." }); gitContext = await getGitContext(directory); - const diffResult = await runGitDiffWithContext("uncommitted", gitContext); + userDiffType = resolveDefaultDiffType(loadConfig()); + const diffResult = await runGitDiffWithContext(userDiffType, gitContext); rawPatch = diffResult.patch; gitRef = diffResult.label; diffError = diffResult.error; @@ -89,7 +92,7 @@ export async function handleReviewCommand( gitRef, error: diffError, origin: "opencode", - diffType: isPRMode ? undefined : "uncommitted", + diffType: isPRMode ? undefined : userDiffType, gitContext, prMetadata, sharingEnabled: await getSharingEnabled(), diff --git a/apps/pi-extension/plannotator-browser.ts b/apps/pi-extension/plannotator-browser.ts index 6c6548fb..3ee5bba7 100644 --- a/apps/pi-extension/plannotator-browser.ts +++ b/apps/pi-extension/plannotator-browser.ts @@ -24,6 +24,7 @@ import { } from "./generated/pr-provider.js"; import { parseRemoteUrl } from "./generated/repo.js"; import { fetchRef, createWorktree, removeWorktree, ensureObjectAvailable } from "./generated/worktree.js"; +import { loadConfig, resolveDefaultDiffType } from "./generated/config.js"; export type AnnotateMode = "annotate" | "annotate-folder" | "annotate-last"; export interface PlanReviewDecision { @@ -336,7 +337,7 @@ export async function openCodeReview( const cwd = options.cwd ?? ctx.cwd; gitCtx = await getGitContext(cwd); const defaultBranch = options.defaultBranch ?? gitCtx.defaultBranch; - diffType = options.diffType ?? "uncommitted"; + diffType = options.diffType ?? resolveDefaultDiffType(loadConfig()); const result = await runGitDiff(diffType, defaultBranch, cwd); rawPatch = result.patch; gitRef = result.label; diff --git a/packages/review-editor/App.tsx b/packages/review-editor/App.tsx index 15f1ab6d..86205844 100644 --- a/packages/review-editor/App.tsx +++ b/packages/review-editor/App.tsx @@ -19,6 +19,8 @@ import { getAgentSwitchSettings, getEffectiveAgentName } from '@plannotator/ui/u import { getAIProviderSettings, saveAIProviderSettings, getPreferredModel } from '@plannotator/ui/utils/aiProvider'; import { AISetupDialog } from '@plannotator/ui/components/AISetupDialog'; import { needsAISetup } from '@plannotator/ui/utils/aiSetup'; +import { DiffTypeSetupDialog } from '@plannotator/ui/components/DiffTypeSetupDialog'; +import { needsDiffTypeSetup } from '@plannotator/ui/utils/diffTypeSetup'; import { CodeAnnotation, CodeAnnotationType, SelectedLineRange, TokenAnnotationMeta, ConventionalLabel, ConventionalDecoration } from '@plannotator/ui/types'; import { useResizablePanel } from '@plannotator/ui/hooks/useResizablePanel'; import { useCodeAnnotationDraft } from '@plannotator/ui/hooks/useCodeAnnotationDraft'; @@ -362,6 +364,9 @@ const ReviewApp: React.FC = () => { }; }); const [showAISetup, setShowAISetup] = useState(false); + const [aiCheckComplete, setAiCheckComplete] = useState(false); + const [showDiffTypeSetup, setShowDiffTypeSetup] = useState(false); + const [diffTypeSetupPending, setDiffTypeSetupPending] = useState(false); const [sidebarTabOverride, setSidebarTabOverride] = useState<'ai' | undefined>(undefined); const aiChat = useAIChat({ patch: diffData?.rawPatch ?? '', @@ -383,8 +388,9 @@ const ReviewApp: React.FC = () => { setShowAISetup(true); } } + setAiCheckComplete(true); }) - .catch(() => {}); + .catch(() => { setAiCheckComplete(true); }); }, []); const handleAIConfigChange = useCallback((config: { providerId?: string | null; model?: string | null }) => { @@ -662,6 +668,10 @@ const ReviewApp: React.FC = () => { } if (data.error) setDiffError(data.error); if (data.isWSL) setIsWSL(true); + // Mark diff type setup as pending on first run (local mode only) + if (data.diffType && !data.prMetadata && data.gitContext?.vcsType !== 'p4' && needsDiffTypeSetup()) { + setDiffTypeSetupPending(true); + } }) .catch(() => { // Not in API mode - use demo content @@ -676,6 +686,14 @@ const ReviewApp: React.FC = () => { .finally(() => setIsLoading(false)); }, []); + // Show diff type setup dialog only after AI setup dialog is dismissed (avoid stacking) + useEffect(() => { + if (diffTypeSetupPending && aiCheckComplete && !showAISetup) { + setDiffTypeSetupPending(false); + setShowDiffTypeSetup(true); + } + }, [diffTypeSetupPending, aiCheckComplete, showAISetup]); + const handleDiffStyleChange = useCallback((style: 'split' | 'unified') => { configStore.set('diffStyle', style); }, []); @@ -891,6 +909,7 @@ const ReviewApp: React.FC = () => { const nextFiles = parseDiffToFiles(data.rawPatch); dockApi?.getPanel(REVIEW_DIFF_PANEL_ID)?.api.close(); needsInitialDiffPanel.current = true; + setDiffData(prev => prev ? { ...prev, rawPatch: data.rawPatch, gitRef: data.gitRef, diffType: data.diffType } : prev); setFiles(nextFiles); setDiffType(data.diffType); setActiveFileIndex(0); @@ -1907,6 +1926,16 @@ const ReviewApp: React.FC = () => { }} /> + {/* Diff type setup dialog — first-run only */} + {showDiffTypeSetup && ( + { + setShowDiffTypeSetup(false); + if (selected !== diffType) handleDiffSwitch(selected); + }} + /> + )} + {/* Completion overlay - shown after approve/feedback/exit */} void; +} + +export const DiffTypeSetupDialog: React.FC = ({ + onComplete, +}) => { + const [selected, setSelected] = useState( + () => configStore.get('defaultDiffType') + ); + + const handleDone = () => { + configStore.set('defaultDiffType', selected); + markDiffTypeSetupDone(); + onComplete(selected); + }; + + return createPortal( +
+
+ {/* Header */} +
+

Default Diff View

+

+ Choose which changes to show when you open a code review. + You can always switch between views during a session. +

+
+ + {/* Options */} +
+ {OPTIONS.map((opt) => ( + + ))} +
+ + {/* Footer */} +
+

+ You can change this later in Settings > Display. +

+ +
+
+
, + document.body + ); +}; diff --git a/packages/ui/components/Settings.tsx b/packages/ui/components/Settings.tsx index bf8e4d19..fbb62092 100644 --- a/packages/ui/components/Settings.tsx +++ b/packages/ui/components/Settings.tsx @@ -122,6 +122,11 @@ const LINE_DIFF_OPTIONS = [ { value: 'char' as const, label: 'Char' }, { value: 'none' as const, label: 'None' }, ]; +const DEFAULT_DIFF_TYPE_OPTIONS = [ + { value: 'uncommitted' as const, label: 'All Changes' }, + { value: 'unstaged' as const, label: 'Unstaged' }, + { value: 'staged' as const, label: 'Staged' }, +]; function SegmentedControl({ options, value, onChange }: { options: { value: T; label: string }[]; @@ -178,6 +183,7 @@ function ToggleSwitch({ checked, onChange, label, description }: { } const ReviewDisplayTab: React.FC = () => { + const defaultDiffType = useConfigValue('defaultDiffType'); const diffStyle = useConfigValue('diffStyle'); const diffOverflow = useConfigValue('diffOverflow'); const diffIndicators = useConfigValue('diffIndicators'); @@ -194,6 +200,17 @@ const ReviewDisplayTab: React.FC = () => { return ( <> + {/* Default Diff View */} +
+
+
Default Diff View
+
Which changes to show when opening a review
+
+ configStore.set('defaultDiffType', v)} /> +
+ +
+ {/* Font Family */}
diff --git a/packages/ui/config/settings.ts b/packages/ui/config/settings.ts index 838c8987..e1bfbfee 100644 --- a/packages/ui/config/settings.ts +++ b/packages/ui/config/settings.ts @@ -35,6 +35,21 @@ export const SETTINGS = { // --- Diff display options (namespaced under diffOptions in config.json) --- + defaultDiffType: { + defaultValue: 'unstaged' as 'uncommitted' | 'unstaged' | 'staged', + fromCookie: () => { + const v = storage.getItem('plannotator-default-diff-type'); + return v === 'uncommitted' || v === 'unstaged' || v === 'staged' ? v : undefined; + }, + toCookie: (v: string) => storage.setItem('plannotator-default-diff-type', v), + serverKey: 'diffOptions', + fromServer: (sc: Record) => { + const v = (sc.diffOptions as Record | undefined)?.defaultDiffType; + return v === 'uncommitted' || v === 'unstaged' || v === 'staged' ? v : undefined; + }, + toServer: (v: string) => ({ diffOptions: { defaultDiffType: v } }), + }, + diffStyle: { defaultValue: 'split' as 'split' | 'unified', fromCookie: () => { diff --git a/packages/ui/utils/diffTypeSetup.ts b/packages/ui/utils/diffTypeSetup.ts new file mode 100644 index 00000000..41d21c74 --- /dev/null +++ b/packages/ui/utils/diffTypeSetup.ts @@ -0,0 +1,18 @@ +/** + * Diff Type Setup Utility + * + * Tracks whether the user has seen the first-run diff type selection dialog. + * Uses cookies (not localStorage) for the same reason as all other settings. + */ + +import { storage } from './storage'; + +const STORAGE_KEY = 'plannotator-diff-type-setup-done'; + +export function needsDiffTypeSetup(): boolean { + return storage.getItem(STORAGE_KEY) !== 'true'; +} + +export function markDiffTypeSetupDone(): void { + storage.setItem(STORAGE_KEY, 'true'); +}