import { useAtomSet, useAtomValue } from "@effect/atom-react";import { type ApprovalRequestId, DEFAULT_MODEL_BY_PROVIDER, type ClaudeCodeEffort, type MessageId, type ModelSelection, type ProjectScript, type ProviderKind, type ProjectEntry, type ProjectId, type ProviderApprovalDecision, PROVIDER_SEND_TURN_MAX_ATTACHMENTS, PROVIDER_SEND_TURN_MAX_IMAGE_BYTES, type ServerProvider, type ThreadId, type TurnId, type KeybindingCommand, OrchestrationThreadActivity, ProviderInteractionMode, RuntimeMode, TerminalOpenInput,} from "@t3tools/contracts";import { applyClaudePromptEffortPrefix, normalizeModelSlug } from "@t3tools/shared/model";import * as Option from "effect/Option";import { truncate } from "@t3tools/shared/String";import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";import { useQuery } from "@tanstack/react-query";import { useDebouncedValue } from "@tanstack/react-pacer";import { useNavigate, useSearch } from "@tanstack/react-router";import { AsyncResult } from "effect/unstable/reactivity";import { projectSearchEntriesQueryOptions } from "~/lib/projectReactQuery";import { isElectron } from "../env";import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";import { clampCollapsedComposerCursor, type ComposerTrigger, collapseExpandedComposerCursor, detectComposerTrigger, expandCollapsedComposerCursor, parseStandaloneComposerSlashCommand, replaceTextRange,} from "../composer-logic";import { deriveCompletionDividerBeforeEntryId, derivePendingApprovals, derivePendingUserInputs, derivePhase, deriveTimelineEntries, deriveActiveWorkStartedAt, deriveActivePlanState, findSidebarProposedPlan, findLatestProposedPlan, deriveWorkLogEntries, hasActionableProposedPlan, hasToolActivityForTurn, isLatestTurnSettled, formatElapsed,} from "../session-logic";import { isScrollContainerNearBottom } from "../chat-scroll";import { buildPendingUserInputAnswers, derivePendingUserInputProgress, setPendingUserInputCustomAnswer, type PendingUserInputDraftAnswer,} from "../pendingUserInput";import { useStore } from "../store";import { useProjectById, useThreadById } from "../storeSelectors";import { useUiStateStore } from "../uiStateStore";import { buildPlanImplementationThreadTitle, buildPlanImplementationPrompt, proposedPlanTitle, 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 { LRUCache } from "../lib/lruCache"; import { basenameOfPath } from "../vscode-icons";import { useTheme } from "../hooks/useTheme";import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";import BranchToolbar from "./BranchToolbar";import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings";import PlanSidebar from "./PlanSidebar";import ThreadTerminalDrawer from "./ThreadTerminalDrawer";import { BotIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, CircleAlertIcon, ListTodoIcon, LockIcon, LockOpenIcon, XIcon,} from "lucide-react";import { Button } from "./ui/button";import { Separator } from "./ui/separator";import { cn, randomUUID } from "~/lib/utils";import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";import { toastManager } from "./ui/toast";import { gitBranchesAtom, gitCreateWorktreeMutationAtom } from "~/rpc/gitAtoms";import { REACTIVITY_KEYS } from "~/rpc/client";import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings";import { type NewProjectScriptInput } from "./ProjectScriptsControl";import { commandForProjectScript, nextProjectScriptId, projectScriptCwd, projectScriptRuntimeEnv, projectScriptIdFromCommand, setupProjectScript,} from "~/projectScripts";import { SidebarTrigger } from "./ui/sidebar";import { newCommandId, newMessageId, newThreadId } from "~/lib/utils";import { readNativeApi } from "~/nativeApi";import { getProviderModelCapabilities, getProviderModels, resolveSelectableProvider,} from "../providerModels";import { useSettings } from "../hooks/useSettings";import { resolveAppModelSelection } from "../modelSelection";import { isTerminalFocused } from "../lib/terminalFocus";import { type ComposerImageAttachment, type DraftThreadEnvMode, type PersistedComposerImageAttachment, useComposerDraftStore, useEffectiveComposerModelState, useComposerThreadDraft,} from "../composerDraftStore";import { appendTerminalContextsToPrompt, formatTerminalContextLabel, insertInlineTerminalContextPlaceholder, removeInlineTerminalContextPlaceholder, type TerminalContextDraft, type TerminalContextSelection,} from "../lib/terminalContext";import { deriveLatestContextWindowSnapshot } from "../lib/contextWindow";import { resolveComposerFooterContentWidth, shouldForceCompactComposerFooterForFit, shouldUseCompactComposerPrimaryActions, shouldUseCompactComposerFooter,} from "./composerFooterLayout";import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore";import { ComposerPromptEditor, type ComposerPromptEditorHandle } from "./ComposerPromptEditor";import { PullRequestThreadDialog } from "./PullRequestThreadDialog";import { MessagesTimeline } from "./chat/MessagesTimeline";import { ChatHeader } from "./chat/ChatHeader";import { ContextWindowMeter } from "./chat/ContextWindowMeter";import { buildExpandedImagePreview, ExpandedImagePreview } from "./chat/ExpandedImagePreview";import { AVAILABLE_PROVIDER_OPTIONS, ProviderModelPicker } from "./chat/ProviderModelPicker";import { ComposerCommandItem, ComposerCommandMenu } from "./chat/ComposerCommandMenu";import { ComposerPendingApprovalActions } from "./chat/ComposerPendingApprovalActions";import { CompactComposerControlsMenu } from "./chat/CompactComposerControlsMenu";import { ComposerPrimaryActions } from "./chat/ComposerPrimaryActions";import { ComposerPendingApprovalPanel } from "./chat/ComposerPendingApprovalPanel";import { ComposerPendingUserInputPanel } from "./chat/ComposerPendingUserInputPanel";import { ComposerPlanFollowUpBanner } from "./chat/ComposerPlanFollowUpBanner";import { getComposerProviderState, renderProviderTraitsMenuContent, renderProviderTraitsPicker,} from "./chat/composerProviderRegistry";import { ProviderStatusBanner } from "./chat/ProviderStatusBanner";import { ThreadErrorBanner } from "./chat/ThreadErrorBanner";import { buildExpiredTerminalContextToastCopy, buildLocalDraftThread, buildTemporaryWorktreeBranchName, cloneComposerImageForRetry, collectUserMessageBlobPreviewUrls, createLocalDispatchSnapshot, deriveComposerSendState, hasServerAcknowledgedLocalDispatch, LAST_INVOKED_SCRIPT_BY_PROJECT_KEY, LastInvokedScriptByProjectSchema, type LocalDispatchSnapshot, PullRequestDialogState, readFileAsDataUrl, revokeBlobPreviewUrl, revokeUserMessagePreviewUrls, threadHasStarted, waitForStartedServerThread,} from "./ChatView.logic";import { useLocalStorage } from "~/hooks/useLocalStorage";import { useServerAvailableEditors, useServerConfig, useServerKeybindings,} from "~/rpc/serverState"; const ATTACHMENT_PREVIEW_HANDOFF_TTL_MS = 5000;const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`;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_PROJECT_ENTRIES: ProjectEntry[] = [];const EMPTY_PROVIDERS: ServerProvider[] = [];const EMPTY_PENDING_USER_INPUT_ANSWERS: Record<string, PendingUserInputDraftAnswer> = {}; type ThreadPlanCatalogEntry = Pick<Thread, "id" | "proposedPlans">; const MAX_THREAD_PLAN_CATALOG_CACHE_ENTRIES = 500;const MAX_THREAD_PLAN_CATALOG_CACHE_MEMORY_BYTES = 512 * 1024;const threadPlanCatalogCache = new LRUCache<{ proposedPlans: Thread["proposedPlans"]; entry: ThreadPlanCatalogEntry;}>(MAX_THREAD_PLAN_CATALOG_CACHE_ENTRIES, MAX_THREAD_PLAN_CATALOG_CACHE_MEMORY_BYTES); function estimateThreadPlanCatalogEntrySize(thread: Thread): number { return Math.max( 64, thread.id.length + thread.proposedPlans.reduce( (total, plan) => total + plan.id.length + plan.planMarkdown.length + plan.updatedAt.length + (plan.turnId?.length ?? 0), 0, ), );} function toThreadPlanCatalogEntry(thread: Thread): ThreadPlanCatalogEntry { const cached = threadPlanCatalogCache.get(thread.id); if (cached && cached.proposedPlans === thread.proposedPlans) { return cached.entry; } const entry: ThreadPlanCatalogEntry = { id: thread.id, proposedPlans: thread.proposedPlans, }; threadPlanCatalogCache.set( thread.id, { proposedPlans: thread.proposedPlans, entry, }, estimateThreadPlanCatalogEntrySize(thread), ); return entry;} function useThreadPlanCatalog(threadIds: readonly ThreadId[]): ThreadPlanCatalogEntry[] { const selector = useMemo(() => { let previousThreads: Array<Thread | undefined> | null = null; let previousEntries: ThreadPlanCatalogEntry[] = []; return (state: { threads: Thread[] }): ThreadPlanCatalogEntry[] => { const nextThreads = threadIds.map((threadId) => state.threads.find((thread) => thread.id === threadId), ); const cachedThreads = previousThreads; if ( cachedThreads && nextThreads.length === cachedThreads.length && nextThreads.every((thread, index) => thread === cachedThreads[index]) ) { return previousEntries; } previousThreads = nextThreads; previousEntries = nextThreads.flatMap((thread) => thread ? [toThreadPlanCatalogEntry(thread)] : [], ); return previousEntries; }; }, [threadIds]); return useStore(selector);} 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); if (params.effort && caps.promptInjectedEffortLevels.includes(params.effort)) { return applyClaudePromptEffortPrefix(params.text, params.effort as ClaudeCodeEffort | null); } return params.text;}const COMPOSER_PATH_QUERY_DEBOUNCE_MS = 120;const SCRIPT_TERMINAL_COLS = 120;const SCRIPT_TERMINAL_ROWS = 30; const extendReplacementRangeForTrailingSpace = ( text: string, rangeEnd: number, replacement: string,): number => { if (!replacement.endsWith(" ")) { return rangeEnd; } return text[rangeEnd] === " " ? rangeEnd + 1 : rangeEnd;}; const syncTerminalContextsByIds = ( contexts: ReadonlyArray<TerminalContextDraft>, ids: ReadonlyArray<string>,): TerminalContextDraft[] => { const contextsById = new Map(contexts.map((context) => [context.id, context])); return ids.flatMap((id) => { const context = contextsById.get(id); return context ? [context] : []; });}; const terminalContextIdListsEqual = ( contexts: ReadonlyArray<TerminalContextDraft>, ids: ReadonlyArray<string>,): boolean => contexts.length === ids.length && contexts.every((context, index) => context.id === ids[index]); interface ChatViewProps { threadId: ThreadId;} interface PendingPullRequestSetupRequest { threadId: ThreadId; worktreePath: string; scriptId: string;} 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, };} export default function ChatView({ threadId }: ChatViewProps) { const serverThread = useThreadById(threadId); const setStoreThreadError = useStore((store) => store.setError); const setStoreThreadBranch = useStore((store) => store.setThreadBranch); const markThreadVisited = useUiStateStore((store) => store.markThreadVisited); const activeThreadLastVisitedAt = useUiStateStore( (store) => store.threadLastVisitedAtById[threadId], ); const settings = useSettings(); const setStickyComposerModelSelection = useComposerDraftStore( (store) => store.setStickyModelSelection, ); const timestampFormat = settings.timestampFormat; const navigate = useNavigate(); const rawSearch = useSearch({ strict: false, select: (params) => parseDiffRouteSearch(params), }); const { resolvedTheme } = useTheme(); const createWorktree = useAtomSet(gitCreateWorktreeMutationAtom, { mode: "promise" }); const composerDraft = useComposerThreadDraft(threadId); const prompt = composerDraft.prompt; const composerImages = composerDraft.images; const composerTerminalContexts = composerDraft.terminalContexts; const composerSendState = useMemo( () => deriveComposerSendState({ prompt, imageCount: composerImages.length, terminalContexts: composerTerminalContexts, }), [composerImages.length, composerTerminalContexts, prompt], ); const nonPersistedComposerImageIds = composerDraft.nonPersistedImageIds; const setComposerDraftPrompt = useComposerDraftStore((store) => store.setPrompt); const setComposerDraftModelSelection = useComposerDraftStore((store) => store.setModelSelection); const setComposerDraftRuntimeMode = useComposerDraftStore((store) => store.setRuntimeMode); const setComposerDraftInteractionMode = useComposerDraftStore( (store) => store.setInteractionMode, ); const addComposerDraftImage = useComposerDraftStore((store) => store.addImage); const addComposerDraftImages = useComposerDraftStore((store) => store.addImages); const removeComposerDraftImage = useComposerDraftStore((store) => store.removeImage); const insertComposerDraftTerminalContext = useComposerDraftStore( (store) => store.insertTerminalContext, ); const addComposerDraftTerminalContexts = useComposerDraftStore( (store) => store.addTerminalContexts, ); const removeComposerDraftTerminalContext = useComposerDraftStore( (store) => store.removeTerminalContext, ); const setComposerDraftTerminalContexts = useComposerDraftStore( (store) => store.setTerminalContexts, ); const clearComposerDraftPersistedAttachments = useComposerDraftStore( (store) => store.clearPersistedAttachments, ); const syncComposerDraftPersistedAttachments = useComposerDraftStore( (store) => store.syncPersistedAttachments, ); const clearComposerDraftContent = useComposerDraftStore((store) => store.clearComposerContent); const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); const getDraftThreadByProjectId = useComposerDraftStore( (store) => store.getDraftThreadByProjectId, ); const getDraftThread = useComposerDraftStore((store) => store.getDraftThread); const setProjectDraftThreadId = useComposerDraftStore((store) => store.setProjectDraftThreadId); const clearProjectDraftThreadId = useComposerDraftStore( (store) => store.clearProjectDraftThreadId, ); const draftThread = useComposerDraftStore( (store) => store.draftThreadsByThreadId[threadId] ?? null, ); const promptRef = useRef(prompt); const [showScrollToBottom, setShowScrollToBottom] = useState(false); const [isDragOverComposer, setIsDragOverComposer] = useState(false); const [expandedImage, setExpandedImage] = useState<ExpandedImagePreview | null>(null); const [optimisticUserMessages, setOptimisticUserMessages] = useState<ChatMessage[]>([]); const optimisticUserMessagesRef = useRef(optimisticUserMessages); optimisticUserMessagesRef.current = optimisticUserMessages; const composerTerminalContextsRef = useRef<TerminalContextDraft[]>(composerTerminalContexts); const [localDraftErrorsByThreadId, setLocalDraftErrorsByThreadId] = useState< Record<ThreadId, string | null> >({}); const [isConnecting, _setIsConnecting] = useState(false); const [isRevertingCheckpoint, setIsRevertingCheckpoint] = useState(false); const [respondingRequestIds, setRespondingRequestIds] = useState<ApprovalRequestId[]>([]); const [respondingUserInputRequestIds, setRespondingUserInputRequestIds] = useState< ApprovalRequestId[]Lines 1–500 of 4396. Load more
import { useAtomSet, useAtomValue } from "@effect/atom-react"; import { type ApprovalRequestId, DEFAULT_MODEL_BY_PROVIDER, type ClaudeCodeEffort, type MessageId, type ModelSelection, type ProjectScript, type ProviderKind, type ProjectEntry, type ProjectId, type ProviderApprovalDecision, PROVIDER_SEND_TURN_MAX_ATTACHMENTS, PROVIDER_SEND_TURN_MAX_IMAGE_BYTES, type ServerProvider, type ThreadId, type TurnId, type KeybindingCommand, OrchestrationThreadActivity, ProviderInteractionMode, RuntimeMode, TerminalOpenInput, } from "@t3tools/contracts"; import { applyClaudePromptEffortPrefix, normalizeModelSlug } from "@t3tools/shared/model"; import * as Option from "effect/Option"; import { truncate } from "@t3tools/shared/String"; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { useDebouncedValue } from "@tanstack/react-pacer"; import { useNavigate, useSearch } from "@tanstack/react-router"; import { AsyncResult } from "effect/unstable/reactivity"; import { projectSearchEntriesQueryOptions } from "~/lib/projectReactQuery"; import { isElectron } from "../env"; import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch"; import { clampCollapsedComposerCursor, type ComposerTrigger, collapseExpandedComposerCursor, detectComposerTrigger, expandCollapsedComposerCursor, parseStandaloneComposerSlashCommand, replaceTextRange, } from "../composer-logic"; import { deriveCompletionDividerBeforeEntryId, derivePendingApprovals, derivePendingUserInputs, derivePhase, deriveTimelineEntries, deriveActiveWorkStartedAt, deriveActivePlanState, findSidebarProposedPlan, findLatestProposedPlan, deriveWorkLogEntries, hasActionableProposedPlan, hasToolActivityForTurn, isLatestTurnSettled, formatElapsed, } from "../session-logic"; import { isScrollContainerNearBottom } from "../chat-scroll"; import { buildPendingUserInputAnswers, derivePendingUserInputProgress, setPendingUserInputCustomAnswer, type PendingUserInputDraftAnswer, } from "../pendingUserInput"; import { useStore } from "../store"; import { useProjectById, useThreadById } from "../storeSelectors"; import { useUiStateStore } from "../uiStateStore"; import { buildPlanImplementationThreadTitle, buildPlanImplementationPrompt, proposedPlanTitle, 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 { LRUCache } from "../lib/lruCache"; import { basenameOfPath } from "../vscode-icons"; import { useTheme } from "../hooks/useTheme"; import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; import BranchToolbar from "./BranchToolbar"; import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; import PlanSidebar from "./PlanSidebar"; import ThreadTerminalDrawer from "./ThreadTerminalDrawer"; import { BotIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, CircleAlertIcon, ListTodoIcon, LockIcon, LockOpenIcon, XIcon, } from "lucide-react"; import { Button } from "./ui/button"; import { Separator } from "./ui/separator"; import { cn, randomUUID } from "~/lib/utils"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; import { toastManager } from "./ui/toast"; import { gitBranchesAtom, gitCreateWorktreeMutationAtom } from "~/rpc/gitAtoms"; import { REACTIVITY_KEYS } from "~/rpc/client"; import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings"; import { type NewProjectScriptInput } from "./ProjectScriptsControl"; import { commandForProjectScript, nextProjectScriptId, projectScriptCwd, projectScriptRuntimeEnv, projectScriptIdFromCommand, setupProjectScript, } from "~/projectScripts"; import { SidebarTrigger } from "./ui/sidebar"; import { newCommandId, newMessageId, newThreadId } from "~/lib/utils"; import { readNativeApi } from "~/nativeApi"; import { getProviderModelCapabilities, getProviderModels, resolveSelectableProvider, } from "../providerModels"; import { useSettings } from "../hooks/useSettings"; import { resolveAppModelSelection } from "../modelSelection"; import { isTerminalFocused } from "../lib/terminalFocus"; import { type ComposerImageAttachment, type DraftThreadEnvMode, type PersistedComposerImageAttachment, useComposerDraftStore, useEffectiveComposerModelState, useComposerThreadDraft, } from "../composerDraftStore"; import { appendTerminalContextsToPrompt, formatTerminalContextLabel, insertInlineTerminalContextPlaceholder, removeInlineTerminalContextPlaceholder, type TerminalContextDraft, type TerminalContextSelection, } from "../lib/terminalContext"; import { deriveLatestContextWindowSnapshot } from "../lib/contextWindow"; import { resolveComposerFooterContentWidth, shouldForceCompactComposerFooterForFit, shouldUseCompactComposerPrimaryActions, shouldUseCompactComposerFooter, } from "./composerFooterLayout"; import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore"; import { ComposerPromptEditor, type ComposerPromptEditorHandle } from "./ComposerPromptEditor"; import { PullRequestThreadDialog } from "./PullRequestThreadDialog"; import { MessagesTimeline } from "./chat/MessagesTimeline"; import { ChatHeader } from "./chat/ChatHeader"; import { ContextWindowMeter } from "./chat/ContextWindowMeter"; import { buildExpandedImagePreview, ExpandedImagePreview } from "./chat/ExpandedImagePreview"; import { AVAILABLE_PROVIDER_OPTIONS, ProviderModelPicker } from "./chat/ProviderModelPicker"; import { ComposerCommandItem, ComposerCommandMenu } from "./chat/ComposerCommandMenu"; import { ComposerPendingApprovalActions } from "./chat/ComposerPendingApprovalActions"; import { CompactComposerControlsMenu } from "./chat/CompactComposerControlsMenu"; import { ComposerPrimaryActions } from "./chat/ComposerPrimaryActions"; import { ComposerPendingApprovalPanel } from "./chat/ComposerPendingApprovalPanel"; import { ComposerPendingUserInputPanel } from "./chat/ComposerPendingUserInputPanel"; import { ComposerPlanFollowUpBanner } from "./chat/ComposerPlanFollowUpBanner"; import { getComposerProviderState, renderProviderTraitsMenuContent, renderProviderTraitsPicker, } from "./chat/composerProviderRegistry"; import { ProviderStatusBanner } from "./chat/ProviderStatusBanner"; import { ThreadErrorBanner } from "./chat/ThreadErrorBanner"; import { buildExpiredTerminalContextToastCopy, buildLocalDraftThread, buildTemporaryWorktreeBranchName, cloneComposerImageForRetry, collectUserMessageBlobPreviewUrls, createLocalDispatchSnapshot, deriveComposerSendState, hasServerAcknowledgedLocalDispatch, LAST_INVOKED_SCRIPT_BY_PROJECT_KEY, LastInvokedScriptByProjectSchema, type LocalDispatchSnapshot, PullRequestDialogState, readFileAsDataUrl, revokeBlobPreviewUrl, revokeUserMessagePreviewUrls, threadHasStarted, waitForStartedServerThread, } from "./ChatView.logic"; import { useLocalStorage } from "~/hooks/useLocalStorage"; import { useServerAvailableEditors, useServerConfig, useServerKeybindings, } from "~/rpc/serverState"; const ATTACHMENT_PREVIEW_HANDOFF_TTL_MS = 5000; const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`; 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_PROJECT_ENTRIES: ProjectEntry[] = []; const EMPTY_PROVIDERS: ServerProvider[] = []; const EMPTY_PENDING_USER_INPUT_ANSWERS: Record<string, PendingUserInputDraftAnswer> = {}; type ThreadPlanCatalogEntry = Pick<Thread, "id" | "proposedPlans">; const MAX_THREAD_PLAN_CATALOG_CACHE_ENTRIES = 500; const MAX_THREAD_PLAN_CATALOG_CACHE_MEMORY_BYTES = 512 * 1024; const threadPlanCatalogCache = new LRUCache<{ proposedPlans: Thread["proposedPlans"]; entry: ThreadPlanCatalogEntry; }>(MAX_THREAD_PLAN_CATALOG_CACHE_ENTRIES, MAX_THREAD_PLAN_CATALOG_CACHE_MEMORY_BYTES); function estimateThreadPlanCatalogEntrySize(thread: Thread): number { return Math.max( 64, thread.id.length + thread.proposedPlans.reduce( (total, plan) => total + plan.id.length + plan.planMarkdown.length + plan.updatedAt.length + (plan.turnId?.length ?? 0), 0, ), ); } function toThreadPlanCatalogEntry(thread: Thread): ThreadPlanCatalogEntry { const cached = threadPlanCatalogCache.get(thread.id); if (cached && cached.proposedPlans === thread.proposedPlans) { return cached.entry; } const entry: ThreadPlanCatalogEntry = { id: thread.id, proposedPlans: thread.proposedPlans, }; threadPlanCatalogCache.set( thread.id, { proposedPlans: thread.proposedPlans, entry, }, estimateThreadPlanCatalogEntrySize(thread), ); return entry; } function useThreadPlanCatalog(threadIds: readonly ThreadId[]): ThreadPlanCatalogEntry[] { const selector = useMemo(() => { let previousThreads: Array<Thread | undefined> | null = null; let previousEntries: ThreadPlanCatalogEntry[] = []; return (state: { threads: Thread[] }): ThreadPlanCatalogEntry[] => { const nextThreads = threadIds.map((threadId) => state.threads.find((thread) => thread.id === threadId), ); const cachedThreads = previousThreads; if ( cachedThreads && nextThreads.length === cachedThreads.length && nextThreads.every((thread, index) => thread === cachedThreads[index]) ) { return previousEntries; } previousThreads = nextThreads; previousEntries = nextThreads.flatMap((thread) => thread ? [toThreadPlanCatalogEntry(thread)] : [], ); return previousEntries; }; }, [threadIds]); return useStore(selector); } 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); if (params.effort && caps.promptInjectedEffortLevels.includes(params.effort)) { return applyClaudePromptEffortPrefix(params.text, params.effort as ClaudeCodeEffort | null); } return params.text; } const COMPOSER_PATH_QUERY_DEBOUNCE_MS = 120; const SCRIPT_TERMINAL_COLS = 120; const SCRIPT_TERMINAL_ROWS = 30; const extendReplacementRangeForTrailingSpace = ( text: string, rangeEnd: number, replacement: string, ): number => { if (!replacement.endsWith(" ")) { return rangeEnd; } return text[rangeEnd] === " " ? rangeEnd + 1 : rangeEnd; }; const syncTerminalContextsByIds = ( contexts: ReadonlyArray<TerminalContextDraft>, ids: ReadonlyArray<string>, ): TerminalContextDraft[] => { const contextsById = new Map(contexts.map((context) => [context.id, context])); return ids.flatMap((id) => { const context = contextsById.get(id); return context ? [context] : []; }); }; const terminalContextIdListsEqual = ( contexts: ReadonlyArray<TerminalContextDraft>, ids: ReadonlyArray<string>, ): boolean => contexts.length === ids.length && contexts.every((context, index) => context.id === ids[index]); interface ChatViewProps { threadId: ThreadId; } interface PendingPullRequestSetupRequest { threadId: ThreadId; worktreePath: string; scriptId: string; } 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, }; } export default function ChatView({ threadId }: ChatViewProps) { const serverThread = useThreadById(threadId); const setStoreThreadError = useStore((store) => store.setError); const setStoreThreadBranch = useStore((store) => store.setThreadBranch); const markThreadVisited = useUiStateStore((store) => store.markThreadVisited); const activeThreadLastVisitedAt = useUiStateStore( (store) => store.threadLastVisitedAtById[threadId], ); const settings = useSettings(); const setStickyComposerModelSelection = useComposerDraftStore( (store) => store.setStickyModelSelection, ); const timestampFormat = settings.timestampFormat; const navigate = useNavigate(); const rawSearch = useSearch({ strict: false, select: (params) => parseDiffRouteSearch(params), }); const { resolvedTheme } = useTheme(); const createWorktree = useAtomSet(gitCreateWorktreeMutationAtom, { mode: "promise" }); const composerDraft = useComposerThreadDraft(threadId); const prompt = composerDraft.prompt; const composerImages = composerDraft.images; const composerTerminalContexts = composerDraft.terminalContexts; const composerSendState = useMemo( () => deriveComposerSendState({ prompt, imageCount: composerImages.length, terminalContexts: composerTerminalContexts, }), [composerImages.length, composerTerminalContexts, prompt], ); const nonPersistedComposerImageIds = composerDraft.nonPersistedImageIds; const setComposerDraftPrompt = useComposerDraftStore((store) => store.setPrompt); const setComposerDraftModelSelection = useComposerDraftStore((store) => store.setModelSelection); const setComposerDraftRuntimeMode = useComposerDraftStore((store) => store.setRuntimeMode); const setComposerDraftInteractionMode = useComposerDraftStore( (store) => store.setInteractionMode, ); const addComposerDraftImage = useComposerDraftStore((store) => store.addImage); const addComposerDraftImages = useComposerDraftStore((store) => store.addImages); const removeComposerDraftImage = useComposerDraftStore((store) => store.removeImage); const insertComposerDraftTerminalContext = useComposerDraftStore( (store) => store.insertTerminalContext, ); const addComposerDraftTerminalContexts = useComposerDraftStore( (store) => store.addTerminalContexts, ); const removeComposerDraftTerminalContext = useComposerDraftStore( (store) => store.removeTerminalContext, ); const setComposerDraftTerminalContexts = useComposerDraftStore( (store) => store.setTerminalContexts, ); const clearComposerDraftPersistedAttachments = useComposerDraftStore( (store) => store.clearPersistedAttachments, ); const syncComposerDraftPersistedAttachments = useComposerDraftStore( (store) => store.syncPersistedAttachments, ); const clearComposerDraftContent = useComposerDraftStore((store) => store.clearComposerContent); const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); const getDraftThreadByProjectId = useComposerDraftStore( (store) => store.getDraftThreadByProjectId, ); const getDraftThread = useComposerDraftStore((store) => store.getDraftThread); const setProjectDraftThreadId = useComposerDraftStore((store) => store.setProjectDraftThreadId); const clearProjectDraftThreadId = useComposerDraftStore( (store) => store.clearProjectDraftThreadId, ); const draftThread = useComposerDraftStore( (store) => store.draftThreadsByThreadId[threadId] ?? null, ); const promptRef = useRef(prompt); const [showScrollToBottom, setShowScrollToBottom] = useState(false); const [isDragOverComposer, setIsDragOverComposer] = useState(false); const [expandedImage, setExpandedImage] = useState<ExpandedImagePreview | null>(null); const [optimisticUserMessages, setOptimisticUserMessages] = useState<ChatMessage[]>([]); const optimisticUserMessagesRef = useRef(optimisticUserMessages); optimisticUserMessagesRef.current = optimisticUserMessages; const composerTerminalContextsRef = useRef<TerminalContextDraft[]>(composerTerminalContexts); const [localDraftErrorsByThreadId, setLocalDraftErrorsByThreadId] = useState< Record<ThreadId, string | null> >({}); const [isConnecting, _setIsConnecting] = useState(false); const [isRevertingCheckpoint, setIsRevertingCheckpoint] = useState(false); const [respondingRequestIds, setRespondingRequestIds] = useState<ApprovalRequestId[]>([]); const [respondingUserInputRequestIds, setRespondingUserInputRequestIds] = useState< ApprovalRequestId[]