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