3118 lines · 110.0 KB · TypeScript
RawDownload
import {  type ApprovalRequestId,  DEFAULT_MODEL_BY_PROVIDER,  type ClaudeCodeEffort,  type EnvironmentId,  type MessageId,  type ModelSelection,  type ProjectScript,  type ProviderKind,  type ProjectId,  type ProviderApprovalDecision,  type ServerProvider,  type ScopedThreadRef,  type ThreadId,  type TurnId,  type KeybindingCommand,  OrchestrationThreadActivity,  ProviderInteractionMode,  RuntimeMode,  TerminalOpenInput,} from "@t3tools/contracts";import { scopedThreadKey, scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime";import { applyClaudePromptEffortPrefix } 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, 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 {  isWorkspaceCommandId,  useWorkspaceCommandExecutor,} from "../hooks/useWorkspaceCommandExecutor";import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";import { useCommandPaletteStore } from "../commandPaletteStore";import { buildTemporaryWorktreeBranchName } from "@t3tools/shared/git";import { BranchToolbar } from "./BranchToolbar";import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings";import PlanSidebar from "./PlanSidebar";import { ChevronDownIcon } from "lucide-react";import { cn, randomUUID } from "~/lib/utils";import { 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 { deriveLogicalProjectKey } 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,} 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 {  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 { useWorkspaceStore, useWorkspaceThreadTerminalOpen } from "../workspace/store"; 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);  if (params.effort && caps.promptInjectedEffortLevels.includes(params.effort)) {    return applyClaudePromptEffortPrefix(params.text, params.effort as ClaudeCodeEffort | null);  }  return params.text;}const SCRIPT_TERMINAL_COLS = 120;const SCRIPT_TERMINAL_ROWS = 30; type ChatViewProps =  | {      bindSharedComposerHandle?: boolean;      environmentId: EnvironmentId;      threadId: ThreadId;      onDiffPanelOpen?: () => void;      reserveTitleBarControlInset?: boolean;      routeKind: "server";      draftId?: never;    }  | {      bindSharedComposerHandle?: boolean;      environmentId: EnvironmentId;      threadId: ThreadId;      onDiffPanelOpen?: () => void;      reserveTitleBarControlInset?: boolean;      routeKind: "draft";      draftId: DraftId;    }; 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(props: ChatViewProps) {  const {    bindSharedComposerHandle = true,    environmentId,    threadId,    routeKind,    onDiffPanelOpen,    reserveTitleBarControlInset = true,  } = props;  const draftId = routeKind === "draft" ? props.draftId : null;  const routeThreadRef = useMemo(    () => scopeThreadRef(environmentId, threadId),    [environmentId, threadId],  );  const routeThreadKey = useMemo(() => scopedThreadKey(routeThreadRef), [routeThreadRef]);  const composerDraftTarget: ScopedThreadRef | DraftId =    routeKind === "server" ? routeThreadRef : props.draftId;  const serverThread = useStore(    useMemo(      () => createThreadSelectorByRef(routeKind === "server" ? routeThreadRef : null),      [routeKind, routeThreadRef],    ),  );  const setStoreThreadError = useStore((store) => store.setError);  const markThreadVisited = useUiStateStore((store) => store.markThreadVisited);  const activeThreadLastVisitedAt = useUiStateStore((store) =>    routeKind === "server" ? store.threadLastVisitedAtById[routeThreadKey] : undefined,  );  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();  // Granular store selectors — avoid subscribing to prompt changes.  const composerRuntimeMode = useComposerDraftStore(    (store) => store.getComposerDraft(composerDraftTarget)?.runtimeMode ?? null,  );  const composerInteractionMode = useComposerDraftStore(    (store) => store.getComposerDraft(composerDraftTarget)?.interactionMode ?? null,  );  const composerActiveProvider = useComposerDraftStore(    (store) => store.getComposerDraft(composerDraftTarget)?.activeProvider ?? null,  );  const setComposerDraftPrompt = useComposerDraftStore((store) => store.setPrompt);  const addComposerDraftImages = useComposerDraftStore((store) => store.addImages);  const setComposerDraftTerminalContexts = useComposerDraftStore(    (store) => store.setTerminalContexts,  );  const setComposerDraftModelSelection = useComposerDraftStore((store) => store.setModelSelection);  const setComposerDraftRuntimeMode = useComposerDraftStore((store) => store.setRuntimeMode);  const setComposerDraftInteractionMode = useComposerDraftStore(    (store) => store.setInteractionMode,  );  const clearComposerDraftContent = useComposerDraftStore((store) => store.clearComposerContent);  const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext);  const getDraftSessionByLogicalProjectKey = useComposerDraftStore(    (store) => store.getDraftSessionByLogicalProjectKey,  );  const getDraftSession = useComposerDraftStore((store) => store.getDraftSession);  const setLogicalProjectDraftThreadId = useComposerDraftStore(    (store) => store.setLogicalProjectDraftThreadId,  );  const draftThread = useComposerDraftStore((store) =>    routeKind === "server"      ? store.getDraftSessionByRef(routeThreadRef)      : draftId        ? store.getDraftSession(draftId)        : null,  );  const promptRef = useRef("");  const composerImagesRef = useRef<ComposerImageAttachment[]>([]);  const composerTerminalContextsRef = useRef<TerminalContextDraft[]>([]);  const localComposerRef = useRef<ChatComposerHandle | null>(null);  const sharedComposerRef = useComposerHandleContext();  const composerRef = bindSharedComposerHandle    ? (sharedComposerRef ?? localComposerRef)    : localComposerRef;  const [showScrollToBottom, setShowScrollToBottom] = useState(false);  const [expandedImage, setExpandedImage] = useState<ExpandedImagePreview | null>(null);  const [optimisticUserMessages, setOptimisticUserMessages] = useState<ChatMessage[]>([]);  const optimisticUserMessagesRef = useRef(optimisticUserMessages);  optimisticUserMessagesRef.current = optimisticUserMessages;  const [localDraftErrorsByDraftId, setLocalDraftErrorsByDraftId] = useState<    Record<string, 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>>  >({});  const [pendingUserInputQuestionIndexByRequestId, setPendingUserInputQuestionIndexByRequestId] =    useState<Record<string, number>>({});  const [planSidebarOpen, setPlanSidebarOpen] = useState(false);  // Tracks whether the user explicitly dismissed the sidebar for the active turn.  const planSidebarDismissedForTurnRef = useRef<string | null>(null);  // When set, the thread-change reset effect will open the sidebar instead of closing it.  // Used by "Implement in a new thread" to carry the sidebar-open intent across navigation.  const planSidebarOpenOnNextThreadRef = useRef(false);

Lines 1–500 of 3118.