Add configurable terminal profiles

This commit is contained in:
Mohamed Boudra
2026-06-12 11:47:00 +07:00
parent 75aa626df5
commit fa800ae78e
30 changed files with 1412 additions and 211 deletions
@@ -16,10 +16,19 @@ import {
upsertCreatedTerminalPayload,
} from "@/screens/workspace/terminals/state";
interface TerminalProfileInput {
name: string;
command: string;
args?: string[];
}
interface PendingTerminalCreateInput {
paneId?: string;
profile?: TerminalProfileInput;
}
export type { TerminalProfileInput };
interface UseWorkspaceTerminalsInput {
client: DaemonClient | null;
isConnected: boolean;
@@ -110,6 +119,10 @@ export function useWorkspaceTerminals(input: UseWorkspaceTerminalsInput) {
if (!client || !workspaceDirectory) {
throw new Error(t("workspace.terminal.hostDisconnected"));
}
if (_input?.profile) {
const { name, command, args } = _input.profile;
return await client.createTerminal(workspaceDirectory, name, undefined, { command, args });
}
return await client.createTerminal(workspaceDirectory);
},
onSuccess: (payload, createInput) => {
@@ -1,4 +1,4 @@
import {
import React, {
useCallback,
useEffect,
useMemo,
@@ -20,6 +20,7 @@ import {
CopyX,
ArrowLeftToLine,
ArrowRightToLine,
ChevronDown,
Columns2,
Copy,
Pencil,
@@ -33,6 +34,7 @@ import {
} from "lucide-react-native";
import { StyleSheet, withUnistyles } from "react-native-unistyles";
import { useTranslation } from "react-i18next";
import { useRouter, type Href } from "expo-router";
import { SortableInlineList } from "@/components/sortable-inline-list";
import type {
DraggableListDragHandleProps,
@@ -46,6 +48,14 @@ import {
ContextMenuSeparator,
ContextMenuTrigger,
} from "@/components/ui/context-menu";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Shortcut } from "@/components/ui/shortcut";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useShortcutKeys } from "@/hooks/use-shortcut-keys";
@@ -67,6 +77,14 @@ import {
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
import type { Theme } from "@/styles/theme";
import { RenderProfile } from "@/utils/render-profiler";
import { useDaemonConfig } from "@/hooks/use-daemon-config";
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";
const DROPDOWN_WIDTH = 220;
const LOADING_TAB_LABEL_SKELETON_WIDTH = 80;
@@ -82,14 +100,33 @@ const ThemedCopyX = withUnistyles(CopyX);
const ThemedPencil = withUnistyles(Pencil);
const ThemedSquarePen = withUnistyles(SquarePen);
const ThemedSquareTerminal = withUnistyles(SquareTerminal);
const ThemedChevronDown = withUnistyles(ChevronDown);
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 });
const AGENT_ICON = <ThemedSquarePen size={14} uniProps={mutedColorMapping} />;
const TERMINAL_ICON = <ThemedSquareTerminal size={14} uniProps={mutedColorMapping} />;
const BROWSER_ICON = <ThemedGlobe size={14} uniProps={mutedColorMapping} />;
function newTabActionButtonStyle({ hovered, pressed }: PressableStateCallbackType) {
return [styles.newTabActionButton, (hovered || pressed) && styles.newTabActionButtonHovered];
}
@@ -103,38 +140,161 @@ 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 DropdownMenuProfileItem({
profile,
disabled,
onCreateTerminalWithProfile,
}: DropdownMenuProfileItemProps) {
const handleSelect = useCallback(() => {
onCreateTerminalWithProfile({
name: profile.name,
command: profile.command,
args: profile.args,
});
}, [onCreateTerminalWithProfile, profile]);
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]);
return (
<DropdownMenuItem leading={leading} disabled={disabled} onSelect={handleSelect}>
{profile.name}
</DropdownMenuItem>
);
}
interface WorkspaceInlineAddTabButtonProps {
shortcutKeys: ShortcutKey[][] | null;
onPress: () => void;
onCreateAgentTab: () => void;
onCreateTerminal: () => void;
onCreateBrowser: () => void;
onCreateTerminalWithProfile: (profile: TerminalProfileInput) => void;
onEditProfiles: () => void;
normalizedServerId: string;
showCreateBrowserTab: boolean;
terminalDisabled: boolean;
onLayout: (event: LayoutChangeEvent) => void;
}
function WorkspaceInlineAddTabButton({
shortcutKeys,
onPress,
onCreateAgentTab,
onCreateTerminal,
onCreateBrowser,
onCreateTerminalWithProfile,
onEditProfiles,
normalizedServerId,
showCreateBrowserTab,
terminalDisabled,
onLayout,
}: WorkspaceInlineAddTabButtonProps) {
const { t } = useTranslation();
const { config } = useDaemonConfig(normalizedServerId);
const profiles = useMemo(
() => resolveTerminalProfiles(config?.terminalProfiles),
[config?.terminalProfiles],
);
const tooltipText = t("workspace.tabs.actions.newAgent");
return (
<View style={styles.inlineAddButton} onLayout={onLayout}>
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger
testID="workspace-new-agent-tab-inline"
onPress={onPress}
onPress={onCreateAgentTab}
accessibilityRole="button"
accessibilityLabel="New agent tab"
accessibilityLabel={tooltipText}
style={inlineAddActionButtonStyle}
>
<ThemedPlus size={16} uniProps={mutedColorMapping} />
<ThemedPlus size={14} uniProps={mutedColorMapping} />
</TooltipTrigger>
<TooltipContent side="bottom" align="center" offset={8}>
<View style={styles.newTabTooltipRow}>
<Text style={styles.newTabTooltipText}>New agent tab</Text>
<Text style={styles.newTabTooltipText}>{tooltipText}</Text>
{shortcutKeys ? (
<Shortcut chord={shortcutKeys} style={styles.newTabTooltipShortcut} />
) : null}
</View>
</TooltipContent>
</Tooltip>
<DropdownMenu>
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger asChild triggerRefProp="triggerRef">
<DropdownMenuTrigger
testID="workspace-new-tab-menu-trigger"
accessibilityRole="button"
accessibilityLabel={t("workspace.tabs.actions.moreActions")}
style={inlineAddActionButtonStyle}
>
<ThemedChevronDown size={14} uniProps={mutedColorMapping} />
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom" align="center" offset={8}>
<Text style={styles.newTabTooltipText}>{t("workspace.tabs.actions.moreActions")}</Text>
</TooltipContent>
</Tooltip>
<DropdownMenuContent side="bottom" align="end" offset={4} minWidth={200}>
<DropdownMenuItem
testID="workspace-new-tab-menu-agent"
leading={AGENT_ICON}
onSelect={onCreateAgentTab}
>
{t("workspace.tabs.actions.newAgent")}
</DropdownMenuItem>
<DropdownMenuItem
testID="workspace-new-tab-menu-terminal"
leading={TERMINAL_ICON}
disabled={terminalDisabled}
onSelect={terminalDisabled ? undefined : onCreateTerminal}
>
{t("workspace.tabs.actions.newTerminal")}
</DropdownMenuItem>
{showCreateBrowserTab ? (
<DropdownMenuItem
testID="workspace-new-tab-menu-browser"
leading={BROWSER_ICON}
onSelect={onCreateBrowser}
>
{t("workspace.tabs.actions.newBrowser")}
</DropdownMenuItem>
) : null}
<DropdownMenuSeparator />
<DropdownMenuLabel>{t("workspace.tabs.actions.terminalProfilesMenu")}</DropdownMenuLabel>
{profiles.map((profile) => (
<DropdownMenuProfileItem
key={profile.id}
profile={profile}
disabled={terminalDisabled}
onCreateTerminalWithProfile={onCreateTerminalWithProfile}
/>
))}
<DropdownMenuSeparator />
<DropdownMenuItem testID="workspace-new-tab-menu-edit-profiles" onSelect={onEditProfiles}>
{t("workspace.tabs.actions.editTerminalProfiles")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</View>
);
}
@@ -194,6 +354,40 @@ export interface WorkspaceDesktopTabRowItem {
isClosingTab: boolean;
}
interface SplitActionButtonProps {
onPress: () => void;
label: string;
shortcutKeys: ShortcutKey[][] | null;
icon: "split-right" | "split-down";
}
function SplitActionButton({ onPress, label, shortcutKeys, icon }: SplitActionButtonProps) {
return (
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger
onPress={onPress}
accessibilityRole="button"
accessibilityLabel={label}
style={newTabActionButtonStyle}
>
{icon === "split-right" ? (
<ThemedColumns2 size={14} uniProps={mutedColorMapping} />
) : (
<ThemedRows2 size={14} uniProps={mutedColorMapping} />
)}
</TooltipTrigger>
<TooltipContent side="bottom" align="center" offset={8}>
<View style={styles.newTabTooltipRow}>
<Text style={styles.newTabTooltipText}>{label}</Text>
{shortcutKeys ? (
<Shortcut chord={shortcutKeys} style={styles.newTabTooltipShortcut} />
) : null}
</View>
</TooltipContent>
</Tooltip>
);
}
interface WorkspaceDesktopTabsRowProps {
paneId?: string;
isFocused?: boolean;
@@ -211,7 +405,7 @@ interface WorkspaceDesktopTabsRowProps {
onCloseTabsToRight: (tabId: string) => Promise<void> | void;
onCloseOtherTabs: (tabId: string) => Promise<void> | void;
onCreateDraftTab: (input: { paneId?: string }) => void;
onCreateTerminalTab: (input: { paneId?: string }) => void;
onCreateTerminalTab: (input: { paneId?: string; profile?: TerminalProfileInput }) => void;
onCreateBrowserTab: (input: { paneId?: string }) => void;
showCreateBrowserTab?: boolean;
disableCreateTerminal?: boolean;
@@ -539,8 +733,8 @@ export function WorkspaceDesktopTabsRow({
showPaneSplitActions = true,
}: WorkspaceDesktopTabsRowProps) {
const { t } = useTranslation();
const router = useRouter();
const newTabKeys = useShortcutKeys("workspace-tab-new");
const newTerminalKeys = useShortcutKeys("workspace-terminal-new");
const splitRightKeys = useShortcutKeys("workspace-pane-split-right");
const splitDownKeys = useShortcutKeys("workspace-pane-split-down");
const [tabsContainerWidth, setTabsContainerWidth] = useState<number>(0);
@@ -641,19 +835,22 @@ export function WorkspaceDesktopTabsRow({
onCreateTerminalTab({ paneId });
}, [onCreateTerminalTab, paneId]);
const handleCreateTerminalWithProfile = useCallback(
(profile: TerminalProfileInput) => {
onCreateTerminalTab({ paneId, profile });
},
[onCreateTerminalTab, paneId],
);
const handleEditProfiles = useCallback(() => {
router.push(buildSettingsHostSectionRoute(normalizedServerId, "terminals") as Href);
}, [normalizedServerId, router]);
const handleCreateBrowser = useCallback(() => {
onCreateBrowserTab({ paneId });
}, [onCreateBrowserTab, paneId]);
const terminalDisabled = disableCreateTerminal || isWaitingOnTerminalReadiness;
const newTerminalActionButtonStyle = useCallback(
({ hovered, pressed }: PressableStateCallbackType) => [
styles.newTabActionButton,
terminalDisabled && styles.newTabActionButtonDisabled,
(hovered || pressed) && styles.newTabActionButtonHovered,
],
[terminalDisabled],
);
const renderTab = useCallback(
({
@@ -762,120 +959,32 @@ export function WorkspaceDesktopTabsRow({
/>
<WorkspaceInlineAddTabButton
shortcutKeys={newTabKeys}
onPress={handleCreateAgentTab}
onCreateAgentTab={handleCreateAgentTab}
onCreateTerminal={handleCreateTerminal}
onCreateBrowser={handleCreateBrowser}
onCreateTerminalWithProfile={handleCreateTerminalWithProfile}
onEditProfiles={handleEditProfiles}
normalizedServerId={normalizedServerId}
showCreateBrowserTab={showCreateBrowserTab}
terminalDisabled={terminalDisabled}
onLayout={handleInlineAddButtonLayout}
/>
</ScrollView>
<View style={styles.tabsActions} onLayout={handleTabsActionsLayout}>
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger
testID="workspace-new-agent-tab"
onPress={handleCreateAgentTab}
accessibilityRole="button"
accessibilityLabel={t("workspace.tabs.actions.newAgent")}
style={newTabActionButtonStyle}
>
<ThemedSquarePen size={14} uniProps={mutedColorMapping} />
</TooltipTrigger>
<TooltipContent side="bottom" align="center" offset={8}>
<View style={styles.newTabTooltipRow}>
<Text style={styles.newTabTooltipText}>{t("workspace.tabs.actions.newAgent")}</Text>
{newTabKeys ? (
<Shortcut chord={newTabKeys} style={styles.newTabTooltipShortcut} />
) : null}
</View>
</TooltipContent>
</Tooltip>
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger
testID="workspace-new-terminal"
onPress={handleCreateTerminal}
disabled={terminalDisabled}
accessibilityRole="button"
accessibilityLabel={
isWaitingOnTerminalReadiness
? t("workspace.tabs.actions.preparingTerminal")
: t("workspace.tabs.actions.newTerminal")
}
style={newTerminalActionButtonStyle}
>
<ThemedSquareTerminal size={14} uniProps={mutedColorMapping} />
</TooltipTrigger>
<TooltipContent side="bottom" align="center" offset={8}>
<View style={styles.newTabTooltipRow}>
<Text style={styles.newTabTooltipText}>
{isWaitingOnTerminalReadiness
? t("workspace.tabs.actions.preparingTerminalTooltip")
: t("workspace.tabs.actions.newTerminal")}
</Text>
{newTerminalKeys ? (
<Shortcut chord={newTerminalKeys} style={styles.newTabTooltipShortcut} />
) : null}
</View>
</TooltipContent>
</Tooltip>
{showCreateBrowserTab ? (
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger
testID="workspace-new-browser"
onPress={handleCreateBrowser}
accessibilityRole="button"
accessibilityLabel={t("workspace.tabs.actions.newBrowser")}
style={newTabActionButtonStyle}
>
<ThemedGlobe size={14} uniProps={mutedColorMapping} />
</TooltipTrigger>
<TooltipContent side="bottom" align="center" offset={8}>
<View style={styles.newTabTooltipRow}>
<Text style={styles.newTabTooltipText}>
{t("workspace.tabs.actions.newBrowser")}
</Text>
</View>
</TooltipContent>
</Tooltip>
) : null}
{showPaneSplitActions ? (
<>
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger
onPress={onSplitRight}
accessibilityRole="button"
accessibilityLabel={t("workspace.tabs.actions.splitRight")}
style={newTabActionButtonStyle}
>
<ThemedColumns2 size={14} uniProps={mutedColorMapping} />
</TooltipTrigger>
<TooltipContent side="bottom" align="center" offset={8}>
<View style={styles.newTabTooltipRow}>
<Text style={styles.newTabTooltipText}>
{t("workspace.tabs.actions.splitRight")}
</Text>
{splitRightKeys ? (
<Shortcut chord={splitRightKeys} style={styles.newTabTooltipShortcut} />
) : null}
</View>
</TooltipContent>
</Tooltip>
<Tooltip delayDuration={0} enabledOnDesktop enabledOnMobile={false}>
<TooltipTrigger
onPress={onSplitDown}
accessibilityRole="button"
accessibilityLabel={t("workspace.tabs.actions.splitDown")}
style={newTabActionButtonStyle}
>
<ThemedRows2 size={14} uniProps={mutedColorMapping} />
</TooltipTrigger>
<TooltipContent side="bottom" align="center" offset={8}>
<View style={styles.newTabTooltipRow}>
<Text style={styles.newTabTooltipText}>
{t("workspace.tabs.actions.splitDown")}
</Text>
{splitDownKeys ? (
<Shortcut chord={splitDownKeys} style={styles.newTabTooltipShortcut} />
) : null}
</View>
</TooltipContent>
</Tooltip>
<SplitActionButton
icon="split-right"
onPress={onSplitRight}
label={t("workspace.tabs.actions.splitRight")}
shortcutKeys={splitRightKeys}
/>
<SplitActionButton
icon="split-down"
onPress={onSplitDown}
label={t("workspace.tabs.actions.splitDown")}
shortcutKeys={splitDownKeys}
/>
</>
) : null}
</View>
@@ -1039,9 +1148,9 @@ const styles = StyleSheet.create((theme) => ({
paddingHorizontal: theme.spacing[2],
},
inlineAddButton: {
paddingHorizontal: theme.spacing[1],
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
paddingHorizontal: theme.spacing[1],
},
tab: {
paddingHorizontal: theme.spacing[3],
@@ -1179,6 +1288,10 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
},
terminalProfileIconWrapper: {
width: 14,
height: 14,
},
}));
const TAB_DROP_INDICATOR_BEFORE_STYLE = [styles.tabDropIndicator, styles.tabDropIndicatorBefore];
@@ -50,6 +50,7 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
@@ -172,9 +173,22 @@ import { isAbsolutePath } from "@/utils/path";
import { useIsCompactFormFactor, supportsDesktopPaneSplits } from "@/constants/layout";
import { getIsElectron, isNative, isWeb } from "@/constants/platform";
import { useContainerWidthBelow } from "@/hooks/use-container-width";
import { buildHostRootRoute, buildSettingsHostRoute } from "@/utils/host-routes";
import {
buildHostRootRoute,
buildSettingsHostRoute,
buildSettingsHostSectionRoute,
} from "@/utils/host-routes";
import { canCreateWorkspaceTerminal } from "@/screens/workspace/terminals/state";
import { useWorkspaceTerminals } from "@/screens/workspace/terminals/use-workspace-terminals";
import {
useWorkspaceTerminals,
type TerminalProfileInput,
} from "@/screens/workspace/terminals/use-workspace-terminals";
import { useDaemonConfig } from "@/hooks/use-daemon-config";
import {
getTerminalProfileIcon,
resolveTerminalProfiles,
} from "@getpaseo/protocol/terminal-profiles";
import { getProviderIcon } from "@/components/provider-icons";
import {
createWorkspaceFileTabTarget,
normalizeWorkspaceFileLocation,
@@ -241,6 +255,19 @@ const ThemedSettings = withUnistyles(Settings);
const ThemedPanelRight = withUnistyles(PanelRight);
const ThemedSourceControlPanelIcon = withUnistyles(SourceControlPanelIcon);
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 });
@@ -927,6 +954,7 @@ function useCloseTabs(): UseCloseTabsResult {
}
interface WorkspaceHeaderMenuProps {
normalizedServerId: string;
normalizedWorkspaceId: string;
currentBranchName: string | null;
showWorkspaceSetup: boolean;
@@ -942,12 +970,55 @@ interface WorkspaceHeaderMenuProps {
menuSettingsIcon: ReactElement;
onCreateDraftTab: () => void;
onCreateTerminal: () => void;
onCreateTerminalWithProfile: (profile: TerminalProfileInput) => void;
onCreateBrowser: () => void;
onOpenImportSheet: () => void;
onCopyWorkspacePath: () => void;
onCopyBranchName: () => void;
onOpenSetupTab: () => void;
}
interface HeaderMenuProfileItemProps {
profile: { id: string; name: string; command: string; args?: string[]; icon?: string };
disabled: boolean;
onCreateTerminalWithProfile: (profile: TerminalProfileInput) => void;
}
function HeaderMenuProfileItem({
profile,
disabled,
onCreateTerminalWithProfile,
}: HeaderMenuProfileItemProps) {
const handleSelect = useCallback(() => {
onCreateTerminalWithProfile({
name: profile.name,
command: profile.command,
args: profile.args,
});
}, [onCreateTerminalWithProfile, profile]);
const icon = getTerminalProfileIcon(profile);
const leading = useMemo(() => {
if (!icon) {
return (
<View style={styles.headerMenuProfileIconWrapper}>
<ThemedSquareTerminal size={16} uniProps={mutedColorMapping} />
</View>
);
}
return (
<View style={styles.headerMenuProfileIconWrapper}>
<ThemedDynamicProviderIcon iconKey={icon} size={16} uniProps={mutedColorMapping} />
</View>
);
}, [icon]);
return (
<DropdownMenuItem leading={leading} disabled={disabled} onSelect={handleSelect}>
{profile.name}
</DropdownMenuItem>
);
}
function WorkspaceHeaderMenuTriggerIcon({
hovered,
@@ -964,6 +1035,7 @@ function WorkspaceHeaderMenuTriggerIcon({
}
function WorkspaceHeaderMenu({
normalizedServerId,
normalizedWorkspaceId,
currentBranchName,
showWorkspaceSetup,
@@ -979,6 +1051,7 @@ function WorkspaceHeaderMenu({
menuSettingsIcon,
onCreateDraftTab,
onCreateTerminal,
onCreateTerminalWithProfile,
onCreateBrowser,
onOpenImportSheet,
onCopyWorkspacePath,
@@ -986,6 +1059,17 @@ function WorkspaceHeaderMenu({
onOpenSetupTab,
}: WorkspaceHeaderMenuProps) {
const { t } = useTranslation();
const router = useRouter();
const { config } = useDaemonConfig(normalizedServerId);
const profiles = useMemo(
() => resolveTerminalProfiles(config?.terminalProfiles),
[config?.terminalProfiles],
);
const handleEditProfiles = useCallback(() => {
router.push(buildSettingsHostSectionRoute(normalizedServerId, "terminals") as Href);
}, [normalizedServerId, router]);
const renderTriggerIcon = useCallback(
({ hovered, open }: { hovered: boolean; open: boolean }) => (
<WorkspaceHeaderMenuTriggerIcon hovered={hovered} open={open} isMobile={isMobile} />
@@ -1011,6 +1095,7 @@ function WorkspaceHeaderMenu({
>
{t("workspace.header.actions.newAgent")}
</DropdownMenuItem>
<DropdownMenuLabel>{t("workspace.tabs.actions.terminalProfilesMenu")}</DropdownMenuLabel>
<DropdownMenuItem
testID="workspace-header-new-terminal"
leading={menuNewTerminalIcon}
@@ -1019,6 +1104,22 @@ function WorkspaceHeaderMenu({
>
{t("workspace.header.actions.newTerminal")}
</DropdownMenuItem>
{profiles.map((profile) => (
<HeaderMenuProfileItem
key={profile.id}
profile={profile}
disabled={createTerminalDisabled}
onCreateTerminalWithProfile={onCreateTerminalWithProfile}
/>
))}
<DropdownMenuSeparator />
<DropdownMenuItem
testID="workspace-header-edit-terminal-profiles"
onSelect={handleEditProfiles}
>
{t("workspace.tabs.actions.editTerminalProfiles")}
</DropdownMenuItem>
<DropdownMenuSeparator />
{showCreateBrowserTab ? (
<DropdownMenuItem
testID="workspace-header-new-browser"
@@ -1094,6 +1195,7 @@ interface WorkspaceHeaderTitleBarProps {
menuSettingsIcon: ReactElement;
onCreateDraftTab: () => void;
onCreateTerminal: () => void;
onCreateTerminalWithProfile: (profile: TerminalProfileInput) => void;
onCreateBrowser: () => void;
onOpenImportSheet: () => void;
onCopyWorkspacePath: () => void;
@@ -1128,6 +1230,7 @@ function WorkspaceHeaderTitleBar({
menuSettingsIcon,
onCreateDraftTab,
onCreateTerminal,
onCreateTerminalWithProfile,
onCreateBrowser,
onOpenImportSheet,
onCopyWorkspacePath,
@@ -1165,6 +1268,7 @@ function WorkspaceHeaderTitleBar({
)}
<View style={styles.compactHeaderMenuCluster}>
<WorkspaceHeaderMenu
normalizedServerId={normalizedServerId}
normalizedWorkspaceId={normalizedWorkspaceId}
currentBranchName={currentBranchName}
showWorkspaceSetup={showWorkspaceSetup}
@@ -1180,6 +1284,7 @@ function WorkspaceHeaderTitleBar({
menuSettingsIcon={menuSettingsIcon}
onCreateDraftTab={onCreateDraftTab}
onCreateTerminal={onCreateTerminal}
onCreateTerminalWithProfile={onCreateTerminalWithProfile}
onCreateBrowser={onCreateBrowser}
onOpenImportSheet={onOpenImportSheet}
onCopyWorkspacePath={onCopyWorkspacePath}
@@ -2416,6 +2521,13 @@ function WorkspaceScreenContent({
const handleCreateTerminal = useStableEvent(createTerminal);
const handleCreateTerminalWithProfile = useCallback(
(profile: TerminalProfileInput) => {
createTerminal({ profile });
},
[createTerminal],
);
const handleCreateBrowserTab = useCallback(
(input?: { paneId?: string }) => {
if (!persistenceKey || !getIsElectron()) {
@@ -3508,6 +3620,7 @@ function WorkspaceScreenContent({
menuSettingsIcon={menuSettingsIcon}
onCreateDraftTab={handleCreateDraftTab}
onCreateTerminal={handleCreateTerminal}
onCreateTerminalWithProfile={handleCreateTerminalWithProfile}
onCreateBrowser={handleCreateBrowserTab}
onOpenImportSheet={openImportSheet}
onCopyWorkspacePath={handleCopyWorkspacePath}
@@ -3845,6 +3958,10 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
},
headerMenuProfileIconWrapper: {
width: 16,
height: 16,
},
tabsContainer: {
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,