diff --git a/packages/app/src/screens/sessions-screen.tsx b/packages/app/src/screens/sessions-screen.tsx
index 3f02f85e5..f47f1250c 100644
--- a/packages/app/src/screens/sessions-screen.tsx
+++ b/packages/app/src/screens/sessions-screen.tsx
@@ -51,6 +51,18 @@ function SessionsScreenContent({ serverId }: { serverId: string }) {
router.navigate(buildHostOpenProjectRoute(serverId));
}, [serverId]);
+ const listFooterComponent = useMemo(
+ () =>
+ hasMore ? (
+
+
+
+ ) : null,
+ [hasMore, loadMore, isLoadingMore],
+ );
+
return (
@@ -71,15 +83,7 @@ function SessionsScreenContent({ serverId }: { serverId: string }) {
showCheckoutInfo={false}
isRefreshing={isManualRefresh && isRevalidating}
onRefresh={handleRefresh}
- listFooterComponent={
- hasMore ? (
-
-
-
- ) : null
- }
+ listFooterComponent={listFooterComponent}
showAttentionIndicator={false}
/>
)}
diff --git a/packages/app/src/screens/settings-screen.tsx b/packages/app/src/screens/settings-screen.tsx
index ce6094ba7..f1e2df9ae 100644
--- a/packages/app/src/screens/settings-screen.tsx
+++ b/packages/app/src/screens/settings-screen.tsx
@@ -206,12 +206,12 @@ function ThemeMenuItem({
const handleSelect = useCallback(() => {
onChange(themeValue);
}, [onChange, themeValue]);
+ const leading = useMemo(
+ () => ,
+ [themeValue, iconSize, iconColor],
+ );
return (
- }
- >
+
{THEME_LABELS[themeValue]}
);
diff --git a/packages/app/src/screens/settings/host-page.tsx b/packages/app/src/screens/settings/host-page.tsx
index 927d506c7..160048370 100644
--- a/packages/app/src/screens/settings/host-page.tsx
+++ b/packages/app/src/screens/settings/host-page.tsx
@@ -571,6 +571,11 @@ function RestartDaemonCard({ host }: { host: HostProfile }) {
});
}, [daemonClient, host.label, host.serverId, isHostConnected, waitForDaemonRestart]);
+ const restartIcon = useMemo(
+ () => ,
+ [theme.iconSize.sm, theme.colors.foreground],
+ );
+
return (
@@ -583,7 +588,7 @@ function RestartDaemonCard({ host }: { host: HostProfile }) {
}
+ leftIcon={restartIcon}
onPress={handleRestart}
disabled={isRestarting || !daemonClient || !isConnected}
testID="host-page-restart-button"
@@ -697,6 +702,11 @@ function RemoveHostSection({ host, onRemoved }: { host: HostProfile; onRemoved?:
.finally(() => setIsRemoving(false));
}, [host.serverId, onRemoved, removeHost]);
+ const removeIcon = useMemo(
+ () => ,
+ [theme.iconSize.sm, theme.colors.destructive],
+ );
+
return (
@@ -710,7 +720,7 @@ function RemoveHostSection({ host, onRemoved }: { host: HostProfile; onRemoved?:
}
+ leftIcon={removeIcon}
textStyle={destructiveTextStyle}
onPress={handleOpenConfirm}
testID="host-page-remove-host-button"
diff --git a/packages/app/src/screens/settings/providers-section.tsx b/packages/app/src/screens/settings/providers-section.tsx
index 2eedaac32..5befca7ea 100644
--- a/packages/app/src/screens/settings/providers-section.tsx
+++ b/packages/app/src/screens/settings/providers-section.tsx
@@ -98,23 +98,35 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) {
const handleCloseDiagnostic = useCallback(() => setDiagnosticProvider(null), []);
- const refreshAction =
- hasServer && isConnected ? (
-
- {providerRefreshInFlight ? (
-
- ) : (
-
- )}
-
- ) : undefined;
+ const refreshAction = useMemo(
+ () =>
+ hasServer && isConnected ? (
+
+ {providerRefreshInFlight ? (
+
+ ) : (
+
+ )}
+
+ ) : undefined,
+ [
+ hasServer,
+ isConnected,
+ handleRefresh,
+ providerRefreshInFlight,
+ theme.iconSize.sm,
+ theme.colors.foregroundMuted,
+ ],
+ );
return (
<>
diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx
index 9ba566890..79a3ccaa2 100644
--- a/packages/app/src/screens/workspace/workspace-screen.tsx
+++ b/packages/app/src/screens/workspace/workspace-screen.tsx
@@ -89,12 +89,16 @@ import {
WorkspaceTabPresentationResolver,
WorkspaceTabIcon,
WorkspaceTabOptionRow,
+ type WorkspaceTabPresentation,
} from "@/screens/workspace/workspace-tab-presentation";
import {
WorkspaceDesktopTabsRow,
type WorkspaceDesktopTabRowItem,
} from "@/screens/workspace/workspace-desktop-tabs-row";
-import { buildWorkspaceTabMenuEntries } from "@/screens/workspace/workspace-tab-menu";
+import {
+ buildWorkspaceTabMenuEntries,
+ type WorkspaceTabMenuEntry,
+} from "@/screens/workspace/workspace-tab-menu";
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
import {
resolveWorkspaceHeaderRenderState,
@@ -289,6 +293,87 @@ function switcherTriggerStyle({ pressed }: { pressed?: boolean }) {
return [styles.switcherTrigger, Boolean(pressed) && styles.switcherTriggerPressed];
}
+function MobileTabTrailingAccessory({
+ menuTestIDBase,
+ presentationLabel,
+ menuEntries,
+ iconColor,
+ iconSizeSm,
+}: {
+ menuTestIDBase: string;
+ presentationLabel: string;
+ menuEntries: WorkspaceTabMenuEntry[];
+ iconColor: string;
+ iconSizeSm: number;
+}) {
+ return (
+
+
+
+
+
+ {menuEntries.map((entry) =>
+ entry.kind === "separator" ? (
+
+ ) : (
+
+ ),
+ )}
+
+
+ );
+}
+
+function MobileTabDropdownMenuItem({
+ entry,
+ iconColor,
+}: {
+ entry: Extract;
+ iconColor: string;
+}) {
+ const leading = useMemo(() => {
+ switch (entry.icon) {
+ case "copy":
+ return ;
+ case "rotate-cw":
+ return ;
+ case "arrow-left-to-line":
+ return ;
+ case "arrow-right-to-line":
+ return ;
+ case "copy-x":
+ return ;
+ case "x":
+ return ;
+ default:
+ return undefined;
+ }
+ }, [entry.icon, iconColor]);
+ const trailing = useMemo(
+ () => (entry.hint ? {entry.hint} : undefined),
+ [entry.hint],
+ );
+ return (
+
+ {entry.label}
+
+ );
+}
+
function MobileWorkspaceTabOption({
tab,
tabIndex,
@@ -339,75 +424,43 @@ function MobileWorkspaceTabOption({
onCloseOtherTabs,
});
+ const iconColor = theme.colors.foregroundMuted;
+ const iconSizeSm = theme.iconSize.sm;
+
+ const fallbackLabel = getFallbackTabOptionLabel(tab);
+ const trailingAccessory = useMemo(
+ () => (
+
+ ),
+ [menuTestIDBase, fallbackLabel, menuEntries, iconColor, iconSizeSm],
+ );
+
+ const renderPresentation = useCallback(
+ (presentation: WorkspaceTabPresentation) => (
+
+ ),
+ [selected, active, onPress, trailingAccessory],
+ );
+
return (
- {(presentation) => (
-
-
-
-
-
- {menuEntries.map((entry) =>
- entry.kind === "separator" ? (
-
- ) : (
- {
- const iconColor = theme.colors.foregroundMuted;
- switch (entry.icon) {
- case "copy":
- return ;
- case "rotate-cw":
- return ;
- case "arrow-left-to-line":
- return ;
- case "arrow-right-to-line":
- return ;
- case "copy-x":
- return ;
- case "x":
- return ;
- default:
- return undefined;
- }
- })()}
- trailing={
- entry.hint ? (
- {entry.hint}
- ) : undefined
- }
- >
- {entry.label}
-
- ),
- )}
-
-
- }
- />
- )}
+ {renderPresentation}
);
}
@@ -2275,6 +2328,160 @@ function WorkspaceScreenContent({
[mainBackgroundColor],
);
+ const menuNewAgentIcon = useMemo(
+ () => ,
+ [theme.colors.foregroundMuted],
+ );
+ const menuNewTerminalIcon = useMemo(
+ () => ,
+ [theme.colors.foregroundMuted],
+ );
+ const menuCopyIcon = useMemo(
+ () => ,
+ [theme.colors.foregroundMuted],
+ );
+ const menuSettingsIcon = useMemo(
+ () => ,
+ [theme.colors.foregroundMuted],
+ );
+
+ const headerRight = useMemo(
+ () => (
+
+ {!isMobile && workspaceDescriptor && workspaceDescriptor.scripts.length > 0 ? (
+
+ ) : null}
+ {!isMobile ? (
+
+ ) : null}
+ {!isMobile && isGitCheckout ? (
+ <>
+
+
+
+
+ {({ hovered, pressed }) => {
+ const active = isExplorerOpen || hovered || pressed;
+ const iconColor = active
+ ? theme.colors.foreground
+ : theme.colors.foregroundMuted;
+ return (
+ <>
+
+ {workspaceDescriptor?.diffStat ? (
+
+ ) : null}
+ >
+ );
+ }}
+
+
+
+
+ Toggle explorer
+
+
+
+
+ >
+ ) : null}
+ {!isMobile && !isGitCheckout ? (
+
+ {({ hovered }) => {
+ const color =
+ isExplorerOpen || hovered ? theme.colors.foreground : theme.colors.foregroundMuted;
+ return ;
+ }}
+
+ ) : null}
+ {isMobile ? (
+
+ {({ hovered }) => {
+ const color =
+ isExplorerOpen || hovered ? theme.colors.foreground : theme.colors.foregroundMuted;
+ return isGitCheckout ? (
+
+ ) : (
+
+ );
+ }}
+
+ ) : null}
+
+ ),
+ [
+ isMobile,
+ workspaceDescriptor,
+ normalizedServerId,
+ normalizedWorkspaceId,
+ liveTerminalIds,
+ handleScriptTerminalStarted,
+ handleViewScriptTerminal,
+ showCompactButtonLabels,
+ isGitCheckout,
+ handleToggleExplorer,
+ isExplorerOpen,
+ explorerToggleAccessibilityState,
+ theme.colors.foreground,
+ theme.colors.foregroundMuted,
+ theme.iconSize.md,
+ theme.iconSize.lg,
+ ],
+ );
+
return (
{isRouteFocused && isWeb && activeTabDescriptor ? (
@@ -2348,16 +2555,14 @@ function WorkspaceScreenContent({
}
+ leading={menuNewAgentIcon}
onSelect={handleCreateDraftTab}
>
New agent
- }
+ leading={menuNewTerminalIcon}
disabled={
createTerminalMutation.isPending || pendingTerminalCreateInput !== null
}
@@ -2367,7 +2572,7 @@ function WorkspaceScreenContent({
}
+ leading={menuCopyIcon}
disabled={!isAbsolutePath(normalizedWorkspaceId)}
onSelect={handleCopyWorkspacePath}
>
@@ -2376,7 +2581,7 @@ function WorkspaceScreenContent({
{currentBranchName ? (
}
+ leading={menuCopyIcon}
onSelect={handleCopyBranchName}
>
Copy branch name
@@ -2387,7 +2592,7 @@ function WorkspaceScreenContent({
}
+ leading={menuSettingsIcon}
onSelect={handleOpenSetupTab}
>
Show setup
@@ -2399,136 +2604,7 @@ function WorkspaceScreenContent({
>
}
- right={
-
- {!isMobile && workspaceDescriptor && workspaceDescriptor.scripts.length > 0 ? (
-
- ) : null}
- {!isMobile ? (
-
- ) : null}
- {!isMobile && isGitCheckout ? (
- <>
-
-
-
-
- {({ hovered, pressed }) => {
- const active = isExplorerOpen || hovered || pressed;
- const iconColor = active
- ? theme.colors.foreground
- : theme.colors.foregroundMuted;
- return (
- <>
-
- {workspaceDescriptor?.diffStat ? (
-
- ) : null}
- >
- );
- }}
-
-
-
-
- Toggle explorer
-
-
-
-
- >
- ) : null}
- {!isMobile && !isGitCheckout ? (
-
- {({ hovered }) => {
- const color =
- isExplorerOpen || hovered
- ? theme.colors.foreground
- : theme.colors.foregroundMuted;
- return ;
- }}
-
- ) : null}
- {isMobile ? (
-
- {({ hovered }) => {
- const color =
- isExplorerOpen || hovered
- ? theme.colors.foreground
- : theme.colors.foregroundMuted;
- return isGitCheckout ? (
-
- ) : (
-
- );
- }}
-
- ) : null}
-
- }
+ right={headerRight}
/>
)}