Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/hook/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand Down
7 changes: 5 additions & 2 deletions apps/opencode-plugin/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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<ReturnType<typeof getGitContext>> | undefined;
let prMetadata: Awaited<ReturnType<typeof fetchPR>>["metadata"] | undefined;

Expand Down Expand Up @@ -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;
Expand All @@ -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(),
Expand Down
3 changes: 2 additions & 1 deletion apps/pi-extension/plannotator-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
31 changes: 30 additions & 1 deletion packages/review-editor/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 ?? '',
Expand All @@ -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 }) => {
Expand Down Expand Up @@ -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
Expand All @@ -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);
}, []);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1907,6 +1926,16 @@ const ReviewApp: React.FC = () => {
}}
/>

{/* Diff type setup dialog — first-run only */}
{showDiffTypeSetup && (
<DiffTypeSetupDialog
onComplete={(selected) => {
setShowDiffTypeSetup(false);
if (selected !== diffType) handleDiffSwitch(selected);
}}
/>
)}

{/* Completion overlay - shown after approve/feedback/exit */}
<CompletionOverlay
submitted={submitted}
Expand Down
11 changes: 11 additions & 0 deletions packages/shared/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { join } from "path";
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
import { execSync } from "child_process";

export type DefaultDiffType = 'uncommitted' | 'unstaged' | 'staged';

export interface DiffOptions {
diffStyle?: 'split' | 'unified';
overflow?: 'scroll' | 'wrap';
Expand All @@ -19,6 +21,7 @@ export interface DiffOptions {
showDiffBackground?: boolean;
fontFamily?: string;
fontSize?: string;
defaultDiffType?: DefaultDiffType;
}

/** Single conventional comment label entry stored in config.json */
Expand Down Expand Up @@ -114,3 +117,11 @@ export function getServerConfig(gitUser: string | null): {
...(cfg.conventionalLabels !== undefined && { conventionalLabels: cfg.conventionalLabels }),
};
}

/**
* Read the user's preferred default diff type from config, falling back to 'unstaged'.
*/
export function resolveDefaultDiffType(cfg?: PlannotatorConfig): DefaultDiffType {
const v = cfg?.diffOptions?.defaultDiffType;
return v === 'uncommitted' || v === 'unstaged' || v === 'staged' ? v : 'unstaged';
}
98 changes: 98 additions & 0 deletions packages/ui/components/DiffTypeSetupDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React, { useState } from 'react';
import { createPortal } from 'react-dom';
import type { DefaultDiffType } from '@plannotator/shared/config';
import { markDiffTypeSetupDone } from '../utils/diffTypeSetup';
import { configStore } from '../config';

const OPTIONS: { value: DefaultDiffType; label: string; description: string }[] = [
{
value: 'unstaged',
label: 'Unstaged',
description: 'Changes not yet staged. Matches `git diff`',
},
{
value: 'uncommitted',
label: 'All Changes',
description: 'Staged and unstaged combined. Matches `git diff HEAD`',
},
{
value: 'staged',
label: 'Staged',
description: 'Only changes added to the index. Matches `git diff --staged`',
},
];

interface DiffTypeSetupDialogProps {
onComplete: (selected: DefaultDiffType) => void;
}

export const DiffTypeSetupDialog: React.FC<DiffTypeSetupDialogProps> = ({
onComplete,
}) => {
const [selected, setSelected] = useState<DefaultDiffType>(
() => configStore.get('defaultDiffType')
);

const handleDone = () => {
configStore.set('defaultDiffType', selected);
markDiffTypeSetupDone();
onComplete(selected);
};

return createPortal(
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-background/90 backdrop-blur-sm p-4">
<div className="bg-card border border-border rounded-xl w-full max-w-lg shadow-2xl">
{/* Header */}
<div className="p-5 border-b border-border">
<h3 className="font-semibold text-base mb-2">Default Diff View</h3>
<p className="text-sm text-muted-foreground">
Choose which changes to show when you open a code review.
You can always switch between views during a session.
</p>
</div>

{/* Options */}
<div className="p-4 space-y-2">
{OPTIONS.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => setSelected(opt.value)}
className={`w-full flex items-start gap-3 p-3 rounded-lg border transition-colors text-left ${
selected === opt.value
? 'border-primary bg-primary/5'
: 'border-border hover:border-muted-foreground/30 hover:bg-muted/50'
}`}
>
<div className={`mt-0.5 w-4 h-4 rounded-full border-2 flex-shrink-0 flex items-center justify-center ${
selected === opt.value ? 'border-primary' : 'border-muted-foreground/40'
}`}>
{selected === opt.value && (
<div className="w-2 h-2 rounded-full bg-primary" />
)}
</div>
<div>
<div className="text-sm font-medium">{opt.label}</div>
<div className="text-xs text-muted-foreground">{opt.description}</div>
</div>
</button>
))}
</div>

{/* Footer */}
<div className="p-4 border-t border-border flex justify-between items-center gap-3">
<p className="text-[10px] text-muted-foreground/70 flex-1">
You can change this later in Settings &gt; Display.
</p>
<button
onClick={handleDone}
className="px-4 py-2 bg-primary text-primary-foreground rounded-lg text-sm font-medium hover:opacity-90 transition-opacity flex-shrink-0"
>
Done
</button>
</div>
</div>
</div>,
document.body
);
};
17 changes: 17 additions & 0 deletions packages/ui/components/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends string>({ options, value, onChange }: {
options: { value: T; label: string }[];
Expand Down Expand Up @@ -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');
Expand All @@ -194,6 +200,17 @@ const ReviewDisplayTab: React.FC = () => {

return (
<>
{/* Default Diff View */}
<div className="space-y-2">
<div>
<div className="text-sm font-medium">Default Diff View</div>
<div className="text-xs text-muted-foreground">Which changes to show when opening a review</div>
</div>
<SegmentedControl options={DEFAULT_DIFF_TYPE_OPTIONS} value={defaultDiffType} onChange={(v) => configStore.set('defaultDiffType', v)} />
</div>

<div className="border-t border-border" />

{/* Font Family */}
<div className="space-y-2">
<div>
Expand Down
15 changes: 15 additions & 0 deletions packages/ui/config/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>) => {
const v = (sc.diffOptions as Record<string, unknown> | undefined)?.defaultDiffType;
return v === 'uncommitted' || v === 'unstaged' || v === 'staged' ? v : undefined;
},
toServer: (v: string) => ({ diffOptions: { defaultDiffType: v } }),
},

diffStyle: {
defaultValue: 'split' as 'split' | 'unified',
fromCookie: () => {
Expand Down
18 changes: 18 additions & 0 deletions packages/ui/utils/diffTypeSetup.ts
Original file line number Diff line number Diff line change
@@ -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');
}