feat(app): make the side panel user-directed (#3605)
* feat(app): let the file panel close its tree rail The file panel's tree rail was permanent on desktop — the changes panel had a folder-tree toggle in its toolbar, the file toolbar had nothing, so the rail was there whether you wanted it or not. The changes panel's private DesktopTreeToggle becomes a shared TreeRailToggle so both toolbars render the same affordance; that also drops the treeToggleStyle prop threaded through ChangesToolbarProps, which recomputed exactly what the component already built internally. The i18n keys move out of workspace.git.diff into workspace.tree, since a shared component reaching into the git namespace would rot. The two rails keep independent flags — the file rail in panel-store, the changes rail in useChangesPreferences — so closing one leaves the other alone. FileTreeToggle wires itself to the store rather than taking a callback: the bar renders several components below FilePanel, where the rail actually lives. Both rails also get narrower (320 -> 260 default, 200 -> 180 minimum). The persist version bump resets treeRailWidth on purpose: nearly every existing install carries the old 320 default, so without the reset the narrower rail would only ever reach new users. Anyone who dragged their rail to a deliberate custom width loses it once. Note that MIN_CONTENT_WIDTH still clamps the rail to `containerWidth - 240`, so in a narrow pane the new default never applies — that floor, not the default, is what sets the width there. * feat(app): make the side panel an empty workspace surface The fixed, hideable side panel keeps supporting context beside an agent without seeding content. Pane-local choices stay local, while implicit supporting opens follow a persisted routing preference. * test(app): exercise side panel policy through its capability boundary The browser rail journey already covers the real tree-toggle behavior. Side-panel policy tests now pass their platform capability through the public boundary instead of replacing a production module. * test(app): expect an empty side panel after upgrade * test(app): expect an empty first split pane * test(app): account for the empty side panel in split retention * feat(app): make workspace panel tabs independent Give explicit New actions fresh tab instances, keep panel state opaque to tab management, preserve File navigation context, and stabilize uncached File transitions. * fix(app): preserve file conflicts during preview refresh Keep the current preview mounted for same-target reads and conversions so the editor retains its conflict observation state. Clear retained previews only when the file target changes.
This commit is contained in:
@@ -3,7 +3,6 @@ import { Pressable, Text, View } from "react-native";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRetainedPanelActive } from "@/components/retained-panel";
|
||||
import { useChangesPreferences } from "@/hooks/use-changes-preferences";
|
||||
import { useCheckoutCommitsQuery, type CheckoutCommitsQueryResult } from "@/git/use-commits-query";
|
||||
import { ThemedChevron, chevronColorMapping } from "@/git/themed-chevron";
|
||||
import { normalizeBranchOptionName } from "@/utils/branch-suggestions";
|
||||
@@ -13,6 +12,8 @@ interface CommitsSectionProps {
|
||||
serverId: string;
|
||||
cwd: string;
|
||||
onCommitPress: (sha: string) => void;
|
||||
collapsed?: boolean;
|
||||
onCollapsedChange?: (collapsed: boolean) => void;
|
||||
}
|
||||
|
||||
function CommitsSectionSkeleton() {
|
||||
@@ -82,11 +83,15 @@ function CommitsSectionContent({
|
||||
);
|
||||
}
|
||||
|
||||
export function CommitsSection({ serverId, cwd, onCommitPress }: CommitsSectionProps) {
|
||||
export function CommitsSection({
|
||||
serverId,
|
||||
cwd,
|
||||
onCommitPress,
|
||||
collapsed = true,
|
||||
onCollapsedChange,
|
||||
}: CommitsSectionProps) {
|
||||
const { t } = useTranslation();
|
||||
const { preferences, updatePreferences } = useChangesPreferences();
|
||||
const isPanelActive = useRetainedPanelActive();
|
||||
const collapsed = preferences.commitsCollapsed;
|
||||
const [now, setNow] = useState(() => new Date());
|
||||
const displayNow = useMemo(() => (isPanelActive ? new Date() : now), [isPanelActive, now]);
|
||||
const query = useCheckoutCommitsQuery({
|
||||
@@ -99,8 +104,8 @@ export function CommitsSection({ serverId, cwd, onCommitPress }: CommitsSectionP
|
||||
if (collapsed) {
|
||||
setNow(new Date());
|
||||
}
|
||||
void updatePreferences({ commitsCollapsed: !collapsed });
|
||||
}, [collapsed, updatePreferences]);
|
||||
onCollapsedChange?.(!collapsed);
|
||||
}, [collapsed, onCollapsedChange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (collapsed || !isPanelActive) {
|
||||
|
||||
+130
-232
@@ -2,6 +2,7 @@ import { useState, useCallback, useMemo, type ReactElement } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
import { TreeRail } from "@/components/tree-rail";
|
||||
import { TreeRailToggle } from "@/components/tree-rail-toggle";
|
||||
import { DiffStat } from "@/components/diff-stat";
|
||||
import {
|
||||
View,
|
||||
@@ -13,13 +14,12 @@ import {
|
||||
type ViewStyle,
|
||||
} from "react-native";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { ICON_SIZE, SPACING, type Theme } from "@/styles/theme";
|
||||
import { useIsCompactFormFactor, WORKSPACE_SECONDARY_HEADER_HEIGHT } from "@/constants/layout";
|
||||
import { ICON_SIZE, type Theme } from "@/styles/theme";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
import {
|
||||
AlignJustify,
|
||||
ChevronDown,
|
||||
Columns2,
|
||||
FolderTree,
|
||||
ListChevronsDownUp,
|
||||
ListChevronsUpDown,
|
||||
Maximize2,
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
WrapText,
|
||||
} from "lucide-react-native";
|
||||
import { type ParsedDiffFile } from "@/git/use-diff-query";
|
||||
import type { ChangesState } from "@/panels/changes/state";
|
||||
import { defaultChangesState } from "@/panels/changes/state";
|
||||
import { DiffDocument, type WorkingDiffMode } from "@/git/diff-document";
|
||||
import { FileHeader } from "@/git/file-header";
|
||||
import {
|
||||
@@ -41,7 +43,6 @@ import {
|
||||
import { DiffFolderRow } from "@/git/diff-folder-row";
|
||||
import { useCheckoutPrStatusQuery } from "@/git/use-pr-status-query";
|
||||
import { CommitsSection } from "@/git/commits-section/commits-section";
|
||||
import { useChangesPreferences } from "@/hooks/use-changes-preferences";
|
||||
import { useAppSettings } from "@/hooks/use-settings";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -68,10 +69,15 @@ import { useToast } from "@/contexts/toast-context";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { confirmDialog } from "@/utils/confirm-dialog";
|
||||
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
import { usePanelStore } from "@/stores/panel-store";
|
||||
import { collectAllTabs, useWorkspaceLayoutStore } from "@/stores/workspace-layout-store";
|
||||
import {
|
||||
PaneContentToolbar,
|
||||
paneContentToolbarIconSize,
|
||||
paneContentToolbarIconButtonStyle,
|
||||
} from "@/components/ui/pane-content-toolbar";
|
||||
import { FOCUSED_PANE_PLACEMENT, useWorkspaceLayoutStore } from "@/stores/workspace-layout-store";
|
||||
import type { WorkspaceTabPlacement } from "@/stores/workspace-layout-actions";
|
||||
import type { WorkspaceTabTarget } from "@/workspace-tabs/model";
|
||||
import { buildWorkspaceTabPersistenceKey } from "@/workspace-tabs/model";
|
||||
import { buildWorkspaceExplorerStateKey } from "@/hooks/use-file-explorer-actions";
|
||||
import { isWeb } from "@/constants/platform";
|
||||
import { usePublishWorkingDiffAttachment, useWorkingDiff } from "@/git/use-working-diff";
|
||||
import { DiffTooLargeState } from "@/git/diff-too-large-state";
|
||||
@@ -163,6 +169,8 @@ interface ChangesSurfaceProps {
|
||||
focusRequestId?: number;
|
||||
onOpenFile?: (path: string) => void;
|
||||
onAddToChat?: (path: string) => void;
|
||||
state?: ChangesState;
|
||||
onStateChange?: (state: ChangesState) => void;
|
||||
}
|
||||
|
||||
type PressableStyleFn = (
|
||||
@@ -176,10 +184,10 @@ const ThemedAlignJustify = withUnistyles(AlignJustify);
|
||||
const ThemedColumns2 = withUnistyles(Columns2);
|
||||
const ThemedPilcrow = withUnistyles(Pilcrow);
|
||||
const ThemedWrapText = withUnistyles(WrapText);
|
||||
const ThemedFolderTree = withUnistyles(FolderTree);
|
||||
const ThemedListChevronsDownUp = withUnistyles(ListChevronsDownUp);
|
||||
const ThemedListChevronsUpDown = withUnistyles(ListChevronsUpDown);
|
||||
const ThemedMaximize2 = withUnistyles(Maximize2);
|
||||
const noopStateChange = () => {};
|
||||
const ThemedChevronDown = withUnistyles(ChevronDown);
|
||||
const ThemedMoreHorizontal = withUnistyles(MoreHorizontal);
|
||||
const DIFF_OPTIONS_WHITESPACE_ICON = (
|
||||
@@ -216,8 +224,8 @@ export function DiffLayoutToggle({
|
||||
onToggle,
|
||||
}: DiffLayoutToggleProps) {
|
||||
const defaultToggleStyle = useMemo(
|
||||
() => buildToggleButtonStyle(false, styles.toolbarIconButton),
|
||||
[],
|
||||
() => buildToggleButtonStyle(false, undefined, isMobile),
|
||||
[isMobile],
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
const label =
|
||||
@@ -316,39 +324,6 @@ export function DiffModeMenu({
|
||||
);
|
||||
}
|
||||
|
||||
interface DesktopTreeToggleProps {
|
||||
visible: boolean;
|
||||
toggleStyle?: PressableStyleFn;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
function DesktopTreeToggle({ visible, toggleStyle, onToggle }: DesktopTreeToggleProps) {
|
||||
const { t } = useTranslation();
|
||||
const defaultToggleStyle = useMemo(
|
||||
() => buildToggleButtonStyle(visible, styles.toolbarIconButton),
|
||||
[visible],
|
||||
);
|
||||
const label = t(visible ? "workspace.git.diff.hideTreeView" : "workspace.git.diff.showTreeView");
|
||||
return (
|
||||
<Tooltip delayDuration={300}>
|
||||
<TooltipTrigger asChild>
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={label}
|
||||
testID="changes-toggle-tree"
|
||||
style={toggleStyle ?? defaultToggleStyle}
|
||||
onPress={onToggle}
|
||||
>
|
||||
<ThemedFolderTree size={14} uniProps={foregroundMutedIconColorMapping} />
|
||||
</Pressable>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
<Text style={styles.tooltipText}>{label}</Text>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
interface ChangesToolbarProps {
|
||||
branchName: string | null;
|
||||
allFilesCollapsed: boolean;
|
||||
@@ -369,7 +344,6 @@ interface ChangesToolbarProps {
|
||||
refreshSupported: boolean;
|
||||
selectedDiffStat: { additions: number; deletions: number } | null;
|
||||
serverId: string;
|
||||
treeToggleStyle: PressableStyleFn;
|
||||
workspaceId?: string | null;
|
||||
wrapLines: boolean;
|
||||
onRefresh: () => void;
|
||||
@@ -400,14 +374,13 @@ function ChangesToolbar(props: ChangesToolbarProps) {
|
||||
isMobile,
|
||||
selectedDiffStat,
|
||||
serverId,
|
||||
treeToggleStyle,
|
||||
workspaceId,
|
||||
onSelectBase,
|
||||
onSelectUncommitted,
|
||||
onToggleDesktopTree,
|
||||
} = props;
|
||||
return (
|
||||
<View style={styles.changesToolbar} testID="changes-header">
|
||||
<PaneContentToolbar style={styles.changesToolbar} testID="changes-header">
|
||||
<View style={styles.changesToolbarIdentity}>
|
||||
<DiffModeMenu
|
||||
diffMode={diffMode}
|
||||
@@ -433,16 +406,16 @@ function ChangesToolbar(props: ChangesToolbarProps) {
|
||||
</View>
|
||||
<View style={styles.changesToolbarControls}>
|
||||
{!isMobile && hasFiles ? (
|
||||
<DesktopTreeToggle
|
||||
<TreeRailToggle
|
||||
visible={desktopTreeVisible}
|
||||
toggleStyle={treeToggleStyle}
|
||||
testID="changes-toggle-tree"
|
||||
onToggle={onToggleDesktopTree}
|
||||
/>
|
||||
) : null}
|
||||
{isMobile ? <GitActionsSplitButton gitActions={gitActions} /> : null}
|
||||
<ChangesOptionsMenu {...props} />
|
||||
</View>
|
||||
</View>
|
||||
</PaneContentToolbar>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -531,7 +504,7 @@ function ChangesOptionsMenu({
|
||||
style={overflowToggleStyle}
|
||||
>
|
||||
<ThemedMoreHorizontal
|
||||
size={isMobile ? 18 : 14}
|
||||
size={paneContentToolbarIconSize(isMobile)}
|
||||
uniProps={foregroundMutedIconColorMapping}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -606,8 +579,6 @@ function ChangesOptionsMenu({
|
||||
|
||||
const ThemedRotateCw = withUnistyles(RotateCw);
|
||||
|
||||
const EMPTY_PATH_LIST: string[] = [];
|
||||
|
||||
function computeEmptyMessage(
|
||||
hideWhitespace: boolean,
|
||||
diffMode: "uncommitted" | "base",
|
||||
@@ -793,74 +764,32 @@ function buildDiffModeTriggerStyle(): PressableStyleFn {
|
||||
];
|
||||
}
|
||||
|
||||
function buildOverflowButtonStyle(): PressableStyleFn {
|
||||
return ({ hovered, pressed }) => [
|
||||
styles.overflowButton,
|
||||
(Boolean(hovered) || pressed) && styles.toggleButtonSelected,
|
||||
];
|
||||
function buildOverflowButtonStyle(isMobile: boolean): PressableStyleFn {
|
||||
return (state) => paneContentToolbarIconButtonStyle(state, false, isMobile);
|
||||
}
|
||||
|
||||
function buildToggleButtonStyle(
|
||||
selected: boolean,
|
||||
baseStyles: StyleProp<ViewStyle> | StyleProp<ViewStyle>[],
|
||||
baseStyles?: StyleProp<ViewStyle> | StyleProp<ViewStyle>[],
|
||||
isMobile = false,
|
||||
): PressableStyleFn {
|
||||
return ({ hovered, pressed }) => [
|
||||
baseStyles,
|
||||
(selected || Boolean(hovered) || pressed) && styles.toggleButtonSelected,
|
||||
];
|
||||
}
|
||||
|
||||
function useCollapsedDiffFolders({
|
||||
workspaceId,
|
||||
cwd,
|
||||
}: {
|
||||
workspaceId?: string | null;
|
||||
cwd: string;
|
||||
}) {
|
||||
const workspaceStateKey = useMemo(
|
||||
() =>
|
||||
buildWorkspaceExplorerStateKey({
|
||||
workspaceId,
|
||||
workspaceRoot: cwd.trim(),
|
||||
}),
|
||||
[cwd, workspaceId],
|
||||
);
|
||||
const collapsedFolders = usePanelStore((state) =>
|
||||
workspaceStateKey ? state.diffCollapsedFoldersByWorkspace[workspaceStateKey] : undefined,
|
||||
);
|
||||
const setCollapsedFolders = usePanelStore((state) => state.setDiffCollapsedFoldersForWorkspace);
|
||||
const stableCollapsedFolders = collapsedFolders ?? EMPTY_PATH_LIST;
|
||||
const updateCollapsedFolders = useCallback(
|
||||
(paths: string[]) => {
|
||||
if (workspaceStateKey) {
|
||||
setCollapsedFolders(workspaceStateKey, paths);
|
||||
}
|
||||
},
|
||||
[setCollapsedFolders, workspaceStateKey],
|
||||
);
|
||||
|
||||
return {
|
||||
collapsedFolders: stableCollapsedFolders,
|
||||
updateCollapsedFolders,
|
||||
};
|
||||
return (state) => [baseStyles, paneContentToolbarIconButtonStyle(state, selected, isMobile)];
|
||||
}
|
||||
|
||||
function ChangedFilesTree({
|
||||
workspaceId,
|
||||
cwd,
|
||||
files,
|
||||
mode,
|
||||
onSelectFile,
|
||||
collapsedFolderPaths,
|
||||
onCollapsedFolderPathsChange,
|
||||
}: {
|
||||
workspaceId?: string | null;
|
||||
cwd: string;
|
||||
files: ParsedDiffFile[];
|
||||
mode: WorkingDiffMode;
|
||||
onSelectFile: (path: string) => void;
|
||||
collapsedFolderPaths: string[];
|
||||
onCollapsedFolderPathsChange: (paths: string[]) => void;
|
||||
}) {
|
||||
const [selectedPath, setSelectedPath] = useState<string | null>(null);
|
||||
const { collapsedFolders: collapsedFolderPaths, updateCollapsedFolders } =
|
||||
useCollapsedDiffFolders({ workspaceId, cwd });
|
||||
const compressedTree = useMemo(() => compressSingleChildChains(buildDiffTree(files)), [files]);
|
||||
const allFolderPaths = useMemo(() => collectDirPaths(compressedTree), [compressedTree]);
|
||||
const collapsedFolders = useMemo(() => new Set(collapsedFolderPaths), [collapsedFolderPaths]);
|
||||
@@ -881,14 +810,14 @@ function ChangedFilesTree({
|
||||
const next = collapsedFolders.has(dirPath)
|
||||
? Array.from(collapsedFolders).filter((path) => path !== dirPath)
|
||||
: [...collapsedFolders, dirPath];
|
||||
updateCollapsedFolders(next);
|
||||
onCollapsedFolderPathsChange(next);
|
||||
},
|
||||
[collapsedFolders, updateCollapsedFolders],
|
||||
[collapsedFolders, onCollapsedFolderPathsChange],
|
||||
);
|
||||
const handleCollapseFolder = useCallback(
|
||||
(dirPath: string) => {
|
||||
const prefix = `${dirPath}/`;
|
||||
updateCollapsedFolders([
|
||||
onCollapsedFolderPathsChange([
|
||||
...new Set([
|
||||
...collapsedFolders,
|
||||
...allFolderPaths.filter(
|
||||
@@ -897,7 +826,7 @@ function ChangedFilesTree({
|
||||
]),
|
||||
]);
|
||||
},
|
||||
[allFolderPaths, collapsedFolders, updateCollapsedFolders],
|
||||
[allFolderPaths, collapsedFolders, onCollapsedFolderPathsChange],
|
||||
);
|
||||
const renderItem = useCallback(
|
||||
({ item }: { item: DiffTreeRow }) => {
|
||||
@@ -979,30 +908,34 @@ function ChangedFilesTree({
|
||||
function ChangesTreeRail({
|
||||
shown,
|
||||
children,
|
||||
workspaceId,
|
||||
cwd,
|
||||
files,
|
||||
mode,
|
||||
onSelectFile,
|
||||
treeWidth,
|
||||
onTreeWidthChange,
|
||||
collapsedFolderPaths,
|
||||
onCollapsedFolderPathsChange,
|
||||
}: {
|
||||
shown: boolean;
|
||||
children: ReactElement;
|
||||
workspaceId?: string | null;
|
||||
cwd: string;
|
||||
files: ParsedDiffFile[];
|
||||
mode: WorkingDiffMode;
|
||||
onSelectFile: (path: string) => void;
|
||||
treeWidth?: number;
|
||||
onTreeWidthChange: (width: number) => void;
|
||||
collapsedFolderPaths: string[];
|
||||
onCollapsedFolderPathsChange: (paths: string[]) => void;
|
||||
}) {
|
||||
if (!shown) return children;
|
||||
return (
|
||||
<TreeRail testID="changes-tree-rail">
|
||||
<TreeRail testID="changes-tree-rail" width={treeWidth ?? 220} onWidthChange={onTreeWidthChange}>
|
||||
{children}
|
||||
<ChangedFilesTree
|
||||
workspaceId={workspaceId}
|
||||
cwd={cwd}
|
||||
files={files}
|
||||
mode={mode}
|
||||
onSelectFile={onSelectFile}
|
||||
collapsedFolderPaths={collapsedFolderPaths}
|
||||
onCollapsedFolderPathsChange={onCollapsedFolderPathsChange}
|
||||
/>
|
||||
</TreeRail>
|
||||
);
|
||||
@@ -1019,33 +952,30 @@ function useDiffTabNavigation({
|
||||
cwd: string;
|
||||
isMobile: boolean;
|
||||
}) {
|
||||
const openWorkspaceTabInFocusedPane = useWorkspaceLayoutStore(
|
||||
(state) => state.openTabInFocusedPane,
|
||||
const openTab = useWorkspaceLayoutStore((state) => state.openTab);
|
||||
const openWorkspaceTabInFocusedPane = useCallback(
|
||||
(workspaceKey: string, target: WorkspaceTabTarget, placement?: WorkspaceTabPlacement) =>
|
||||
openTab({ workspaceKey, target, intent: "reveal", placement }),
|
||||
[openTab],
|
||||
);
|
||||
const closeWorkspaceTab = useWorkspaceLayoutStore((state) => state.closeTab);
|
||||
const persistenceKey = useMemo(
|
||||
() => buildWorkspaceTabPersistenceKey({ serverId, workspaceId: workspaceId ?? cwd }),
|
||||
[cwd, serverId, workspaceId],
|
||||
);
|
||||
const changesTabId = useWorkspaceLayoutStore((state) => {
|
||||
if (!persistenceKey) {
|
||||
return null;
|
||||
}
|
||||
const layout = state.layoutByWorkspace[persistenceKey];
|
||||
return (
|
||||
layout && collectAllTabs(layout.root).find((tab) => tab.target.kind === "working_diff")?.tabId
|
||||
);
|
||||
});
|
||||
const changesTabOpen = !isMobile && Boolean(changesTabId);
|
||||
const changesTabOpen = false;
|
||||
const openChanges = useCallback(
|
||||
(path?: string) => {
|
||||
if (!persistenceKey || isMobile) {
|
||||
return;
|
||||
}
|
||||
openWorkspaceTabInFocusedPane(persistenceKey, {
|
||||
kind: "working_diff",
|
||||
...(path ? { focusPath: path, focusRequestId: Date.now() } : {}),
|
||||
});
|
||||
openWorkspaceTabInFocusedPane(
|
||||
persistenceKey,
|
||||
{
|
||||
kind: "working_diff",
|
||||
...(path ? { focusPath: path, focusRequestId: Date.now() } : {}),
|
||||
},
|
||||
FOCUSED_PANE_PLACEMENT,
|
||||
);
|
||||
},
|
||||
[isMobile, openWorkspaceTabInFocusedPane, persistenceKey],
|
||||
);
|
||||
@@ -1053,16 +983,16 @@ function useDiffTabNavigation({
|
||||
if (!persistenceKey || isMobile) {
|
||||
return;
|
||||
}
|
||||
if (changesTabId) {
|
||||
closeWorkspaceTab(persistenceKey, changesTabId);
|
||||
return;
|
||||
}
|
||||
openChanges();
|
||||
}, [changesTabId, closeWorkspaceTab, isMobile, openChanges, persistenceKey]);
|
||||
}, [isMobile, openChanges, persistenceKey]);
|
||||
const openCommit = useCallback(
|
||||
(sha: string) => {
|
||||
if (persistenceKey) {
|
||||
openWorkspaceTabInFocusedPane(persistenceKey, { kind: "commit_diff", sha });
|
||||
openWorkspaceTabInFocusedPane(
|
||||
persistenceKey,
|
||||
{ kind: "commit_diff", sha },
|
||||
FOCUSED_PANE_PLACEMENT,
|
||||
);
|
||||
}
|
||||
},
|
||||
[openWorkspaceTabInFocusedPane, persistenceKey],
|
||||
@@ -1086,27 +1016,26 @@ export function ChangesSurface({
|
||||
focusRequestId,
|
||||
onOpenFile,
|
||||
onAddToChat,
|
||||
state: changesState,
|
||||
onStateChange,
|
||||
}: ChangesSurfaceProps) {
|
||||
const { settings: appSettings } = useAppSettings();
|
||||
const { t } = useTranslation();
|
||||
const isMobile = useIsCompactFormFactor();
|
||||
const canUseSplitLayout = isWeb && !isMobile;
|
||||
const { preferences: changesPreferences, updatePreferences: updateChangesPreferences } =
|
||||
useChangesPreferences();
|
||||
const wrapLines = changesPreferences.wrapLines;
|
||||
const desktopTreeVisible = changesPreferences.desktopTreeVisible;
|
||||
const effectiveLayout = resolveDiffLayout(changesPreferences.layout, canUseSplitLayout);
|
||||
const collapsedFilesWorkspaceKey = `${serverId}:${workspaceId ?? cwd}`;
|
||||
const persistedCollapsedFilePaths = usePanelStore(
|
||||
(state) => state.collapsedFilePathsByWorkspace[collapsedFilesWorkspaceKey],
|
||||
);
|
||||
const setCollapsedFilePaths = usePanelStore((state) => state.setCollapsedFilePathsForWorkspace);
|
||||
const collapsedFilePaths = persistedCollapsedFilePaths ?? EMPTY_PATH_LIST;
|
||||
const instanceState = changesState ?? defaultChangesState;
|
||||
const updateState = onStateChange ?? noopStateChange;
|
||||
const wrapLines = instanceState.wrapLines;
|
||||
const desktopTreeVisible = instanceState.treeVisible;
|
||||
const effectiveLayout = resolveDiffLayout(instanceState.layout, canUseSplitLayout);
|
||||
const collapsedFilePaths = instanceState.collapsedFilePaths;
|
||||
const updateCollapsedFilePaths = useCallback(
|
||||
(paths: string[]) => {
|
||||
setCollapsedFilePaths(collapsedFilesWorkspaceKey, paths);
|
||||
},
|
||||
[collapsedFilesWorkspaceKey, setCollapsedFilePaths],
|
||||
(paths: string[]) => updateState({ ...instanceState, collapsedFilePaths: paths }),
|
||||
[instanceState, updateState],
|
||||
);
|
||||
const updateCollapsedFolderPaths = useCallback(
|
||||
(paths: string[]) => updateState({ ...instanceState, collapsedFolderPaths: paths }),
|
||||
[instanceState, updateState],
|
||||
);
|
||||
const collapseState = useMemo(
|
||||
() => ({ paths: collapsedFilePaths, onChange: updateCollapsedFilePaths }),
|
||||
@@ -1114,26 +1043,30 @@ export function ChangesSurface({
|
||||
);
|
||||
|
||||
const handleToggleWrapLines = useCallback(() => {
|
||||
void updateChangesPreferences({ wrapLines: !wrapLines });
|
||||
}, [updateChangesPreferences, wrapLines]);
|
||||
updateState({ ...instanceState, wrapLines: !wrapLines });
|
||||
}, [instanceState, updateState, wrapLines]);
|
||||
|
||||
const handleToggleHideWhitespace = useCallback(() => {
|
||||
void updateChangesPreferences({ hideWhitespace: !changesPreferences.hideWhitespace });
|
||||
}, [changesPreferences.hideWhitespace, updateChangesPreferences]);
|
||||
updateState({
|
||||
...instanceState,
|
||||
hideWhitespace: !instanceState.hideWhitespace,
|
||||
});
|
||||
}, [instanceState, updateState]);
|
||||
|
||||
const handleToggleLayout = useCallback(() => {
|
||||
void updateChangesPreferences({
|
||||
layout: changesPreferences.layout === "unified" ? "split" : "unified",
|
||||
updateState({
|
||||
...instanceState,
|
||||
layout: instanceState.layout === "unified" ? "split" : "unified",
|
||||
});
|
||||
}, [changesPreferences.layout, updateChangesPreferences]);
|
||||
|
||||
const codeFontSize = appSettings.codeFontSize;
|
||||
const treeToggleStyle = useMemo(
|
||||
() => buildToggleButtonStyle(desktopTreeVisible, styles.toolbarIconButton),
|
||||
[desktopTreeVisible],
|
||||
}, [instanceState, updateState]);
|
||||
const handleDiffModeChange = useCallback(
|
||||
(mode: "uncommitted" | "base") => updateState({ ...instanceState, mode }),
|
||||
[instanceState, updateState],
|
||||
);
|
||||
|
||||
const overflowToggleStyle = useMemo(() => buildOverflowButtonStyle(), []);
|
||||
const codeFontSize = appSettings.codeFontSize;
|
||||
|
||||
const overflowToggleStyle = useMemo(() => buildOverflowButtonStyle(isMobile), [isMobile]);
|
||||
|
||||
const toast = useToast();
|
||||
const isLocalDaemon = useIsLocalDaemon(serverId);
|
||||
@@ -1192,8 +1125,11 @@ export function ChangesSurface({
|
||||
serverId,
|
||||
workspaceId: workspaceId ?? undefined,
|
||||
cwd,
|
||||
ignoreWhitespace: changesPreferences.hideWhitespace,
|
||||
ignoreWhitespace: instanceState.hideWhitespace,
|
||||
enabled: enabled !== false,
|
||||
mode: instanceState.mode,
|
||||
baseRef: instanceState.baseRef,
|
||||
onModeChange: handleDiffModeChange,
|
||||
});
|
||||
usePublishWorkingDiffAttachment({
|
||||
serverId,
|
||||
@@ -1231,8 +1167,16 @@ export function ChangesSurface({
|
||||
[forgeSetupAction, forge, status?.remoteUrl, t],
|
||||
);
|
||||
const handleToggleDesktopTree = useCallback(() => {
|
||||
void updateChangesPreferences({ desktopTreeVisible: !desktopTreeVisible });
|
||||
}, [desktopTreeVisible, updateChangesPreferences]);
|
||||
updateState({ ...instanceState, treeVisible: !desktopTreeVisible });
|
||||
}, [desktopTreeVisible, instanceState, updateState]);
|
||||
const handleCommitsCollapsedChange = useCallback(
|
||||
(commitsCollapsed: boolean) => updateState({ ...instanceState, commitsCollapsed }),
|
||||
[instanceState, updateState],
|
||||
);
|
||||
const handleChangesTreeWidth = useCallback(
|
||||
(treeWidth: number) => updateState({ ...instanceState, treeWidth }),
|
||||
[instanceState, updateState],
|
||||
);
|
||||
const sharedDisplayPreferences = useMemo(
|
||||
() => ({
|
||||
layout: effectiveLayout,
|
||||
@@ -1383,16 +1327,11 @@ export function ChangesSurface({
|
||||
() => computeCommittedDiffDescription(branchLabel, baseRefLabel),
|
||||
[baseRefLabel, branchLabel],
|
||||
);
|
||||
const emptyMessage = computeEmptyMessage(
|
||||
changesPreferences.hideWhitespace,
|
||||
diffMode,
|
||||
baseRefLabel,
|
||||
{
|
||||
hiddenWhitespace: t("workspace.git.diff.emptyHiddenWhitespace"),
|
||||
uncommitted: t("workspace.git.diff.emptyUncommitted"),
|
||||
againstBase: (label) => t("workspace.git.diff.emptyAgainstBase", { baseRef: label }),
|
||||
},
|
||||
);
|
||||
const emptyMessage = computeEmptyMessage(instanceState.hideWhitespace, diffMode, baseRefLabel, {
|
||||
hiddenWhitespace: t("workspace.git.diff.emptyHiddenWhitespace"),
|
||||
uncommitted: t("workspace.git.diff.emptyUncommitted"),
|
||||
againstBase: (label) => t("workspace.git.diff.emptyAgainstBase", { baseRef: label }),
|
||||
});
|
||||
|
||||
const diffContent: ReactElement = (
|
||||
<DiffBodyContent
|
||||
@@ -1418,11 +1357,13 @@ export function ChangesSurface({
|
||||
const bodyContent = (
|
||||
<ChangesTreeRail
|
||||
shown={desktopTreeVisible && !isMobile && files.length > 0}
|
||||
workspaceId={workspaceId}
|
||||
cwd={cwd}
|
||||
files={files}
|
||||
mode={workingMode}
|
||||
onSelectFile={handleSelectTreeFile}
|
||||
treeWidth={instanceState.treeWidth}
|
||||
onTreeWidthChange={handleChangesTreeWidth}
|
||||
collapsedFolderPaths={instanceState.collapsedFolderPaths}
|
||||
onCollapsedFolderPathsChange={updateCollapsedFolderPaths}
|
||||
>
|
||||
{diffContent}
|
||||
</ChangesTreeRail>
|
||||
@@ -1447,16 +1388,15 @@ export function ChangesSurface({
|
||||
diffMode={diffMode}
|
||||
gitActions={gitActions}
|
||||
hasFiles={hasChanges}
|
||||
hideWhitespace={changesPreferences.hideWhitespace}
|
||||
hideWhitespace={instanceState.hideWhitespace}
|
||||
host={host}
|
||||
isMobile={isMobile}
|
||||
isRefreshing={isRefreshing}
|
||||
layout={changesPreferences.layout}
|
||||
layout={instanceState.layout}
|
||||
overflowToggleStyle={overflowToggleStyle}
|
||||
refreshSupported={refreshSupported}
|
||||
selectedDiffStat={selectedDiffStat}
|
||||
serverId={serverId}
|
||||
treeToggleStyle={treeToggleStyle}
|
||||
workspaceId={workspaceId}
|
||||
wrapLines={wrapLines}
|
||||
onCollapseAll={handleCollapseAllFiles}
|
||||
@@ -1482,7 +1422,13 @@ export function ChangesSurface({
|
||||
|
||||
<View style={styles.diffContainer}>{bodyContent}</View>
|
||||
|
||||
<CommitsSection serverId={serverId} cwd={cwd} onCommitPress={handleCommitPress} />
|
||||
<CommitsSection
|
||||
serverId={serverId}
|
||||
cwd={cwd}
|
||||
onCommitPress={handleCommitPress}
|
||||
collapsed={instanceState.commitsCollapsed}
|
||||
onCollapsedChange={handleCommitsCollapsedChange}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1493,16 +1439,12 @@ const styles = StyleSheet.create((theme) => ({
|
||||
minHeight: 0,
|
||||
},
|
||||
changesToolbar: {
|
||||
minHeight: WORKSPACE_SECONDARY_HEADER_HEIGHT,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: theme.spacing[2],
|
||||
paddingLeft: theme.spacing[2],
|
||||
paddingRight: theme.spacing[3],
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.colors.border,
|
||||
flexShrink: 0,
|
||||
},
|
||||
changesToolbarIdentity: {
|
||||
minWidth: 0,
|
||||
@@ -1535,12 +1477,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
diffModeTriggerHovered: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
diffModeTriggerPressed: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
diffStatusRowHovered: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
diffStatusText: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
lineHeight: theme.fontSize.sm * 1.25,
|
||||
@@ -1549,44 +1485,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
diffStatusIconHidden: {
|
||||
opacity: 0,
|
||||
},
|
||||
toggleButtonSelected: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
toolbarIconButton: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: theme.spacing[1],
|
||||
minWidth: {
|
||||
xs: 32,
|
||||
sm: 32,
|
||||
md: 24,
|
||||
},
|
||||
height: {
|
||||
xs: 32,
|
||||
sm: 32,
|
||||
md: 24,
|
||||
},
|
||||
paddingHorizontal: {
|
||||
xs: theme.spacing[2],
|
||||
sm: theme.spacing[2],
|
||||
md: theme.spacing[1],
|
||||
},
|
||||
borderRadius: theme.borderRadius.base,
|
||||
flexShrink: 0,
|
||||
},
|
||||
overflowButton: {
|
||||
width: ICON_SIZE.sm + 2 * SPACING[1],
|
||||
height: {
|
||||
xs: 32,
|
||||
sm: 32,
|
||||
md: 24,
|
||||
},
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: theme.borderRadius.base,
|
||||
flexShrink: 0,
|
||||
},
|
||||
actionErrorText: {
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingBottom: theme.spacing[1],
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import type { CheckStatus } from "./check-status";
|
||||
|
||||
/** Per-mounted PR panel state; never shared with another tab instance. */
|
||||
export function useCheckGroupState() {
|
||||
const [collapsedGroups, setCollapsedGroups] = useState<ReadonlySet<CheckStatus>>(() => new Set());
|
||||
const toggle = useCallback((status: CheckStatus) => {
|
||||
setCollapsedGroups((current) => {
|
||||
const next = new Set(current);
|
||||
if (!next.delete(status)) next.add(status);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
return { collapsedGroups, toggle };
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { Pressable, ScrollView, Text, View, type GestureResponderEvent } from "react-native";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { ChevronDown, ChevronRight, MessageSquarePlus } from "lucide-react-native";
|
||||
@@ -11,6 +11,7 @@ import { summarizeChecks, type ChecksGroup } from "./checks-summary";
|
||||
import { canAddPullRequestCheckLogsToChat } from "./context-attachment";
|
||||
import type { PrPaneCheck } from "./data";
|
||||
import { CheckStatusIcon, foregroundMutedColorMapping, sectionKitStyles } from "./section-kit";
|
||||
import { useCheckGroupState } from "./check-group-state";
|
||||
|
||||
const ThemedChevronDown = withUnistyles(ChevronDown);
|
||||
const ThemedChevronRight = withUnistyles(ChevronRight);
|
||||
@@ -67,17 +68,7 @@ export function ChecksSection({
|
||||
onAddLogsToChat,
|
||||
}: ChecksSectionProps) {
|
||||
const summary = useMemo(() => summarizeChecks(checks), [checks]);
|
||||
const [collapsedGroups, setCollapsedGroups] = useState<ReadonlySet<CheckStatus>>(() => new Set());
|
||||
|
||||
const handleToggleGroup = useCallback((status: CheckStatus) => {
|
||||
setCollapsedGroups((current) => {
|
||||
const next = new Set(current);
|
||||
if (!next.delete(status)) {
|
||||
next.add(status);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
const { collapsedGroups, toggle: handleToggleGroup } = useCheckGroupState();
|
||||
|
||||
return (
|
||||
<View testID="pr-pane-checks">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Text, View } from "react-native";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PaneContentToolbar } from "@/components/ui/pane-content-toolbar";
|
||||
import { PrActivitySkeleton, SkeletonPulse, useSkeletonPulse } from "./activity-skeleton";
|
||||
|
||||
const CHECK_ROW_KEYS = [0, 1, 2].map((i) => `pr-pane-skeleton-check-${i}`);
|
||||
@@ -16,10 +17,10 @@ export function PullRequestPaneSkeleton() {
|
||||
<SkeletonPulse pulse={pulse} style={styles.subtitle} />
|
||||
</View>
|
||||
|
||||
<View style={styles.toolbar}>
|
||||
<PaneContentToolbar style={styles.toolbar}>
|
||||
<SkeletonPulse pulse={pulse} style={styles.toolbarButton} />
|
||||
<SkeletonPulse pulse={pulse} style={styles.toolbarButton} />
|
||||
</View>
|
||||
</PaneContentToolbar>
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>{t("workspace.git.pr.sections.checks")}</Text>
|
||||
@@ -68,9 +69,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingVertical: theme.spacing[2],
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.colors.border,
|
||||
},
|
||||
toolbarButton: {
|
||||
width: 96,
|
||||
|
||||
@@ -36,7 +36,12 @@ import { useWorkspaceAttachmentsStore } from "@/attachments/workspace-attachment
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
import { useCheckoutGitActionsStore } from "@/git/actions-store";
|
||||
import { isNative } from "@/constants/platform";
|
||||
import { useIsCompactFormFactor, WORKSPACE_SECONDARY_HEADER_HEIGHT } from "@/constants/layout";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
import {
|
||||
PaneContentToolbar,
|
||||
paneContentToolbarIconSize,
|
||||
paneContentToolbarIconButtonStyle,
|
||||
} from "@/components/ui/pane-content-toolbar";
|
||||
import { ICON_SIZE, type Theme } from "@/styles/theme";
|
||||
import { getForgePresentation } from "@/git/forge";
|
||||
import { CLIENT_FORGE_VIEW_MODULES } from "@/git/forges/view";
|
||||
@@ -145,11 +150,11 @@ function kebabTriggerStyle({
|
||||
return [styles.kebabButton, hovered && styles.kebabButtonHovered];
|
||||
}
|
||||
|
||||
function refreshButtonStyle({
|
||||
hovered = false,
|
||||
pressed = false,
|
||||
}: PressableStateCallbackType & { hovered?: boolean }) {
|
||||
return [styles.refreshButton, (hovered || pressed) && styles.refreshButtonHovered];
|
||||
function refreshButtonStyle(
|
||||
{ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean },
|
||||
isCompact: boolean,
|
||||
) {
|
||||
return paneContentToolbarIconButtonStyle({ hovered, pressed }, false, isCompact);
|
||||
}
|
||||
|
||||
function renderKebabTriggerIcon({ hovered }: { hovered?: boolean }) {
|
||||
@@ -193,6 +198,11 @@ export function PullRequestPane({
|
||||
workspaceAttachmentScopeKey?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const isCompact = useIsCompactFormFactor();
|
||||
const toolbarRefreshButtonStyle = useCallback(
|
||||
(state: PressableStateCallbackType) => refreshButtonStyle(state, isCompact),
|
||||
[isCompact],
|
||||
);
|
||||
const toast = useToast();
|
||||
const daemonClient = useHostRuntimeClient(serverId);
|
||||
// COMPAT(githubCheckDetailsRpc): added in v0.1.106, remove after 2026-12-28 once
|
||||
@@ -467,7 +477,7 @@ export function PullRequestPane({
|
||||
return (
|
||||
<View style={styles.root} testID="pr-pane">
|
||||
<ScrollView style={styles.scroll} showsVerticalScrollIndicator={false}>
|
||||
<View style={styles.toolbar} testID="pr-pane-toolbar">
|
||||
<PaneContentToolbar style={styles.toolbar} testID="pr-pane-toolbar">
|
||||
<View style={styles.toolbarActions}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -489,7 +499,7 @@ export function PullRequestPane({
|
||||
: t("workspace.git.diff.refreshState", { brand: forgePresentation.brandLabel })
|
||||
}
|
||||
testID="pr-pane-refresh"
|
||||
style={refreshButtonStyle}
|
||||
style={toolbarRefreshButtonStyle}
|
||||
hitSlop={8}
|
||||
onPress={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
@@ -497,16 +507,19 @@ export function PullRequestPane({
|
||||
<View style={styles.refreshIcon}>
|
||||
{isRefreshing ? (
|
||||
<ThemedLoadingSpinner
|
||||
size={ICON_SIZE.sm}
|
||||
size={paneContentToolbarIconSize(isCompact)}
|
||||
uniProps={foregroundMutedColorMapping}
|
||||
/>
|
||||
) : (
|
||||
<ThemedRotateCw size={ICON_SIZE.sm} uniProps={foregroundMutedColorMapping} />
|
||||
<ThemedRotateCw
|
||||
size={paneContentToolbarIconSize(isCompact)}
|
||||
uniProps={foregroundMutedColorMapping}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
</PaneContentToolbar>
|
||||
|
||||
<Pressable onPress={handleOpenPrUrl} style={styles.header}>
|
||||
{({ hovered }) => (
|
||||
@@ -1239,15 +1252,10 @@ const styles = StyleSheet.create((theme) => ({
|
||||
backgroundColor: theme.colors.border,
|
||||
},
|
||||
toolbar: {
|
||||
height: WORKSPACE_SECONDARY_HEADER_HEIGHT,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.colors.border,
|
||||
paddingTop: theme.spacing[2],
|
||||
paddingRight: theme.spacing[3],
|
||||
paddingBottom: theme.spacing[2],
|
||||
paddingLeft: theme.spacing[3],
|
||||
},
|
||||
toolbarActions: {
|
||||
@@ -1263,17 +1271,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
paddingHorizontal: theme.spacing[1],
|
||||
borderRadius: theme.borderRadius.base,
|
||||
},
|
||||
refreshButton: {
|
||||
marginLeft: "auto",
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: theme.borderRadius.md,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
refreshButtonHovered: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
refreshIcon: {
|
||||
width: ICON_SIZE.md,
|
||||
height: ICON_SIZE.md,
|
||||
|
||||
@@ -5,11 +5,8 @@ import {
|
||||
} from "@/attachments/workspace-attachments-store";
|
||||
import {
|
||||
buildReviewDraftKey,
|
||||
buildReviewDraftScopeKey,
|
||||
useInlineReviewController,
|
||||
useResolvedDiffMode,
|
||||
useReviewAttachmentSnapshot,
|
||||
useSetDiffModeOverride,
|
||||
} from "@/review";
|
||||
import { useCheckoutDiffQuery } from "@/git/use-diff-query";
|
||||
import { useCheckoutStatusQuery } from "@/git/use-status-query";
|
||||
@@ -21,6 +18,9 @@ interface UseWorkingDiffOptions {
|
||||
ignoreWhitespace: boolean;
|
||||
enabled: boolean;
|
||||
queryScope?: string;
|
||||
mode?: "uncommitted" | "base";
|
||||
baseRef?: string;
|
||||
onModeChange?: (mode: "uncommitted" | "base") => void;
|
||||
}
|
||||
|
||||
export function useWorkingDiff({
|
||||
@@ -30,6 +30,9 @@ export function useWorkingDiff({
|
||||
ignoreWhitespace,
|
||||
enabled,
|
||||
queryScope,
|
||||
mode,
|
||||
baseRef: selectedBaseRef,
|
||||
onModeChange,
|
||||
}: UseWorkingDiffOptions) {
|
||||
const {
|
||||
status,
|
||||
@@ -43,38 +46,26 @@ export function useWorkingDiff({
|
||||
const statusErrorMessage =
|
||||
status?.error?.message ??
|
||||
(isStatusError && statusError instanceof Error ? statusError.message : null);
|
||||
const baseRef = gitStatus?.baseRef ?? undefined;
|
||||
const baseRef = selectedBaseRef ?? gitStatus?.baseRef ?? undefined;
|
||||
const hasUncommittedChanges = Boolean(gitStatus?.isDirty);
|
||||
const currentBranchName =
|
||||
gitStatus?.currentBranch && gitStatus.currentBranch !== "HEAD" ? gitStatus.currentBranch : null;
|
||||
|
||||
const reviewDraftScopeKey = useMemo(
|
||||
() =>
|
||||
buildReviewDraftScopeKey({
|
||||
serverId,
|
||||
workspaceId,
|
||||
cwd,
|
||||
baseRef,
|
||||
ignoreWhitespace,
|
||||
}),
|
||||
[baseRef, cwd, ignoreWhitespace, serverId, workspaceId],
|
||||
);
|
||||
const diffMode = useResolvedDiffMode({
|
||||
scopeKey: reviewDraftScopeKey,
|
||||
hasUncommittedChanges,
|
||||
});
|
||||
const setDiffModeOverride = useSetDiffModeOverride();
|
||||
const selectDiffMode = useCallback(
|
||||
(mode: "uncommitted" | "base") => {
|
||||
setDiffModeOverride({
|
||||
scopeKey: reviewDraftScopeKey,
|
||||
override: { serverId, cwd, mode, isDirtyAtSelection: hasUncommittedChanges },
|
||||
});
|
||||
(nextMode: "uncommitted" | "base") => {
|
||||
if (onModeChange) {
|
||||
onModeChange(nextMode);
|
||||
return;
|
||||
}
|
||||
// A Changes surface always supplies its tab-owned presentation. Keep this
|
||||
// fallback deterministic for legacy non-tab consumers without mutating
|
||||
// workspace-wide review preferences.
|
||||
},
|
||||
[cwd, hasUncommittedChanges, reviewDraftScopeKey, serverId, setDiffModeOverride],
|
||||
[onModeChange],
|
||||
);
|
||||
const selectUncommitted = useCallback(() => selectDiffMode("uncommitted"), [selectDiffMode]);
|
||||
const selectBase = useCallback(() => selectDiffMode("base"), [selectDiffMode]);
|
||||
const diffMode = mode ?? (hasUncommittedChanges ? "uncommitted" : "base");
|
||||
|
||||
const {
|
||||
files,
|
||||
|
||||
Reference in New Issue
Block a user