diff --git a/README.md b/README.md index 123b3bf..9ddd7d0 100644 --- a/README.md +++ b/README.md @@ -176,12 +176,14 @@ hunk session reload --repo /path/to/worktree -- diff hunk session reload --session-path /path/to/live-window --source /path/to/other-checkout -- diff hunk session reload --repo . -- show HEAD~1 -- README.md hunk session comment add --repo . --file README.md --new-line 103 --summary "Tighten this wording" +hunk session comment add --repo . --file README.md --new-line 103 --summary "Tighten this wording" --focus hunk session comment list --repo . hunk session comment rm --repo . hunk session comment clear --repo . --file README.md --yes ``` `hunk session reload ... -- ` swaps what a live session is showing without opening a new TUI window. +Pass `--focus` to jump the live session to the new note. - `--repo ` selects the live session by its current loaded repo root. - `--source ` is reload-only: it changes where the nested `diff` / `show` command runs, but does not select the session. diff --git a/skills/hunk-review/SKILL.md b/skills/hunk-review/SKILL.md index 907a3c7..0ea5812 100644 --- a/skills/hunk-review/SKILL.md +++ b/skills/hunk-review/SKILL.md @@ -91,14 +91,14 @@ hunk session reload --session-path /path/to/live-window --source /path/to/other- ### Comments ```bash -hunk session comment add --repo . --file README.md --new-line 103 --summary "Tighten this wording" [--rationale "..."] [--author "agent"] [--no-reveal] +hunk session comment add --repo . --file README.md --new-line 103 --summary "Tighten this wording" [--rationale "..."] [--author "agent"] [--focus] hunk session comment list --repo . [--file README.md] hunk session comment rm --repo . hunk session comment clear --repo . --yes [--file README.md] ``` - `comment add` requires `--file`, `--summary`, and exactly one of `--old-line` or `--new-line` -- `comment add` reveals the note by default; pass `--no-reveal` to keep the current focus +- Pass `--focus` when you want to jump to the new note - `comment list` and `comment clear` accept optional `--file` - Quote `--summary` and `--rationale` defensively in the shell @@ -126,6 +126,7 @@ Guidelines: - Work in the order that tells the clearest story, not necessarily file order - Navigate before commenting so the user sees the code you're discussing +- Use `--focus` sparingly when the note itself should actively steer the review - Keep comments focused: intent, structure, risks, or follow-ups - Don't comment on every hunk -- highlight what the user wouldn't spot themselves diff --git a/src/core/cli.ts b/src/core/cli.ts index 5282ebb..1150f5a 100644 --- a/src/core/cli.ts +++ b/src/core/cli.ts @@ -484,7 +484,7 @@ async function parseSessionCommand(tokens: string[]): Promise { " hunk session navigate ( | --repo ) (--next-comment | --prev-comment)", " hunk session reload ( | --repo | --session-path ) [--source ] -- diff [ref] [-- ]", " hunk session reload ( | --repo | --session-path ) [--source ] -- show [ref] [-- ]", - " hunk session comment add ( | --repo ) --file (--old-line | --new-line ) --summary ", + " hunk session comment add ( | --repo ) --file (--old-line | --new-line ) --summary [--focus]", " hunk session comment list ( | --repo )", " hunk session comment rm ( | --repo ) ", " hunk session comment clear ( | --repo ) --yes", @@ -728,7 +728,7 @@ async function parseSessionCommand(tokens: string[]): Promise { text: [ "Usage:", - " hunk session comment add ( | --repo ) --file (--old-line | --new-line ) --summary ", + " hunk session comment add ( | --repo ) --file (--old-line | --new-line ) --summary [--focus]", " hunk session comment list ( | --repo ) [--file ]", " hunk session comment rm ( | --repo ) ", " hunk session comment clear ( | --repo ) [--file ] --yes", @@ -747,8 +747,7 @@ async function parseSessionCommand(tokens: string[]): Promise { .option("--new-line ", "1-based line number on the new side", parsePositiveInt) .option("--rationale ", "optional longer explanation") .option("--author ", "optional author label") - .option("--reveal", "jump to and reveal the note") - .option("--no-reveal", "add the note without moving focus") + .option("--focus", "add the note and focus the viewport on it") .option("--json", "emit structured JSON"); let parsedSessionId: string | undefined; @@ -760,7 +759,7 @@ async function parseSessionCommand(tokens: string[]): Promise { newLine?: number; rationale?: string; author?: string; - reveal?: boolean; + focus?: boolean; json?: boolean; } = { file: "", @@ -778,7 +777,7 @@ async function parseSessionCommand(tokens: string[]): Promise { newLine?: number; rationale?: string; author?: string; - reveal?: boolean; + focus?: boolean; json?: boolean; }, ) => { @@ -812,7 +811,7 @@ async function parseSessionCommand(tokens: string[]): Promise { summary: parsedOptions.summary, rationale: parsedOptions.rationale, author: parsedOptions.author, - reveal: parsedOptions.reveal ?? true, + reveal: parsedOptions.focus ?? false, }; } diff --git a/src/ui/hooks/useHunkSessionBridge.ts b/src/ui/hooks/useHunkSessionBridge.ts index f6986e9..dbd3528 100644 --- a/src/ui/hooks/useHunkSessionBridge.ts +++ b/src/ui/hooks/useHunkSessionBridge.ts @@ -177,7 +177,7 @@ export function useHunkSessionBridge({ [file.id]: [...(current[file.id] ?? []), liveComment], })); - if (message.input.reveal ?? true) { + if (message.input.reveal ?? false) { jumpToFile(file.id, hunkIndex); openAgentNotes(); } diff --git a/test/cli.test.ts b/test/cli.test.ts index 71bced1..fbfc410 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -306,7 +306,7 @@ describe("parseCli", () => { ).rejects.toThrow("Pass the replacement Hunk command after `--`"); }); - test("parses session comment add", async () => { + test("parses session comment add without focusing by default", async () => { const parsed = await parseCli([ "bun", "hunk", @@ -324,7 +324,6 @@ describe("parseCli", () => { "Live review is the main value.", "--author", "Pi", - "--no-reveal", ]); expect(parsed).toEqual({ @@ -342,6 +341,36 @@ describe("parseCli", () => { }); }); + test("parses session comment add with --focus", async () => { + const parsed = await parseCli([ + "bun", + "hunk", + "session", + "comment", + "add", + "session-1", + "--file", + "README.md", + "--new-line", + "103", + "--summary", + "Frame this as MCP-first", + "--focus", + ]); + + expect(parsed).toEqual({ + kind: "session", + action: "comment-add", + selector: { sessionId: "session-1" }, + filePath: "README.md", + side: "new", + line: 103, + summary: "Frame this as MCP-first", + reveal: true, + output: "text", + }); + }); + test("parses session comment list with file filter", async () => { const parsed = await parseCli([ "bun", diff --git a/test/mcp-e2e.test.ts b/test/mcp-e2e.test.ts index a5ef01f..1c3aed9 100644 --- a/test/mcp-e2e.test.ts +++ b/test/mcp-e2e.test.ts @@ -221,6 +221,7 @@ describe("live session end-to-end", () => { "Injected after the Hunk session auto-started the local daemon.", "--author", "Pi", + "--focus", "--json", ], port, @@ -428,6 +429,7 @@ describe("live session end-to-end", () => { "Alpha note", "--rationale", "Delivered only to the alpha Hunk session.", + "--focus", ], port, ); @@ -446,6 +448,7 @@ describe("live session end-to-end", () => { "Beta note", "--rationale", "Delivered only to the beta Hunk session.", + "--focus", ], port, ); diff --git a/test/session-cli.test.ts b/test/session-cli.test.ts index 1c1a6de..32591c3 100644 --- a/test/session-cli.test.ts +++ b/test/session-cli.test.ts @@ -261,7 +261,7 @@ describe("session CLI", () => { } }, 20_000); - test("navigate and comment add control a live Hunk session", async () => { + test("navigate works, and comment add only focuses the session when --focus is passed", async () => { if (!ttyToolsAvailable) { return; } @@ -340,6 +340,27 @@ describe("session CLI", () => { return parsed.context?.selectedHunk?.index === 1 ? parsed : null; }); + const resetSelection = runSessionCli( + ["navigate", sessionId, "--file", fixture.afterName, "--hunk", "1", "--json"], + port, + ); + expect(resetSelection.proc.exitCode).toBe(0); + expect(resetSelection.stderr).toBe(""); + + await waitUntil("reset session 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 }; showAgentNotes?: boolean }; + }; + return parsed.context?.selectedHunk?.index === 0 && parsed.context?.showAgentNotes === false + ? parsed + : null; + }); + const comment = runSessionCli( [ "comment", @@ -378,8 +399,73 @@ describe("session CLI", () => { line: 10, }, }); - expect(typeof addedComment.result?.commentId).toBe("string"); + + await waitUntil("comment registered without focus", () => { + const listedComments = runSessionCli(["comment", "list", sessionId, "--json"], port); + if (listedComments.proc.exitCode !== 0) { + return null; + } + + const parsed = JSON.parse(listedComments.stdout) as { + comments?: Array<{ summary?: string }>; + }; + return parsed.comments?.some((comment) => comment.summary === "Second hunk note") + ? parsed + : null; + }); + + const unchangedContext = runSessionCli(["context", sessionId, "--json"], port); + expect(unchangedContext.proc.exitCode).toBe(0); + expect(JSON.parse(unchangedContext.stdout)).toMatchObject({ + context: { + selectedHunk: { + index: 0, + }, + showAgentNotes: false, + }, + }); + + const focusedComment = runSessionCli( + [ + "comment", + "add", + sessionId, + "--file", + fixture.afterName, + "--new-line", + "10", + "--summary", + "Second hunk focused note", + "--focus", + "--json", + ], + port, + ); + expect(focusedComment.proc.exitCode).toBe(0); + expect(focusedComment.stderr).toBe(""); + expect(JSON.parse(focusedComment.stdout)).toMatchObject({ + result: { + filePath: fixture.afterName, + hunkIndex: 1, + side: "new", + line: 10, + }, + }); + + await waitUntil("focused session 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 }; showAgentNotes?: boolean }; + }; + return parsed.context?.selectedHunk?.index === 1 && parsed.context?.showAgentNotes === true + ? parsed + : null; + }); } finally { session.kill(); await session.exited;