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:
@@ -23,7 +23,7 @@ import {
|
||||
type FlatListProps,
|
||||
type StyleProp,
|
||||
type ViewStyle,
|
||||
TextStyle,
|
||||
type TextStyle,
|
||||
} from "react-native";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
@@ -70,6 +70,7 @@ import { shouldAnchorHeaderBeforeCollapse } from "@/utils/git-diff-scroll";
|
||||
import {
|
||||
buildSplitDiffRows,
|
||||
buildUnifiedDiffLines,
|
||||
type ReviewableDiffTarget,
|
||||
type SplitDiffDisplayLine,
|
||||
type SplitDiffRow,
|
||||
} from "@/utils/diff-layout";
|
||||
@@ -97,6 +98,26 @@ import {
|
||||
hasVisibleDiffTokens,
|
||||
} from "@/utils/diff-rendering";
|
||||
import { isWeb, isNative } from "@/constants/platform";
|
||||
import {
|
||||
buildWorkspaceAttachmentScopeKey,
|
||||
useWorkspaceAttachmentsStore,
|
||||
} from "@/attachments/workspace-attachments-store";
|
||||
import {
|
||||
buildReviewDraftScopeKey,
|
||||
buildReviewDraftKey,
|
||||
useActiveReviewDraftMode,
|
||||
useReviewAttachmentSnapshot,
|
||||
useSetActiveReviewDraftMode,
|
||||
type ReviewDraftComment,
|
||||
type ReviewDraftMode,
|
||||
getInlineReviewThreadState,
|
||||
getSplitInlineReviewThreadState,
|
||||
InlineReviewGutterCell,
|
||||
InlineReviewThread,
|
||||
isInlineReviewEditorForTarget,
|
||||
useInlineReviewController,
|
||||
type InlineReviewActions,
|
||||
} from "@/review";
|
||||
|
||||
export type { GitActionId, GitAction, GitActions } from "@/components/git-actions-policy";
|
||||
|
||||
@@ -205,6 +226,10 @@ interface DiffFileSectionProps {
|
||||
testID?: string;
|
||||
}
|
||||
|
||||
const EMPTY_COMMENTS: readonly ReviewDraftComment[] = [];
|
||||
|
||||
function noopStartComment(): void {}
|
||||
|
||||
function lineTypeBackground(type: DiffLine["type"] | undefined | null) {
|
||||
if (!type) return styles.emptySplitCell;
|
||||
if (type === "add") return styles.addLineContainer;
|
||||
@@ -217,14 +242,20 @@ function DiffGutterCell({
|
||||
lineNumber,
|
||||
type,
|
||||
gutterWidth,
|
||||
reviewTarget,
|
||||
reviewActions,
|
||||
style,
|
||||
}: {
|
||||
lineNumber: number | null;
|
||||
type: DiffLine["type"] | undefined | null;
|
||||
gutterWidth: number;
|
||||
reviewTarget?: ReviewableDiffTarget | null;
|
||||
reviewActions?: InlineReviewActions;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}) {
|
||||
const containerStyle = useMemo(
|
||||
() => [styles.gutterCell, lineTypeBackground(type), { width: gutterWidth }],
|
||||
[type, gutterWidth],
|
||||
() => [styles.gutterCell, lineTypeBackground(type), { width: gutterWidth }, style],
|
||||
[type, gutterWidth, style],
|
||||
);
|
||||
const textStyle = useMemo(
|
||||
() => [
|
||||
@@ -234,12 +265,29 @@ function DiffGutterCell({
|
||||
],
|
||||
[type],
|
||||
);
|
||||
const comments = useMemo(
|
||||
() =>
|
||||
reviewTarget
|
||||
? (reviewActions?.commentsByTarget.get(reviewTarget.key) ?? EMPTY_COMMENTS)
|
||||
: EMPTY_COMMENTS,
|
||||
[reviewTarget, reviewActions?.commentsByTarget],
|
||||
);
|
||||
const isEditorOpen = isInlineReviewEditorForTarget(reviewActions?.editor ?? null, reviewTarget);
|
||||
const onStartComment = reviewActions?.onStartComment ?? noopStartComment;
|
||||
|
||||
return (
|
||||
<View style={containerStyle}>
|
||||
<InlineReviewGutterCell
|
||||
reviewTarget={reviewTarget}
|
||||
comments={comments}
|
||||
isEditorOpen={isEditorOpen}
|
||||
showPersistentAction={reviewActions?.showPersistentAction ?? false}
|
||||
onStartComment={onStartComment}
|
||||
style={containerStyle}
|
||||
>
|
||||
<Text numberOfLines={1} style={textStyle}>
|
||||
{formatDiffGutterText(lineNumber)}
|
||||
</Text>
|
||||
</View>
|
||||
</InlineReviewGutterCell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -314,11 +362,15 @@ function DiffLineView({
|
||||
lineNumber,
|
||||
gutterWidth,
|
||||
wrapLines,
|
||||
reviewTarget,
|
||||
reviewActions,
|
||||
}: {
|
||||
line: DiffLine;
|
||||
lineNumber: number | null;
|
||||
gutterWidth: number;
|
||||
wrapLines: boolean;
|
||||
reviewTarget?: ReviewableDiffTarget | null;
|
||||
reviewActions?: InlineReviewActions;
|
||||
}) {
|
||||
const visibleTokens = hasVisibleDiffTokens(line.tokens) ? line.tokens : null;
|
||||
|
||||
@@ -326,18 +378,6 @@ function DiffLineView({
|
||||
() => [styles.diffLineContainer, lineTypeBackground(line.type)],
|
||||
[line.type],
|
||||
);
|
||||
const gutterStyle = useMemo(
|
||||
() => [styles.lineNumberGutter, { width: gutterWidth }],
|
||||
[gutterWidth],
|
||||
);
|
||||
const gutterTextStyle = useMemo(
|
||||
() => [
|
||||
styles.lineNumberText,
|
||||
line.type === "add" && styles.addLineNumberText,
|
||||
line.type === "remove" && styles.removeLineNumberText,
|
||||
],
|
||||
[line.type],
|
||||
);
|
||||
const textStyle = useMemo(
|
||||
() => [
|
||||
styles.diffLineText,
|
||||
@@ -352,11 +392,14 @@ function DiffLineView({
|
||||
|
||||
return (
|
||||
<View style={containerStyle}>
|
||||
<View style={gutterStyle}>
|
||||
<Text numberOfLines={1} style={gutterTextStyle}>
|
||||
{formatDiffGutterText(lineNumber)}
|
||||
</Text>
|
||||
</View>
|
||||
<DiffGutterCell
|
||||
lineNumber={lineNumber}
|
||||
type={line.type}
|
||||
gutterWidth={gutterWidth}
|
||||
reviewTarget={reviewTarget}
|
||||
reviewActions={reviewActions}
|
||||
style={styles.lineNumberGutter}
|
||||
/>
|
||||
{line.type !== "header" && visibleTokens ? (
|
||||
<HighlightedText tokens={visibleTokens} wrapLines={wrapLines} />
|
||||
) : (
|
||||
@@ -370,10 +413,12 @@ function SplitDiffLine({
|
||||
line,
|
||||
gutterWidth,
|
||||
wrapLines,
|
||||
reviewActions,
|
||||
}: {
|
||||
line: SplitDiffDisplayLine | null;
|
||||
gutterWidth: number;
|
||||
wrapLines: boolean;
|
||||
reviewActions?: InlineReviewActions;
|
||||
}) {
|
||||
const visibleTokens = line && hasVisibleDiffTokens(line.tokens) ? line.tokens : null;
|
||||
|
||||
@@ -381,18 +426,6 @@ function SplitDiffLine({
|
||||
() => [styles.diffLineContainer, lineTypeBackground(line?.type)],
|
||||
[line?.type],
|
||||
);
|
||||
const gutterStyle = useMemo(
|
||||
() => [styles.lineNumberGutter, { width: gutterWidth }],
|
||||
[gutterWidth],
|
||||
);
|
||||
const gutterTextStyle = useMemo(
|
||||
() => [
|
||||
styles.lineNumberText,
|
||||
line?.type === "add" && styles.addLineNumberText,
|
||||
line?.type === "remove" && styles.removeLineNumberText,
|
||||
],
|
||||
[line?.type],
|
||||
);
|
||||
const textStyle = useMemo(
|
||||
() => [
|
||||
styles.diffLineText,
|
||||
@@ -407,11 +440,14 @@ function SplitDiffLine({
|
||||
|
||||
return (
|
||||
<View style={containerStyle}>
|
||||
<View style={gutterStyle}>
|
||||
<Text numberOfLines={1} style={gutterTextStyle}>
|
||||
{formatDiffGutterText(line?.lineNumber ?? null)}
|
||||
</Text>
|
||||
</View>
|
||||
<DiffGutterCell
|
||||
lineNumber={line?.lineNumber ?? null}
|
||||
type={line?.type}
|
||||
gutterWidth={gutterWidth}
|
||||
reviewTarget={line?.reviewTarget}
|
||||
reviewActions={reviewActions}
|
||||
style={styles.lineNumberGutter}
|
||||
/>
|
||||
{visibleTokens ? (
|
||||
<HighlightedText tokens={visibleTokens} wrapLines={wrapLines} />
|
||||
) : (
|
||||
@@ -421,17 +457,119 @@ function SplitDiffLine({
|
||||
);
|
||||
}
|
||||
|
||||
function InlineReviewThreadContent({
|
||||
reviewTarget,
|
||||
reviewActions,
|
||||
reservedHeight,
|
||||
viewportWidth,
|
||||
pinToViewport,
|
||||
}: {
|
||||
reviewTarget: ReviewableDiffTarget | null | undefined;
|
||||
reviewActions?: InlineReviewActions;
|
||||
reservedHeight?: number;
|
||||
viewportWidth?: number;
|
||||
pinToViewport?: boolean;
|
||||
}) {
|
||||
const threadState = getInlineReviewThreadState({ reviewTarget, reviewActions });
|
||||
const height = reservedHeight ?? threadState?.height ?? 0;
|
||||
const placeholderStyle = useMemo<ViewStyle>(() => ({ minHeight: height }), [height]);
|
||||
if (height === 0) {
|
||||
return null;
|
||||
}
|
||||
if (!reviewTarget || !reviewActions || !threadState) {
|
||||
return <View style={placeholderStyle} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<InlineReviewThread
|
||||
reviewTarget={reviewTarget}
|
||||
reviewActions={reviewActions}
|
||||
height={height}
|
||||
viewportWidth={viewportWidth}
|
||||
pinToViewport={pinToViewport}
|
||||
testID={`review-thread-${reviewTarget.key}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InlineReviewGutterSpacer({
|
||||
reviewTarget,
|
||||
reviewActions,
|
||||
gutterWidth,
|
||||
reservedHeight,
|
||||
style,
|
||||
}: {
|
||||
reviewTarget: ReviewableDiffTarget | null | undefined;
|
||||
reviewActions?: InlineReviewActions;
|
||||
gutterWidth: number;
|
||||
reservedHeight?: number;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}) {
|
||||
const threadState = getInlineReviewThreadState({ reviewTarget, reviewActions });
|
||||
const height = reservedHeight ?? threadState?.height ?? 0;
|
||||
const spacerStyle = useMemo<StyleProp<ViewStyle>>(
|
||||
() => [styles.inlineReviewGutterSpacer, { width: gutterWidth, minHeight: height }, style],
|
||||
[gutterWidth, height, style],
|
||||
);
|
||||
if (height === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <View style={spacerStyle} />;
|
||||
}
|
||||
|
||||
function InlineReviewRow({
|
||||
reviewTarget,
|
||||
reviewActions,
|
||||
gutterWidth,
|
||||
reservedHeight,
|
||||
}: {
|
||||
reviewTarget: ReviewableDiffTarget | null | undefined;
|
||||
reviewActions?: InlineReviewActions;
|
||||
gutterWidth: number;
|
||||
reservedHeight?: number;
|
||||
}) {
|
||||
const threadState = getInlineReviewThreadState({ reviewTarget, reviewActions });
|
||||
const height = reservedHeight ?? threadState?.height ?? 0;
|
||||
const gutterSpacerStyle = useMemo<StyleProp<ViewStyle>>(
|
||||
() => [styles.inlineReviewGutterSpacer, { width: gutterWidth }],
|
||||
[gutterWidth],
|
||||
);
|
||||
const placeholderStyle = useMemo<ViewStyle>(() => ({ minHeight: height }), [height]);
|
||||
if (height === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.inlineReviewRow}>
|
||||
<View style={gutterSpacerStyle} />
|
||||
{reviewTarget && reviewActions && threadState ? (
|
||||
<InlineReviewThread
|
||||
reviewTarget={reviewTarget}
|
||||
reviewActions={reviewActions}
|
||||
height={height}
|
||||
testID={`review-thread-${reviewTarget.key}`}
|
||||
/>
|
||||
) : (
|
||||
<View style={placeholderStyle} />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function SplitDiffColumn({
|
||||
rows,
|
||||
side,
|
||||
gutterWidth,
|
||||
wrapLines,
|
||||
reviewActions,
|
||||
showDivider = false,
|
||||
}: {
|
||||
rows: SplitDiffRow[];
|
||||
side: "left" | "right";
|
||||
gutterWidth: number;
|
||||
wrapLines: boolean;
|
||||
reviewActions?: InlineReviewActions;
|
||||
showDivider?: boolean;
|
||||
}) {
|
||||
const [scrollWidth, setScrollWidth] = useState(0);
|
||||
@@ -463,13 +601,27 @@ function SplitDiffColumn({
|
||||
</View>
|
||||
);
|
||||
}
|
||||
const line = side === "left" ? row.left : row.right;
|
||||
const reviewRowState = getSplitInlineReviewThreadState({
|
||||
left: row.left?.reviewTarget,
|
||||
right: row.right?.reviewTarget,
|
||||
reviewActions,
|
||||
});
|
||||
return (
|
||||
<SplitDiffLine
|
||||
key={key}
|
||||
line={side === "left" ? row.left : row.right}
|
||||
gutterWidth={gutterWidth}
|
||||
wrapLines={wrapLines}
|
||||
/>
|
||||
<View key={key}>
|
||||
<SplitDiffLine
|
||||
line={line}
|
||||
gutterWidth={gutterWidth}
|
||||
wrapLines={wrapLines}
|
||||
reviewActions={reviewActions}
|
||||
/>
|
||||
<InlineReviewRow
|
||||
reviewTarget={line?.reviewTarget}
|
||||
reviewActions={reviewActions}
|
||||
gutterWidth={gutterWidth}
|
||||
reservedHeight={reviewRowState?.height}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
@@ -487,13 +639,27 @@ function SplitDiffColumn({
|
||||
);
|
||||
}
|
||||
const line = side === "left" ? row.left : row.right;
|
||||
const reviewRowState = getSplitInlineReviewThreadState({
|
||||
left: row.left?.reviewTarget,
|
||||
right: row.right?.reviewTarget,
|
||||
reviewActions,
|
||||
});
|
||||
return (
|
||||
<DiffGutterCell
|
||||
key={key}
|
||||
lineNumber={line?.lineNumber ?? null}
|
||||
type={line?.type}
|
||||
gutterWidth={gutterWidth}
|
||||
/>
|
||||
<View key={key}>
|
||||
<DiffGutterCell
|
||||
lineNumber={line?.lineNumber ?? null}
|
||||
type={line?.type}
|
||||
gutterWidth={gutterWidth}
|
||||
reviewTarget={line?.reviewTarget}
|
||||
reviewActions={reviewActions}
|
||||
/>
|
||||
<InlineReviewGutterSpacer
|
||||
reviewTarget={line?.reviewTarget}
|
||||
reviewActions={reviewActions}
|
||||
gutterWidth={gutterWidth}
|
||||
reservedHeight={reviewRowState?.height}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
@@ -512,12 +678,23 @@ function SplitDiffColumn({
|
||||
</View>
|
||||
);
|
||||
}
|
||||
const line = side === "left" ? row.left : row.right;
|
||||
const reviewRowState = getSplitInlineReviewThreadState({
|
||||
left: row.left?.reviewTarget,
|
||||
right: row.right?.reviewTarget,
|
||||
reviewActions,
|
||||
});
|
||||
return (
|
||||
<SplitTextLine
|
||||
key={key}
|
||||
line={side === "left" ? row.left : row.right}
|
||||
wrapLines={false}
|
||||
/>
|
||||
<View key={key}>
|
||||
<SplitTextLine line={line} wrapLines={false} />
|
||||
<InlineReviewThreadContent
|
||||
reviewTarget={line?.reviewTarget}
|
||||
reviewActions={reviewActions}
|
||||
reservedHeight={reviewRowState?.height}
|
||||
viewportWidth={scrollWidth}
|
||||
pinToViewport
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
@@ -620,12 +797,14 @@ function DiffFileBody({
|
||||
file,
|
||||
layout,
|
||||
wrapLines,
|
||||
reviewActions,
|
||||
onBodyHeightChange,
|
||||
testID,
|
||||
}: {
|
||||
file: ParsedDiffFile;
|
||||
layout: "unified" | "split";
|
||||
wrapLines: boolean;
|
||||
reviewActions?: InlineReviewActions;
|
||||
onBodyHeightChange?: (file: ParsedDiffFile, height: number) => void;
|
||||
testID?: string;
|
||||
}) {
|
||||
@@ -679,12 +858,14 @@ function DiffFileBody({
|
||||
side="left"
|
||||
gutterWidth={gutterWidth}
|
||||
wrapLines={wrapLines}
|
||||
reviewActions={reviewActions}
|
||||
/>
|
||||
<SplitDiffColumn
|
||||
rows={rows}
|
||||
side="right"
|
||||
gutterWidth={gutterWidth}
|
||||
wrapLines={wrapLines}
|
||||
reviewActions={reviewActions}
|
||||
showDivider
|
||||
/>
|
||||
</View>
|
||||
@@ -697,30 +878,48 @@ function DiffFileBody({
|
||||
return (
|
||||
<View style={styles.diffContent}>
|
||||
<View style={styles.linesContainer}>
|
||||
{computedLines.map(({ line, lineNumber, key }) => (
|
||||
<DiffLineView
|
||||
key={key}
|
||||
line={line}
|
||||
lineNumber={lineNumber}
|
||||
gutterWidth={gutterWidth}
|
||||
wrapLines={wrapLines}
|
||||
/>
|
||||
{computedLines.map(({ line, lineNumber, key, reviewTarget }) => (
|
||||
<View key={key}>
|
||||
<DiffLineView
|
||||
line={line}
|
||||
lineNumber={lineNumber}
|
||||
gutterWidth={gutterWidth}
|
||||
wrapLines={wrapLines}
|
||||
reviewTarget={reviewTarget}
|
||||
reviewActions={reviewActions}
|
||||
/>
|
||||
<InlineReviewRow
|
||||
reviewTarget={reviewTarget}
|
||||
reviewActions={reviewActions}
|
||||
gutterWidth={gutterWidth}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const textViewportWidth =
|
||||
scrollViewWidth > 0 ? scrollViewWidth : Math.max(0, bodyWidth - gutterWidth);
|
||||
return (
|
||||
<View style={DIFF_CONTENT_ROW_STYLE}>
|
||||
<View style={styles.gutterColumn}>
|
||||
{computedLines.map(({ line, lineNumber, key }) => (
|
||||
<DiffGutterCell
|
||||
key={key}
|
||||
lineNumber={lineNumber}
|
||||
type={line.type}
|
||||
gutterWidth={gutterWidth}
|
||||
/>
|
||||
{computedLines.map(({ line, lineNumber, key, reviewTarget }) => (
|
||||
<View key={key}>
|
||||
<DiffGutterCell
|
||||
lineNumber={lineNumber}
|
||||
type={line.type}
|
||||
gutterWidth={gutterWidth}
|
||||
reviewTarget={reviewTarget}
|
||||
reviewActions={reviewActions}
|
||||
/>
|
||||
<InlineReviewGutterSpacer
|
||||
reviewTarget={reviewTarget}
|
||||
reviewActions={reviewActions}
|
||||
gutterWidth={gutterWidth}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
<DiffScroll
|
||||
@@ -730,8 +929,16 @@ function DiffFileBody({
|
||||
contentContainerStyle={styles.diffContentInner}
|
||||
>
|
||||
<View style={linesContainerRowStyle}>
|
||||
{computedLines.map(({ line, key }) => (
|
||||
<DiffTextLine key={key} line={line} wrapLines={false} />
|
||||
{computedLines.map(({ line, key, reviewTarget }) => (
|
||||
<View key={key}>
|
||||
<DiffTextLine line={line} wrapLines={false} />
|
||||
<InlineReviewThreadContent
|
||||
reviewTarget={reviewTarget}
|
||||
reviewActions={reviewActions}
|
||||
viewportWidth={textViewportWidth}
|
||||
pinToViewport
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</DiffScroll>
|
||||
@@ -1524,7 +1731,7 @@ export function GitDiffPane({
|
||||
workspaceId,
|
||||
cwd,
|
||||
hideHeaderRow,
|
||||
enabled = true,
|
||||
enabled,
|
||||
}: GitDiffPaneProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const toast = useToast();
|
||||
@@ -1532,7 +1739,7 @@ export function GitDiffPane({
|
||||
const showDesktopWebScrollbar = isWeb && !isMobile;
|
||||
const canUseSplitLayout = isWeb && !isMobile;
|
||||
const router = useRouter();
|
||||
const [diffModeOverride, setDiffModeOverride] = useState<"uncommitted" | "base" | null>(null);
|
||||
const [diffModeOverride, setDiffModeOverride] = useState<ReviewDraftMode | null>(null);
|
||||
const [postShipArchiveSuggested, setPostShipArchiveSuggested] = useState(false);
|
||||
const [shipDefault, setShipDefault] = useState<"merge" | "pr">("merge");
|
||||
const { preferences: changesPreferences, updatePreferences: updateChangesPreferences } =
|
||||
@@ -1555,13 +1762,8 @@ export function GitDiffPane({
|
||||
void updateChangesPreferences({ hideWhitespace: !changesPreferences.hideWhitespace });
|
||||
}, [changesPreferences.hideWhitespace, updateChangesPreferences]);
|
||||
|
||||
const handleSelectUncommitted = useCallback(() => {
|
||||
setDiffModeOverride("uncommitted");
|
||||
}, []);
|
||||
|
||||
const handleSelectBase = useCallback(() => {
|
||||
setDiffModeOverride("base");
|
||||
}, []);
|
||||
// handleSelectUncommitted/handleSelectBase are defined later, after reviewDraftScopeKey
|
||||
// and setActiveReviewMode are available, so they can record the active review mode.
|
||||
|
||||
const handleLayoutUnified = useCallback(() => {
|
||||
handleLayoutChange("unified");
|
||||
@@ -1617,8 +1819,20 @@ export function GitDiffPane({
|
||||
} = statusState;
|
||||
|
||||
// Auto-select diff mode based on state: uncommitted when dirty, base when clean
|
||||
const autoDiffMode = hasUncommittedChanges ? "uncommitted" : "base";
|
||||
const diffMode = diffModeOverride ?? autoDiffMode;
|
||||
const autoDiffMode: ReviewDraftMode = hasUncommittedChanges ? "uncommitted" : "base";
|
||||
const reviewDraftScopeKey = useMemo(
|
||||
() =>
|
||||
buildReviewDraftScopeKey({
|
||||
serverId,
|
||||
workspaceId,
|
||||
cwd,
|
||||
baseRef,
|
||||
ignoreWhitespace: changesPreferences.hideWhitespace,
|
||||
}),
|
||||
[baseRef, changesPreferences.hideWhitespace, cwd, serverId, workspaceId],
|
||||
);
|
||||
const activeReviewMode = useActiveReviewDraftMode({ scopeKey: reviewDraftScopeKey });
|
||||
const diffMode = diffModeOverride ?? activeReviewMode ?? autoDiffMode;
|
||||
|
||||
const {
|
||||
files,
|
||||
@@ -1630,8 +1844,69 @@ export function GitDiffPane({
|
||||
mode: diffMode,
|
||||
baseRef,
|
||||
ignoreWhitespace: changesPreferences.hideWhitespace,
|
||||
enabled: shouldEnableCheckoutDiff({ paneEnabled: enabled, isGit }),
|
||||
enabled: shouldEnableCheckoutDiff({ paneEnabled: enabled !== false, isGit }),
|
||||
});
|
||||
const reviewDraftKey = useMemo(
|
||||
() =>
|
||||
buildReviewDraftKey({
|
||||
serverId,
|
||||
workspaceId,
|
||||
cwd,
|
||||
mode: diffMode,
|
||||
baseRef,
|
||||
ignoreWhitespace: changesPreferences.hideWhitespace,
|
||||
}),
|
||||
[baseRef, changesPreferences.hideWhitespace, cwd, diffMode, serverId, workspaceId],
|
||||
);
|
||||
const setActiveReviewMode = useSetActiveReviewDraftMode();
|
||||
|
||||
const handleSelectUncommitted = useCallback(() => {
|
||||
setDiffModeOverride("uncommitted");
|
||||
setActiveReviewMode({ scopeKey: reviewDraftScopeKey, mode: "uncommitted" });
|
||||
}, [reviewDraftScopeKey, setActiveReviewMode]);
|
||||
|
||||
const handleSelectBase = useCallback(() => {
|
||||
setDiffModeOverride("base");
|
||||
setActiveReviewMode({ scopeKey: reviewDraftScopeKey, mode: "base" });
|
||||
}, [reviewDraftScopeKey, setActiveReviewMode]);
|
||||
|
||||
const reviewActions = useInlineReviewController({
|
||||
reviewDraftKey,
|
||||
showPersistentAction: isMobile,
|
||||
});
|
||||
const reviewAttachment = useReviewAttachmentSnapshot({
|
||||
key: reviewDraftKey,
|
||||
diffFiles: files,
|
||||
cwd,
|
||||
mode: diffMode,
|
||||
baseRef,
|
||||
});
|
||||
const workspaceAttachmentScopeKey = useMemo(
|
||||
() => buildWorkspaceAttachmentScopeKey({ serverId, workspaceId, cwd }),
|
||||
[cwd, serverId, workspaceId],
|
||||
);
|
||||
const setWorkspaceAttachments = useWorkspaceAttachmentsStore(
|
||||
(state) => state.setWorkspaceAttachments,
|
||||
);
|
||||
const clearWorkspaceAttachments = useWorkspaceAttachmentsStore(
|
||||
(state) => state.clearWorkspaceAttachments,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setWorkspaceAttachments({
|
||||
scopeKey: workspaceAttachmentScopeKey,
|
||||
attachments: reviewAttachment ? [reviewAttachment] : [],
|
||||
});
|
||||
|
||||
return () => {
|
||||
clearWorkspaceAttachments({ scopeKey: workspaceAttachmentScopeKey });
|
||||
};
|
||||
}, [
|
||||
clearWorkspaceAttachments,
|
||||
reviewAttachment,
|
||||
setWorkspaceAttachments,
|
||||
workspaceAttachmentScopeKey,
|
||||
]);
|
||||
const {
|
||||
status: prStatus,
|
||||
githubFeaturesEnabled,
|
||||
@@ -2026,6 +2301,7 @@ export function GitDiffPane({
|
||||
file={item.file}
|
||||
layout={effectiveLayout}
|
||||
wrapLines={wrapLines}
|
||||
reviewActions={reviewActions}
|
||||
onBodyHeightChange={handleBodyHeightChange}
|
||||
testID={`diff-file-${item.fileIndex}-body`}
|
||||
/>
|
||||
@@ -2036,6 +2312,7 @@ export function GitDiffPane({
|
||||
handleBodyHeightChange,
|
||||
handleHeaderHeightChange,
|
||||
handleToggleExpanded,
|
||||
reviewActions,
|
||||
wrapLines,
|
||||
],
|
||||
);
|
||||
@@ -2075,8 +2352,14 @@ export function GitDiffPane({
|
||||
);
|
||||
|
||||
const flatExtraData = useMemo(
|
||||
() => ({ expandedPathsArray, effectiveLayout, heightVersion, wrapLines }),
|
||||
[expandedPathsArray, effectiveLayout, heightVersion, wrapLines],
|
||||
() => ({
|
||||
expandedPathsArray,
|
||||
effectiveLayout,
|
||||
heightVersion,
|
||||
wrapLines,
|
||||
reviewActions,
|
||||
}),
|
||||
[expandedPathsArray, effectiveLayout, heightVersion, wrapLines, reviewActions],
|
||||
);
|
||||
|
||||
const hasChanges = files.length > 0;
|
||||
@@ -2655,6 +2938,17 @@ const styles = StyleSheet.create((theme) => ({
|
||||
borderRightColor: theme.colors.border,
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
inlineReviewRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "stretch",
|
||||
backgroundColor: theme.colors.surface1,
|
||||
},
|
||||
inlineReviewGutterSpacer: {
|
||||
borderRightWidth: theme.borderWidth[1],
|
||||
borderRightColor: theme.colors.border,
|
||||
backgroundColor: theme.colors.surface1,
|
||||
flexShrink: 0,
|
||||
},
|
||||
textLineContainer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "stretch",
|
||||
|
||||
Reference in New Issue
Block a user