Julius MarmingeT3 Code Mobile [WIP] (#2013)b3e8c035 months agoHistory
2401 lines · 90.6 KB · TypeScript
RawDownload
import type {  ApprovalRequestId,  EnvironmentId,  ModelSelection,  ProviderApprovalDecision,  ProviderInteractionMode,  ResolvedKeybindingsConfig,  RuntimeMode,  ScopedThreadRef,  ServerProvider,  ThreadId,  TurnId,} from "@t3tools/contracts";import {  ProviderDriverKind,  ProviderInstanceId,  PROVIDER_SEND_TURN_MAX_ATTACHMENTS,  PROVIDER_SEND_TURN_MAX_IMAGE_BYTES,} from "@t3tools/contracts";import { createModelSelection, normalizeModelSlug } from "@t3tools/shared/model";import {  memo,  useCallback,  useEffect,  useImperativeHandle,  useLayoutEffect,  useMemo,  useRef,  useState,} from "react";import {  clampCollapsedComposerCursor,  type ComposerTrigger,  collapseExpandedComposerCursor,  detectComposerTrigger,  expandCollapsedComposerCursor,  replaceTextRange,} from "../../composer-logic";import { deriveComposerSendState, readFileAsDataUrl } from "../ChatView.logic";import {  type ComposerImageAttachment,  type DraftId,  type PersistedComposerImageAttachment,  useComposerDraftStore,  useComposerThreadDraft,  useEffectiveComposerModelState,} from "../../composerDraftStore";import {  type TerminalContextDraft,  type TerminalContextSelection,  insertInlineTerminalContextPlaceholder,  removeInlineTerminalContextPlaceholder,} from "../../lib/terminalContext";import { useComposerPathSearch } from "../../lib/composerPathSearchState";import {  shouldUseCompactComposerPrimaryActions,  shouldUseCompactComposerFooter,} from "../composerFooterLayout";import { type ComposerPromptEditorHandle, ComposerPromptEditor } from "../ComposerPromptEditor";import { ProviderModelPicker } from "./ProviderModelPicker";import { type ComposerCommandItem, ComposerCommandMenu } from "./ComposerCommandMenu";import { ComposerPendingApprovalActions } from "./ComposerPendingApprovalActions";import { CompactComposerControlsMenu } from "./CompactComposerControlsMenu";import { ComposerPrimaryActions } from "./ComposerPrimaryActions";import { ComposerPendingApprovalPanel } from "./ComposerPendingApprovalPanel";import { ComposerPendingUserInputPanel } from "./ComposerPendingUserInputPanel";import { ComposerPlanFollowUpBanner } from "./ComposerPlanFollowUpBanner";import { resolveComposerMenuActiveItemId } from "./composerMenuHighlight";import { searchSlashCommandItems } from "./composerSlashCommandSearch";import {  getComposerProviderState,  renderProviderTraitsMenuContent,  renderProviderTraitsPicker,} from "./composerProviderState";import { ContextWindowMeter } from "./ContextWindowMeter";import { buildExpandedImagePreview, type ExpandedImagePreview } from "./ExpandedImagePreview";import { basenameOfPath } from "../../vscode-icons";import { cn, randomUUID } from "~/lib/utils";import { Separator } from "../ui/separator";import { Button } from "../ui/button";import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";import { toastManager } from "../ui/toast";import {  BotIcon,  CircleAlertIcon,  ListTodoIcon,  type LucideIcon,  LockIcon,  LockOpenIcon,  PenLineIcon,  XIcon,} from "lucide-react";import { proposedPlanTitle } from "../../proposedPlan";import { getProviderInteractionModeToggle } from "../../providerModels";import {  deriveProviderInstanceEntries,  resolveProviderDriverKindForInstanceSelection,  sortProviderInstanceEntries,  type ProviderInstanceEntry,} from "../../providerInstances";import { type AppModelOption, getAppModelOptionsForInstance } from "../../modelSelection";import type { UnifiedSettings } from "@t3tools/contracts/settings";import type { SessionPhase, Thread } from "../../types";import type { PendingUserInputDraftAnswer } from "../../pendingUserInput";import type { PendingApproval, PendingUserInput } from "../../session-logic";import { deriveLatestContextWindowSnapshot } from "../../lib/contextWindow";import { formatProviderSkillDisplayName } from "../../providerSkillPresentation";import { searchProviderSkills } from "../../providerSkillSearch";import { useMediaQuery } from "../../hooks/useMediaQuery"; const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`; const runtimeModeConfig: Record<  RuntimeMode,  { label: string; description: string; icon: LucideIcon }> = {  "approval-required": {    label: "Supervised",    description: "Ask before commands and file changes.",    icon: LockIcon,  },  "auto-accept-edits": {    label: "Auto-accept edits",    description: "Auto-approve edits, ask before other actions.",    icon: PenLineIcon,  },  "full-access": {    label: "Full access",    description: "Allow commands and edits without prompts.",    icon: LockOpenIcon,  },}; const runtimeModeOptions = Object.keys(runtimeModeConfig) as RuntimeMode[];const COMPOSER_FLOATING_LAYER_SELECTOR = [  '[data-slot="popover-popup"]',  '[data-slot="menu-popup"]',  '[data-slot="select-popup"]',  '[data-slot="combobox-popup"]',  '[data-slot="autocomplete-popup"]',].join(","); 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]); function isInsideComposerFloatingLayer(element: Element): boolean {  return element.closest(COMPOSER_FLOATING_LAYER_SELECTOR) !== null;} const ComposerFooterModeControls = memo(function ComposerFooterModeControls(props: {  showInteractionModeToggle: boolean;  interactionMode: ProviderInteractionMode;  runtimeMode: RuntimeMode;  showPlanToggle: boolean;  planSidebarLabel: string;  planSidebarOpen: boolean;  onToggleInteractionMode: () => void;  onRuntimeModeChange: (mode: RuntimeMode) => void;  onTogglePlanSidebar: () => void;}) {  const runtimeModeOption = runtimeModeConfig[props.runtimeMode];  const RuntimeModeIcon = runtimeModeOption.icon;   return (    <>      <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" />       {props.showInteractionModeToggle ? (        <>          <Button            variant="ghost"            className="shrink-0 whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80 sm:px-3"            size="sm"            type="button"            onClick={props.onToggleInteractionMode}            title={              props.interactionMode === "plan"                ? "Plan mode — click to return to normal build mode"                : "Default mode — click to enter plan mode"            }          >            <BotIcon />            <span className="sr-only sm:not-sr-only">              {props.interactionMode === "plan" ? "Plan" : "Build"}            </span>          </Button>           <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" />        </>      ) : null}       <Select        value={props.runtimeMode}        onValueChange={(value) => props.onRuntimeModeChange(value!)}      >        <SelectTrigger          variant="ghost"          size="sm"          className="font-medium"          aria-label="Runtime mode"          title={runtimeModeOption.description}        >          <RuntimeModeIcon className="size-4" />          <SelectValue>{runtimeModeOption.label}</SelectValue>        </SelectTrigger>        <SelectPopup alignItemWithTrigger={false}>          {runtimeModeOptions.map((mode) => {            const option = runtimeModeConfig[mode];            const OptionIcon = option.icon;            return (              <SelectItem key={mode} value={mode} className="min-w-64 py-2">                <div className="grid min-w-0 gap-0.5">                  <span className="inline-flex items-center gap-1.5 font-medium text-foreground">                    <OptionIcon className="size-3.5 shrink-0 text-muted-foreground" />                    {option.label}                  </span>                  <span className="text-muted-foreground text-xs leading-4">                    {option.description}                  </span>                </div>              </SelectItem>            );          })}        </SelectPopup>      </Select>       {props.showPlanToggle ? (        <>          <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" />          <Button            variant="ghost"            className={cn(              "shrink-0 whitespace-nowrap px-2 sm:px-3",              props.planSidebarOpen                ? "text-blue-400 hover:text-blue-300"                : "text-muted-foreground/70 hover:text-foreground/80",            )}            size="sm"            type="button"            onClick={props.onTogglePlanSidebar}            title={              props.planSidebarOpen                ? `Hide ${props.planSidebarLabel.toLowerCase()} sidebar`                : `Show ${props.planSidebarLabel.toLowerCase()} sidebar`            }          >            <ListTodoIcon />            <span className="sr-only sm:not-sr-only">{props.planSidebarLabel}</span>          </Button>        </>      ) : null}    </>  );}); const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions(props: {  compact: boolean;  activeContextWindow: ReturnType<typeof deriveLatestContextWindowSnapshot>;  isPreparingWorktree: boolean;  pendingAction: {    questionIndex: number;    isLastQuestion: boolean;    canAdvance: boolean;    isResponding: boolean;    isComplete: boolean;  } | null;  isRunning: boolean;  showPlanFollowUpPrompt: boolean;  promptHasText: boolean;  isSendBusy: boolean;  isConnecting: boolean;  isEnvironmentUnavailable: boolean;  hasSendableContent: boolean;  preserveComposerFocusOnPointerDown?: boolean;  onPreviousPendingQuestion: () => void;  onInterrupt: () => void;  onImplementPlanInNewThread: () => void;}) {  return (    <>      {props.activeContextWindow ? <ContextWindowMeter usage={props.activeContextWindow} /> : null}      {props.isPreparingWorktree ? (        <span className="text-muted-foreground/70 text-xs">Preparing worktree...</span>      ) : null}      <ComposerPrimaryActions        compact={props.compact}        pendingAction={props.pendingAction}        isRunning={props.isRunning}        showPlanFollowUpPrompt={props.showPlanFollowUpPrompt}        promptHasText={props.promptHasText}        isSendBusy={props.isSendBusy}        isConnecting={props.isConnecting}        isEnvironmentUnavailable={props.isEnvironmentUnavailable}        isPreparingWorktree={props.isPreparingWorktree}        hasSendableContent={props.hasSendableContent}        preserveComposerFocusOnPointerDown={props.preserveComposerFocusOnPointerDown ?? false}        onPreviousPendingQuestion={props.onPreviousPendingQuestion}        onInterrupt={props.onInterrupt}        onImplementPlanInNewThread={props.onImplementPlanInNewThread}      />    </>  );}); // --------------------------------------------------------------------------// Handle exposed to ChatView// -------------------------------------------------------------------------- export interface ChatComposerHandle {  focusAtEnd: () => void;  focusAt: (cursor: number) => void;  openModelPicker: () => void;  toggleModelPicker: () => void;  isModelPickerOpen: () => boolean;  readSnapshot: () => {    value: string;    cursor: number;    expandedCursor: number;    terminalContextIds: string[];  };  /** Reset composer cursor/trigger/highlight after external prompt mutations (e.g. onSend). */  resetCursorState: (options?: {    cursor?: number;    prompt?: string;    detectTrigger?: boolean;  }) => void;  /** Insert a terminal context from the terminal drawer. */  addTerminalContext: (selection: TerminalContextSelection) => void;  /** Get the current prompt/effort/model state for use in send. */  getSendContext: () => {    prompt: string;    images: ComposerImageAttachment[];    terminalContexts: TerminalContextDraft[];    selectedPromptEffort: string | null;    selectedModelOptionsForDispatch: unknown;    selectedModelSelection: ModelSelection;    selectedProvider: ProviderDriverKind;    selectedModel: string;    selectedProviderModels: ReadonlyArray<ServerProvider["models"][number]>;  };} // --------------------------------------------------------------------------// Props// -------------------------------------------------------------------------- export interface ChatComposerProps {  composerDraftTarget: ScopedThreadRef | DraftId;  environmentId: EnvironmentId;  routeKind: "server" | "draft";  routeThreadRef: ScopedThreadRef;  draftId: DraftId | null;   // Thread context  activeThreadId: ThreadId | null;  activeThreadEnvironmentId: EnvironmentId | undefined;  activeThread: Thread | undefined;  isServerThread: boolean;  isLocalDraftThread: boolean;   // Session phase  phase: SessionPhase;  isConnecting: boolean;  isSendBusy: boolean;  isPreparingWorktree: boolean;  environmentUnavailable: {    readonly label: string;    readonly connectionState: "connecting" | "disconnected" | "error";  } | null;   // Pending approvals / inputs  activePendingApproval: PendingApproval | null;  pendingApprovals: PendingApproval[];  pendingUserInputs: PendingUserInput[];  activePendingProgress: {    questionIndex: number;    isLastQuestion: boolean;    canAdvance: boolean;    customAnswer: string;    activeQuestion: { id: string; multiSelect?: boolean | undefined } | null;  } | null;  activePendingResolvedAnswers: Record<string, unknown> | null;  activePendingIsResponding: boolean;  activePendingDraftAnswers: Record<string, PendingUserInputDraftAnswer>;  activePendingQuestionIndex: number;  respondingRequestIds: ApprovalRequestId[];   // Plan  showPlanFollowUpPrompt: boolean;  activeProposedPlan: Thread["proposedPlans"][number] | null;  activePlan: { turnId?: TurnId } | null;  sidebarProposedPlan: { turnId?: TurnId } | null;  planSidebarLabel: string;  planSidebarOpen: boolean;   // Mode  runtimeMode: RuntimeMode;  interactionMode: ProviderInteractionMode;   // Provider / model  lockedProvider: ProviderDriverKind | null;  providerStatuses: ServerProvider[];  activeProjectDefaultModelSelection: ModelSelection | null | undefined;  activeThreadModelSelection: ModelSelection | null | undefined;   // Context window  activeThreadActivities: Thread["activities"] | undefined;   // Misc  resolvedTheme: "light" | "dark";  settings: UnifiedSettings;  keybindings: ResolvedKeybindingsConfig;  terminalOpen: boolean;  gitCwd: string | null;   // Refs the parent needs kept in sync  promptRef: React.RefObject<string>;  composerImagesRef: React.RefObject<ComposerImageAttachment[]>;  composerTerminalContextsRef: React.RefObject<TerminalContextDraft[]>;  composerRef: React.RefObject<ChatComposerHandle | null>;   // Scroll  shouldAutoScrollRef: React.RefObject<boolean>;  scheduleStickToBottom: () => void;   // Callbacks  onSend: (e?: { preventDefault: () => void }) => void;  onInterrupt: () => void;  onImplementPlanInNewThread: () => void;  onRespondToApproval: (    requestId: ApprovalRequestId,    decision: ProviderApprovalDecision,  ) => Promise<void>;  onSelectActivePendingUserInputOption: (questionId: string, optionLabel: string) => void;  onAdvanceActivePendingUserInput: () => void;  onPreviousActivePendingUserInputQuestion: () => void;  onChangeActivePendingUserInputCustomAnswer: (    questionId: string,    value: string,    nextCursor: number,    expandedCursor: number,    cursorAdjacentToMention: boolean,  ) => void;   onProviderModelSelect: (instanceId: ProviderInstanceId, model: string) => void;  toggleInteractionMode: () => void;  handleRuntimeModeChange: (mode: RuntimeMode) => void;  handleInteractionModeChange: (mode: ProviderInteractionMode) => void;  togglePlanSidebar: () => void;   focusComposer: () => void;  scheduleComposerFocus: () => void;  setThreadError: (threadId: ThreadId | null, error: string | null) => void;  onExpandImage: (preview: ExpandedImagePreview) => void;} // --------------------------------------------------------------------------// Component// -------------------------------------------------------------------------- export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) {  const {    composerDraftTarget,    environmentId,    routeKind,    routeThreadRef,    draftId,    activeThreadId,    activeThreadEnvironmentId: _activeThreadEnvironmentId,    activeThread,    isServerThread: _isServerThread,    isLocalDraftThread: _isLocalDraftThread,    phase,    isConnecting,

Lines 1–500 of 2401.