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. Load more
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();