From ab0dba01ee902d164dbce8773f765467ebedc498 Mon Sep 17 00:00:00 2001 From: Austin Date: Tue, 30 Jun 2026 12:42:25 -0700 Subject: [PATCH] feat(session): filter comment list by author - Add `--author ` and `--no-author` flags to `hunk session comment list` - Thread the author filters through CLI parsing, types, and session protocol - Apply filtering in the broker for both live and stored comment paths - Cover parsing and broker filtering with unit tests and update agent docs Fixes #489 --- .changeset/comment-list-author-filter.md | 5 ++ docs/agent-workflows.md | 2 + src/core/cli.test.ts | 41 +++++++++++++++ src/core/cli.ts | 22 ++++++-- src/core/types.ts | 2 + src/hunk-session/cli.ts | 2 + .../brokerServer.helpers.test.ts | 50 +++++++++++++++++++ src/session-broker/brokerServer.ts | 23 ++++++--- src/session/protocol.ts | 2 + 9 files changed, 140 insertions(+), 9 deletions(-) create mode 100644 .changeset/comment-list-author-filter.md diff --git a/.changeset/comment-list-author-filter.md b/.changeset/comment-list-author-filter.md new file mode 100644 index 00000000..0292eb82 --- /dev/null +++ b/.changeset/comment-list-author-filter.md @@ -0,0 +1,5 @@ +--- +"hunkdiff": minor +--- + +Add `--author ` and `--no-author` filters to `hunk session comment list` to narrow inline review comments by author. diff --git a/docs/agent-workflows.md b/docs/agent-workflows.md index e7cf2cc8..87feb49b 100644 --- a/docs/agent-workflows.md +++ b/docs/agent-workflows.md @@ -106,6 +106,8 @@ For comment cleanup and inspection, use: ```bash hunk session comment list --repo . +hunk session comment list --repo . --author pi --json # only comments authored by "pi" +hunk session comment list --repo . --no-author --json # only comments with no author tag hunk session comment rm --repo . hunk session comment clear --repo . --file README.md --yes hunk session comment clear --repo . --all --yes # also clears human `c` notes diff --git a/src/core/cli.test.ts b/src/core/cli.test.ts index 45bdbb41..fecd6a2c 100644 --- a/src/core/cli.test.ts +++ b/src/core/cli.test.ts @@ -715,6 +715,47 @@ describe("parseCli", () => { ).rejects.toThrow("Comment type must be one of live, all, ai, agent, or user."); }); + test("parses session comment list with an author filter", async () => { + const parsed = await parseCli([ + "bun", + "hunk", + "session", + "comment", + "list", + "session-1", + "--author", + "pi", + ]); + + expect(parsed).toEqual({ + kind: "session", + action: "comment-list", + selector: { sessionId: "session-1" }, + author: "pi", + output: "text", + }); + }); + + test("parses session comment list with the no-author filter", async () => { + const parsed = await parseCli([ + "bun", + "hunk", + "session", + "comment", + "list", + "session-1", + "--no-author", + ]); + + expect(parsed).toEqual({ + kind: "session", + action: "comment-list", + selector: { sessionId: "session-1" }, + noAuthor: true, + output: "text", + }); + }); + test("parses session comment rm", async () => { const parsed = await parseCli([ "bun", diff --git a/src/core/cli.ts b/src/core/cli.ts index 043ed265..536b3d90 100644 --- a/src/core/cli.ts +++ b/src/core/cli.ts @@ -902,7 +902,7 @@ async function parseSessionCommand(tokens: string[]): Promise { "Usage:", " hunk session comment add ( | --repo ) --file (--old-line | --new-line ) --summary [--focus]", " hunk session comment apply ( | --repo ) --stdin [--focus]", - " hunk session comment list ( | --repo ) [--file ] [--type ]", + " hunk session comment list ( | --repo ) [--file ] [--type ] [--author ] [--no-author]", " hunk session comment rm ( | --repo ) ", " hunk session comment clear ( | --repo ) [--file ] [--include-user|--all] --yes", ].join("\n") + "\n", @@ -1069,15 +1069,29 @@ async function parseSessionCommand(tokens: string[]): Promise { .option("--repo ", "target the live session whose repo root matches this path") .option("--file ", "filter comments to one diff file") .option("--type ", "filter to live, all, ai, agent, or user comments") + .option("--author ", "filter comments to one author") + .option("--no-author", "filter to comments that have no author") .option("--json", "emit structured JSON"); let parsedSessionId: string | undefined; - let parsedOptions: { repo?: string; file?: string; type?: string; json?: boolean } = {}; + let parsedOptions: { + repo?: string; + file?: string; + type?: string; + author?: string | boolean; + json?: boolean; + } = {}; command.action( ( sessionId: string | undefined, - options: { repo?: string; file?: string; type?: string; json?: boolean }, + options: { + repo?: string; + file?: string; + type?: string; + author?: string | boolean; + json?: boolean; + }, ) => { parsedSessionId = sessionId; parsedOptions = options; @@ -1107,6 +1121,8 @@ async function parseSessionCommand(tokens: string[]): Promise { selector: resolveExplicitSessionSelector(parsedSessionId, parsedOptions.repo), filePath: parsedOptions.file, ...(parsedOptions.type ? { type: parsedOptions.type as SessionCommentListType } : {}), + ...(typeof parsedOptions.author === "string" ? { author: parsedOptions.author } : {}), + ...(parsedOptions.author === false ? { noAuthor: true } : {}), }; } diff --git a/src/core/types.ts b/src/core/types.ts index 4c6c5c95..af0d2dfc 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -266,6 +266,8 @@ export interface SessionCommentListCommandInput { selector: SessionSelectorInput; filePath?: string; type?: SessionCommentListType; + author?: string; + noAuthor?: boolean; } export interface SessionCommentRemoveCommandInput { diff --git a/src/hunk-session/cli.ts b/src/hunk-session/cli.ts index 36e29806..29f7f182 100644 --- a/src/hunk-session/cli.ts +++ b/src/hunk-session/cli.ts @@ -183,6 +183,8 @@ class HttpHunkSessionCliClient implements HunkSessionCliClient { selector: input.selector, filePath: input.filePath, type: input.type, + author: input.author, + noAuthor: input.noAuthor, }, ) ).comments; diff --git a/src/session-broker/brokerServer.helpers.test.ts b/src/session-broker/brokerServer.helpers.test.ts index 73dc8b71..368245ad 100644 --- a/src/session-broker/brokerServer.helpers.test.ts +++ b/src/session-broker/brokerServer.helpers.test.ts @@ -314,6 +314,56 @@ describe("handleSessionApiRequest", () => { expect(await response.json()).toHaveProperty("comments"); }); + test("filters the comment-list to one author with --author", async () => { + const comments = [ + { commentId: "c1", author: "pi" }, + { commentId: "c2", author: "claude" }, + { commentId: "c3" }, + ]; + const { state } = createFakeState({ listComments: () => comments }); + const response = await handleSessionApiRequest( + state, + apiRequest({ + action: "comment-list", + selector: { sessionId: "s-1" }, + author: "pi", + } as SessionDaemonRequest), + ); + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ comments: [{ commentId: "c1", author: "pi" }] }); + }); + + test("filters the comment-list to author-less comments with --no-author", async () => { + const comments = [{ commentId: "c1", author: "pi" }, { commentId: "c2" }, { commentId: "c3" }]; + const { state } = createFakeState({ listComments: () => comments }); + const response = await handleSessionApiRequest( + state, + apiRequest({ + action: "comment-list", + selector: { sessionId: "s-1" }, + noAuthor: true, + } as SessionDaemonRequest), + ); + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + comments: [{ commentId: "c2" }, { commentId: "c3" }], + }); + }); + + test("returns every comment when no author filter is set", async () => { + const comments = [{ commentId: "c1", author: "pi" }, { commentId: "c2" }]; + const { state } = createFakeState({ listComments: () => comments }); + const response = await handleSessionApiRequest( + state, + apiRequest({ + action: "comment-list", + selector: { sessionId: "s-1" }, + } as SessionDaemonRequest), + ); + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ comments }); + }); + test("returns 400 when a dispatched command rejects", async () => { const { state } = createFakeState({ dispatchCommand: () => { diff --git a/src/session-broker/brokerServer.ts b/src/session-broker/brokerServer.ts index 869a7e8d..85cd30ac 100644 --- a/src/session-broker/brokerServer.ts +++ b/src/session-broker/brokerServer.ts @@ -330,19 +330,30 @@ export async function handleSessionApiRequest(state: HunkSessionBrokerState, req }), }; break; - case "comment-list": + case "comment-list": { + const matchesAuthor = (items: T[]): T[] => + input.author !== undefined + ? items.filter((comment) => comment.author === input.author) + : input.noAuthor + ? items.filter((comment) => !comment.author) + : items; response = input.type && input.type !== "live" ? { - comments: listHunkSessionNotes(state.getSession(input.selector), { - filePath: input.filePath, - source: input.type === "all" ? undefined : input.type, - }), + comments: matchesAuthor( + listHunkSessionNotes(state.getSession(input.selector), { + filePath: input.filePath, + source: input.type === "all" ? undefined : input.type, + }), + ), } : { - comments: state.listComments(input.selector, { filePath: input.filePath }), + comments: matchesAuthor( + state.listComments(input.selector, { filePath: input.filePath }), + ), }; break; + } case "comment-rm": response = { result: await state.dispatchCommand({ diff --git a/src/session/protocol.ts b/src/session/protocol.ts index 6f1a5aea..ae9a5ac9 100644 --- a/src/session/protocol.ts +++ b/src/session/protocol.ts @@ -107,6 +107,8 @@ export type SessionDaemonRequest = selector: SessionCommentListCommandInput["selector"]; filePath?: string; type?: SessionCommentListCommandInput["type"]; + author?: SessionCommentListCommandInput["author"]; + noAuthor?: SessionCommentListCommandInput["noAuthor"]; } | { action: "comment-rm";