Skip to content

feat: comment-to-comment navigation#126

Merged
benvinegar merged 5 commits intomainfrom
feat/annotated-hunk-navigation
Mar 29, 2026
Merged

feat: comment-to-comment navigation#126
benvinegar merged 5 commits intomainfrom
feat/annotated-hunk-navigation

Conversation

@elucid
Copy link
Copy Markdown
Member

@elucid elucid commented Mar 26, 2026

Summary

Jump directly between annotated hunks — skipping unannotated ones — via keyboard, menu, or CLI. This makes it easy for both humans and agents to step through review comments without scrolling through the full diff stream.

What's new

Keyboard: { / }

Navigate to the previous/next hunk that has an annotation (static sidecar or live comment). Mirrors [ / ] for all-hunk navigation but filtered to only commented hunks.

Navigate menu

"Previous comment" and "Next comment" entries with { / } hints, alongside the existing hunk navigation entries.

CLI: --next-comment / --prev-comment

Agents can jump between comments without manually resolving positions:

hunk session navigate --repo . --next-comment
hunk session navigate <session-id> --prev-comment --json

The TUI resolves the next annotated hunk from the current selection and responds with the navigation result. These flags are mutually exclusive with the existing --file/--hunk/--line targeting.

Scroll-to-note

When navigating via {/} or --next-comment/--prev-comment, the viewport scrolls to position the inline note card near the top rather than the hunk top. This keeps the review note visible without requiring extra scrolling through large hunks.

Help dialog

The ? keyboard help now includes { / } previous / next comment.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 26, 2026

Greptile Summary

This PR adds comment-to-comment navigation — {/} keyboard shortcuts, Navigate menu entries, and --next-comment/--prev-comment CLI flags — so both humans and agents can jump directly between annotated hunks without scrolling through the full diff. It also introduces a scroll-to-note behaviour that positions the inline note card near the top of the viewport instead of the hunk header.\n\nKey changes:\n- src/ui/lib/hunks.ts: new buildAnnotatedHunkCursors that filters the flat cursor list to only hunks with overlapping AgentAnnotations or live comments\n- src/ui/App.tsx: moveAnnotatedHunk handler + {/} key bindings + scrollToNote state threaded through to DiffPane\n- src/ui/components/panes/DiffPane.tsx: selectedNoteBounds memo resolves the first inline-note: row in the selected hunk and scrolls to it when scrollToNote is active\n- src/ui/hooks/useHunkSessionBridge.ts: filesWithLiveComments helper merges live comments for annotation queries; commentDirection branch in navigateToHunkSelection for the CLI path\n- src/core/cli.ts / src/session/protocol.ts / src/mcp/server.ts / src/mcp/types.ts: --file relaxed to optional; new commentDirection field propagated through the full CLI→server→TUI stack\n\nIssues found:\n- jumpToFile in App.tsx always sets scrollToNote(false), so CLI --next-comment/--prev-comment never triggers the scroll-to-note behaviour described in the PR — only the keyboard/menu path does\n- When a filter is active, the keyboard path uses filteredFiles for cursor resolution while the CLI path uses the raw unfiltered bootstrap.changeset.files, allowing --next-comment to navigate to a filtered-out file\n- selectedNoteBounds row search has no upper-bound check on row.offset, so it could theoretically latch onto a note in a later hunk if the selected hunk's note row hasn't rendered yet

Confidence Score: 3/5

Safe to merge for the navigation mechanics, but two behavioural gaps vs. the PR description need addressing before the feature is complete as documented

The core navigation logic (buildAnnotatedHunkCursors, findNextHunkCursor, CLI parsing, menu wiring) is correct and well-tested. Two P1 issues break documented behaviour: (1) scroll-to-note doesn't fire for CLI callers because jumpToFile resets the flag, and (2) active filters are ignored for CLI navigation so an agent can silently jump to a filtered-out file. Neither causes data loss or a crash, but both are user-visible regressions relative to the stated goals.

src/ui/App.tsx (jumpToFile reset) and src/ui/hooks/useHunkSessionBridge.ts (unfiltered file list)

Important Files Changed

Filename Overview
src/ui/App.tsx Adds scrollToNote state and moveAnnotatedHunk handler; {/} keyboard bindings; passes scrollToNote to DiffPane. jumpToFile always resets scrollToNote(false), so CLI navigation misses the scroll-to-note feature.
src/ui/hooks/useHunkSessionBridge.ts New filesWithLiveComments helper and commentDirection branch in navigateToHunkSelection. Uses unfiltered bootstrap.changeset.files, diverging from the keyboard path which uses filteredFiles.
src/ui/components/panes/DiffPane.tsx Adds scrollToNote prop and selectedNoteBounds memo that scrolls to the first inline note in the selected hunk. Lacks upper-bound check on the note-row search.
src/ui/lib/hunks.ts Adds buildAnnotatedHunkCursors filtering to only annotated hunks via getAnnotatedHunkIndices. Logic is clean and well-tested.
src/core/cli.ts Adds --next-comment/--prev-comment flags, relaxes --file to optional, and validates mutual exclusivity. CLI-level validation is thorough.
test/hunk-navigation.test.ts New tests for buildAnnotatedHunkCursors using real Pierre-parsed diffs: covers annotated/unannotated hunks, empty sets, gap annotations, and forward/backward navigation.
test/cli.test.ts New tests cover --next-comment, --prev-comment, mutual exclusivity rejection, and missing---file rejection. Complete coverage for new CLI flags.
test/ui-components.test.tsx New DiffPane scrollToNote test verifies the inline note is visible in a 12-row viewport. terminalHeight updated from 28→29 to accommodate the new {/} help row.

Sequence Diagram

sequenceDiagram
    participant User
    participant KeyHandler as App.tsx key handler
    participant Bridge as useHunkSessionBridge
    participant DiffPane

    Note over User,DiffPane: Keyboard path (scroll-to-note works)
    User->>KeyHandler: press { or }
    KeyHandler->>KeyHandler: moveAnnotatedHunk(±1)
    KeyHandler->>KeyHandler: setScrollToNote(true)
    KeyHandler->>DiffPane: selectedHunkIndex + scrollToNote=true
    DiffPane->>DiffPane: selectedNoteBounds → scroll to note row

    Note over User,DiffPane: CLI path (scroll-to-note missing)
    User->>Bridge: --next-comment / --prev-comment
    Bridge->>Bridge: navigateToHunkSelection(commentDirection)
    Bridge->>Bridge: buildAnnotatedHunkCursors(filesWithLiveComments())
    Bridge->>KeyHandler: jumpToFile(fileId, hunkIndex)
    KeyHandler->>KeyHandler: setScrollToNote(false) resets flag
    KeyHandler->>DiffPane: selectedHunkIndex + scrollToNote=false
    DiffPane->>DiffPane: falls back to hunk-top scroll
Loading

Comments Outside Diff (1)

  1. src/ui/App.tsx, line 135-140 (link)

    P1 Scroll-to-note missing for CLI navigation

    jumpToFile unconditionally calls setScrollToNote(false), so when --next-comment/--prev-comment reach the TUI via navigateToHunkSelectionjumpToFile, the scroll-to-note behaviour described in the PR is never triggered for CLI callers.

    The PR description states: "When navigating via {/} or --next-comment/--prev-comment, the viewport scrolls to position the inline note card near the top" — but only the keyboard/menu path sets scrollToNote(true) via moveAnnotatedHunk. The CLI path always lands on scrollToNote = false.

    A straightforward fix is to expose a dedicated jump variant that the bridge can call:

    const jumpToAnnotatedHunk = useCallback((fileId: string, nextHunkIndex = 0) => {
      filesScrollRef.current?.scrollChildIntoView(fileRowId(fileId));
      setSelectedFileId(fileId);
      setSelectedHunkIndex(nextHunkIndex);
      setScrollToNote(true);  // keep note visible after CLI navigation
    }, []);

    Then pass jumpToAnnotatedHunk to the bridge for use in the commentDirection branch of navigateToHunkSelection, while keeping jumpToFile (with scrollToNote(false)) for ordinary file/hunk navigation.

Reviews (1): Last reviewed commit: "feat: scroll to inline note card on comm..." | Re-trigger Greptile

Comment on lines +83 to +110
if (message.input.commentDirection) {
const delta = message.input.commentDirection === "next" ? 1 : -1;
const merged = filesWithLiveComments();
const annotatedCursors = buildAnnotatedHunkCursors(merged);
const nextCursor = findNextHunkCursor(
annotatedCursors,
selectedFile?.id,
selectedHunkIndex,
delta,
);

if (!nextCursor) {
throw new Error("No annotated hunks found in the current review.");
}

const targetFile = merged.find((f) => f.id === nextCursor.fileId);
if (!targetFile) {
throw new Error("Resolved annotated hunk references an unknown file.");
}

jumpToFile(targetFile.id, nextCursor.hunkIndex);
return {
fileId: targetFile.id,
filePath: targetFile.path,
hunkIndex: nextCursor.hunkIndex,
selectedHunk: buildSelectedHunkSummary(targetFile, nextCursor.hunkIndex),
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 CLI comment-navigation uses unfiltered files; keyboard uses filtered files

navigateToHunkSelection calls filesWithLiveComments() which iterates files — and files is wired to bootstrap.changeset.files (all files, see App.tsx line 151).

The keyboard path (moveAnnotatedHunk in App.tsx) builds its cursor list from annotatedHunkCursors = buildAnnotatedHunkCursors(filteredFiles).

When the user has an active filter, pressing {/} navigates only among the visible (filtered) annotated hunks, but --next-comment/--prev-comment can navigate to annotated hunks in filtered-out files. This divergence can silently jump the TUI to a file the user deliberately filtered away.

Consider plumbing the filtered file list into the bridge (e.g., by adding a filteredFiles prop or a getFilteredFiles callback) so both navigation paths operate on the same cursor space.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The bridge now applies the same filter query as the App before building annotated cursors, so {/} and --next-comment / --prev-comment navigate within the same visible review stream. I also added an App interaction test that filters to beta and verifies CLI comment navigation no longer jumps to a hidden annotated file.

Responded by pi-coding-agent using unknown-model.

Comment on lines +459 to +463
const noteRow = metrics.rowMetrics.find(
(row) =>
row.key.startsWith("inline-note:") &&
row.offset >= selectedEstimatedHunkBounds.top - selectedEstimatedHunkBounds.sectionTop,
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No upper-bound guard on inline-note search

The find picks the first inline-note: row whose offset is at or after the selected hunk's section-relative top, but never checks that the row is still within the selected hunk's bottom. If the selected hunk has no inline note rendered yet but a later hunk does, selectedNoteBounds will point to that next hunk's note and the viewport will scroll past the intended position.

In practice scrollToNote is only true after navigating to an annotated hunk (which should always produce a note row), so this is latent rather than immediately broken. Adding an upper-bound guard makes the logic self-documenting and more defensive:

const noteRow = metrics.rowMetrics.find(
  (row) =>
    row.key.startsWith("inline-note:") &&
    row.offset >= selectedEstimatedHunkBounds.top - selectedEstimatedHunkBounds.sectionTop &&
    row.offset < selectedEstimatedHunkBounds.top - selectedEstimatedHunkBounds.sectionTop + selectedEstimatedHunkBounds.height,
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the upper-bound guard and switched the note lookup to explicit section-relative hunk top/bottom bounds, so the scroll-to-note path cannot latch onto a later hunk's inline note.

Responded by pi-coding-agent using unknown-model.

@benvinegar benvinegar force-pushed the feat/annotated-hunk-navigation branch from d655330 to 89651dc Compare March 29, 2026 03:21
@benvinegar
Copy link
Copy Markdown
Member

benvinegar commented Mar 29, 2026

Also addressed the review summary's CLI parity note: --next-comment / --prev-comment now go through a dedicated annotated-hunk jump path, so CLI comment navigation gets the same scroll-to-note behavior as the { / } keyboard path.

Responded by pi-coding-agent using unknown-model.

elucid and others added 5 commits March 28, 2026 23:30
Navigate directly to the next or previous hunk that has an annotation
(static sidecar or live comment), skipping unannotated hunks. This
mirrors the existing [/] hunk navigation but filtered to only the
hunks a reviewer has commented on.

- Add buildAnnotatedHunkCursors() that filters the review stream to
  hunks overlapping any annotation, reusing getAnnotatedHunkIndices
- Wire {/} keybindings in the App keyboard handler
- Add "Previous comment" / "Next comment" entries to the Navigate menu
- Add { / } row to the keyboard help dialog
- Add unit tests for annotated cursor building and navigation
Agents can now jump between annotated hunks without manually resolving
comment positions. The TUI bridge resolves the next or previous
annotated hunk from the current selection using the same cursor logic
as the {/} keyboard shortcuts.

  hunk session navigate --repo . --next-comment
  hunk session navigate <session-id> --prev-comment --json

The comment direction flags are mutually exclusive with the existing
--file/--hunk/--line targeting, which continues to work unchanged.

- Extend SessionNavigateCommandInput with optional commentDirection
- Thread commentDirection through protocol, daemon, and HTTP client
- Resolve relative comment navigation in useHunkSessionBridge using
  buildAnnotatedHunkCursors and findNextHunkCursor
- Make --file optional in the CLI parser when using comment direction
- Add CLI parse tests for both flags and rejection cases
The session bridge receives raw changeset files without live comments
merged into file.agent.annotations. When --next-comment or
--prev-comment was requested, buildAnnotatedHunkCursors saw no
annotations and always returned "No annotated hunks found."

Merge liveCommentsByFileId into the file list before building the
annotated cursor list, mirroring the allFiles useMemo in App.tsx.
When navigating via {/} or --next-comment/--prev-comment, scroll the
viewport so the inline note card is positioned near the top (with 25%
padding) rather than scrolling to the hunk top. This keeps the review
note visible without requiring the user to scroll down through large
hunks to find it.

- Add scrollToNote prop to DiffPane that switches the scroll anchor
  from hunk bounds to the first inline-note row offset
- Set scrollToNote in moveAnnotatedHunk, clear it on other navigation
- Find the note row offset via section metrics row key prefix match
- Add TDD-style component test verifying note visibility with and
  without the flag on a large hunk with a deep annotation
@benvinegar benvinegar force-pushed the feat/annotated-hunk-navigation branch from 89651dc to de7de15 Compare March 29, 2026 03:33
@benvinegar benvinegar merged commit 39982f8 into main Mar 29, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants