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
@@ -46,4 +46,32 @@ describe("focused chat target", () => {
}),
).toBeNull();
});
it("targets a utility tab's parent chat", () => {
const layout = {
root: {
kind: "pane",
pane: {
id: "pane-1",
tabIds: ["agent-tab", "changes-tab"],
focusedTabId: "changes-tab",
tabs: [
{
tabId: "agent-tab",
target: { kind: "agent", agentId: "agent-1" },
createdAt: 1,
},
{ tabId: "changes-tab", target: { kind: "working_diff" }, createdAt: 2 },
],
},
},
focusedPaneId: "pane-1",
parentTabIdByTabId: { "changes-tab": "agent-tab" },
} as WorkspaceLayout;
expect(resolveFocusedChatTarget({ serverId: "server-1", layout })).toEqual({
tabId: "agent-tab",
draftKey: "agent:server-1:agent-1",
});
});
});
@@ -10,6 +10,29 @@ export interface FocusedChatTarget {
draftKey: string;
}
function resolveChatTab(
serverId: string,
tab: ReturnType<typeof collectAllTabs>[number] | undefined,
): FocusedChatTarget | null {
if (tab?.target.kind === "agent") {
return {
tabId: tab.tabId,
draftKey: buildDraftStoreKey({ serverId, agentId: tab.target.agentId }),
};
}
if (tab?.target.kind === "draft") {
return {
tabId: tab.tabId,
draftKey: buildDraftStoreKey({
serverId,
agentId: tab.tabId,
draftId: tab.target.draftId,
}),
};
}
return null;
}
export function resolveFocusedChatTarget(input: {
serverId: string;
layout: WorkspaceLayout | undefined;
@@ -22,27 +45,15 @@ export function resolveFocusedChatTarget(input: {
if (!focusedTabId) {
return null;
}
const tab = collectAllTabs(input.layout.root).find(
(candidate) => candidate.tabId === focusedTabId,
const tabs = collectAllTabs(input.layout.root);
const tab = tabs.find((candidate) => candidate.tabId === focusedTabId);
const focusedChat = resolveChatTab(input.serverId, tab);
if (focusedChat) {
return focusedChat;
}
const parentTabId = input.layout.parentTabIdByTabId?.[focusedTabId];
return resolveChatTab(
input.serverId,
tabs.find((candidate) => candidate.tabId === parentTabId),
);
if (!tab) {
return null;
}
if (tab.target.kind === "agent") {
return {
tabId: tab.tabId,
draftKey: buildDraftStoreKey({ serverId: input.serverId, agentId: tab.target.agentId }),
};
}
if (tab.target.kind === "draft") {
return {
tabId: tab.tabId,
draftKey: buildDraftStoreKey({
serverId: input.serverId,
agentId: tab.tabId,
draftId: tab.target.draftId,
}),
};
}
return null;
}