import { type ApprovalRequestId, DEFAULT_MODEL_BY_PROVIDER, type EnvironmentId, type MessageId, type ModelSelection, type ProjectScript, type ProviderKind, type ProjectId, type ProviderApprovalDecision, type ServerProvider, type ResolvedKeybindingsConfig, type ScopedThreadRef, type ThreadId, type TurnId, type KeybindingCommand, OrchestrationThreadActivity, ProviderInteractionMode, RuntimeMode, TerminalOpenInput,} from "@t3tools/contracts";import { parseScopedThreadKey, scopedThreadKey, scopeProjectRef, scopeThreadRef,} from "@t3tools/client-runtime";import { applyClaudePromptEffortPrefix, createModelSelection, resolvePromptInjectedEffort,} from "@t3tools/shared/model";import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts";import { truncate } from "@t3tools/shared/String";import { Debouncer } from "@tanstack/react-pacer";import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";import { useNavigate, useSearch } from "@tanstack/react-router";import { useShallow } from "zustand/react/shallow";import { useGitStatus } from "~/lib/gitStatusState";import { usePrimaryEnvironmentId } from "../environments/primary";import { readEnvironmentApi } from "../environmentApi";import { isElectron } from "../env";import { readLocalApi } from "../localApi";import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";import { collapseExpandedComposerCursor, parseStandaloneComposerSlashCommand,} from "../composer-logic";import { deriveCompletionDividerBeforeEntryId, derivePendingApprovals, derivePendingUserInputs, derivePhase, deriveTimelineEntries, deriveActiveWorkStartedAt, deriveActivePlanState, findSidebarProposedPlan, findLatestProposedPlan, deriveWorkLogEntries, hasActionableProposedPlan, hasToolActivityForTurn, isLatestTurnSettled, formatElapsed,} from "../session-logic";import { type LegendListRef } from "@legendapp/list/react";import { buildPendingUserInputAnswers, derivePendingUserInputProgress, setPendingUserInputCustomAnswer, togglePendingUserInputOptionSelection, type PendingUserInputDraftAnswer,} from "../pendingUserInput";import { selectProjectsAcrossEnvironments, selectThreadsAcrossEnvironments, useStore,} from "../store";import { createProjectSelectorByRef, createThreadSelectorByRef } from "../storeSelectors";import { useUiStateStore } from "../uiStateStore";import { buildPlanImplementationThreadTitle, buildPlanImplementationPrompt, resolvePlanFollowUpSubmission,} from "../proposedPlan";import { DEFAULT_INTERACTION_MODE, DEFAULT_RUNTIME_MODE, DEFAULT_THREAD_TERMINAL_ID, MAX_TERMINALS_PER_GROUP, type ChatMessage, type SessionPhase, type Thread, type TurnDiffSummary,} from "../types";import { useTheme } from "../hooks/useTheme";import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";import { useCommandPaletteStore } from "../commandPaletteStore";import { buildTemporaryWorktreeBranchName } from "@t3tools/shared/git";import { useMediaQuery } from "../hooks/useMediaQuery";import { RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY } from "../rightPanelLayout";import { BranchToolbar } from "./BranchToolbar";import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings";import PlanSidebar from "./PlanSidebar";import ThreadTerminalDrawer from "./ThreadTerminalDrawer";import { ChevronDownIcon } from "lucide-react";import { cn, randomUUID } from "~/lib/utils";import { stackedThreadToast, toastManager } from "./ui/toast";import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings";import { type NewProjectScriptInput } from "./ProjectScriptsControl";import { commandForProjectScript, nextProjectScriptId, projectScriptIdFromCommand,} from "~/projectScripts";import { newCommandId, newDraftId, newMessageId, newThreadId } from "~/lib/utils";import { getProviderModelCapabilities, resolveSelectableProvider } from "../providerModels";import { useSettings } from "../hooks/useSettings";import { resolveAppModelSelection } from "../modelSelection";import { isTerminalFocused } from "../lib/terminalFocus";import { deriveLogicalProjectKeyFromSettings } from "../logicalProject";import { useSavedEnvironmentRegistryStore, useSavedEnvironmentRuntimeStore,} from "../environments/runtime";import { buildDraftThreadRouteParams } from "../threadRoutes";import { type ComposerImageAttachment, type DraftThreadEnvMode, useComposerDraftStore, type DraftId,} from "../composerDraftStore";import { appendTerminalContextsToPrompt, formatTerminalContextLabel, type TerminalContextDraft, type TerminalContextSelection,} from "../lib/terminalContext";import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore";import { ChatComposer, type ChatComposerHandle } from "./chat/ChatComposer";import { ExpandedImageDialog } from "./chat/ExpandedImageDialog";import { PullRequestThreadDialog } from "./PullRequestThreadDialog";import { MessagesTimeline } from "./chat/MessagesTimeline";import { ChatHeader } from "./chat/ChatHeader";import { type ExpandedImagePreview } from "./chat/ExpandedImagePreview";import { NoActiveThreadState } from "./NoActiveThreadState";import { resolveEffectiveEnvMode, resolveEnvironmentOptionLabel } from "./BranchToolbar.logic";import { ProviderStatusBanner } from "./chat/ProviderStatusBanner";import { ThreadErrorBanner } from "./chat/ThreadErrorBanner";import { MAX_HIDDEN_MOUNTED_TERMINAL_THREADS, buildExpiredTerminalContextToastCopy, buildLocalDraftThread, collectUserMessageBlobPreviewUrls, createLocalDispatchSnapshot, deriveComposerSendState, hasServerAcknowledgedLocalDispatch, LAST_INVOKED_SCRIPT_BY_PROJECT_KEY, LastInvokedScriptByProjectSchema, type LocalDispatchSnapshot, PullRequestDialogState, cloneComposerImageForRetry, deriveLockedProvider, readFileAsDataUrl, reconcileMountedTerminalThreadIds, resolveSendEnvMode, revokeBlobPreviewUrl, revokeUserMessagePreviewUrls, shouldWriteThreadErrorToCurrentServerThread, waitForStartedServerThread,} from "./ChatView.logic";import { useLocalStorage } from "~/hooks/useLocalStorage";import { useComposerHandleContext } from "../composerHandleContext";import { useServerAvailableEditors, useServerConfig, useServerKeybindings,} from "~/rpc/serverState";import { sanitizeThreadErrorMessage } from "~/rpc/transportError";import { retainThreadDetailSubscription } from "../environments/runtime/service";import { RightPanelSheet } from "./RightPanelSheet"; const IMAGE_ONLY_BOOTSTRAP_PROMPT = "[User attached one or more images without additional text. Respond using the conversation context and the attached image(s).]";const EMPTY_ACTIVITIES: OrchestrationThreadActivity[] = [];const EMPTY_PROPOSED_PLANS: Thread["proposedPlans"] = [];const EMPTY_PROVIDERS: ServerProvider[] = [];const EMPTY_PENDING_USER_INPUT_ANSWERS: Record<string, PendingUserInputDraftAnswer> = {}; type ThreadPlanCatalogEntry = Pick<Thread, "id" | "proposedPlans">; function useThreadPlanCatalog(threadIds: readonly ThreadId[]): ThreadPlanCatalogEntry[] { return useStore( useMemo(() => { let previousThreadIds: readonly ThreadId[] = []; let previousResult: ThreadPlanCatalogEntry[] = []; let previousEntries = new Map< ThreadId, { shell: object | null; proposedPlanIds: readonly string[] | undefined; proposedPlansById: Record<string, Thread["proposedPlans"][number]> | undefined; entry: ThreadPlanCatalogEntry; } >(); return (state) => { const sameThreadIds = previousThreadIds.length === threadIds.length && previousThreadIds.every((id, index) => id === threadIds[index]); const nextEntries = new Map< ThreadId, { shell: object | null; proposedPlanIds: readonly string[] | undefined; proposedPlansById: Record<string, Thread["proposedPlans"][number]> | undefined; entry: ThreadPlanCatalogEntry; } >(); const nextResult: ThreadPlanCatalogEntry[] = []; let changed = !sameThreadIds; for (const threadId of threadIds) { let shell: object | undefined; let proposedPlanIds: readonly string[] | undefined; let proposedPlansById: Record<string, Thread["proposedPlans"][number]> | undefined; for (const environmentState of Object.values(state.environmentStateById)) { const matchedShell = environmentState.threadShellById[threadId]; if (!matchedShell) { continue; } shell = matchedShell; proposedPlanIds = environmentState.proposedPlanIdsByThreadId[threadId]; proposedPlansById = environmentState.proposedPlanByThreadId[threadId] as | Record<string, Thread["proposedPlans"][number]> | undefined; break; } if (!shell) { const previous = previousEntries.get(threadId); if ( previous && previous.shell === null && previous.proposedPlanIds === undefined && previous.proposedPlansById === undefined ) { nextEntries.set(threadId, previous); continue; } changed = true; nextEntries.set(threadId, { shell: null, proposedPlanIds: undefined, proposedPlansById: undefined, entry: { id: threadId, proposedPlans: EMPTY_PROPOSED_PLANS }, }); continue; } const previous = previousEntries.get(threadId); if ( previous && previous.shell === shell && previous.proposedPlanIds === proposedPlanIds && previous.proposedPlansById === proposedPlansById ) { nextEntries.set(threadId, previous); nextResult.push(previous.entry); continue; } changed = true; const proposedPlans = proposedPlanIds && proposedPlanIds.length > 0 && proposedPlansById ? proposedPlanIds.flatMap((planId) => { const proposedPlan = proposedPlansById?.[planId]; return proposedPlan ? [proposedPlan] : []; }) : EMPTY_PROPOSED_PLANS; const entry = { id: threadId, proposedPlans }; nextEntries.set(threadId, { shell, proposedPlanIds, proposedPlansById, entry, }); nextResult.push(entry); } if (!changed && previousResult.length === nextResult.length) { return previousResult; } previousThreadIds = threadIds; previousEntries = nextEntries; previousResult = nextResult; return nextResult; }; }, [threadIds]), );} function formatOutgoingPrompt(params: { provider: ProviderKind; model: string | null; models: ReadonlyArray<ServerProvider["models"][number]>; effort: string | null; text: string;}): string { const caps = getProviderModelCapabilities(params.models, params.model, params.provider); const promptEffort = resolvePromptInjectedEffort(caps, params.effort); return applyClaudePromptEffortPrefix(params.text, promptEffort);}const SCRIPT_TERMINAL_COLS = 120;const SCRIPT_TERMINAL_ROWS = 30; type ChatViewProps = | { environmentId: EnvironmentId; threadId: ThreadId; onDiffPanelOpen?: () => void; reserveTitleBarControlInset?: boolean; routeKind: "server"; draftId?: never; } | { environmentId: EnvironmentId; threadId: ThreadId; onDiffPanelOpen?: () => void; reserveTitleBarControlInset?: boolean; routeKind: "draft"; draftId: DraftId; }; interface TerminalLaunchContext { threadId: ThreadId; cwd: string; worktreePath: string | null;} type PersistentTerminalLaunchContext = Pick<TerminalLaunchContext, "cwd" | "worktreePath">; function useLocalDispatchState(input: { activeThread: Thread | undefined; activeLatestTurn: Thread["latestTurn"] | null; phase: SessionPhase; activePendingApproval: ApprovalRequestId | null; activePendingUserInput: ApprovalRequestId | null; threadError: string | null | undefined;}) { const [localDispatch, setLocalDispatch] = useState<LocalDispatchSnapshot | null>(null); const beginLocalDispatch = useCallback( (options?: { preparingWorktree?: boolean }) => { const preparingWorktree = Boolean(options?.preparingWorktree); setLocalDispatch((current) => { if (current) { return current.preparingWorktree === preparingWorktree ? current : { ...current, preparingWorktree }; } return createLocalDispatchSnapshot(input.activeThread, options); }); }, [input.activeThread], ); const resetLocalDispatch = useCallback(() => { setLocalDispatch(null); }, []); const serverAcknowledgedLocalDispatch = useMemo( () => hasServerAcknowledgedLocalDispatch({ localDispatch, phase: input.phase, latestTurn: input.activeLatestTurn, session: input.activeThread?.session ?? null, hasPendingApproval: input.activePendingApproval !== null, hasPendingUserInput: input.activePendingUserInput !== null, threadError: input.threadError, }), [ input.activeLatestTurn, input.activePendingApproval, input.activePendingUserInput, input.activeThread?.session, input.phase, input.threadError, localDispatch, ], ); useEffect(() => { if (!serverAcknowledgedLocalDispatch) { return; } resetLocalDispatch(); }, [resetLocalDispatch, serverAcknowledgedLocalDispatch]); return { beginLocalDispatch, resetLocalDispatch, localDispatchStartedAt: localDispatch?.startedAt ?? null, isPreparingWorktree: localDispatch?.preparingWorktree ?? false, isSendBusy: localDispatch !== null && !serverAcknowledgedLocalDispatch, };} interface PersistentThreadTerminalDrawerProps { threadRef: { environmentId: EnvironmentId; threadId: ThreadId }; threadId: ThreadId; visible: boolean; launchContext: PersistentTerminalLaunchContext | null; focusRequestId: number; splitShortcutLabel: string | undefined; newShortcutLabel: string | undefined; closeShortcutLabel: string | undefined; keybindings: ResolvedKeybindingsConfig; onAddTerminalContext: (selection: TerminalContextSelection) => void;} const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDrawer({ threadRef, threadId, visible, launchContext, focusRequestId, splitShortcutLabel, newShortcutLabel, closeShortcutLabel, keybindings, onAddTerminalContext,}: PersistentThreadTerminalDrawerProps) { const serverThread = useStore(useMemo(() => createThreadSelectorByRef(threadRef), [threadRef])); const draftThread = useComposerDraftStore((store) => store.getDraftThreadByRef(threadRef)); const projectRef = serverThread ? scopeProjectRef(serverThread.environmentId, serverThread.projectId) : draftThread ? scopeProjectRef(draftThread.environmentId, draftThread.projectId) : null; const project = useStore(useMemo(() => createProjectSelectorByRef(projectRef), [projectRef])); const terminalState = useTerminalStateStore((state) => selectThreadTerminalState(state.terminalStateByThreadKey, threadRef), ); const storeSetTerminalHeight = useTerminalStateStore((state) => state.setTerminalHeight); const storeSplitTerminal = useTerminalStateStore((state) => state.splitTerminal); const storeNewTerminal = useTerminalStateStore((state) => state.newTerminal); const storeSetActiveTerminal = useTerminalStateStore((state) => state.setActiveTerminal); const storeCloseTerminal = useTerminalStateStore((state) => state.closeTerminal); const [localFocusRequestId, setLocalFocusRequestId] = useState(0); const worktreePath = serverThread?.worktreePath ?? draftThread?.worktreePath ?? null; const effectiveWorktreePath = useMemo(() => { if (launchContext !== null) { return launchContext.worktreePath; } return worktreePath; }, [launchContext, worktreePath]); const cwd = useMemo( () => launchContext?.cwd ?? (project ? projectScriptCwd({ project: { cwd: project.cwd }, worktreePath: effectiveWorktreePath, }) : null), [effectiveWorktreePath, launchContext?.cwd, project], ); const runtimeEnv = useMemo( () => project ? projectScriptRuntimeEnv({ project: { cwd: project.cwd }, worktreePath: effectiveWorktreePath, }) : {}, [effectiveWorktreePath, project], ); const bumpFocusRequestId = useCallback(() => { if (!visible) { return; } setLocalFocusRequestId((value) => value + 1); }, [visible]); const setTerminalHeight = useCallback( (height: number) => { storeSetTerminalHeight(threadRef, height); }, [storeSetTerminalHeight, threadRef], ); const splitTerminal = useCallback(() => { storeSplitTerminal(threadRef, `terminal-${randomUUID()}`); bumpFocusRequestId(); }, [bumpFocusRequestId, storeSplitTerminal, threadRef]); Lines 1–500 of 3493. Load more
import { type ApprovalRequestId, DEFAULT_MODEL_BY_PROVIDER, type EnvironmentId, type MessageId, type ModelSelection, type ProjectScript, type ProviderKind, type ProjectId, type ProviderApprovalDecision, type ServerProvider, type ResolvedKeybindingsConfig, type ScopedThreadRef, type ThreadId, type TurnId, type KeybindingCommand, OrchestrationThreadActivity, ProviderInteractionMode, RuntimeMode, TerminalOpenInput, } from "@t3tools/contracts"; import { parseScopedThreadKey, scopedThreadKey, scopeProjectRef, scopeThreadRef, } from "@t3tools/client-runtime"; import { applyClaudePromptEffortPrefix, createModelSelection, resolvePromptInjectedEffort, } from "@t3tools/shared/model"; import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts"; import { truncate } from "@t3tools/shared/String"; import { Debouncer } from "@tanstack/react-pacer"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useNavigate, useSearch } from "@tanstack/react-router"; import { useShallow } from "zustand/react/shallow"; import { useGitStatus } from "~/lib/gitStatusState"; import { usePrimaryEnvironmentId } from "../environments/primary"; import { readEnvironmentApi } from "../environmentApi"; import { isElectron } from "../env"; import { readLocalApi } from "../localApi"; import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch"; import { collapseExpandedComposerCursor, parseStandaloneComposerSlashCommand, } from "../composer-logic"; import { deriveCompletionDividerBeforeEntryId, derivePendingApprovals, derivePendingUserInputs, derivePhase, deriveTimelineEntries, deriveActiveWorkStartedAt, deriveActivePlanState, findSidebarProposedPlan, findLatestProposedPlan, deriveWorkLogEntries, hasActionableProposedPlan, hasToolActivityForTurn, isLatestTurnSettled, formatElapsed, } from "../session-logic"; import { type LegendListRef } from "@legendapp/list/react"; import { buildPendingUserInputAnswers, derivePendingUserInputProgress, setPendingUserInputCustomAnswer, togglePendingUserInputOptionSelection, type PendingUserInputDraftAnswer, } from "../pendingUserInput"; import { selectProjectsAcrossEnvironments, selectThreadsAcrossEnvironments, useStore, } from "../store"; import { createProjectSelectorByRef, createThreadSelectorByRef } from "../storeSelectors"; import { useUiStateStore } from "../uiStateStore"; import { buildPlanImplementationThreadTitle, buildPlanImplementationPrompt, resolvePlanFollowUpSubmission, } from "../proposedPlan"; import { DEFAULT_INTERACTION_MODE, DEFAULT_RUNTIME_MODE, DEFAULT_THREAD_TERMINAL_ID, MAX_TERMINALS_PER_GROUP, type ChatMessage, type SessionPhase, type Thread, type TurnDiffSummary, } from "../types"; import { useTheme } from "../hooks/useTheme"; import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; import { useCommandPaletteStore } from "../commandPaletteStore"; import { buildTemporaryWorktreeBranchName } from "@t3tools/shared/git"; import { useMediaQuery } from "../hooks/useMediaQuery"; import { RIGHT_PANEL_INLINE_LAYOUT_MEDIA_QUERY } from "../rightPanelLayout"; import { BranchToolbar } from "./BranchToolbar"; import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; import PlanSidebar from "./PlanSidebar"; import ThreadTerminalDrawer from "./ThreadTerminalDrawer"; import { ChevronDownIcon } from "lucide-react"; import { cn, randomUUID } from "~/lib/utils"; import { stackedThreadToast, toastManager } from "./ui/toast"; import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings"; import { type NewProjectScriptInput } from "./ProjectScriptsControl"; import { commandForProjectScript, nextProjectScriptId, projectScriptIdFromCommand, } from "~/projectScripts"; import { newCommandId, newDraftId, newMessageId, newThreadId } from "~/lib/utils"; import { getProviderModelCapabilities, resolveSelectableProvider } from "../providerModels"; import { useSettings } from "../hooks/useSettings"; import { resolveAppModelSelection } from "../modelSelection"; import { isTerminalFocused } from "../lib/terminalFocus"; import { deriveLogicalProjectKeyFromSettings } from "../logicalProject"; import { useSavedEnvironmentRegistryStore, useSavedEnvironmentRuntimeStore, } from "../environments/runtime"; import { buildDraftThreadRouteParams } from "../threadRoutes"; import { type ComposerImageAttachment, type DraftThreadEnvMode, useComposerDraftStore, type DraftId, } from "../composerDraftStore"; import { appendTerminalContextsToPrompt, formatTerminalContextLabel, type TerminalContextDraft, type TerminalContextSelection, } from "../lib/terminalContext"; import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore"; import { ChatComposer, type ChatComposerHandle } from "./chat/ChatComposer"; import { ExpandedImageDialog } from "./chat/ExpandedImageDialog"; import { PullRequestThreadDialog } from "./PullRequestThreadDialog"; import { MessagesTimeline } from "./chat/MessagesTimeline"; import { ChatHeader } from "./chat/ChatHeader"; import { type ExpandedImagePreview } from "./chat/ExpandedImagePreview"; import { NoActiveThreadState } from "./NoActiveThreadState"; import { resolveEffectiveEnvMode, resolveEnvironmentOptionLabel } from "./BranchToolbar.logic"; import { ProviderStatusBanner } from "./chat/ProviderStatusBanner"; import { ThreadErrorBanner } from "./chat/ThreadErrorBanner"; import { MAX_HIDDEN_MOUNTED_TERMINAL_THREADS, buildExpiredTerminalContextToastCopy, buildLocalDraftThread, collectUserMessageBlobPreviewUrls, createLocalDispatchSnapshot, deriveComposerSendState, hasServerAcknowledgedLocalDispatch, LAST_INVOKED_SCRIPT_BY_PROJECT_KEY, LastInvokedScriptByProjectSchema, type LocalDispatchSnapshot, PullRequestDialogState, cloneComposerImageForRetry, deriveLockedProvider, readFileAsDataUrl, reconcileMountedTerminalThreadIds, resolveSendEnvMode, revokeBlobPreviewUrl, revokeUserMessagePreviewUrls, shouldWriteThreadErrorToCurrentServerThread, waitForStartedServerThread, } from "./ChatView.logic"; import { useLocalStorage } from "~/hooks/useLocalStorage"; import { useComposerHandleContext } from "../composerHandleContext"; import { useServerAvailableEditors, useServerConfig, useServerKeybindings, } from "~/rpc/serverState"; import { sanitizeThreadErrorMessage } from "~/rpc/transportError"; import { retainThreadDetailSubscription } from "../environments/runtime/service"; import { RightPanelSheet } from "./RightPanelSheet"; const IMAGE_ONLY_BOOTSTRAP_PROMPT = "[User attached one or more images without additional text. Respond using the conversation context and the attached image(s).]"; const EMPTY_ACTIVITIES: OrchestrationThreadActivity[] = []; const EMPTY_PROPOSED_PLANS: Thread["proposedPlans"] = []; const EMPTY_PROVIDERS: ServerProvider[] = []; const EMPTY_PENDING_USER_INPUT_ANSWERS: Record<string, PendingUserInputDraftAnswer> = {}; type ThreadPlanCatalogEntry = Pick<Thread, "id" | "proposedPlans">; function useThreadPlanCatalog(threadIds: readonly ThreadId[]): ThreadPlanCatalogEntry[] { return useStore( useMemo(() => { let previousThreadIds: readonly ThreadId[] = []; let previousResult: ThreadPlanCatalogEntry[] = []; let previousEntries = new Map< ThreadId, { shell: object | null; proposedPlanIds: readonly string[] | undefined; proposedPlansById: Record<string, Thread["proposedPlans"][number]> | undefined; entry: ThreadPlanCatalogEntry; } >(); return (state) => { const sameThreadIds = previousThreadIds.length === threadIds.length && previousThreadIds.every((id, index) => id === threadIds[index]); const nextEntries = new Map< ThreadId, { shell: object | null; proposedPlanIds: readonly string[] | undefined; proposedPlansById: Record<string, Thread["proposedPlans"][number]> | undefined; entry: ThreadPlanCatalogEntry; } >(); const nextResult: ThreadPlanCatalogEntry[] = []; let changed = !sameThreadIds; for (const threadId of threadIds) { let shell: object | undefined; let proposedPlanIds: readonly string[] | undefined; let proposedPlansById: Record<string, Thread["proposedPlans"][number]> | undefined; for (const environmentState of Object.values(state.environmentStateById)) { const matchedShell = environmentState.threadShellById[threadId]; if (!matchedShell) { continue; } shell = matchedShell; proposedPlanIds = environmentState.proposedPlanIdsByThreadId[threadId]; proposedPlansById = environmentState.proposedPlanByThreadId[threadId] as | Record<string, Thread["proposedPlans"][number]> | undefined; break; } if (!shell) { const previous = previousEntries.get(threadId); if ( previous && previous.shell === null && previous.proposedPlanIds === undefined && previous.proposedPlansById === undefined ) { nextEntries.set(threadId, previous); continue; } changed = true; nextEntries.set(threadId, { shell: null, proposedPlanIds: undefined, proposedPlansById: undefined, entry: { id: threadId, proposedPlans: EMPTY_PROPOSED_PLANS }, }); continue; } const previous = previousEntries.get(threadId); if ( previous && previous.shell === shell && previous.proposedPlanIds === proposedPlanIds && previous.proposedPlansById === proposedPlansById ) { nextEntries.set(threadId, previous); nextResult.push(previous.entry); continue; } changed = true; const proposedPlans = proposedPlanIds && proposedPlanIds.length > 0 && proposedPlansById ? proposedPlanIds.flatMap((planId) => { const proposedPlan = proposedPlansById?.[planId]; return proposedPlan ? [proposedPlan] : []; }) : EMPTY_PROPOSED_PLANS; const entry = { id: threadId, proposedPlans }; nextEntries.set(threadId, { shell, proposedPlanIds, proposedPlansById, entry, }); nextResult.push(entry); } if (!changed && previousResult.length === nextResult.length) { return previousResult; } previousThreadIds = threadIds; previousEntries = nextEntries; previousResult = nextResult; return nextResult; }; }, [threadIds]), ); } function formatOutgoingPrompt(params: { provider: ProviderKind; model: string | null; models: ReadonlyArray<ServerProvider["models"][number]>; effort: string | null; text: string; }): string { const caps = getProviderModelCapabilities(params.models, params.model, params.provider); const promptEffort = resolvePromptInjectedEffort(caps, params.effort); return applyClaudePromptEffortPrefix(params.text, promptEffort); } const SCRIPT_TERMINAL_COLS = 120; const SCRIPT_TERMINAL_ROWS = 30; type ChatViewProps = | { environmentId: EnvironmentId; threadId: ThreadId; onDiffPanelOpen?: () => void; reserveTitleBarControlInset?: boolean; routeKind: "server"; draftId?: never; } | { environmentId: EnvironmentId; threadId: ThreadId; onDiffPanelOpen?: () => void; reserveTitleBarControlInset?: boolean; routeKind: "draft"; draftId: DraftId; }; interface TerminalLaunchContext { threadId: ThreadId; cwd: string; worktreePath: string | null; } type PersistentTerminalLaunchContext = Pick<TerminalLaunchContext, "cwd" | "worktreePath">; function useLocalDispatchState(input: { activeThread: Thread | undefined; activeLatestTurn: Thread["latestTurn"] | null; phase: SessionPhase; activePendingApproval: ApprovalRequestId | null; activePendingUserInput: ApprovalRequestId | null; threadError: string | null | undefined; }) { const [localDispatch, setLocalDispatch] = useState<LocalDispatchSnapshot | null>(null); const beginLocalDispatch = useCallback( (options?: { preparingWorktree?: boolean }) => { const preparingWorktree = Boolean(options?.preparingWorktree); setLocalDispatch((current) => { if (current) { return current.preparingWorktree === preparingWorktree ? current : { ...current, preparingWorktree }; } return createLocalDispatchSnapshot(input.activeThread, options); }); }, [input.activeThread], ); const resetLocalDispatch = useCallback(() => { setLocalDispatch(null); }, []); const serverAcknowledgedLocalDispatch = useMemo( () => hasServerAcknowledgedLocalDispatch({ localDispatch, phase: input.phase, latestTurn: input.activeLatestTurn, session: input.activeThread?.session ?? null, hasPendingApproval: input.activePendingApproval !== null, hasPendingUserInput: input.activePendingUserInput !== null, threadError: input.threadError, }), [ input.activeLatestTurn, input.activePendingApproval, input.activePendingUserInput, input.activeThread?.session, input.phase, input.threadError, localDispatch, ], ); useEffect(() => { if (!serverAcknowledgedLocalDispatch) { return; } resetLocalDispatch(); }, [resetLocalDispatch, serverAcknowledgedLocalDispatch]); return { beginLocalDispatch, resetLocalDispatch, localDispatchStartedAt: localDispatch?.startedAt ?? null, isPreparingWorktree: localDispatch?.preparingWorktree ?? false, isSendBusy: localDispatch !== null && !serverAcknowledgedLocalDispatch, }; } interface PersistentThreadTerminalDrawerProps { threadRef: { environmentId: EnvironmentId; threadId: ThreadId }; threadId: ThreadId; visible: boolean; launchContext: PersistentTerminalLaunchContext | null; focusRequestId: number; splitShortcutLabel: string | undefined; newShortcutLabel: string | undefined; closeShortcutLabel: string | undefined; keybindings: ResolvedKeybindingsConfig; onAddTerminalContext: (selection: TerminalContextSelection) => void; } const PersistentThreadTerminalDrawer = memo(function PersistentThreadTerminalDrawer({ threadRef, threadId, visible, launchContext, focusRequestId, splitShortcutLabel, newShortcutLabel, closeShortcutLabel, keybindings, onAddTerminalContext, }: PersistentThreadTerminalDrawerProps) { const serverThread = useStore(useMemo(() => createThreadSelectorByRef(threadRef), [threadRef])); const draftThread = useComposerDraftStore((store) => store.getDraftThreadByRef(threadRef)); const projectRef = serverThread ? scopeProjectRef(serverThread.environmentId, serverThread.projectId) : draftThread ? scopeProjectRef(draftThread.environmentId, draftThread.projectId) : null; const project = useStore(useMemo(() => createProjectSelectorByRef(projectRef), [projectRef])); const terminalState = useTerminalStateStore((state) => selectThreadTerminalState(state.terminalStateByThreadKey, threadRef), ); const storeSetTerminalHeight = useTerminalStateStore((state) => state.setTerminalHeight); const storeSplitTerminal = useTerminalStateStore((state) => state.splitTerminal); const storeNewTerminal = useTerminalStateStore((state) => state.newTerminal); const storeSetActiveTerminal = useTerminalStateStore((state) => state.setActiveTerminal); const storeCloseTerminal = useTerminalStateStore((state) => state.closeTerminal); const [localFocusRequestId, setLocalFocusRequestId] = useState(0); const worktreePath = serverThread?.worktreePath ?? draftThread?.worktreePath ?? null; const effectiveWorktreePath = useMemo(() => { if (launchContext !== null) { return launchContext.worktreePath; } return worktreePath; }, [launchContext, worktreePath]); const cwd = useMemo( () => launchContext?.cwd ?? (project ? projectScriptCwd({ project: { cwd: project.cwd }, worktreePath: effectiveWorktreePath, }) : null), [effectiveWorktreePath, launchContext?.cwd, project], ); const runtimeEnv = useMemo( () => project ? projectScriptRuntimeEnv({ project: { cwd: project.cwd }, worktreePath: effectiveWorktreePath, }) : {}, [effectiveWorktreePath, project], ); const bumpFocusRequestId = useCallback(() => { if (!visible) { return; } setLocalFocusRequestId((value) => value + 1); }, [visible]); const setTerminalHeight = useCallback( (height: number) => { storeSetTerminalHeight(threadRef, height); }, [storeSetTerminalHeight, threadRef], ); const splitTerminal = useCallback(() => { storeSplitTerminal(threadRef, `terminal-${randomUUID()}`); bumpFocusRequestId(); }, [bumpFocusRequestId, storeSplitTerminal, threadRef]);