4014 lines · 141.9 KB · TypeScript
RawDownload
import {  type ApprovalRequestId,  DEFAULT_MODEL,  defaultInstanceIdForDriver,  type EnvironmentId,  type MessageId,  type ModelSelection,  type ProjectScript,  type ProjectId,  type ProviderApprovalDecision,  ProviderInstanceId,  type ServerProvider,  type ResolvedKeybindingsConfig,  type ScopedThreadRef,  type ThreadId,  type TurnId,  type KeybindingCommand,  OrchestrationThreadActivity,  ProviderInteractionMode,  ProviderDriverKind,  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 { nextTerminalId, resolveTerminalSessionLabel } from "@t3tools/shared/terminalLabels";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 { useVcsStatus } from "~/lib/vcsStatusState";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, TriangleAlertIcon, WifiOffIcon } from "lucide-react";import { cn, randomHex } 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 { resolveAppModelSelectionForInstance } from "../modelSelection";import { isTerminalFocused } from "../lib/terminalFocus";import {  deriveLogicalProjectKeyFromSettings,  selectProjectGroupingSettings,} from "../logicalProject";import {  reconnectSavedEnvironment,  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 { selectThreadTerminalUiState, useTerminalUiStateStore } from "../terminalUiStateStore";import { useKnownTerminalSessions, useThreadRunningTerminalIds } from "../terminalSessionState";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 { ComposerBannerStack, type ComposerBannerStackItem } from "./chat/ComposerBannerStack";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";import { Button } from "./ui/button";import {  buildVersionMismatchDismissalKey,  dismissVersionMismatch,  isVersionMismatchDismissed,  resolveServerConfigVersionMismatch,} from "../versionSkew"; 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_PROVIDER_SKILLS: ServerProvider["skills"] = [];const EMPTY_PENDING_USER_INPUT_ANSWERS: Record<string, PendingUserInputDraftAnswer> = {};type EnvironmentUnavailableState = {  readonly environmentId: EnvironmentId;  readonly label: string;  readonly connectionState: "connecting" | "disconnected" | "error";}; type ThreadPlanCatalogEntry = Pick<Thread, "id" | "proposedPlans">;type ThreadPlanCatalogStoreState = ReturnType<typeof useStore.getState>; interface ThreadPlanCatalogCacheEntry {  shell: object | null;  proposedPlanIds: readonly string[] | undefined;  proposedPlansById: Record<string, Thread["proposedPlans"][number]> | undefined;  entry: ThreadPlanCatalogEntry;} function createThreadPlanCatalogSelector(threadIds: readonly ThreadId[]) {  let previousThreadIds: readonly ThreadId[] = [];  let previousResult: ThreadPlanCatalogEntry[] = [];  let previousEntries = new Map<ThreadId, ThreadPlanCatalogCacheEntry>();   return (state: ThreadPlanCatalogStoreState): ThreadPlanCatalogEntry[] => {    const sameThreadIds =      previousThreadIds.length === threadIds.length &&      previousThreadIds.every((id, index) => id === threadIds[index]);    const nextEntries = new Map<ThreadId, ThreadPlanCatalogCacheEntry>();    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;  };} function deriveThreadPlanCatalogThreadIds(input: {  activeThreadId: ThreadId | null;  sourceProposedPlanThreadId: ThreadId | null;}): readonly ThreadId[] {  const threadIds: ThreadId[] = [];  if (input.activeThreadId) {    threadIds.push(input.activeThreadId);  }  if (    input.sourceProposedPlanThreadId &&    input.sourceProposedPlanThreadId !== input.activeThreadId  ) {    threadIds.push(input.sourceProposedPlanThreadId);  }  return threadIds;} function useThreadPlanCatalog(threadIds: readonly ThreadId[]): ThreadPlanCatalogEntry[] {  const selectThreadPlanCatalog = useMemo(    () => createThreadPlanCatalogSelector(threadIds),    [threadIds],  );  return useStore(selectThreadPlanCatalog);} function formatOutgoingPrompt(params: {  provider: ProviderDriverKind;  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,  };} /** Same terminal ids (order ignored) — avoids reconcile when only server session ordering differs. */function terminalIdListsEqual(left: readonly string[], right: readonly string[]): boolean {  if (left.length !== right.length) {    return false;  }  if (left.length === 0) {    return true;  }  const sortedLeft = left.toSorted((a, b) => a.localeCompare(b));  const sortedRight = right.toSorted((a, b) => a.localeCompare(b));  for (let index = 0; index < sortedLeft.length; index += 1) {    if (sortedLeft[index] !== sortedRight[index]) {      return false;    }  }  return true;} /** * Server knows about fewer sessions than the client, but every server id still exists locally. * Typical right after `terminal.open`: known-session list lags; reconciling would drop the new id * and later re-add it as a separate group (no split layout). */function serverTerminalIdsStrictSubsetOfClient(  serverIds: readonly string[],  clientIds: readonly string[],): boolean {  if (serverIds.length >= clientIds.length || clientIds.length === 0) {    return false;  }  const clientSet = new Set(clientIds);  for (const id of serverIds) {    if (!clientSet.has(id)) {      return false;    }  }  return true;} 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,

Lines 1–500 of 4014.