Make the workspace right pane a first-class tab host (#3287)

* feat(app): add persistent workspace side panes

Keep the common review and file-browsing workflow beside the active agent without tearing down pane contents. Workspace tabs now share one compact chip language, assistant file links route into the retained right pane, and diff/file trees share stable geometry.

* fix(app): localize pull request empty state

* test(app): update explorer pane coverage

* fix(app): preserve explorer pane workflows

* test(app): make explorer tab selection idempotent

* fix(app): persist workspace review pins

* fix(app): preserve sidebar scrim drag hook

* fix(app): restore companion pane interactions

* test(app): follow selected sidebar surface
This commit is contained in:
Mohamed Boudra
2026-08-16 18:18:32 +02:00
committed by GitHub
parent 6dcf6a4b08
commit ab274d635e
81 changed files with 2900 additions and 1051 deletions
+139 -8
View File
@@ -12,6 +12,7 @@ import {
import { useTranslation } from "react-i18next";
import type { TFunction } from "i18next";
import { DiffStat } from "@/components/diff-stat";
import { TreeRail } from "@/components/tree-rail";
import {
View,
Text,
@@ -1575,6 +1576,7 @@ interface GitDiffPaneProps {
workspaceId?: string | null;
cwd: string;
enabled?: boolean;
host: "explorer" | "panel";
onOpenFile?: (path: string) => void;
onAddToChat?: (path: string) => void;
}
@@ -1660,6 +1662,29 @@ interface ChangesTabToggleProps {
onPress: () => void;
}
function resolveChangesTabOpen(host: "explorer" | "panel", changesTabOpen: boolean): boolean {
return host === "explorer" ? changesTabOpen : false;
}
function resolveChangesFilePress(
host: "explorer" | "panel",
onChangesFilePress: ((path?: string) => void) | undefined,
): ((path?: string) => void) | undefined {
return host === "explorer" ? onChangesFilePress : undefined;
}
function ChangesTabToggleForHost({
host,
isMobile,
selected,
onPress,
}: ChangesTabToggleProps & { host: "explorer" | "panel" }) {
if (host === "panel") {
return null;
}
return <ChangesTabToggle isMobile={isMobile} selected={selected} onPress={onPress} />;
}
interface DiffModeMenuProps {
diffMode: "uncommitted" | "base";
committedDescription?: string;
@@ -1749,17 +1774,21 @@ function ChangesTabToggle({ isMobile, selected, onPress }: ChangesTabToggleProps
interface DiffViewModeToggleProps {
viewMode: "flat" | "tree";
isMobile: boolean;
toggleStyle: PressableStyleFn;
toggleStyle?: PressableStyleFn;
onToggle: () => void;
}
function DiffViewModeToggle({
export function DiffViewModeToggle({
viewMode,
isMobile,
toggleStyle,
onToggle,
}: DiffViewModeToggleProps) {
const { t } = useTranslation();
const defaultToggleStyle = useMemo(
() => buildToggleButtonStyle(viewMode === "tree", styles.expandAllButton),
[viewMode],
);
const label =
viewMode === "flat"
? t("workspace.git.diff.showTreeView")
@@ -1771,7 +1800,7 @@ function DiffViewModeToggle({
accessibilityRole="button"
accessibilityLabel={label}
testID="changes-toggle-view-mode"
style={toggleStyle}
style={toggleStyle ?? defaultToggleStyle}
onPress={onToggle}
>
{viewMode === "flat" ? (
@@ -2140,6 +2169,8 @@ interface SharedDiffViewProps {
};
}
type WorkingTreeDiffMode = Extract<SharedDiffViewProps["mode"], { kind: "working_tree" }>;
export function SharedDiffView({ files, displayPreferences, mode }: SharedDiffViewProps) {
const isCompact = useIsCompactFormFactor();
const { layout, wrapLines, codeFontSize, monoFontFamily } = displayPreferences;
@@ -2929,6 +2960,81 @@ function useChangesTreeState({
};
}
export function ChangedFilesTree({
workspaceId,
cwd,
files,
displayPreferences,
workingTreeMode,
}: {
workspaceId?: string | null;
cwd: string;
files: ParsedDiffFile[];
displayPreferences: SharedDiffViewProps["displayPreferences"];
workingTreeMode: WorkingTreeDiffMode;
}) {
const treeState = useChangesTreeState({
workspaceId,
cwd,
files,
viewMode: "tree",
changesTabOpen: false,
onViewModeChange: () => {},
});
const mode = useMemo(
() => ({
...workingTreeMode,
viewMode: "tree" as const,
expandedPaths: EMPTY_PATH_LIST,
collapsedFolders: treeState.collapsedFolders,
onExpandedPathsChange: () => {},
onCollapsedFoldersChange: treeState.updateCollapsedFolders,
}),
[treeState.collapsedFolders, treeState.updateCollapsedFolders, workingTreeMode],
);
return <SharedDiffView files={files} displayPreferences={displayPreferences} mode={mode} />;
}
function GitDiffTreeRail({
shown,
children,
workspaceId,
cwd,
files,
displayPreferences,
workingTreeMode,
}: {
shown: boolean;
children: ReactElement;
workspaceId?: string | null;
cwd: string;
files: ParsedDiffFile[];
displayPreferences: SharedDiffViewProps["displayPreferences"];
workingTreeMode: WorkingTreeDiffMode;
}) {
if (!shown) return children;
return (
<TreeRail testID="changes-tree-rail">
{children}
<ChangedFilesTree
workspaceId={workspaceId}
cwd={cwd}
files={files}
displayPreferences={displayPreferences}
workingTreeMode={workingTreeMode}
/>
</TreeRail>
);
}
function resolveWorkingTreeMode<T>(isMobile: boolean, mobileMode: T, desktopMode: T): T {
return isMobile ? mobileMode : desktopMode;
}
function shouldShowTreeRail(viewMode: "flat" | "tree", isMobile: boolean): boolean {
return viewMode === "tree" && !isMobile;
}
function useDiffTabNavigation({
serverId,
workspaceId,
@@ -3000,6 +3106,7 @@ export function GitDiffPane({
workspaceId,
cwd,
enabled,
host,
onOpenFile,
onAddToChat,
}: GitDiffPaneProps) {
@@ -3049,11 +3156,13 @@ export function GitDiffPane({
});
const fileManagerTarget = desktopOpenTargets.find((target) => target.kind === "file-manager");
const {
changesTabOpen,
changesTabOpen: workspaceChangesTabOpen,
toggleChanges: handleToggleChangesTab,
openCommit: handleCommitPress,
onChangesFilePress,
onChangesFilePress: workspaceOnChangesFilePress,
} = useDiffTabNavigation({ serverId, workspaceId, cwd, isMobile });
const changesTabOpen = resolveChangesTabOpen(host, workspaceChangesTabOpen);
const onChangesFilePress = resolveChangesFilePress(host, workspaceOnChangesFilePress);
const refreshSupported = useSessionStore(
(s) => s.sessions[serverId]?.serverInfo?.features?.checkoutRefresh === true,
);
@@ -3283,7 +3392,16 @@ export function GitDiffPane({
},
);
const bodyContent: ReactElement = (
const flatWorkingTreeMode = useMemo(
() => ({ ...workingTreeMode, viewMode: "flat" as const }),
[workingTreeMode],
);
const primaryWorkingTreeMode = resolveWorkingTreeMode(
isMobile,
workingTreeMode,
flatWorkingTreeMode,
);
const diffContent: ReactElement = (
<DiffBodyContent
isStatusLoading={isStatusLoading}
statusErrorMessage={statusErrorMessage}
@@ -3299,10 +3417,22 @@ export function GitDiffPane({
<SharedDiffView
files={files}
displayPreferences={sharedDisplayPreferences}
mode={workingTreeMode}
mode={primaryWorkingTreeMode}
/>
</DiffBodyContent>
);
const bodyContent = (
<GitDiffTreeRail
shown={shouldShowTreeRail(viewMode, isMobile)}
workspaceId={workspaceId}
cwd={cwd}
files={files}
displayPreferences={sharedDisplayPreferences}
workingTreeMode={workingTreeMode}
>
{diffContent}
</GitDiffTreeRail>
);
return (
<View
@@ -3335,7 +3465,8 @@ export function GitDiffPane({
onSelectBase={handleSelectBase}
/>
<View style={styles.diffStatusButtons}>
<ChangesTabToggle
<ChangesTabToggleForHost
host={host}
isMobile={isMobile}
selected={changesTabOpen}
onPress={handleToggleChangesTab}