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:
@@ -1,5 +1,6 @@
|
||||
import { create } from "zustand";
|
||||
import type { UserMessageImageAttachment } from "@/types/stream";
|
||||
import type { AgentAttachment } from "@server/shared/messages";
|
||||
|
||||
export type CreateFlowLifecycleState = "active" | "abandoned" | "sent";
|
||||
|
||||
@@ -12,6 +13,7 @@ interface PendingCreateAttempt {
|
||||
timestamp: number;
|
||||
lifecycle: CreateFlowLifecycleState;
|
||||
images?: UserMessageImageAttachment[];
|
||||
attachments?: AgentAttachment[];
|
||||
}
|
||||
|
||||
interface CreateFlowState {
|
||||
|
||||
@@ -1,8 +1,53 @@
|
||||
import "@/test/window-local-storage";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { __draftStoreTestUtils } from "./draft-store";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import type { ComposerAttachment } from "@/attachments/types";
|
||||
import { __draftStoreTestUtils, useDraftStore } from "./draft-store";
|
||||
|
||||
function workspaceReviewAttachment(): Extract<ComposerAttachment, { kind: "review" }> {
|
||||
return {
|
||||
kind: "review",
|
||||
reviewDraftKey: "review:key",
|
||||
commentCount: 1,
|
||||
attachment: {
|
||||
type: "review",
|
||||
mimeType: "application/paseo-review",
|
||||
cwd: "/repo",
|
||||
mode: "uncommitted",
|
||||
baseRef: null,
|
||||
comments: [
|
||||
{
|
||||
filePath: "src/example.ts",
|
||||
side: "new",
|
||||
lineNumber: 41,
|
||||
body: "Please simplify this.",
|
||||
context: {
|
||||
hunkHeader: "@@ -40,1 +40,1 @@",
|
||||
targetLine: {
|
||||
oldLineNumber: null,
|
||||
newLineNumber: 41,
|
||||
type: "add",
|
||||
content: "const value = newValue;",
|
||||
},
|
||||
lines: [
|
||||
{
|
||||
oldLineNumber: null,
|
||||
newLineNumber: 41,
|
||||
type: "add",
|
||||
content: "const value = newValue;",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("draft-store migration", () => {
|
||||
beforeEach(() => {
|
||||
useDraftStore.setState({ drafts: {}, createModalDraft: null });
|
||||
});
|
||||
|
||||
it("normalizes legacy image metadata into image attachments and strips persisted preview URLs", async () => {
|
||||
const migrated = await __draftStoreTestUtils.migratePersistedState({
|
||||
drafts: {
|
||||
@@ -79,4 +124,24 @@ describe("draft-store migration", () => {
|
||||
|
||||
expect(twice).toEqual(once);
|
||||
});
|
||||
|
||||
it("rejects workspace review attachments from migrated draft attachments", async () => {
|
||||
const migrated = await __draftStoreTestUtils.migratePersistedState({
|
||||
drafts: {
|
||||
"agent:server:agent": {
|
||||
input: {
|
||||
text: "hello",
|
||||
attachments: [workspaceReviewAttachment()],
|
||||
cwd: "/repo",
|
||||
},
|
||||
lifecycle: "active",
|
||||
updatedAt: 1700000000001,
|
||||
version: 2,
|
||||
},
|
||||
},
|
||||
createModalDraft: null,
|
||||
});
|
||||
|
||||
expect(migrated.drafts["agent:server:agent"]?.input.attachments).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { create } from "zustand";
|
||||
import { createJSONStorage, persist } from "zustand/middleware";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import type { AttachmentMetadata, ComposerAttachment } from "@/attachments/types";
|
||||
import type { AttachmentMetadata, UserComposerAttachment } from "@/attachments/types";
|
||||
import { GitHubSearchItemSchema } from "@server/shared/messages";
|
||||
import {
|
||||
garbageCollectAttachments,
|
||||
@@ -23,7 +23,7 @@ type PersistedDraftImage = AttachmentMetadata | LegacyDraftImage;
|
||||
|
||||
export interface DraftInput {
|
||||
text: string;
|
||||
attachments: ComposerAttachment[];
|
||||
attachments: UserComposerAttachment[];
|
||||
cwd: string;
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ function normalizePersistedImage(value: unknown): PersistedDraftImage | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function isComposerAttachment(value: unknown): value is ComposerAttachment {
|
||||
function isUserComposerAttachment(value: unknown): value is UserComposerAttachment {
|
||||
if (!value || typeof value !== "object") {
|
||||
return false;
|
||||
}
|
||||
@@ -150,7 +150,7 @@ function isComposerAttachment(value: unknown): value is ComposerAttachment {
|
||||
return GitHubSearchItemSchema.safeParse(record.item).success;
|
||||
}
|
||||
|
||||
function normalizeComposerAttachment(attachment: ComposerAttachment): ComposerAttachment {
|
||||
function normalizeComposerAttachment(attachment: UserComposerAttachment): UserComposerAttachment {
|
||||
if (attachment.kind === "image") {
|
||||
return {
|
||||
kind: "image",
|
||||
@@ -160,14 +160,16 @@ function normalizeComposerAttachment(attachment: ComposerAttachment): ComposerAt
|
||||
return attachment;
|
||||
}
|
||||
|
||||
function normalizePersistedComposerAttachment(value: unknown): ComposerAttachment | null {
|
||||
if (!isComposerAttachment(value)) {
|
||||
function normalizePersistedComposerAttachment(value: unknown): UserComposerAttachment | null {
|
||||
if (!isUserComposerAttachment(value)) {
|
||||
return null;
|
||||
}
|
||||
return normalizeComposerAttachment(value);
|
||||
}
|
||||
|
||||
function legacyImagesToAttachments(images: readonly AttachmentMetadata[]): ComposerAttachment[] {
|
||||
function legacyImagesToAttachments(
|
||||
images: readonly AttachmentMetadata[],
|
||||
): UserComposerAttachment[] {
|
||||
return images.map((metadata) => ({
|
||||
kind: "image",
|
||||
metadata,
|
||||
@@ -182,7 +184,7 @@ function isCanonicalDraftInput(value: unknown): value is CanonicalDraftInput {
|
||||
return (
|
||||
typeof input.text === "string" &&
|
||||
Array.isArray(input.attachments) &&
|
||||
input.attachments.every(isComposerAttachment) &&
|
||||
input.attachments.every(isUserComposerAttachment) &&
|
||||
typeof input.cwd === "string"
|
||||
);
|
||||
}
|
||||
@@ -426,7 +428,7 @@ async function migrateDraftInput(input: {
|
||||
const attachments = Array.isArray(rawInput.attachments)
|
||||
? rawInput.attachments
|
||||
.map((entry) => normalizePersistedComposerAttachment(entry))
|
||||
.filter((entry): entry is ComposerAttachment => entry !== null)
|
||||
.filter((entry): entry is UserComposerAttachment => entry !== null)
|
||||
: [];
|
||||
const legacyImages = Array.isArray(rawInput.images)
|
||||
? rawInput.images
|
||||
|
||||
Reference in New Issue
Block a user