diff --git a/packages/app/src/panels/new-tab-action-order.ts b/packages/app/src/panels/new-tab-action-order.ts new file mode 100644 index 0000000000..848c01504c --- /dev/null +++ b/packages/app/src/panels/new-tab-action-order.ts @@ -0,0 +1,8 @@ +export type LauncherAction = "agent" | "terminal" | "changes" | "files" | "browser" | "pullRequest"; + +export function getLauncherActionOrder(isSidePanel: boolean): LauncherAction[] { + if (isSidePanel) { + return ["changes", "files", "terminal", "agent", "browser", "pullRequest"]; + } + return ["agent", "terminal", "changes", "files", "browser", "pullRequest"]; +} diff --git a/packages/app/src/panels/new-tab-panel.test.ts b/packages/app/src/panels/new-tab-panel.test.ts new file mode 100644 index 0000000000..ec0f50aae8 --- /dev/null +++ b/packages/app/src/panels/new-tab-panel.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { getLauncherActionOrder } from "@/panels/new-tab-action-order"; + +describe("getLauncherActionOrder", () => { + it("leads with the work companion tools in the Side panel", () => { + expect(getLauncherActionOrder(true)).toEqual([ + "changes", + "files", + "terminal", + "agent", + "browser", + "pullRequest", + ]); + }); + + it("leads with creating work in an ordinary pane", () => { + expect(getLauncherActionOrder(false)).toEqual([ + "agent", + "terminal", + "changes", + "files", + "browser", + "pullRequest", + ]); + }); +}); diff --git a/packages/app/src/panels/new-tab-panel.tsx b/packages/app/src/panels/new-tab-panel.tsx index e6e9cc170b..8c8ef2b79f 100644 --- a/packages/app/src/panels/new-tab-panel.tsx +++ b/packages/app/src/panels/new-tab-panel.tsx @@ -33,6 +33,7 @@ import { useShortcutKeys } from "@/hooks/use-shortcut-keys"; import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler"; import type { KeyboardActionDefinition } from "@/keyboard/keyboard-action-dispatcher"; import { usePaneContext, usePaneFocus } from "@/panels/pane-context"; +import { getLauncherActionOrder, type LauncherAction } from "@/panels/new-tab-action-order"; import type { PanelRegistration } from "@/panels/panel-registry"; import { resolvePluginIcon } from "@/plugins/icons"; import { useInstalledPlugins } from "@/plugins/registry"; @@ -92,6 +93,16 @@ const AGENT_SELECTION: NewTabSelection = { kind: "agent" }; const TERMINAL_SELECTION: NewTabSelection = { kind: "terminal" }; const BROWSER_SELECTION: NewTabSelection = { kind: "browser" }; +interface LauncherActionDefinition { + label: string; + Icon: LucideIcon; + disabled?: boolean; + hidden?: boolean; + selection: NewTabSelection; + shortcut?: string; + testID: string; +} + function LauncherIcon({ Icon, color = "" }: { Icon: LucideIcon; color?: string }) { return ; } @@ -255,7 +266,7 @@ function useNewTabDescriptor() { const NewTabPanel = memo(function NewTabPanel(): ReactElement { const { t } = useTranslation(); const router = useRouter(); - const { serverId, tabId } = usePaneContext(); + const { isSidePanel, serverId, tabId } = usePaneContext(); const { isInteractive, focusPane } = usePaneFocus(); const launcher = useContext(NewTabLauncherContext); invariant(launcher, "NewTabLauncherProvider is required"); @@ -273,6 +284,53 @@ const NewTabPanel = memo(function NewTabPanel(): ReactElement { const editTerminalProfiles = useCallback(() => { router.push(buildSettingsHostSectionRoute(serverId, "terminals") as Href); }, [router, serverId]); + const launcherActions: Record = { + agent: { + label: t("workspace.tabs.fallback.agent"), + Icon: SquarePen, + selection: AGENT_SELECTION, + shortcut: "workspace-tab-target-agent", + testID: "workspace-new-tab-agent", + }, + terminal: { + label: t("workspace.tabs.fallback.terminal"), + Icon: SquareTerminal, + disabled: launcher.terminalDisabled, + selection: TERMINAL_SELECTION, + shortcut: "workspace-terminal-new", + testID: "workspace-new-tab-terminal", + }, + changes: { + label: t("workspace.tabs.actions.changes"), + Icon: FileDiff, + hidden: !launcher.showChanges, + selection: CHANGES_SELECTION, + shortcut: "workspace-tab-target-changes", + testID: "workspace-new-tab-changes", + }, + files: { + label: t("workspace.tabs.actions.files"), + Icon: FolderTree, + selection: FILES_SELECTION, + shortcut: "workspace-tab-target-files", + testID: "workspace-new-tab-files", + }, + browser: { + label: t("workspace.tabs.fallback.browser"), + Icon: Globe, + hidden: !launcher.showBrowser, + selection: BROWSER_SELECTION, + shortcut: "workspace-tab-target-browser", + testID: "workspace-new-tab-browser", + }, + pullRequest: { + label: t("workspace.tabs.actions.pullRequest"), + Icon: GitPullRequest, + hidden: !launcher.showPullRequest, + selection: PULL_REQUEST_SELECTION, + testID: "workspace-new-tab-pull-request", + }, + }; useEffect(() => { if (!isWeb || !isInteractive) return; @@ -380,54 +438,11 @@ const NewTabPanel = memo(function NewTabPanel(): ReactElement { - - - {launcher.showChanges ? ( - - ) : null} - - {launcher.showBrowser ? ( - - ) : null} - {launcher.showPullRequest ? ( - - ) : null} + {getLauncherActionOrder(isSidePanel).map((actionId) => { + const action = launcherActions[actionId]; + if (action.hidden) return null; + return ; + })} {pluginPanels.map(({ pluginId, panel }) => ( { }); }); + it("exposes whether the pane is the Side panel to its content", () => { + container = document.createElement("div"); + document.body.appendChild(container); + root = createRoot(container); + + act(() => { + root?.render( + , + ); + }); + + expect(snapshots[0]?.paneContextValue.isSidePanel).toBe(true); + }); + it("keeps pane content mounted when a draft tab is retargeted in place", () => { container = document.createElement("div"); document.body.appendChild(container); diff --git a/packages/app/src/screens/workspace/workspace-pane-content.tsx b/packages/app/src/screens/workspace/workspace-pane-content.tsx index e8ab76500a..aa02409f19 100644 --- a/packages/app/src/screens/workspace/workspace-pane-content.tsx +++ b/packages/app/src/screens/workspace/workspace-pane-content.tsx @@ -23,6 +23,7 @@ export interface BuildWorkspacePaneContentModelInput { tab: WorkspaceTabDescriptor; normalizedServerId: string; normalizedWorkspaceId: string; + isSidePanel: boolean; fileNavigationRevision?: number; onOpenTab: (target: WorkspaceTabDescriptor["target"]) => void; onCloseCurrentTab: () => void; @@ -36,6 +37,7 @@ export function buildWorkspacePaneContentModel({ tab, normalizedServerId, normalizedWorkspaceId, + isSidePanel, fileNavigationRevision, onOpenTab, onCloseCurrentTab, @@ -53,6 +55,7 @@ export function buildWorkspacePaneContentModel({ paneContextValue: { serverId: normalizedServerId, workspaceId: normalizedWorkspaceId, + isSidePanel, tabId: tab.tabId, target: tab.target, state: tab.state, @@ -91,6 +94,7 @@ export function WorkspacePaneContent({ () => ({ serverId: paneContextValue.serverId, workspaceId: paneContextValue.workspaceId, + isSidePanel: paneContextValue.isSidePanel, tabId: paneContextValue.tabId, target: paneContextValue.target, state: paneContextValue.state, @@ -113,6 +117,7 @@ export function WorkspacePaneContent({ paneContextValue.target, paneContextValue.state, paneContextValue.workspaceId, + paneContextValue.isSidePanel, retargetCurrentTab, setCurrentTabState, ], diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index 5048b81908..003827f23d 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -64,6 +64,7 @@ import { findPaneById, getFocusedBrowserId, FOCUSED_PANE_PLACEMENT, + selectSidePanelPaneId, type WorkspaceLayout, type WorkspaceTabPlacement, useWorkspaceLayoutStore, @@ -1745,6 +1746,9 @@ function WorkspaceScreenContent({ const workspaceLayout = useWorkspaceLayoutStore((state) => persistenceKey ? (state.layoutByWorkspace[persistenceKey] ?? null) : null, ); + const sidePanelPaneId = useWorkspaceLayoutStore((state) => + persistenceKey ? selectSidePanelPaneId(state, persistenceKey) : null, + ); const hasHydratedWorkspaceLayoutStore = useWorkspaceLayoutStoreHydrated(); const workspaceSetupSnapshot = useWorkspaceSetupStore((state) => persistenceKey ? (state.snapshots[persistenceKey] ?? null) : null, @@ -3360,6 +3364,8 @@ function WorkspaceScreenContent({ tab: input.tab, normalizedServerId, normalizedWorkspaceId, + isSidePanel: + canRenderDesktopPaneSplits && input.paneId !== null && input.paneId === sidePanelPaneId, fileNavigationRevision: fileNavigationRevisionByTabId[input.tab.tabId] ?? 0, onOpenTab: (target) => { if (!persistenceKey) { @@ -3415,11 +3421,13 @@ function WorkspaceScreenContent({ navigateToTabId, normalizedServerId, normalizedWorkspaceId, + canRenderDesktopPaneSplits, openImportSheet, revealWorkspaceChildTab, persistenceKey, replaceWorkspaceTabTarget, setWorkspaceTabState, + sidePanelPaneId, ], ); const focusedPaneId = useMemo(