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
37 changes: 33 additions & 4 deletions src/core/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@ async function parseSessionCommand(tokens: string[]): Promise<ParsedCliInput> {
" hunk session context <session-id>",
" hunk session context --repo <path>",
" hunk session navigate (<session-id> | --repo <path>) --file <path> (--hunk <n> | --old-line <n> | --new-line <n>)",
" hunk session navigate (<session-id> | --repo <path>) (--next-comment | --prev-comment)",
" hunk session reload (<session-id> | --repo <path> | --session-path <path>) [--source <path>] -- diff [ref] [-- <pathspec...>]",
" hunk session reload (<session-id> | --repo <path> | --session-path <path>) [--source <path>] -- show [ref] [-- <pathspec...>]",
" hunk session comment add (<session-id> | --repo <path>) --file <path> (--old-line <n> | --new-line <n>) --summary <text>",
Expand Down Expand Up @@ -569,32 +570,38 @@ async function parseSessionCommand(tokens: string[]): Promise<ParsedCliInput> {
const command = new Command("session navigate")
.description("move a live Hunk session to one diff hunk")
.argument("[sessionId]")
.requiredOption("--file <path>", "diff file path as shown by Hunk")
.option("--file <path>", "diff file path as shown by Hunk")
.option("--repo <path>", "target the live session whose repo root matches this path")
.option("--hunk <n>", "1-based hunk number within the file", parsePositiveInt)
.option("--old-line <n>", "1-based line number on the old side", parsePositiveInt)
.option("--new-line <n>", "1-based line number on the new side", parsePositiveInt)
.option("--next-comment", "jump to the next annotated hunk")
.option("--prev-comment", "jump to the previous annotated hunk")
.option("--json", "emit structured JSON");

let parsedSessionId: string | undefined;
let parsedOptions: {
repo?: string;
file: string;
file?: string;
hunk?: number;
oldLine?: number;
newLine?: number;
nextComment?: boolean;
prevComment?: boolean;
json?: boolean;
} = { file: "" };
} = {};

command.action(
(
sessionId: string | undefined,
options: {
repo?: string;
file: string;
file?: string;
hunk?: number;
oldLine?: number;
newLine?: number;
nextComment?: boolean;
prevComment?: boolean;
json?: boolean;
},
) => {
Expand All @@ -609,6 +616,28 @@ async function parseSessionCommand(tokens: string[]): Promise<ParsedCliInput> {

await parseStandaloneCommand(command, rest);

/** Relative comment navigation mode. */
if (parsedOptions.nextComment || parsedOptions.prevComment) {
if (parsedOptions.nextComment && parsedOptions.prevComment) {
throw new Error("Specify either --next-comment or --prev-comment, not both.");
}

return {
kind: "session",
action: "navigate",
output: resolveJsonOutput(parsedOptions),
selector: resolveExplicitSessionSelector(parsedSessionId, parsedOptions.repo),
commentDirection: parsedOptions.nextComment ? "next" : "prev",
} as const;
}

/** Absolute navigation mode requires --file and a target. */
if (!parsedOptions.file) {
throw new Error(
"Specify --file <path> with a navigation target, or use --next-comment / --prev-comment.",
);
}

const selectors = [
parsedOptions.hunk !== undefined,
parsedOptions.oldLine !== undefined,
Expand Down
3 changes: 2 additions & 1 deletion src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,11 @@ export interface SessionNavigateCommandInput {
action: "navigate";
output: SessionCommandOutput;
selector: SessionSelectorInput;
filePath: string;
filePath?: string;
hunkNumber?: number;
side?: "old" | "new";
line?: number;
commentDirection?: "next" | "prev";
}

export interface SessionReloadCommandInput {
Expand Down
2 changes: 2 additions & 0 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ async function handleSessionApiRequest(state: HunkDaemonState, request: Request)
break;
case "navigate": {
if (
!input.commentDirection &&
input.hunkNumber === undefined &&
(input.side === undefined || input.line === undefined)
) {
Expand All @@ -107,6 +108,7 @@ async function handleSessionApiRequest(state: HunkDaemonState, request: Request)
hunkIndex: input.hunkNumber !== undefined ? input.hunkNumber - 1 : undefined,
side: input.side,
line: input.line,
commentDirection: input.commentDirection,
}),
};
break;
Expand Down
3 changes: 2 additions & 1 deletion src/mcp/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,11 @@ export interface NavigateToFileToolInput extends SessionTargetInput {
}

export interface NavigateToHunkToolInput extends SessionTargetInput {
filePath: string;
filePath?: string;
hunkIndex?: number;
side?: DiffSide;
line?: number;
commentDirection?: "next" | "prev";
}

export interface ReloadSessionToolInput extends SessionTargetInput {
Expand Down
1 change: 1 addition & 0 deletions src/session/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ class HttpHunkDaemonCliClient implements HunkDaemonCliClient {
hunkNumber: input.hunkNumber,
side: input.side,
line: input.line,
commentDirection: input.commentDirection,
})
).result;
}
Expand Down
3 changes: 2 additions & 1 deletion src/session/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,11 @@ export type SessionDaemonRequest =
| {
action: "navigate";
selector: SessionNavigateCommandInput["selector"];
filePath: string;
filePath?: string;
hunkNumber?: number;
side?: "old" | "new";
line?: number;
commentDirection?: "next" | "prev";
}
| {
action: "reload";
Expand Down
79 changes: 50 additions & 29 deletions src/ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ import { PaneDivider } from "./components/panes/PaneDivider";
import { useHunkSessionBridge } from "./hooks/useHunkSessionBridge";
import { useMenuController } from "./hooks/useMenuController";
import { buildAppMenus } from "./lib/appMenus";
import { buildSidebarEntries } from "./lib/files";
import { buildHunkCursors, findNextHunkCursor } from "./lib/hunks";
import { buildSidebarEntries, filterReviewFiles, mergeFileAnnotationsByFileId } from "./lib/files";
import { buildAnnotatedHunkCursors, buildHunkCursors, findNextHunkCursor } from "./lib/hunks";
import { fileRowId } from "./lib/ids";
import { resolveResponsiveLayout } from "./lib/responsive";
import { resizeSidebarWidth } from "./lib/sidebar";
Expand Down Expand Up @@ -126,6 +126,7 @@ function AppShell({
const [resizeStartWidth, setResizeStartWidth] = useState<number | null>(null);
const [selectedFileId, setSelectedFileId] = useState(bootstrap.changeset.files[0]?.id ?? "");
const [selectedHunkIndex, setSelectedHunkIndex] = useState(0);
const [scrollToNote, setScrollToNote] = useState(false);
const deferredFilter = useDeferredValue(filter);

const pagerMode = Boolean(bootstrap.input.options.pager);
Expand All @@ -135,6 +136,14 @@ function AppShell({
filesScrollRef.current?.scrollChildIntoView(fileRowId(fileId));
setSelectedFileId(fileId);
setSelectedHunkIndex(nextHunkIndex);
setScrollToNote(false);
}, []);

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

const openAgentNotes = useCallback(() => {
Expand All @@ -147,7 +156,9 @@ function AppShell({
const { liveCommentsByFileId } = useHunkSessionBridge({
currentHunk: baseSelectedFile?.metadata.hunks[selectedHunkIndex],
files: bootstrap.changeset.files,
filterQuery: deferredFilter,
hostClient,
jumpToAnnotatedHunk,
jumpToFile,
openAgentNotes,
reloadSession: onReloadSession,
Expand All @@ -157,42 +168,21 @@ function AppShell({
});

const allFiles = useMemo(
() =>
bootstrap.changeset.files.map((file) => {
const liveComments = liveCommentsByFileId[file.id];
if (!liveComments || liveComments.length === 0) {
return file;
}

return {
...file,
agent: {
path: file.path,
summary: file.agent?.summary,
annotations: [...(file.agent?.annotations ?? []), ...liveComments],
},
};
}),
() => mergeFileAnnotationsByFileId(bootstrap.changeset.files, liveCommentsByFileId),
[bootstrap.changeset.files, liveCommentsByFileId],
);

const filteredFiles = allFiles.filter((file) => {
if (!deferredFilter.trim()) {
return true;
}

const haystack = [file.path, file.previousPath, file.agent?.summary]
.filter(Boolean)
.join(" ")
.toLowerCase();
return haystack.includes(deferredFilter.trim().toLowerCase());
});
const filteredFiles = useMemo(
() => filterReviewFiles(allFiles, deferredFilter),
[allFiles, deferredFilter],
);

const selectedFile =
filteredFiles.find((file) => file.id === selectedFileId) ??
allFiles.find((file) => file.id === selectedFileId) ??
filteredFiles[0];
const hunkCursors = buildHunkCursors(filteredFiles);
const annotatedHunkCursors = buildAnnotatedHunkCursors(filteredFiles);

const bodyPadding = pagerMode ? 0 : BODY_PADDING;
const bodyWidth = Math.max(0, terminal.width - bodyPadding);
Expand Down Expand Up @@ -284,6 +274,22 @@ function AppShell({
filesScrollRef.current?.scrollChildIntoView(fileRowId(nextCursor.fileId));
setSelectedFileId(nextCursor.fileId);
setSelectedHunkIndex(nextCursor.hunkIndex);
setScrollToNote(false);
};

/** Move the review focus to the next or previous annotated hunk. */
const moveAnnotatedHunk = (delta: number) => {
const nextCursor = findNextHunkCursor(
annotatedHunkCursors,
selectedFile?.id,
selectedHunkIndex,
delta,
);
if (!nextCursor) {
return;
}

jumpToAnnotatedHunk(nextCursor.fileId, nextCursor.hunkIndex);
};

/** Scroll the main review pane by line steps, viewport fractions, or whole-content jumps. */
Expand Down Expand Up @@ -478,6 +484,7 @@ function AppShell({
focusFilter: () => setFocusArea("filter"),
layoutMode,
moveAnnotatedFile,
moveAnnotatedHunk,
moveHunk,
refreshCurrentInput: triggerRefreshCurrentInput,
requestQuit,
Expand All @@ -502,6 +509,7 @@ function AppShell({
canRefreshCurrentInput,
layoutMode,
moveAnnotatedFile,
moveAnnotatedHunk,
moveHunk,
requestQuit,
triggerRefreshCurrentInput,
Expand Down Expand Up @@ -877,6 +885,18 @@ function AppShell({
closeMenu();
return;
}

if (key.sequence === "{") {
moveAnnotatedHunk(-1);
closeMenu();
return;
}

if (key.sequence === "}") {
moveAnnotatedHunk(1);
closeMenu();
return;
}
});

return (
Expand Down Expand Up @@ -960,6 +980,7 @@ function AppShell({
scrollRef={diffScrollRef}
selectedFileId={selectedFile?.id}
selectedHunkIndex={selectedHunkIndex}
scrollToNote={scrollToNote}
separatorWidth={diffSeparatorWidth}
showAgentNotes={showAgentNotes}
showLineNumbers={showLineNumbers}
Expand Down
1 change: 1 addition & 0 deletions src/ui/components/chrome/HelpDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function HelpDialog({
["Shift+Space", "page up (alt)"],
["d / u", "half page down / up"],
["[ / ]", "previous / next hunk"],
["{ / }", "previous / next comment"],
["Home / End", "jump to top / bottom"],
],
},
Expand Down
Loading
Loading