mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-24 02:02:42 +00:00
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:
@@ -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.
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user