Expand workspace panel actions and keyboard navigation (#3685)
CI / changes (push) Successful in 6m6s
Docker / setup (push) Successful in 14s
Docker / publish (push) Skipped
Docker / build (push) Failing after 10m1s
CI / relay-tests (push) Canceled after 0s
CI / lint (push) Canceled after 0s
CI / typecheck (push) Canceled after 0s
CI / server-tests (ubuntu-latest) (push) Canceled after 0s
CI / server-tests (windows-latest) (push) Canceled after 0s
CI / desktop-tests (ubuntu-latest) (push) Canceled after 0s
CI / desktop-tests (windows-latest) (push) Canceled after 0s
CI / app-tests (push) Canceled after 0s
CI / sdk-tests (push) Canceled after 0s
CI / playwright (shard 1/4) (push) Canceled after 0s
CI / playwright (shard 2/4) (push) Canceled after 0s
CI / playwright (shard 3/4) (push) Canceled after 0s
CI / playwright (shard 4/4) (push) Canceled after 0s
CI / format (push) Canceled after 0s
CI / cli-tests (shard 1/3) (push) Canceled after 0s
CI / cli-tests (shard 2/3) (push) Canceled after 0s
CI / cli-tests (shard 3/3) (push) Canceled after 0s

* fix(app): keep file navigation in place

Retargeting a File tab allocated a new tab identity, causing the tab track to unmount and remount during navigation. Preserve the identity for File-to-File changes while retaining new instances for cross-kind replacements.

* feat(app): expand workspace panel actions

Keep the Command Center aligned with workspace tab and pane primitives. Add explicit panel placement, focused open behavior, keyboard navigation for empty panes, and a Changes pill that toggles the visible Side panel without closing its tab.
This commit is contained in:
Mohamed Boudra
2026-08-22 14:15:23 +02:00
committed by GitHub
parent 3029b5991a
commit a69dca2907
25 changed files with 1101 additions and 78 deletions
@@ -31,6 +31,7 @@ import {
openSupportingTab,
showSidePanel,
toggleSidePanel,
toggleSupportingTab,
} from "@/workspace-tabs/side-panel";
const WORKSPACE_KEY = "server-1:ws-main";
@@ -205,6 +206,27 @@ describe("non-compact with splits", () => {
expect(isSidePanelOpen(wide)).toBe(false);
expect(tabKinds()).toEqual(["files"]);
});
it("toggles the visible supporting target without closing its tab", () => {
const changes = {
...wide,
target: { kind: "working_diff" } as const,
openInSidePanelByDefault: true,
};
const tabId = toggleSupportingTab(changes);
openSidePanelView({ ...wide, view: "files" });
toggleSupportingTab(changes);
expect(isSidePanelOpen(wide)).toBe(true);
expect(focusedPaneTabKinds()).toContain("working_diff");
toggleSupportingTab(changes);
expect(isSidePanelOpen(wide)).toBe(false);
expect(tabKinds()).toContain("working_diff");
expect(toggleSupportingTab(changes)).toBe(tabId);
expect(isSidePanelOpen(wide)).toBe(true);
});
});
describe("non-compact without splits", () => {
@@ -14,6 +14,7 @@ import {
type WorkspaceTabPlacement,
} from "@/stores/workspace-layout-store";
import type { WorkspaceTabTarget } from "@/workspace-tabs/model";
import { workspaceTabTargetsEqual } from "@/workspace-tabs/identity";
/**
* The side panel is the companion surface beside the user's work — Changes, Files,
@@ -62,6 +63,20 @@ function canUseSidePanelPane(input: SidePanelQuery): boolean {
return !input.isCompact && (input.supportsPaneSplits ?? supportsDesktopPaneSplits());
}
function isTargetVisibleInSidePanel(
state: LayoutState,
workspaceKey: string,
target: WorkspaceTabTarget,
): boolean {
if (!selectIsSidePanelVisible(state, workspaceKey)) return false;
const layout = state.layoutByWorkspace[workspaceKey];
const paneId = selectSidePanelPaneId(state, workspaceKey);
const focusedTabId = layout && paneId ? findPaneById(layout.root, paneId)?.focusedTabId : null;
if (!layout || !focusedTabId) return false;
const focusedTab = collectAllTabs(layout.root).find((tab) => tab.tabId === focusedTabId);
return Boolean(focusedTab && workspaceTabTargetsEqual(focusedTab.target, target));
}
// ─── Side panel tabs (non-compact, no splits) ──────────────────────────────
/**
@@ -92,6 +107,11 @@ export interface OpenSupportingTabInput extends SidePanelQuery {
background?: boolean;
}
export interface ToggleSupportingTabInput extends SidePanelQuery {
target: WorkspaceTabTarget;
openInSidePanelByDefault: boolean;
}
/**
* Opens a tab the user did not place themselves — an agent's file link, a detected
* pull request, workspace setup progress. A supporting tab that is already open is
@@ -123,6 +143,52 @@ export function openSupportingTab(input: OpenSupportingTabInput): string | null
});
}
/** Toggle a supporting target when it is the visible Side panel tab; otherwise reveal it. */
export function toggleSupportingTab(input: ToggleSupportingTabInput): string | null {
if (!input.workspaceKey) return null;
const store = useWorkspaceLayoutStore.getState();
if (
input.openInSidePanelByDefault &&
canUseSidePanelPane(input) &&
isTargetVisibleInSidePanel(store, input.workspaceKey, input.target)
) {
store.hideSidePanel(input.workspaceKey);
return null;
}
return openSupportingTab(input);
}
/** Explicitly place a companion tab in the Side panel, moving an existing tab there. */
export function openTabInSidePanel(
input: SidePanelInput & { target: WorkspaceTabTarget },
): string | null {
if (!input.workspaceKey) {
return null;
}
if (input.isCompact) {
const view = Object.entries(SIDE_PANEL_VIEW_TARGETS).find(
([, target]) => target.kind === input.target.kind,
)?.[0] as ExplorerTab | undefined;
if (!view || !input.checkout) {
return null;
}
const panel = usePanelStore.getState();
panel.setExplorerTabForCheckout({ ...input.checkout, tab: view });
panel.openCompactFileExplorer(input.checkout);
return null;
}
const store = useWorkspaceLayoutStore.getState();
const paneId = canUseSidePanelPane(input) ? store.showSidePanel(input.workspaceKey) : undefined;
return store.openTab({
workspaceKey: input.workspaceKey,
target: input.target,
intent: "reveal",
placement: paneId ? { mode: "pane", paneId } : undefined,
});
}
/**
* Undefined means "no opinion, use the focused pane". Revealing is part of resolving
* the side panel: a focused tab in a hidden pane is nothing the user can see, so a