From c45e45833f58ea1d947125b99c347c8912145af4 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 17 Sep 2026 21:33:34 -0700 Subject: [PATCH 1/6] refactor(mobile): split terminal headers by platform --- .../terminal/TerminalHeader.android.tsx | 115 ++++++++++ .../features/terminal/TerminalHeader.ios.tsx | 98 +++++++++ .../src/features/terminal/TerminalHeader.tsx | 1 + .../features/terminal/TerminalHeader.types.ts | 16 ++ .../terminal/ThreadTerminalRouteScreen.tsx | 202 ++---------------- 5 files changed, 247 insertions(+), 185 deletions(-) create mode 100644 apps/mobile/src/features/terminal/TerminalHeader.android.tsx create mode 100644 apps/mobile/src/features/terminal/TerminalHeader.ios.tsx create mode 100644 apps/mobile/src/features/terminal/TerminalHeader.tsx create mode 100644 apps/mobile/src/features/terminal/TerminalHeader.types.ts diff --git a/apps/mobile/src/features/terminal/TerminalHeader.android.tsx b/apps/mobile/src/features/terminal/TerminalHeader.android.tsx new file mode 100644 index 000000000000..abb9b36f78dc --- /dev/null +++ b/apps/mobile/src/features/terminal/TerminalHeader.android.tsx @@ -0,0 +1,115 @@ +import type { MenuAction } from "@react-native-menu/menu"; +import { useMemo, useCallback } from "react"; +import { NativeStackScreenOptions } from "../../native/StackHeader"; +import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader"; +import { ControlPillMenu } from "../../components/ControlPill"; +import { + MAX_TERMINAL_FONT_SIZE, + MIN_TERMINAL_FONT_SIZE, + TERMINAL_FONT_SIZE_STEP, +} from "../../lib/appearancePreferences"; +import { AndroidWorkspaceSidebarButton } from "../layout/workspace-sidebar-toolbar"; +import { basename, getTerminalStatusLabel } from "./terminalMenu"; +import type { TerminalHeaderProps } from "./TerminalHeader.types"; + +export function TerminalHeader(props: TerminalHeaderProps) { + const { + fontSize, + workspaceRoot, + terminalId, + sessions, + onDecreaseFontSize, + onIncreaseFontSize, + onOpenNewTerminal, + onSelectTerminal, + } = props; + // Android mirror of the iOS NativeHeaderToolbar terminal menu below: text + // size, session switching, and "Open new terminal", rendered through the + // token-styled anchored menu (the native header items are iOS-only). + const androidTerminalMenuActions = useMemo( + () => [ + { + id: "text-size", + title: "Text size", + subactions: [ + { + id: "font-decrease", + title: `A- ${Math.max(MIN_TERMINAL_FONT_SIZE, fontSize - TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`, + attributes: fontSize <= MIN_TERMINAL_FONT_SIZE ? { disabled: true } : undefined, + }, + { + id: "font-increase", + title: `A+ ${Math.min(MAX_TERMINAL_FONT_SIZE, fontSize + TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`, + attributes: fontSize >= MAX_TERMINAL_FONT_SIZE ? { disabled: true } : undefined, + }, + ], + }, + ...terminalMenuSessions.map((session): MenuAction => ({ + id: `terminal-session:${session.terminalId}`, + title: session.displayLabel, + subtitle: [getTerminalStatusLabel({ status: session.status }), basename(session.cwd)] + .filter(Boolean) + .join(" · "), + state: session.terminalId === terminalId ? ("on" as const) : undefined, + })), + { + id: "terminal-new", + title: "Open new terminal", + image: "plus", + subtitle: `Start another shell in ${basename(workspaceRoot) ?? "this workspace"}`, + }, + ], + [fontSize, workspaceRoot, terminalId, sessions], + ); + + const handleAndroidTerminalMenuAction = useCallback( + (event: { nativeEvent: { event: string } }) => { + const id = event.nativeEvent.event; + if (id === "font-decrease") { + onDecreaseFontSize(); + return; + } + if (id === "font-increase") { + onIncreaseFontSize(); + return; + } + if (id === "terminal-new") { + onOpenNewTerminal(); + return; + } + if (id.startsWith("terminal-session:")) { + onSelectTerminal(id.slice("terminal-session:".length)); + } + }, + [onDecreaseFontSize, onIncreaseFontSize, onOpenNewTerminal, onSelectTerminal], + ); + + return ( + <> + + } + onBack={props.onCloseTerminal} + trailing={ + <> + {props.isEnvironmentReady ? ( + + + + ) : null} + + } + /> + + ); +} diff --git a/apps/mobile/src/features/terminal/TerminalHeader.ios.tsx b/apps/mobile/src/features/terminal/TerminalHeader.ios.tsx new file mode 100644 index 000000000000..50ae1c8d4d99 --- /dev/null +++ b/apps/mobile/src/features/terminal/TerminalHeader.ios.tsx @@ -0,0 +1,98 @@ +import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; +import { + MAX_TERMINAL_FONT_SIZE, + MIN_TERMINAL_FONT_SIZE, + TERMINAL_FONT_SIZE_STEP, +} from "../../lib/appearancePreferences"; +import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout"; +import { basename, getTerminalStatusLabel } from "./terminalMenu"; +import type { TerminalHeaderProps } from "./TerminalHeader.types"; + +export function TerminalHeader(props: TerminalHeaderProps) { + const { layout, panes, togglePrimarySidebar } = useAdaptiveWorkspaceLayout(); + return ( + <> + 0 ? props.subtitle : undefined, + }} + /> + + {layout.usesSplitView ? ( + + + + + ) : null} + + {props.isEnvironmentReady ? ( + + + + {getTerminalStatusLabel({ + status: props.status.status, + hasRunningSubprocess: props.status.hasRunningSubprocess, + })} + + + Text size + + {`A- ${Math.max(MIN_TERMINAL_FONT_SIZE, props.fontSize - TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`} + + = MAX_TERMINAL_FONT_SIZE} + discoverabilityLabel="Increase terminal text size" + onPress={props.onIncreaseFontSize} + > + {`A+ ${Math.min(MAX_TERMINAL_FONT_SIZE, props.fontSize + TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`} + + + {props.sessions.map((session) => ( + props.onSelectTerminal(session.terminalId)} + subtitle={[ + getTerminalStatusLabel({ status: session.status }), + basename(session.cwd), + ] + .filter(Boolean) + .join(" · ")} + > + {session.displayLabel} + + ))} + + Open new terminal + + + + ) : null} + + ); +} diff --git a/apps/mobile/src/features/terminal/TerminalHeader.tsx b/apps/mobile/src/features/terminal/TerminalHeader.tsx new file mode 100644 index 000000000000..d8c96905bb0d --- /dev/null +++ b/apps/mobile/src/features/terminal/TerminalHeader.tsx @@ -0,0 +1 @@ +export { TerminalHeader } from "./TerminalHeader.ios"; diff --git a/apps/mobile/src/features/terminal/TerminalHeader.types.ts b/apps/mobile/src/features/terminal/TerminalHeader.types.ts new file mode 100644 index 000000000000..4c0bfa242713 --- /dev/null +++ b/apps/mobile/src/features/terminal/TerminalHeader.types.ts @@ -0,0 +1,16 @@ +import type { TerminalMenuSession, getTerminalStatusLabel } from "./terminalMenu"; + +export interface TerminalHeaderProps { + readonly subtitle: string; + readonly isEnvironmentReady: boolean; + readonly fontSize: number; + readonly terminalId: string; + readonly sessions: ReadonlyArray; + readonly status: Parameters[0]; + readonly workspaceRoot: string; + readonly onCloseTerminal: () => void; + readonly onDecreaseFontSize: () => void; + readonly onIncreaseFontSize: () => void; + readonly onOpenNewTerminal: () => void; + readonly onSelectTerminal: (terminalId: string) => void; +} diff --git a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx index e490200acf8d..a8ab34d8443e 100644 --- a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx +++ b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx @@ -1,8 +1,7 @@ import { DEFAULT_TERMINAL_ID, EnvironmentId, ThreadId } from "@t3tools/contracts"; import { type KnownTerminalSession } from "@t3tools/client-runtime/state/terminal"; -import type { MenuAction } from "@react-native-menu/menu"; import { SymbolView } from "../../components/AppSymbol"; -import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; +import { TerminalHeader } from "./TerminalHeader"; import { StackActions, useNavigation, type StaticScreenProps } from "@react-navigation/native"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Alert, Platform, Pressable, View } from "react-native"; @@ -19,13 +18,11 @@ import { useKeyboardState, } from "react-native-keyboard-controller"; -import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader"; import { ComposerToolbarButton, ComposerToolbarRow, ComposerToolbarScroller, } from "../../components/ComposerToolbar"; -import { ControlPillMenu } from "../../components/ControlPill"; import { EmptyState } from "../../components/EmptyState"; import { GlassSurface } from "../../components/GlassSurface"; import { LoadingScreen } from "../../components/LoadingScreen"; @@ -38,12 +35,7 @@ import { terminalEnvironment } from "../../state/terminal"; import { useAtomCommand } from "../../state/use-atom-command"; import { useServerConfigs } from "../../state/entities"; import { useWorkspaceState } from "../../state/workspace"; -import { - MAX_TERMINAL_FONT_SIZE, - MIN_TERMINAL_FONT_SIZE, - TERMINAL_FONT_SIZE_STEP, - stepTerminalFontSize, -} from "../../lib/appearancePreferences"; +import { stepTerminalFontSize } from "../../lib/appearancePreferences"; import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; import { useAttachedTerminalSession, @@ -52,8 +44,6 @@ import { import { useThreadSelection } from "../../state/use-thread-selection"; import { useSelectedThreadDetail } from "../../state/use-thread-detail"; import { EnvironmentConnectionNotice } from "../connection/EnvironmentConnectionNotice"; -import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout"; -import { AndroidWorkspaceSidebarButton } from "../layout/workspace-sidebar-toolbar"; import { TerminalSurface } from "./NativeTerminalSurface"; import { getMobileTerminalTheme } from "./terminalTheme"; import { terminalDebugLog } from "./terminalDebugLog"; @@ -68,9 +58,7 @@ import { type PendingTerminalLaunch, } from "./terminalLaunchContext"; import { - basename, buildTerminalMenuSessions, - getTerminalStatusLabel, nextOpenTerminalId, previousLiveTerminalId, resolveTerminalSessionLabel, @@ -171,7 +159,6 @@ export function ThreadTerminalRouteScreen(props: ThreadTerminalRouteScreenProps) const openTerminal = useAtomCommand(terminalEnvironment.open, "terminal open"); const retryEnvironment = useAtomCommand(environmentCatalog.retryNow, "environment retry"); const { state: workspaceState } = useWorkspaceState(); - const { layout, panes, togglePrimarySidebar } = useAdaptiveWorkspaceLayout(); const params = props.route.params; const { selectedThread, selectedThreadProject, selectedEnvironmentConnection } = useThreadSelection(); @@ -483,7 +470,6 @@ export function ThreadTerminalRouteScreen(props: ThreadTerminalRouteScreenProps) ); const terminalTheme = getMobileTerminalTheme(themeId, appearanceScheme); - const usesNativeHeaderGlass = Platform.OS === "ios"; const pendingModifier = pendingModifierState.terminalId === terminalId ? pendingModifierState.value : null; const headerSubtitle = selectedThreadProject?.title ?? ""; @@ -1003,67 +989,6 @@ export function ThreadTerminalRouteScreen(props: ThreadTerminalRouteScreenProps) setTerminalFontSize(stepTerminalFontSize(fontSize, 1)); }, [fontSize, setTerminalFontSize]); - // Android mirror of the iOS NativeHeaderToolbar terminal menu below: text - // size, session switching, and "Open new terminal", rendered through the - // token-styled anchored menu (the native header items are iOS-only). - const androidTerminalMenuActions = useMemo( - () => [ - { - id: "text-size", - title: "Text size", - subactions: [ - { - id: "font-decrease", - title: `A- ${Math.max(MIN_TERMINAL_FONT_SIZE, fontSize - TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`, - attributes: fontSize <= MIN_TERMINAL_FONT_SIZE ? { disabled: true } : undefined, - }, - { - id: "font-increase", - title: `A+ ${Math.min(MAX_TERMINAL_FONT_SIZE, fontSize + TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`, - attributes: fontSize >= MAX_TERMINAL_FONT_SIZE ? { disabled: true } : undefined, - }, - ], - }, - ...terminalMenuSessions.map((session): MenuAction => ({ - id: `terminal-session:${session.terminalId}`, - title: session.displayLabel, - subtitle: [getTerminalStatusLabel({ status: session.status }), basename(session.cwd)] - .filter(Boolean) - .join(" · "), - state: session.terminalId === terminalId ? ("on" as const) : undefined, - })), - { - id: "terminal-new", - title: "Open new terminal", - image: "plus", - subtitle: `Start another shell in ${basename(selectedThreadProject?.workspaceRoot ?? null) ?? "this workspace"}`, - }, - ], - [fontSize, selectedThreadProject?.workspaceRoot, terminalId, terminalMenuSessions], - ); - - const handleAndroidTerminalMenuAction = useCallback( - (event: { nativeEvent: { event: string } }) => { - const id = event.nativeEvent.event; - if (id === "font-decrease") { - handleDecreaseFontSize(); - return; - } - if (id === "font-increase") { - handleIncreaseFontSize(); - return; - } - if (id === "terminal-new") { - handleOpenNewTerminal(); - return; - } - if (id.startsWith("terminal-session:")) { - handleSelectTerminal(id.slice("terminal-session:".length)); - } - }, - [handleDecreaseFontSize, handleIncreaseFontSize, handleOpenNewTerminal, handleSelectTerminal], - ); - const handleClearTerminal = useCallback(() => { if (!selectedThread) { return; @@ -1169,117 +1094,24 @@ export function ThreadTerminalRouteScreen(props: ThreadTerminalRouteScreenProps) }} /> ) : null} - 0 ? headerSubtitle : undefined, + - {Platform.OS === "android" ? ( - } - onBack={handleCloseTerminal} - trailing={ - <> - {isEnvironmentReady ? ( - - - - ) : null} - - } - /> - ) : null} - - {layout.usesSplitView ? ( - - - - - ) : null} - - {isEnvironmentReady ? ( - - - - {getTerminalStatusLabel({ - status: terminal.status, - hasRunningSubprocess: terminal.hasRunningSubprocess, - })} - - - Text size - - {`A- ${Math.max(MIN_TERMINAL_FONT_SIZE, fontSize - TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`} - - = MAX_TERMINAL_FONT_SIZE} - discoverabilityLabel="Increase terminal text size" - onPress={handleIncreaseFontSize} - > - {`A+ ${Math.min(MAX_TERMINAL_FONT_SIZE, fontSize + TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`} - - - {terminalMenuSessions.map((session) => ( - handleSelectTerminal(session.terminalId)} - subtitle={[ - getTerminalStatusLabel({ status: session.status }), - basename(session.cwd), - ] - .filter(Boolean) - .join(" · ")} - > - {session.displayLabel} - - ))} - - Open new terminal - - - - ) : null} - Date: Thu, 17 Sep 2026 21:37:29 -0700 Subject: [PATCH 2/6] fix(mobile): preserve terminal session menu after header split --- apps/mobile/src/features/terminal/TerminalHeader.android.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/mobile/src/features/terminal/TerminalHeader.android.tsx b/apps/mobile/src/features/terminal/TerminalHeader.android.tsx index abb9b36f78dc..dd38c5002691 100644 --- a/apps/mobile/src/features/terminal/TerminalHeader.android.tsx +++ b/apps/mobile/src/features/terminal/TerminalHeader.android.tsx @@ -23,9 +23,6 @@ export function TerminalHeader(props: TerminalHeaderProps) { onOpenNewTerminal, onSelectTerminal, } = props; - // Android mirror of the iOS NativeHeaderToolbar terminal menu below: text - // size, session switching, and "Open new terminal", rendered through the - // token-styled anchored menu (the native header items are iOS-only). const androidTerminalMenuActions = useMemo( () => [ { @@ -44,7 +41,7 @@ export function TerminalHeader(props: TerminalHeaderProps) { }, ], }, - ...terminalMenuSessions.map((session): MenuAction => ({ + ...sessions.map((session): MenuAction => ({ id: `terminal-session:${session.terminalId}`, title: session.displayLabel, subtitle: [getTerminalStatusLabel({ status: session.status }), basename(session.cwd)] From c0ddb551a0709e4982fceb27662a19bf7d913419 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 17 Sep 2026 21:55:57 -0700 Subject: [PATCH 3/6] refactor(mobile): share terminal header and menu configuration --- .../terminal/TerminalHeader.android.tsx | 112 ------------------ .../features/terminal/TerminalHeader.ios.tsx | 98 --------------- .../src/features/terminal/TerminalHeader.tsx | 75 +++++++++++- 3 files changed, 74 insertions(+), 211 deletions(-) delete mode 100644 apps/mobile/src/features/terminal/TerminalHeader.android.tsx delete mode 100644 apps/mobile/src/features/terminal/TerminalHeader.ios.tsx diff --git a/apps/mobile/src/features/terminal/TerminalHeader.android.tsx b/apps/mobile/src/features/terminal/TerminalHeader.android.tsx deleted file mode 100644 index dd38c5002691..000000000000 --- a/apps/mobile/src/features/terminal/TerminalHeader.android.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import type { MenuAction } from "@react-native-menu/menu"; -import { useMemo, useCallback } from "react"; -import { NativeStackScreenOptions } from "../../native/StackHeader"; -import { AndroidHeaderIconButton, AndroidScreenHeader } from "../../components/AndroidScreenHeader"; -import { ControlPillMenu } from "../../components/ControlPill"; -import { - MAX_TERMINAL_FONT_SIZE, - MIN_TERMINAL_FONT_SIZE, - TERMINAL_FONT_SIZE_STEP, -} from "../../lib/appearancePreferences"; -import { AndroidWorkspaceSidebarButton } from "../layout/workspace-sidebar-toolbar"; -import { basename, getTerminalStatusLabel } from "./terminalMenu"; -import type { TerminalHeaderProps } from "./TerminalHeader.types"; - -export function TerminalHeader(props: TerminalHeaderProps) { - const { - fontSize, - workspaceRoot, - terminalId, - sessions, - onDecreaseFontSize, - onIncreaseFontSize, - onOpenNewTerminal, - onSelectTerminal, - } = props; - const androidTerminalMenuActions = useMemo( - () => [ - { - id: "text-size", - title: "Text size", - subactions: [ - { - id: "font-decrease", - title: `A- ${Math.max(MIN_TERMINAL_FONT_SIZE, fontSize - TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`, - attributes: fontSize <= MIN_TERMINAL_FONT_SIZE ? { disabled: true } : undefined, - }, - { - id: "font-increase", - title: `A+ ${Math.min(MAX_TERMINAL_FONT_SIZE, fontSize + TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`, - attributes: fontSize >= MAX_TERMINAL_FONT_SIZE ? { disabled: true } : undefined, - }, - ], - }, - ...sessions.map((session): MenuAction => ({ - id: `terminal-session:${session.terminalId}`, - title: session.displayLabel, - subtitle: [getTerminalStatusLabel({ status: session.status }), basename(session.cwd)] - .filter(Boolean) - .join(" · "), - state: session.terminalId === terminalId ? ("on" as const) : undefined, - })), - { - id: "terminal-new", - title: "Open new terminal", - image: "plus", - subtitle: `Start another shell in ${basename(workspaceRoot) ?? "this workspace"}`, - }, - ], - [fontSize, workspaceRoot, terminalId, sessions], - ); - - const handleAndroidTerminalMenuAction = useCallback( - (event: { nativeEvent: { event: string } }) => { - const id = event.nativeEvent.event; - if (id === "font-decrease") { - onDecreaseFontSize(); - return; - } - if (id === "font-increase") { - onIncreaseFontSize(); - return; - } - if (id === "terminal-new") { - onOpenNewTerminal(); - return; - } - if (id.startsWith("terminal-session:")) { - onSelectTerminal(id.slice("terminal-session:".length)); - } - }, - [onDecreaseFontSize, onIncreaseFontSize, onOpenNewTerminal, onSelectTerminal], - ); - - return ( - <> - - } - onBack={props.onCloseTerminal} - trailing={ - <> - {props.isEnvironmentReady ? ( - - - - ) : null} - - } - /> - - ); -} diff --git a/apps/mobile/src/features/terminal/TerminalHeader.ios.tsx b/apps/mobile/src/features/terminal/TerminalHeader.ios.tsx deleted file mode 100644 index 50ae1c8d4d99..000000000000 --- a/apps/mobile/src/features/terminal/TerminalHeader.ios.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; -import { - MAX_TERMINAL_FONT_SIZE, - MIN_TERMINAL_FONT_SIZE, - TERMINAL_FONT_SIZE_STEP, -} from "../../lib/appearancePreferences"; -import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout"; -import { basename, getTerminalStatusLabel } from "./terminalMenu"; -import type { TerminalHeaderProps } from "./TerminalHeader.types"; - -export function TerminalHeader(props: TerminalHeaderProps) { - const { layout, panes, togglePrimarySidebar } = useAdaptiveWorkspaceLayout(); - return ( - <> - 0 ? props.subtitle : undefined, - }} - /> - - {layout.usesSplitView ? ( - - - - - ) : null} - - {props.isEnvironmentReady ? ( - - - - {getTerminalStatusLabel({ - status: props.status.status, - hasRunningSubprocess: props.status.hasRunningSubprocess, - })} - - - Text size - - {`A- ${Math.max(MIN_TERMINAL_FONT_SIZE, props.fontSize - TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`} - - = MAX_TERMINAL_FONT_SIZE} - discoverabilityLabel="Increase terminal text size" - onPress={props.onIncreaseFontSize} - > - {`A+ ${Math.min(MAX_TERMINAL_FONT_SIZE, props.fontSize + TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`} - - - {props.sessions.map((session) => ( - props.onSelectTerminal(session.terminalId)} - subtitle={[ - getTerminalStatusLabel({ status: session.status }), - basename(session.cwd), - ] - .filter(Boolean) - .join(" · ")} - > - {session.displayLabel} - - ))} - - Open new terminal - - - - ) : null} - - ); -} diff --git a/apps/mobile/src/features/terminal/TerminalHeader.tsx b/apps/mobile/src/features/terminal/TerminalHeader.tsx index d8c96905bb0d..629de41816fa 100644 --- a/apps/mobile/src/features/terminal/TerminalHeader.tsx +++ b/apps/mobile/src/features/terminal/TerminalHeader.tsx @@ -1 +1,74 @@ -export { TerminalHeader } from "./TerminalHeader.ios"; +import { ScreenHeader } from "../../components/ScreenHeader"; +import { + MAX_TERMINAL_FONT_SIZE, + MIN_TERMINAL_FONT_SIZE, + TERMINAL_FONT_SIZE_STEP, +} from "../../lib/appearancePreferences"; +import { basename, getTerminalStatusLabel } from "./terminalMenu"; +import type { TerminalHeaderProps } from "./TerminalHeader.types"; + +export function TerminalHeader(props: TerminalHeaderProps) { + return ( + = MAX_TERMINAL_FONT_SIZE, + onPress: props.onIncreaseFontSize, + }, + ], + }, + ...props.sessions.map((session) => ({ + id: `terminal-session:${session.terminalId}`, + title: session.displayLabel, + icon: "terminal", + subtitle: [ + getTerminalStatusLabel({ status: session.status }), + basename(session.cwd), + ] + .filter(Boolean) + .join(" · "), + selected: session.terminalId === props.terminalId, + onPress: () => props.onSelectTerminal(session.terminalId), + })), + { + id: "terminal-new", + title: "Open new terminal", + icon: "plus", + subtitle: `Start another shell in ${basename(props.workspaceRoot) ?? "this workspace"}`, + onPress: props.onOpenNewTerminal, + }, + ], + } + : undefined + } + /> + ); +} From e5faa136a7584adb66384f54653ec2fea047ea2d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 17 Sep 2026 22:09:59 -0700 Subject: [PATCH 4/6] refactor(mobile): use the shared header menu collection --- .../src/features/terminal/TerminalHeader.tsx | 98 ++++++++++--------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/apps/mobile/src/features/terminal/TerminalHeader.tsx b/apps/mobile/src/features/terminal/TerminalHeader.tsx index 629de41816fa..63aade6a38c6 100644 --- a/apps/mobile/src/features/terminal/TerminalHeader.tsx +++ b/apps/mobile/src/features/terminal/TerminalHeader.tsx @@ -18,55 +18,57 @@ export function TerminalHeader(props: TerminalHeaderProps) { icon: "xmark", separateBackground: true, }} - menu={ + menus={ props.isEnvironmentReady - ? { - title: "Terminal options", - icon: "terminal", - status: getTerminalStatusLabel(props.status), - items: [ - { - id: "text-size", - title: "Text size", - icon: "textformat.size", - inline: true, - items: [ - { - id: "font-decrease", - title: `A- ${Math.max(MIN_TERMINAL_FONT_SIZE, props.fontSize - TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`, - disabled: props.fontSize <= MIN_TERMINAL_FONT_SIZE, - onPress: props.onDecreaseFontSize, - }, - { - id: "font-increase", - title: `A+ ${Math.min(MAX_TERMINAL_FONT_SIZE, props.fontSize + TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`, - disabled: props.fontSize >= MAX_TERMINAL_FONT_SIZE, - onPress: props.onIncreaseFontSize, - }, - ], - }, - ...props.sessions.map((session) => ({ - id: `terminal-session:${session.terminalId}`, - title: session.displayLabel, - icon: "terminal", - subtitle: [ - getTerminalStatusLabel({ status: session.status }), - basename(session.cwd), - ] - .filter(Boolean) - .join(" · "), - selected: session.terminalId === props.terminalId, - onPress: () => props.onSelectTerminal(session.terminalId), - })), - { - id: "terminal-new", - title: "Open new terminal", - icon: "plus", - subtitle: `Start another shell in ${basename(props.workspaceRoot) ?? "this workspace"}`, - onPress: props.onOpenNewTerminal, - }, - ], - } + ? [ + { + title: "Terminal options", + icon: "terminal", + status: getTerminalStatusLabel(props.status), + items: [ + { + id: "text-size", + title: "Text size", + icon: "textformat.size", + inline: true, + items: [ + { + id: "font-decrease", + title: `A- ${Math.max(MIN_TERMINAL_FONT_SIZE, props.fontSize - TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`, + disabled: props.fontSize <= MIN_TERMINAL_FONT_SIZE, + onPress: props.onDecreaseFontSize, + }, + { + id: "font-increase", + title: `A+ ${Math.min(MAX_TERMINAL_FONT_SIZE, props.fontSize + TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`, + disabled: props.fontSize >= MAX_TERMINAL_FONT_SIZE, + onPress: props.onIncreaseFontSize, + }, + ], + }, + ...props.sessions.map((session) => ({ + id: `terminal-session:${session.terminalId}`, + title: session.displayLabel, + icon: "terminal", + subtitle: [ + getTerminalStatusLabel({ status: session.status }), + basename(session.cwd), + ] + .filter(Boolean) + .join(" · "), + selected: session.terminalId === props.terminalId, + onPress: () => props.onSelectTerminal(session.terminalId), + })), + { + id: "terminal-new", + title: "Open new terminal", + icon: "plus", + subtitle: `Start another shell in ${basename(props.workspaceRoot) ?? "this workspace"}`, + onPress: props.onOpenNewTerminal, + }, + ], + }, + ] : undefined } /> From fe9f03160a5a9aaf51cb9bad303e8ad4ac57b834 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 17 Sep 2026 22:21:28 -0700 Subject: [PATCH 5/6] fix(mobile): preserve running task status in terminal menus --- apps/mobile/src/features/terminal/TerminalHeader.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/mobile/src/features/terminal/TerminalHeader.tsx b/apps/mobile/src/features/terminal/TerminalHeader.tsx index 63aade6a38c6..70327e4ee447 100644 --- a/apps/mobile/src/features/terminal/TerminalHeader.tsx +++ b/apps/mobile/src/features/terminal/TerminalHeader.tsx @@ -51,7 +51,10 @@ export function TerminalHeader(props: TerminalHeaderProps) { title: session.displayLabel, icon: "terminal", subtitle: [ - getTerminalStatusLabel({ status: session.status }), + getTerminalStatusLabel({ + status: session.status, + hasRunningSubprocess: session.hasRunningSubprocess, + }), basename(session.cwd), ] .filter(Boolean) From d30e1fdc9b0a4582ef66df1209f1bc13e5bdaf1f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 17 Sep 2026 23:07:06 -0700 Subject: [PATCH 6/6] refactor(mobile): keep screen header configuration local --- .../src/features/terminal/TerminalHeader.tsx | 79 --------------- .../features/terminal/TerminalHeader.types.ts | 16 ---- .../terminal/ThreadTerminalRouteScreen.tsx | 95 ++++++++++++++++++- 3 files changed, 93 insertions(+), 97 deletions(-) delete mode 100644 apps/mobile/src/features/terminal/TerminalHeader.tsx delete mode 100644 apps/mobile/src/features/terminal/TerminalHeader.types.ts diff --git a/apps/mobile/src/features/terminal/TerminalHeader.tsx b/apps/mobile/src/features/terminal/TerminalHeader.tsx deleted file mode 100644 index 70327e4ee447..000000000000 --- a/apps/mobile/src/features/terminal/TerminalHeader.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { ScreenHeader } from "../../components/ScreenHeader"; -import { - MAX_TERMINAL_FONT_SIZE, - MIN_TERMINAL_FONT_SIZE, - TERMINAL_FONT_SIZE_STEP, -} from "../../lib/appearancePreferences"; -import { basename, getTerminalStatusLabel } from "./terminalMenu"; -import type { TerminalHeaderProps } from "./TerminalHeader.types"; - -export function TerminalHeader(props: TerminalHeaderProps) { - return ( - = MAX_TERMINAL_FONT_SIZE, - onPress: props.onIncreaseFontSize, - }, - ], - }, - ...props.sessions.map((session) => ({ - id: `terminal-session:${session.terminalId}`, - title: session.displayLabel, - icon: "terminal", - subtitle: [ - getTerminalStatusLabel({ - status: session.status, - hasRunningSubprocess: session.hasRunningSubprocess, - }), - basename(session.cwd), - ] - .filter(Boolean) - .join(" · "), - selected: session.terminalId === props.terminalId, - onPress: () => props.onSelectTerminal(session.terminalId), - })), - { - id: "terminal-new", - title: "Open new terminal", - icon: "plus", - subtitle: `Start another shell in ${basename(props.workspaceRoot) ?? "this workspace"}`, - onPress: props.onOpenNewTerminal, - }, - ], - }, - ] - : undefined - } - /> - ); -} diff --git a/apps/mobile/src/features/terminal/TerminalHeader.types.ts b/apps/mobile/src/features/terminal/TerminalHeader.types.ts deleted file mode 100644 index 4c0bfa242713..000000000000 --- a/apps/mobile/src/features/terminal/TerminalHeader.types.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { TerminalMenuSession, getTerminalStatusLabel } from "./terminalMenu"; - -export interface TerminalHeaderProps { - readonly subtitle: string; - readonly isEnvironmentReady: boolean; - readonly fontSize: number; - readonly terminalId: string; - readonly sessions: ReadonlyArray; - readonly status: Parameters[0]; - readonly workspaceRoot: string; - readonly onCloseTerminal: () => void; - readonly onDecreaseFontSize: () => void; - readonly onIncreaseFontSize: () => void; - readonly onOpenNewTerminal: () => void; - readonly onSelectTerminal: (terminalId: string) => void; -} diff --git a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx index a8ab34d8443e..794cb9dce476 100644 --- a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx +++ b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx @@ -1,7 +1,7 @@ import { DEFAULT_TERMINAL_ID, EnvironmentId, ThreadId } from "@t3tools/contracts"; import { type KnownTerminalSession } from "@t3tools/client-runtime/state/terminal"; import { SymbolView } from "../../components/AppSymbol"; -import { TerminalHeader } from "./TerminalHeader"; +import { ScreenHeader } from "../../components/ScreenHeader"; import { StackActions, useNavigation, type StaticScreenProps } from "@react-navigation/native"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Alert, Platform, Pressable, View } from "react-native"; @@ -35,7 +35,12 @@ import { terminalEnvironment } from "../../state/terminal"; import { useAtomCommand } from "../../state/use-atom-command"; import { useServerConfigs } from "../../state/entities"; import { useWorkspaceState } from "../../state/workspace"; -import { stepTerminalFontSize } from "../../lib/appearancePreferences"; +import { + MAX_TERMINAL_FONT_SIZE, + MIN_TERMINAL_FONT_SIZE, + TERMINAL_FONT_SIZE_STEP, + stepTerminalFontSize, +} from "../../lib/appearancePreferences"; import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; import { useAttachedTerminalSession, @@ -58,7 +63,9 @@ import { type PendingTerminalLaunch, } from "./terminalLaunchContext"; import { + basename, buildTerminalMenuSessions, + getTerminalStatusLabel, nextOpenTerminalId, previousLiveTerminalId, resolveTerminalSessionLabel, @@ -73,6 +80,90 @@ import { import { createTerminalPasteSession } from "./terminalPaste"; import { cacheTerminalGridSize, getCachedTerminalGridSize } from "./terminalUiState"; +function TerminalHeader(props: { + readonly subtitle: string; + readonly isEnvironmentReady: boolean; + readonly fontSize: number; + readonly terminalId: string; + readonly sessions: ReadonlyArray; + readonly status: Parameters[0]; + readonly workspaceRoot: string; + readonly onCloseTerminal: () => void; + readonly onDecreaseFontSize: () => void; + readonly onIncreaseFontSize: () => void; + readonly onOpenNewTerminal: () => void; + readonly onSelectTerminal: (terminalId: string) => void; +}) { + return ( + = MAX_TERMINAL_FONT_SIZE, + onPress: props.onIncreaseFontSize, + }, + ], + }, + ...props.sessions.map((session) => ({ + id: `terminal-session:${session.terminalId}`, + title: session.displayLabel, + icon: "terminal", + subtitle: [ + getTerminalStatusLabel({ + status: session.status, + hasRunningSubprocess: session.hasRunningSubprocess, + }), + basename(session.cwd), + ] + .filter(Boolean) + .join(" · "), + selected: session.terminalId === props.terminalId, + onPress: () => props.onSelectTerminal(session.terminalId), + })), + { + id: "terminal-new", + title: "Open new terminal", + icon: "plus", + subtitle: `Start another shell in ${basename(props.workspaceRoot) ?? "this workspace"}`, + onPress: props.onOpenNewTerminal, + }, + ], + }, + ] + : undefined + } + /> + ); +} + const DEFAULT_TERMINAL_COLS = 80; const DEFAULT_TERMINAL_ROWS = 24; const TERMINAL_ACCESSORY_HEIGHT = 52;