6152 lines · 220.6 KB · TypeScript
RawDownload
import {  type ApprovalRequestId,  DEFAULT_MODEL_BY_PROVIDER,  EDITORS,  type EditorId,  type KeybindingCommand,  type CodexReasoningEffort,  type MessageId,  type ProjectId,  type ProjectEntry,  type ProjectScript,  type ModelSlug,  PROVIDER_SEND_TURN_MAX_ATTACHMENTS,  PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,  type ResolvedKeybindingsConfig,  type ProviderApprovalDecision,  type ServerProviderStatus,  type ProviderKind,  type ThreadId,  type TurnId,  OrchestrationThreadActivity,  RuntimeMode,  ProviderInteractionMode,} from "@t3tools/contracts";import {  getDefaultModel,  getDefaultReasoningEffort,  getReasoningEffortOptions,  normalizeModelSlug,  resolveModelSlugForProvider,} from "@t3tools/shared/model";import {  memo,  useCallback,  useEffect,  useLayoutEffect,  useMemo,  useRef,  useState,  useId,} from "react";import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";import { useDebouncedValue } from "@tanstack/react-pacer";import { useNavigate, useSearch } from "@tanstack/react-router";import {  measureElement as measureVirtualElement,  type VirtualItem,  useVirtualizer,} from "@tanstack/react-virtual";import { gitBranchesQueryOptions, gitCreateWorktreeMutationOptions } from "~/lib/gitReactQuery";import { projectSearchEntriesQueryOptions } from "~/lib/projectReactQuery";import { serverConfigQueryOptions, serverQueryKeys } from "~/lib/serverReactQuery"; import { isElectron } from "../env";import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch";import {  type ComposerSlashCommand,  type ComposerTrigger,  type ComposerTriggerKind,  detectComposerTrigger,  expandCollapsedComposerCursor,  parseStandaloneComposerSlashCommand,  replaceTextRange,} from "../composer-logic";import {  derivePendingApprovals,  derivePendingUserInputs,  derivePhase,  deriveTimelineEntries,  deriveActiveWorkStartedAt,  deriveActivePlanState,  findLatestProposedPlan,  type PendingApproval,  type PendingUserInput,  type ProviderPickerKind,  PROVIDER_OPTIONS,  deriveWorkLogEntries,  hasToolActivityForTurn,  isLatestTurnSettled,  formatElapsed,  formatTimestamp,} from "../session-logic";import { AUTO_SCROLL_BOTTOM_THRESHOLD_PX, isScrollContainerNearBottom } from "../chat-scroll";import {  buildPendingUserInputAnswers,  derivePendingUserInputProgress,  setPendingUserInputCustomAnswer,  type PendingUserInputDraftAnswer,} from "../pendingUserInput";import { useStore } from "../store";import {  buildCollapsedProposedPlanPreviewMarkdown,  buildPlanImplementationThreadTitle,  buildPlanImplementationPrompt,  buildProposedPlanMarkdownFilename,  downloadPlanAsTextFile,  normalizePlanMarkdownForExport,  proposedPlanTitle,  resolvePlanFollowUpSubmission,  stripDisplayedPlanMarkdown,} from "../proposedPlan";import { truncateTitle } from "../truncateTitle";import {  DEFAULT_INTERACTION_MODE,  DEFAULT_RUNTIME_MODE,  DEFAULT_THREAD_TERMINAL_ID,  MAX_THREAD_TERMINAL_COUNT,  type ChatMessage,  type Thread,  type TurnDiffFileChange,  type TurnDiffSummary,} from "../types";import { basenameOfPath, getVscodeIconUrlForEntry } from "../vscode-icons";import { useTheme } from "../hooks/useTheme";import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries";import {  buildTurnDiffTree,  summarizeTurnDiffStats,  type TurnDiffTreeNode,} from "../lib/turnDiffTree";import BranchToolbar from "./BranchToolbar";import GitActionsControl from "./GitActionsControl";import {  isOpenFavoriteEditorShortcut,  resolveShortcutCommand,  shortcutLabelForCommand,} from "../keybindings";import ChatMarkdown from "./ChatMarkdown";import PlanSidebar from "./PlanSidebar";import ThreadTerminalDrawer from "./ThreadTerminalDrawer";import { Alert, AlertAction, AlertDescription, AlertTitle } from "./ui/alert";import {  BotIcon,  ChevronDownIcon,  ChevronLeftIcon,  ChevronRightIcon,  CircleAlertIcon,  FileIcon,  FolderIcon,  DiffIcon,  EllipsisIcon,  FolderClosedIcon,  ListTodoIcon,  LockIcon,  LockOpenIcon,  Undo2Icon,  XIcon,  CopyIcon,  CheckIcon,} from "lucide-react";import { Button } from "./ui/button";import { Input } from "./ui/input";import { Separator } from "./ui/separator";import { Group, GroupSeparator } from "./ui/group";import {  Menu,  MenuGroup,  MenuItem,  MenuPopup,  MenuRadioGroup,  MenuRadioItem,  MenuSeparator as MenuDivider,  MenuSub,  MenuSubPopup,  MenuSubTrigger,  MenuShortcut,  MenuTrigger,} from "./ui/menu";import {  ClaudeAI,  CursorIcon,  Gemini,  Icon,  OpenAI,  OpenCodeIcon,  VisualStudioCode,  Zed,} from "./Icons";import { cn, isMacPlatform, isWindowsPlatform, randomUUID } from "~/lib/utils";import { Badge } from "./ui/badge";import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";import { Command, CommandItem, CommandList } from "./ui/command";import {  Dialog,  DialogDescription,  DialogFooter,  DialogHeader,  DialogPanel,  DialogPopup,  DialogTitle,} from "./ui/dialog";import { toastManager } from "./ui/toast";import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings";import ProjectScriptsControl, { type NewProjectScriptInput } from "./ProjectScriptsControl";import {  commandForProjectScript,  nextProjectScriptId,  projectScriptRuntimeEnv,  projectScriptIdFromCommand,  setupProjectScript,} from "~/projectScripts";import { Toggle } from "./ui/toggle";import { SidebarTrigger } from "./ui/sidebar";import { newCommandId, newMessageId, newThreadId } from "~/lib/utils";import { readNativeApi } from "~/nativeApi";import { getAppModelOptions, resolveAppModelSelection, useAppSettings } from "../appSettings";import {  type ComposerImageAttachment,  type DraftThreadEnvMode,  type DraftThreadState,  type PersistedComposerImageAttachment,  useComposerDraftStore,  useComposerThreadDraft,} from "../composerDraftStore";import { shouldUseCompactComposerFooter } from "./composerFooterLayout";import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore";import { clamp } from "effect/Number";import { ComposerPromptEditor, type ComposerPromptEditorHandle } from "./ComposerPromptEditor";import { PullRequestThreadDialog } from "./PullRequestThreadDialog";import { estimateTimelineMessageHeight } from "./timelineHeight"; function formatMessageMeta(createdAt: string, duration: string | null): string {  if (!duration) return formatTimestamp(createdAt);  return `${formatTimestamp(createdAt)} • ${duration}`;} function formatWorkingTimer(startIso: string, endIso: string): string | null {  const startedAtMs = Date.parse(startIso);  const endedAtMs = Date.parse(endIso);  if (!Number.isFinite(startedAtMs) || !Number.isFinite(endedAtMs)) {    return null;  }   const elapsedSeconds = Math.max(0, Math.floor((endedAtMs - startedAtMs) / 1000));  if (elapsedSeconds < 60) {    return `${elapsedSeconds}s`;  }   const hours = Math.floor(elapsedSeconds / 3600);  const minutes = Math.floor((elapsedSeconds % 3600) / 60);  const seconds = elapsedSeconds % 60;   if (hours > 0) {    return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;  }   return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;} const LAST_EDITOR_KEY = "t3code:last-editor";const LAST_INVOKED_SCRIPT_BY_PROJECT_KEY = "t3code:last-invoked-script-by-project";const MAX_VISIBLE_WORK_LOG_ENTRIES = 6;const ALWAYS_UNVIRTUALIZED_TAIL_ROWS = 8;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_KEYBINDINGS: ResolvedKeybindingsConfig = [];const EMPTY_PROJECT_ENTRIES: ProjectEntry[] = [];const EMPTY_AVAILABLE_EDITORS: EditorId[] = [];const EMPTY_PROVIDER_STATUSES: ServerProviderStatus[] = [];const EMPTY_PENDING_USER_INPUT_ANSWERS: Record<string, PendingUserInputDraftAnswer> = {};const COMPOSER_PATH_QUERY_DEBOUNCE_MS = 120;const SCRIPT_TERMINAL_COLS = 120;const SCRIPT_TERMINAL_ROWS = 30;const WORKTREE_BRANCH_PREFIX = "t3code"; function readLastInvokedScriptByProjectFromStorage(): Record<string, string> {  const stored = localStorage.getItem(LAST_INVOKED_SCRIPT_BY_PROJECT_KEY);  if (!stored) return {};   try {    const parsed: unknown = JSON.parse(stored);    if (!parsed || typeof parsed !== "object") return {};    return Object.fromEntries(      Object.entries(parsed).filter(        (entry): entry is [string, string] =>          typeof entry[0] === "string" && typeof entry[1] === "string",      ),    );  } catch {    return {};  }} function workToneClass(tone: "thinking" | "tool" | "info" | "error"): string {  if (tone === "error") return "text-rose-300/50 dark:text-rose-300/50";  if (tone === "tool") return "text-muted-foreground/70";  if (tone === "thinking") return "text-muted-foreground/50";  return "text-muted-foreground/40";} interface ExpandedImageItem {  src: string;  name: string;} interface ExpandedImagePreview {  images: ExpandedImageItem[];  index: number;} function buildExpandedImagePreview(  images: ReadonlyArray<{ id: string; name: string; previewUrl?: string }>,  selectedImageId: string,): ExpandedImagePreview | null {  const previewableImages = images.flatMap((image) =>    image.previewUrl ? [{ id: image.id, src: image.previewUrl, name: image.name }] : [],  );  if (previewableImages.length === 0) {    return null;  }  const selectedIndex = previewableImages.findIndex((image) => image.id === selectedImageId);  if (selectedIndex < 0) {    return null;  }  return {    images: previewableImages.map((image) => ({ src: image.src, name: image.name })),    index: selectedIndex,  };} function buildLocalDraftThread(  threadId: ThreadId,  draftThread: DraftThreadState,  fallbackModel: string,  error: string | null,): Thread {  return {    id: threadId,    codexThreadId: null,    projectId: draftThread.projectId,    title: "New thread",    model: fallbackModel,    runtimeMode: draftThread.runtimeMode,    interactionMode: draftThread.interactionMode,    session: null,    messages: [],    error,    createdAt: draftThread.createdAt,    latestTurn: null,    lastVisitedAt: draftThread.createdAt,    branch: draftThread.branch,    worktreePath: draftThread.worktreePath,    turnDiffSummaries: [],    activities: [],    proposedPlans: [],  };} function revokeBlobPreviewUrl(previewUrl: string | undefined): void {  if (!previewUrl || typeof URL === "undefined" || !previewUrl.startsWith("blob:")) {    return;  }  URL.revokeObjectURL(previewUrl);} function revokeUserMessagePreviewUrls(message: ChatMessage): void {  if (message.role !== "user" || !message.attachments) {    return;  }  for (const attachment of message.attachments) {    if (attachment.type !== "image") {      continue;    }    revokeBlobPreviewUrl(attachment.previewUrl);  }} function collectUserMessageBlobPreviewUrls(message: ChatMessage): string[] {  if (message.role !== "user" || !message.attachments) {    return [];  }  const previewUrls: string[] = [];  for (const attachment of message.attachments) {    if (attachment.type !== "image") continue;    if (!attachment.previewUrl || !attachment.previewUrl.startsWith("blob:")) continue;    previewUrls.push(attachment.previewUrl);  }  return previewUrls;} type ComposerCommandItem =  | {      id: string;      type: "path";      path: string;      pathKind: ProjectEntry["kind"];      label: string;      description: string;    }  | {      id: string;      type: "slash-command";      command: ComposerSlashCommand;      label: string;      description: string;    }  | {      id: string;      type: "model";      provider: ProviderKind;      model: ModelSlug;      label: string;      description: string;    }; type SendPhase = "idle" | "preparing-worktree" | "sending-turn"; interface PullRequestDialogState {  initialReference: string | null;  key: number;} function readFileAsDataUrl(file: File): Promise<string> {  return new Promise((resolve, reject) => {    const reader = new FileReader();    reader.addEventListener("load", () => {      if (typeof reader.result === "string") {        resolve(reader.result);        return;      }      reject(new Error("Could not read image data."));    });    reader.addEventListener("error", () => {      reject(reader.error ?? new Error("Failed to read image."));    });    reader.readAsDataURL(file);  });} function buildTemporaryWorktreeBranchName(): string {  // Keep the 8-hex suffix shape for backend temporary-branch detection.  const token = randomUUID().slice(0, 8).toLowerCase();  return `${WORKTREE_BRANCH_PREFIX}/${token}`;} function cloneComposerImageForRetry(image: ComposerImageAttachment): ComposerImageAttachment {  if (typeof URL === "undefined" || !image.previewUrl.startsWith("blob:")) {    return image;  }  try {    return {      ...image,      previewUrl: URL.createObjectURL(image.file),    };  } catch {    return image;  }} const VscodeEntryIcon = memo(function VscodeEntryIcon(props: {  pathValue: string;  kind: "file" | "directory";  theme: "light" | "dark";  className?: string;}) {  const [failedIconUrl, setFailedIconUrl] = useState<string | null>(null);  const iconUrl = useMemo(    () => getVscodeIconUrlForEntry(props.pathValue, props.kind, props.theme),    [props.kind, props.pathValue, props.theme],  );  const failed = failedIconUrl === iconUrl;   if (failed) {    return props.kind === "directory" ? (      <FolderIcon className={cn("size-4 text-muted-foreground/80", props.className)} />    ) : (      <FileIcon className={cn("size-4 text-muted-foreground/80", props.className)} />    );  }   return (    <img      src={iconUrl}      alt=""      aria-hidden="true"      className={cn("size-4 shrink-0", props.className)}      loading="lazy"      onError={() => setFailedIconUrl(iconUrl)}    />  );}); const ComposerCommandMenuItem = memo(function ComposerCommandMenuItem(props: {  item: ComposerCommandItem;  resolvedTheme: "light" | "dark";  isActive: boolean;  onSelect: (item: ComposerCommandItem) => void;}) {  return (    <CommandItem      value={props.item.id}      className={cn(        "cursor-pointer select-none gap-2",        props.isActive && "bg-accent text-accent-foreground",      )}      onMouseDown={(event) => {        event.preventDefault();

Lines 1–500 of 6152.