4385 lines · 159.8 KB · TypeScript
RawDownload
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 { truncate } from "@t3tools/shared/String";import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";import { useDebouncedValue } from "@tanstack/react-pacer";import { useNavigate, useSearch } from "@tanstack/react-router";import { gitBranchesQueryOptions, gitCreateWorktreeMutationOptions } from "~/lib/gitReactQuery";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 { 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 queryClient = useQueryClient();  const createWorktreeMutation = useMutation(gitCreateWorktreeMutationOptions({ queryClient }));  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[]  >([]);  const [pendingUserInputAnswersByRequestId, setPendingUserInputAnswersByRequestId] = useState<    Record<string, Record<string, PendingUserInputDraftAnswer>>

Lines 1–500 of 4385.