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")}
/>
>
);
}