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
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { ComposerAttachment } from "@/attachments/types";
import type { UserComposerAttachment } from "@/attachments/types";
import type { DraftAgentStatusBarProps } from "@/components/agent-status-bar";
import type { DraftCommandConfig } from "@/hooks/use-agent-commands-query";
import {
@@ -13,8 +13,8 @@ import type { AgentModelDefinition } from "@server/server/agent/agent-sdk-types"
import type { AgentProvider } from "@server/server/agent/agent-sdk-types";
type AttachmentUpdater =
| ComposerAttachment[]
| ((prev: ComposerAttachment[]) => ComposerAttachment[]);
| UserComposerAttachment[]
| ((prev: UserComposerAttachment[]) => UserComposerAttachment[]);
interface AgentInputDraftComposerOptions {
initialServerId: string | null;
@@ -48,7 +48,7 @@ type DraftComposerState = UseAgentFormStateResult & {
interface AgentInputDraft {
text: string;
setText: (text: string) => void;
attachments: ComposerAttachment[];
attachments: UserComposerAttachment[];
setAttachments: (updater: AttachmentUpdater) => void;
cwd: string;
setCwd: (cwd: string) => void;
@@ -59,7 +59,7 @@ interface AgentInputDraft {
function hasDraftContent(input: {
text: string;
attachments: ComposerAttachment[];
attachments: UserComposerAttachment[];
cwd: string;
}): boolean {
return (
@@ -68,8 +68,8 @@ function hasDraftContent(input: {
}
function areAttachmentsEqual(input: {
left: ComposerAttachment[];
right: ComposerAttachment[];
left: UserComposerAttachment[];
right: UserComposerAttachment[];
}): boolean {
if (input.left.length !== input.right.length) {
return false;
@@ -190,7 +190,7 @@ export function useAgentInputDraft(input: UseAgentInputDraftInput): AgentInputDr
[formState.selectedServerId, input.draftKey],
);
const [text, setText] = useState("");
const [attachments, setAttachmentsState] = useState<ComposerAttachment[]>([]);
const [attachments, setAttachmentsState] = useState<UserComposerAttachment[]>([]);
const [cwd, setCwd] = useState(input.initialCwd ?? "");
const [isHydrated, setIsHydrated] = useState(false);
const draftGenerationRef = useRef(0);
@@ -16,6 +16,7 @@ interface CreateAttempt {
text: string;
timestamp: Date;
images?: UserMessageImageAttachment[];
attachments?: AgentAttachment[];
}
type DraftAgentMachineState =
@@ -118,7 +119,11 @@ export function useDraftAgentCreateFlow<TDraftAgent, TCreateResult>({
return EMPTY_STREAM_ITEMS;
}
if (!machine.attempt.text && (!machine.attempt.images || machine.attempt.images.length === 0)) {
if (
!machine.attempt.text &&
(!machine.attempt.images || machine.attempt.images.length === 0) &&
(!machine.attempt.attachments || machine.attempt.attachments.length === 0)
) {
return EMPTY_STREAM_ITEMS;
}
@@ -131,6 +136,9 @@ export function useDraftAgentCreateFlow<TDraftAgent, TCreateResult>({
...(machine.attempt.images && machine.attempt.images.length > 0
? { images: machine.attempt.images }
: {}),
...(machine.attempt.attachments && machine.attempt.attachments.length > 0
? { attachments: machine.attempt.attachments }
: {}),
},
];
}, [machine]);
@@ -182,6 +190,7 @@ export function useDraftAgentCreateFlow<TDraftAgent, TCreateResult>({
text: trimmedPrompt,
timestamp: new Date(),
...(images && images.length > 0 ? { images } : {}),
...(wirePayload.attachments.length > 0 ? { attachments: wirePayload.attachments } : {}),
};
setPendingCreateAttempt({
@@ -192,6 +201,9 @@ export function useDraftAgentCreateFlow<TDraftAgent, TCreateResult>({
text: attempt.text,
timestamp: attempt.timestamp.getTime(),
...(attempt.images && attempt.images.length > 0 ? { images: attempt.images } : {}),
...(attempt.attachments && attempt.attachments.length > 0
? { attachments: attempt.attachments }
: {}),
});
onBeforeSubmit?.({