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:
@@ -15,12 +15,16 @@ import { WorkspaceDraftAgentTab } from "./workspace-draft-agent-tab";
|
||||
|
||||
const {
|
||||
createAgentMock,
|
||||
getCheckoutStatusMock,
|
||||
onRuntimeEventMock,
|
||||
latestComposerText,
|
||||
latestComposerDisabled,
|
||||
latestStreamText,
|
||||
onCreatedMock,
|
||||
} = vi.hoisted(() => ({
|
||||
createAgentMock: vi.fn(),
|
||||
getCheckoutStatusMock: vi.fn(),
|
||||
onRuntimeEventMock: vi.fn(() => () => {}),
|
||||
latestComposerText: { current: null as string | null },
|
||||
latestComposerDisabled: { current: null as boolean | null },
|
||||
latestStreamText: { current: null as string | null },
|
||||
@@ -43,6 +47,7 @@ vi.mock("react-native-unistyles", () => ({
|
||||
})
|
||||
: factory,
|
||||
},
|
||||
useUnistyles: () => ({ rt: { breakpoint: "lg" } }),
|
||||
}));
|
||||
|
||||
vi.mock("react-native-safe-area-context", () => ({
|
||||
@@ -57,6 +62,8 @@ vi.mock("@/constants/platform", () => ({
|
||||
vi.mock("@/runtime/host-runtime", () => ({
|
||||
useHostRuntimeClient: () => ({
|
||||
createAgent: createAgentMock,
|
||||
getCheckoutStatus: getCheckoutStatusMock,
|
||||
on: onRuntimeEventMock,
|
||||
}),
|
||||
useHostRuntimeIsConnected: () => true,
|
||||
}));
|
||||
@@ -131,6 +138,16 @@ vi.mock("@/components/composer-attachments", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/attachments/composer-workspace-attachments", () => ({
|
||||
composerWorkspaceAttachment: {
|
||||
userAttachmentsOnly: (attachments: ComposerAttachment[]) =>
|
||||
attachments.filter(
|
||||
(attachment): attachment is Exclude<ComposerAttachment, { kind: "review" }> =>
|
||||
attachment.kind !== "review",
|
||||
),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/components/composer", () => ({
|
||||
Composer: ({
|
||||
value,
|
||||
@@ -196,6 +213,23 @@ beforeEach(() => {
|
||||
root = createRoot(container);
|
||||
queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
createAgentMock.mockReset();
|
||||
getCheckoutStatusMock.mockResolvedValue({
|
||||
cwd: "/repo/.paseo/worktrees/workspace-1",
|
||||
error: null,
|
||||
requestId: "checkout-status-1",
|
||||
isGit: false,
|
||||
isPaseoOwnedWorktree: true,
|
||||
repoRoot: null,
|
||||
currentBranch: null,
|
||||
isDirty: false,
|
||||
baseRef: null,
|
||||
aheadBehind: null,
|
||||
aheadOfOrigin: null,
|
||||
behindOfOrigin: null,
|
||||
hasRemote: false,
|
||||
remoteUrl: null,
|
||||
});
|
||||
onRuntimeEventMock.mockClear();
|
||||
onCreatedMock.mockReset();
|
||||
latestComposerText.current = null;
|
||||
latestComposerDisabled.current = null;
|
||||
|
||||
@@ -6,12 +6,14 @@ import invariant from "tiny-invariant";
|
||||
import { Composer } from "@/components/composer";
|
||||
import { FileDropZone } from "@/components/file-drop-zone";
|
||||
import { AgentStreamView } from "@/components/agent-stream-view";
|
||||
import { composerWorkspaceAttachment } from "@/attachments/composer-workspace-attachments";
|
||||
import type { ImageAttachment } from "@/components/message-input";
|
||||
import { useAgentInputDraft } from "@/hooks/use-agent-input-draft";
|
||||
import { useDraftAgentCreateFlow } from "@/hooks/use-draft-agent-create-flow";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
import { buildWorkspaceDraftAgentConfig } from "@/screens/workspace/workspace-draft-agent-config";
|
||||
import { buildDraftStoreKey } from "@/stores/draft-keys";
|
||||
import { usePanelStore } from "@/stores/panel-store";
|
||||
import type { Agent } from "@/stores/session-store";
|
||||
import { useWorkspaceExecutionAuthority } from "@/stores/session-store-hooks";
|
||||
import { useWorkspaceDraftSubmissionStore } from "@/stores/workspace-draft-submission-store";
|
||||
@@ -20,8 +22,13 @@ import { shouldAutoFocusWorkspaceDraftComposer } from "@/screens/workspace/works
|
||||
import type { AgentCapabilityFlags } from "@server/server/agent/agent-sdk-types";
|
||||
import type { AgentSnapshotPayload } from "@server/shared/messages";
|
||||
import type { DaemonClient } from "@server/client/daemon-client";
|
||||
import type { ComposerAttachment } from "@/attachments/types";
|
||||
import type { ComposerAttachment, WorkspaceComposerAttachment } from "@/attachments/types";
|
||||
import {
|
||||
useWorkspaceAttachments,
|
||||
useWorkspaceAttachmentScopeKey,
|
||||
} from "@/attachments/workspace-attachments-store";
|
||||
import type { UserMessageImageAttachment } from "@/types/stream";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { isWeb } from "@/constants/platform";
|
||||
|
||||
const EMPTY_PENDING_PERMISSIONS = new Map();
|
||||
@@ -324,6 +331,33 @@ export function WorkspaceDraftAgentTab({
|
||||
);
|
||||
const autoSubmitConfig = resolveAutoSubmitConfig(pendingAutoSubmit);
|
||||
const allowsEmptyAutoSubmit = pendingAutoSubmit?.allowEmptyText === true;
|
||||
const isCompact = useIsCompactFormFactor();
|
||||
const workspaceAttachmentScopeKey = useWorkspaceAttachmentScopeKey({
|
||||
serverId,
|
||||
cwd: draftInput.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 {
|
||||
formErrorMessage,
|
||||
@@ -408,7 +442,7 @@ export function WorkspaceDraftAgentTab({
|
||||
cwd: submission.cwd,
|
||||
}).catch(() => {
|
||||
setDraftText(submission.text);
|
||||
setDraftAttachments(submission.attachments);
|
||||
setDraftAttachments(composerWorkspaceAttachment.userAttachmentsOnly(submission.attachments));
|
||||
autoSubmitKeyRef.current = null;
|
||||
});
|
||||
}, [
|
||||
@@ -562,6 +596,8 @@ export function WorkspaceDraftAgentTab({
|
||||
value={draftInput.text}
|
||||
onChangeText={draftInput.setText}
|
||||
attachments={draftInput.attachments}
|
||||
workspaceAttachments={workspaceAttachments}
|
||||
onOpenWorkspaceAttachment={handleOpenWorkspaceAttachment}
|
||||
onChangeAttachments={draftInput.setAttachments}
|
||||
cwd={draftInput.cwd}
|
||||
clearDraft={draftInput.clear}
|
||||
|
||||
Reference in New Issue
Block a user