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
+16 -10
View File
@@ -31,9 +31,9 @@ export const MIN_EXPLORER_SIDEBAR_WIDTH = 280;
// Upper bound is intentionally generous; desktop resizing enforces a min-chat-width constraint.
export const MAX_EXPLORER_SIDEBAR_WIDTH = 2000;
export const DEFAULT_EXPLORER_FILES_SPLIT_RATIO = 0.38;
export const MIN_EXPLORER_FILES_SPLIT_RATIO = 0.2;
export const MAX_EXPLORER_FILES_SPLIT_RATIO = 0.8;
export const DEFAULT_TREE_RAIL_WIDTH = 320;
export const MIN_TREE_RAIL_WIDTH = 200;
export const MAX_TREE_RAIL_WIDTH = 600;
export interface PanelVisibilityState {
isAgentListOpen: boolean;
@@ -70,8 +70,8 @@ export function clampExplorerWidth(width: number): number {
return clampNumber(width, MIN_EXPLORER_SIDEBAR_WIDTH, MAX_EXPLORER_SIDEBAR_WIDTH);
}
export function clampExplorerFilesSplitRatio(ratio: number): number {
return clampNumber(ratio, MIN_EXPLORER_FILES_SPLIT_RATIO, MAX_EXPLORER_FILES_SPLIT_RATIO);
export function clampTreeRailWidth(width: number): number {
return clampNumber(width, MIN_TREE_RAIL_WIDTH, MAX_TREE_RAIL_WIDTH);
}
export function selectPanelVisibility(
@@ -190,6 +190,7 @@ export const PanelPersistedStateSchema = z.strictObject({
explorerSortOption: z.enum(["name", "modified", "size"]).optional(),
explorerShowHiddenFiles: z.boolean().optional(),
explorerFilesSplitRatio: z.number().optional(),
treeRailWidth: z.number().optional(),
});
type MigratablePanelState = z.infer<typeof PanelPersistedStateSchema>;
@@ -198,11 +199,6 @@ function migratePanelV2Explorer(state: MigratablePanelState, isWeb: boolean): vo
if (isWeb && typeof state.explorerWidth === "number" && state.explorerWidth === 400) {
state.explorerWidth = DEFAULT_EXPLORER_SIDEBAR_WIDTH;
}
if (typeof state.explorerFilesSplitRatio !== "number") {
state.explorerFilesSplitRatio = DEFAULT_EXPLORER_FILES_SPLIT_RATIO;
} else {
state.explorerFilesSplitRatio = clampExplorerFilesSplitRatio(state.explorerFilesSplitRatio);
}
}
function migratePanelV3Explorer(state: MigratablePanelState, isWeb: boolean): void {
@@ -253,6 +249,15 @@ function migratePanelDesktopFocusMode(state: MigratablePanelState): void {
}
}
function migrateTreeRailWidth(state: MigratablePanelState, version: number): void {
if (version < 13 || typeof state.treeRailWidth !== "number") {
delete state.explorerFilesSplitRatio;
state.treeRailWidth = DEFAULT_TREE_RAIL_WIDTH;
return;
}
state.treeRailWidth = clampTreeRailWidth(state.treeRailWidth);
}
export function migratePanelState(
persistedState: unknown,
version: number,
@@ -302,6 +307,7 @@ export function migratePanelState(
if (typeof state.explorerShowHiddenFiles !== "boolean") {
state.explorerShowHiddenFiles = true;
}
migrateTreeRailWidth(state, version);
if (version < 12) {
// Compact panel position is transient UI state. Cold starts always begin
// at content, regardless of what an older version persisted.