import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useMemo, useState } from "react"; import * as Option from "effect/Option"; import { EnvironmentId, type ProjectScript } from "@t3tools/contracts"; import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts"; import { Pressable, ScrollView, Text as RNText, View } from "react-native"; import { useWorkspaceState } from "../../state/workspace"; import { useThemeColor } from "../../lib/useThemeColor"; 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 { LoadingScreen } from "../../components/LoadingScreen"; import { buildThreadRoutePath, buildThreadTerminalNavigation } from "../../lib/routes"; import { scopedThreadKey } from "../../lib/scopedEntities"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; 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 } from "./ThreadDetailScreen"; import { ThreadGitControls } from "./ThreadGitControls"; import { ThreadNavigationDrawer } from "./ThreadNavigationDrawer"; 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"; function firstRouteParam(value: string | string[] | undefined): string | null { if (Array.isArray(value)) { return value[0] ?? null; } return value ?? null; } function OpeningThreadLoadingScreen() { return ; } export function ThreadRouteScreen() { const { state: workspaceState } = useWorkspaceState(); const { connectionState } = useRemoteConnectionStatus(); const { onReconnectEnvironment } = useRemoteConnections(); const { selectedThread, selectedThreadProject, selectedEnvironmentConnection } = useThreadSelection(); const selectedThreadDetailState = useSelectedThreadDetailState(); const selectedThreadDetail = Option.getOrNull(selectedThreadDetailState.data); const { selectedThreadCwd } = useSelectedThreadWorktree(); const composer = useThreadComposerState(); const gitState = useSelectedThreadGitState(); const gitActions = useSelectedThreadGitActions(); const requests = useSelectedThreadRequests(); const interruptThreadTurn = useAtomCommand(threadEnvironment.interruptTurn, "thread interrupt"); const router = useRouter(); const params = useLocalSearchParams<{ environmentId?: string | string[]; threadId?: string | string[]; }>(); const [drawerVisible, setDrawerVisible] = useState(false); const environmentIdRaw = firstRouteParam(params.environmentId); const environmentId = environmentIdRaw ? EnvironmentId.make(environmentIdRaw) : null; const threadId = firstRouteParam(params.threadId); 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 iconColor = String(useThemeColor("--color-icon")); const foregroundColor = String(useThemeColor("--color-foreground")); const secondaryFg = String(useThemeColor("--color-foreground-secondary")); /* ─── 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 handleOpenDrawer = useCallback(() => { setDrawerVisible(true); }, []); const handleOpenConnectionEditor = useCallback(() => { void router.push("/connections"); }, [router]); 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 router.push(buildThreadTerminalNavigation(selectedThread, nextTerminalId)); }, [router, 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 router.push(buildThreadTerminalNavigation(selectedThread, nextId)); }, [router, 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 router.push(buildThreadTerminalNavigation(selectedThread, targetTerminalId)); }, [ router, selectedThread, selectedThreadDetailWorktreePath, selectedThreadProject, terminalMenuSessions, ], ); if (!environmentId || !threadId) { return ; } if (!selectedThread) { const stillHydrating = workspaceState.isLoadingConnections || routeConnectionState === "connecting" || routeConnectionState === "reconnecting"; if (stillHydrating) { return ; } return ( ); } const selectedThreadKey = scopedThreadKey(selectedThread.environmentId, selectedThread.id); const contentPresentation = projectThreadContentPresentation({ hasDetail: selectedThreadDetail !== null, detailError: Option.getOrNull(selectedThreadDetailState.error), detailDeleted: selectedThreadDetailState.status === "deleted", connectionState: routeConnectionState, }); const serverConfig = routeEnvironmentRuntime?.serverConfig ?? null; const headerSubtitle = [ selectedThreadProject?.title ?? null, selectedEnvironmentConnection?.environmentLabel ?? null, ] .filter(Boolean) .join(" · "); return ( <> ( { // TODO: trigger rename modal }} > {selectedThread.title} {headerSubtitle} ), }} /> setDrawerVisible(false)} onSelectThread={(thread) => { router.replace(buildThreadRoutePath(thread)); }} onStartNewTask={() => router.push("/new")} /> ); }