2565 lines · 97.3 KB · TypeScript
RawDownload
import type {  ApprovalRequestId,  EnvironmentId,  ModelSelection,  PreviewAnnotationPayload,  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 {  connectionStatusText,  type EnvironmentConnectionPresentation,} from "@t3tools/client-runtime/connection";import { serializeComposerFileLink } from "@t3tools/shared/composerTrigger";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 { type ElementContextDraft } from "../../lib/elementContext";import { ComposerPendingElementContexts } from "./ComposerPendingElementContexts";import { ComposerPendingReviewComments } from "./ComposerPendingReviewComments";import { ComposerPreviewAnnotationCards } from "./ComposerPreviewAnnotationCards";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 {  getComposerPromptInjectionState,  getComposerProviderState,  renderProviderTraitsMenuContent,  renderProviderTraitsPicker,} from "./composerProviderState";import { ContextWindowMeter } from "./ContextWindowMeter";import { buildExpandedImagePreview, type ExpandedImagePreview } from "./ExpandedImagePreview";import { basenameOfPath } from "../../pierre-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,  PencilRulerIcon,  type LucideIcon,  LockIcon,  LockOpenIcon,  PenLineIcon,  XIcon,} from "lucide-react";import { proposedPlanTitle } from "../../proposedPlan";import { getProviderDisplayName, getProviderInteractionModeToggle } from "../../providerModels";import {  applyProviderInstanceSettings,  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,  formatProviderDisplayName,} from "../../lib/contextWindow";import { formatProviderSkillDisplayName } from "../../providerSkillPresentation";import { searchProviderSkills } from "../../providerSkillSearch";import { useMediaQuery } from "../../hooks/useMediaQuery";import type { ReviewCommentContext } from "../../reviewCommentContext"; 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;  const interactionModeTooltip =    props.interactionMode === "plan"      ? "Plan mode — click to return to normal build mode"      : "Default mode — click to enter plan mode";  const planSidebarTooltip = props.planSidebarOpen    ? `Hide ${props.planSidebarLabel.toLowerCase()} sidebar`    : `Show ${props.planSidebarLabel.toLowerCase()} sidebar`;   const interactionModeToggle = props.showInteractionModeToggle ? (    <>      <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" />      <Tooltip>        <TooltipTrigger          render={            <Button              variant="ghost"              className={cn(                "shrink-0 whitespace-nowrap px-2 sm:px-3",                props.interactionMode === "plan"                  ? "bg-blue-500/10 text-blue-400 hover:bg-blue-500/15 hover:text-blue-300"                  : "text-muted-foreground/70 hover:text-foreground/80",              )}              size="sm"              type="button"              onClick={props.onToggleInteractionMode}              aria-label={interactionModeTooltip}            />          }        >          {props.interactionMode === "plan" ? (            <PencilRulerIcon className="text-current opacity-100" />          ) : (            <BotIcon />          )}          <span className="sr-only sm:not-sr-only">            {props.interactionMode === "plan" ? "Plan" : "Build"}          </span>        </TooltipTrigger>        <TooltipPopup side="top">{interactionModeTooltip}</TooltipPopup>      </Tooltip>    </>  ) : null;   return (    <>      <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" />       <Tooltip>        <Select          value={props.runtimeMode}          onValueChange={(value) => props.onRuntimeModeChange(value!)}        >          <TooltipTrigger            render={              <SelectTrigger                variant="ghost"                size="sm"                className="font-medium"                aria-label="Runtime mode"              />            }          >            <RuntimeModeIcon className="size-4" />            <SelectValue>{runtimeModeOption.label}</SelectValue>          </TooltipTrigger>          <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>        <TooltipPopup side="top">{runtimeModeOption.description}</TooltipPopup>      </Tooltip>       {interactionModeToggle}       {props.showPlanToggle ? (        <>          <Separator orientation="vertical" className="mx-0.5 hidden h-4 sm:block" />          <Tooltip>            <TooltipTrigger              render={                <Button                  variant="ghost"                  className={cn(                    "shrink-0 whitespace-nowrap px-2 sm:px-3",                    props.planSidebarOpen                      ? "bg-blue-500/10 text-blue-400 hover:bg-blue-500/15 hover:text-blue-300"                      : "text-muted-foreground/70 hover:text-foreground/80",                  )}                  size="sm"                  type="button"                  onClick={props.onTogglePlanSidebar}                  aria-label={planSidebarTooltip}                />              }            >              <ListTodoIcon                className={props.planSidebarOpen ? "text-current opacity-100" : undefined}              />              <span className="sr-only sm:not-sr-only">{props.planSidebarLabel}</span>            </TooltipTrigger>            <TooltipPopup side="top">{planSidebarTooltip}</TooltipPopup>          </Tooltip>        </>      ) : null}    </>  );}); const ComposerFooterPrimaryActions = memo(function ComposerFooterPrimaryActions(props: {  compact: boolean;  activeContextWindow: ReturnType<typeof deriveLatestContextWindowSnapshot>;  activeThreadProviderDisplayName: string | null;  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}          providerDisplayName={props.activeThreadProviderDisplayName}        />      ) : 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;  insertTextAtEnd: (text: string) => boolean;  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[];    elementContexts: ElementContextDraft[];    previewAnnotations: PreviewAnnotationPayload[];    reviewComments: ReviewCommentContext[];    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 connection: EnvironmentConnectionPresentation;  } | 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

Lines 1–500 of 2565.