diff --git a/src/ui/App.tsx b/src/ui/App.tsx index a010d6a7..d420f974 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -33,7 +33,6 @@ import { FilesPane } from "./components/panes/FilesPane"; import { PaneDivider } from "./components/panes/PaneDivider"; import { useHunkSessionBridge } from "./hooks/useHunkSessionBridge"; import { useMenuController } from "./hooks/useMenuController"; -import { getSelectedAnnotations } from "./lib/agentAnnotations"; import { buildAppMenus } from "./lib/appMenus"; import { buildFileListEntry } from "./lib/files"; import { buildHunkCursors, findNextHunkCursor } from "./lib/hunks"; @@ -94,7 +93,6 @@ function AppShell({ const [filesPaneWidth, setFilesPaneWidth] = useState(34); const [resizeDragOriginX, setResizeDragOriginX] = useState(null); const [resizeStartWidth, setResizeStartWidth] = useState(null); - const [dismissedAgentNoteIds, setDismissedAgentNoteIds] = useState([]); const [selectedFileId, setSelectedFileId] = useState(bootstrap.changeset.files[0]?.id ?? ""); const [selectedHunkIndex, setSelectedHunkIndex] = useState(0); const deferredFilter = useDeferredValue(filter); @@ -109,7 +107,6 @@ function AppShell({ }, []); const openAgentNotes = useCallback(() => { - setDismissedAgentNoteIds([]); setShowAgentNotes(true); }, []); @@ -176,8 +173,6 @@ function AppShell({ (responsiveLayout.showFilesPane || (forceSidebarOpen && canForceShowFilesPane)); const centerWidth = bodyWidth; const resolvedLayout = responsiveLayout.layout; - const currentHunk = selectedFile?.metadata.hunks[selectedHunkIndex]; - const activeAnnotations = getSelectedAnnotations(selectedFile, currentHunk); const availableCenterWidth = showFilesPane ? Math.max(0, centerWidth - DIVIDER_WIDTH) : Math.max(0, centerWidth); @@ -247,11 +242,6 @@ function AppShell({ filesScrollRef.current?.scrollChildIntoView(fileRowId(selectedFile.id)); }, [selectedFile]); - useEffect(() => { - // Dismissed notes are hunk-local, so reset them when the review focus moves. - setDismissedAgentNoteIds([]); - }, [selectedFile?.id, selectedHunkIndex]); - /** Move the review focus across hunks in stream order. */ const moveHunk = (delta: number) => { const nextCursor = findNextHunkCursor(hunkCursors, selectedFile?.id, selectedHunkIndex, delta); @@ -287,20 +277,9 @@ function AppShell({ jumpToFile(nextFile.id); }; - /** Toggle the note layer while keeping dismissals scoped to the visible hunk. */ + /** Toggle the global agent note layer on or off. */ const toggleAgentNotes = () => { - if (showAgentNotes) { - setShowAgentNotes(false); - setDismissedAgentNoteIds([]); - return; - } - - openAgentNotes(); - }; - - /** Hide one visible note card until the selection changes. */ - const dismissAgentNote = (noteId: string) => { - setDismissedAgentNoteIds((current) => [...current, noteId]); + setShowAgentNotes((current) => !current); }; /** Toggle line-number gutters without changing the diff content itself. */ @@ -337,10 +316,9 @@ function AppShell({ setShowHunkHeaders((current) => !current); }; - /** Jump to the annotated hunk before opening the note layer. */ + /** Jump to an annotated hunk without changing the global note visibility toggle. */ const openAgentNotesAtHunk = (fileId: string, hunkIndex: number) => { jumpToFile(fileId, hunkIndex); - openAgentNotes(); }; /** Leave the app through the shell-owned shutdown path. */ @@ -784,9 +762,7 @@ function AppShell({ ) : null} diff --git a/src/ui/components/panes/DiffPane.tsx b/src/ui/components/panes/DiffPane.tsx index 09b17734..e250a343 100644 --- a/src/ui/components/panes/DiffPane.tsx +++ b/src/ui/components/panes/DiffPane.tsx @@ -1,6 +1,6 @@ import type { ScrollBoxRenderable } from "@opentui/core"; import { useCallback, useEffect, useLayoutEffect, useMemo, useState, type RefObject } from "react"; -import type { AgentAnnotation, DiffFile, LayoutMode } from "../../../core/types"; +import type { DiffFile, LayoutMode } from "../../../core/types"; import type { VisibleAgentNote } from "../../lib/agentAnnotations"; import { measureDiffSectionMetrics } from "../../lib/sectionHeights"; import { diffHunkId, diffSectionId } from "../../lib/ids"; @@ -12,9 +12,7 @@ const EMPTY_VISIBLE_AGENT_NOTES: VisibleAgentNote[] = []; /** Render the main multi-file review stream. */ export function DiffPane({ - activeAnnotations, diffContentWidth, - dismissedAgentNoteIds, files, headerLabelWidth, headerStatsWidth, @@ -30,13 +28,10 @@ export function DiffPane({ wrapLines, theme, width, - onDismissAgentNote, onOpenAgentNotesAtHunk, onSelectFile, }: { - activeAnnotations: AgentAnnotation[]; diffContentWidth: number; - dismissedAgentNoteIds: string[]; files: DiffFile[]; headerLabelWidth: number; headerStatsWidth: number; @@ -52,7 +47,6 @@ export function DiffPane({ wrapLines: boolean; theme: AppTheme; width: number; - onDismissAgentNote: (id: string) => void; onOpenAgentNotesAtHunk: (fileId: string, hunkIndex: number) => void; onSelectFile: (fileId: string) => void; }) { @@ -100,25 +94,27 @@ export function DiffPane({ const visibleAgentNotesByFile = useMemo(() => { const next = new Map(); - if (!showAgentNotes || !selectedFileId) { + if (!showAgentNotes) { return next; } - const dismissedIdSet = new Set(dismissedAgentNoteIds); - const visibleNotes = activeAnnotations - .map((annotation, index) => ({ - id: `annotation:${selectedFileId}:${selectedHunkIndex}:${index}`, - annotation, - })) - .filter((note) => !dismissedIdSet.has(note.id)); - - // Notes only render for the currently selected file/hunk so they stay spatially anchored. - if (visibleNotes.length > 0) { - next.set(selectedFileId, visibleNotes); - } + files.forEach((file) => { + const annotations = file.agent?.annotations ?? []; + if (annotations.length === 0) { + return; + } + + next.set( + file.id, + annotations.map((annotation, index) => ({ + id: `annotation:${file.id}:${annotation.id ?? index}`, + annotation, + })), + ); + }); return next; - }, [activeAnnotations, dismissedAgentNoteIds, selectedFileId, selectedHunkIndex, showAgentNotes]); + }, [files, showAgentNotes]); // Keep exact row rendering for wrapped lines and visible notes; otherwise reserve // offscreen section height and only materialize rows near the viewport. @@ -292,14 +288,7 @@ export function DiffPane({ verticalScrollbarOptions={{ visible: false }} horizontalScrollbarOptions={{ visible: false }} > - + {files.map((file, index) => { const shouldRenderSection = visibleWindowedFileIds?.has(file.id) ?? true; const shouldPrefetchVisibleHighlight = @@ -334,7 +323,6 @@ export function DiffPane({ visibleAgentNotes={ visibleAgentNotesByFile.get(file.id) ?? EMPTY_VISIBLE_AGENT_NOTES } - onDismissAgentNote={onDismissAgentNote} onOpenAgentNotesAtHunk={(hunkIndex) => onOpenAgentNotesAtHunk(file.id, hunkIndex)} onSelect={() => onSelectFile(file.id)} /> diff --git a/src/ui/components/panes/DiffSection.tsx b/src/ui/components/panes/DiffSection.tsx index 630261d8..9cb3d0b1 100644 --- a/src/ui/components/panes/DiffSection.tsx +++ b/src/ui/components/panes/DiffSection.tsx @@ -24,7 +24,6 @@ interface DiffSectionProps { theme: AppTheme; visibleAgentNotes: VisibleAgentNote[]; viewWidth: number; - onDismissAgentNote: (id: string) => void; onOpenAgentNotesAtHunk: (hunkIndex: number) => void; onSelect: () => void; } @@ -47,7 +46,6 @@ function DiffSectionComponent({ theme, visibleAgentNotes, viewWidth, - onDismissAgentNote, onOpenAgentNotesAtHunk, onSelect, }: DiffSectionProps) { @@ -117,7 +115,6 @@ function DiffSectionComponent({ width={viewWidth} annotatedHunkIndices={annotatedHunkIndices} visibleAgentNotes={visibleAgentNotes} - onDismissAgentNote={onDismissAgentNote} onOpenAgentNotesAtHunk={onOpenAgentNotesAtHunk} onHighlightReady={onHighlightReady} selectedHunkIndex={selectedHunkIndex} diff --git a/src/ui/diff/PierreDiffView.tsx b/src/ui/diff/PierreDiffView.tsx index b9d028cf..f5fbed7c 100644 --- a/src/ui/diff/PierreDiffView.tsx +++ b/src/ui/diff/PierreDiffView.tsx @@ -1,7 +1,7 @@ import { useMemo } from "react"; import type { DiffFile, LayoutMode } from "../../core/types"; import { AgentInlineNote, AgentInlineNoteGuideCap } from "../components/panes/AgentInlineNote"; -import { type VisibleAgentNote } from "../lib/agentAnnotations"; +import type { VisibleAgentNote } from "../lib/agentAnnotations"; import type { AppTheme } from "../themes"; import { buildSplitRows, buildStackRows } from "./pierre"; import { buildReviewRenderPlan } from "./reviewRenderPlan"; @@ -16,7 +16,6 @@ export function PierreDiffView({ annotatedHunkIndices = EMPTY_ANNOTATED_HUNK_INDICES, file, layout, - onDismissAgentNote, onOpenAgentNotesAtHunk, onHighlightReady, showLineNumbers = true, @@ -32,7 +31,6 @@ export function PierreDiffView({ annotatedHunkIndices?: Set; file: DiffFile | undefined; layout: Exclude; - onDismissAgentNote?: (id: string) => void; onOpenAgentNotesAtHunk?: (hunkIndex: number) => void; onHighlightReady?: () => void; showLineNumbers?: boolean; @@ -67,12 +65,11 @@ export function PierreDiffView({ ? buildReviewRenderPlan({ fileId: file.id, rows, - selectedHunkIndex, showHunkHeaders, visibleAgentNotes, }) : [], - [file, rows, selectedHunkIndex, showHunkHeaders, visibleAgentNotes], + [file, rows, showHunkHeaders, visibleAgentNotes], ); const lineNumberDigits = useMemo(() => String(file ? findMaxLineNumber(file) : 1).length, [file]); @@ -106,9 +103,6 @@ export function PierreDiffView({ noteIndex={plannedRow.noteIndex} theme={theme} width={width} - onClose={ - onDismissAgentNote ? () => onDismissAgentNote(plannedRow.annotationId) : undefined - } /> ); } diff --git a/src/ui/diff/reviewRenderPlan.ts b/src/ui/diff/reviewRenderPlan.ts index bc8c953d..eebeaa4e 100644 --- a/src/ui/diff/reviewRenderPlan.ts +++ b/src/ui/diff/reviewRenderPlan.ts @@ -4,14 +4,16 @@ import { diffHunkId } from "../lib/ids"; import type { DiffRow } from "./pierre"; const EMPTY_VISIBLE_AGENT_NOTES: VisibleAgentNote[] = []; +const EMPTY_ROW_KEYS = new Set(); type DiffLineRow = Extract; -interface PrimaryVisibleInlineNote { +interface InlineVisibleNotePlacement { anchorKey: string; anchorSide?: "old" | "new"; - guidedRowKeys: Set; endGuideAfterKey?: string; + guidedRowKeys: Set; + hunkIndex: number; note: VisibleAgentNote; noteCount: number; noteIndex: number; @@ -46,12 +48,8 @@ export type PlannedReviewRow = side: "old" | "new"; }; -function hunkRows(rows: DiffRow[], hunkIndex: number) { - return rows.filter((row) => row.hunkIndex === hunkIndex); -} - -function hunkLineRows(rows: DiffRow[], hunkIndex: number) { - return hunkRows(rows, hunkIndex).filter( +function lineRows(rows: DiffRow[]) { + return rows.filter( (row): row is DiffLineRow => row.type === "split-line" || row.type === "stack-line", ); } @@ -104,79 +102,95 @@ function rowOverlapsAnnotation(row: DiffLineRow, annotation: AgentAnnotation) { /** * Resolve the rendered diff row before which the inline note should appear. - * Range-less notes intentionally anchor beside the first code row in the hunk, - * not above the hunk header metadata. + * Range-less notes intentionally anchor beside the first code row in the file, + * not above hunk header metadata. */ -function findInlineNoteAnchorRow( - rows: DiffRow[], - annotation: AgentAnnotation, - selectedHunkIndex: number, -) { - const selectedHunkRows = hunkRows(rows, selectedHunkIndex); - const lineRows = hunkLineRows(rows, selectedHunkIndex); - const headerRow = selectedHunkRows.find((row) => row.type === "hunk-header"); - - return lineRows.find((row) => rowMatchesNote(row, annotation)) ?? lineRows[0] ?? headerRow; +function findInlineNoteAnchorRow(rows: DiffRow[], annotation: AgentAnnotation) { + const fileLineRows = lineRows(rows); + const headerRow = rows.find((row) => row.type === "hunk-header"); + + return ( + fileLineRows.find((row) => rowMatchesNote(row, annotation)) ?? fileLineRows[0] ?? headerRow + ); } -/** - * The render plan shows at most one inline note at a time. - * The first entry in visibleAgentNotes is the primary visible note, while - * noteIndex/noteCount preserve its position within the current visible list. - */ -function selectPrimaryVisibleNote(visibleAgentNotes: VisibleAgentNote[]) { - if (visibleAgentNotes.length === 0) { - return null; +function buildInlineVisibleNotePlacements(rows: DiffRow[], visibleAgentNotes: VisibleAgentNote[]) { + const fileLineRows = lineRows(rows); + const placementsByAnchor = new Map(); + + for (const note of visibleAgentNotes) { + const anchorRow = findInlineNoteAnchorRow(rows, note.annotation); + if (!anchorRow) { + continue; + } + + const anchorSide = annotationAnchor(note.annotation)?.side; + const coveredRows = fileLineRows.filter((row) => rowOverlapsAnnotation(row, note.annotation)); + const fallbackGuideRow = anchorSide ? anchorRow : undefined; + const guideRows = + coveredRows.length > 0 ? coveredRows : fallbackGuideRow ? [fallbackGuideRow] : []; + const anchorPlacements = placementsByAnchor.get(anchorRow.key) ?? []; + + anchorPlacements.push({ + anchorKey: anchorRow.key, + anchorSide, + endGuideAfterKey: guideRows.at(-1)?.key, + guidedRowKeys: + guideRows.length > 0 ? new Set(guideRows.map((row) => row.key)) : EMPTY_ROW_KEYS, + hunkIndex: anchorRow.hunkIndex, + note, + noteCount: 1, + noteIndex: 0, + }); + placementsByAnchor.set(anchorRow.key, anchorPlacements); + } + + for (const placements of placementsByAnchor.values()) { + placements.forEach((placement, index) => { + placement.noteIndex = index; + placement.noteCount = placements.length; + }); } - return { - note: visibleAgentNotes[0]!, - noteIndex: 0, - noteCount: visibleAgentNotes.length, - }; + return placementsByAnchor; } -/** Return the primary visible note, plus the diff rows that should show its guide rail. */ -function buildPrimaryVisibleInlineNote( - rows: DiffRow[], - visibleAgentNotes: VisibleAgentNote[], - selectedHunkIndex: number, -) { - if (selectedHunkIndex < 0) { - return null; - } +function buildNoteGuideSideByRowKey(placementsByAnchor: Map) { + const guideSideByRowKey = new Map(); - const selectedNote = selectPrimaryVisibleNote(visibleAgentNotes); - if (!selectedNote) { - return null; + for (const placements of placementsByAnchor.values()) { + for (const placement of placements) { + if (!placement.anchorSide) { + continue; + } + + for (const rowKey of placement.guidedRowKeys) { + if (!guideSideByRowKey.has(rowKey)) { + guideSideByRowKey.set(rowKey, placement.anchorSide); + } + } + } } - const anchorRow = findInlineNoteAnchorRow(rows, selectedNote.note.annotation, selectedHunkIndex); - if (!anchorRow) { - return null; + return guideSideByRowKey; +} + +function buildGuideCapsByRowKey(placementsByAnchor: Map) { + const guideCapsByRowKey = new Map>(); + + for (const placements of placementsByAnchor.values()) { + for (const placement of placements) { + if (!placement.anchorSide || !placement.endGuideAfterKey) { + continue; + } + + const rowCaps = guideCapsByRowKey.get(placement.endGuideAfterKey) ?? new Set<"old" | "new">(); + rowCaps.add(placement.anchorSide); + guideCapsByRowKey.set(placement.endGuideAfterKey, rowCaps); + } } - const selectedHunkLineRows = hunkLineRows(rows, selectedHunkIndex); - const anchorSide = annotationAnchor(selectedNote.note.annotation)?.side; - const coveredRows = selectedHunkLineRows.filter((row) => - rowOverlapsAnnotation(row, selectedNote.note.annotation), - ); - const fallbackGuideRow = - anchorSide && (anchorRow.type === "split-line" || anchorRow.type === "stack-line") - ? anchorRow - : undefined; - const guideRows = - coveredRows.length > 0 ? coveredRows : fallbackGuideRow ? [fallbackGuideRow] : []; - - return { - anchorKey: anchorRow.key, - anchorSide, - guidedRowKeys: new Set(guideRows.map((row) => row.key)), - endGuideAfterKey: guideRows.at(-1)?.key, - note: selectedNote.note, - noteIndex: selectedNote.noteIndex, - noteCount: selectedNote.noteCount, - } satisfies PrimaryVisibleInlineNote; + return guideCapsByRowKey; } function rowCanAnchorHunk(row: DiffRow, showHunkHeaders: boolean) { @@ -189,27 +203,25 @@ function rowCanAnchorHunk(row: DiffRow, showHunkHeaders: boolean) { /** * Build the explicit presentational row plan for one file diff body. - * The plan always preserves diff-row order and may insert one inline note plus - * one trailing guide cap for the primary visible note. + * The plan always preserves diff-row order and may insert inline notes plus + * trailing guide caps for every visible note anchored in this file. */ export function buildReviewRenderPlan({ fileId, rows, - selectedHunkIndex, showHunkHeaders, visibleAgentNotes = EMPTY_VISIBLE_AGENT_NOTES, + selectedHunkIndex: _selectedHunkIndex, }: { fileId: string; rows: DiffRow[]; - selectedHunkIndex: number; showHunkHeaders: boolean; visibleAgentNotes?: VisibleAgentNote[]; + selectedHunkIndex?: number; }) { - const primaryVisibleNote = buildPrimaryVisibleInlineNote( - rows, - visibleAgentNotes, - selectedHunkIndex, - ); + const placementsByAnchor = buildInlineVisibleNotePlacements(rows, visibleAgentNotes); + const noteGuideSideByRowKey = buildNoteGuideSideByRowKey(placementsByAnchor); + const guideCapsByRowKey = buildGuideCapsByRowKey(placementsByAnchor); const plannedRows: PlannedReviewRow[] = []; const anchoredHunks = new Set(); @@ -222,19 +234,20 @@ export function buildReviewRenderPlan({ anchoredHunks.add(row.hunkIndex); } - if (primaryVisibleNote?.anchorKey === row.key) { + const anchoredNotes = placementsByAnchor.get(row.key) ?? []; + anchoredNotes.forEach((placement) => { plannedRows.push({ kind: "inline-note", - key: `inline-note:${primaryVisibleNote.note.id}:${row.key}`, + key: `inline-note:${placement.note.id}:${row.key}:${placement.noteIndex}`, fileId, - hunkIndex: row.hunkIndex, - annotationId: primaryVisibleNote.note.id, - annotation: primaryVisibleNote.note.annotation, - anchorSide: primaryVisibleNote.anchorSide, - noteCount: primaryVisibleNote.noteCount, - noteIndex: primaryVisibleNote.noteIndex, + hunkIndex: placement.hunkIndex, + annotationId: placement.note.id, + annotation: placement.note.annotation, + anchorSide: placement.anchorSide, + noteCount: placement.noteCount, + noteIndex: placement.noteIndex, }); - } + }); plannedRows.push({ kind: "diff-row", @@ -243,18 +256,19 @@ export function buildReviewRenderPlan({ hunkIndex: row.hunkIndex, row, anchorId, - noteGuideSide: primaryVisibleNote?.guidedRowKeys.has(row.key) - ? primaryVisibleNote.anchorSide - : undefined, + noteGuideSide: noteGuideSideByRowKey.get(row.key), }); - if (primaryVisibleNote?.anchorSide && primaryVisibleNote.endGuideAfterKey === row.key) { - plannedRows.push({ - kind: "note-guide-cap", - key: `note-guide-cap:${primaryVisibleNote.note.id}:${row.key}`, - fileId, - hunkIndex: row.hunkIndex, - side: primaryVisibleNote.anchorSide, + const guideCaps = guideCapsByRowKey.get(row.key); + if (guideCaps) { + Array.from(guideCaps).forEach((side) => { + plannedRows.push({ + kind: "note-guide-cap", + key: `note-guide-cap:${row.key}:${side}`, + fileId, + hunkIndex: row.hunkIndex, + side, + }); }); } } diff --git a/test/app-interactions.test.tsx b/test/app-interactions.test.tsx index d76ccc25..dfe81450 100644 --- a/test/app-interactions.test.tsx +++ b/test/app-interactions.test.tsx @@ -213,6 +213,14 @@ describe("App interactions", () => { expect(frame).toContain("Annotation for alpha.ts"); expect(frame).toContain("Why alpha.ts changed"); + await act(async () => { + await setup.mockInput.typeText("a"); + }); + await flush(setup); + + frame = setup.captureCharFrame(); + expect(frame).not.toContain("Annotation for alpha.ts"); + await act(async () => { await setup.mockInput.typeText("l"); }); @@ -335,6 +343,14 @@ describe("App interactions", () => { await flush(setup); let frame = setup.captureCharFrame(); + if (!frame.includes("Focus files")) { + await act(async () => { + await setup.mockInput.pressKey("F10"); + }); + await flush(setup); + frame = setup.captureCharFrame(); + } + expect(frame).toContain("Focus files"); expect(frame).toContain("Quit"); @@ -361,19 +377,25 @@ describe("App interactions", () => { } }); - test("arrow keys keep the current file selected for agent notes", async () => { - const setup = await testRender(, { - width: 240, - height: 24, - }); + test("a toggles notes for the whole review stream, not just the active hunk", async () => { + const bootstrap = createBootstrap(); + bootstrap.changeset.files[1]!.agent = { + path: "beta.ts", + summary: "beta.ts note", + annotations: [ + { + newRange: [1, 1], + summary: "Annotation for beta.ts", + rationale: "Why beta.ts changed", + }, + ], + }; + + const setup = await testRender(, { width: 240, height: 32 }); try { await flush(setup); - await act(async () => { - await setup.mockInput.pressArrow("down"); - }); - await flush(setup); await act(async () => { await setup.mockInput.typeText("a"); }); @@ -382,6 +404,8 @@ describe("App interactions", () => { const frame = setup.captureCharFrame(); expect(frame).toContain("Annotation for alpha.ts"); expect(frame).toContain("Why alpha.ts changed"); + expect(frame).toContain("Annotation for beta.ts"); + expect(frame).toContain("Why beta.ts changed"); } finally { await act(async () => { setup.renderer.destroy(); diff --git a/test/review-render-plan.test.ts b/test/review-render-plan.test.ts index 0941f079..75026a12 100644 --- a/test/review-render-plan.test.ts +++ b/test/review-render-plan.test.ts @@ -271,7 +271,7 @@ describe("review render plan", () => { } }); - test("keeps note placement scoped to the selected hunk in multi-hunk diffs", () => { + test("anchors notes on the matching hunk in multi-hunk diffs", () => { const theme = resolveTheme("midnight", null); const file = createDiffFile( "multi", @@ -334,7 +334,7 @@ describe("review render plan", () => { } }); - test("renders only the first visible note while preserving visible note count metadata", () => { + test("renders every visible note at its own anchor row", () => { const theme = resolveTheme("midnight", null); const file = createDiffFile( "counted", @@ -371,9 +371,11 @@ describe("review render plan", () => { row.kind === "inline-note", ); - expect(inlineNotes).toHaveLength(1); - expect(inlineNotes[0]?.annotationId).toBe("annotation:counted:0:0"); - expect(inlineNotes[0]?.noteIndex).toBe(0); - expect(inlineNotes[0]?.noteCount).toBe(2); + expect(inlineNotes).toHaveLength(2); + expect(inlineNotes.map((row) => row.annotationId)).toEqual([ + "annotation:counted:0:1", + "annotation:counted:0:0", + ]); + expect(inlineNotes.every((row) => row.noteIndex === 0 && row.noteCount === 1)).toBe(true); }); }); diff --git a/test/tty-render-smoke.test.ts b/test/tty-render-smoke.test.ts index f2198696..e021cd7f 100644 --- a/test/tty-render-smoke.test.ts +++ b/test/tty-render-smoke.test.ts @@ -142,8 +142,8 @@ async function runTtySmoke(options: { } const hunkCommand = `bun run ${shellQuote(sourceEntrypoint)} ${args.map(shellQuote).join(" ")}`; - const scriptCommand = `timeout 5 script -q -f -e -c ${shellQuote(hunkCommand)} ${shellQuote(transcript)}`; - const inputCommand = `(sleep 1; printf q)`; + const scriptCommand = `timeout 7 script -q -f -e -c ${shellQuote(hunkCommand)} ${shellQuote(transcript)}`; + const inputCommand = `(sleep 2; printf q)`; const proc = Bun.spawnSync(["bash", "-lc", `${inputCommand} | ${scriptCommand}`], { cwd: fixture.dir, stdin: "ignore", diff --git a/test/ui-components.test.tsx b/test/ui-components.test.tsx index 5aa322de..b97174a8 100644 --- a/test/ui-components.test.tsx +++ b/test/ui-components.test.tsx @@ -226,9 +226,7 @@ describe("UI components", () => { const theme = resolveTheme("midnight", null); const frame = await captureFrame( { wrapLines={false} theme={theme} width={76} - onDismissAgentNote={() => {}} onOpenAgentNotesAtHunk={() => {}} onSelectFile={() => {}} />, @@ -315,14 +312,26 @@ describe("UI components", () => { expect(lines[5]?.trimStart().startsWith("│")).toBe(true); }); - test("DiffPane renders inline hunk notes beneath the anchored diff row", async () => { + test("DiffPane renders all visible hunk notes across the review stream", async () => { const bootstrap = createBootstrap(); + bootstrap.changeset.files[1]!.agent = { + path: "beta.ts", + summary: "beta.ts note", + annotations: [ + { + newRange: [1, 1], + summary: "Annotation for beta.ts", + rationale: "Why beta.ts changed", + tags: ["review"], + confidence: "high", + }, + ], + }; + const theme = resolveTheme("midnight", null); const frame = await captureFrame( { wrapLines={false} theme={theme} width={92} - onDismissAgentNote={() => {}} onOpenAgentNotesAtHunk={() => {}} onSelectFile={() => {}} />, 96, - 18, + 28, ); expect(frame).toContain("AI note · ▶ new 2"); @@ -351,13 +359,15 @@ describe("UI components", () => { expect(frame.indexOf("AI note · ▶ new 2")).toBeLessThan( frame.indexOf("2 + export const add = true;"), ); + expect(frame).toContain("AI note · ▶ new 1"); + expect(frame).toContain("Annotation for beta.ts"); + expect(frame).toContain("Why beta.ts changed"); expect(frame).not.toContain("alpha.ts note"); expect(frame).not.toContain("review"); expect(frame).not.toContain("confidence"); - expect(frame).toContain("[x]"); }); - test("DiffPane shows one inline note at a time when a hunk has multiple notes", async () => { + test("DiffPane shows all inline notes when a hunk has multiple notes", async () => { const bootstrap = createBootstrap(); const theme = resolveTheme("midnight", null); const file = bootstrap.changeset.files[0]!; @@ -379,9 +389,7 @@ describe("UI components", () => { const frame = await captureFrame( { wrapLines={false} theme={theme} width={92} - onDismissAgentNote={() => {}} onOpenAgentNotesAtHunk={() => {}} onSelectFile={() => {}} />, 96, - 18, + 24, ); expect(frame).toContain("AI note 1/2"); + expect(frame).toContain("AI note 2/2"); expect(frame).toContain("First note"); expect(frame).toContain("First rationale."); - expect(frame).not.toContain("Second note"); - expect(frame).not.toContain("Second rationale."); + expect(frame).toContain("Second note"); + expect(frame).toContain("Second rationale."); }); test("MenuDropdown renders checked items and key hints", async () => { @@ -507,9 +515,7 @@ describe("UI components", () => { const theme = resolveTheme("midnight", null); const frame = await captureFrame( { wrapLines={false} theme={theme} width={76} - onDismissAgentNote={() => {}} onOpenAgentNotesAtHunk={() => {}} onSelectFile={() => {}} />, @@ -540,9 +545,7 @@ describe("UI components", () => { const theme = resolveTheme("midnight", null); const frame = await captureFrame( { wrapLines={false} theme={theme} width={76} - onDismissAgentNote={() => {}} onOpenAgentNotesAtHunk={() => {}} onSelectFile={() => {}} />, @@ -576,9 +578,7 @@ describe("UI components", () => { const theme = resolveTheme("midnight", null); const frame = await captureFrame( { wrapLines={true} theme={theme} width={52} - onDismissAgentNote={() => {}} onOpenAgentNotesAtHunk={() => {}} onSelectFile={() => {}} />, @@ -612,9 +611,7 @@ describe("UI components", () => { const theme = resolveTheme("midnight", null); const frame = await captureFrame( { wrapLines={false} theme={theme} width={76} - onDismissAgentNote={() => {}} onOpenAgentNotesAtHunk={() => {}} onSelectFile={() => {}} />,