feat(app): auto-open the pull request tab once when a PR is detected (#4956)

* feat(app): auto-open the pull request tab once when a PR is detected

* docs(app): clarify automatic pull request tab policy
This commit is contained in:
Mohamed Boudra
2026-09-16 18:28:14 +02:00
committed by GitHub
parent 0f20e6dfe4
commit 4f710d9930
9 changed files with 323 additions and 11 deletions
+4 -1
View File
@@ -30,7 +30,10 @@ never read or modify that width.
`packages/app/src/workspace-tabs/open-supporting-view.ts` owns semantic Changes and pull-request
opens. Compact and wide native layouts select the matching Explorer tab. Desktop Changes opens
follow the shared diff preference. Desktop pull requests use their Main panel, On the side, or
Explorer sidebar setting. Callers request the content and never choose the shell.
Explorer sidebar setting. Automatic PR discovery follows that preference once per workspace without
interrupting the user's work. Closing the tab opts that workspace out of future automatic opens,
even for a different PR; moving or reordering it remains the user's choice.
Callers request the content and never choose the shell.
The composer Changes pill is a two-stage desktop action: it first reveals Explorer on Changes, then
routes later presses to the working diff through the shared diff preference.
+21 -3
View File
@@ -1,4 +1,4 @@
import { useCallback } from "react";
import { useCallback, useEffect } from "react";
import { Text, View } from "react-native";
import { useTranslation } from "react-i18next";
import { StyleSheet } from "react-native-unistyles";
@@ -14,11 +14,14 @@ import {
import type { UsePrPaneDataResult } from "@/git/pull-request-panel/use-data";
import { useCheckoutPrStatusQuery } from "@/git/use-pr-status-query";
import { useSettings } from "@/hooks/use-settings";
import { useWorkspaceLayoutStoreHydrated } from "@/stores/workspace-layout-store";
import { autoOpenWorkspacePullRequest } from "@/workspace-tabs/open-supporting-view";
import { getPullRequestIdentity, resolvePullRequestContentState } from "./state";
/**
* Whether this workspace's branch has a pull request. Detection only offers the
* pull request as something the user can open; it never opens a tab on its own.
* Whether this workspace's branch has a pull request.
*/
export function useHasPullRequest(input: {
serverId: string;
@@ -33,6 +36,21 @@ export function useHasPullRequest(input: {
return getPullRequestIdentity(status) !== null;
}
/** Connects desktop PR detection to the persisted layout after hydration. */
export function usePullRequestAutoAdd(input: {
workspaceKey: string | null;
hasPullRequest: boolean;
enabled: boolean;
}): void {
const hydrated = useWorkspaceLayoutStoreHydrated();
const destination = useSettings((settings) => settings.pullRequestOpenLocation);
useEffect(() => {
if (hydrated && input.enabled && input.hasPullRequest) {
autoOpenWorkspacePullRequest({ workspaceKey: input.workspaceKey, destination });
}
}, [hydrated, input.enabled, input.hasPullRequest, input.workspaceKey, destination]);
}
export function PullRequestContent(input: {
serverId: string;
workspaceId?: string | null;
@@ -206,7 +206,7 @@ import {
} from "@/workspace/file-open";
import { RenderProfile } from "@/utils/render-profiler";
import { useWorkspaceCheckoutStatus } from "@/screens/workspace/use-workspace-checkout-status";
import { useHasPullRequest } from "@/panels/pull-request";
import { useHasPullRequest, usePullRequestAutoAdd } from "@/panels/pull-request";
const WORKSPACE_FLOATING_PANEL_PORTAL_HOST_PREFIX = "workspace-floating-panels";
const EMPTY_UI_TABS: WorkspaceTab[] = [];
@@ -1760,10 +1760,16 @@ function WorkspaceScreenContent({
workspace: workspaceDescriptor,
checkoutState: workspaceHeaderCheckoutState,
});
const canDetectPullRequestTab = canDetectPullRequest(isRouteFocused, isGitCheckout, isMobile);
const hasPullRequest = useHasPullRequest({
serverId: normalizedServerId,
cwd: workspaceDirectory,
enabled: canDetectPullRequest(isRouteFocused, isGitCheckout, isMobile),
enabled: canDetectPullRequestTab,
});
usePullRequestAutoAdd({
workspaceKey: persistenceKey,
hasPullRequest,
enabled: canDetectPullRequestTab,
});
const showMobileAgent = usePanelStore((state) => state.showMobileAgent);
@@ -84,6 +84,7 @@ interface DetachTabFromTreeResult {
}
interface InsertTabIntoPaneInput {
insertionPosition?: WorkspaceTabInsertionPosition;
paneId: string;
tab: WorkspaceTab;
focusTabId?: string | null;
@@ -124,7 +125,12 @@ export type WorkspaceTabPlacement =
export const FOCUSED_PANE_PLACEMENT: WorkspaceTabPlacement = { mode: "focused" };
export const AMBIENT_PLACEMENT: WorkspaceTabPlacement = { mode: "ambient" };
export interface WorkspaceTabInsertionPosition {
afterTabId: string;
}
interface OpenTabInLayoutInput {
insertionPosition?: WorkspaceTabInsertionPosition;
layout: WorkspaceLayout;
target: WorkspaceTabTarget;
now: number;
@@ -848,7 +854,11 @@ function insertTabIntoPane(
} else if (existingIndex >= 0) {
nextTabs = node.pane.tabs.map((tab, index) => (index === existingIndex ? input.tab : tab));
} else {
nextTabs = [...node.pane.tabs, input.tab];
nextTabs = [...node.pane.tabs];
const afterIndex = nextTabs.findIndex(
(tab) => tab.tabId === input.insertionPosition?.afterTabId,
);
nextTabs.splice(afterIndex >= 0 ? afterIndex + 1 : nextTabs.length, 0, input.tab);
}
return {
kind: "pane",
@@ -1380,6 +1390,7 @@ function insertNewTabIntoPane(
root: insertTabIntoPane(layout.root, {
paneId: targetPane.id,
tab: nextTab,
insertionPosition: input.insertionPosition,
focusTabId: input.focus ? tabId : preservedFocusTabId,
}),
focusedPaneId: input.focus ? targetPane.id : layout.focusedPaneId,
@@ -108,6 +108,7 @@ export const WorkspaceLayoutPersistedStateSchema = z.strictObject({
explorerPaneIdByWorkspace: z.record(z.string(), z.string().nullable()).optional(),
explorerSidebarPaneIdByWorkspace: z.record(z.string(), z.string().nullable()).optional(),
sidePaneIdByWorkspace: z.record(z.string(), z.string().nullable()).optional(),
pullRequestTabAutoOpenedByWorkspace: z.record(z.string(), z.literal(true)).optional(),
// COMPAT(pullRequestAutoAdd): PR detection stopped opening a tab in v0.5; accepted
// and ignored so upgrading does not discard the layout. Remove after 2027-08-20.
acknowledgedPullRequestByWorkspace: z.record(z.string(), z.string()).optional(),
@@ -498,6 +498,7 @@ describe("workspace-layout-store version 2 migration", () => {
"explorerSidebarWidthByWorkspace",
"layoutByWorkspace",
"pinnedAgentIdsByWorkspace",
"pullRequestTabAutoOpenedByWorkspace",
"sidePaneIdByWorkspace",
"splitSizesByWorkspace",
]);
@@ -3091,6 +3092,7 @@ describe("workspace-layout-store actions", () => {
splitSizesByWorkspace: currentState.splitSizesByWorkspace,
explorerSidebarWidthByWorkspace: currentState.explorerSidebarWidthByWorkspace,
explorerPaneIdByWorkspace: {},
pullRequestTabAutoOpenedByWorkspace: currentState.pullRequestTabAutoOpenedByWorkspace,
sidePaneIdByWorkspace: currentState.sidePaneIdByWorkspace,
});
expect(layout && collectAllTabs(layout.root).map((tab) => tab.target)).toEqual([
@@ -3319,6 +3321,7 @@ describe("workspace-layout-store actions", () => {
const partialize = workspaceLayoutStore.persist.getOptions().partialize;
expect(partialize).toBeTypeOf("function");
expect(partialize?.(state)).toEqual({
pullRequestTabAutoOpenedByWorkspace: {},
layoutByWorkspace: {},
pinnedAgentIdsByWorkspace: {},
splitSizesByWorkspace: {},
@@ -4359,3 +4362,25 @@ describe("workspace-layout-store actions", () => {
expect(collectAllPanes(layout.root).map((pane) => pane.id)).toEqual(["main"]);
});
});
it("persists the once-only PR add after closing, and clears it when purging the workspace", async () => {
await AsyncStorage.removeItem("workspace-layout-state");
const source = createWorkspaceLayoutStore(workspaceLayoutIds);
await source.persist.rehydrate();
const workspaceKey = "server-1:pr-once";
const placement = () => ({ placement: { mode: "prefer" as const, paneId: "explorer" } });
source.getState().autoOpenPullRequestTab(workspaceKey, placement);
source.getState().closeTab(workspaceKey, "pull_request");
const restored = createWorkspaceLayoutStore(workspaceLayoutIds);
await restored.persist.rehydrate();
expect(restored.getState().pullRequestTabAutoOpenedByWorkspace[workspaceKey]).toBe(true);
expect(restored.getState().autoOpenPullRequestTab(workspaceKey, placement)).toBeNull();
expect(
collectAllTabs(restored.getState().layoutByWorkspace[workspaceKey].root).map(
(tab) => tab.target.kind,
),
).not.toContain("pull_request");
restored.getState().purgeWorkspace(workspaceKey);
expect(restored.getState().pullRequestTabAutoOpenedByWorkspace[workspaceKey]).toBeUndefined();
expect(restored.getState().autoOpenPullRequestTab(workspaceKey, placement)).toBe("pull_request");
});
@@ -54,6 +54,7 @@ import {
type SplitNode,
type SplitPane,
type WorkspaceTabPlacement,
type WorkspaceTabInsertionPosition,
type WorkspaceTabReconcileState,
type WorkspaceTabSnapshot,
type WorkspaceLayout,
@@ -99,6 +100,7 @@ export interface OpenWorkspaceTabInput {
/** Keeps an explicitly opened agent visible even when it is archived. */
pin?: boolean;
placement?: WorkspaceTabPlacement;
insertionPosition?: WorkspaceTabInsertionPosition;
parentTabId?: string;
state?: JsonValue;
}
@@ -112,12 +114,19 @@ interface WorkspaceLayoutStore {
focusRestorationByWorkspace: Record<string, WorkspaceFocusRestorationState>;
explorerSidebarPaneIdByWorkspace: Record<string, string | null>;
sidePaneIdByWorkspace: Record<string, string | null>;
/** Workspaces where PR detection already added its tab once; a closed tab never returns. */
pullRequestTabAutoOpenedByWorkspace: Record<string, true>;
openTab: (input: OpenWorkspaceTabInput) => string | null;
/** Placement resolves lazily so an already acknowledged workspace never creates a side pane. */
autoOpenPullRequestTab: (
workspaceKey: string,
resolvePlacement: () => Pick<OpenWorkspaceTabInput, "placement" | "insertionPosition">,
) => string | null;
/** Reveals the Explorer sidebar without selecting a view. Returns its pane id. */
showExplorerSidebar: (workspaceKey: string) => string | null;
hideExplorerSidebar: (workspaceKey: string) => void;
/** Returns the ordinary right-side workspace pane, creating it when absent. */
ensureSidePane: (workspaceKey: string) => string | null;
ensureSidePane: (workspaceKey: string, options?: { focus: boolean }) => string | null;
closeTab: (workspaceKey: string, tabId: string) => void;
focusTab: (workspaceKey: string, tabId: string) => void;
selectTabInPane: (workspaceKey: string, paneId: string, tabId: string) => void;
@@ -648,6 +657,25 @@ export function createWorkspaceLayoutStore(
return create<WorkspaceLayoutStore>()(
persist(
(set, get) => ({
autoOpenPullRequestTab: (workspaceKey, resolvePlacement) => {
const key = trimNonEmpty(workspaceKey);
if (!key || get().pullRequestTabAutoOpenedByWorkspace[key]) return null;
const tabId = get().openTab({
workspaceKey: key,
target: { kind: "pull_request" },
intent: "background",
...resolvePlacement(),
});
if (tabId) {
set((state) => ({
pullRequestTabAutoOpenedByWorkspace: {
...state.pullRequestTabAutoOpenedByWorkspace,
[key]: true,
},
}));
}
return tabId;
},
layoutByWorkspace: {},
splitSizesByWorkspace: {},
explorerSidebarWidthByWorkspace: {},
@@ -656,6 +684,7 @@ export function createWorkspaceLayoutStore(
focusRestorationByWorkspace: {},
explorerSidebarPaneIdByWorkspace: {},
sidePaneIdByWorkspace: {},
pullRequestTabAutoOpenedByWorkspace: {},
openTab: (input) => {
const normalizedWorkspaceKey = trimNonEmpty(input.workspaceKey);
const normalizedTarget = normalizeWorkspaceTabTarget(input.target);
@@ -672,6 +701,7 @@ export function createWorkspaceLayoutStore(
if (input.intent === "new") {
result = createTabInLayout({
...placement,
insertionPosition: input.insertionPosition,
target: normalizedTarget,
now: Date.now(),
createTabId: createWorkspaceTabInstanceId,
@@ -680,12 +710,14 @@ export function createWorkspaceLayoutStore(
} else if (input.intent === "background") {
result = openTabInLayoutBackground({
...placement,
insertionPosition: input.insertionPosition,
target: normalizedTarget,
now: Date.now(),
});
} else {
result = revealTargetInLayout({
...placement,
insertionPosition: input.insertionPosition,
target: normalizedTarget,
now: Date.now(),
createTabId: createWorkspaceTabInstanceId,
@@ -798,7 +830,7 @@ export function createWorkspaceLayoutStore(
};
});
},
ensureSidePane: (workspaceKey) => {
ensureSidePane: (workspaceKey, options) => {
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
if (!normalizedWorkspaceKey) {
return null;
@@ -829,7 +861,10 @@ export function createWorkspaceLayoutStore(
...withoutFocusRestoration(state, normalizedWorkspaceKey),
layoutByWorkspace: {
...state.layoutByWorkspace,
[normalizedWorkspaceKey]: result.layout,
[normalizedWorkspaceKey]:
options?.focus === false
? { ...result.layout, focusedPaneId: layout.focusedPaneId }
: result.layout,
},
sidePaneIdByWorkspace: {
...state.sidePaneIdByWorkspace,
@@ -1625,10 +1660,15 @@ export function createWorkspaceLayoutStore(
normalizedWorkspaceKey in state.hiddenAgentIdsByWorkspace ||
normalizedWorkspaceKey in state.focusRestorationByWorkspace ||
normalizedWorkspaceKey in state.explorerSidebarPaneIdByWorkspace ||
normalizedWorkspaceKey in state.sidePaneIdByWorkspace;
normalizedWorkspaceKey in state.sidePaneIdByWorkspace ||
normalizedWorkspaceKey in state.pullRequestTabAutoOpenedByWorkspace;
if (!hasAny) {
return state;
}
const {
[normalizedWorkspaceKey]: _autoOpened,
...pullRequestTabAutoOpenedByWorkspace
} = state.pullRequestTabAutoOpenedByWorkspace;
const { [normalizedWorkspaceKey]: _layout, ...layoutByWorkspace } =
state.layoutByWorkspace;
const { [normalizedWorkspaceKey]: _splits, ...splitSizesByWorkspace } =
@@ -1650,6 +1690,7 @@ export function createWorkspaceLayoutStore(
const { [normalizedWorkspaceKey]: _sidePane, ...sidePaneIdByWorkspace } =
state.sidePaneIdByWorkspace;
return {
pullRequestTabAutoOpenedByWorkspace,
layoutByWorkspace,
splitSizesByWorkspace,
explorerSidebarWidthByWorkspace,
@@ -1689,6 +1730,7 @@ export function createWorkspaceLayoutStore(
explorerSidebarWidthByWorkspace: state.explorerSidebarWidthByWorkspace,
explorerPaneIdByWorkspace: state.explorerSidebarPaneIdByWorkspace,
sidePaneIdByWorkspace: state.sidePaneIdByWorkspace,
pullRequestTabAutoOpenedByWorkspace: state.pullRequestTabAutoOpenedByWorkspace,
};
},
merge: (persistedState, currentState) => {
@@ -1741,6 +1783,8 @@ export function createWorkspaceLayoutStore(
),
explorerSidebarPaneIdByWorkspace,
sidePaneIdByWorkspace: result.data.sidePaneIdByWorkspace ?? {},
pullRequestTabAutoOpenedByWorkspace:
result.data.pullRequestTabAutoOpenedByWorkspace ?? {},
};
},
},
@@ -16,6 +16,7 @@ import {
useWorkspaceLayoutStore,
} from "@/stores/workspace-layout-store";
import {
autoOpenWorkspacePullRequest,
openComposerChanges,
openWorkspaceChanges,
openWorkspacePullRequest,
@@ -31,6 +32,7 @@ beforeEach(() => {
explorerTabByCheckout: {},
});
useWorkspaceLayoutStore.setState({
pullRequestTabAutoOpenedByWorkspace: {},
layoutByWorkspace: {},
explorerSidebarPaneIdByWorkspace: {},
sidePaneIdByWorkspace: {},
@@ -178,3 +180,152 @@ describe("openWorkspacePullRequest", () => {
expect(sidePane?.tabIds).toContain(pullRequestTab?.tabId);
});
});
describe("autoOpenWorkspacePullRequest", () => {
it("silently adds the PR after Changes once, and never restores it after closing", () => {
const store = useWorkspaceLayoutStore.getState();
store.openTab({
workspaceKey: WORKSPACE_KEY,
target: { kind: "agent", agentId: "agent-1" },
intent: "reveal",
});
store.openTab({
workspaceKey: WORKSPACE_KEY,
target: { kind: "terminal", terminalId: "terminal-1" },
intent: "background",
placement: { mode: "prefer", paneId: "explorer" },
});
const before = useWorkspaceLayoutStore.getState().layoutByWorkspace[WORKSPACE_KEY];
const input = { workspaceKey: WORKSPACE_KEY, destination: "explorer" as const };
autoOpenWorkspacePullRequest(input);
const after = useWorkspaceLayoutStore.getState().layoutByWorkspace[WORKSPACE_KEY];
const explorer = findPaneById(after.root, "explorer")!;
expect(explorer.tabIds).toEqual([
"files",
"changes_tree",
"pull_request",
"terminal_terminal-1",
]);
expect(explorer.hidden).toBe(true);
expect(explorer.focusedTabId).toBe(findPaneById(before.root, "explorer")!.focusedTabId);
expect(after.focusedPaneId).toBe(before.focusedPaneId);
store.closeTab(WORKSPACE_KEY, "pull_request");
autoOpenWorkspacePullRequest(input);
expect(
collectAllTabs(useWorkspaceLayoutStore.getState().layoutByWorkspace[WORKSPACE_KEY].root).map(
(tab) => tab.target.kind,
),
).not.toContain("pull_request");
});
});
describe("automatic PR placement", () => {
it.each(["main", "side"] as const)(
"silently appends to the configured %s pane",
(destination) => {
const store = useWorkspaceLayoutStore.getState();
store.openTab({
workspaceKey: WORKSPACE_KEY,
target: { kind: "agent", agentId: "agent-1" },
intent: "reveal",
});
const paneId = destination === "main" ? "main" : store.ensureSidePane(WORKSPACE_KEY)!;
store.openTab({
workspaceKey: WORKSPACE_KEY,
target: { kind: "terminal", terminalId: "terminal-1" },
intent: "background",
placement: { mode: "prefer", paneId },
});
const before = useWorkspaceLayoutStore.getState().layoutByWorkspace[WORKSPACE_KEY];
autoOpenWorkspacePullRequest({ workspaceKey: WORKSPACE_KEY, destination });
const after = useWorkspaceLayoutStore.getState().layoutByWorkspace[WORKSPACE_KEY];
expect(findPaneById(after.root, paneId)!.tabIds).toEqual([
...findPaneById(before.root, paneId)!.tabIds,
"pull_request",
]);
expect(findPaneById(after.root, paneId)!.focusedTabId).toBe(
findPaneById(before.root, paneId)!.focusedTabId,
);
expect(after.focusedPaneId).toBe(before.focusedPaneId);
expect(findPaneById(after.root, "explorer")!.hidden).toBe(true);
},
);
it("creates a side pane without stealing workspace focus", () => {
const store = useWorkspaceLayoutStore.getState();
store.openTab({
workspaceKey: WORKSPACE_KEY,
target: { kind: "agent", agentId: "agent-1" },
intent: "reveal",
});
autoOpenWorkspacePullRequest({ workspaceKey: WORKSPACE_KEY, destination: "side" });
const state = useWorkspaceLayoutStore.getState();
const layout = state.layoutByWorkspace[WORKSPACE_KEY];
expect(findPaneById(layout.root, state.sidePaneIdByWorkspace[WORKSPACE_KEY])!.tabIds).toEqual([
"pull_request",
]);
expect(layout.focusedPaneId).toBe("main");
});
it.each(["main", "side", "explorer"] as const)(
"leaves a manually moved PR in main when detection prefers %s",
(destination) => {
const store = useWorkspaceLayoutStore.getState();
store.openTab({
workspaceKey: WORKSPACE_KEY,
target: { kind: "pull_request" },
intent: "background",
placement: { mode: "prefer", paneId: "explorer" },
});
store.moveTabToPane(WORKSPACE_KEY, "pull_request", "main");
const before = useWorkspaceLayoutStore.getState().layoutByWorkspace[WORKSPACE_KEY];
autoOpenWorkspacePullRequest({ workspaceKey: WORKSPACE_KEY, destination });
expect(useWorkspaceLayoutStore.getState().layoutByWorkspace[WORKSPACE_KEY]).toEqual(before);
expect(
useWorkspaceLayoutStore.getState().sidePaneIdByWorkspace[WORKSPACE_KEY],
).toBeUndefined();
store.closeTab(WORKSPACE_KEY, "pull_request");
autoOpenWorkspacePullRequest({ workspaceKey: WORKSPACE_KEY, destination });
expect(
collectAllTabs(
useWorkspaceLayoutStore.getState().layoutByWorkspace[WORKSPACE_KEY].root,
).map((tab) => tab.target.kind),
).not.toContain("pull_request");
expect(
useWorkspaceLayoutStore.getState().sidePaneIdByWorkspace[WORKSPACE_KEY],
).toBeUndefined();
},
);
it("preserves reordering before and after detection", () => {
const store = useWorkspaceLayoutStore.getState();
store.openTab({
workspaceKey: WORKSPACE_KEY,
target: { kind: "pull_request" },
intent: "background",
placement: { mode: "prefer", paneId: "explorer" },
});
store.reorderTabsInPane(WORKSPACE_KEY, "explorer", ["pull_request", "files", "changes_tree"]);
const before = useWorkspaceLayoutStore.getState().layoutByWorkspace[WORKSPACE_KEY];
autoOpenWorkspacePullRequest({ workspaceKey: WORKSPACE_KEY, destination: "explorer" });
autoOpenWorkspacePullRequest({ workspaceKey: WORKSPACE_KEY, destination: "side" });
expect(useWorkspaceLayoutStore.getState().layoutByWorkspace[WORKSPACE_KEY]).toEqual(before);
});
it("appends in Explorer when Changes was closed", () => {
const store = useWorkspaceLayoutStore.getState();
store.openTab({
workspaceKey: WORKSPACE_KEY,
target: { kind: "agent", agentId: "agent-1" },
intent: "reveal",
});
store.closeTab(WORKSPACE_KEY, "changes_tree");
autoOpenWorkspacePullRequest({ workspaceKey: WORKSPACE_KEY, destination: "explorer" });
expect(
findPaneById(
useWorkspaceLayoutStore.getState().layoutByWorkspace[WORKSPACE_KEY].root,
"explorer",
)!.tabIds,
).toEqual(["files", "pull_request"]);
});
});
@@ -1,3 +1,12 @@
import {
collectAllPanes,
collectAllTabs,
createWorkspaceLayoutWithExplorerSidebar,
DEFAULT_PANE_ID,
findPaneContainingTab,
resolveExplorerSidebarPaneId,
useWorkspaceLayoutStore,
} from "@/stores/workspace-layout-store";
import type { OpenInSidePanePreferences, PullRequestOpenLocation } from "@/hooks/use-settings";
import type { ExplorerCheckoutContext } from "@/stores/explorer-checkout-context";
import {
@@ -67,3 +76,47 @@ export function openWorkspacePullRequest(input: OpenWorkspacePullRequestInput):
location: input.destination,
});
}
/** Adds a detected desktop PR once without revealing or selecting its destination. */
export function autoOpenWorkspacePullRequest(input: {
workspaceKey: string | null;
destination: PullRequestOpenLocation;
}): string | null {
if (!input.workspaceKey) return null;
const workspaceKey = input.workspaceKey;
const store = useWorkspaceLayoutStore.getState();
return store.autoOpenPullRequestTab(workspaceKey, () => {
const layout =
store.layoutByWorkspace[workspaceKey] ?? createWorkspaceLayoutWithExplorerSidebar();
// A background open finds an existing PR tab wherever the user left it.
if (collectAllTabs(layout.root).some((tab) => tab.target.kind === "pull_request")) {
return {};
}
const explorerPaneId = resolveExplorerSidebarPaneId(
layout,
store.explorerSidebarPaneIdByWorkspace[workspaceKey],
);
let paneId = explorerPaneId;
if (input.destination === "side") {
paneId = store.ensureSidePane(workspaceKey, { focus: false });
} else if (input.destination === "main") {
const panes = collectAllPanes(layout.root);
const mainPane =
panes.find((pane) => pane.id === DEFAULT_PANE_ID) ??
panes.find((pane) => pane.id !== explorerPaneId && !pane.hidden);
paneId = mainPane?.id ?? null;
}
const changes =
input.destination === "explorer"
? collectAllTabs(layout.root).find(
(tab) =>
tab.target.kind === "changes_tree" &&
findPaneContainingTab(layout.root, tab.tabId)?.id === paneId,
)
: undefined;
return {
placement: paneId ? { mode: "prefer", paneId } : undefined,
insertionPosition: changes ? { afterTabId: changes.tabId } : undefined,
};
});
}