mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-23 01:32:40 +00:00
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:
@@ -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",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user