import { NativeStackScreenOptions } from "../../native/StackHeader"; import { StackActions, useFocusEffect, useNavigation, type StaticScreenProps, } from "@react-navigation/native"; import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import * as Option from "effect/Option"; import { DEFAULT_SERVER_SETTINGS, EnvironmentId, ThreadId, type ProjectScript, } from "@t3tools/contracts"; import { requestOlderThreadTurns, threadHasOlderTurns, } from "@t3tools/client-runtime/state/threads"; import { projectScriptCwd, projectScriptRuntimeEnv, resolveProjectScripts, } from "@t3tools/shared/projectScripts"; import { Alert, Platform, ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useWorkspaceState } from "../../state/workspace"; import { restoredNewTaskDraftKey } from "../../state/new-task-draft-key"; import { clearPendingThreadCreationOutcome } from "../../state/pending-thread-creation"; import { recoverFailedThreadDraft } from "../../state/recover-failed-thread-draft"; import { useEnvironmentQuery } from "../../state/query"; import { dismissGitActionResult, useGitActionProgress } from "../../state/use-vcs-action-state"; import { vcsEnvironment } from "../../state/vcs"; import { EmptyState } from "../../components/EmptyState"; import { AndroidScreenHeader, type AndroidHeaderAction, } from "../../components/AndroidScreenHeader"; import { LoadingScreen } from "../../components/LoadingScreen"; import { scopedThreadKey } from "../../lib/scopedEntities"; import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; import { connectionTone } from "../connection/connectionTone"; import { useRemoteConnections, useRemoteConnectionStatus, useRemoteEnvironmentRuntime, } from "../../state/use-remote-environment-registry"; import { useKnownTerminalSessions } from "../../state/use-terminal-session"; import { useSelectedThreadDetailState } from "../../state/use-thread-detail"; import { useThreadSelection } from "../../state/use-thread-selection"; import { GitActionProgressOverlay } from "./GitActionProgressOverlay"; import { buildTerminalMenuSessions, nextOpenTerminalId, resolveProjectScriptTerminalId, } from "../terminal/terminalMenu"; import { resolvePreferredThreadWorktreePath, stagePendingTerminalLaunch, } from "../terminal/terminalLaunchContext"; import { terminalDebugLog } from "../terminal/terminalDebugLog"; import { ThreadDetailScreen, type ThreadDetailScreenProps } from "./ThreadDetailScreen"; import { ThreadGitControls, useThreadGitCenterHeaderItems, useThreadGitRightHeaderItems, } from "./ThreadGitControls"; import { GitOverviewSheet } from "./git/GitOverviewSheet"; import { useAtomCommand } from "../../state/use-atom-command"; import { useSelectedThreadGitActions } from "../../state/use-selected-thread-git-actions"; import { useSelectedThreadGitState } from "../../state/use-selected-thread-git-state"; import { useSelectedThreadRequests } from "../../state/use-selected-thread-requests"; import { useSelectedThreadWorktree } from "../../state/use-selected-thread-worktree"; import { useThreadComposerState } from "../../state/use-thread-composer-state"; import { threadEnvironment } from "../../state/threads"; import { projectThreadContentPresentation } from "./threadContentPresentation"; import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; import { useAdaptiveWorkspaceLayout, useAdaptiveWorkspacePaneRole, useRegisterWorkspaceInspector, } from "../layout/AdaptiveWorkspaceLayout"; import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items"; import { ThreadFileNavigatorPane } from "../files/thread-file-navigator-pane"; import { ThreadInspectorContentStack, type ThreadInspectorMode, } from "./thread-inspector-content-stack"; interface ThreadInspectorSelection { readonly routeThreadIdentity: string | null; readonly mode: ThreadInspectorMode; } type NativeHeaderItems = ReadonlyArray>; function InspectorPaneRoleActivation() { useAdaptiveWorkspacePaneRole("inspector"); return null; } function firstRouteParam(value: string | string[] | undefined): string | null { if (Array.isArray(value)) { return value[0] ?? null; } return value ?? null; } function OpeningThreadLoadingScreen() { return ; } type ThreadRouteScreenRouteProps = StaticScreenProps<{ readonly environmentId: string; readonly threadId: string; }>; interface ThreadRouteScreenProps extends ThreadRouteScreenRouteProps { readonly onReturnToThread?: () => void; readonly renderInspector?: (headerInset: number) => ReactNode; } function ThreadUnavailableScreen() { return ( ); } export function ThreadRouteScreen(props: ThreadRouteScreenProps) { const { state: workspaceState } = useWorkspaceState(); const { connectionState } = useRemoteConnectionStatus(); const { selectedThread } = useThreadSelection(); const params = props.route.params; const environmentIdRaw = firstRouteParam(params.environmentId); const threadIdRaw = firstRouteParam(params.threadId); const environmentId = environmentIdRaw ? EnvironmentId.make(environmentIdRaw) : null; const routeEnvironmentRuntime = useRemoteEnvironmentRuntime(environmentId); const routeConnectionState = routeEnvironmentRuntime?.connectionState ?? (environmentId ? "available" : connectionState); const routeThreadKey = environmentId !== null && threadIdRaw !== null ? scopedThreadKey(environmentId, ThreadId.make(threadIdRaw)) : null; const selectedThreadKey = selectedThread === null ? null : scopedThreadKey(selectedThread.environmentId, selectedThread.id); const selectedThreadDetailState = useSelectedThreadDetailState(); if (environmentId === null || threadIdRaw === null) { return ; } // Render the full thread chrome (header, feed, composer) as soon as the // thread SHELL is known — no blocking on message detail. The feed shows a // loading placeholder while messages fetch, the floating pill above the // composer reports loading/syncing, and the composer's connection pill // reports connecting/reconnecting status. if (selectedThread !== null && selectedThreadKey === routeThreadKey) { return ; } const stillHydrating = workspaceState.isLoadingConnections || routeConnectionState === "connecting" || routeConnectionState === "reconnecting"; if (stillHydrating) { return ; } return ; } function ThreadRouteContent( props: ThreadRouteScreenProps & { readonly selectedThreadDetailState: ReturnType; }, ) { const { materialYouStyleLayoutActive, themeVariables } = useAppearancePreferences(); const headerColor = themeVariables["--color-header"]; const { fileInspector, layout, panes, showAuxiliaryPane, toggleAuxiliaryPane, togglePrimarySidebar, } = useAdaptiveWorkspaceLayout(); const { connectionState } = useRemoteConnectionStatus(); const { onReconnectEnvironment } = useRemoteConnections(); const { selectedThread, selectedThreadCreation, selectedThreadProject, selectedEnvironmentConnection, } = useThreadSelection(); const selectedThreadDetailState = props.selectedThreadDetailState; const selectedThreadDetail = Option.getOrNull(selectedThreadDetailState.data); // "Load earlier turns" header state for windowed (paginated) thread loads. const loadEarlierTurns = useMemo(() => { if (selectedThread === null || !threadHasOlderTurns(selectedThreadDetailState)) { return null; } return { loading: selectedThreadDetailState.page._tag === "Some" && selectedThreadDetailState.page.value.loadingOlder, onLoadEarlier: () => { requestOlderThreadTurns(selectedThread.environmentId, selectedThread.id); }, }; }, [selectedThread, selectedThreadDetailState]); const { selectedThreadCwd } = useSelectedThreadWorktree(); const composer = useThreadComposerState(); const gitState = useSelectedThreadGitState(); const gitActions = useSelectedThreadGitActions(); const requests = useSelectedThreadRequests(); const interruptThreadTurn = useAtomCommand(threadEnvironment.interruptTurn, "thread interrupt"); const navigation = useNavigation(); const params = props.route.params; const environmentIdRaw = firstRouteParam(params.environmentId); const environmentId = environmentIdRaw ? EnvironmentId.make(environmentIdRaw) : null; const threadId = firstRouteParam(params.threadId); const routeThreadIdentity = environmentIdRaw !== null && threadId !== null ? `${environmentIdRaw}:${threadId}` : null; const [inspectorSelection, setInspectorSelection] = useState( () => (props.renderInspector ? { routeThreadIdentity, mode: "route" } : null), ); const inspectorMode = (() => { if (inspectorSelection?.routeThreadIdentity === routeThreadIdentity) { if (inspectorSelection.mode === "files" && selectedThreadCwd === null) { return null; } return inspectorSelection.mode; } return null; })(); useEffect(() => { if ( fileInspector.supported && selectedThreadCwd === null && inspectorMode === null && panes.auxiliaryPaneVisible ) { toggleAuxiliaryPane(); } }, [ fileInspector.supported, inspectorMode, panes.auxiliaryPaneVisible, selectedThreadCwd, toggleAuxiliaryPane, ]); useEffect(() => { setInspectorSelection((current) => { if (props.renderInspector === undefined) { if (current === null || current.mode === "route") { return null; } return { ...current, routeThreadIdentity }; } if (current === null || current.mode === "route") { return { routeThreadIdentity, mode: "route" }; } return { ...current, routeThreadIdentity }; }); }, [props.renderInspector, routeThreadIdentity]); useFocusEffect( useCallback(() => { return () => { if (props.renderInspector === undefined) { // Inspectors are contextual to this chat destination. Clear the // hidden chat copy after a native push so returning from Files, // Review, or Terminal cannot reserve an empty trailing pane. setInspectorSelection(null); } }; }, [props.renderInspector]), ); const routeEnvironmentRuntime = useRemoteEnvironmentRuntime(environmentId); const routeConnectionState = routeEnvironmentRuntime?.connectionState ?? (environmentId ? "available" : connectionState); const routeConnectionError = routeEnvironmentRuntime?.connectionError ?? null; const selectedThreadWithDraftSettings = useMemo( () => selectedThread ? { ...selectedThread, modelSelection: composer.modelSelection ?? selectedThread.modelSelection, runtimeMode: composer.runtimeMode ?? selectedThread.runtimeMode, interactionMode: composer.interactionMode ?? selectedThread.interactionMode, } : null, [composer.interactionMode, composer.modelSelection, composer.runtimeMode, selectedThread], ); /* ─── Native header theming ──────────────────────────────────────── */ const usesNativeHeaderGlass = NATIVE_LIQUID_GLASS_SUPPORTED; const headerSubtitle = [ selectedThreadProject?.title ?? null, selectedEnvironmentConnection?.environmentLabel ?? null, ] .filter(Boolean) .join(" · "); /* ─── Git status for native header trigger ───────────────────────── */ const gitStatus = useEnvironmentQuery( selectedThread !== null && selectedThreadCwd !== null ? vcsEnvironment.status({ environmentId: selectedThread.environmentId, input: { cwd: selectedThreadCwd }, }) : null, ); const knownTerminalSessions = useKnownTerminalSessions({ environmentId: selectedThread?.environmentId ?? null, threadId: selectedThread?.id ?? null, }); const terminalMenuSessions = useMemo( () => buildTerminalMenuSessions({ knownSessions: knownTerminalSessions, workspaceRoot: selectedThreadProject?.workspaceRoot ?? null, }), [knownTerminalSessions, selectedThreadProject?.workspaceRoot], ); const selectedThreadDetailWorktreePath = selectedThreadDetail?.worktreePath ?? null; const handleReconnectEnvironment = useCallback(() => { if (!environmentId) { return; } onReconnectEnvironment(environmentId); }, [environmentId, onReconnectEnvironment]); /* ─── Git action progress (for overlay banner) ──────────────────── */ const gitActionProgressTarget = useMemo( () => ({ environmentId: selectedThread?.environmentId ?? null, cwd: selectedThreadCwd, }), [selectedThread?.environmentId, selectedThreadCwd], ); const gitActionProgress = useGitActionProgress(gitActionProgressTarget); const handleOpenGitInspector = useCallback(() => { if (!fileInspector.supported) { if (selectedThread === null) { return; } navigation.navigate("GitOverview", { environmentId: String(selectedThread.environmentId), threadId: String(selectedThread.id), }); return; } setInspectorSelection({ routeThreadIdentity, mode: "git" }); showAuxiliaryPane("inspector"); }, [fileInspector.supported, navigation, routeThreadIdentity, selectedThread, showAuxiliaryPane]); const handleOpenFilesInspector = useCallback(() => { if (selectedThread === null || selectedThreadCwd === null) { return; } if (!fileInspector.supported) { navigation.navigate("ThreadFiles", { environmentId: String(selectedThread.environmentId), threadId: String(selectedThread.id), }); return; } setInspectorSelection({ routeThreadIdentity, mode: props.renderInspector === undefined ? "files" : "route", }); showAuxiliaryPane("inspector"); }, [ fileInspector.supported, navigation, props.renderInspector, routeThreadIdentity, selectedThread, selectedThreadCwd, showAuxiliaryPane, ]); const inspectorToggleActionRef = useRef({ inspectorMode, openFilesInspector: handleOpenFilesInspector, toggleAuxiliaryPane, }); inspectorToggleActionRef.current = { inspectorMode, openFilesInspector: handleOpenFilesInspector, toggleAuxiliaryPane, }; const handleToggleInspector = useCallback(() => { const action = inspectorToggleActionRef.current; if (action.inspectorMode === null) { action.openFilesInspector(); return; } action.toggleAuxiliaryPane(); }, []); const handleSelectInspectorFile = useCallback( (path: string) => { if (selectedThread === null) { return; } const params = { environmentId: String(selectedThread.environmentId), threadId: String(selectedThread.id), path: path.split("/").filter((segment) => segment.length > 0), }; if (fileInspector.supported) { navigation.navigate("ThreadFile", params); return; } navigation.navigate("ThreadFile", params); }, [fileInspector.supported, navigation, selectedThread], ); // The workspace inspector column spans the full window height. On iOS the // panes bring their own nested native headers (which underlap the status // bar); elsewhere the pane content pads itself below the top inset. const safeAreaInsets = useSafeAreaInsets(); const inspectorHeaderInset = Platform.OS === "ios" ? 0 : safeAreaInsets.top; const GitInspector = useCallback( () => ( ), [inspectorHeaderInset, props.route.params], ); const FilesInspector = useCallback( () => selectedThread !== null && selectedThreadCwd !== null ? ( ) : null, [ handleSelectInspectorFile, inspectorHeaderInset, selectedThread, selectedThreadCwd, selectedThreadProject?.title, ], ); const RouteInspector = useCallback( () => props.renderInspector?.(inspectorHeaderInset), [inspectorHeaderInset, props.renderInspector], ); const renderInspectorStack = useCallback( () => inspectorMode === null ? null : ( ), [FilesInspector, GitInspector, RouteInspector, inspectorMode, props.renderInspector], ); const activeInspectorRenderer = inspectorMode === null ? undefined : renderInspectorStack; // Hand the inspector to the workspace so it renders beside the navigator, // outside this screen's native header — the terminal/git/files toolbar // stays anchored to the chat pane instead of floating above the inspector. useRegisterWorkspaceInspector(activeInspectorRenderer); const handleOpenConnectionEditor = useCallback(() => { void navigation.navigate("Connections"); }, [navigation]); const handleStopThread = useCallback(() => { if ( !selectedThread || (selectedThread.session?.status !== "running" && selectedThread.session?.status !== "starting") ) { return; } return interruptThreadTurn({ environmentId: selectedThread.environmentId, input: { threadId: selectedThread.id, ...(selectedThread.session.activeTurnId ? { turnId: selectedThread.session.activeTurnId } : {}), }, }); }, [interruptThreadTurn, selectedThread]); const handleOpenTerminal = useCallback( (nextTerminalId?: string | null) => { terminalDebugLog("terminal-menu:open-existing", { terminalId: nextTerminalId ?? null, hasThread: Boolean(selectedThread), hasWorkspaceRoot: Boolean(selectedThreadProject?.workspaceRoot), }); if (!selectedThread || !selectedThreadProject?.workspaceRoot) { return; } void navigation.navigate("ThreadTerminal", { environmentId: String(selectedThread.environmentId), threadId: String(selectedThread.id), ...(nextTerminalId ? { terminalId: nextTerminalId } : {}), }); }, [navigation, selectedThread, selectedThreadProject?.workspaceRoot], ); const handleOpenNewTerminal = useCallback(() => { terminalDebugLog("terminal-menu:open-new", { hasThread: Boolean(selectedThread), hasWorkspaceRoot: Boolean(selectedThreadProject?.workspaceRoot), listedTerminalIds: terminalMenuSessions.map((session) => session.terminalId), }); if (!selectedThread || !selectedThreadProject?.workspaceRoot) { return; } const nextId = nextOpenTerminalId({ listedTerminalIds: terminalMenuSessions.map((session) => session.terminalId), }); void navigation.navigate("ThreadTerminal", { environmentId: String(selectedThread.environmentId), threadId: String(selectedThread.id), terminalId: nextId, }); }, [navigation, selectedThread, selectedThreadProject?.workspaceRoot, terminalMenuSessions]); const handleRunProjectScript = useCallback( async (script: ProjectScript) => { terminalDebugLog("project-script:press", { scriptId: script.id, command: script.command, hasThread: Boolean(selectedThread), hasWorkspaceRoot: Boolean(selectedThreadProject?.workspaceRoot), }); if (!selectedThread || !selectedThreadProject?.workspaceRoot) { terminalDebugLog("project-script:abort", { scriptId: script.id, reason: "no-thread-or-workspace", }); return; } const targetTerminalId = resolveProjectScriptTerminalId({ existingTerminalIds: terminalMenuSessions.map((session) => session.terminalId), hasRunningTerminal: terminalMenuSessions.some( (session) => session.status === "running" || session.status === "starting", ), }); const preferredWorktreePath = resolvePreferredThreadWorktreePath({ threadShellWorktreePath: selectedThread.worktreePath ?? null, threadDetailWorktreePath: selectedThreadDetailWorktreePath, }); const cwd = projectScriptCwd({ project: { cwd: selectedThreadProject.workspaceRoot }, worktreePath: preferredWorktreePath, }); const env = projectScriptRuntimeEnv({ project: { cwd: selectedThreadProject.workspaceRoot }, worktreePath: preferredWorktreePath, }); stagePendingTerminalLaunch({ target: { environmentId: selectedThread.environmentId, threadId: selectedThread.id, terminalId: targetTerminalId, }, launch: { cwd, worktreePath: preferredWorktreePath, env, initialInput: `${script.command}\r`, }, }); terminalDebugLog("project-script:staged", { scriptId: script.id, terminalId: targetTerminalId, cwd, worktreePath: preferredWorktreePath, }); void navigation.navigate("ThreadTerminal", { environmentId: String(selectedThread.environmentId), threadId: String(selectedThread.id), terminalId: targetTerminalId, }); }, [ navigation, selectedThread, selectedThreadDetailWorktreePath, selectedThreadProject, terminalMenuSessions, ], ); const threadGitControlProps = { environmentId: environmentIdRaw ?? "", threadId: threadId ?? "", auxiliaryPaneControl: !layout.usesSplitView && fileInspector.supported && selectedThreadCwd !== null ? { accessibilityLabel: "Toggle inspector", onPress: handleToggleInspector, } : undefined, onOpenFilesInspector: fileInspector.supported && selectedThreadCwd !== null ? handleOpenFilesInspector : undefined, onOpenGitInspector: fileInspector.supported ? handleOpenGitInspector : undefined, currentBranch: selectedThread?.branch ?? null, gitStatus: gitStatus.data, gitOperationLabel: gitState.gitOperationLabel, canOpenTerminal: Boolean(selectedThreadProject?.workspaceRoot), canOpenFiles: Boolean(selectedThreadProject?.workspaceRoot), projectScripts: selectedThreadProject ? resolveProjectScripts( routeEnvironmentRuntime?.serverConfig?.settings ?? DEFAULT_SERVER_SETTINGS, selectedThreadProject, ) : [], terminalSessions: terminalMenuSessions, showDirectFileControl: layout.usesSplitView, onOpenTerminal: handleOpenTerminal, onOpenNewTerminal: handleOpenNewTerminal, onRunProjectScript: handleRunProjectScript, onPull: gitActions.onPullSelectedThreadBranch, onRunAction: gitActions.onRunSelectedThreadGitAction, }; const threadCenterHeaderItems = useThreadGitCenterHeaderItems(threadGitControlProps); const compactRightHeaderItems = useThreadGitRightHeaderItems(threadGitControlProps); const splitLeftHeaderItems = useMemo( () => [ { // Match Mail's split-view detail toolbar: the first detail action sits // inside the content pane, not flush against the sidebar divider. spacing: 18, type: "spacing" as const, }, ...(props.onReturnToThread ? [ withNativeGlassHeaderItem({ accessibilityLabel: "Return to chat", icon: { name: "chevron.left", type: "sfSymbol" as const }, identifier: "thread-left-return", onPress: props.onReturnToThread, type: "button" as const, }), ] : []), withNativeGlassHeaderItem({ accessibilityLabel: panes.primarySidebarVisible ? "Maximize content" : "Show thread sidebar", icon: { name: panes.primarySidebarVisible ? "arrow.up.left.and.arrow.down.right" : "sidebar.left", type: "sfSymbol" as const, }, identifier: "thread-left-sidebar", onPress: togglePrimarySidebar, type: "button" as const, }), withNativeGlassHeaderItem({ accessibilityLabel: "New task", icon: { name: "square.and.pencil", type: "sfSymbol" as const }, identifier: "thread-left-new-task", onPress: () => navigation.navigate("NewTaskSheet", { screen: "NewTask" }), type: "button" as const, }), ], [panes.primarySidebarVisible, props.onReturnToThread, navigation, togglePrimarySidebar], ); const androidHeaderActions = useMemo>(() => { if (Platform.OS !== "android") return []; const actions: AndroidHeaderAction[] = []; if (props.onReturnToThread) { actions.push({ accessibilityLabel: "Return to chat", icon: "chevron.left", onPress: props.onReturnToThread, }); } if (selectedThreadCwd !== null) { actions.push({ accessibilityLabel: "Open files", icon: "folder", onPress: handleOpenFilesInspector, }); } if (selectedThreadProject?.workspaceRoot) { actions.push({ accessibilityLabel: "Open terminal", icon: "terminal", onPress: () => handleOpenTerminal(null), }); } actions.push({ accessibilityLabel: "Open git controls", icon: "point.topleft.down.curvedto.point.bottomright.up", onPress: handleOpenGitInspector, }); if (fileInspector.supported && selectedThreadCwd !== null) { actions.push({ accessibilityLabel: "Toggle inspector", icon: "sidebar.right", onPress: handleToggleInspector, }); } return actions; }, [ fileInspector.supported, handleOpenFilesInspector, handleOpenTerminal, handleOpenGitInspector, handleToggleInspector, props.onReturnToThread, selectedThreadCwd, selectedThreadProject?.workspaceRoot, ]); const handleEditFailedCreation = useCallback(async () => { const creation = selectedThreadCreation?.message; if (!creation?.creation || routeThreadIdentity === null) { return; } // The drain restored the prompt and attachments into the recovery draft // the rejected creation owns. Open that draft by id: without it the sheet // mints a fresh empty one and the restored content is unreachable. try { await recoverFailedThreadDraft(creation); } catch (error) { Alert.alert( "Could not restore draft", error instanceof Error ? error.message : String(error), ); return; } clearPendingThreadCreationOutcome(routeThreadIdentity); navigation.dispatch( StackActions.replace("NewTaskSheet", { screen: "NewTaskDraft", params: { draftId: restoredNewTaskDraftKey(creation.messageId), environmentId: String(creation.environmentId), projectId: String(creation.creation.projectId), ...(selectedThreadProject ? { title: selectedThreadProject.title } : {}), }, }), ); }, [navigation, routeThreadIdentity, selectedThreadCreation, selectedThreadProject]); const creationState = ((): ThreadDetailScreenProps["creationState"] => { if (selectedThreadCreation === null) { return null; } if (selectedThreadCreation.outcome?.kind === "failed") { return { kind: "failed", reason: selectedThreadCreation.outcome.reason, onEditTask: handleEditFailedCreation, }; } return { kind: "preparing", preparingWorktree: selectedThreadCreation.message.creation?.workspaceMode === "worktree", }; })(); // Deep links / cold starts land with Thread as the ONLY route, where the // native back button does not render. Provide an explicit Home escape for // that case; when history exists the native back button is used instead. const canGoBack = navigation.canGoBack(); const compactHomeHeaderItems = useMemo( () => [ withNativeGlassHeaderItem({ accessibilityLabel: "Go to threads list", icon: { name: "list.bullet", type: "sfSymbol" as const }, identifier: "thread-left-home", onPress: () => navigation.dispatch(StackActions.replace("Home")), type: "button" as const, }), ], [navigation], ); if (!environmentId || !threadId) { return ; } if (!selectedThread) { return ; } // A queued creation renders as ready content: its prompt is the whole // conversation until the server creates the thread. The subscription's // not-found error for that window is expected, not a load failure. const contentPresentation = creationState !== null ? { kind: "ready" as const } : projectThreadContentPresentation({ hasDetail: selectedThreadDetail !== null, detailError: Option.getOrNull(selectedThreadDetailState.error), detailDeleted: selectedThreadDetailState.status === "deleted", connectionState: routeConnectionState, }); const serverConfig = routeEnvironmentRuntime?.serverConfig ?? null; const renderThreadRouteBody = (showActionControls: boolean) => ( <> ); return ( <> {activeInspectorRenderer ? : null} splitLeftHeaderItems : canGoBack ? undefined : () => compactHomeHeaderItems : undefined, // Search lives in the persistent sidebar, so the split header keeps // the git controls on the RIGHT (no center items — center space is // reserved for future breadcrumbs/status). unstable_headerRightItems: Platform.OS === "ios" ? () => (layout.usesSplitView ? threadCenterHeaderItems : compactRightHeaderItems) : undefined, unstable_headerSubtitle: usesNativeHeaderGlass ? headerSubtitle : undefined, contentStyle: Platform.OS === "android" && materialYouStyleLayoutActive ? { backgroundColor: headerColor } : undefined, }} /> {Platform.OS === "android" ? ( navigation.goBack()} actions={androidHeaderActions} hideBottomBorder={materialYouStyleLayoutActive} /> ) : null} {/* Android surfaces the git/files/inspector actions in its in-flow header above, so the fallback action toolbar stays iOS-only. */} {renderThreadRouteBody( Platform.OS !== "android" && !layout.usesSplitView && !usesNativeHeaderGlass, )} ); }