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
@@ -109,6 +109,7 @@ vi.mock("react-native-unistyles", () => {
create: createPanelTestStyles,
},
withUnistyles: <T,>(component: T) => component,
useUnistyles: () => ({ theme, rt: { breakpoint: "lg" } }),
};
});
+45 -1
View File
@@ -14,6 +14,12 @@ import { FileDropZone } from "@/components/file-drop-zone";
import type { ImageAttachment } from "@/components/message-input";
import { getProviderIcon } from "@/components/provider-icons";
import { ToastViewport, useToastHost } from "@/components/toast-host";
import type { WorkspaceComposerAttachment } from "@/attachments/types";
import {
useWorkspaceAttachments,
useWorkspaceAttachmentScopeKey,
} from "@/attachments/workspace-attachments-store";
import { useIsCompactFormFactor } from "@/constants/layout";
import { isNative, isWeb } from "@/constants/platform";
import { useAgentAttentionClear } from "@/hooks/use-agent-attention-clear";
import { useAgentInitialization } from "@/hooks/use-agent-initialization";
@@ -43,6 +49,7 @@ import {
} from "@/screens/agent/agent-ready-screen-bottom-anchor";
import { useCreateFlowStore } from "@/stores/create-flow-store";
import { buildDraftStoreKey } from "@/stores/draft-keys";
import { usePanelStore } from "@/stores/panel-store";
import { type Agent, useSessionStore } from "@/stores/session-store";
import type { Theme } from "@/styles/theme";
import type { PendingPermission } from "@/types/shared";
@@ -1071,6 +1078,9 @@ function AgentStreamSection({
...(pendingCreate.images && pendingCreate.images.length > 0
? { images: pendingCreate.images }
: {}),
...(pendingCreate.attachments && pendingCreate.attachments.length > 0
? { attachments: pendingCreate.attachments }
: {}),
},
];
}, [pendingCreate, shouldUseOptimisticStream]);
@@ -1101,7 +1111,10 @@ function AgentStreamSection({
}
const pendingImages = pendingCreate.images;
if (agentId && pendingImages && pendingImages.length > 0) {
const pendingAttachments = pendingCreate.attachments;
const hasPendingImages = Boolean(pendingImages && pendingImages.length > 0);
const hasPendingAttachments = Boolean(pendingAttachments && pendingAttachments.length > 0);
if (agentId && (hasPendingImages || hasPendingAttachments)) {
setAgentStreamTail(serverId, (previous) => {
const current = previous.get(agentId);
if (!current) {
@@ -1112,6 +1125,7 @@ function AgentStreamSection({
streamItems: current,
clientMessageId: pendingCreate.clientMessageId,
images: pendingImages,
attachments: pendingAttachments,
});
if (merged === current) {
return previous;
@@ -1231,6 +1245,8 @@ function ActiveAgentComposer({
onMessageSent: () => void;
}) {
const insets = useSafeAreaInsets();
const isCompact = useIsCompactFormFactor();
const { workspaceId } = usePaneContext();
const agentInputDraft = useAgentInputDraft({
draftKey: buildDraftStoreKey({
serverId,
@@ -1238,6 +1254,32 @@ function ActiveAgentComposer({
}),
initialCwd,
});
const workspaceAttachmentScopeKey = useWorkspaceAttachmentScopeKey({
serverId,
cwd: agentInputDraft.cwd,
workspaceId,
});
const workspaceAttachments = useWorkspaceAttachments(workspaceAttachmentScopeKey);
const openFileExplorerForCheckout = usePanelStore((state) => state.openFileExplorerForCheckout);
const setExplorerTabForCheckout = usePanelStore((state) => state.setExplorerTabForCheckout);
const handleOpenWorkspaceAttachment = useCallback(
(attachment: WorkspaceComposerAttachment) => {
const checkout = {
serverId,
cwd: attachment.attachment.cwd,
isGit: true,
};
openFileExplorerForCheckout({
checkout,
isCompact,
});
setExplorerTabForCheckout({
...checkout,
tab: "changes",
});
},
[isCompact, openFileExplorerForCheckout, serverId, setExplorerTabForCheckout],
);
const inputAreaStyle = useMemo(
() => [styles.inputAreaWrapper, { paddingBottom: insets.bottom }],
@@ -1253,6 +1295,8 @@ function ActiveAgentComposer({
value={agentInputDraft.text}
onChangeText={agentInputDraft.setText}
attachments={agentInputDraft.attachments}
workspaceAttachments={workspaceAttachments}
onOpenWorkspaceAttachment={handleOpenWorkspaceAttachment}
onChangeAttachments={agentInputDraft.setAttachments}
cwd={agentInputDraft.cwd}
clearDraft={agentInputDraft.clear}