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:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user