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(