Add inline review comments to git diff pane (#530)

* Add inline git diff review comments

Adds a review draft store keyed per diff target, inline gutter + thread
rendering in the diff pane, and a generated review attachment that flows
through the composer and agent providers.

* Strip generated review attachments before saving to draft store

Drafts persist only UserComposerAttachment (no review kind), so filter the
auto-generated review attachment out when seeding the draft or restoring
after a failed auto-submit.

* Fix inline review typecheck and test harness drift

* Unslop inline review feature

- Fix stale-closure bug in composer queueMessage: clear drafts for the
  actually-queued attachments, not the latest props
- Drop dead useEffect mirroring initialBody in inline review editor
  (parent already remounts via key)
- Restore typed AgentAttachment signature on renderPromptAttachmentAsText;
  delete schema redispatch helper and drop null-checks across 5 providers
- Inline single-use review prompt formatters and replace marker switch
  with const map
- Drop normalizeComment field-copy redundancy after type guard
- Remove __reviewDraftStoreTestUtils export; test migration via
  rehydrate() public path
- Refactor findTarget to return the target line directly (no non-null
  assertion at call site)
- Drop redundant isGeneratedReviewAttachment helper; inline kind check

* Update review pill to withUnistyles after rebase

Main's unistyles refactor stripped useUnistyles in favor of withUnistyles +
StyleSheet. Convert ReviewAttachmentPill to use ThemedCircleDot + ICON_SIZE so
the icon picks up the foreground-muted mapping like the other pills.

* Fix lint and typecheck after rebase onto main

Resolve drift after main's withUnistyles refactor:
- Composer complexity: dedup voice?.isVoiceSwitching to drop to 20
- Move per-comment Pressable handlers in git-diff-inline-review into
  CommentRow subcomponent so callbacks are stable
- Hoist style/onPress factories to module level (iconButton,
  iconButtonDestructive, ghostButton)
- Memoize style arrays in git-diff-pane review wrappers
- Replace inline {} placeholders with useMemo styles
- review-draft-store: extract applyCommentUpdates to avoid map-spread,
  include input.key in useMemo deps
- Test fixtures: hoist EMPTY_COMMENTS, add buildReviewActions helper

* Drop GitDiffPane default param to keep complexity at 20

After the rebase, main's `enabled = true` default param combined with the
inline review additions pushed cyclomatic complexity to 21. Drop the
default and resolve `enabled !== false` at the call site (comparison
doesn't count toward complexity).

* Reshape inline review behind @/review module

Drop the `generated: true` attachment flag and consolidate inline-review
logic behind a deep `@/review` module so composer and git-diff-pane stop
hand-composing review primitives. Workspace-scoped review snapshots are
now derived at submit/queue time rather than persisted in the draft
store. Composer review references drop from 51 to 4; git-diff-pane drops
~108 lines and now consumes a controller hook + slot components.

* Fix optimistic review attachments
This commit is contained in:
Mohamed Boudra
2026-04-30 21:35:58 +08:00
committed by GitHub
parent 4bcf98a9e3
commit 92afb826d9
39 changed files with 4244 additions and 317 deletions
@@ -7,6 +7,148 @@ import {
} from "./messages.js";
describe("shared messages attachments", () => {
it("keeps valid review attachments", () => {
const parsed = SendAgentMessageRequestSchema.parse({
type: "send_agent_message_request",
requestId: "req-review",
agentId: "agent-1",
text: "Please address these comments",
attachments: [
{
type: "review",
mimeType: "application/paseo-review",
cwd: "/tmp/repo",
mode: "base",
baseRef: "main",
comments: [
{
filePath: "src/index.ts",
side: "new",
lineNumber: 42,
body: "Please guard this nullable value.",
context: {
hunkHeader: "@@ -40,3 +40,4 @@",
targetLine: {
oldLineNumber: null,
newLineNumber: 42,
type: "add",
content: "const value = maybeNull.name;",
},
lines: [
{
oldLineNumber: 41,
newLineNumber: 41,
type: "context",
content: "const before = true;",
},
{
oldLineNumber: null,
newLineNumber: 42,
type: "add",
content: "const value = maybeNull.name;",
},
],
},
},
],
},
],
});
expect(parsed.attachments).toEqual([
{
type: "review",
mimeType: "application/paseo-review",
cwd: "/tmp/repo",
mode: "base",
baseRef: "main",
comments: [
{
filePath: "src/index.ts",
side: "new",
lineNumber: 42,
body: "Please guard this nullable value.",
context: {
hunkHeader: "@@ -40,3 +40,4 @@",
targetLine: {
oldLineNumber: null,
newLineNumber: 42,
type: "add",
content: "const value = maybeNull.name;",
},
lines: [
{
oldLineNumber: 41,
newLineNumber: 41,
type: "context",
content: "const before = true;",
},
{
oldLineNumber: null,
newLineNumber: 42,
type: "add",
content: "const value = maybeNull.name;",
},
],
},
},
],
},
]);
});
it("drops malformed review attachments while keeping valid attachments", () => {
const parsed = SendAgentMessageRequestSchema.parse({
type: "send_agent_message_request",
requestId: "req-bad-review",
agentId: "agent-1",
text: "Review",
attachments: [
{
type: "review",
mimeType: "application/paseo-review",
cwd: "/tmp/repo",
mode: "uncommitted",
comments: [
{
filePath: "src/index.ts",
side: "new",
lineNumber: "42",
body: "This line number is malformed.",
context: {
hunkHeader: "@@ -40,3 +40,4 @@",
targetLine: {
oldLineNumber: null,
newLineNumber: 42,
type: "add",
content: "const value = maybeNull.name;",
},
lines: [],
},
},
],
},
{
type: "github_issue",
mimeType: "application/github-issue",
number: 55,
title: "Improve startup error details",
url: "https://github.com/getpaseo/paseo/issues/55",
},
],
});
expect(parsed.attachments).toEqual([
{
type: "github_issue",
mimeType: "application/github-issue",
number: 55,
title: "Improve startup error details",
url: "https://github.com/getpaseo/paseo/issues/55",
},
]);
});
it("keeps known attachments and drops unknown create-agent attachments", () => {
const parsed = CreateAgentRequestMessageSchema.parse({
type: "create_agent_request",
+30
View File
@@ -733,9 +733,38 @@ export const GitHubIssueAttachmentSchema = z.object({
body: z.string().nullable().optional(),
});
export const ReviewAttachmentContextLineSchema = z.object({
oldLineNumber: z.number().int().positive().nullable(),
newLineNumber: z.number().int().positive().nullable(),
type: z.enum(["add", "remove", "context"]),
content: z.string(),
});
export const ReviewAttachmentCommentSchema = z.object({
filePath: z.string(),
side: z.enum(["old", "new"]),
lineNumber: z.number().int().positive(),
body: z.string(),
context: z.object({
hunkHeader: z.string(),
targetLine: ReviewAttachmentContextLineSchema,
lines: z.array(ReviewAttachmentContextLineSchema),
}),
});
export const ReviewAttachmentSchema = z.object({
type: z.literal("review"),
mimeType: z.literal("application/paseo-review"),
cwd: z.string(),
mode: z.enum(["uncommitted", "base"]),
baseRef: z.string().nullable().optional(),
comments: z.array(ReviewAttachmentCommentSchema),
});
export const AgentAttachmentSchema = z.discriminatedUnion("type", [
GitHubPrAttachmentSchema,
GitHubIssueAttachmentSchema,
ReviewAttachmentSchema,
]);
function normalizeAgentAttachments(input: unknown): AgentAttachment[] {
@@ -3471,6 +3500,7 @@ export type DictationStreamFinishMessage = z.infer<typeof DictationStreamFinishM
export type DictationStreamCancelMessage = z.infer<typeof DictationStreamCancelMessageSchema>;
export type CreateAgentRequestMessage = z.infer<typeof CreateAgentRequestMessageSchema>;
export type AgentAttachment = z.infer<typeof AgentAttachmentSchema>;
export type ReviewAttachment = z.infer<typeof ReviewAttachmentSchema>;
export type ListProviderModelsRequestMessage = z.infer<
typeof ListProviderModelsRequestMessageSchema
>;