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
5 changes: 5 additions & 0 deletions .changeset/pr-comment-reply-reactions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Show GitHub review-thread reactions and timestamps for every comment reply in Agent Manager.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion packages/kilo-vscode/src/agent-manager/pr-status-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function reactionRequest(m: Record<string, unknown>): ReactionRequest | undefine

function hasComment(pr: PRStatus, id: string): boolean {
return (
pr.comments?.comments.some((comment) => comment.id === id) ||
pr.comments?.comments.some((comment) => comment.id === id || comment.replies?.some((reply) => reply.id === id)) ||
pr.conversation?.some((comment) => comment.id === id) ||
false
)
Expand Down
16 changes: 13 additions & 3 deletions packages/kilo-vscode/src/agent-manager/pr/am-pr-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
CheckStatus,
PRCheck,
PRComment,
PRCommentReply,
PRConversationComment,
PRReaction,
PRReactionContent,
Expand Down Expand Up @@ -195,12 +196,21 @@ function location(
}
}

function parseReplies(nodes: GhComment[]): PRComment["replies"] {
const list = nodes.slice(1).map((node) => ({
function parseReply(node: GhComment): PRCommentReply {
const reactions = parseReactions(node.reactionGroups)
return {
id: node.id,
author: node.author?.login ?? "unknown",
body: node.body ?? "",
...(node.author?.avatarUrl ? { avatar: node.author.avatarUrl } : {}),
}))
...(node.createdAt ? { createdAt: new Date(node.createdAt).getTime() } : {}),
...(node.url ? { url: node.url } : {}),
...(reactions.length > 0 ? { reactions } : {}),
}
}

function parseReplies(nodes: GhComment[]): PRComment["replies"] {
const list = nodes.slice(1).map(parseReply)
return list.length > 0 ? list : undefined
}

Expand Down
34 changes: 29 additions & 5 deletions packages/kilo-vscode/tests/fixtures/pr-comments-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,15 @@ const [comments, setComments] = createSignal({
diffHunk: HUNK,
// Read from the worktree by the extension: a hunk stops at the commented line.
after: [" return <View {...options} />", "}", ""],
replies: [{ author: "marius", body: "reply body is visible" }],
replies: [
{
id: "PRRC_reply",
author: "marius",
body: "reply body is visible",
createdAt: Date.now() - 2 * 60 * 1000,
reactions: [{ content: "ROCKET", count: 1, viewerHasReacted: false }],
},
],
reactions: [
{ content: "THUMBS_UP", count: 2, viewerHasReacted: false },
{ content: "HEART", count: 1, viewerHasReacted: true },
Expand Down Expand Up @@ -151,7 +159,7 @@ const comment = shadow?.querySelector('[data-content] span[style*="--syntax-comm
const code = shadow?.querySelectorAll("[data-content] [data-line]")
assert.match(root.textContent ?? "", /comment body survives Pierre rendering/)
assert.match(root.textContent ?? "", /reply body is visible/)
const reactionButtons = [...root.querySelectorAll<HTMLButtonElement>(".am-pr-reaction")]
const reactionButtons = [...root.querySelectorAll<HTMLButtonElement>('[data-comment-id="PRRC_open"] .am-pr-reaction')]
const addReaction = reactionButtons.at(0)
const removeReaction = reactionButtons.at(1)
assert.ok(addReaction, "add reaction control is rendered")
Expand All @@ -176,12 +184,12 @@ assert.equal(addReaction!.getAttribute("aria-busy"), "true")
assert.equal(removeReaction!.querySelector('[data-component="spinner"]'), null)
assert.equal(removeReaction!.getAttribute("aria-busy"), "false")
assert.ok(pickerTrigger(), "the picker trigger stays in place while an update runs")
const reactionResult = (reaction: string, add: boolean, success: boolean) => {
const reactionResult = (reaction: string, add: boolean, success: boolean, id = "PRRC_open") => {
post(
{
type: "agentManager.commentReactionResult",
worktreeId: "wt-test",
commentId: "PRRC_open",
commentId: id,
reaction,
add,
success,
Expand Down Expand Up @@ -247,6 +255,22 @@ assert.deepEqual(reactions[4], {
reaction: "HEART",
add: false,
})
const replyReaction = root.querySelector<HTMLButtonElement>('[data-comment-id="PRRC_reply"] .am-pr-reaction')
assert.ok(replyReaction, "reply reaction control is rendered")
assert.equal(reactionCount(replyReaction!), "1")
replyReaction!.click()
await window.happyDOM.waitUntilComplete()
assert.deepEqual(reactions[5], {
type: "agentManager.commentReaction",
projectId: undefined,
worktreeId: "wt-test",
commentId: "PRRC_reply",
reaction: "ROCKET",
add: true,
})
reactionResult("ROCKET", true, true, "PRRC_reply")
await window.happyDOM.waitUntilComplete()
assert.ok(root.querySelector(".am-pr-comment-reply .am-pr-comment-time"))
assert.equal(root.querySelector('[data-thread-id="PRRT_open"] .am-pr-comment-time')?.textContent, "5 min ago")
assert.equal(root.querySelector('[data-thread-id="PRRT_done"] .am-pr-comment-time'), null)
assert.equal(root.querySelectorAll('[data-component="diff"]').length, 1)
Expand Down Expand Up @@ -475,7 +499,7 @@ const inlineReaction = first.host.querySelector<HTMLButtonElement>(".am-pr-react
assert.ok(inlineReaction, "inline reaction control is rendered")
inlineReaction!.click()
await window.happyDOM.waitUntilComplete()
assert.deepEqual(reactions.at(5), {
assert.deepEqual(reactions.at(6), {
type: "agentManager.commentReaction",
projectId: undefined,
worktreeId: "wt-test",
Expand Down
46 changes: 46 additions & 0 deletions packages/kilo-vscode/tests/unit/am-pr-status-bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,52 @@ describe("PRStatusBridge.handleMessage commentReaction", () => {
)
})

it("adds a reaction to a cached thread reply and reports success", async () => {
const { bridge, sent, onStatus } = harness()
onStatus("wt1", {
...pr,
comments: {
total: 1,
unresolved: 1,
comments: [
{
id: "PRRC_1",
threadId: "PRRT_1",
author: "alice",
body: "note",
resolved: false,
outdated: false,
replies: [{ id: "PRRC_2", author: "bob", body: "reply" }],
},
],
},
})
addCommentReaction.mockResolvedValueOnce(undefined)

expect(
bridge.handleMessage({
type: "agentManager.commentReaction",
worktreeId: "wt1",
commentId: "PRRC_2",
reaction: "HEART",
add: true,
}),
).toBe(true)
await Promise.resolve()

expect(addCommentReaction).toHaveBeenCalledWith("PRRC_2", "HEART", "/repo/wt1")
expect(sent).toContainEqual(
expect.objectContaining({
type: "agentManager.commentReactionResult",
worktreeId: "wt1",
commentId: "PRRC_2",
reaction: "HEART",
add: true,
success: true,
}),
)
})

it("removes a reaction and reports a failure", async () => {
const { bridge, sent, onStatus } = harness()
onStatus("wt1", {
Expand Down
22 changes: 19 additions & 3 deletions packages/kilo-vscode/tests/unit/am-pr-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,15 +455,31 @@ describe("parseComments", () => {
comments: {
nodes: [
{ id: "first", body: "first comment", author: { login: "alice" } },
{ id: "second", body: "second comment", author: { login: "bob" } },
{
id: "second",
body: "second comment",
author: { login: "bob" },
createdAt: "2024-01-02T00:00:00Z",
url: "https://github.com/example/repo/pull/1#discussion_r2",
reactionGroups: [{ content: "HEART", reactors: { totalCount: 2 }, viewerHasReacted: true }],
},
],
},
},
]
const result = parseComments(threads)
expect(result).toHaveLength(1)
expect(result[0]?.id).toBe("first")
expect(result[0]?.replies).toEqual([{ author: "bob", body: "second comment" }])
expect(result[0]?.replies).toEqual([
{
id: "second",
author: "bob",
body: "second comment",
createdAt: new Date("2024-01-02T00:00:00Z").getTime(),
url: "https://github.com/example/repo/pull/1#discussion_r2",
reactions: [{ content: "HEART", count: 2, viewerHasReacted: true }],
},
])
})

it("marks an outdated thread", () => {
Expand Down Expand Up @@ -549,7 +565,7 @@ describe("parseComments", () => {
line: 12,
originalLine: 9,
startLine: 10,
replies: [{ author: "bob", body: "reply", avatar: "https://avatar/bob" }],
replies: [{ id: "left-reply", author: "bob", body: "reply", avatar: "https://avatar/bob" }],
}),
)
expect(result[1]).toEqual(
Expand Down
35 changes: 27 additions & 8 deletions packages/kilo-vscode/webview-ui/agent-manager/pr/PRCommentCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ interface Props {
reactions?: PRReaction[]
reactionPending?: (content: PRReactionContent) => boolean
onReaction?: (content: PRReactionContent, add: boolean) => void
replyReactionError?: (id: string) => string | undefined
replyReactions?: (id: string, reactions?: PRReaction[]) => PRReaction[]
replyReactionPending?: (id: string, content: PRReactionContent) => boolean
onReplyReaction?: (id: string, content: PRReactionContent, add: boolean) => void
onToggleOpen: () => void
onToggleResolved?: () => void
onSend: () => void
Expand Down Expand Up @@ -126,22 +130,44 @@ export function PRCommentCard(props: Props) {
<Content>
<div class="am-pr-comment-body">
<Markdown text={props.comment.body} />
<Show when={props.onReaction}>
<div class="am-pr-comment-reactions" data-comment-id={props.comment.id}>
<PRReactions
reactions={props.reactions ?? props.comment.reactions}
pending={props.reactionPending}
onToggle={(content, add) => props.onReaction?.(content, add)}
/>
<Show when={props.reactionError}>{(err) => <div class="am-pr-comment-error">{err()}</div>}</Show>
</div>
</Show>
</div>
<For each={props.comment.replies}>
{(reply) => (
<div class="am-pr-comment-reply">
<div class="am-pr-comment-reply-head am-pr-row">
<PRAvatar avatar={reply.avatar} author={reply.author} />
<span class="am-pr-comment-author">{reply.author}</span>
<PRCommentTime time={reply.createdAt} />
</div>
<div class="am-pr-comment-body">
<Markdown text={reply.body} />
</div>
<Show when={reply.id && props.onReplyReaction}>
<div class="am-pr-comment-reactions" data-comment-id={reply.id}>
<PRReactions
reactions={props.replyReactions?.(reply.id!, reply.reactions) ?? reply.reactions}
pending={(content) => props.replyReactionPending?.(reply.id!, content) ?? false}
onToggle={(content, add) => props.onReplyReaction?.(reply.id!, content, add)}
/>
<Show when={props.replyReactionError?.(reply.id!)}>
{(err) => <div class="am-pr-comment-error">{err()}</div>}
</Show>
</div>
</Show>
</div>
)}
</For>
<Show when={props.error}>{(err) => <div class="am-pr-comment-error">{err()}</div>}</Show>
<Show when={props.reactionError}>{(err) => <div class="am-pr-comment-error">{err()}</div>}</Show>
<div class="am-pr-comment-actions am-pr-row">
<Button variant="primary" size="small" disabled={props.sent} onClick={props.onSend}>
{t("agentManager.pr.fixWithKilo")}
Expand All @@ -160,13 +186,6 @@ export function PRCommentCard(props: Props) {
{props.resolved ? t("agentManager.pr.comment.unresolve") : t("agentManager.pr.comment.resolve")}
</Button>
</Show>
<Show when={props.onReaction}>
<PRReactions
reactions={props.reactions ?? props.comment.reactions}
pending={props.reactionPending}
onToggle={(content, add) => props.onReaction?.(content, add)}
/>
</Show>
<span class="am-pr-comment-actions-gap" />
<CopyButton text={prMarkdown(props.comment)} label={t("agentManager.pr.comment.copy")} />
<Show when={props.onOpenDiff}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ export function PRComments(props: Props) {
reactions={reactions.list(comment().id, comment().reactions)}
reactionPending={(content) => reactions.pending(comment().id, content)}
onReaction={(content, add) => reactions.toggle(comment().id, content, add)}
replyReactionError={(id) => reactions.error(id)}
replyReactions={(id, values) => reactions.list(id, values)}
replyReactionPending={(id, content) => reactions.pending(id, content)}
onReplyReaction={(id, content, add) => reactions.toggle(id, content, add)}
onToggleOpen={() => {
const next = !expandedFor(comment())
patch((prev) => ({ expanded: { ...prev.expanded, [id]: next } }))
Expand Down
4 changes: 4 additions & 0 deletions packages/kilo-vscode/webview-ui/agent-manager/pr/pr-panel.css
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,10 @@
padding-left: 0;
}

.am-pr-comment-reactions {
padding-top: 4px;
}

.am-pr-comment [data-component="avatar"] {
border-radius: 50%;
}
Expand Down
4 changes: 4 additions & 0 deletions packages/kilo-vscode/webview-ui/agent-manager/pr/pr-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,13 @@ export interface PRCheck {
}

export interface PRCommentReply {
id?: string
author: string
body: string
avatar?: string
createdAt?: number
url?: string
reactions?: PRReaction[]
}

export interface PRComment {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ export function createRemoteCommentController(options: Options): RemoteCommentCo

const RemoteCard: Component<CardProps> = (props) => {
const comment = () => props.item.comment()
const ctrl = options.reactions?.enabled() ? options.reactions : undefined
return (
<PRCommentCard
comment={comment()}
Expand Down Expand Up @@ -201,16 +202,14 @@ export function createRemoteCommentController(options: Options): RemoteCommentCo
? () => options.onOpenUrl?.(githubUrl(comment().url)!)
: undefined
}
reactionError={options.reactions?.enabled() ? options.reactions.error(comment().id) : undefined}
reactions={options.reactions?.enabled() ? options.reactions.list(comment().id, comment().reactions) : undefined}
reactionPending={
options.reactions?.enabled() ? (content) => options.reactions!.pending(comment().id, content) : undefined
}
onReaction={
options.reactions?.enabled()
? (content, add) => options.reactions?.toggle(comment().id, content, add)
: undefined
}
reactionError={ctrl?.error(comment().id)}
reactions={ctrl?.list(comment().id, comment().reactions)}
reactionPending={ctrl ? (content) => ctrl.pending(comment().id, content) : undefined}
onReaction={ctrl ? (content, add) => ctrl.toggle(comment().id, content, add) : undefined}
replyReactionError={ctrl?.error}
replyReactions={ctrl?.list}
replyReactionPending={ctrl?.pending}
onReplyReaction={ctrl?.toggle}
/>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2009,6 +2009,7 @@ const prComments: NonNullable<PRStatus["comments"]> = {
diffHunk:
'@@ -39,7 +39,7 @@ export function execGhRead(args: string[]) {\n- return execWithShellEnv("gh", args, options)\n+ return execWithShellEnv("gh", args, { ...options, env: env(options) })',
side: "additions",
reactions: [{ content: "THUMBS_UP", count: 2, viewerHasReacted: false }],
preview: {
patch:
'@@ -40,6 +40,6 @@\n const options = { timeout: 5000 }\n const result = await\n- execWithShellEnv("gh", args, options)\n+ execWithShellEnv("gh", args, { ...options, env: env(options) })\n return result\n }\n ',
Expand All @@ -2019,7 +2020,15 @@ const prComments: NonNullable<PRStatus["comments"]> = {
top: true,
bottom: true,
},
replies: [{ author: "hubot", body: "Agreed. A guard plus a log line is enough here." }],
replies: [
{
id: "PRRC_1_REPLY",
author: "hubot",
body: "Agreed. A guard plus a log line is enough here.",
createdAt: Date.now() - 4 * 60 * 1000,
reactions: [{ content: "HEART", count: 1, viewerHasReacted: false }],
},
],
},
{
id: "PRRC_2",
Expand Down
Loading