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
+381 -87
View File
@@ -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",