Add pinnable new-tab targets to the tab row and sidebar

Any new-tab target (new agent, terminal, browser, or a terminal profile)
can be pinned as a quick-launch button. Pins are global client app state
(persisted), not daemon config, so the same set shows on every workspace;
profile pins resolve per host and are dropped where that profile is absent.

The sidebar mirror launches via a navigate-then-launch intent store rather
than calling the create handlers directly: terminal creation only works in
the mounted/focused workspace, so the sidebar requests the launch and the
workspace screen consumes it on focus.
This commit is contained in:
Mohamed Boudra
2026-06-15 14:09:29 +07:00
parent 42969e40d5
commit 713a042dc5
22 changed files with 835 additions and 65 deletions
@@ -82,9 +82,13 @@ import {
getTerminalProfileIcon,
resolveTerminalProfiles,
} from "@getpaseo/protocol/terminal-profiles";
import { getProviderIcon } from "@/components/provider-icons";
import { buildSettingsHostSectionRoute } from "@/utils/host-routes";
import type { TerminalProfileInput } from "@/screens/workspace/terminals/use-workspace-terminals";
import { ProfileIcon, usePinnedLaunchers } from "@/workspace-pins/launch";
import { runPinnedTabTarget, type TabTargetHandlers } from "@/workspace-pins/run";
import type { PinnedTabTarget } from "@/workspace-pins/target";
import { PinnedTargetsRow } from "@/workspace-pins/pinned-targets-row";
import { PinnableMenuItem } from "@/workspace-pins/pinnable-menu-item";
const DROPDOWN_WIDTH = 220;
const LOADING_TAB_LABEL_SKELETON_WIDTH = 80;
@@ -105,21 +109,6 @@ const ThemedGlobe = withUnistyles(Globe);
const ThemedColumns2 = withUnistyles(Columns2);
const ThemedRows2 = withUnistyles(Rows2);
const ThemedPlus = withUnistyles(Plus);
const ThemedProfileSquareTerminal = withUnistyles(SquareTerminal);
interface DynamicProviderIconProps {
iconKey: string;
size: number;
color?: string;
}
function DynamicProviderIcon({ iconKey, size, color = "" }: DynamicProviderIconProps) {
const Icon = getProviderIcon(iconKey);
return <Icon size={size} color={color} />;
}
const ThemedDynamicProviderIcon = withUnistyles(DynamicProviderIcon);
const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground });
const mutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
@@ -127,6 +116,10 @@ const AGENT_ICON = <ThemedSquarePen size={14} uniProps={mutedColorMapping} />;
const TERMINAL_ICON = <ThemedSquareTerminal size={14} uniProps={mutedColorMapping} />;
const BROWSER_ICON = <ThemedGlobe size={14} uniProps={mutedColorMapping} />;
const DRAFT_TARGET: PinnedTabTarget = { kind: "draft" };
const TERMINAL_TARGET: PinnedTabTarget = { kind: "terminal" };
const BROWSER_TARGET: PinnedTabTarget = { kind: "browser" };
function newTabActionButtonStyle({ hovered, pressed }: PressableStateCallbackType) {
return [styles.newTabActionButton, (hovered || pressed) && styles.newTabActionButtonHovered];
}
@@ -140,46 +133,39 @@ function updateMeasuredWidth(setWidth: Dispatch<SetStateAction<number>>, event:
setWidth((current) => (Math.abs(current - nextWidth) > 1 ? nextWidth : current));
}
interface DropdownMenuProfileItemProps {
profile: { id: string; name: string; command: string; args?: string[]; icon?: string };
disabled?: boolean;
onCreateTerminalWithProfile: (profile: TerminalProfileInput) => void;
function ProfileLeadingIcon({ iconKey }: { iconKey: string | undefined }) {
return (
<View style={styles.terminalProfileIconWrapper}>
<ProfileIcon iconKey={iconKey} />
</View>
);
}
function DropdownMenuProfileItem({
profile,
disabled,
onCreateTerminalWithProfile,
}: DropdownMenuProfileItemProps) {
const handleSelect = useCallback(() => {
onCreateTerminalWithProfile({
name: profile.name,
command: profile.command,
args: profile.args,
});
}, [onCreateTerminalWithProfile, profile]);
interface PinnableProfileMenuItemProps {
profile: { id: string; name: string; command: string; args?: string[]; icon?: string };
disabled?: boolean;
onLaunch: (target: PinnedTabTarget) => void;
}
const icon = getTerminalProfileIcon(profile);
const leading = useMemo(() => {
if (!icon) {
return (
<View style={styles.terminalProfileIconWrapper}>
<ThemedProfileSquareTerminal size={14} uniProps={mutedColorMapping} />
</View>
);
}
return (
<View style={styles.terminalProfileIconWrapper}>
<ThemedDynamicProviderIcon iconKey={icon} size={14} uniProps={mutedColorMapping} />
</View>
);
}, [icon]);
function PinnableProfileMenuItem({ profile, disabled, onLaunch }: PinnableProfileMenuItemProps) {
const target = useMemo<PinnedTabTarget>(
() => ({ kind: "profile", profileId: profile.id }),
[profile.id],
);
const leading = useMemo(
() => <ProfileLeadingIcon iconKey={getTerminalProfileIcon(profile)} />,
[profile],
);
const handleSelect = useCallback(() => onLaunch(target), [onLaunch, target]);
return (
<DropdownMenuItem leading={leading} disabled={disabled} onSelect={handleSelect}>
{profile.name}
</DropdownMenuItem>
<PinnableMenuItem
target={target}
label={profile.name}
leading={leading}
disabled={disabled}
onSelect={handleSelect}
/>
);
}
@@ -215,6 +201,25 @@ function WorkspaceInlineAddTabButton({
[config?.terminalProfiles],
);
const handlers = useMemo<TabTargetHandlers>(
() => ({
createDraft: onCreateAgentTab,
createTerminal: onCreateTerminal,
createBrowser: onCreateBrowser,
createTerminalWithProfile: onCreateTerminalWithProfile,
}),
[onCreateAgentTab, onCreateBrowser, onCreateTerminal, onCreateTerminalWithProfile],
);
const onLaunch = useCallback(
(target: PinnedTabTarget) => {
runPinnedTabTarget(target, profiles, handlers);
},
[handlers, profiles],
);
const launchers = usePinnedLaunchers({ serverId: normalizedServerId, onLaunch });
const tooltipText = t("workspace.tabs.actions.newAgent");
return (
@@ -238,6 +243,7 @@ function WorkspaceInlineAddTabButton({
</View>
</TooltipContent>
</Tooltip>
<PinnedTargetsRow launchers={launchers} testIdPrefix="workspace-pinned-target" />
<DropdownMenu>
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger asChild triggerRefProp="triggerRef">
@@ -255,38 +261,38 @@ function WorkspaceInlineAddTabButton({
</TooltipContent>
</Tooltip>
<DropdownMenuContent side="bottom" align="end" offset={4} minWidth={200}>
<DropdownMenuItem
<PinnableMenuItem
testID="workspace-new-tab-menu-agent"
target={DRAFT_TARGET}
label={t("workspace.tabs.actions.newAgent")}
leading={AGENT_ICON}
onSelect={onCreateAgentTab}
>
{t("workspace.tabs.actions.newAgent")}
</DropdownMenuItem>
<DropdownMenuItem
/>
<PinnableMenuItem
testID="workspace-new-tab-menu-terminal"
target={TERMINAL_TARGET}
label={t("workspace.tabs.actions.newTerminal")}
leading={TERMINAL_ICON}
disabled={terminalDisabled}
onSelect={terminalDisabled ? undefined : onCreateTerminal}
>
{t("workspace.tabs.actions.newTerminal")}
</DropdownMenuItem>
/>
{showCreateBrowserTab ? (
<DropdownMenuItem
<PinnableMenuItem
testID="workspace-new-tab-menu-browser"
target={BROWSER_TARGET}
label={t("workspace.tabs.actions.newBrowser")}
leading={BROWSER_ICON}
onSelect={onCreateBrowser}
>
{t("workspace.tabs.actions.newBrowser")}
</DropdownMenuItem>
/>
) : null}
<DropdownMenuSeparator />
<DropdownMenuLabel>{t("workspace.tabs.actions.terminalProfilesMenu")}</DropdownMenuLabel>
{profiles.map((profile) => (
<DropdownMenuProfileItem
<PinnableProfileMenuItem
key={profile.id}
profile={profile}
disabled={terminalDisabled}
onCreateTerminalWithProfile={onCreateTerminalWithProfile}
onLaunch={onLaunch}
/>
))}
<DropdownMenuSeparator />
@@ -185,6 +185,8 @@ import {
resolveTerminalProfiles,
} from "@getpaseo/protocol/terminal-profiles";
import { getProviderIcon } from "@/components/provider-icons";
import { useLaunchIntentStore } from "@/workspace-pins/launch-intent-store";
import { runPinnedTabTarget, type TabTargetHandlers } from "@/workspace-pins/run";
import {
createWorkspaceFileTabTarget,
normalizeWorkspaceFileLocation,
@@ -1718,6 +1720,37 @@ function useWorkspaceCheckoutStatus(input: {
return { checkoutQuery, isCheckoutStatusLoading };
}
interface PinnedTabLaunchConsumerInput {
serverId: string;
persistenceKey: string | null;
isRouteFocused: boolean;
handlers: TabTargetHandlers;
}
function usePinnedTabLaunchConsumer({
serverId,
persistenceKey,
isRouteFocused,
handlers,
}: PinnedTabLaunchConsumerInput) {
const { config } = useDaemonConfig(serverId);
const profiles = useMemo(
() => resolveTerminalProfiles(config?.terminalProfiles),
[config?.terminalProfiles],
);
const pending = useLaunchIntentStore((state) => state.pending);
useEffect(() => {
if (!persistenceKey || !isRouteFocused || pending?.workspaceKey !== persistenceKey) {
return;
}
const target = useLaunchIntentStore.getState().consume(persistenceKey);
if (target) {
runPinnedTabTarget(target, profiles, handlers);
}
}, [handlers, isRouteFocused, pending, persistenceKey, profiles]);
}
function WorkspaceScreenContent({
serverId,
workspaceId,
@@ -2564,6 +2597,28 @@ function WorkspaceScreenContent({
[openWorkspaceTabFocused, persistenceKey],
);
const pinnedTabHandlers = useMemo<TabTargetHandlers>(
() => ({
createDraft: handleCreateDraftTab,
createTerminal: handleCreateTerminal,
createBrowser: handleCreateBrowserTab,
createTerminalWithProfile: handleCreateTerminalWithProfile,
}),
[
handleCreateBrowserTab,
handleCreateDraftTab,
handleCreateTerminal,
handleCreateTerminalWithProfile,
],
);
usePinnedTabLaunchConsumer({
serverId: normalizedServerId,
persistenceKey,
isRouteFocused,
handlers: pinnedTabHandlers,
});
useDesktopBrowserNewTabRequests({
enabled: Boolean(persistenceKey),
workspaceLayout,