fix(app): prioritize side panel launcher actions

Lead with supporting workspace tools when the launcher is in the Side panel, while ordinary panes continue to lead with agent creation.
This commit is contained in:
Mohamed Boudra
2026-08-22 22:24:35 +02:00
parent b139046a3e
commit cc2012be7c
7 changed files with 133 additions and 50 deletions
@@ -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"];
}
@@ -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",
]);
});
});
+64 -49
View File
@@ -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 <Icon size={LAUNCHER_ICON_SIZE} color={color} />;
}
@@ -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<LauncherAction, LauncherActionDefinition> = {
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 {
<ScrollView contentContainerStyle={styles.scrollContent}>
<View style={styles.rail}>
<View style={styles.group}>
<LauncherRow
label={t("workspace.tabs.fallback.agent")}
Icon={SquarePen}
selection={AGENT_SELECTION}
shortcut="workspace-tab-target-agent"
testID="workspace-new-tab-agent"
/>
<LauncherRow
label={t("workspace.tabs.fallback.terminal")}
Icon={SquareTerminal}
disabled={launcher.terminalDisabled}
selection={TERMINAL_SELECTION}
shortcut="workspace-terminal-new"
testID="workspace-new-tab-terminal"
/>
{launcher.showChanges ? (
<LauncherRow
label={t("workspace.tabs.actions.changes")}
Icon={FileDiff}
selection={CHANGES_SELECTION}
shortcut="workspace-tab-target-changes"
testID="workspace-new-tab-changes"
/>
) : null}
<LauncherRow
label={t("workspace.tabs.actions.files")}
Icon={FolderTree}
selection={FILES_SELECTION}
shortcut="workspace-tab-target-files"
testID="workspace-new-tab-files"
/>
{launcher.showBrowser ? (
<LauncherRow
label={t("workspace.tabs.fallback.browser")}
Icon={Globe}
selection={BROWSER_SELECTION}
shortcut="workspace-tab-target-browser"
testID="workspace-new-tab-browser"
/>
) : null}
{launcher.showPullRequest ? (
<LauncherRow
label={t("workspace.tabs.actions.pullRequest")}
Icon={GitPullRequest}
selection={PULL_REQUEST_SELECTION}
testID="workspace-new-tab-pull-request"
/>
) : null}
{getLauncherActionOrder(isSidePanel).map((actionId) => {
const action = launcherActions[actionId];
if (action.hidden) return null;
return <LauncherRow key={actionId} {...action} />;
})}
{pluginPanels.map(({ pluginId, panel }) => (
<PluginLauncherRow
key={`${pluginId}:${panel.id}`}
+2
View File
@@ -7,6 +7,8 @@ import type { WorkspaceFileOpenRequest } from "@/workspace/file-open";
export interface PaneContextValue {
serverId: string;
workspaceId: string;
/** Whether this pane is the workspace's dedicated Side panel. */
isSidePanel: boolean;
tabId: string;
target: WorkspaceTabTarget;
state?: JsonValue;
@@ -55,11 +55,12 @@ const agentTab: WorkspaceTabDescriptor = {
target: { kind: "agent", agentId: "agent-a" },
};
function buildContent(tab: WorkspaceTabDescriptor = agentTab) {
function buildContent(tab: WorkspaceTabDescriptor = agentTab, isSidePanel = false) {
return buildWorkspacePaneContentModel({
tab,
normalizedServerId: "server-a",
normalizedWorkspaceId: "workspace-a",
isSidePanel,
onOpenTab: vi.fn(),
onCloseCurrentTab: vi.fn(),
onRetargetCurrentTab: vi.fn(),
@@ -122,6 +123,24 @@ describe("WorkspacePaneContent", () => {
});
});
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(
<WorkspacePaneContent
content={buildContent(agentTab, true)}
isPaneFocused
isWorkspaceFocused
/>,
);
});
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);
@@ -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,
],
@@ -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(