Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/comment-list-author-filter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": minor
---

Add `--author <name>` and `--no-author` filters to `hunk session comment list` to narrow inline review comments by author.
2 changes: 2 additions & 0 deletions docs/agent-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 . <comment-id>
hunk session comment clear --repo . --file README.md --yes
hunk session comment clear --repo . --all --yes # also clears human `c` notes
Expand Down
41 changes: 41 additions & 0 deletions src/core/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
22 changes: 19 additions & 3 deletions src/core/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -902,7 +902,7 @@ async function parseSessionCommand(tokens: string[]): Promise<ParsedCliInput> {
"Usage:",
" hunk session comment add (<session-id> | --repo <path>) --file <path> (--old-line <n> | --new-line <n>) --summary <text> [--focus]",
" hunk session comment apply (<session-id> | --repo <path>) --stdin [--focus]",
" hunk session comment list (<session-id> | --repo <path>) [--file <path>] [--type <live|all|ai|agent|user>]",
" hunk session comment list (<session-id> | --repo <path>) [--file <path>] [--type <live|all|ai|agent|user>] [--author <name>] [--no-author]",
" hunk session comment rm (<session-id> | --repo <path>) <comment-id>",
" hunk session comment clear (<session-id> | --repo <path>) [--file <path>] [--include-user|--all] --yes",
].join("\n") + "\n",
Expand Down Expand Up @@ -1069,15 +1069,29 @@ async function parseSessionCommand(tokens: string[]): Promise<ParsedCliInput> {
.option("--repo <path>", "target the live session whose repo root matches this path")
.option("--file <path>", "filter comments to one diff file")
.option("--type <type>", "filter to live, all, ai, agent, or user comments")
.option("--author <name>", "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;
Expand Down Expand Up @@ -1107,6 +1121,8 @@ async function parseSessionCommand(tokens: string[]): Promise<ParsedCliInput> {
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 } : {}),
};
}

Expand Down
2 changes: 2 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ export interface SessionCommentListCommandInput {
selector: SessionSelectorInput;
filePath?: string;
type?: SessionCommentListType;
author?: string;
noAuthor?: boolean;
}

export interface SessionCommentRemoveCommandInput {
Expand Down
2 changes: 2 additions & 0 deletions src/hunk-session/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ class HttpHunkSessionCliClient implements HunkSessionCliClient {
selector: input.selector,
filePath: input.filePath,
type: input.type,
author: input.author,
noAuthor: input.noAuthor,
},
)
).comments;
Expand Down
50 changes: 50 additions & 0 deletions src/session-broker/brokerServer.helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => {
Expand Down
23 changes: 17 additions & 6 deletions src/session-broker/brokerServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,19 +330,30 @@ export async function handleSessionApiRequest(state: HunkSessionBrokerState, req
}),
};
break;
case "comment-list":
case "comment-list": {
const matchesAuthor = <T extends { author?: string }>(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<RemovedCommentResult, "remove_comment">({
Expand Down
2 changes: 2 additions & 0 deletions src/session/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ export type SessionDaemonRequest =
selector: SessionCommentListCommandInput["selector"];
filePath?: string;
type?: SessionCommentListCommandInput["type"];
author?: SessionCommentListCommandInput["author"];
noAuthor?: SessionCommentListCommandInput["noAuthor"];
}
| {
action: "comment-rm";
Expand Down