From d17290db158753524a59100dacf32d4f2acbc0f5 Mon Sep 17 00:00:00 2001 From: Yuki9814 <222397878+Yuki9814@users.noreply.github.com> Date: Tue, 25 Aug 2026 02:44:09 +0800 Subject: [PATCH 1/5] feat(session): navigate to comments by id Co-authored-by: OpenAI Codex --- .changeset/calm-coins-navigate.md | 5 ++ docs/agent-workflows.md | 7 ++ scripts/generate-docs.test.ts | 1 + scripts/generate-docs.ts | 13 ++-- skills/hunk-review/SKILL.md | 7 ++ src/app/cli.test.ts | 48 +++++++++++++ src/app/cli.ts | 25 +++++++ src/core/run/commandInputs.ts | 1 + src/hunk-review/skillDocument.ts | 18 ++++- src/session/agent/cliClient.test.ts | 14 ++++ src/session/agent/cliClient.ts | 1 + src/session/agent/surface.ts | 8 +++ .../broker/brokerServer.helpers.test.ts | 71 +++++++++++++++++++ src/session/broker/brokerServer.ts | 35 +++++++-- src/session/protocol.ts | 3 +- src/session/protocolSchemas.test.ts | 1 + src/session/protocolSchemas.ts | 1 + test/session/cli.test.ts | 50 +++++++++++++ website/public/docs/hunk-review-skill.md | 7 ++ .../src/content/docs/docs/reference/cli.md | 5 +- 20 files changed, 306 insertions(+), 15 deletions(-) create mode 100644 .changeset/calm-coins-navigate.md diff --git a/.changeset/calm-coins-navigate.md b/.changeset/calm-coins-navigate.md new file mode 100644 index 000000000..0429bfebc --- /dev/null +++ b/.changeset/calm-coins-navigate.md @@ -0,0 +1,5 @@ +--- +"hunkdiff": minor +--- + +Navigate a live review directly to a comment returned by `hunk session comment list`. diff --git a/docs/agent-workflows.md b/docs/agent-workflows.md index 4dbcfa842..390247dd1 100644 --- a/docs/agent-workflows.md +++ b/docs/agent-workflows.md @@ -66,6 +66,13 @@ hunk session navigate --repo . --file src/App.tsx --hunk 2 hunk session navigate --repo . --next-comment ``` +To jump to an exact comment returned by the JSON list, copy its `commentId` into `--comment`: + +```bash +hunk session comment list --repo . --json +hunk session navigate --repo . --comment --json +``` + Use `reload` when you want the already-open Hunk window to show a different diff or commit: ```bash diff --git a/scripts/generate-docs.test.ts b/scripts/generate-docs.test.ts index 2a0c0793a..11fb0d7d2 100644 --- a/scripts/generate-docs.test.ts +++ b/scripts/generate-docs.test.ts @@ -53,6 +53,7 @@ describe("generated website references", () => { expect(reference).toContain(option.flag); } } + expect(reference).toContain("for `--file` navigation, exactly one of"); }); test("renders every runtime-parsed config key with defaults and compatibility metadata", () => { diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index 8832d4ae8..ad2f02e43 100644 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -188,11 +188,14 @@ export function renderCliReference() { "", "**Constraints:** " + command.constraints - .map((constraint) => - constraint.kind === "exactly-one" - ? `exactly one of ${constraint.flags.map((flag) => `\`${flag}\``).join(", ")}` - : `at most one of ${constraint.flags.map((flag) => `\`${flag}\``).join(", ")}`, - ) + .map((constraint) => { + const scope = constraint.documentationScope + ? `${constraint.documentationScope}, ` + : ""; + return constraint.kind === "exactly-one" + ? `${scope}exactly one of ${constraint.flags.map((flag) => `\`${flag}\``).join(", ")}` + : `${scope}at most one of ${constraint.flags.map((flag) => `\`${flag}\``).join(", ")}`; + }) .join("; ") + ".", ); diff --git a/skills/hunk-review/SKILL.md b/skills/hunk-review/SKILL.md index 01c7472d5..df18001a8 100644 --- a/skills/hunk-review/SKILL.md +++ b/skills/hunk-review/SKILL.md @@ -59,6 +59,7 @@ hunk session review ( | --repo ) [--include-patch] [--include- ```bash hunk session navigate ( | --repo ) --file (--hunk | --old-line | --new-line ) [--json] +hunk session navigate ( | --repo ) --comment [--json] hunk session navigate ( | --repo ) (--next-comment | --prev-comment) [--json] ``` @@ -70,6 +71,12 @@ hunk session navigate --repo . --file src/App.tsx --new-line 372 hunk session navigate --repo . --file src/App.tsx --old-line 355 ``` +Exact comment navigation uses the `commentId` returned by `hunk session comment list --json` and does not require `--file`: + +```bash +hunk session navigate --repo . --comment comment-1 +``` + Relative comment navigation jumps between annotated hunks and does not require `--file`: ```bash diff --git a/src/app/cli.test.ts b/src/app/cli.test.ts index c3f3dcf53..cf5fcc2ed 100644 --- a/src/app/cli.test.ts +++ b/src/app/cli.test.ts @@ -1251,6 +1251,54 @@ describe("parseCli", () => { }); }); + test("parses session navigate with a comment id", async () => { + const parsed = await parseCli([ + "bun", + "hunk", + "session", + "navigate", + "--repo", + "/tmp/repo", + "--comment", + "comment-1", + "--json", + ]); + + expect(parsed).toEqual({ + kind: "session", + action: "navigate", + selector: { repoRoot: resolve("/tmp/repo") }, + commentId: "comment-1", + output: "json", + }); + }); + + test("rejects session navigate when --comment is combined with another selector", async () => { + const conflictingOptions = [ + ["--file", "README.md"], + ["--hunk", "1"], + ["--old-line", "10"], + ["--new-line", "10"], + ["--next-comment"], + ["--prev-comment"], + ]; + + for (const conflictingOption of conflictingOptions) { + await expect( + parseCli([ + "bun", + "hunk", + "session", + "navigate", + "session-1", + "--comment", + "comment-1", + ...conflictingOption, + ]), + ).rejects.toThrow("Specify exactly one navigation selector"); + } + }); + test("rejects session navigate with both --next-comment and --prev-comment", async () => { await expect( parseCli([ diff --git a/src/app/cli.ts b/src/app/cli.ts index f761b7e97..f9f29e83c 100644 --- a/src/app/cli.ts +++ b/src/app/cli.ts @@ -1072,6 +1072,31 @@ async function parseSessionCommand(tokens: string[]): Promise { await parseStandaloneCommand(command, rest); + // A comment id is resolved by the daemon and must not silently discard another selector. + const commentHasConflictingSelector = + parsedOptions.comment !== undefined && + (parsedOptions.file !== undefined || + parsedOptions.hunk !== undefined || + parsedOptions.oldLine !== undefined || + parsedOptions.newLine !== undefined || + parsedOptions.nextComment === true || + parsedOptions.prevComment === true); + if (commentHasConflictingSelector) { + throw new Error( + "Specify exactly one navigation selector: --comment, --next-comment / --prev-comment, or --file with a navigation target.", + ); + } + + if (parsedOptions.comment !== undefined) { + return { + kind: "session", + action: "navigate", + output: resolveJsonOutput(parsedOptions), + selector: resolveExplicitSessionSelector(parsedSessionId, parsedOptions.repo), + commentId: parsedOptions.comment, + } as const; + } + /** Relative comment navigation mode. */ if (parsedOptions.nextComment || parsedOptions.prevComment) { enforceConstraint(COMMENT_DIRECTION_CONSTRAINT, parsedOptions); diff --git a/src/core/run/commandInputs.ts b/src/core/run/commandInputs.ts index 54d4b58b3..67fcb1c20 100644 --- a/src/core/run/commandInputs.ts +++ b/src/core/run/commandInputs.ts @@ -162,6 +162,7 @@ export interface SessionNavigateCommandInput { side?: "old" | "new"; line?: number; commentDirection?: "next" | "prev"; + commentId?: string; } export interface SessionReloadCommandInput { diff --git a/src/hunk-review/skillDocument.ts b/src/hunk-review/skillDocument.ts index fdfa9013a..4fbfabe75 100644 --- a/src/hunk-review/skillDocument.ts +++ b/src/hunk-review/skillDocument.ts @@ -23,10 +23,18 @@ function synopsisLines(...specs: AgentCommandSpec[]) { const commands = SESSION_AGENT_COMMANDS; -/** Navigate examples that anchor on a file are absolute; the rest jump between comments. */ -function navigateExamples(kind: "absolute" | "relative") { +/** Group navigation examples by their selector mode. */ +function navigateExamples(kind: "absolute" | "comment" | "relative") { const examples = commands.navigate.examples ?? []; - return examples.filter((example) => example.includes("--file") === (kind === "absolute")); + return examples.filter((example) => { + if (kind === "absolute") { + return example.includes("--file"); + } + if (kind === "comment") { + return example.includes("--comment "); + } + return example.includes("--next-comment") || example.includes("--prev-comment"); + }); } const FRONTMATTER = [ @@ -98,6 +106,10 @@ const NAVIGATE_SECTION = [ "", ...bashFence(navigateExamples("absolute")), "", + "Exact comment navigation uses the `commentId` returned by `hunk session comment list --json` and does not require `--file`:", + "", + ...bashFence(navigateExamples("comment")), + "", "Relative comment navigation jumps between annotated hunks and does not require `--file`:", "", ...bashFence(navigateExamples("relative")), diff --git a/src/session/agent/cliClient.test.ts b/src/session/agent/cliClient.test.ts index b0b1f7b45..79431964a 100644 --- a/src/session/agent/cliClient.test.ts +++ b/src/session/agent/cliClient.test.ts @@ -162,6 +162,15 @@ describe("HTTP Hunk session CLI client", () => { output: "json", }), ).toEqual({ fileId: "file-1", filePath: "src/app.ts", hunkIndex: 1 }); + expect( + await client.navigateToHunk({ + kind: "session", + action: "navigate", + selector, + commentId: "comment-1", + output: "json", + }), + ).toEqual({ fileId: "file-1", filePath: "src/app.ts", hunkIndex: 1 }); expect( await client.reloadSession({ kind: "session", @@ -264,6 +273,11 @@ describe("HTTP Hunk session CLI client", () => { line: 12, commentDirection: "next", }, + { + action: "navigate", + selector, + commentId: "comment-1", + }, { action: "reload", selector, diff --git a/src/session/agent/cliClient.ts b/src/session/agent/cliClient.ts index 62c8d14b6..511bf0eed 100644 --- a/src/session/agent/cliClient.ts +++ b/src/session/agent/cliClient.ts @@ -140,6 +140,7 @@ class HttpHunkSessionCliClient implements HunkSessionCliClient { side: input.side, line: input.line, commentDirection: input.commentDirection, + commentId: input.commentId, }) ).result; } diff --git a/src/session/agent/surface.ts b/src/session/agent/surface.ts index 16993e421..6a215699d 100644 --- a/src/session/agent/surface.ts +++ b/src/session/agent/surface.ts @@ -95,10 +95,14 @@ export type AgentCommandConstraint = readonly kind: "exactly-one"; /** Names the choice in the error message, e.g. "navigation target". */ readonly label: string; + /** Optional context shown in generated docs when this rule applies to one command mode. */ + readonly documentationScope?: string; readonly flags: readonly string[]; } | { readonly kind: "at-most-one"; + /** Optional context shown in generated docs when this rule applies to one command mode. */ + readonly documentationScope?: string; readonly flags: readonly string[]; }; @@ -147,6 +151,7 @@ export const RELOAD_SELECTOR_SYNOPSIS = "( | --repo | --sessi export const NAVIGATE_TARGET_CONSTRAINT = { kind: "exactly-one", label: "navigation target", + documentationScope: "for `--file` navigation", flags: ["--hunk ", "--old-line ", "--new-line "], } as const satisfies AgentCommandConstraint; @@ -268,6 +273,7 @@ export const SESSION_AGENT_COMMANDS = { }, oldLineOption, newLineOption, + { flag: "--comment ", description: "jump to the live comment with this id" }, { flag: "--next-comment", description: "jump to the next annotated hunk" }, { flag: "--prev-comment", description: "jump to the previous annotated hunk" }, jsonOption, @@ -275,12 +281,14 @@ export const SESSION_AGENT_COMMANDS = { constraints: [NAVIGATE_TARGET_CONSTRAINT, COMMENT_DIRECTION_CONSTRAINT], synopsis: [ `hunk session navigate ${SESSION_SELECTOR_SYNOPSIS} --file ${constraintSynopsis(NAVIGATE_TARGET_CONSTRAINT)} [--json]`, + `hunk session navigate ${SESSION_SELECTOR_SYNOPSIS} --comment [--json]`, `hunk session navigate ${SESSION_SELECTOR_SYNOPSIS} ${constraintSynopsis(COMMENT_DIRECTION_CONSTRAINT)} [--json]`, ], examples: [ "hunk session navigate --repo . --file src/App.tsx --hunk 2", "hunk session navigate --repo . --file src/App.tsx --new-line 372", "hunk session navigate --repo . --file src/App.tsx --old-line 355", + "hunk session navigate --repo . --comment comment-1", "hunk session navigate --repo . --next-comment", "hunk session navigate --repo . --prev-comment", ], diff --git a/src/session/broker/brokerServer.helpers.test.ts b/src/session/broker/brokerServer.helpers.test.ts index 41b047411..77b2520c5 100644 --- a/src/session/broker/brokerServer.helpers.test.ts +++ b/src/session/broker/brokerServer.helpers.test.ts @@ -258,6 +258,77 @@ describe("handleSessionApiRequest", () => { expect(dispatchInput.hunkIndex).toBe(1); }); + test("resolves a comment id before dispatching a navigate command", async () => { + const { state, calls } = createFakeState({ + listComments: () => [ + { + commentId: "comment-1", + filePath: "src/example.ts", + hunkIndex: 2, + side: "old", + line: 17, + summary: "Inspect this line", + createdAt: "2026-08-25T00:00:00.000Z", + }, + ], + }); + const response = await handleSessionApiRequest( + state, + apiRequest({ + action: "navigate", + selector: { sessionId: "s-1" }, + commentId: "comment-1", + } as SessionDaemonRequest), + ); + + expect(response.status).toBe(200); + const dispatch = calls.find((call) => call.method === "dispatchCommand"); + expect(dispatch).toBeDefined(); + expect((dispatch!.args[0] as { input: unknown }).input).toMatchObject({ + sessionId: "s-1", + filePath: "src/example.ts", + hunkIndex: 2, + side: "old", + line: 17, + }); + expect((dispatch!.args[0] as { input: unknown }).input).not.toHaveProperty("commentId"); + }); + + test("rejects navigation to an unknown comment id", async () => { + const { state } = createFakeState(); + const response = await handleSessionApiRequest( + state, + apiRequest({ + action: "navigate", + selector: { sessionId: "s-1" }, + commentId: "missing-comment", + } as SessionDaemonRequest), + ); + + expect(response.status).toBe(400); + expect(await response.json()).toMatchObject({ + error: expect.stringContaining("missing-comment"), + }); + }); + + test("rejects a comment id combined with another navigation target", async () => { + const { state } = createFakeState(); + const response = await handleSessionApiRequest( + state, + apiRequest({ + action: "navigate", + selector: { sessionId: "s-1" }, + commentId: "comment-1", + hunkNumber: 2, + } as SessionDaemonRequest), + ); + + expect(response.status).toBe(400); + expect(await response.json()).toMatchObject({ + error: expect.stringContaining("cannot be combined"), + }); + }); + test("dispatches reload, comment-add, comment-rm, and comment-clear commands", async () => { const { state, calls } = createFakeState(); const requests: SessionDaemonRequest[] = [ diff --git a/src/session/broker/brokerServer.ts b/src/session/broker/brokerServer.ts index dfe11c800..7d1340b2a 100644 --- a/src/session/broker/brokerServer.ts +++ b/src/session/broker/brokerServer.ts @@ -255,7 +255,30 @@ export async function handleSessionApiRequest(state: HunkSessionBrokerState, req break; } case "navigate": { + const commentHasConflictingTarget = + input.commentId !== undefined && + (input.commentDirection !== undefined || + input.filePath !== undefined || + input.hunkNumber !== undefined || + input.side !== undefined || + input.line !== undefined); + if (commentHasConflictingTarget) { + throw new Error("navigate commentId cannot be combined with another navigation target."); + } + + const comment = + input.commentId === undefined + ? undefined + : state + .listComments(input.selector) + .find((candidate) => candidate.commentId === input.commentId); + if (input.commentId !== undefined && !comment) { + throw new Error( + `No live comment with id "${input.commentId}" exists in the selected session.`, + ); + } if ( + input.commentId === undefined && !input.commentDirection && input.hunkNumber === undefined && (input.side === undefined || input.line === undefined) @@ -269,11 +292,13 @@ export async function handleSessionApiRequest(state: HunkSessionBrokerState, req command: "navigate_to_hunk", input: { ...input.selector, - filePath: input.filePath, - hunkIndex: input.hunkNumber !== undefined ? input.hunkNumber - 1 : undefined, - side: input.side, - line: input.line, - commentDirection: input.commentDirection, + filePath: comment?.filePath ?? input.filePath, + hunkIndex: + comment?.hunkIndex ?? + (input.hunkNumber !== undefined ? input.hunkNumber - 1 : undefined), + side: comment?.side ?? input.side, + line: comment?.line ?? input.line, + commentDirection: comment ? undefined : input.commentDirection, }, timeoutMessage: "Timed out waiting for the session to navigate to the requested hunk.", }), diff --git a/src/session/protocol.ts b/src/session/protocol.ts index bdc3a9c52..ae56001da 100644 --- a/src/session/protocol.ts +++ b/src/session/protocol.ts @@ -36,7 +36,7 @@ export const HUNK_SESSION_API_VERSION = 1; * builds can refresh an older daemon even when it still exposes the same API endpoints. Bump this * when daemon-forwarded payloads change, even if the supported action names stay stable. */ -export const HUNK_SESSION_DAEMON_VERSION = 9; +export const HUNK_SESSION_DAEMON_VERSION = 10; export type SessionDaemonAction = | "list" @@ -85,6 +85,7 @@ export type SessionDaemonRequest = side?: "old" | "new"; line?: number; commentDirection?: "next" | "prev"; + commentId?: string; } | { action: "reload"; diff --git a/src/session/protocolSchemas.test.ts b/src/session/protocolSchemas.test.ts index e1c309cbb..6504965f4 100644 --- a/src/session/protocolSchemas.test.ts +++ b/src/session/protocolSchemas.test.ts @@ -35,6 +35,7 @@ describe("session daemon request validation", () => { line: 12, }, { action: "navigate", selector: { sessionId: "s-1" }, commentDirection: "next" }, + { action: "navigate", selector: { sessionId: "s-1" }, commentId: "comment-1" }, { action: "reload", selector: { sessionId: "s-1" }, diff --git a/src/session/protocolSchemas.ts b/src/session/protocolSchemas.ts index 2de226cc9..faa4836e1 100644 --- a/src/session/protocolSchemas.ts +++ b/src/session/protocolSchemas.ts @@ -64,6 +64,7 @@ export const sessionDaemonRequestSchema = z.discriminatedUnion("action", [ side: sideSchema.optional(), line: z.int().positive().optional(), commentDirection: z.enum(["next", "prev"]).optional(), + commentId: z.string().min(1).optional(), }), z.strictObject({ action: z.literal("reload"), diff --git a/test/session/cli.test.ts b/test/session/cli.test.ts index c506e277b..af6f1591f 100644 --- a/test/session/cli.test.ts +++ b/test/session/cli.test.ts @@ -644,6 +644,56 @@ sessionDescribe("session CLI integration", () => { }, }); + const commentId = addedComment.result?.commentId; + expect(commentId).toBeDefined(); + const navigateToComment = runSessionCli( + ["navigate", sessionId, "--comment", commentId!, "--json"], + port, + ); + expect(navigateToComment.proc.exitCode).toBe(0); + expect(navigateToComment.stderr).toBe(""); + expect(JSON.parse(navigateToComment.stdout)).toMatchObject({ + result: { + filePath: fixture.afterName, + hunkIndex: 1, + side: "new", + line: 10, + }, + }); + + await waitUntil("comment navigation context", () => { + const context = runSessionCli(["context", sessionId, "--json"], port); + if (context.proc.exitCode !== 0) { + return null; + } + + const parsed = JSON.parse(context.stdout) as { + context?: { selectedHunk?: { index: number } }; + }; + return parsed.context?.selectedHunk?.index === 1 ? parsed : null; + }); + + const resetAfterCommentNavigation = runSessionCli( + ["navigate", sessionId, "--file", fixture.afterName, "--hunk", "1", "--json"], + port, + ); + expect(resetAfterCommentNavigation.proc.exitCode).toBe(0); + expect(resetAfterCommentNavigation.stderr).toBe(""); + + await waitUntil("reset after comment navigation", () => { + const context = runSessionCli(["context", sessionId, "--json"], port); + if (context.proc.exitCode !== 0) { + return null; + } + + const parsed = JSON.parse(context.stdout) as { + context?: { selectedHunk?: { index: number }; showAgentNotes?: boolean }; + }; + return parsed.context?.selectedHunk?.index === 0 && parsed.context?.showAgentNotes === false + ? parsed + : null; + }); + const focusedComment = runSessionCli( [ "comment", diff --git a/website/public/docs/hunk-review-skill.md b/website/public/docs/hunk-review-skill.md index 01c7472d5..df18001a8 100644 --- a/website/public/docs/hunk-review-skill.md +++ b/website/public/docs/hunk-review-skill.md @@ -59,6 +59,7 @@ hunk session review ( | --repo ) [--include-patch] [--include- ```bash hunk session navigate ( | --repo ) --file (--hunk | --old-line | --new-line ) [--json] +hunk session navigate ( | --repo ) --comment [--json] hunk session navigate ( | --repo ) (--next-comment | --prev-comment) [--json] ``` @@ -70,6 +71,12 @@ hunk session navigate --repo . --file src/App.tsx --new-line 372 hunk session navigate --repo . --file src/App.tsx --old-line 355 ``` +Exact comment navigation uses the `commentId` returned by `hunk session comment list --json` and does not require `--file`: + +```bash +hunk session navigate --repo . --comment comment-1 +``` + Relative comment navigation jumps between annotated hunks and does not require `--file`: ```bash diff --git a/website/src/content/docs/docs/reference/cli.md b/website/src/content/docs/docs/reference/cli.md index 1c1529b00..93d2bf771 100644 --- a/website/src/content/docs/docs/reference/cli.md +++ b/website/src/content/docs/docs/reference/cli.md @@ -339,6 +339,7 @@ move a live Hunk session to one diff hunk ```bash hunk session navigate ( | --repo ) --file (--hunk | --old-line | --new-line ) [--json] +hunk session navigate ( | --repo ) --comment [--json] hunk session navigate ( | --repo ) (--next-comment | --prev-comment) [--json] ``` @@ -349,13 +350,14 @@ hunk session navigate ( | --repo ) (--next-comment | --prev-co | `--hunk ` | 1-based hunk number within the file | | `--old-line ` | 1-based line number on the old side | | `--new-line ` | 1-based line number on the new side | +| `--comment ` | jump to the live comment with this id | | `--next-comment` | jump to the next annotated hunk | | `--prev-comment` | jump to the previous annotated hunk | | `--json` | emit structured JSON | **Positionals:** `[sessionId]`. -**Constraints:** exactly one of `--hunk `, `--old-line `, `--new-line `; at most one of `--next-comment`, `--prev-comment`. +**Constraints:** for `--file` navigation, exactly one of `--hunk `, `--old-line `, `--new-line `; at most one of `--next-comment`, `--prev-comment`. **Examples:** @@ -363,6 +365,7 @@ hunk session navigate ( | --repo ) (--next-comment | --prev-co hunk session navigate --repo . --file src/App.tsx --hunk 2 hunk session navigate --repo . --file src/App.tsx --new-line 372 hunk session navigate --repo . --file src/App.tsx --old-line 355 +hunk session navigate --repo . --comment comment-1 hunk session navigate --repo . --next-comment hunk session navigate --repo . --prev-comment ``` From 1690ec890629b146fa7eb37db6bcaae2518addfb Mon Sep 17 00:00:00 2001 From: Yuki9814 Date: Tue, 25 Aug 2026 16:32:45 +0800 Subject: [PATCH 2/5] fix(session): preserve exact-line comment navigation --- src/app/session/bridge.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/app/session/bridge.ts b/src/app/session/bridge.ts index ae24acc65..5e983a5f3 100644 --- a/src/app/session/bridge.ts +++ b/src/app/session/bridge.ts @@ -97,8 +97,15 @@ export function createHunkSessionBridge(handlers: HunkSessionBridgeHandlers) { return result; } - case "navigate_to_hunk": - return handlers.navigateToLocation(message.input); + case "navigate_to_hunk": { + // Exact line coordinates are more specific than a hunk index. Comment-id navigation + // resolves to both, so drop the hunk hint and let the terminal reveal the annotated row. + const input = + message.input.side && message.input.line !== undefined + ? { ...message.input, hunkIndex: undefined } + : message.input; + return handlers.navigateToLocation(input); + } case "highlight": return handlers.addAgentLineHighlight(message.input); case "clear_highlights": @@ -128,4 +135,4 @@ export function createHunkSessionBridge(handlers: HunkSessionBridgeHandlers) { } }, }; -} +} \ No newline at end of file From 9aa40bffe822c5ce094abc8af152a40ab673659c Mon Sep 17 00:00:00 2001 From: Yuki9814 Date: Tue, 25 Aug 2026 16:33:21 +0800 Subject: [PATCH 3/5] test(session): cover exact-line navigation precedence --- src/app/session/bridge.test.ts | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/app/session/bridge.test.ts b/src/app/session/bridge.test.ts index 33e4d71e2..6e45251d9 100644 --- a/src/app/session/bridge.test.ts +++ b/src/app/session/bridge.test.ts @@ -118,6 +118,32 @@ describe("createHunkSessionBridge", () => { expect(handlers.openAgentNotes).toHaveBeenCalledTimes(1); }); + test("prefers exact line coordinates over a hunk hint", async () => { + const handlers = createHandlers(); + const bridge = createHunkSessionBridge(handlers); + + await bridge.dispatchCommand({ + type: "command", + requestId: "nav-line-1", + command: "navigate_to_hunk", + input: { + sessionId: "session-1", + filePath: "src/example.ts", + hunkIndex: 2, + side: "new", + line: 17, + }, + }); + + expect(handlers.navigateToLocation).toHaveBeenCalledWith({ + sessionId: "session-1", + filePath: "src/example.ts", + hunkIndex: undefined, + side: "new", + line: 17, + }); + }); + test("routes navigate, reload, remove, and clear commands through their dedicated handlers", async () => { const handlers = createHandlers(); const bridge = createHunkSessionBridge(handlers); @@ -163,4 +189,4 @@ describe("createHunkSessionBridge", () => { includeUser: true, }); }); -}); +}); \ No newline at end of file From 9c5fd845e17ed7ba3471e63627ddd6dd819a3c78 Mon Sep 17 00:00:00 2001 From: Yuki9814 Date: Tue, 25 Aug 2026 16:35:53 +0800 Subject: [PATCH 4/5] chore(session): restore trailing newline --- src/app/session/bridge.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/session/bridge.ts b/src/app/session/bridge.ts index 5e983a5f3..4e1315521 100644 --- a/src/app/session/bridge.ts +++ b/src/app/session/bridge.ts @@ -135,4 +135,4 @@ export function createHunkSessionBridge(handlers: HunkSessionBridgeHandlers) { } }, }; -} \ No newline at end of file +} From 59aa164d7ab9648244c1a3818ff77b435649d788 Mon Sep 17 00:00:00 2001 From: Yuki9814 Date: Tue, 25 Aug 2026 16:36:15 +0800 Subject: [PATCH 5/5] chore(session): restore test file newline --- src/app/session/bridge.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/session/bridge.test.ts b/src/app/session/bridge.test.ts index 6e45251d9..d84a27c31 100644 --- a/src/app/session/bridge.test.ts +++ b/src/app/session/bridge.test.ts @@ -189,4 +189,4 @@ describe("createHunkSessionBridge", () => { includeUser: true, }); }); -}); \ No newline at end of file +});