Skip to content
Open
63 changes: 63 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
"lint:secrets": "command -v gitleaks >/dev/null 2>&1 && gitleaks detect --config .gitleaks.toml || (echo 'gitleaks not installed (brew install gitleaks)' >&2; exit 1)",
"format": "prettier --write .",
"format:check": "prettier --check .",
"test": "vitest run",
"test": "npm run test:unit && npm run test:client",
"test:unit": "vitest run",
"test:client": "vitest run --config vitest.client.config.ts",
"test:coordinator-pty": "RUN_COORDINATOR_PTY_TEST=1 vitest run electron/mcp/coordinator-real-pty.integration.test.ts",
"test:coverage": "vitest run --coverage",
"check:coordinator-log": "node scripts/check-coordinator-run.mjs",
Expand Down Expand Up @@ -75,6 +77,7 @@
"eslint": "^9.39.3",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-solid": "^0.14.5",
"happy-dom": "^20.11.1",
"husky": "^9.1.7",
"knip": "^6.12.2",
"lint-staged": "^16.2.7",
Expand Down
101 changes: 66 additions & 35 deletions src/components/DiffViewerDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ import { errMessage } from '../lib/log';
import { invoke } from '../lib/ipc';
import { IPC } from '../../electron/ipc/channels';
import { createDialogScroll } from '../lib/dialog-scroll';
import {
createDiffIdentity,
createRequestGenerationGuard,
createReviewIdentity,
} from '../lib/diff-review-lifecycle';
import { theme } from '../lib/theme';
import { sf } from '../lib/fontScale';
import { parseUnifiedDiff } from '../lib/unified-diff-parser';
import { evictStaleAnnotations } from '../lib/review-eviction';
import { type QualityFindingProvider } from '../lib/quality-findings';
import { windowChromeTopInset } from '../lib/platform';
import { ScrollingDiffView } from './ScrollingDiffView';
import {
Expand Down Expand Up @@ -50,6 +55,8 @@ interface DiffViewerDialogProps {
onCommitNavigate?: (selection: CommitSelection) => void;
/** Git isolation mode — CommitNavBar is only shown for worktree-isolated tasks */
gitIsolation?: GitIsolationMode;
/** Optional structured-finding source. Providers capture their own repository context. */
findingProvider?: QualityFindingProvider;
}

/** Compile review annotations into a prompt string for the agent. */
Expand All @@ -68,33 +75,43 @@ export function compileDiffReview(annotations: ReviewAnnotation[]): string {

export function DiffViewerDialog(props: DiffViewerDialogProps) {
const titleId = createUniqueId();
const reviewIdentity = () =>
createReviewIdentity({
taskId: props.taskId,
worktreePath: props.worktreePath,
projectRoot: props.projectRoot,
branchName: props.branchName,
});
return (
<Dialog
<ReviewProvider
taskId={props.taskId}
agentId={props.agentId}
findingProvider={props.findingProvider}
reviewIdentity={reviewIdentity()}
open={props.scrollToFile !== null}
onClose={props.onClose}
width="100vw"
labelledBy={titleId}
panelStyle={{
height: '100vh',
'max-height': 'none',
'max-width': 'none',
'border-radius': '0',
border: 'none',
overflow: 'hidden',
padding: '0',
gap: '0',
}}
compilePrompt={compileDiffReview}
onSubmitted={props.onClose}
>
<h2 id={titleId} class="dialog-sr-only">
Diff viewer for {props.taskName ?? 'task'}: {props.scrollToFile ?? 'all changes'}
</h2>
<Show when={props.scrollToFile !== null}>
<ReviewProvider
taskId={props.taskId}
agentId={props.agentId}
compilePrompt={compileDiffReview}
onSubmitted={props.onClose}
>
<Dialog
open={props.scrollToFile !== null}
onClose={props.onClose}
width="100vw"
labelledBy={titleId}
panelStyle={{
height: '100vh',
'max-height': 'none',
'max-width': 'none',
'border-radius': '0',
border: 'none',
overflow: 'hidden',
padding: '0',
gap: '0',
}}
>
<h2 id={titleId} class="dialog-sr-only">
Diff viewer for {props.taskName ?? 'task'}: {props.scrollToFile ?? 'all changes'}
</h2>
<Show when={props.scrollToFile !== null}>
<DiffViewerContent
scrollToFile={props.scrollToFile}
taskName={props.taskName}
Expand All @@ -111,9 +128,9 @@ export function DiffViewerDialog(props: DiffViewerDialogProps) {
onCommitNavigate={props.onCommitNavigate}
gitIsolation={props.gitIsolation}
/>
</ReviewProvider>
</Show>
</Dialog>
</Show>
</Dialog>
</ReviewProvider>
);
}

Expand All @@ -128,7 +145,7 @@ function DiffViewerContent(props: DiffViewerDialogProps) {
const [searchQuery, setSearchQuery] = createSignal('');
const [activeFilePath, setActiveFilePath] = createSignal<string | null>(null);

let fetchGeneration = 0;
const fetchGeneration = createRequestGenerationGuard();
let searchInputRef: HTMLInputElement | undefined;
let diffScrollRef: HTMLDivElement | undefined;
let containerRef: HTMLDivElement | undefined;
Expand Down Expand Up @@ -166,8 +183,20 @@ function DiffViewerContent(props: DiffViewerDialogProps) {
const projectRoot = props.projectRoot;
const branchName = props.branchName;
const baseBranch = props.baseBranch;
const thisGen = ++fetchGeneration;
const reviewIdentity = createReviewIdentity({
taskId: props.taskId,
worktreePath,
projectRoot,
branchName,
});
const thisGen = fetchGeneration.begin();

onCleanup(() => {
fetchGeneration.invalidate();
review.suspendDiffLoad();
});

review.beginDiffLoad();
setSearchQuery('');
setLoading(true);
setError('');
Expand Down Expand Up @@ -204,18 +233,20 @@ function DiffViewerContent(props: DiffViewerDialogProps) {
}

diffPromise
.then((rawDiff) => {
if (thisGen !== fetchGeneration) return;
.then(async (rawDiff) => {
if (!fetchGeneration.isCurrent(thisGen)) return;
const newFiles = parseUnifiedDiff(rawDiff);
const diffIdentity = await createDiffIdentity(reviewIdentity, rawDiff);
if (!fetchGeneration.isCurrent(thisGen)) return;
setParsedFiles(newFiles);
review.replaceAnnotations((prev) => evictStaleAnnotations(prev, newFiles));
review.completeDiffLoad(diffIdentity, newFiles);
})
.catch((err) => {
if (thisGen !== fetchGeneration) return;
if (!fetchGeneration.isCurrent(thisGen)) return;
setError(errMessage(err));
})
.finally(() => {
if (thisGen === fetchGeneration) setLoading(false);
if (fetchGeneration.isCurrent(thisGen)) setLoading(false);
});
});

Expand Down
2 changes: 1 addition & 1 deletion src/components/PlanViewerDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ function PlanViewerContent(props: PlanViewerContentProps) {
// Scroll to annotation when scrollTarget changes
createEffect(() => {
const target = review.scrollTarget();
if (!target) return;
if (!target?.id) return;
const y = cardOffsets()[target.id];
if (y !== undefined && scrollRef) {
scrollRef.scrollTo({ top: Math.max(0, y - 100), behavior: 'smooth' });
Expand Down
92 changes: 92 additions & 0 deletions src/components/QualityFindingCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { theme } from '../lib/theme';
import { sf } from '../lib/fontScale';
import { formatQualityFindingLocation, type QualityFinding } from '../lib/quality-findings';
import { CloseIcon } from './icons';
import { qualityFindingSeverityColor } from './quality-finding-colors';

interface QualityFindingCardProps {
finding: QualityFinding;
onDismiss: () => void;
}

export function QualityFindingCard(props: QualityFindingCardProps) {
const color = () => qualityFindingSeverityColor(props.finding.severity);

return (
<div
style={{
margin: '4px 40px 4px 80px',
'max-width': '560px',
'border-left': `3px solid ${color()}`,
'border-radius': '0 4px 4px 0',
background: `color-mix(in srgb, ${theme.bgElevated} 90%, ${color()} 10%)`,
padding: '8px 12px',
'font-family': "'JetBrains Mono', monospace",
}}
>
<div
style={{
display: 'flex',
'align-items': 'center',
gap: '6px',
'font-size': sf(11),
color: color(),
'font-weight': '600',
'text-transform': 'uppercase',
}}
>
<span>Automated</span>
<span aria-hidden="true">·</span>
<span>{props.finding.severity}</span>
<span aria-hidden="true">·</span>
<span>{props.finding.category}</span>
<span style={{ flex: '1' }} />
<button
type="button"
onClick={() => props.onDismiss()}
title="Dismiss finding"
aria-label="Dismiss finding"
style={{
display: 'flex',
background: 'transparent',
border: 'none',
color: theme.fgMuted,
cursor: 'pointer',
padding: '2px',
'border-radius': '3px',
}}
>
<CloseIcon size={13} />
</button>
</div>

<div
style={{
'margin-top': '3px',
color: theme.fgSubtle,
'font-size': sf(11),
display: 'flex',
gap: '6px',
'flex-wrap': 'wrap',
}}
>
<span>{props.finding.source}</span>
<span aria-hidden="true">/</span>
<span>{props.finding.ruleId}</span>
<span aria-hidden="true">·</span>
<span>{formatQualityFindingLocation(props.finding)}</span>
</div>

<div
style={{
color: theme.fg,
'font-size': sf(13),
'white-space': 'pre-wrap',
'margin-top': '5px',
}}
>
{props.finding.explanation}
</div>
</div>
);
}
Loading
Loading