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:
@@ -11,16 +11,16 @@ import { type ExplorerCheckoutContext } from "../explorer-checkout-context";
|
||||
import {
|
||||
buildOpenFileExplorerPatch,
|
||||
buildToggleFileExplorerPatch,
|
||||
clampExplorerFilesSplitRatio,
|
||||
clampTreeRailWidth,
|
||||
clampExplorerWidth,
|
||||
clampSidebarWidth,
|
||||
DEFAULT_EXPLORER_FILES_SPLIT_RATIO,
|
||||
DEFAULT_TREE_RAIL_WIDTH,
|
||||
DEFAULT_EXPLORER_SIDEBAR_WIDTH,
|
||||
DEFAULT_SIDEBAR_WIDTH,
|
||||
MAX_EXPLORER_FILES_SPLIT_RATIO,
|
||||
MAX_TREE_RAIL_WIDTH,
|
||||
MAX_EXPLORER_SIDEBAR_WIDTH,
|
||||
MAX_SIDEBAR_WIDTH,
|
||||
MIN_EXPLORER_FILES_SPLIT_RATIO,
|
||||
MIN_TREE_RAIL_WIDTH,
|
||||
MIN_EXPLORER_SIDEBAR_WIDTH,
|
||||
MIN_SIDEBAR_WIDTH,
|
||||
migratePanelState,
|
||||
@@ -51,15 +51,16 @@ export type {
|
||||
SortOption,
|
||||
} from "./state";
|
||||
export {
|
||||
DEFAULT_EXPLORER_FILES_SPLIT_RATIO,
|
||||
DEFAULT_TREE_RAIL_WIDTH,
|
||||
DEFAULT_EXPLORER_SIDEBAR_WIDTH,
|
||||
DEFAULT_SIDEBAR_WIDTH,
|
||||
MAX_EXPLORER_FILES_SPLIT_RATIO,
|
||||
MAX_TREE_RAIL_WIDTH,
|
||||
MAX_EXPLORER_SIDEBAR_WIDTH,
|
||||
MAX_SIDEBAR_WIDTH,
|
||||
MIN_EXPLORER_FILES_SPLIT_RATIO,
|
||||
MIN_TREE_RAIL_WIDTH,
|
||||
MIN_EXPLORER_SIDEBAR_WIDTH,
|
||||
MIN_SIDEBAR_WIDTH,
|
||||
clampTreeRailWidth,
|
||||
selectIsAgentListOpen,
|
||||
selectIsFileExplorerOpen,
|
||||
selectPanelVisibility,
|
||||
@@ -88,7 +89,7 @@ export interface PanelState {
|
||||
explorerWidth: number;
|
||||
explorerSortOption: SortOption;
|
||||
explorerShowHiddenFiles: boolean;
|
||||
explorerFilesSplitRatio: number;
|
||||
treeRailWidth: number;
|
||||
|
||||
// Actions
|
||||
toggleFocusMode: () => void;
|
||||
@@ -117,7 +118,7 @@ export interface PanelState {
|
||||
setExplorerWidth: (width: number) => void;
|
||||
setExplorerSortOption: (option: SortOption) => void;
|
||||
toggleExplorerShowHiddenFiles: () => void;
|
||||
setExplorerFilesSplitRatio: (ratio: number) => void;
|
||||
setTreeRailWidth: (width: number) => void;
|
||||
}
|
||||
|
||||
const DEFAULT_DESKTOP_OPEN = isWeb;
|
||||
@@ -153,7 +154,7 @@ export const usePanelStore = create<PanelState>()(
|
||||
explorerWidth: DEFAULT_EXPLORER_SIDEBAR_WIDTH,
|
||||
explorerSortOption: "name",
|
||||
explorerShowHiddenFiles: true,
|
||||
explorerFilesSplitRatio: DEFAULT_EXPLORER_FILES_SPLIT_RATIO,
|
||||
treeRailWidth: DEFAULT_TREE_RAIL_WIDTH,
|
||||
|
||||
toggleFocusMode: () =>
|
||||
set((state) => ({
|
||||
@@ -303,16 +304,11 @@ export const usePanelStore = create<PanelState>()(
|
||||
setExplorerSortOption: (option) => set({ explorerSortOption: option }),
|
||||
toggleExplorerShowHiddenFiles: () =>
|
||||
set((state) => ({ explorerShowHiddenFiles: !state.explorerShowHiddenFiles })),
|
||||
setExplorerFilesSplitRatio: (ratio) =>
|
||||
set({
|
||||
explorerFilesSplitRatio: Number.isFinite(ratio)
|
||||
? clampExplorerFilesSplitRatio(ratio)
|
||||
: DEFAULT_EXPLORER_FILES_SPLIT_RATIO,
|
||||
}),
|
||||
setTreeRailWidth: (width) => set({ treeRailWidth: clampTreeRailWidth(width) }),
|
||||
}),
|
||||
{
|
||||
name: "panel-state",
|
||||
version: 12,
|
||||
version: 13,
|
||||
storage: createValidatedPersistStorage(AsyncStorage, PanelPersistedStateSchema),
|
||||
migrate: (persistedState, version) => migratePanelState(persistedState, version, { isWeb }),
|
||||
partialize: (state) => ({
|
||||
@@ -326,7 +322,7 @@ export const usePanelStore = create<PanelState>()(
|
||||
explorerWidth: state.explorerWidth,
|
||||
explorerSortOption: state.explorerSortOption,
|
||||
explorerShowHiddenFiles: state.explorerShowHiddenFiles,
|
||||
explorerFilesSplitRatio: state.explorerFilesSplitRatio,
|
||||
treeRailWidth: state.treeRailWidth,
|
||||
}),
|
||||
},
|
||||
),
|
||||
@@ -356,7 +352,7 @@ export function usePanelState(isMobile: boolean) {
|
||||
const explorerTabByCheckout = usePanelStore((state) => state.explorerTabByCheckout);
|
||||
const explorerWidth = usePanelStore((state) => state.explorerWidth);
|
||||
const explorerSortOption = usePanelStore((state) => state.explorerSortOption);
|
||||
const explorerFilesSplitRatio = usePanelStore((state) => state.explorerFilesSplitRatio);
|
||||
const treeRailWidth = usePanelStore((state) => state.treeRailWidth);
|
||||
const setExplorerTab = usePanelStore((state) => state.setExplorerTab);
|
||||
const setExplorerTabForCheckout = usePanelStore((state) => state.setExplorerTabForCheckout);
|
||||
const activateExplorerTabForCheckout = usePanelStore(
|
||||
@@ -364,7 +360,7 @@ export function usePanelState(isMobile: boolean) {
|
||||
);
|
||||
const setExplorerWidth = usePanelStore((state) => state.setExplorerWidth);
|
||||
const setExplorerSortOption = usePanelStore((state) => state.setExplorerSortOption);
|
||||
const setExplorerFilesSplitRatio = usePanelStore((state) => state.setExplorerFilesSplitRatio);
|
||||
const setTreeRailWidth = usePanelStore((state) => state.setTreeRailWidth);
|
||||
|
||||
return {
|
||||
isAgentListOpen,
|
||||
@@ -377,12 +373,12 @@ export function usePanelState(isMobile: boolean) {
|
||||
explorerTabByCheckout,
|
||||
explorerWidth,
|
||||
explorerSortOption,
|
||||
explorerFilesSplitRatio,
|
||||
treeRailWidth,
|
||||
setExplorerTab,
|
||||
setExplorerTabForCheckout,
|
||||
activateExplorerTabForCheckout,
|
||||
setExplorerWidth,
|
||||
setExplorerSortOption,
|
||||
setExplorerFilesSplitRatio,
|
||||
setTreeRailWidth,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import {
|
||||
buildOpenFileExplorerPatch,
|
||||
buildToggleFileExplorerPatch,
|
||||
DEFAULT_TREE_RAIL_WIDTH,
|
||||
migratePanelState,
|
||||
selectIsAgentListOpen,
|
||||
selectIsFileExplorerOpen,
|
||||
@@ -119,6 +120,13 @@ describe("panel-store migration", () => {
|
||||
expect(state.diffCollapsedFoldersByWorkspace).toEqual({ ws: ["src/app"] });
|
||||
});
|
||||
|
||||
it("replaces the dead explorer split ratio with the shared tree rail width", () => {
|
||||
const state = migratePanelState({ explorerFilesSplitRatio: 0.5 }, 12, { isWeb: false });
|
||||
|
||||
expect(state.explorerFilesSplitRatio).toBeUndefined();
|
||||
expect(state.treeRailWidth).toBe(DEFAULT_TREE_RAIL_WIDTH);
|
||||
});
|
||||
|
||||
it("drops persisted compact panel state so cold starts return to content", () => {
|
||||
const state = migratePanelState(
|
||||
{ mobileView: "agent-list", mobilePanel: { target: "file-explorer", revision: 42 } },
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface SplitPane {
|
||||
id: string;
|
||||
tabIds: string[];
|
||||
focusedTabId: string | null;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export interface SplitGroup {
|
||||
@@ -240,6 +241,7 @@ function createPaneNode(input: {
|
||||
id: string;
|
||||
tabs?: WorkspaceTab[];
|
||||
focusedTabId?: string | null;
|
||||
hidden?: boolean;
|
||||
}): SplitNodeInternal {
|
||||
const normalizedTabs = normalizeWorkspaceTabs(input.tabs ?? []);
|
||||
const tabIds = normalizedTabs.map((tab) => tab.tabId);
|
||||
@@ -254,6 +256,7 @@ function createPaneNode(input: {
|
||||
tabs: normalizedTabs,
|
||||
tabIds,
|
||||
focusedTabId,
|
||||
...(input.hidden === true ? { hidden: true } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -511,7 +514,7 @@ function insertChildIntoGroup(
|
||||
|
||||
function listPaneIds(node: SplitNodeInternal): string[] {
|
||||
if (node.kind === "pane") {
|
||||
return [node.pane.id];
|
||||
return node.pane.hidden === true ? [] : [node.pane.id];
|
||||
}
|
||||
const next: string[] = [];
|
||||
for (const child of node.group.children) {
|
||||
@@ -562,6 +565,7 @@ function normalizePaneAfterTabChange(pane: SplitPaneInternal): SplitPaneInternal
|
||||
tabs,
|
||||
tabIds,
|
||||
focusedTabId,
|
||||
...(pane.hidden === true ? { hidden: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -584,6 +588,7 @@ function normalizePaneNode(rawPane: SplitPaneInternal | undefined): SplitNodeInt
|
||||
id: paneId,
|
||||
tabs: mergedTabs,
|
||||
focusedTabId: trimNonEmpty(rawPane?.focusedTabId) ?? null,
|
||||
hidden: rawPane?.hidden === true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -756,6 +761,7 @@ function focusTabInPane(root: SplitNodeInternal, paneId: string, tabId: string):
|
||||
pane: normalizePaneAfterTabChange({
|
||||
...node.pane,
|
||||
focusedTabId: tabId,
|
||||
hidden: undefined,
|
||||
}),
|
||||
};
|
||||
});
|
||||
@@ -972,7 +978,7 @@ export function collectAllTabs(root: SplitNode): WorkspaceTab[] {
|
||||
export function collectAllPanes(root: SplitNode): SplitPane[] {
|
||||
const internalRoot = asInternalNode(root);
|
||||
if (internalRoot.kind === "pane") {
|
||||
return [internalRoot.pane];
|
||||
return internalRoot.pane.hidden === true ? [] : [internalRoot.pane];
|
||||
}
|
||||
return internalRoot.group.children.flatMap((child) => collectAllPanes(child));
|
||||
}
|
||||
@@ -995,6 +1001,7 @@ function stripEphemeralTabsFromNode(node: SplitNodeInternal): SplitNodeInternal
|
||||
id: node.pane.id,
|
||||
tabs: nextTabs,
|
||||
focusedTabId: node.pane.focusedTabId,
|
||||
hidden: node.pane.hidden,
|
||||
});
|
||||
}
|
||||
return createGroupNode({
|
||||
@@ -1028,7 +1035,7 @@ export function getFocusedBrowserId(layout: WorkspaceLayout | null | undefined):
|
||||
return null;
|
||||
}
|
||||
const focusedPane = findPaneById(layout.root, layout.focusedPaneId);
|
||||
if (!focusedPane?.focusedTabId) {
|
||||
if (!focusedPane?.focusedTabId || focusedPane.hidden === true) {
|
||||
return null;
|
||||
}
|
||||
const focusedTab = collectAllTabs(layout.root).find(
|
||||
@@ -1221,7 +1228,11 @@ export function focusTabInLayout(input: FocusTabInLayoutInput): WorkspaceLayout
|
||||
return null;
|
||||
}
|
||||
|
||||
if (pane.focusedTabId === input.tabId && layout.focusedPaneId === pane.id) {
|
||||
if (
|
||||
pane.focusedTabId === input.tabId &&
|
||||
layout.focusedPaneId === pane.id &&
|
||||
pane.hidden !== true
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1448,7 +1459,8 @@ export function splitPaneEmptyInLayout(
|
||||
export function moveTabToPaneInLayout(input: MoveTabToPaneInLayoutInput): WorkspaceLayout | null {
|
||||
const layout = asInternalLayout(input.layout);
|
||||
const sourcePane = findPaneContainingTab(layout.root, input.tabId);
|
||||
if (!sourcePane || !findPaneById(layout.root, input.toPaneId)) {
|
||||
const targetPane = findPaneById(layout.root, input.toPaneId);
|
||||
if (!sourcePane || !targetPane || targetPane.hidden === true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1472,19 +1484,59 @@ export function moveTabToPaneInLayout(input: MoveTabToPaneInLayoutInput): Worksp
|
||||
}
|
||||
|
||||
export function focusPaneInLayout(input: FocusPaneInLayoutInput): WorkspaceLayout | null {
|
||||
if (!findPaneById(input.layout.root, input.paneId)) {
|
||||
const pane = findPaneById(input.layout.root, input.paneId);
|
||||
if (!pane) {
|
||||
return null;
|
||||
}
|
||||
if (input.layout.focusedPaneId === input.paneId) {
|
||||
if (input.layout.focusedPaneId === input.paneId && pane.hidden !== true) {
|
||||
return null;
|
||||
}
|
||||
return withNormalizedParentTabMap({
|
||||
root: input.layout.root,
|
||||
root:
|
||||
pane.hidden === true
|
||||
? updatePaneInTree(asInternalNode(input.layout.root), {
|
||||
paneId: input.paneId,
|
||||
updater: (currentPane) => ({ ...currentPane, hidden: undefined }),
|
||||
})
|
||||
: input.layout.root,
|
||||
focusedPaneId: input.paneId,
|
||||
parentTabIdByTabId: input.layout.parentTabIdByTabId,
|
||||
});
|
||||
}
|
||||
|
||||
export function setPaneHiddenInLayout(input: {
|
||||
layout: WorkspaceLayout;
|
||||
paneId: string;
|
||||
hidden: boolean;
|
||||
}): WorkspaceLayout | null {
|
||||
const pane = findPaneById(input.layout.root, input.paneId);
|
||||
const isHidden = pane?.hidden === true;
|
||||
if (!pane || isHidden === input.hidden) {
|
||||
return null;
|
||||
}
|
||||
if (input.hidden && collectAllPanes(input.layout.root).length === 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const root = updatePaneInTree(asInternalNode(input.layout.root), {
|
||||
paneId: input.paneId,
|
||||
updater: (currentPane) => ({
|
||||
...currentPane,
|
||||
hidden: input.hidden ? true : undefined,
|
||||
}),
|
||||
});
|
||||
const focusedPaneId =
|
||||
input.hidden && input.layout.focusedPaneId === input.paneId
|
||||
? (collectAllPanes(root)[0]?.id ?? null)
|
||||
: input.layout.focusedPaneId;
|
||||
|
||||
return withNormalizedParentTabMap({
|
||||
root,
|
||||
focusedPaneId,
|
||||
parentTabIdByTabId: input.layout.parentTabIdByTabId,
|
||||
});
|
||||
}
|
||||
|
||||
export function resizeSplitInLayout(input: ResizeSplitInLayoutInput): WorkspaceLayout {
|
||||
const layout = asInternalLayout(input.layout);
|
||||
return withNormalizedParentTabMap({
|
||||
|
||||
@@ -84,6 +84,7 @@ function createPane(input: {
|
||||
id: string;
|
||||
tabIds: string[];
|
||||
focusedTabId?: string | null;
|
||||
hidden?: boolean;
|
||||
targetsByTabId?: Record<string, WorkspaceTab["target"]>;
|
||||
}): SplitNode {
|
||||
const tabs = input.tabIds.map((tabId) => createTab(tabId, input.targetsByTabId?.[tabId]));
|
||||
@@ -93,6 +94,7 @@ function createPane(input: {
|
||||
id: input.id,
|
||||
tabIds: input.tabIds,
|
||||
focusedTabId: input.focusedTabId ?? input.tabIds[input.tabIds.length - 1] ?? null,
|
||||
...(input.hidden === true ? { hidden: true } : {}),
|
||||
tabs,
|
||||
} as SplitPane,
|
||||
};
|
||||
@@ -200,6 +202,14 @@ describe("workspace-layout-store helpers", () => {
|
||||
|
||||
expect(getFocusedBrowserId({ root, focusedPaneId: "main" })).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps tabs in hidden panes while excluding those panes from focus helpers", () => {
|
||||
const root = createPane({ id: "hidden", tabIds: ["tab-a"], hidden: true });
|
||||
|
||||
expect(collectAllTabs(root).map((tab) => tab.tabId)).toEqual(["tab-a"]);
|
||||
expect(collectAllPanes(root)).toEqual([]);
|
||||
expect(getFocusedBrowserId({ root, focusedPaneId: "hidden" })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("workspace-layout-store tree transforms", () => {
|
||||
@@ -314,6 +324,8 @@ describe("workspace-layout-store actions", () => {
|
||||
pinnedAgentIdsByWorkspace: {},
|
||||
hiddenAgentIdsByWorkspace: {},
|
||||
focusRestorationByWorkspace: {},
|
||||
explorerPaneIdByWorkspace: {},
|
||||
acknowledgedPullRequestByWorkspace: {},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -335,6 +347,142 @@ describe("workspace-layout-store actions", () => {
|
||||
await expect(AsyncStorage.getItem("workspace-layout-state")).resolves.not.toBeNull();
|
||||
});
|
||||
|
||||
it("persists first-class pane targets, visibility, and focus", async () => {
|
||||
await AsyncStorage.removeItem("workspace-layout-state");
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const source = createWorkspaceLayoutStore(createDeterministicWorkspaceLayoutIds());
|
||||
await source.persist.rehydrate();
|
||||
|
||||
source.getState().openTabFocused(workspaceKey, { kind: "agent", agentId: "agent-1" });
|
||||
const focusedAgentTabId = source
|
||||
.getState()
|
||||
.openTabFocused(workspaceKey, { kind: "agent", agentId: "agent-2" });
|
||||
const explorer = source.getState().ensureExplorerPane(workspaceKey);
|
||||
expect(explorer).toBeTruthy();
|
||||
source.getState().openTabFocused(workspaceKey, { kind: "files" });
|
||||
source.getState().observePullRequest(workspaceKey, "pr:1");
|
||||
source.getState().hidePane(workspaceKey, explorer!.paneId);
|
||||
|
||||
await vi.waitFor(async () => {
|
||||
expect(await AsyncStorage.getItem("workspace-layout-state")).not.toBeNull();
|
||||
});
|
||||
|
||||
const restored = createWorkspaceLayoutStore(createDeterministicWorkspaceLayoutIds());
|
||||
await restored.persist.rehydrate();
|
||||
const state = restored.getState();
|
||||
const layout = state.layoutByWorkspace[workspaceKey];
|
||||
|
||||
expect(layout.focusedPaneId).toBe("main");
|
||||
expect(findPaneById(layout.root, "main")?.focusedTabId).toBe(focusedAgentTabId);
|
||||
expect(findPaneById(layout.root, explorer!.paneId)?.hidden).toBe(true);
|
||||
expect(collectAllTabs(layout.root).map((tab) => tab.target.kind)).toEqual([
|
||||
"agent",
|
||||
"agent",
|
||||
"files",
|
||||
"pull_request",
|
||||
]);
|
||||
expect(state.explorerPaneIdByWorkspace[workspaceKey]).toBe(explorer!.paneId);
|
||||
expect(state.acknowledgedPullRequestByWorkspace[workspaceKey]).toBe("pr:1");
|
||||
});
|
||||
|
||||
it("auto-adds each detected pull request once and respects a deliberate close", () => {
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const store = workspaceLayoutStore.getState();
|
||||
|
||||
store.observePullRequest(workspaceKey, null);
|
||||
expect(store.getWorkspaceTabs(workspaceKey)).toEqual([]);
|
||||
|
||||
store.observePullRequest(workspaceKey, "url:https://example.test/pulls/1");
|
||||
expect(store.getWorkspaceTabs(workspaceKey).map((tab) => tab.target.kind)).toEqual([
|
||||
"pull_request",
|
||||
]);
|
||||
|
||||
store.closeTab(workspaceKey, "pull_request");
|
||||
store.observePullRequest(workspaceKey, "url:https://example.test/pulls/1");
|
||||
expect(store.getWorkspaceTabs(workspaceKey)).toEqual([]);
|
||||
|
||||
store.observePullRequest(workspaceKey, "url:https://example.test/pulls/2");
|
||||
expect(store.getWorkspaceTabs(workspaceKey).map((tab) => tab.target.kind)).toEqual([
|
||||
"pull_request",
|
||||
]);
|
||||
});
|
||||
|
||||
it("places an auto-added pull request in the registered explorer pane", () => {
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const store = workspaceLayoutStore.getState();
|
||||
useWorkspaceLayoutIds("explorer");
|
||||
const explorerPaneId = store.splitPaneEmpty(workspaceKey, {
|
||||
targetPaneId: "main",
|
||||
position: "right",
|
||||
});
|
||||
store.setExplorerPaneId(workspaceKey, explorerPaneId);
|
||||
|
||||
store.observePullRequest(workspaceKey, "url:https://example.test/pulls/1");
|
||||
|
||||
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
|
||||
expect(findPaneContainingTab(layout.root, "pull_request")?.id).toBe(explorerPaneId);
|
||||
});
|
||||
|
||||
it("opens assistant files in an ensured explorer pane and reveals it on the next open", () => {
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const store = workspaceLayoutStore.getState();
|
||||
useWorkspaceLayoutIds("explorer");
|
||||
|
||||
const tabId = store.openTabInExplorerPaneFocused(workspaceKey, {
|
||||
target: { kind: "file", path: "/repo/worktree/a.ts" },
|
||||
parentTabId: "agent_agent-a",
|
||||
});
|
||||
const createdState = workspaceLayoutStore.getState();
|
||||
const explorerPaneId = createdState.explorerPaneIdByWorkspace[workspaceKey];
|
||||
|
||||
expect(explorerPaneId).toBe("pane_explorer");
|
||||
expect(
|
||||
findPaneContainingTab(createdState.layoutByWorkspace[workspaceKey].root, tabId!)?.id,
|
||||
).toBe(explorerPaneId);
|
||||
expect(collectAllTabs(createdState.layoutByWorkspace[workspaceKey].root)).toContainEqual({
|
||||
tabId,
|
||||
target: { kind: "file", path: "/repo/worktree/a.ts" },
|
||||
createdAt: expect.any(Number),
|
||||
});
|
||||
|
||||
store.hidePane(workspaceKey, explorerPaneId!);
|
||||
store.openTabInExplorerPaneFocused(workspaceKey, {
|
||||
target: { kind: "file", path: "/repo/worktree/b.ts" },
|
||||
});
|
||||
const revealedLayout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
|
||||
|
||||
expect(findPaneById(revealedLayout.root, explorerPaneId)?.hidden).toBeUndefined();
|
||||
expect(findPaneContainingTab(revealedLayout.root, "file_/repo/worktree/b.ts")?.id).toBe(
|
||||
explorerPaneId,
|
||||
);
|
||||
});
|
||||
|
||||
it("moves a canonical duplicate file tab into the revealed explorer pane", () => {
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const store = workspaceLayoutStore.getState();
|
||||
useWorkspaceLayoutIds("explorer");
|
||||
const tabId = store.openTabFocused(workspaceKey, {
|
||||
kind: "file",
|
||||
path: "/repo/worktree/a.ts",
|
||||
});
|
||||
const explorerPane = store.ensureExplorerPane(workspaceKey);
|
||||
store.hidePane(workspaceKey, explorerPane!.paneId);
|
||||
|
||||
const duplicateTabId = store.openTabInExplorerPaneFocused(workspaceKey, {
|
||||
target: { kind: "file", path: "/repo/worktree/a.ts", lineStart: 12 },
|
||||
parentTabId: "agent_agent-a",
|
||||
});
|
||||
const state = workspaceLayoutStore.getState();
|
||||
const layout = state.layoutByWorkspace[workspaceKey];
|
||||
|
||||
expect(duplicateTabId).toBe(tabId);
|
||||
expect(collectAllTabs(layout.root).filter((tab) => tab.tabId === tabId)).toHaveLength(1);
|
||||
expect(findPaneContainingTab(layout.root, tabId!)?.id).toBe(explorerPane?.paneId);
|
||||
expect(findPaneById(layout.root, explorerPane?.paneId)?.hidden).toBeUndefined();
|
||||
expect(layout.focusedPaneId).toBe(explorerPane?.paneId);
|
||||
expect(findPaneById(layout.root, explorerPane?.paneId)?.focusedTabId).toBe(tabId);
|
||||
});
|
||||
|
||||
it("opens tabs into the focused pane and focuses duplicate opens instead of creating them", () => {
|
||||
useWorkspaceLayoutIds("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
@@ -641,6 +789,36 @@ describe("workspace-layout-store actions", () => {
|
||||
expect(findPaneById(layout.root, newPaneId)?.focusedTabId).toBe(draftTabId);
|
||||
});
|
||||
|
||||
it("hides and shows a pane without changing its tabs or split sizes", () => {
|
||||
useWorkspaceLayoutIds("88888888-8888-8888-8888-888888888888");
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const store = workspaceLayoutStore.getState();
|
||||
|
||||
store.openTabFocused(workspaceKey, { kind: "draft", draftId: "main-tab" });
|
||||
const paneId = store.splitPaneEmpty(workspaceKey, {
|
||||
targetPaneId: "main",
|
||||
position: "right",
|
||||
})!;
|
||||
store.openTabFocused(workspaceKey, { kind: "working_diff" });
|
||||
const sizes = expectGroup(workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey].root)
|
||||
.group.sizes;
|
||||
|
||||
store.hidePane(workspaceKey, paneId);
|
||||
let layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
|
||||
expect(findPaneById(layout.root, paneId)).toMatchObject({
|
||||
hidden: true,
|
||||
tabIds: ["working_diff"],
|
||||
});
|
||||
expect(expectGroup(layout.root).group.sizes).toEqual(sizes);
|
||||
expect(layout.focusedPaneId).toBe("main");
|
||||
|
||||
store.showPane(workspaceKey, paneId);
|
||||
layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
|
||||
expect(findPaneById(layout.root, paneId)).toMatchObject({ tabIds: ["working_diff"] });
|
||||
expect(findPaneById(layout.root, paneId)?.hidden).toBeUndefined();
|
||||
expect(expectGroup(layout.root).group.sizes).toEqual(sizes);
|
||||
});
|
||||
|
||||
it("focusTab moves workspace focus to the pane containing the tab", () => {
|
||||
useWorkspaceLayoutIds("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb");
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
@@ -671,6 +849,44 @@ describe("workspace-layout-store actions", () => {
|
||||
expect(findPaneById(layout.root, splitPaneId)?.focusedTabId).toBe(terminalTabId);
|
||||
});
|
||||
|
||||
it("focusTab reveals a hidden pane and focuses its requested tab without changing sizes", () => {
|
||||
useWorkspaceLayoutIds("89898989-8989-8989-8989-898989898989");
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const store = workspaceLayoutStore.getState();
|
||||
|
||||
store.openTabFocused(workspaceKey, { kind: "file", path: "/repo/worktree/a.ts" });
|
||||
const targetTabId = store.openTabFocused(workspaceKey, {
|
||||
kind: "terminal",
|
||||
terminalId: "term-hidden",
|
||||
})!;
|
||||
const paneId = store.splitPane(workspaceKey, {
|
||||
tabId: targetTabId,
|
||||
targetPaneId: "main",
|
||||
position: "right",
|
||||
})!;
|
||||
store.openTabFocused(workspaceKey, { kind: "working_diff" });
|
||||
store.focusPane(workspaceKey, "main");
|
||||
const group = expectGroup(
|
||||
workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey].root,
|
||||
).group;
|
||||
store.resizeSplit(workspaceKey, group.id, [0.7, 0.3]);
|
||||
store.hidePane(workspaceKey, paneId);
|
||||
const splitSizes = workspaceLayoutStore.getState().splitSizesByWorkspace[workspaceKey];
|
||||
|
||||
store.focusTab(workspaceKey, targetTabId);
|
||||
|
||||
const state = workspaceLayoutStore.getState();
|
||||
const layout = state.layoutByWorkspace[workspaceKey];
|
||||
expect(layout.focusedPaneId).toBe(paneId);
|
||||
expect(findPaneById(layout.root, paneId)).toMatchObject({
|
||||
focusedTabId: targetTabId,
|
||||
tabIds: [targetTabId, "working_diff"],
|
||||
});
|
||||
expect(findPaneById(layout.root, paneId)?.hidden).toBeUndefined();
|
||||
expect(expectGroup(layout.root).group.sizes).toEqual(group.sizes);
|
||||
expect(state.splitSizesByWorkspace[workspaceKey]).toBe(splitSizes);
|
||||
});
|
||||
|
||||
it("convertDraftToAgent replaces the draft tab with a canonical agent tab in the same pane", () => {
|
||||
useWorkspaceLayoutIds("12121212-1212-1212-1212-121212121212");
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
@@ -946,6 +1162,26 @@ describe("workspace-layout-store actions", () => {
|
||||
expect(layout.focusedPaneId).toBe(splitPaneId);
|
||||
});
|
||||
|
||||
it("focusPane reveals an explicitly targeted hidden pane", () => {
|
||||
useWorkspaceLayoutIds("57575757-5757-5757-5757-575757575757");
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
const store = workspaceLayoutStore.getState();
|
||||
|
||||
store.openTabFocused(workspaceKey, { kind: "file", path: "/repo/worktree/a.ts" });
|
||||
const paneId = store.splitPaneEmpty(workspaceKey, {
|
||||
targetPaneId: "main",
|
||||
position: "right",
|
||||
})!;
|
||||
store.focusPane(workspaceKey, "main");
|
||||
store.hidePane(workspaceKey, paneId);
|
||||
|
||||
store.focusPane(workspaceKey, paneId);
|
||||
|
||||
const layout = workspaceLayoutStore.getState().layoutByWorkspace[workspaceKey];
|
||||
expect(layout.focusedPaneId).toBe(paneId);
|
||||
expect(findPaneById(layout.root, paneId)?.hidden).toBeUndefined();
|
||||
});
|
||||
|
||||
it("closeTab collapses an emptied pane and keeps the nearest sibling focused", () => {
|
||||
useWorkspaceLayoutIds("cccccccc-cccc-cccc-cccc-cccccccccccc");
|
||||
const workspaceKey = createWorkspaceKey();
|
||||
@@ -1170,6 +1406,8 @@ describe("workspace-layout-store actions", () => {
|
||||
expect(persisted).toEqual({
|
||||
layoutByWorkspace: { [workspaceKey]: layout },
|
||||
splitSizesByWorkspace: currentState.splitSizesByWorkspace,
|
||||
explorerPaneIdByWorkspace: {},
|
||||
acknowledgedPullRequestByWorkspace: {},
|
||||
});
|
||||
expect(layout && collectAllTabs(layout.root).map((tab) => tab.target)).toEqual([
|
||||
{
|
||||
@@ -1302,6 +1540,8 @@ describe("workspace-layout-store actions", () => {
|
||||
expect(partialize?.(state)).toEqual({
|
||||
layoutByWorkspace: {},
|
||||
splitSizesByWorkspace: {},
|
||||
explorerPaneIdByWorkspace: {},
|
||||
acknowledgedPullRequestByWorkspace: {},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1338,6 +1578,8 @@ describe("workspace-layout-store actions", () => {
|
||||
expect(partialize?.(state)).toEqual({
|
||||
layoutByWorkspace: {},
|
||||
splitSizesByWorkspace: {},
|
||||
explorerPaneIdByWorkspace: {},
|
||||
acknowledgedPullRequestByWorkspace: {},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
reorderFocusedPaneTabsInLayout,
|
||||
reorderPaneTabsInLayout,
|
||||
retargetTabInLayout,
|
||||
setPaneHiddenInLayout,
|
||||
splitPaneEmptyInLayout,
|
||||
splitPaneInLayout,
|
||||
stripEphemeralTabsFromLayout,
|
||||
@@ -75,6 +76,8 @@ interface WorkspaceLayoutStore {
|
||||
pendingAgentIdsByWorkspace: Record<string, Set<string>>;
|
||||
hiddenAgentIdsByWorkspace: Record<string, Set<string>>;
|
||||
focusRestorationByWorkspace: Record<string, WorkspaceFocusRestorationState>;
|
||||
explorerPaneIdByWorkspace: Record<string, string | null>;
|
||||
acknowledgedPullRequestByWorkspace: Record<string, string>;
|
||||
openTabFocused: (workspaceKey: string, target: WorkspaceTabTarget) => string | null;
|
||||
openChildTabFocused: (
|
||||
workspaceKey: string,
|
||||
@@ -82,6 +85,12 @@ interface WorkspaceLayoutStore {
|
||||
parentTabId: string,
|
||||
) => string | null;
|
||||
openTabInBackground: (workspaceKey: string, target: WorkspaceTabTarget) => string | null;
|
||||
ensureExplorerPane: (workspaceKey: string) => EnsureExplorerPaneResult | null;
|
||||
openTabInExplorerPaneFocused: (
|
||||
workspaceKey: string,
|
||||
input: { target: WorkspaceTabTarget; parentTabId?: string | null },
|
||||
) => string | null;
|
||||
observePullRequest: (workspaceKey: string, identity: string | null) => void;
|
||||
closeTab: (workspaceKey: string, tabId: string) => void;
|
||||
focusTab: (workspaceKey: string, tabId: string) => void;
|
||||
retargetTab: (workspaceKey: string, tabId: string, target: WorkspaceTabTarget) => string | null;
|
||||
@@ -107,6 +116,9 @@ interface WorkspaceLayoutStore {
|
||||
) => string | null;
|
||||
moveTabToPane: (workspaceKey: string, tabId: string, toPaneId: string) => void;
|
||||
focusPane: (workspaceKey: string, paneId: string) => void;
|
||||
hidePane: (workspaceKey: string, paneId: string) => void;
|
||||
showPane: (workspaceKey: string, paneId: string) => void;
|
||||
setExplorerPaneId: (workspaceKey: string, paneId: string | null) => void;
|
||||
unfocusPane: (workspaceKey: string) => string | null;
|
||||
restorePaneFocus: (workspaceKey: string, token: string) => void;
|
||||
resizeSplit: (workspaceKey: string, groupId: string, sizes: number[]) => void;
|
||||
@@ -118,6 +130,11 @@ interface WorkspaceLayoutStore {
|
||||
purgeWorkspace: (workspaceKey: string) => void;
|
||||
}
|
||||
|
||||
interface EnsureExplorerPaneResult {
|
||||
paneId: string;
|
||||
created: boolean;
|
||||
}
|
||||
|
||||
interface WorkspaceFocusRestorationState {
|
||||
restorePaneId: string | null;
|
||||
tokens: string[];
|
||||
@@ -147,6 +164,8 @@ const WorkspaceTabTargetStorageSchema = z.discriminatedUnion("kind", [
|
||||
}),
|
||||
z.strictObject({ kind: z.literal("terminal"), terminalId: z.string() }),
|
||||
z.strictObject({ kind: z.literal("browser"), browserId: z.string() }),
|
||||
z.strictObject({ kind: z.literal("files") }),
|
||||
z.strictObject({ kind: z.literal("pull_request") }),
|
||||
z.strictObject({
|
||||
kind: z.literal("file"),
|
||||
path: z.string(),
|
||||
@@ -179,6 +198,7 @@ const SplitNodeStorageSchema: z.ZodType<SplitNode> = z.lazy(() =>
|
||||
tabIds: z.array(z.string()),
|
||||
focusedTabId: z.string().nullable(),
|
||||
tabs: z.array(WorkspaceTabStorageSchema).optional(),
|
||||
hidden: z.boolean().optional(),
|
||||
}),
|
||||
}),
|
||||
z.strictObject({
|
||||
@@ -200,6 +220,8 @@ const WorkspaceLayoutStorageSchema: z.ZodType<WorkspaceLayout> = z.strictObject(
|
||||
const WorkspaceLayoutPersistedStateSchema = z.strictObject({
|
||||
layoutByWorkspace: z.record(z.string(), WorkspaceLayoutStorageSchema),
|
||||
splitSizesByWorkspace: z.record(z.string(), z.record(z.string(), z.array(z.number()))).optional(),
|
||||
explorerPaneIdByWorkspace: z.record(z.string(), z.string().nullable()).optional(),
|
||||
acknowledgedPullRequestByWorkspace: z.record(z.string(), z.string()).optional(),
|
||||
});
|
||||
|
||||
function trimNonEmpty(value: string | null | undefined): string | null {
|
||||
@@ -308,6 +330,8 @@ export function createWorkspaceLayoutStore(
|
||||
pendingAgentIdsByWorkspace: {},
|
||||
hiddenAgentIdsByWorkspace: {},
|
||||
focusRestorationByWorkspace: {},
|
||||
explorerPaneIdByWorkspace: {},
|
||||
acknowledgedPullRequestByWorkspace: {},
|
||||
openTabFocused: (workspaceKey, target) => {
|
||||
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
||||
const normalizedTarget = normalizeWorkspaceTabTarget(target);
|
||||
@@ -408,6 +432,104 @@ export function createWorkspaceLayoutStore(
|
||||
|
||||
return result.tabId;
|
||||
},
|
||||
ensureExplorerPane: (workspaceKey) => {
|
||||
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
||||
if (!normalizedWorkspaceKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const store = get();
|
||||
const layout = getWorkspaceLayout(store.layoutByWorkspace, normalizedWorkspaceKey);
|
||||
const explorerPaneId = store.explorerPaneIdByWorkspace[normalizedWorkspaceKey] ?? null;
|
||||
const explorerPane = findPaneById(layout.root, explorerPaneId);
|
||||
if (explorerPane) {
|
||||
store.focusPane(normalizedWorkspaceKey, explorerPane.id);
|
||||
return { paneId: explorerPane.id, created: false };
|
||||
}
|
||||
|
||||
const targetPaneId =
|
||||
findPaneById(layout.root, layout.focusedPaneId)?.id ??
|
||||
collectAllPanes(layout.root)[0]?.id;
|
||||
if (!targetPaneId) {
|
||||
return null;
|
||||
}
|
||||
const paneId = store.splitPaneEmpty(normalizedWorkspaceKey, {
|
||||
targetPaneId,
|
||||
position: "right",
|
||||
});
|
||||
if (!paneId) {
|
||||
return null;
|
||||
}
|
||||
get().setExplorerPaneId(normalizedWorkspaceKey, paneId);
|
||||
return { paneId, created: true };
|
||||
},
|
||||
openTabInExplorerPaneFocused: (workspaceKey, input) => {
|
||||
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
||||
const normalizedTarget = normalizeWorkspaceTabTarget(input.target);
|
||||
if (!normalizedWorkspaceKey || !normalizedTarget) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const store = get();
|
||||
const explorerPane = store.ensureExplorerPane(normalizedWorkspaceKey);
|
||||
if (!explorerPane) {
|
||||
return null;
|
||||
}
|
||||
const parentTabId = trimNonEmpty(input.parentTabId);
|
||||
const tabId = parentTabId
|
||||
? get().openChildTabFocused(normalizedWorkspaceKey, normalizedTarget, parentTabId)
|
||||
: get().openTabFocused(normalizedWorkspaceKey, normalizedTarget);
|
||||
if (!tabId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const layout = getWorkspaceLayout(get().layoutByWorkspace, normalizedWorkspaceKey);
|
||||
const currentPane = findPaneContainingTab(layout.root, tabId);
|
||||
if (currentPane?.id !== explorerPane.paneId) {
|
||||
get().moveTabToPane(normalizedWorkspaceKey, tabId, explorerPane.paneId);
|
||||
}
|
||||
return tabId;
|
||||
},
|
||||
observePullRequest: (workspaceKey, identity) => {
|
||||
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
||||
const normalizedIdentity = trimNonEmpty(identity);
|
||||
if (!normalizedWorkspaceKey || !normalizedIdentity) {
|
||||
return;
|
||||
}
|
||||
|
||||
set((state) => {
|
||||
if (
|
||||
state.acknowledgedPullRequestByWorkspace[normalizedWorkspaceKey] ===
|
||||
normalizedIdentity
|
||||
) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const layout = getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey);
|
||||
const explorerPaneId = state.explorerPaneIdByWorkspace[normalizedWorkspaceKey] ?? null;
|
||||
const explorerPane = findPaneById(layout.root, explorerPaneId);
|
||||
const placementLayout = explorerPane
|
||||
? (focusPaneInLayout({ layout, paneId: explorerPane.id }) ?? layout)
|
||||
: layout;
|
||||
const result = openTabInLayoutFocused({
|
||||
layout: placementLayout,
|
||||
target: { kind: "pull_request" },
|
||||
now: Date.now(),
|
||||
});
|
||||
|
||||
return {
|
||||
...withoutFocusRestoration(state, normalizedWorkspaceKey),
|
||||
acknowledgedPullRequestByWorkspace: {
|
||||
...state.acknowledgedPullRequestByWorkspace,
|
||||
[normalizedWorkspaceKey]: normalizedIdentity,
|
||||
},
|
||||
layoutByWorkspace: {
|
||||
...state.layoutByWorkspace,
|
||||
[normalizedWorkspaceKey]: result.layout,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
closeTab: (workspaceKey, tabId) => {
|
||||
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
||||
const normalizedTabId = trimNonEmpty(tabId);
|
||||
@@ -722,6 +844,67 @@ export function createWorkspaceLayoutStore(
|
||||
};
|
||||
});
|
||||
},
|
||||
hidePane: (workspaceKey, paneId) => {
|
||||
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
||||
const normalizedPaneId = trimNonEmpty(paneId);
|
||||
if (!normalizedWorkspaceKey || !normalizedPaneId) {
|
||||
return;
|
||||
}
|
||||
|
||||
set((state) => {
|
||||
const nextLayout = setPaneHiddenInLayout({
|
||||
layout: getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey),
|
||||
paneId: normalizedPaneId,
|
||||
hidden: true,
|
||||
});
|
||||
if (!nextLayout) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...withoutFocusRestoration(state, normalizedWorkspaceKey),
|
||||
layoutByWorkspace: {
|
||||
...state.layoutByWorkspace,
|
||||
[normalizedWorkspaceKey]: nextLayout,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
showPane: (workspaceKey, paneId) => {
|
||||
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
||||
const normalizedPaneId = trimNonEmpty(paneId);
|
||||
if (!normalizedWorkspaceKey || !normalizedPaneId) {
|
||||
return;
|
||||
}
|
||||
|
||||
set((state) => {
|
||||
const nextLayout = setPaneHiddenInLayout({
|
||||
layout: getWorkspaceLayout(state.layoutByWorkspace, normalizedWorkspaceKey),
|
||||
paneId: normalizedPaneId,
|
||||
hidden: false,
|
||||
});
|
||||
if (!nextLayout) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
layoutByWorkspace: {
|
||||
...state.layoutByWorkspace,
|
||||
[normalizedWorkspaceKey]: nextLayout,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
setExplorerPaneId: (workspaceKey, paneId) => {
|
||||
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
||||
if (!normalizedWorkspaceKey) {
|
||||
return;
|
||||
}
|
||||
set((state) => ({
|
||||
explorerPaneIdByWorkspace: {
|
||||
...state.explorerPaneIdByWorkspace,
|
||||
[normalizedWorkspaceKey]: trimNonEmpty(paneId),
|
||||
},
|
||||
}));
|
||||
},
|
||||
unfocusPane: (workspaceKey) => {
|
||||
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
||||
if (!normalizedWorkspaceKey) {
|
||||
@@ -787,7 +970,8 @@ export function createWorkspaceLayoutStore(
|
||||
};
|
||||
}
|
||||
|
||||
const restorePaneId = findPaneById(layout.root, restoration.restorePaneId)?.id ?? null;
|
||||
const restorePane = findPaneById(layout.root, restoration.restorePaneId);
|
||||
const restorePaneId = restorePane?.hidden === true ? null : (restorePane?.id ?? null);
|
||||
if (!restorePaneId) {
|
||||
return {
|
||||
focusRestorationByWorkspace: remainingRestorations,
|
||||
@@ -991,7 +1175,9 @@ export function createWorkspaceLayoutStore(
|
||||
normalizedWorkspaceKey in state.pinnedAgentIdsByWorkspace ||
|
||||
normalizedWorkspaceKey in state.pendingAgentIdsByWorkspace ||
|
||||
normalizedWorkspaceKey in state.hiddenAgentIdsByWorkspace ||
|
||||
normalizedWorkspaceKey in state.focusRestorationByWorkspace;
|
||||
normalizedWorkspaceKey in state.focusRestorationByWorkspace ||
|
||||
normalizedWorkspaceKey in state.explorerPaneIdByWorkspace ||
|
||||
normalizedWorkspaceKey in state.acknowledgedPullRequestByWorkspace;
|
||||
if (!hasAny) {
|
||||
return state;
|
||||
}
|
||||
@@ -1007,6 +1193,12 @@ export function createWorkspaceLayoutStore(
|
||||
state.hiddenAgentIdsByWorkspace;
|
||||
const { [normalizedWorkspaceKey]: _restoration, ...focusRestorationByWorkspace } =
|
||||
state.focusRestorationByWorkspace;
|
||||
const { [normalizedWorkspaceKey]: _explorerPane, ...explorerPaneIdByWorkspace } =
|
||||
state.explorerPaneIdByWorkspace;
|
||||
const {
|
||||
[normalizedWorkspaceKey]: _acknowledgedPullRequest,
|
||||
...acknowledgedPullRequestByWorkspace
|
||||
} = state.acknowledgedPullRequestByWorkspace;
|
||||
return {
|
||||
layoutByWorkspace,
|
||||
splitSizesByWorkspace,
|
||||
@@ -1014,6 +1206,8 @@ export function createWorkspaceLayoutStore(
|
||||
pendingAgentIdsByWorkspace,
|
||||
hiddenAgentIdsByWorkspace,
|
||||
focusRestorationByWorkspace,
|
||||
explorerPaneIdByWorkspace,
|
||||
acknowledgedPullRequestByWorkspace,
|
||||
};
|
||||
});
|
||||
},
|
||||
@@ -1034,6 +1228,8 @@ export function createWorkspaceLayoutStore(
|
||||
return {
|
||||
layoutByWorkspace,
|
||||
splitSizesByWorkspace: state.splitSizesByWorkspace,
|
||||
explorerPaneIdByWorkspace: state.explorerPaneIdByWorkspace,
|
||||
acknowledgedPullRequestByWorkspace: state.acknowledgedPullRequestByWorkspace,
|
||||
};
|
||||
},
|
||||
merge: (persistedState, currentState) => {
|
||||
@@ -1045,6 +1241,9 @@ export function createWorkspaceLayoutStore(
|
||||
...currentState,
|
||||
layoutByWorkspace: result.data.layoutByWorkspace,
|
||||
splitSizesByWorkspace: result.data.splitSizesByWorkspace ?? {},
|
||||
explorerPaneIdByWorkspace: result.data.explorerPaneIdByWorkspace ?? {},
|
||||
acknowledgedPullRequestByWorkspace:
|
||||
result.data.acknowledgedPullRequestByWorkspace ?? {},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user