| ... | @@ -7,11 +7,8 @@ import { | ... | @@ -7,11 +7,8 @@ import { |
| 7 | type ModelSelection, | 7 | type ModelSelection, |
| 8 | type ProjectScript, | 8 | type ProjectScript, |
| 9 | type ProviderKind, | 9 | type ProviderKind, |
| 10 | type ProjectEntry, | | |
| 11 | type ProjectId, | 10 | type ProjectId, |
| 12 | type ProviderApprovalDecision, | 11 | type ProviderApprovalDecision, |
| 13 | PROVIDER_SEND_TURN_MAX_ATTACHMENTS, | | |
| 14 | PROVIDER_SEND_TURN_MAX_IMAGE_BYTES, | | |
| 15 | type ServerProvider, | 12 | type ServerProvider, |
| 16 | type ScopedThreadRef, | 13 | type ScopedThreadRef, |
| 17 | type ThreadId, | 14 | type ThreadId, |
| ... | @@ -28,29 +25,21 @@ import { | ... | @@ -28,29 +25,21 @@ import { |
| 28 | scopeProjectRef, | 25 | scopeProjectRef, |
| 29 | scopeThreadRef, | 26 | scopeThreadRef, |
| 30 | } from "@t3tools/client-runtime"; | 27 | } from "@t3tools/client-runtime"; |
| 31 | import { applyClaudePromptEffortPrefix, normalizeModelSlug } from "@t3tools/shared/model"; | 28 | import { applyClaudePromptEffortPrefix } from "@t3tools/shared/model"; |
| 32 | import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts"; | 29 | import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts"; |
| 33 | import { truncate } from "@t3tools/shared/String"; | 30 | import { truncate } from "@t3tools/shared/String"; |
| 34 | import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; | 31 | import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; |
| 35 | import { useQuery } from "@tanstack/react-query"; | | |
| 36 | import { useDebouncedValue } from "@tanstack/react-pacer"; | | |
| 37 | import { useNavigate, useSearch } from "@tanstack/react-router"; | 32 | import { useNavigate, useSearch } from "@tanstack/react-router"; |
| 38 | import { useShallow } from "zustand/react/shallow"; | 33 | import { useShallow } from "zustand/react/shallow"; |
| 39 | import { useGitStatus } from "~/lib/gitStatusState"; | 34 | import { useGitStatus } from "~/lib/gitStatusState"; |
| 40 | import { projectSearchEntriesQueryOptions } from "~/lib/projectReactQuery"; | | |
| 41 | import { usePrimaryEnvironmentId } from "../environments/primary"; | 35 | import { usePrimaryEnvironmentId } from "../environments/primary"; |
| 42 | import { readEnvironmentApi } from "../environmentApi"; | 36 | import { readEnvironmentApi } from "../environmentApi"; |
| 43 | import { isElectron } from "../env"; | 37 | import { isElectron } from "../env"; |
| 44 | import { readLocalApi } from "../localApi"; | 38 | import { readLocalApi } from "../localApi"; |
| 45 | import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch"; | 39 | import { parseDiffRouteSearch, stripDiffSearchParams } from "../diffRouteSearch"; |
| 46 | import { | 40 | import { |
| 47 | clampCollapsedComposerCursor, | | |
| 48 | type ComposerTrigger, | | |
| 49 | collapseExpandedComposerCursor, | 41 | collapseExpandedComposerCursor, |
| 50 | detectComposerTrigger, | | |
| 51 | expandCollapsedComposerCursor, | | |
| 52 | parseStandaloneComposerSlashCommand, | 42 | parseStandaloneComposerSlashCommand, |
| 53 | replaceTextRange, | | |
| 54 | } from "../composer-logic"; | 43 | } from "../composer-logic"; |
| 55 | import { | 44 | import { |
| 56 | deriveCompletionDividerBeforeEntryId, | 45 | deriveCompletionDividerBeforeEntryId, |
| ... | @@ -86,7 +75,6 @@ import { useUiStateStore } from "../uiStateStore"; | ... | @@ -86,7 +75,6 @@ import { useUiStateStore } from "../uiStateStore"; |
| 86 | import { | 75 | import { |
| 87 | buildPlanImplementationThreadTitle, | 76 | buildPlanImplementationThreadTitle, |
| 88 | buildPlanImplementationPrompt, | 77 | buildPlanImplementationPrompt, |
| 89 | proposedPlanTitle, | | |
| 90 | resolvePlanFollowUpSubmission, | 78 | resolvePlanFollowUpSubmission, |
| 91 | } from "../proposedPlan"; | 79 | } from "../proposedPlan"; |
| 92 | import { | 80 | import { |
| ... | @@ -99,31 +87,14 @@ import { | ... | @@ -99,31 +87,14 @@ import { |
| 99 | type Thread, | 87 | type Thread, |
| 100 | type TurnDiffSummary, | 88 | type TurnDiffSummary, |
| 101 | } from "../types"; | 89 | } from "../types"; |
| 102 | import { basenameOfPath } from "../vscode-icons"; | | |
| 103 | import { useTheme } from "../hooks/useTheme"; | 90 | import { useTheme } from "../hooks/useTheme"; |
| 104 | import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; | 91 | import { useTurnDiffSummaries } from "../hooks/useTurnDiffSummaries"; |
| 105 | import { BranchToolbar } from "./BranchToolbar"; | 92 | import { BranchToolbar } from "./BranchToolbar"; |
| 106 | import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; | 93 | import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; |
| 107 | import PlanSidebar from "./PlanSidebar"; | 94 | import PlanSidebar from "./PlanSidebar"; |
| 108 | import ThreadTerminalDrawer from "./ThreadTerminalDrawer"; | 95 | import ThreadTerminalDrawer from "./ThreadTerminalDrawer"; |
| 109 | import { | 96 | import { ChevronDownIcon } from "lucide-react"; |
| 110 | BotIcon, | | |
| 111 | ChevronDownIcon, | | |
| 112 | ChevronLeftIcon, | | |
| 113 | ChevronRightIcon, | | |
| 114 | CircleAlertIcon, | | |
| 115 | ListTodoIcon, | | |
| 116 | LockIcon, | | |
| 117 | LockOpenIcon, | | |
| 118 | type LucideIcon, | | |
| 119 | PenLineIcon, | | |
| 120 | XIcon, | | |
| 121 | } from "lucide-react"; | | |
| 122 | import { Button } from "./ui/button"; | | |
| 123 | import { Separator } from "./ui/separator"; | | |
| 124 | import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select"; | | |
| 125 | import { cn, randomUUID } from "~/lib/utils"; | 97 | import { cn, randomUUID } from "~/lib/utils"; |
| 126 | import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; | | |
| 127 | import { toastManager } from "./ui/toast"; | 98 | import { toastManager } from "./ui/toast"; |
| 128 | import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings"; | 99 | import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings"; |
| 129 | import { type NewProjectScriptInput } from "./ProjectScriptsControl"; | 100 | import { type NewProjectScriptInput } from "./ProjectScriptsControl"; |
| ... | @@ -133,11 +104,7 @@ import { | ... | @@ -133,11 +104,7 @@ import { |
| 133 | projectScriptIdFromCommand, | 104 | projectScriptIdFromCommand, |
| 134 | } from "~/projectScripts"; | 105 | } from "~/projectScripts"; |
| 135 | import { newCommandId, newDraftId, newMessageId, newThreadId } from "~/lib/utils"; | 106 | import { newCommandId, newDraftId, newMessageId, newThreadId } from "~/lib/utils"; |
| 136 | import { | 107 | import { getProviderModelCapabilities, resolveSelectableProvider } from "../providerModels"; |
| 137 | getProviderModelCapabilities, | | |
| 138 | getProviderModels, | | |
| 139 | resolveSelectableProvider, | | |
| 140 | } from "../providerModels"; | | |
| 141 | import { useSettings } from "../hooks/useSettings"; | 108 | import { useSettings } from "../hooks/useSettings"; |
| 142 | import { resolveAppModelSelection } from "../modelSelection"; | 109 | import { resolveAppModelSelection } from "../modelSelection"; |
| 143 | import { isTerminalFocused } from "../lib/terminalFocus"; | 110 | import { isTerminalFocused } from "../lib/terminalFocus"; |
| ... | @@ -150,49 +117,24 @@ import { buildDraftThreadRouteParams } from "../threadRoutes"; | ... | @@ -150,49 +117,24 @@ import { buildDraftThreadRouteParams } from "../threadRoutes"; |
| 150 | import { | 117 | import { |
| 151 | type ComposerImageAttachment, | 118 | type ComposerImageAttachment, |
| 152 | type DraftThreadEnvMode, | 119 | type DraftThreadEnvMode, |
| 153 | type PersistedComposerImageAttachment, | | |
| 154 | useComposerDraftStore, | 120 | useComposerDraftStore, |
| 155 | useEffectiveComposerModelState, | | |
| 156 | useComposerThreadDraft, | | |
| 157 | type DraftId, | 121 | type DraftId, |
| 158 | } from "../composerDraftStore"; | 122 | } from "../composerDraftStore"; |
| 159 | import { | 123 | import { |
| 160 | appendTerminalContextsToPrompt, | 124 | appendTerminalContextsToPrompt, |
| 161 | formatTerminalContextLabel, | 125 | formatTerminalContextLabel, |
| 162 | insertInlineTerminalContextPlaceholder, | | |
| 163 | removeInlineTerminalContextPlaceholder, | | |
| 164 | type TerminalContextDraft, | 126 | type TerminalContextDraft, |
| 165 | type TerminalContextSelection, | 127 | type TerminalContextSelection, |
| 166 | } from "../lib/terminalContext"; | 128 | } from "../lib/terminalContext"; |
| 167 | import { deriveLatestContextWindowSnapshot } from "../lib/contextWindow"; | | |
| 168 | import { | | |
| 169 | resolveComposerFooterContentWidth, | | |
| 170 | shouldForceCompactComposerFooterForFit, | | |
| 171 | shouldUseCompactComposerPrimaryActions, | | |
| 172 | shouldUseCompactComposerFooter, | | |
| 173 | } from "./composerFooterLayout"; | | |
| 174 | import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore"; | 129 | import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore"; |
| 175 | import { ComposerPromptEditor, type ComposerPromptEditorHandle } from "./ComposerPromptEditor"; | 130 | import { ChatComposer, type ChatComposerHandle } from "./chat/ChatComposer"; |
| | 131 | import { ExpandedImageDialog } from "./chat/ExpandedImageDialog"; |
| 176 | import { PullRequestThreadDialog } from "./PullRequestThreadDialog"; | 132 | import { PullRequestThreadDialog } from "./PullRequestThreadDialog"; |
| 177 | import { MessagesTimeline } from "./chat/MessagesTimeline"; | 133 | import { MessagesTimeline } from "./chat/MessagesTimeline"; |
| 178 | import { ChatHeader } from "./chat/ChatHeader"; | 134 | import { ChatHeader } from "./chat/ChatHeader"; |
| 179 | import { ContextWindowMeter } from "./chat/ContextWindowMeter"; | 135 | import { type ExpandedImagePreview } from "./chat/ExpandedImagePreview"; |
| 180 | import { buildExpandedImagePreview, ExpandedImagePreview } from "./chat/ExpandedImagePreview"; | | |
| 181 | import { NoActiveThreadState } from "./NoActiveThreadState"; | 136 | import { NoActiveThreadState } from "./NoActiveThreadState"; |
| 182 | import { AVAILABLE_PROVIDER_OPTIONS, ProviderModelPicker } from "./chat/ProviderModelPicker"; | | |
| 183 | import { ComposerCommandItem, ComposerCommandMenu } from "./chat/ComposerCommandMenu"; | | |
| 184 | import { ComposerPendingApprovalActions } from "./chat/ComposerPendingApprovalActions"; | | |
| 185 | import { CompactComposerControlsMenu } from "./chat/CompactComposerControlsMenu"; | | |
| 186 | import { ComposerPrimaryActions } from "./chat/ComposerPrimaryActions"; | | |
| 187 | import { ComposerPendingApprovalPanel } from "./chat/ComposerPendingApprovalPanel"; | | |
| 188 | import { ComposerPendingUserInputPanel } from "./chat/ComposerPendingUserInputPanel"; | | |
| 189 | import { ComposerPlanFollowUpBanner } from "./chat/ComposerPlanFollowUpBanner"; | | |
| 190 | import { resolveEffectiveEnvMode, resolveEnvironmentOptionLabel } from "./BranchToolbar.logic"; | 137 | import { resolveEffectiveEnvMode, resolveEnvironmentOptionLabel } from "./BranchToolbar.logic"; |
| 191 | import { | | |
| 192 | getComposerProviderState, | | |
| 193 | renderProviderTraitsMenuContent, | | |
| 194 | renderProviderTraitsPicker, | | |
| 195 | } from "./chat/composerProviderRegistry"; | | |
| 196 | import { ProviderStatusBanner } from "./chat/ProviderStatusBanner"; | 138 | import { ProviderStatusBanner } from "./chat/ProviderStatusBanner"; |
| 197 | import { ThreadErrorBanner } from "./chat/ThreadErrorBanner"; | 139 | import { ThreadErrorBanner } from "./chat/ThreadErrorBanner"; |
| 198 | import { | 140 | import { |
| ... | @@ -200,7 +142,6 @@ import { | ... | @@ -200,7 +142,6 @@ import { |
| 200 | buildExpiredTerminalContextToastCopy, | 142 | buildExpiredTerminalContextToastCopy, |
| 201 | buildLocalDraftThread, | 143 | buildLocalDraftThread, |
| 202 | buildTemporaryWorktreeBranchName, | 144 | buildTemporaryWorktreeBranchName, |
| 203 | cloneComposerImageForRetry, | | |
| 204 | collectUserMessageBlobPreviewUrls, | 145 | collectUserMessageBlobPreviewUrls, |
| 205 | createLocalDispatchSnapshot, | 146 | createLocalDispatchSnapshot, |
| 206 | deriveComposerSendState, | 147 | deriveComposerSendState, |
| ... | @@ -209,12 +150,13 @@ import { | ... | @@ -209,12 +150,13 @@ import { |
| 209 | LastInvokedScriptByProjectSchema, | 150 | LastInvokedScriptByProjectSchema, |
| 210 | type LocalDispatchSnapshot, | 151 | type LocalDispatchSnapshot, |
| 211 | PullRequestDialogState, | 152 | PullRequestDialogState, |
| | 153 | cloneComposerImageForRetry, |
| | 154 | deriveLockedProvider, |
| 212 | readFileAsDataUrl, | 155 | readFileAsDataUrl, |
| 213 | reconcileMountedTerminalThreadIds, | 156 | reconcileMountedTerminalThreadIds, |
| 214 | revokeBlobPreviewUrl, | 157 | revokeBlobPreviewUrl, |
| 215 | revokeUserMessagePreviewUrls, | 158 | revokeUserMessagePreviewUrls, |
| 216 | shouldWriteThreadErrorToCurrentServerThread, | 159 | shouldWriteThreadErrorToCurrentServerThread, |
| 217 | threadHasStarted, | | |
| 218 | waitForStartedServerThread, | 160 | waitForStartedServerThread, |
| 219 | } from "./ChatView.logic"; | 161 | } from "./ChatView.logic"; |
| 220 | import { useLocalStorage } from "~/hooks/useLocalStorage"; | 162 | import { useLocalStorage } from "~/hooks/useLocalStorage"; |
| ... | @@ -225,12 +167,10 @@ import { | ... | @@ -225,12 +167,10 @@ import { |
| 225 | } from "~/rpc/serverState"; | 167 | } from "~/rpc/serverState"; |
| 226 | import { sanitizeThreadErrorMessage } from "~/rpc/transportError"; | 168 | import { sanitizeThreadErrorMessage } from "~/rpc/transportError"; |
| 227 | | 169 | |
| 228 | const IMAGE_SIZE_LIMIT_LABEL = `${Math.round(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES / (1024 * 1024))}MB`; | | |
| 229 | const IMAGE_ONLY_BOOTSTRAP_PROMPT = | 170 | const IMAGE_ONLY_BOOTSTRAP_PROMPT = |
| 230 | "[User attached one or more images without additional text. Respond using the conversation context and the attached image(s).]"; | 171 | "[User attached one or more images without additional text. Respond using the conversation context and the attached image(s).]"; |
| 231 | const EMPTY_ACTIVITIES: OrchestrationThreadActivity[] = []; | 172 | const EMPTY_ACTIVITIES: OrchestrationThreadActivity[] = []; |
| 232 | const EMPTY_PROPOSED_PLANS: Thread["proposedPlans"] = []; | 173 | const EMPTY_PROPOSED_PLANS: Thread["proposedPlans"] = []; |
| 233 | const EMPTY_PROJECT_ENTRIES: ProjectEntry[] = []; | | |
| 234 | const EMPTY_PROVIDERS: ServerProvider[] = []; | 174 | const EMPTY_PROVIDERS: ServerProvider[] = []; |
| 235 | const EMPTY_PENDING_USER_INPUT_ANSWERS: Record<string, PendingUserInputDraftAnswer> = {}; | 175 | const EMPTY_PENDING_USER_INPUT_ANSWERS: Record<string, PendingUserInputDraftAnswer> = {}; |
| 236 | | 176 | |
| ... | @@ -362,38 +302,9 @@ function formatOutgoingPrompt(params: { | ... | @@ -362,38 +302,9 @@ function formatOutgoingPrompt(params: { |
| 362 | } | 302 | } |
| 363 | return params.text; | 303 | return params.text; |
| 364 | } | 304 | } |
| 365 | const COMPOSER_PATH_QUERY_DEBOUNCE_MS = 120; | | |
| 366 | const SCRIPT_TERMINAL_COLS = 120; | 305 | const SCRIPT_TERMINAL_COLS = 120; |
| 367 | const SCRIPT_TERMINAL_ROWS = 30; | 306 | const SCRIPT_TERMINAL_ROWS = 30; |
| 368 | | 307 | |
| 369 | const extendReplacementRangeForTrailingSpace = ( | | |
| 370 | text: string, | | |
| 371 | rangeEnd: number, | | |
| 372 | replacement: string, | | |
| 373 | ): number => { | | |
| 374 | if (!replacement.endsWith(" ")) { | | |
| 375 | return rangeEnd; | | |
| 376 | } | | |
| 377 | return text[rangeEnd] === " " ? rangeEnd + 1 : rangeEnd; | | |
| 378 | }; | | |
| 379 | | | |
| 380 | const syncTerminalContextsByIds = ( | | |
| 381 | contexts: ReadonlyArray<TerminalContextDraft>, | | |
| 382 | ids: ReadonlyArray<string>, | | |
| 383 | ): TerminalContextDraft[] => { | | |
| 384 | const contextsById = new Map(contexts.map((context) => [context.id, context])); | | |
| 385 | return ids.flatMap((id) => { | | |
| 386 | const context = contextsById.get(id); | | |
| 387 | return context ? [context] : []; | | |
| 388 | }); | | |
| 389 | }; | | |
| 390 | | | |
| 391 | const terminalContextIdListsEqual = ( | | |
| 392 | contexts: ReadonlyArray<TerminalContextDraft>, | | |
| 393 | ids: ReadonlyArray<string>, | | |
| 394 | ): boolean => | | |
| 395 | contexts.length === ids.length && contexts.every((context, index) => context.id === ids[index]); | | |
| 396 | | | |
| 397 | type ChatViewProps = | 308 | type ChatViewProps = |
| 398 | | { | 309 | | { |
| 399 | environmentId: EnvironmentId; | 310 | environmentId: EnvironmentId; |
| ... | @@ -414,29 +325,6 @@ interface TerminalLaunchContext { | ... | @@ -414,29 +325,6 @@ interface TerminalLaunchContext { |
| 414 | worktreePath: string | null; | 325 | worktreePath: string | null; |
| 415 | } | 326 | } |
| 416 | | 327 | |
| 417 | const runtimeModeConfig: Record< | | |
| 418 | RuntimeMode, | | |
| 419 | { label: string; description: string; icon: LucideIcon } | | |
| 420 | > = { | | |
| 421 | "approval-required": { | | |
| 422 | label: "Supervised", | | |
| 423 | description: "Ask before commands and file changes.", | | |
| 424 | icon: LockIcon, | | |
| 425 | }, | | |
| 426 | "auto-accept-edits": { | | |
| 427 | label: "Auto-accept edits", | | |
| 428 | description: "Auto-approve edits, ask before other actions.", | | |
| 429 | icon: PenLineIcon, | | |
| 430 | }, | | |
| 431 | "full-access": { | | |
| 432 | label: "Full access", | | |
| 433 | description: "Allow commands and edits without prompts.", | | |
| 434 | icon: LockOpenIcon, | | |
| 435 | }, | | |
| 436 | }; | | |
| 437 | | | |
| 438 | const runtimeModeOptions = Object.keys(runtimeModeConfig) as RuntimeMode[]; | | |
| 439 | | | |
| 440 | type PersistentTerminalLaunchContext = Pick<TerminalLaunchContext, "cwd" | "worktreePath">; | 328 | type PersistentTerminalLaunchContext = Pick<TerminalLaunchContext, "cwd" | "worktreePath">; |
| 441 | | 329 | |
| 442 | function useLocalDispatchState(input: { | 330 | function useLocalDispatchState(input: { |
| ... | @@ -712,53 +600,31 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -712,53 +600,31 @@ export default function ChatView(props: ChatViewProps) { |
| 712 | select: (params) => parseDiffRouteSearch(params), | 600 | select: (params) => parseDiffRouteSearch(params), |
| 713 | }); | 601 | }); |
| 714 | const { resolvedTheme } = useTheme(); | 602 | const { resolvedTheme } = useTheme(); |
| 715 | const composerDraft = useComposerThreadDraft(composerDraftTarget); | 603 | // Granular store selectors — avoid subscribing to prompt changes. |
| 716 | const prompt = composerDraft.prompt; | 604 | const composerRuntimeMode = useComposerDraftStore( |
| 717 | const composerImages = composerDraft.images; | 605 | (store) => store.getComposerDraft(composerDraftTarget)?.runtimeMode ?? null, |
| 718 | const composerTerminalContexts = composerDraft.terminalContexts; | | |
| 719 | const composerSendState = useMemo( | | |
| 720 | () => | | |
| 721 | deriveComposerSendState({ | | |
| 722 | prompt, | | |
| 723 | imageCount: composerImages.length, | | |
| 724 | terminalContexts: composerTerminalContexts, | | |
| 725 | }), | | |
| 726 | [composerImages.length, composerTerminalContexts, prompt], | | |
| 727 | ); | | |
| 728 | const nonPersistedComposerImageIds = composerDraft.nonPersistedImageIds; | | |
| 729 | const setComposerDraftPrompt = useComposerDraftStore((store) => store.setPrompt); | | |
| 730 | const setComposerDraftModelSelection = useComposerDraftStore((store) => store.setModelSelection); | | |
| 731 | const setComposerDraftRuntimeMode = useComposerDraftStore((store) => store.setRuntimeMode); | | |
| 732 | const setComposerDraftInteractionMode = useComposerDraftStore( | | |
| 733 | (store) => store.setInteractionMode, | | |
| 734 | ); | | |
| 735 | const addComposerDraftImage = useComposerDraftStore((store) => store.addImage); | | |
| 736 | const addComposerDraftImages = useComposerDraftStore((store) => store.addImages); | | |
| 737 | const removeComposerDraftImage = useComposerDraftStore((store) => store.removeImage); | | |
| 738 | const insertComposerDraftTerminalContext = useComposerDraftStore( | | |
| 739 | (store) => store.insertTerminalContext, | | |
| 740 | ); | 606 | ); |
| 741 | const addComposerDraftTerminalContexts = useComposerDraftStore( | 607 | const composerInteractionMode = useComposerDraftStore( |
| 742 | (store) => store.addTerminalContexts, | 608 | (store) => store.getComposerDraft(composerDraftTarget)?.interactionMode ?? null, |
| 743 | ); | 609 | ); |
| 744 | const removeComposerDraftTerminalContext = useComposerDraftStore( | 610 | const composerActiveProvider = useComposerDraftStore( |
| 745 | (store) => store.removeTerminalContext, | 611 | (store) => store.getComposerDraft(composerDraftTarget)?.activeProvider ?? null, |
| 746 | ); | 612 | ); |
| | 613 | const setComposerDraftPrompt = useComposerDraftStore((store) => store.setPrompt); |
| | 614 | const addComposerDraftImages = useComposerDraftStore((store) => store.addImages); |
| 747 | const setComposerDraftTerminalContexts = useComposerDraftStore( | 615 | const setComposerDraftTerminalContexts = useComposerDraftStore( |
| 748 | (store) => store.setTerminalContexts, | 616 | (store) => store.setTerminalContexts, |
| 749 | ); | 617 | ); |
| 750 | const clearComposerDraftPersistedAttachments = useComposerDraftStore( | 618 | const setComposerDraftModelSelection = useComposerDraftStore((store) => store.setModelSelection); |
| 751 | (store) => store.clearPersistedAttachments, | 619 | const setComposerDraftRuntimeMode = useComposerDraftStore((store) => store.setRuntimeMode); |
| 752 | ); | 620 | const setComposerDraftInteractionMode = useComposerDraftStore( |
| 753 | const syncComposerDraftPersistedAttachments = useComposerDraftStore( | 621 | (store) => store.setInteractionMode, |
| 754 | (store) => store.syncPersistedAttachments, | | |
| 755 | ); | 622 | ); |
| 756 | const clearComposerDraftContent = useComposerDraftStore((store) => store.clearComposerContent); | 623 | const clearComposerDraftContent = useComposerDraftStore((store) => store.clearComposerContent); |
| 757 | const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); | 624 | const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); |
| 758 | const getDraftSessionByLogicalProjectKey = useComposerDraftStore( | 625 | const getDraftSessionByLogicalProjectKey = useComposerDraftStore( |
| 759 | (store) => store.getDraftSessionByLogicalProjectKey, | 626 | (store) => store.getDraftSessionByLogicalProjectKey, |
| 760 | ); | 627 | ); |
| 761 | const getComposerDraft = useComposerDraftStore((store) => store.getComposerDraft); | | |
| 762 | const getDraftSession = useComposerDraftStore((store) => store.getDraftSession); | 628 | const getDraftSession = useComposerDraftStore((store) => store.getDraftSession); |
| 763 | const setLogicalProjectDraftThreadId = useComposerDraftStore( | 629 | const setLogicalProjectDraftThreadId = useComposerDraftStore( |
| 764 | (store) => store.setLogicalProjectDraftThreadId, | 630 | (store) => store.setLogicalProjectDraftThreadId, |
| ... | @@ -770,14 +636,15 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -770,14 +636,15 @@ export default function ChatView(props: ChatViewProps) { |
| 770 | ? store.getDraftSession(draftId) | 636 | ? store.getDraftSession(draftId) |
| 771 | : null, | 637 | : null, |
| 772 | ); | 638 | ); |
| 773 | const promptRef = useRef(prompt); | 639 | const promptRef = useRef(""); |
| | 640 | const composerImagesRef = useRef<ComposerImageAttachment[]>([]); |
| | 641 | const composerTerminalContextsRef = useRef<TerminalContextDraft[]>([]); |
| | 642 | const composerRef = useRef<ChatComposerHandle>(null); |
| 774 | const [showScrollToBottom, setShowScrollToBottom] = useState(false); | 643 | const [showScrollToBottom, setShowScrollToBottom] = useState(false); |
| 775 | const [isDragOverComposer, setIsDragOverComposer] = useState(false); | | |
| 776 | const [expandedImage, setExpandedImage] = useState<ExpandedImagePreview | null>(null); | 644 | const [expandedImage, setExpandedImage] = useState<ExpandedImagePreview | null>(null); |
| 777 | const [optimisticUserMessages, setOptimisticUserMessages] = useState<ChatMessage[]>([]); | 645 | const [optimisticUserMessages, setOptimisticUserMessages] = useState<ChatMessage[]>([]); |
| 778 | const optimisticUserMessagesRef = useRef(optimisticUserMessages); | 646 | const optimisticUserMessagesRef = useRef(optimisticUserMessages); |
| 779 | optimisticUserMessagesRef.current = optimisticUserMessages; | 647 | optimisticUserMessagesRef.current = optimisticUserMessages; |
| 780 | const composerTerminalContextsRef = useRef<TerminalContextDraft[]>(composerTerminalContexts); | | |
| 781 | const [localDraftErrorsByDraftId, setLocalDraftErrorsByDraftId] = useState< | 648 | const [localDraftErrorsByDraftId, setLocalDraftErrorsByDraftId] = useState< |
| 782 | Record<string, string | null> | 649 | Record<string, string | null> |
| 783 | >({}); | 650 | >({}); |
| ... | @@ -794,8 +661,6 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -794,8 +661,6 @@ export default function ChatView(props: ChatViewProps) { |
| 794 | useState<Record<string, number>>({}); | 661 | useState<Record<string, number>>({}); |
| 795 | const [expandedWorkGroups, setExpandedWorkGroups] = useState<Record<string, boolean>>({}); | 662 | const [expandedWorkGroups, setExpandedWorkGroups] = useState<Record<string, boolean>>({}); |
| 796 | const [planSidebarOpen, setPlanSidebarOpen] = useState(false); | 663 | const [planSidebarOpen, setPlanSidebarOpen] = useState(false); |
| 797 | const [isComposerFooterCompact, setIsComposerFooterCompact] = useState(false); | | |
| 798 | const [isComposerPrimaryActionsCompact, setIsComposerPrimaryActionsCompact] = useState(false); | | |
| 799 | // Tracks whether the user explicitly dismissed the sidebar for the active turn. | 664 | // Tracks whether the user explicitly dismissed the sidebar for the active turn. |
| 800 | const planSidebarDismissedForTurnRef = useRef<string | null>(null); | 665 | const planSidebarDismissedForTurnRef = useRef<string | null>(null); |
| 801 | // When set, the thread-change reset effect will open the sidebar instead of closing it. | 666 | // When set, the thread-change reset effect will open the sidebar instead of closing it. |
| ... | @@ -803,7 +668,6 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -803,7 +668,6 @@ export default function ChatView(props: ChatViewProps) { |
| 803 | const planSidebarOpenOnNextThreadRef = useRef(false); | 668 | const planSidebarOpenOnNextThreadRef = useRef(false); |
| 804 | const [nowTick, setNowTick] = useState(() => Date.now()); | 669 | const [nowTick, setNowTick] = useState(() => Date.now()); |
| 805 | const [terminalFocusRequestId, setTerminalFocusRequestId] = useState(0); | 670 | const [terminalFocusRequestId, setTerminalFocusRequestId] = useState(0); |
| 806 | const [composerHighlightedItemId, setComposerHighlightedItemId] = useState<string | null>(null); | | |
| 807 | const [pullRequestDialogState, setPullRequestDialogState] = | 671 | const [pullRequestDialogState, setPullRequestDialogState] = |
| 808 | useState<PullRequestDialogState | null>(null); | 672 | useState<PullRequestDialogState | null>(null); |
| 809 | const [terminalLaunchContext, setTerminalLaunchContext] = useState<TerminalLaunchContext | null>( | 673 | const [terminalLaunchContext, setTerminalLaunchContext] = useState<TerminalLaunchContext | null>( |
| ... | @@ -812,12 +676,6 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -812,12 +676,6 @@ export default function ChatView(props: ChatViewProps) { |
| 812 | const [attachmentPreviewHandoffByMessageId, setAttachmentPreviewHandoffByMessageId] = useState< | 676 | const [attachmentPreviewHandoffByMessageId, setAttachmentPreviewHandoffByMessageId] = useState< |
| 813 | Record<string, string[]> | 677 | Record<string, string[]> |
| 814 | >({}); | 678 | >({}); |
| 815 | const [composerCursor, setComposerCursor] = useState(() => | | |
| 816 | collapseExpandedComposerCursor(prompt, prompt.length), | | |
| 817 | ); | | |
| 818 | const [composerTrigger, setComposerTrigger] = useState<ComposerTrigger | null>(() => | | |
| 819 | detectComposerTrigger(prompt, prompt.length), | | |
| 820 | ); | | |
| 821 | const [lastInvokedScriptByProjectId, setLastInvokedScriptByProjectId] = useLocalStorage( | 679 | const [lastInvokedScriptByProjectId, setLastInvokedScriptByProjectId] = useLocalStorage( |
| 822 | LAST_INVOKED_SCRIPT_BY_PROJECT_KEY, | 680 | LAST_INVOKED_SCRIPT_BY_PROJECT_KEY, |
| 823 | {}, | 681 | {}, |
| ... | @@ -836,21 +694,9 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -836,21 +694,9 @@ export default function ChatView(props: ChatViewProps) { |
| 836 | top: number; | 694 | top: number; |
| 837 | } | null>(null); | 695 | } | null>(null); |
| 838 | const pendingInteractionAnchorFrameRef = useRef<number | null>(null); | 696 | const pendingInteractionAnchorFrameRef = useRef<number | null>(null); |
| 839 | const composerEditorRef = useRef<ComposerPromptEditorHandle>(null); | | |
| 840 | const composerFormRef = useRef<HTMLFormElement>(null); | | |
| 841 | const composerFormHeightRef = useRef(0); | | |
| 842 | const composerFooterRef = useRef<HTMLDivElement>(null); | | |
| 843 | const composerFooterLeadingRef = useRef<HTMLDivElement>(null); | | |
| 844 | const composerFooterActionsRef = useRef<HTMLDivElement>(null); | | |
| 845 | const composerImagesRef = useRef<ComposerImageAttachment[]>([]); | | |
| 846 | const composerSelectLockRef = useRef(false); | | |
| 847 | const composerMenuOpenRef = useRef(false); | | |
| 848 | const composerMenuItemsRef = useRef<ComposerCommandItem[]>([]); | | |
| 849 | const activeComposerMenuItemRef = useRef<ComposerCommandItem | null>(null); | | |
| 850 | const attachmentPreviewHandoffByMessageIdRef = useRef<Record<string, string[]>>({}); | 697 | const attachmentPreviewHandoffByMessageIdRef = useRef<Record<string, string[]>>({}); |
| 851 | const attachmentPreviewPromotionInFlightByMessageIdRef = useRef<Record<string, true>>({}); | 698 | const attachmentPreviewPromotionInFlightByMessageIdRef = useRef<Record<string, true>>({}); |
| 852 | const sendInFlightRef = useRef(false); | 699 | const sendInFlightRef = useRef(false); |
| 853 | const dragDepthRef = useRef(0); | | |
| 854 | const terminalOpenByThreadRef = useRef<Record<string, boolean>>({}); | 700 | const terminalOpenByThreadRef = useRef<Record<string, boolean>>({}); |
| 855 | const setMessagesScrollContainerRef = useCallback((element: HTMLDivElement | null) => { | 701 | const setMessagesScrollContainerRef = useCallback((element: HTMLDivElement | null) => { |
| 856 | messagesScrollRef.current = element; | 702 | messagesScrollRef.current = element; |
| ... | @@ -903,59 +749,6 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -903,59 +749,6 @@ export default function ChatView(props: ChatViewProps) { |
| 903 | [mountedTerminalThreadKeys], | 749 | [mountedTerminalThreadKeys], |
| 904 | ); | 750 | ); |
| 905 | | 751 | |
| 906 | const setPrompt = useCallback( | | |
| 907 | (nextPrompt: string) => { | | |
| 908 | setComposerDraftPrompt(composerDraftTarget, nextPrompt); | | |
| 909 | }, | | |
| 910 | [composerDraftTarget, setComposerDraftPrompt], | | |
| 911 | ); | | |
| 912 | const addComposerImage = useCallback( | | |
| 913 | (image: ComposerImageAttachment) => { | | |
| 914 | addComposerDraftImage(composerDraftTarget, image); | | |
| 915 | }, | | |
| 916 | [addComposerDraftImage, composerDraftTarget], | | |
| 917 | ); | | |
| 918 | const addComposerImagesToDraft = useCallback( | | |
| 919 | (images: ComposerImageAttachment[]) => { | | |
| 920 | addComposerDraftImages(composerDraftTarget, images); | | |
| 921 | }, | | |
| 922 | [addComposerDraftImages, composerDraftTarget], | | |
| 923 | ); | | |
| 924 | const addComposerTerminalContextsToDraft = useCallback( | | |
| 925 | (contexts: TerminalContextDraft[]) => { | | |
| 926 | addComposerDraftTerminalContexts(composerDraftTarget, contexts); | | |
| 927 | }, | | |
| 928 | [addComposerDraftTerminalContexts, composerDraftTarget], | | |
| 929 | ); | | |
| 930 | const removeComposerImageFromDraft = useCallback( | | |
| 931 | (imageId: string) => { | | |
| 932 | removeComposerDraftImage(composerDraftTarget, imageId); | | |
| 933 | }, | | |
| 934 | [composerDraftTarget, removeComposerDraftImage], | | |
| 935 | ); | | |
| 936 | const removeComposerTerminalContextFromDraft = useCallback( | | |
| 937 | (contextId: string) => { | | |
| 938 | const contextIndex = composerTerminalContexts.findIndex( | | |
| 939 | (context) => context.id === contextId, | | |
| 940 | ); | | |
| 941 | if (contextIndex < 0) { | | |
| 942 | return; | | |
| 943 | } | | |
| 944 | const nextPrompt = removeInlineTerminalContextPlaceholder(promptRef.current, contextIndex); | | |
| 945 | promptRef.current = nextPrompt.prompt; | | |
| 946 | setPrompt(nextPrompt.prompt); | | |
| 947 | removeComposerDraftTerminalContext(composerDraftTarget, contextId); | | |
| 948 | setComposerCursor(nextPrompt.cursor); | | |
| 949 | setComposerTrigger( | | |
| 950 | detectComposerTrigger( | | |
| 951 | nextPrompt.prompt, | | |
| 952 | expandCollapsedComposerCursor(nextPrompt.prompt, nextPrompt.cursor), | | |
| 953 | ), | | |
| 954 | ); | | |
| 955 | }, | | |
| 956 | [composerDraftTarget, composerTerminalContexts, removeComposerDraftTerminalContext, setPrompt], | | |
| 957 | ); | | |
| 958 | | | |
| 959 | const fallbackDraftProjectRef = draftThread | 752 | const fallbackDraftProjectRef = draftThread |
| 960 | ? scopeProjectRef(draftThread.environmentId, draftThread.projectId) | 753 | ? scopeProjectRef(draftThread.environmentId, draftThread.projectId) |
| 961 | : null; | 754 | : null; |
| ... | @@ -983,12 +776,9 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -983,12 +776,9 @@ export default function ChatView(props: ChatViewProps) { |
| 983 | ); | 776 | ); |
| 984 | const isServerThread = routeKind === "server" && serverThread !== undefined; | 777 | const isServerThread = routeKind === "server" && serverThread !== undefined; |
| 985 | const activeThread = isServerThread ? serverThread : localDraftThread; | 778 | const activeThread = isServerThread ? serverThread : localDraftThread; |
| 986 | const runtimeMode = | 779 | const runtimeMode = composerRuntimeMode ?? activeThread?.runtimeMode ?? DEFAULT_RUNTIME_MODE; |
| 987 | composerDraft.runtimeMode ?? activeThread?.runtimeMode ?? DEFAULT_RUNTIME_MODE; | | |
| 988 | const interactionMode = | 780 | const interactionMode = |
| 989 | composerDraft.interactionMode ?? activeThread?.interactionMode ?? DEFAULT_INTERACTION_MODE; | 781 | composerInteractionMode ?? activeThread?.interactionMode ?? DEFAULT_INTERACTION_MODE; |
| 990 | const runtimeModeOption = runtimeModeConfig[runtimeMode]; | | |
| 991 | const RuntimeModeIcon = runtimeModeOption.icon; | | |
| 992 | const isLocalDraftThread = !isServerThread && localDraftThread !== undefined; | 782 | const isLocalDraftThread = !isServerThread && localDraftThread !== undefined; |
| 993 | const canCheckoutPullRequestIntoThread = isLocalDraftThread; | 783 | const canCheckoutPullRequestIntoThread = isLocalDraftThread; |
| 994 | const diffOpen = rawSearch.diff === "1"; | 784 | const diffOpen = rawSearch.diff === "1"; |
| ... | @@ -1016,10 +806,6 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -1016,10 +806,6 @@ export default function ChatView(props: ChatViewProps) { |
| 1016 | return threadIds; | 806 | return threadIds; |
| 1017 | }, [activeLatestTurn?.sourceProposedPlan?.threadId, activeThread?.id]), | 807 | }, [activeLatestTurn?.sourceProposedPlan?.threadId, activeThread?.id]), |
| 1018 | ); | 808 | ); |
| 1019 | const activeContextWindow = useMemo( | | |
| 1020 | () => deriveLatestContextWindowSnapshot(activeThread?.activities ?? []), | | |
| 1021 | [activeThread?.activities], | | |
| 1022 | ); | | |
| 1023 | useEffect(() => { | 809 | useEffect(() => { |
| 1024 | setMountedTerminalThreadKeys((currentThreadIds) => { | 810 | setMountedTerminalThreadKeys((currentThreadIds) => { |
| 1025 | const nextThreadIds = reconcileMountedTerminalThreadIds({ | 811 | const nextThreadIds = reconcileMountedTerminalThreadIds({ |
| ... | @@ -1103,7 +889,6 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -1103,7 +889,6 @@ export default function ChatView(props: ChatViewProps) { |
| 1103 | initialReference: reference ?? null, | 889 | initialReference: reference ?? null, |
| 1104 | key: Date.now(), | 890 | key: Date.now(), |
| 1105 | }); | 891 | }); |
| 1106 | setComposerHighlightedItemId(null); | | |
| 1107 | }, | 892 | }, |
| 1108 | [canCheckoutPullRequestIntoThread], | 893 | [canCheckoutPullRequestIntoThread], |
| 1109 | ); | 894 | ); |
| ... | @@ -1215,14 +1000,14 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -1215,14 +1000,14 @@ export default function ChatView(props: ChatViewProps) { |
| 1215 | serverThread?.id, | 1000 | serverThread?.id, |
| 1216 | ]); | 1001 | ]); |
| 1217 | | 1002 | |
| 1218 | const sessionProvider = activeThread?.session?.provider ?? null; | 1003 | const selectedProviderByThreadId = composerActiveProvider ?? null; |
| 1219 | const selectedProviderByThreadId = composerDraft.activeProvider ?? null; | | |
| 1220 | const threadProvider = | 1004 | const threadProvider = |
| 1221 | activeThread?.modelSelection.provider ?? activeProject?.defaultModelSelection?.provider ?? null; | 1005 | activeThread?.modelSelection.provider ?? activeProject?.defaultModelSelection?.provider ?? null; |
| 1222 | const hasThreadStarted = threadHasStarted(activeThread); | 1006 | const lockedProvider = deriveLockedProvider({ |
| 1223 | const lockedProvider: ProviderKind | null = hasThreadStarted | 1007 | thread: activeThread, |
| 1224 | ? (sessionProvider ?? threadProvider ?? selectedProviderByThreadId ?? null) | 1008 | selectedProvider: selectedProviderByThreadId, |
| 1225 | : null; | 1009 | threadProvider, |
| | 1010 | }); |
| 1226 | const primaryServerConfig = useServerConfig(); | 1011 | const primaryServerConfig = useServerConfig(); |
| 1227 | const activeEnvRuntimeState = useSavedEnvironmentRuntimeStore((s) => | 1012 | const activeEnvRuntimeState = useSavedEnvironmentRuntimeStore((s) => |
| 1228 | activeThread?.environmentId ? s.byId[activeThread.environmentId] : null, | 1013 | activeThread?.environmentId ? s.byId[activeThread.environmentId] : null, |
| ... | @@ -1240,37 +1025,6 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -1240,37 +1025,6 @@ export default function ChatView(props: ChatViewProps) { |
| 1240 | selectedProviderByThreadId ?? threadProvider ?? "codex", | 1025 | selectedProviderByThreadId ?? threadProvider ?? "codex", |
| 1241 | ); | 1026 | ); |
| 1242 | const selectedProvider: ProviderKind = lockedProvider ?? unlockedSelectedProvider; | 1027 | const selectedProvider: ProviderKind = lockedProvider ?? unlockedSelectedProvider; |
| 1243 | const { modelOptions: composerModelOptions, selectedModel } = useEffectiveComposerModelState({ | | |
| 1244 | threadRef: composerDraftTarget, | | |
| 1245 | providers: providerStatuses, | | |
| 1246 | selectedProvider, | | |
| 1247 | threadModelSelection: activeThread?.modelSelection, | | |
| 1248 | projectModelSelection: activeProject?.defaultModelSelection, | | |
| 1249 | settings, | | |
| 1250 | }); | | |
| 1251 | const selectedProviderModels = getProviderModels(providerStatuses, selectedProvider); | | |
| 1252 | const composerProviderState = useMemo( | | |
| 1253 | () => | | |
| 1254 | getComposerProviderState({ | | |
| 1255 | provider: selectedProvider, | | |
| 1256 | model: selectedModel, | | |
| 1257 | models: selectedProviderModels, | | |
| 1258 | prompt, | | |
| 1259 | modelOptions: composerModelOptions, | | |
| 1260 | }), | | |
| 1261 | [composerModelOptions, prompt, selectedModel, selectedProvider, selectedProviderModels], | | |
| 1262 | ); | | |
| 1263 | const selectedPromptEffort = composerProviderState.promptEffort; | | |
| 1264 | const selectedModelOptionsForDispatch = composerProviderState.modelOptionsForDispatch; | | |
| 1265 | const selectedModelSelection = useMemo<ModelSelection>( | | |
| 1266 | () => ({ | | |
| 1267 | provider: selectedProvider, | | |
| 1268 | model: selectedModel, | | |
| 1269 | ...(selectedModelOptionsForDispatch ? { options: selectedModelOptionsForDispatch } : {}), | | |
| 1270 | }), | | |
| 1271 | [selectedModel, selectedModelOptionsForDispatch, selectedProvider], | | |
| 1272 | ); | | |
| 1273 | const selectedModelForPicker = selectedModel; | | |
| 1274 | const phase = derivePhase(activeThread?.session ?? null); | 1028 | const phase = derivePhase(activeThread?.session ?? null); |
| 1275 | const threadActivities = activeThread?.activities ?? EMPTY_ACTIVITIES; | 1029 | const threadActivities = activeThread?.activities ?? EMPTY_ACTIVITIES; |
| 1276 | const workLogEntries = useMemo( | 1030 | const workLogEntries = useMemo( |
| ... | @@ -1372,75 +1126,6 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -1372,75 +1126,6 @@ export default function ChatView(props: ChatViewProps) { |
| 1372 | activeThread?.session ?? null, | 1126 | activeThread?.session ?? null, |
| 1373 | localDispatchStartedAt, | 1127 | localDispatchStartedAt, |
| 1374 | ); | 1128 | ); |
| 1375 | const isComposerApprovalState = activePendingApproval !== null; | | |
| 1376 | const hasComposerHeader = | | |
| 1377 | isComposerApprovalState || | | |
| 1378 | pendingUserInputs.length > 0 || | | |
| 1379 | (showPlanFollowUpPrompt && activeProposedPlan !== null); | | |
| 1380 | const composerFooterHasWideActions = showPlanFollowUpPrompt || activePendingProgress !== null; | | |
| 1381 | const composerFooterActionLayoutKey = useMemo(() => { | | |
| 1382 | if (activePendingProgress) { | | |
| 1383 | return `pending:${activePendingProgress.questionIndex}:${activePendingProgress.isLastQuestion}:${activePendingIsResponding}`; | | |
| 1384 | } | | |
| 1385 | if (phase === "running") { | | |
| 1386 | return "running"; | | |
| 1387 | } | | |
| 1388 | if (showPlanFollowUpPrompt) { | | |
| 1389 | return prompt.trim().length > 0 ? "plan:refine" : "plan:implement"; | | |
| 1390 | } | | |
| 1391 | return `idle:${composerSendState.hasSendableContent}:${isSendBusy}:${isConnecting}:${isPreparingWorktree}`; | | |
| 1392 | }, [ | | |
| 1393 | activePendingIsResponding, | | |
| 1394 | activePendingProgress, | | |
| 1395 | composerSendState.hasSendableContent, | | |
| 1396 | isConnecting, | | |
| 1397 | isPreparingWorktree, | | |
| 1398 | isSendBusy, | | |
| 1399 | phase, | | |
| 1400 | prompt, | | |
| 1401 | showPlanFollowUpPrompt, | | |
| 1402 | ]); | | |
| 1403 | const lastSyncedPendingInputRef = useRef<{ | | |
| 1404 | requestId: string | null; | | |
| 1405 | questionId: string | null; | | |
| 1406 | } | null>(null); | | |
| 1407 | useEffect(() => { | | |
| 1408 | const nextCustomAnswer = activePendingProgress?.customAnswer; | | |
| 1409 | if (typeof nextCustomAnswer !== "string") { | | |
| 1410 | lastSyncedPendingInputRef.current = null; | | |
| 1411 | return; | | |
| 1412 | } | | |
| 1413 | const nextRequestId = activePendingUserInput?.requestId ?? null; | | |
| 1414 | const nextQuestionId = activePendingProgress?.activeQuestion?.id ?? null; | | |
| 1415 | const questionChanged = | | |
| 1416 | lastSyncedPendingInputRef.current?.requestId !== nextRequestId || | | |
| 1417 | lastSyncedPendingInputRef.current?.questionId !== nextQuestionId; | | |
| 1418 | const textChangedExternally = promptRef.current !== nextCustomAnswer; | | |
| 1419 | | | |
| 1420 | lastSyncedPendingInputRef.current = { | | |
| 1421 | requestId: nextRequestId, | | |
| 1422 | questionId: nextQuestionId, | | |
| 1423 | }; | | |
| 1424 | | | |
| 1425 | if (!questionChanged && !textChangedExternally) { | | |
| 1426 | return; | | |
| 1427 | } | | |
| 1428 | | | |
| 1429 | promptRef.current = nextCustomAnswer; | | |
| 1430 | const nextCursor = collapseExpandedComposerCursor(nextCustomAnswer, nextCustomAnswer.length); | | |
| 1431 | setComposerCursor(nextCursor); | | |
| 1432 | setComposerTrigger( | | |
| 1433 | detectComposerTrigger( | | |
| 1434 | nextCustomAnswer, | | |
| 1435 | expandCollapsedComposerCursor(nextCustomAnswer, nextCursor), | | |
| 1436 | ), | | |
| 1437 | ); | | |
| 1438 | setComposerHighlightedItemId(null); | | |
| 1439 | }, [ | | |
| 1440 | activePendingProgress?.customAnswer, | | |
| 1441 | activePendingUserInput?.requestId, | | |
| 1442 | activePendingProgress?.activeQuestion?.id, | | |
| 1443 | ]); | | |
| 1444 | useEffect(() => { | 1129 | useEffect(() => { |
| 1445 | attachmentPreviewHandoffByMessageIdRef.current = attachmentPreviewHandoffByMessageId; | 1130 | attachmentPreviewHandoffByMessageIdRef.current = attachmentPreviewHandoffByMessageId; |
| 1446 | }, [attachmentPreviewHandoffByMessageId]); | 1131 | }, [attachmentPreviewHandoffByMessageId]); |
| ... | @@ -1708,139 +1393,9 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -1708,139 +1393,9 @@ export default function ChatView(props: ChatViewProps) { |
| 1708 | worktreePath: activeThread?.worktreePath ?? null, | 1393 | worktreePath: activeThread?.worktreePath ?? null, |
| 1709 | }) | 1394 | }) |
| 1710 | : null; | 1395 | : null; |
| 1711 | const composerTriggerKind = composerTrigger?.kind ?? null; | | |
| 1712 | const pathTriggerQuery = composerTrigger?.kind === "path" ? composerTrigger.query : ""; | | |
| 1713 | const isPathTrigger = composerTriggerKind === "path"; | | |
| 1714 | const [debouncedPathQuery, composerPathQueryDebouncer] = useDebouncedValue( | | |
| 1715 | pathTriggerQuery, | | |
| 1716 | { wait: COMPOSER_PATH_QUERY_DEBOUNCE_MS }, | | |
| 1717 | (debouncerState) => ({ isPending: debouncerState.isPending }), | | |
| 1718 | ); | | |
| 1719 | const effectivePathQuery = pathTriggerQuery.length > 0 ? debouncedPathQuery : ""; | | |
| 1720 | const gitStatusQuery = useGitStatus({ environmentId, cwd: gitCwd }); | 1396 | const gitStatusQuery = useGitStatus({ environmentId, cwd: gitCwd }); |
| 1721 | const keybindings = useServerKeybindings(); | 1397 | const keybindings = useServerKeybindings(); |
| 1722 | const availableEditors = useServerAvailableEditors(); | 1398 | const availableEditors = useServerAvailableEditors(); |
| 1723 | const modelOptionsByProvider = useMemo< | | |
| 1724 | Record<ProviderKind, ReadonlyArray<ServerProvider["models"][number]>> | | |
| 1725 | >( | | |
| 1726 | () => ({ | | |
| 1727 | codex: providerStatuses.find((provider) => provider.provider === "codex")?.models ?? [], | | |
| 1728 | claudeAgent: | | |
| 1729 | providerStatuses.find((provider) => provider.provider === "claudeAgent")?.models ?? [], | | |
| 1730 | }), | | |
| 1731 | [providerStatuses], | | |
| 1732 | ); | | |
| 1733 | const selectedModelForPickerWithCustomFallback = useMemo(() => { | | |
| 1734 | const currentOptions = modelOptionsByProvider[selectedProvider]; | | |
| 1735 | return currentOptions.some((option) => option.slug === selectedModelForPicker) | | |
| 1736 | ? selectedModelForPicker | | |
| 1737 | : (normalizeModelSlug(selectedModelForPicker, selectedProvider) ?? selectedModelForPicker); | | |
| 1738 | }, [modelOptionsByProvider, selectedModelForPicker, selectedProvider]); | | |
| 1739 | const searchableModelOptions = useMemo( | | |
| 1740 | () => | | |
| 1741 | AVAILABLE_PROVIDER_OPTIONS.filter( | | |
| 1742 | (option) => lockedProvider === null || option.value === lockedProvider, | | |
| 1743 | ).flatMap((option) => | | |
| 1744 | modelOptionsByProvider[option.value].map(({ slug, name }) => ({ | | |
| 1745 | provider: option.value, | | |
| 1746 | providerLabel: option.label, | | |
| 1747 | slug, | | |
| 1748 | name, | | |
| 1749 | searchSlug: slug.toLowerCase(), | | |
| 1750 | searchName: name.toLowerCase(), | | |
| 1751 | searchProvider: option.label.toLowerCase(), | | |
| 1752 | })), | | |
| 1753 | ), | | |
| 1754 | [lockedProvider, modelOptionsByProvider], | | |
| 1755 | ); | | |
| 1756 | const workspaceEntriesQuery = useQuery( | | |
| 1757 | projectSearchEntriesQueryOptions({ | | |
| 1758 | environmentId, | | |
| 1759 | cwd: gitCwd, | | |
| 1760 | query: effectivePathQuery, | | |
| 1761 | enabled: isPathTrigger, | | |
| 1762 | limit: 80, | | |
| 1763 | }), | | |
| 1764 | ); | | |
| 1765 | const workspaceEntries = workspaceEntriesQuery.data?.entries ?? EMPTY_PROJECT_ENTRIES; | | |
| 1766 | const composerMenuItems = useMemo<ComposerCommandItem[]>(() => { | | |
| 1767 | if (!composerTrigger) return []; | | |
| 1768 | if (composerTrigger.kind === "path") { | | |
| 1769 | return workspaceEntries.map((entry) => ({ | | |
| 1770 | id: `path:${entry.kind}:${entry.path}`, | | |
| 1771 | type: "path", | | |
| 1772 | path: entry.path, | | |
| 1773 | pathKind: entry.kind, | | |
| 1774 | label: basenameOfPath(entry.path), | | |
| 1775 | description: entry.parentPath ?? "", | | |
| 1776 | })); | | |
| 1777 | } | | |
| 1778 | | | |
| 1779 | if (composerTrigger.kind === "slash-command") { | | |
| 1780 | const slashCommandItems = [ | | |
| 1781 | { | | |
| 1782 | id: "slash:model", | | |
| 1783 | type: "slash-command", | | |
| 1784 | command: "model", | | |
| 1785 | label: "/model", | | |
| 1786 | description: "Switch response model for this thread", | | |
| 1787 | }, | | |
| 1788 | { | | |
| 1789 | id: "slash:plan", | | |
| 1790 | type: "slash-command", | | |
| 1791 | command: "plan", | | |
| 1792 | label: "/plan", | | |
| 1793 | description: "Switch this thread into plan mode", | | |
| 1794 | }, | | |
| 1795 | { | | |
| 1796 | id: "slash:default", | | |
| 1797 | type: "slash-command", | | |
| 1798 | command: "default", | | |
| 1799 | label: "/default", | | |
| 1800 | description: "Switch this thread back to normal build mode", | | |
| 1801 | }, | | |
| 1802 | ] satisfies ReadonlyArray<Extract<ComposerCommandItem, { type: "slash-command" }>>; | | |
| 1803 | const query = composerTrigger.query.trim().toLowerCase(); | | |
| 1804 | if (!query) { | | |
| 1805 | return [...slashCommandItems]; | | |
| 1806 | } | | |
| 1807 | return slashCommandItems.filter( | | |
| 1808 | (item) => item.command.includes(query) || item.label.slice(1).includes(query), | | |
| 1809 | ); | | |
| 1810 | } | | |
| 1811 | | | |
| 1812 | return searchableModelOptions | | |
| 1813 | .filter(({ searchSlug, searchName, searchProvider }) => { | | |
| 1814 | const query = composerTrigger.query.trim().toLowerCase(); | | |
| 1815 | if (!query) return true; | | |
| 1816 | return ( | | |
| 1817 | searchSlug.includes(query) || searchName.includes(query) || searchProvider.includes(query) | | |
| 1818 | ); | | |
| 1819 | }) | | |
| 1820 | .map(({ provider, providerLabel, slug, name }) => ({ | | |
| 1821 | id: `model:${provider}:${slug}`, | | |
| 1822 | type: "model", | | |
| 1823 | provider, | | |
| 1824 | model: slug, | | |
| 1825 | label: name, | | |
| 1826 | description: `${providerLabel} · ${slug}`, | | |
| 1827 | })); | | |
| 1828 | }, [composerTrigger, searchableModelOptions, workspaceEntries]); | | |
| 1829 | const composerMenuOpen = Boolean(composerTrigger); | | |
| 1830 | const activeComposerMenuItem = useMemo( | | |
| 1831 | () => | | |
| 1832 | composerMenuItems.find((item) => item.id === composerHighlightedItemId) ?? | | |
| 1833 | composerMenuItems[0] ?? | | |
| 1834 | null, | | |
| 1835 | [composerHighlightedItemId, composerMenuItems], | | |
| 1836 | ); | | |
| 1837 | composerMenuOpenRef.current = composerMenuOpen; | | |
| 1838 | composerMenuItemsRef.current = composerMenuItems; | | |
| 1839 | activeComposerMenuItemRef.current = activeComposerMenuItem; | | |
| 1840 | const nonPersistedComposerImageIdSet = useMemo( | | |
| 1841 | () => new Set(nonPersistedComposerImageIds), | | |
| 1842 | [nonPersistedComposerImageIds], | | |
| 1843 | ); | | |
| 1844 | const activeProviderStatus = useMemo( | 1399 | const activeProviderStatus = useMemo( |
| 1845 | () => providerStatuses.find((status) => status.provider === selectedProvider) ?? null, | 1400 | () => providerStatuses.find((status) => status.provider === selectedProvider) ?? null, |
| 1846 | [selectedProvider, providerStatuses], | 1401 | [selectedProvider, providerStatuses], |
| ... | @@ -1933,29 +1488,6 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -1933,29 +1488,6 @@ export default function ChatView(props: ChatViewProps) { |
| 1933 | [draftId, envLocked, logicalProjectEnvironments, setDraftThreadContext], | 1488 | [draftId, envLocked, logicalProjectEnvironments, setDraftThreadContext], |
| 1934 | ); | 1489 | ); |
| 1935 | | 1490 | |
| 1936 | // Auto-correct the draft model selection when the environment changes and | | |
| 1937 | // the previously-selected provider/model is no longer available. This keeps | | |
| 1938 | // the stored draft state consistent with the resolved picker values and | | |
| 1939 | // prevents stale model references when the user sends a message. | | |
| 1940 | const prevEnvironmentIdRef = useRef(activeThread?.environmentId); | | |
| 1941 | useEffect(() => { | | |
| 1942 | const currentEnvId = activeThread?.environmentId; | | |
| 1943 | if (!currentEnvId || envLocked || prevEnvironmentIdRef.current === currentEnvId) { | | |
| 1944 | prevEnvironmentIdRef.current = currentEnvId; | | |
| 1945 | return; | | |
| 1946 | } | | |
| 1947 | prevEnvironmentIdRef.current = currentEnvId; | | |
| 1948 | | | |
| 1949 | // The resolved provider/model already account for the new environment's | | |
| 1950 | // provider list. Persist that resolved selection into the draft. | | |
| 1951 | if (activeThread) { | | |
| 1952 | setComposerDraftModelSelection(scopeThreadRef(activeThread.environmentId, activeThread.id), { | | |
| 1953 | provider: selectedProvider, | | |
| 1954 | model: selectedModel, | | |
| 1955 | }); | | |
| 1956 | } | | |
| 1957 | }, [activeThread, envLocked, selectedModel, selectedProvider, setComposerDraftModelSelection]); | | |
| 1958 | | | |
| 1959 | const activeTerminalGroup = | 1491 | const activeTerminalGroup = |
| 1960 | terminalState.terminalGroups.find( | 1492 | terminalState.terminalGroups.find( |
| 1961 | (group) => group.id === terminalState.activeTerminalGroupId, | 1493 | (group) => group.id === terminalState.activeTerminalGroupId, |
| ... | @@ -1994,55 +1526,16 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -1994,55 +1526,16 @@ export default function ChatView(props: ChatViewProps) { |
| 1994 | ); | 1526 | ); |
| 1995 | | 1527 | |
| 1996 | const focusComposer = useCallback(() => { | 1528 | const focusComposer = useCallback(() => { |
| 1997 | composerEditorRef.current?.focusAtEnd(); | 1529 | composerRef.current?.focusAtEnd(); |
| 1998 | }, []); | 1530 | }, []); |
| 1999 | const scheduleComposerFocus = useCallback(() => { | 1531 | const scheduleComposerFocus = useCallback(() => { |
| 2000 | window.requestAnimationFrame(() => { | 1532 | window.requestAnimationFrame(() => { |
| 2001 | focusComposer(); | 1533 | focusComposer(); |
| 2002 | }); | 1534 | }); |
| 2003 | }, [focusComposer]); | 1535 | }, [focusComposer]); |
| 2004 | const addTerminalContextToDraft = useCallback( | 1536 | const addTerminalContextToDraft = useCallback((selection: TerminalContextSelection) => { |
| 2005 | (selection: TerminalContextSelection) => { | 1537 | composerRef.current?.addTerminalContext(selection); |
| 2006 | if (!activeThread) { | 1538 | }, []); |
| 2007 | return; | | |
| 2008 | } | | |
| 2009 | const snapshot = composerEditorRef.current?.readSnapshot() ?? { | | |
| 2010 | value: promptRef.current, | | |
| 2011 | cursor: composerCursor, | | |
| 2012 | expandedCursor: expandCollapsedComposerCursor(promptRef.current, composerCursor), | | |
| 2013 | terminalContextIds: composerTerminalContexts.map((context) => context.id), | | |
| 2014 | }; | | |
| 2015 | const insertion = insertInlineTerminalContextPlaceholder( | | |
| 2016 | snapshot.value, | | |
| 2017 | snapshot.expandedCursor, | | |
| 2018 | ); | | |
| 2019 | const nextCollapsedCursor = collapseExpandedComposerCursor( | | |
| 2020 | insertion.prompt, | | |
| 2021 | insertion.cursor, | | |
| 2022 | ); | | |
| 2023 | const inserted = insertComposerDraftTerminalContext( | | |
| 2024 | scopeThreadRef(activeThread.environmentId, activeThread.id), | | |
| 2025 | insertion.prompt, | | |
| 2026 | { | | |
| 2027 | id: randomUUID(), | | |
| 2028 | threadId: activeThread.id, | | |
| 2029 | createdAt: new Date().toISOString(), | | |
| 2030 | ...selection, | | |
| 2031 | }, | | |
| 2032 | insertion.contextIndex, | | |
| 2033 | ); | | |
| 2034 | if (!inserted) { | | |
| 2035 | return; | | |
| 2036 | } | | |
| 2037 | promptRef.current = insertion.prompt; | | |
| 2038 | setComposerCursor(nextCollapsedCursor); | | |
| 2039 | setComposerTrigger(detectComposerTrigger(insertion.prompt, insertion.cursor)); | | |
| 2040 | window.requestAnimationFrame(() => { | | |
| 2041 | composerEditorRef.current?.focusAt(nextCollapsedCursor); | | |
| 2042 | }); | | |
| 2043 | }, | | |
| 2044 | [activeThread, composerCursor, composerTerminalContexts, insertComposerDraftTerminalContext], | | |
| 2045 | ); | | |
| 2046 | const setTerminalOpen = useCallback( | 1539 | const setTerminalOpen = useCallback( |
| 2047 | (open: boolean) => { | 1540 | (open: boolean) => { |
| 2048 | if (!activeThreadRef) return; | 1541 | if (!activeThreadRef) return; |
| ... | @@ -2600,80 +2093,6 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -2600,80 +2093,6 @@ export default function ChatView(props: ChatViewProps) { |
| 2600 | window.clearTimeout(timeout); | 2093 | window.clearTimeout(timeout); |
| 2601 | }; | 2094 | }; |
| 2602 | }, [activeThread?.id, scheduleStickToBottom]); | 2095 | }, [activeThread?.id, scheduleStickToBottom]); |
| 2603 | useLayoutEffect(() => { | | |
| 2604 | const composerForm = composerFormRef.current; | | |
| 2605 | if (!composerForm) return; | | |
| 2606 | const measureComposerFormWidth = () => composerForm.clientWidth; | | |
| 2607 | const measureFooterCompactness = () => { | | |
| 2608 | const composerFormWidth = measureComposerFormWidth(); | | |
| 2609 | const heuristicFooterCompact = shouldUseCompactComposerFooter(composerFormWidth, { | | |
| 2610 | hasWideActions: composerFooterHasWideActions, | | |
| 2611 | }); | | |
| 2612 | const footer = composerFooterRef.current; | | |
| 2613 | const footerStyle = footer ? window.getComputedStyle(footer) : null; | | |
| 2614 | const footerContentWidth = resolveComposerFooterContentWidth({ | | |
| 2615 | footerWidth: footer?.clientWidth ?? null, | | |
| 2616 | paddingLeft: footerStyle ? Number.parseFloat(footerStyle.paddingLeft) : null, | | |
| 2617 | paddingRight: footerStyle ? Number.parseFloat(footerStyle.paddingRight) : null, | | |
| 2618 | }); | | |
| 2619 | const fitInput = { | | |
| 2620 | footerContentWidth, | | |
| 2621 | leadingContentWidth: composerFooterLeadingRef.current?.scrollWidth ?? null, | | |
| 2622 | actionsWidth: composerFooterActionsRef.current?.scrollWidth ?? null, | | |
| 2623 | }; | | |
| 2624 | const nextFooterCompact = | | |
| 2625 | heuristicFooterCompact || shouldForceCompactComposerFooterForFit(fitInput); | | |
| 2626 | const nextPrimaryActionsCompact = | | |
| 2627 | nextFooterCompact && | | |
| 2628 | shouldUseCompactComposerPrimaryActions(composerFormWidth, { | | |
| 2629 | hasWideActions: composerFooterHasWideActions, | | |
| 2630 | }); | | |
| 2631 | | | |
| 2632 | return { | | |
| 2633 | primaryActionsCompact: nextPrimaryActionsCompact, | | |
| 2634 | footerCompact: nextFooterCompact, | | |
| 2635 | }; | | |
| 2636 | }; | | |
| 2637 | | | |
| 2638 | composerFormHeightRef.current = composerForm.getBoundingClientRect().height; | | |
| 2639 | const initialCompactness = measureFooterCompactness(); | | |
| 2640 | setIsComposerPrimaryActionsCompact(initialCompactness.primaryActionsCompact); | | |
| 2641 | setIsComposerFooterCompact(initialCompactness.footerCompact); | | |
| 2642 | if (typeof ResizeObserver === "undefined") return; | | |
| 2643 | | | |
| 2644 | const observer = new ResizeObserver((entries) => { | | |
| 2645 | const [entry] = entries; | | |
| 2646 | if (!entry) return; | | |
| 2647 | | | |
| 2648 | const nextCompactness = measureFooterCompactness(); | | |
| 2649 | setIsComposerPrimaryActionsCompact((previous) => | | |
| 2650 | previous === nextCompactness.primaryActionsCompact | | |
| 2651 | ? previous | | |
| 2652 | : nextCompactness.primaryActionsCompact, | | |
| 2653 | ); | | |
| 2654 | setIsComposerFooterCompact((previous) => | | |
| 2655 | previous === nextCompactness.footerCompact ? previous : nextCompactness.footerCompact, | | |
| 2656 | ); | | |
| 2657 | | | |
| 2658 | const nextHeight = entry.contentRect.height; | | |
| 2659 | const previousHeight = composerFormHeightRef.current; | | |
| 2660 | composerFormHeightRef.current = nextHeight; | | |
| 2661 | | | |
| 2662 | if (previousHeight > 0 && Math.abs(nextHeight - previousHeight) < 0.5) return; | | |
| 2663 | if (!shouldAutoScrollRef.current) return; | | |
| 2664 | scheduleStickToBottom(); | | |
| 2665 | }); | | |
| 2666 | | | |
| 2667 | observer.observe(composerForm); | | |
| 2668 | return () => { | | |
| 2669 | observer.disconnect(); | | |
| 2670 | }; | | |
| 2671 | }, [ | | |
| 2672 | activeThread?.id, | | |
| 2673 | composerFooterActionLayoutKey, | | |
| 2674 | composerFooterHasWideActions, | | |
| 2675 | scheduleStickToBottom, | | |
| 2676 | ]); | | |
| 2677 | useEffect(() => { | 2096 | useEffect(() => { |
| 2678 | if (!shouldAutoScrollRef.current) return; | 2097 | if (!shouldAutoScrollRef.current) return; |
| 2679 | scheduleStickToBottom(); | 2098 | scheduleStickToBottom(); |
| ... | @@ -2696,18 +2115,6 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -2696,18 +2115,6 @@ export default function ChatView(props: ChatViewProps) { |
| 2696 | planSidebarDismissedForTurnRef.current = null; | 2115 | planSidebarDismissedForTurnRef.current = null; |
| 2697 | }, [activeThread?.id]); | 2116 | }, [activeThread?.id]); |
| 2698 | | 2117 | |
| 2699 | useEffect(() => { | | |
| 2700 | if (!composerMenuOpen) { | | |
| 2701 | setComposerHighlightedItemId(null); | | |
| 2702 | return; | | |
| 2703 | } | | |
| 2704 | setComposerHighlightedItemId((existing) => | | |
| 2705 | existing && composerMenuItems.some((item) => item.id === existing) | | |
| 2706 | ? existing | | |
| 2707 | : (composerMenuItems[0]?.id ?? null), | | |
| 2708 | ); | | |
| 2709 | }, [composerMenuItems, composerMenuOpen]); | | |
| 2710 | | | |
| 2711 | useEffect(() => { | 2118 | useEffect(() => { |
| 2712 | setIsRevertingCheckpoint(false); | 2119 | setIsRevertingCheckpoint(false); |
| 2713 | }, [activeThread?.id]); | 2120 | }, [activeThread?.id]); |
| ... | @@ -2722,14 +2129,6 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -2722,14 +2129,6 @@ export default function ChatView(props: ChatViewProps) { |
| 2722 | }; | 2129 | }; |
| 2723 | }, [activeThread?.id, focusComposer, terminalState.terminalOpen]); | 2130 | }, [activeThread?.id, focusComposer, terminalState.terminalOpen]); |
| 2724 | | 2131 | |
| 2725 | useEffect(() => { | | |
| 2726 | composerImagesRef.current = composerImages; | | |
| 2727 | }, [composerImages]); | | |
| 2728 | | | |
| 2729 | useEffect(() => { | | |
| 2730 | composerTerminalContextsRef.current = composerTerminalContexts; | | |
| 2731 | }, [composerTerminalContexts]); | | |
| 2732 | | | |
| 2733 | useEffect(() => { | 2132 | useEffect(() => { |
| 2734 | if (!activeThread?.id) return; | 2133 | if (!activeThread?.id) return; |
| 2735 | if (activeThread.messages.length === 0) { | 2134 | if (activeThread.messages.length === 0) { |
| ... | @@ -2758,11 +2157,6 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -2758,11 +2157,6 @@ export default function ChatView(props: ChatViewProps) { |
| 2758 | }; | 2157 | }; |
| 2759 | }, [activeThread?.id, activeThread?.messages, handoffAttachmentPreviews, optimisticUserMessages]); | 2158 | }, [activeThread?.id, activeThread?.messages, handoffAttachmentPreviews, optimisticUserMessages]); |
| 2760 | | 2159 | |
| 2761 | useEffect(() => { | | |
| 2762 | promptRef.current = prompt; | | |
| 2763 | setComposerCursor((existing) => clampCollapsedComposerCursor(prompt, existing)); | | |
| 2764 | }, [prompt]); | | |
| 2765 | | | |
| 2766 | useEffect(() => { | 2160 | useEffect(() => { |
| 2767 | setOptimisticUserMessages((existing) => { | 2161 | setOptimisticUserMessages((existing) => { |
| 2768 | for (const message of existing) { | 2162 | for (const message of existing) { |
| ... | @@ -2771,128 +2165,12 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -2771,128 +2165,12 @@ export default function ChatView(props: ChatViewProps) { |
| 2771 | return []; | 2165 | return []; |
| 2772 | }); | 2166 | }); |
| 2773 | resetLocalDispatch(); | 2167 | resetLocalDispatch(); |
| 2774 | setComposerHighlightedItemId(null); | | |
| 2775 | setComposerCursor(collapseExpandedComposerCursor(promptRef.current, promptRef.current.length)); | | |
| 2776 | setComposerTrigger(detectComposerTrigger(promptRef.current, promptRef.current.length)); | | |
| 2777 | dragDepthRef.current = 0; | | |
| 2778 | setIsDragOverComposer(false); | | |
| 2779 | setExpandedImage(null); | 2168 | setExpandedImage(null); |
| 2780 | }, [draftId, resetLocalDispatch, threadId]); | 2169 | }, [draftId, resetLocalDispatch, threadId]); |
| 2781 | | 2170 | |
| 2782 | useEffect(() => { | | |
| 2783 | let cancelled = false; | | |
| 2784 | void (async () => { | | |
| 2785 | if (composerImages.length === 0) { | | |
| 2786 | clearComposerDraftPersistedAttachments(composerDraftTarget); | | |
| 2787 | return; | | |
| 2788 | } | | |
| 2789 | const getPersistedAttachmentsForThread = () => | | |
| 2790 | getComposerDraft(composerDraftTarget)?.persistedAttachments ?? []; | | |
| 2791 | try { | | |
| 2792 | const currentPersistedAttachments = getPersistedAttachmentsForThread(); | | |
| 2793 | const existingPersistedById = new Map( | | |
| 2794 | currentPersistedAttachments.map((attachment) => [attachment.id, attachment]), | | |
| 2795 | ); | | |
| 2796 | const stagedAttachmentById = new Map<string, PersistedComposerImageAttachment>(); | | |
| 2797 | await Promise.all( | | |
| 2798 | composerImages.map(async (image) => { | | |
| 2799 | try { | | |
| 2800 | const dataUrl = await readFileAsDataUrl(image.file); | | |
| 2801 | stagedAttachmentById.set(image.id, { | | |
| 2802 | id: image.id, | | |
| 2803 | name: image.name, | | |
| 2804 | mimeType: image.mimeType, | | |
| 2805 | sizeBytes: image.sizeBytes, | | |
| 2806 | dataUrl, | | |
| 2807 | }); | | |
| 2808 | } catch { | | |
| 2809 | const existingPersisted = existingPersistedById.get(image.id); | | |
| 2810 | if (existingPersisted) { | | |
| 2811 | stagedAttachmentById.set(image.id, existingPersisted); | | |
| 2812 | } | | |
| 2813 | } | | |
| 2814 | }), | | |
| 2815 | ); | | |
| 2816 | const serialized = Array.from(stagedAttachmentById.values()); | | |
| 2817 | if (cancelled) { | | |
| 2818 | return; | | |
| 2819 | } | | |
| 2820 | // Stage attachments in persisted draft state first so persist middleware can write them. | | |
| 2821 | syncComposerDraftPersistedAttachments(composerDraftTarget, serialized); | | |
| 2822 | } catch { | | |
| 2823 | const currentImageIds = new Set(composerImages.map((image) => image.id)); | | |
| 2824 | const fallbackPersistedAttachments = getPersistedAttachmentsForThread(); | | |
| 2825 | const fallbackPersistedIds = fallbackPersistedAttachments | | |
| 2826 | .map((attachment) => attachment.id) | | |
| 2827 | .filter((id) => currentImageIds.has(id)); | | |
| 2828 | const fallbackPersistedIdSet = new Set(fallbackPersistedIds); | | |
| 2829 | const fallbackAttachments = fallbackPersistedAttachments.filter((attachment) => | | |
| 2830 | fallbackPersistedIdSet.has(attachment.id), | | |
| 2831 | ); | | |
| 2832 | if (cancelled) { | | |
| 2833 | return; | | |
| 2834 | } | | |
| 2835 | syncComposerDraftPersistedAttachments(composerDraftTarget, fallbackAttachments); | | |
| 2836 | } | | |
| 2837 | })(); | | |
| 2838 | return () => { | | |
| 2839 | cancelled = true; | | |
| 2840 | }; | | |
| 2841 | }, [ | | |
| 2842 | composerDraftTarget, | | |
| 2843 | clearComposerDraftPersistedAttachments, | | |
| 2844 | composerImages, | | |
| 2845 | getComposerDraft, | | |
| 2846 | syncComposerDraftPersistedAttachments, | | |
| 2847 | ]); | | |
| 2848 | | | |
| 2849 | const closeExpandedImage = useCallback(() => { | 2171 | const closeExpandedImage = useCallback(() => { |
| 2850 | setExpandedImage(null); | 2172 | setExpandedImage(null); |
| 2851 | }, []); | 2173 | }, []); |
| 2852 | const navigateExpandedImage = useCallback((direction: -1 | 1) => { | | |
| 2853 | setExpandedImage((existing) => { | | |
| 2854 | if (!existing || existing.images.length <= 1) { | | |
| 2855 | return existing; | | |
| 2856 | } | | |
| 2857 | const nextIndex = | | |
| 2858 | (existing.index + direction + existing.images.length) % existing.images.length; | | |
| 2859 | if (nextIndex === existing.index) { | | |
| 2860 | return existing; | | |
| 2861 | } | | |
| 2862 | return { ...existing, index: nextIndex }; | | |
| 2863 | }); | | |
| 2864 | }, []); | | |
| 2865 | | | |
| 2866 | useEffect(() => { | | |
| 2867 | if (!expandedImage) { | | |
| 2868 | return; | | |
| 2869 | } | | |
| 2870 | | | |
| 2871 | const onKeyDown = (event: globalThis.KeyboardEvent) => { | | |
| 2872 | if (event.key === "Escape") { | | |
| 2873 | event.preventDefault(); | | |
| 2874 | event.stopPropagation(); | | |
| 2875 | closeExpandedImage(); | | |
| 2876 | return; | | |
| 2877 | } | | |
| 2878 | if (expandedImage.images.length <= 1) { | | |
| 2879 | return; | | |
| 2880 | } | | |
| 2881 | if (event.key === "ArrowLeft") { | | |
| 2882 | event.preventDefault(); | | |
| 2883 | event.stopPropagation(); | | |
| 2884 | navigateExpandedImage(-1); | | |
| 2885 | return; | | |
| 2886 | } | | |
| 2887 | if (event.key !== "ArrowRight") return; | | |
| 2888 | event.preventDefault(); | | |
| 2889 | event.stopPropagation(); | | |
| 2890 | navigateExpandedImage(1); | | |
| 2891 | }; | | |
| 2892 | | | |
| 2893 | window.addEventListener("keydown", onKeyDown); | | |
| 2894 | return () => window.removeEventListener("keydown", onKeyDown); | | |
| 2895 | }, [closeExpandedImage, expandedImage, navigateExpandedImage]); | | |
| 2896 | | 2174 | |
| 2897 | const activeWorktreePath = activeThread?.worktreePath ?? null; | 2175 | const activeWorktreePath = activeThread?.worktreePath ?? null; |
| 2898 | const envMode: DraftThreadEnvMode = resolveEffectiveEnvMode({ | 2176 | const envMode: DraftThreadEnvMode = resolveEffectiveEnvMode({ |
| ... | @@ -3097,117 +2375,6 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3097,117 +2375,6 @@ export default function ChatView(props: ChatViewProps) { |
| 3097 | toggleTerminalVisibility, | 2375 | toggleTerminalVisibility, |
| 3098 | ]); | 2376 | ]); |
| 3099 | | 2377 | |
| 3100 | const addComposerImages = (files: File[]) => { | | |
| 3101 | if (!activeThreadId || files.length === 0) return; | | |
| 3102 | | | |
| 3103 | if (pendingUserInputs.length > 0) { | | |
| 3104 | toastManager.add({ | | |
| 3105 | type: "error", | | |
| 3106 | title: "Attach images after answering plan questions.", | | |
| 3107 | }); | | |
| 3108 | return; | | |
| 3109 | } | | |
| 3110 | | | |
| 3111 | const nextImages: ComposerImageAttachment[] = []; | | |
| 3112 | let nextImageCount = composerImagesRef.current.length; | | |
| 3113 | let error: string | null = null; | | |
| 3114 | for (const file of files) { | | |
| 3115 | if (!file.type.startsWith("image/")) { | | |
| 3116 | error = `Unsupported file type for '${file.name}'. Please attach image files only.`; | | |
| 3117 | continue; | | |
| 3118 | } | | |
| 3119 | if (file.size > PROVIDER_SEND_TURN_MAX_IMAGE_BYTES) { | | |
| 3120 | error = `'${file.name}' exceeds the ${IMAGE_SIZE_LIMIT_LABEL} attachment limit.`; | | |
| 3121 | continue; | | |
| 3122 | } | | |
| 3123 | if (nextImageCount >= PROVIDER_SEND_TURN_MAX_ATTACHMENTS) { | | |
| 3124 | error = `You can attach up to ${PROVIDER_SEND_TURN_MAX_ATTACHMENTS} images per message.`; | | |
| 3125 | break; | | |
| 3126 | } | | |
| 3127 | | | |
| 3128 | const previewUrl = URL.createObjectURL(file); | | |
| 3129 | nextImages.push({ | | |
| 3130 | type: "image", | | |
| 3131 | id: randomUUID(), | | |
| 3132 | name: file.name || "image", | | |
| 3133 | mimeType: file.type, | | |
| 3134 | sizeBytes: file.size, | | |
| 3135 | previewUrl, | | |
| 3136 | file, | | |
| 3137 | }); | | |
| 3138 | nextImageCount += 1; | | |
| 3139 | } | | |
| 3140 | | | |
| 3141 | if (nextImages.length === 1 && nextImages[0]) { | | |
| 3142 | addComposerImage(nextImages[0]); | | |
| 3143 | } else if (nextImages.length > 1) { | | |
| 3144 | addComposerImagesToDraft(nextImages); | | |
| 3145 | } | | |
| 3146 | setThreadError(activeThreadId, error); | | |
| 3147 | }; | | |
| 3148 | | | |
| 3149 | const removeComposerImage = (imageId: string) => { | | |
| 3150 | removeComposerImageFromDraft(imageId); | | |
| 3151 | }; | | |
| 3152 | | | |
| 3153 | const onComposerPaste = (event: React.ClipboardEvent<HTMLElement>) => { | | |
| 3154 | const files = Array.from(event.clipboardData.files); | | |
| 3155 | if (files.length === 0) { | | |
| 3156 | return; | | |
| 3157 | } | | |
| 3158 | const imageFiles = files.filter((file) => file.type.startsWith("image/")); | | |
| 3159 | if (imageFiles.length === 0) { | | |
| 3160 | return; | | |
| 3161 | } | | |
| 3162 | event.preventDefault(); | | |
| 3163 | addComposerImages(imageFiles); | | |
| 3164 | }; | | |
| 3165 | | | |
| 3166 | const onComposerDragEnter = (event: React.DragEvent<HTMLDivElement>) => { | | |
| 3167 | if (!event.dataTransfer.types.includes("Files")) { | | |
| 3168 | return; | | |
| 3169 | } | | |
| 3170 | event.preventDefault(); | | |
| 3171 | dragDepthRef.current += 1; | | |
| 3172 | setIsDragOverComposer(true); | | |
| 3173 | }; | | |
| 3174 | | | |
| 3175 | const onComposerDragOver = (event: React.DragEvent<HTMLDivElement>) => { | | |
| 3176 | if (!event.dataTransfer.types.includes("Files")) { | | |
| 3177 | return; | | |
| 3178 | } | | |
| 3179 | event.preventDefault(); | | |
| 3180 | event.dataTransfer.dropEffect = "copy"; | | |
| 3181 | setIsDragOverComposer(true); | | |
| 3182 | }; | | |
| 3183 | | | |
| 3184 | const onComposerDragLeave = (event: React.DragEvent<HTMLDivElement>) => { | | |
| 3185 | if (!event.dataTransfer.types.includes("Files")) { | | |
| 3186 | return; | | |
| 3187 | } | | |
| 3188 | event.preventDefault(); | | |
| 3189 | const nextTarget = event.relatedTarget; | | |
| 3190 | if (nextTarget instanceof Node && event.currentTarget.contains(nextTarget)) { | | |
| 3191 | return; | | |
| 3192 | } | | |
| 3193 | dragDepthRef.current = Math.max(0, dragDepthRef.current - 1); | | |
| 3194 | if (dragDepthRef.current === 0) { | | |
| 3195 | setIsDragOverComposer(false); | | |
| 3196 | } | | |
| 3197 | }; | | |
| 3198 | | | |
| 3199 | const onComposerDrop = (event: React.DragEvent<HTMLDivElement>) => { | | |
| 3200 | if (!event.dataTransfer.types.includes("Files")) { | | |
| 3201 | return; | | |
| 3202 | } | | |
| 3203 | event.preventDefault(); | | |
| 3204 | dragDepthRef.current = 0; | | |
| 3205 | setIsDragOverComposer(false); | | |
| 3206 | const files = Array.from(event.dataTransfer.files); | | |
| 3207 | addComposerImages(files); | | |
| 3208 | focusComposer(); | | |
| 3209 | }; | | |
| 3210 | | | |
| 3211 | const onRevertToTurnCount = useCallback( | 2378 | const onRevertToTurnCount = useCallback( |
| 3212 | async (turnCount: number) => { | 2379 | async (turnCount: number) => { |
| 3213 | const api = readEnvironmentApi(environmentId); | 2380 | const api = readEnvironmentApi(environmentId); |
| ... | @@ -3266,6 +2433,17 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3266,6 +2433,17 @@ export default function ChatView(props: ChatViewProps) { |
| 3266 | onAdvanceActivePendingUserInput(); | 2433 | onAdvanceActivePendingUserInput(); |
| 3267 | return; | 2434 | return; |
| 3268 | } | 2435 | } |
| | 2436 | const sendCtx = composerRef.current?.getSendContext(); |
| | 2437 | if (!sendCtx) return; |
| | 2438 | const { |
| | 2439 | images: composerImages, |
| | 2440 | terminalContexts: composerTerminalContexts, |
| | 2441 | selectedProvider: ctxSelectedProvider, |
| | 2442 | selectedModel: ctxSelectedModel, |
| | 2443 | selectedProviderModels: ctxSelectedProviderModels, |
| | 2444 | selectedPromptEffort: ctxSelectedPromptEffort, |
| | 2445 | selectedModelSelection: ctxSelectedModelSelection, |
| | 2446 | } = sendCtx; |
| 3269 | const promptForSend = promptRef.current; | 2447 | const promptForSend = promptRef.current; |
| 3270 | const { | 2448 | const { |
| 3271 | trimmedPrompt: trimmed, | 2449 | trimmedPrompt: trimmed, |
| ... | @@ -3283,10 +2461,8 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3283,10 +2461,8 @@ export default function ChatView(props: ChatViewProps) { |
| 3283 | planMarkdown: activeProposedPlan.planMarkdown, | 2461 | planMarkdown: activeProposedPlan.planMarkdown, |
| 3284 | }); | 2462 | }); |
| 3285 | promptRef.current = ""; | 2463 | promptRef.current = ""; |
| 3286 | clearComposerDraftContent(scopeThreadRef(activeThread.environmentId, activeThread.id)); | 2464 | clearComposerDraftContent(composerDraftTarget); |
| 3287 | setComposerHighlightedItemId(null); | 2465 | composerRef.current?.resetCursorState(); |
| 3288 | setComposerCursor(0); | | |
| 3289 | setComposerTrigger(null); | | |
| 3290 | await onSubmitPlanFollowUp({ | 2466 | await onSubmitPlanFollowUp({ |
| 3291 | text: followUp.text, | 2467 | text: followUp.text, |
| 3292 | interactionMode: followUp.interactionMode, | 2468 | interactionMode: followUp.interactionMode, |
| ... | @@ -3300,10 +2476,8 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3300,10 +2476,8 @@ export default function ChatView(props: ChatViewProps) { |
| 3300 | if (standaloneSlashCommand) { | 2476 | if (standaloneSlashCommand) { |
| 3301 | handleInteractionModeChange(standaloneSlashCommand); | 2477 | handleInteractionModeChange(standaloneSlashCommand); |
| 3302 | promptRef.current = ""; | 2478 | promptRef.current = ""; |
| 3303 | clearComposerDraftContent(scopeThreadRef(activeThread.environmentId, activeThread.id)); | 2479 | clearComposerDraftContent(composerDraftTarget); |
| 3304 | setComposerHighlightedItemId(null); | 2480 | composerRef.current?.resetCursorState(); |
| 3305 | setComposerCursor(0); | | |
| 3306 | setComposerTrigger(null); | | |
| 3307 | return; | 2481 | return; |
| 3308 | } | 2482 | } |
| 3309 | if (!hasSendableContent) { | 2483 | if (!hasSendableContent) { |
| ... | @@ -3349,10 +2523,10 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3349,10 +2523,10 @@ export default function ChatView(props: ChatViewProps) { |
| 3349 | const messageIdForSend = newMessageId(); | 2523 | const messageIdForSend = newMessageId(); |
| 3350 | const messageCreatedAt = new Date().toISOString(); | 2524 | const messageCreatedAt = new Date().toISOString(); |
| 3351 | const outgoingMessageText = formatOutgoingPrompt({ | 2525 | const outgoingMessageText = formatOutgoingPrompt({ |
| 3352 | provider: selectedProvider, | 2526 | provider: ctxSelectedProvider, |
| 3353 | model: selectedModel, | 2527 | model: ctxSelectedModel, |
| 3354 | models: selectedProviderModels, | 2528 | models: ctxSelectedProviderModels, |
| 3355 | effort: selectedPromptEffort, | 2529 | effort: ctxSelectedPromptEffort, |
| 3356 | text: messageTextForSend || IMAGE_ONLY_BOOTSTRAP_PROMPT, | 2530 | text: messageTextForSend || IMAGE_ONLY_BOOTSTRAP_PROMPT, |
| 3357 | }); | 2531 | }); |
| 3358 | const turnAttachmentsPromise = Promise.all( | 2532 | const turnAttachmentsPromise = Promise.all( |
| ... | @@ -3400,10 +2574,8 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3400,10 +2574,8 @@ export default function ChatView(props: ChatViewProps) { |
| 3400 | }); | 2574 | }); |
| 3401 | } | 2575 | } |
| 3402 | promptRef.current = ""; | 2576 | promptRef.current = ""; |
| 3403 | clearComposerDraftContent(scopeThreadRef(activeThread.environmentId, threadIdForSend)); | 2577 | clearComposerDraftContent(composerDraftTarget); |
| 3404 | setComposerHighlightedItemId(null); | 2578 | composerRef.current?.resetCursorState(); |
| 3405 | setComposerCursor(0); | | |
| 3406 | setComposerTrigger(null); | | |
| 3407 | | 2579 | |
| 3408 | let turnStartSucceeded = false; | 2580 | let turnStartSucceeded = false; |
| 3409 | await (async () => { | 2581 | await (async () => { |
| ... | @@ -3426,12 +2598,14 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3426,12 +2598,14 @@ export default function ChatView(props: ChatViewProps) { |
| 3426 | } | 2598 | } |
| 3427 | const title = truncate(titleSeed); | 2599 | const title = truncate(titleSeed); |
| 3428 | const threadCreateModelSelection: ModelSelection = { | 2600 | const threadCreateModelSelection: ModelSelection = { |
| 3429 | provider: selectedProvider, | 2601 | provider: ctxSelectedProvider, |
| 3430 | model: | 2602 | model: |
| 3431 | selectedModel || | 2603 | ctxSelectedModel || |
| 3432 | activeProject.defaultModelSelection?.model || | 2604 | activeProject.defaultModelSelection?.model || |
| 3433 | DEFAULT_MODEL_BY_PROVIDER.codex, | 2605 | DEFAULT_MODEL_BY_PROVIDER.codex, |
| 3434 | ...(selectedModelSelection.options ? { options: selectedModelSelection.options } : {}), | 2606 | ...(ctxSelectedModelSelection.options |
| | 2607 | ? { options: ctxSelectedModelSelection.options } |
| | 2608 | : {}), |
| 3435 | }; | 2609 | }; |
| 3436 | | 2610 | |
| 3437 | // Auto-title from first message | 2611 | // Auto-title from first message |
| ... | @@ -3448,7 +2622,7 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3448,7 +2622,7 @@ export default function ChatView(props: ChatViewProps) { |
| 3448 | await persistThreadSettingsForNextTurn({ | 2622 | await persistThreadSettingsForNextTurn({ |
| 3449 | threadId: threadIdForSend, | 2623 | threadId: threadIdForSend, |
| 3450 | createdAt: messageCreatedAt, | 2624 | createdAt: messageCreatedAt, |
| 3451 | ...(selectedModel ? { modelSelection: selectedModelSelection } : {}), | 2625 | ...(ctxSelectedModel ? { modelSelection: ctxSelectedModelSelection } : {}), |
| 3452 | runtimeMode, | 2626 | runtimeMode, |
| 3453 | interactionMode, | 2627 | interactionMode, |
| 3454 | }); | 2628 | }); |
| ... | @@ -3495,7 +2669,7 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3495,7 +2669,7 @@ export default function ChatView(props: ChatViewProps) { |
| 3495 | text: outgoingMessageText, | 2669 | text: outgoingMessageText, |
| 3496 | attachments: turnAttachments, | 2670 | attachments: turnAttachments, |
| 3497 | }, | 2671 | }, |
| 3498 | modelSelection: selectedModelSelection, | 2672 | modelSelection: ctxSelectedModelSelection, |
| 3499 | titleSeed: title, | 2673 | titleSeed: title, |
| 3500 | runtimeMode, | 2674 | runtimeMode, |
| 3501 | interactionMode, | 2675 | interactionMode, |
| ... | @@ -3519,11 +2693,17 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3519,11 +2693,17 @@ export default function ChatView(props: ChatViewProps) { |
| 3519 | return next.length === existing.length ? existing : next; | 2693 | return next.length === existing.length ? existing : next; |
| 3520 | }); | 2694 | }); |
| 3521 | promptRef.current = promptForSend; | 2695 | promptRef.current = promptForSend; |
| 3522 | setPrompt(promptForSend); | 2696 | const retryComposerImages = composerImagesSnapshot.map(cloneComposerImageForRetry); |
| 3523 | setComposerCursor(collapseExpandedComposerCursor(promptForSend, promptForSend.length)); | 2697 | composerImagesRef.current = retryComposerImages; |
| 3524 | addComposerImagesToDraft(composerImagesSnapshot.map(cloneComposerImageForRetry)); | 2698 | composerTerminalContextsRef.current = composerTerminalContextsSnapshot; |
| 3525 | addComposerTerminalContextsToDraft(composerTerminalContextsSnapshot); | 2699 | setComposerDraftPrompt(composerDraftTarget, promptForSend); |
| 3526 | setComposerTrigger(detectComposerTrigger(promptForSend, promptForSend.length)); | 2700 | addComposerDraftImages(composerDraftTarget, retryComposerImages); |
| | 2701 | setComposerDraftTerminalContexts(composerDraftTarget, composerTerminalContextsSnapshot); |
| | 2702 | composerRef.current?.resetCursorState({ |
| | 2703 | cursor: collapseExpandedComposerCursor(promptForSend, promptForSend.length), |
| | 2704 | prompt: promptForSend, |
| | 2705 | detectTrigger: true, |
| | 2706 | }); |
| 3527 | } | 2707 | } |
| 3528 | setThreadError( | 2708 | setThreadError( |
| 3529 | threadIdForSend, | 2709 | threadIdForSend, |
| ... | @@ -3644,8 +2824,7 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3644,8 +2824,7 @@ export default function ChatView(props: ChatViewProps) { |
| 3644 | }; | 2824 | }; |
| 3645 | }); | 2825 | }); |
| 3646 | promptRef.current = ""; | 2826 | promptRef.current = ""; |
| 3647 | setComposerCursor(0); | 2827 | composerRef.current?.resetCursorState({ cursor: 0 }); |
| 3648 | setComposerTrigger(null); | | |
| 3649 | }, | 2828 | }, |
| 3650 | [activePendingProgress?.activeQuestion, activePendingUserInput], | 2829 | [activePendingProgress?.activeQuestion, activePendingUserInput], |
| 3651 | ); | 2830 | ); |
| ... | @@ -3656,7 +2835,7 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3656,7 +2835,7 @@ export default function ChatView(props: ChatViewProps) { |
| 3656 | value: string, | 2835 | value: string, |
| 3657 | nextCursor: number, | 2836 | nextCursor: number, |
| 3658 | expandedCursor: number, | 2837 | expandedCursor: number, |
| 3659 | cursorAdjacentToMention: boolean, | 2838 | _cursorAdjacentToMention: boolean, |
| 3660 | ) => { | 2839 | ) => { |
| 3661 | if (!activePendingUserInput) { | 2840 | if (!activePendingUserInput) { |
| 3662 | return; | 2841 | return; |
| ... | @@ -3672,10 +2851,14 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3672,10 +2851,14 @@ export default function ChatView(props: ChatViewProps) { |
| 3672 | ), | 2851 | ), |
| 3673 | }, | 2852 | }, |
| 3674 | })); | 2853 | })); |
| 3675 | setComposerCursor(nextCursor); | 2854 | const snapshot = composerRef.current?.readSnapshot(); |
| 3676 | setComposerTrigger( | 2855 | if ( |
| 3677 | cursorAdjacentToMention ? null : detectComposerTrigger(value, expandedCursor), | 2856 | snapshot?.value !== value || |
| 3678 | ); | 2857 | snapshot.cursor !== nextCursor || |
| | 2858 | snapshot.expandedCursor !== expandedCursor |
| | 2859 | ) { |
| | 2860 | composerRef.current?.focusAt(nextCursor); |
| | 2861 | } |
| 3679 | }, | 2862 | }, |
| 3680 | [activePendingUserInput], | 2863 | [activePendingUserInput], |
| 3681 | ); | 2864 | ); |
| ... | @@ -3731,14 +2914,26 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3731,14 +2914,26 @@ export default function ChatView(props: ChatViewProps) { |
| 3731 | return; | 2914 | return; |
| 3732 | } | 2915 | } |
| 3733 | | 2916 | |
| | 2917 | const sendCtx = composerRef.current?.getSendContext(); |
| | 2918 | if (!sendCtx) { |
| | 2919 | return; |
| | 2920 | } |
| | 2921 | const { |
| | 2922 | selectedProvider: ctxSelectedProvider, |
| | 2923 | selectedModel: ctxSelectedModel, |
| | 2924 | selectedProviderModels: ctxSelectedProviderModels, |
| | 2925 | selectedPromptEffort: ctxSelectedPromptEffort, |
| | 2926 | selectedModelSelection: ctxSelectedModelSelection, |
| | 2927 | } = sendCtx; |
| | 2928 | |
| 3734 | const threadIdForSend = activeThread.id; | 2929 | const threadIdForSend = activeThread.id; |
| 3735 | const messageIdForSend = newMessageId(); | 2930 | const messageIdForSend = newMessageId(); |
| 3736 | const messageCreatedAt = new Date().toISOString(); | 2931 | const messageCreatedAt = new Date().toISOString(); |
| 3737 | const outgoingMessageText = formatOutgoingPrompt({ | 2932 | const outgoingMessageText = formatOutgoingPrompt({ |
| 3738 | provider: selectedProvider, | 2933 | provider: ctxSelectedProvider, |
| 3739 | model: selectedModel, | 2934 | model: ctxSelectedModel, |
| 3740 | models: selectedProviderModels, | 2935 | models: ctxSelectedProviderModels, |
| 3741 | effort: selectedPromptEffort, | 2936 | effort: ctxSelectedPromptEffort, |
| 3742 | text: trimmed, | 2937 | text: trimmed, |
| 3743 | }); | 2938 | }); |
| 3744 | | 2939 | |
| ... | @@ -3762,7 +2957,7 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3762,7 +2957,7 @@ export default function ChatView(props: ChatViewProps) { |
| 3762 | await persistThreadSettingsForNextTurn({ | 2957 | await persistThreadSettingsForNextTurn({ |
| 3763 | threadId: threadIdForSend, | 2958 | threadId: threadIdForSend, |
| 3764 | createdAt: messageCreatedAt, | 2959 | createdAt: messageCreatedAt, |
| 3765 | modelSelection: selectedModelSelection, | 2960 | modelSelection: ctxSelectedModelSelection, |
| 3766 | runtimeMode, | 2961 | runtimeMode, |
| 3767 | interactionMode: nextInteractionMode, | 2962 | interactionMode: nextInteractionMode, |
| 3768 | }); | 2963 | }); |
| ... | @@ -3784,7 +2979,7 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3784,7 +2979,7 @@ export default function ChatView(props: ChatViewProps) { |
| 3784 | text: outgoingMessageText, | 2979 | text: outgoingMessageText, |
| 3785 | attachments: [], | 2980 | attachments: [], |
| 3786 | }, | 2981 | }, |
| 3787 | modelSelection: selectedModelSelection, | 2982 | modelSelection: ctxSelectedModelSelection, |
| 3788 | titleSeed: activeThread.title, | 2983 | titleSeed: activeThread.title, |
| 3789 | runtimeMode, | 2984 | runtimeMode, |
| 3790 | interactionMode: nextInteractionMode, | 2985 | interactionMode: nextInteractionMode, |
| ... | @@ -3829,13 +3024,8 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3829,13 +3024,8 @@ export default function ChatView(props: ChatViewProps) { |
| 3829 | persistThreadSettingsForNextTurn, | 3024 | persistThreadSettingsForNextTurn, |
| 3830 | resetLocalDispatch, | 3025 | resetLocalDispatch, |
| 3831 | runtimeMode, | 3026 | runtimeMode, |
| 3832 | selectedPromptEffort, | | |
| 3833 | selectedModelSelection, | | |
| 3834 | selectedProvider, | | |
| 3835 | selectedProviderModels, | | |
| 3836 | setComposerDraftInteractionMode, | 3027 | setComposerDraftInteractionMode, |
| 3837 | setThreadError, | 3028 | setThreadError, |
| 3838 | selectedModel, | | |
| 3839 | environmentId, | 3029 | environmentId, |
| 3840 | ], | 3030 | ], |
| 3841 | ); | 3031 | ); |
| ... | @@ -3855,19 +3045,31 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3855,19 +3045,31 @@ export default function ChatView(props: ChatViewProps) { |
| 3855 | return; | 3045 | return; |
| 3856 | } | 3046 | } |
| 3857 | | 3047 | |
| | 3048 | const sendCtx = composerRef.current?.getSendContext(); |
| | 3049 | if (!sendCtx) { |
| | 3050 | return; |
| | 3051 | } |
| | 3052 | const { |
| | 3053 | selectedProvider: ctxSelectedProvider, |
| | 3054 | selectedModel: ctxSelectedModel, |
| | 3055 | selectedProviderModels: ctxSelectedProviderModels, |
| | 3056 | selectedPromptEffort: ctxSelectedPromptEffort, |
| | 3057 | selectedModelSelection: ctxSelectedModelSelection, |
| | 3058 | } = sendCtx; |
| | 3059 | |
| 3858 | const createdAt = new Date().toISOString(); | 3060 | const createdAt = new Date().toISOString(); |
| 3859 | const nextThreadId = newThreadId(); | 3061 | const nextThreadId = newThreadId(); |
| 3860 | const planMarkdown = activeProposedPlan.planMarkdown; | 3062 | const planMarkdown = activeProposedPlan.planMarkdown; |
| 3861 | const implementationPrompt = buildPlanImplementationPrompt(planMarkdown); | 3063 | const implementationPrompt = buildPlanImplementationPrompt(planMarkdown); |
| 3862 | const outgoingImplementationPrompt = formatOutgoingPrompt({ | 3064 | const outgoingImplementationPrompt = formatOutgoingPrompt({ |
| 3863 | provider: selectedProvider, | 3065 | provider: ctxSelectedProvider, |
| 3864 | model: selectedModel, | 3066 | model: ctxSelectedModel, |
| 3865 | models: selectedProviderModels, | 3067 | models: ctxSelectedProviderModels, |
| 3866 | effort: selectedPromptEffort, | 3068 | effort: ctxSelectedPromptEffort, |
| 3867 | text: implementationPrompt, | 3069 | text: implementationPrompt, |
| 3868 | }); | 3070 | }); |
| 3869 | const nextThreadTitle = truncate(buildPlanImplementationThreadTitle(planMarkdown)); | 3071 | const nextThreadTitle = truncate(buildPlanImplementationThreadTitle(planMarkdown)); |
| 3870 | const nextThreadModelSelection: ModelSelection = selectedModelSelection; | 3072 | const nextThreadModelSelection: ModelSelection = ctxSelectedModelSelection; |
| 3871 | | 3073 | |
| 3872 | sendInFlightRef.current = true; | 3074 | sendInFlightRef.current = true; |
| 3873 | beginLocalDispatch({ preparingWorktree: false }); | 3075 | beginLocalDispatch({ preparingWorktree: false }); |
| ... | @@ -3901,7 +3103,7 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3901,7 +3103,7 @@ export default function ChatView(props: ChatViewProps) { |
| 3901 | text: outgoingImplementationPrompt, | 3103 | text: outgoingImplementationPrompt, |
| 3902 | attachments: [], | 3104 | attachments: [], |
| 3903 | }, | 3105 | }, |
| 3904 | modelSelection: selectedModelSelection, | 3106 | modelSelection: ctxSelectedModelSelection, |
| 3905 | titleSeed: nextThreadTitle, | 3107 | titleSeed: nextThreadTitle, |
| 3906 | runtimeMode, | 3108 | runtimeMode, |
| 3907 | interactionMode: "default", | 3109 | interactionMode: "default", |
| ... | @@ -3953,11 +3155,6 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3953,11 +3155,6 @@ export default function ChatView(props: ChatViewProps) { |
| 3953 | navigate, | 3155 | navigate, |
| 3954 | resetLocalDispatch, | 3156 | resetLocalDispatch, |
| 3955 | runtimeMode, | 3157 | runtimeMode, |
| 3956 | selectedPromptEffort, | | |
| 3957 | selectedModelSelection, | | |
| 3958 | selectedProvider, | | |
| 3959 | selectedProviderModels, | | |
| 3960 | selectedModel, | | |
| 3961 | environmentId, | 3158 | environmentId, |
| 3962 | ]); | 3159 | ]); |
| 3963 | | 3160 | |
| ... | @@ -3996,42 +3193,6 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -3996,42 +3193,6 @@ export default function ChatView(props: ChatViewProps) { |
| 3996 | settings, | 3193 | settings, |
| 3997 | ], | 3194 | ], |
| 3998 | ); | 3195 | ); |
| 3999 | const setPromptFromTraits = useCallback( | | |
| 4000 | (nextPrompt: string) => { | | |
| 4001 | const currentPrompt = promptRef.current; | | |
| 4002 | if (nextPrompt === currentPrompt) { | | |
| 4003 | scheduleComposerFocus(); | | |
| 4004 | return; | | |
| 4005 | } | | |
| 4006 | promptRef.current = nextPrompt; | | |
| 4007 | setPrompt(nextPrompt); | | |
| 4008 | const nextCursor = collapseExpandedComposerCursor(nextPrompt, nextPrompt.length); | | |
| 4009 | setComposerCursor(nextCursor); | | |
| 4010 | setComposerTrigger(detectComposerTrigger(nextPrompt, nextPrompt.length)); | | |
| 4011 | scheduleComposerFocus(); | | |
| 4012 | }, | | |
| 4013 | [scheduleComposerFocus, setPrompt], | | |
| 4014 | ); | | |
| 4015 | const providerTraitsMenuContent = renderProviderTraitsMenuContent({ | | |
| 4016 | provider: selectedProvider, | | |
| 4017 | ...(routeKind === "server" ? { threadRef: routeThreadRef } : {}), | | |
| 4018 | ...(routeKind === "draft" && draftId ? { draftId } : {}), | | |
| 4019 | model: selectedModel, | | |
| 4020 | models: selectedProviderModels, | | |
| 4021 | modelOptions: composerModelOptions?.[selectedProvider], | | |
| 4022 | prompt, | | |
| 4023 | onPromptChange: setPromptFromTraits, | | |
| 4024 | }); | | |
| 4025 | const providerTraitsPicker = renderProviderTraitsPicker({ | | |
| 4026 | provider: selectedProvider, | | |
| 4027 | ...(routeKind === "server" ? { threadRef: routeThreadRef } : {}), | | |
| 4028 | ...(routeKind === "draft" && draftId ? { draftId } : {}), | | |
| 4029 | model: selectedModel, | | |
| 4030 | models: selectedProviderModels, | | |
| 4031 | modelOptions: composerModelOptions?.[selectedProvider], | | |
| 4032 | prompt, | | |
| 4033 | onPromptChange: setPromptFromTraits, | | |
| 4034 | }); | | |
| 4035 | const onEnvModeChange = useCallback( | 3196 | const onEnvModeChange = useCallback( |
| 4036 | (mode: DraftThreadEnvMode) => { | 3197 | (mode: DraftThreadEnvMode) => { |
| 4037 | if (isLocalDraftThread) { | 3198 | if (isLocalDraftThread) { |
| ... | @@ -4051,257 +3212,6 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -4051,257 +3212,6 @@ export default function ChatView(props: ChatViewProps) { |
| 4051 | ], | 3212 | ], |
| 4052 | ); | 3213 | ); |
| 4053 | | 3214 | |
| 4054 | const applyPromptReplacement = useCallback( | | |
| 4055 | ( | | |
| 4056 | rangeStart: number, | | |
| 4057 | rangeEnd: number, | | |
| 4058 | replacement: string, | | |
| 4059 | options?: { expectedText?: string }, | | |
| 4060 | ): boolean => { | | |
| 4061 | const currentText = promptRef.current; | | |
| 4062 | const safeStart = Math.max(0, Math.min(currentText.length, rangeStart)); | | |
| 4063 | const safeEnd = Math.max(safeStart, Math.min(currentText.length, rangeEnd)); | | |
| 4064 | if ( | | |
| 4065 | options?.expectedText !== undefined && | | |
| 4066 | currentText.slice(safeStart, safeEnd) !== options.expectedText | | |
| 4067 | ) { | | |
| 4068 | return false; | | |
| 4069 | } | | |
| 4070 | const next = replaceTextRange(promptRef.current, rangeStart, rangeEnd, replacement); | | |
| 4071 | const nextCursor = collapseExpandedComposerCursor(next.text, next.cursor); | | |
| 4072 | promptRef.current = next.text; | | |
| 4073 | const activePendingQuestion = activePendingProgress?.activeQuestion; | | |
| 4074 | if (activePendingQuestion && activePendingUserInput) { | | |
| 4075 | setPendingUserInputAnswersByRequestId((existing) => ({ | | |
| 4076 | ...existing, | | |
| 4077 | [activePendingUserInput.requestId]: { | | |
| 4078 | ...existing[activePendingUserInput.requestId], | | |
| 4079 | [activePendingQuestion.id]: setPendingUserInputCustomAnswer( | | |
| 4080 | existing[activePendingUserInput.requestId]?.[activePendingQuestion.id], | | |
| 4081 | next.text, | | |
| 4082 | ), | | |
| 4083 | }, | | |
| 4084 | })); | | |
| 4085 | } else { | | |
| 4086 | setPrompt(next.text); | | |
| 4087 | } | | |
| 4088 | setComposerCursor(nextCursor); | | |
| 4089 | setComposerTrigger( | | |
| 4090 | detectComposerTrigger(next.text, expandCollapsedComposerCursor(next.text, nextCursor)), | | |
| 4091 | ); | | |
| 4092 | window.requestAnimationFrame(() => { | | |
| 4093 | composerEditorRef.current?.focusAt(nextCursor); | | |
| 4094 | }); | | |
| 4095 | return true; | | |
| 4096 | }, | | |
| 4097 | [activePendingProgress?.activeQuestion, activePendingUserInput, setPrompt], | | |
| 4098 | ); | | |
| 4099 | | | |
| 4100 | const readComposerSnapshot = useCallback((): { | | |
| 4101 | value: string; | | |
| 4102 | cursor: number; | | |
| 4103 | expandedCursor: number; | | |
| 4104 | terminalContextIds: string[]; | | |
| 4105 | } => { | | |
| 4106 | const editorSnapshot = composerEditorRef.current?.readSnapshot(); | | |
| 4107 | if (editorSnapshot) { | | |
| 4108 | return editorSnapshot; | | |
| 4109 | } | | |
| 4110 | return { | | |
| 4111 | value: promptRef.current, | | |
| 4112 | cursor: composerCursor, | | |
| 4113 | expandedCursor: expandCollapsedComposerCursor(promptRef.current, composerCursor), | | |
| 4114 | terminalContextIds: composerTerminalContexts.map((context) => context.id), | | |
| 4115 | }; | | |
| 4116 | }, [composerCursor, composerTerminalContexts]); | | |
| 4117 | | | |
| 4118 | const resolveActiveComposerTrigger = useCallback((): { | | |
| 4119 | snapshot: { value: string; cursor: number; expandedCursor: number }; | | |
| 4120 | trigger: ComposerTrigger | null; | | |
| 4121 | } => { | | |
| 4122 | const snapshot = readComposerSnapshot(); | | |
| 4123 | return { | | |
| 4124 | snapshot, | | |
| 4125 | trigger: detectComposerTrigger(snapshot.value, snapshot.expandedCursor), | | |
| 4126 | }; | | |
| 4127 | }, [readComposerSnapshot]); | | |
| 4128 | | | |
| 4129 | const onSelectComposerItem = useCallback( | | |
| 4130 | (item: ComposerCommandItem) => { | | |
| 4131 | if (composerSelectLockRef.current) return; | | |
| 4132 | composerSelectLockRef.current = true; | | |
| 4133 | window.requestAnimationFrame(() => { | | |
| 4134 | composerSelectLockRef.current = false; | | |
| 4135 | }); | | |
| 4136 | const { snapshot, trigger } = resolveActiveComposerTrigger(); | | |
| 4137 | if (!trigger) return; | | |
| 4138 | if (item.type === "path") { | | |
| 4139 | const replacement = `@${item.path} `; | | |
| 4140 | const replacementRangeEnd = extendReplacementRangeForTrailingSpace( | | |
| 4141 | snapshot.value, | | |
| 4142 | trigger.rangeEnd, | | |
| 4143 | replacement, | | |
| 4144 | ); | | |
| 4145 | const applied = applyPromptReplacement( | | |
| 4146 | trigger.rangeStart, | | |
| 4147 | replacementRangeEnd, | | |
| 4148 | replacement, | | |
| 4149 | { expectedText: snapshot.value.slice(trigger.rangeStart, replacementRangeEnd) }, | | |
| 4150 | ); | | |
| 4151 | if (applied) { | | |
| 4152 | setComposerHighlightedItemId(null); | | |
| 4153 | } | | |
| 4154 | return; | | |
| 4155 | } | | |
| 4156 | if (item.type === "slash-command") { | | |
| 4157 | if (item.command === "model") { | | |
| 4158 | const replacement = "/model "; | | |
| 4159 | const replacementRangeEnd = extendReplacementRangeForTrailingSpace( | | |
| 4160 | snapshot.value, | | |
| 4161 | trigger.rangeEnd, | | |
| 4162 | replacement, | | |
| 4163 | ); | | |
| 4164 | const applied = applyPromptReplacement( | | |
| 4165 | trigger.rangeStart, | | |
| 4166 | replacementRangeEnd, | | |
| 4167 | replacement, | | |
| 4168 | { expectedText: snapshot.value.slice(trigger.rangeStart, replacementRangeEnd) }, | | |
| 4169 | ); | | |
| 4170 | if (applied) { | | |
| 4171 | setComposerHighlightedItemId(null); | | |
| 4172 | } | | |
| 4173 | return; | | |
| 4174 | } | | |
| 4175 | void handleInteractionModeChange(item.command === "plan" ? "plan" : "default"); | | |
| 4176 | const applied = applyPromptReplacement(trigger.rangeStart, trigger.rangeEnd, "", { | | |
| 4177 | expectedText: snapshot.value.slice(trigger.rangeStart, trigger.rangeEnd), | | |
| 4178 | }); | | |
| 4179 | if (applied) { | | |
| 4180 | setComposerHighlightedItemId(null); | | |
| 4181 | } | | |
| 4182 | return; | | |
| 4183 | } | | |
| 4184 | onProviderModelSelect(item.provider, item.model); | | |
| 4185 | const applied = applyPromptReplacement(trigger.rangeStart, trigger.rangeEnd, "", { | | |
| 4186 | expectedText: snapshot.value.slice(trigger.rangeStart, trigger.rangeEnd), | | |
| 4187 | }); | | |
| 4188 | if (applied) { | | |
| 4189 | setComposerHighlightedItemId(null); | | |
| 4190 | } | | |
| 4191 | }, | | |
| 4192 | [ | | |
| 4193 | applyPromptReplacement, | | |
| 4194 | handleInteractionModeChange, | | |
| 4195 | onProviderModelSelect, | | |
| 4196 | resolveActiveComposerTrigger, | | |
| 4197 | ], | | |
| 4198 | ); | | |
| 4199 | const onComposerMenuItemHighlighted = useCallback((itemId: string | null) => { | | |
| 4200 | setComposerHighlightedItemId(itemId); | | |
| 4201 | }, []); | | |
| 4202 | const nudgeComposerMenuHighlight = useCallback( | | |
| 4203 | (key: "ArrowDown" | "ArrowUp") => { | | |
| 4204 | if (composerMenuItems.length === 0) { | | |
| 4205 | return; | | |
| 4206 | } | | |
| 4207 | const highlightedIndex = composerMenuItems.findIndex( | | |
| 4208 | (item) => item.id === composerHighlightedItemId, | | |
| 4209 | ); | | |
| 4210 | const normalizedIndex = | | |
| 4211 | highlightedIndex >= 0 ? highlightedIndex : key === "ArrowDown" ? -1 : 0; | | |
| 4212 | const offset = key === "ArrowDown" ? 1 : -1; | | |
| 4213 | const nextIndex = | | |
| 4214 | (normalizedIndex + offset + composerMenuItems.length) % composerMenuItems.length; | | |
| 4215 | const nextItem = composerMenuItems[nextIndex]; | | |
| 4216 | setComposerHighlightedItemId(nextItem?.id ?? null); | | |
| 4217 | }, | | |
| 4218 | [composerHighlightedItemId, composerMenuItems], | | |
| 4219 | ); | | |
| 4220 | const isComposerMenuLoading = | | |
| 4221 | composerTriggerKind === "path" && | | |
| 4222 | ((pathTriggerQuery.length > 0 && composerPathQueryDebouncer.state.isPending) || | | |
| 4223 | workspaceEntriesQuery.isLoading || | | |
| 4224 | workspaceEntriesQuery.isFetching); | | |
| 4225 | | | |
| 4226 | const onPromptChange = useCallback( | | |
| 4227 | ( | | |
| 4228 | nextPrompt: string, | | |
| 4229 | nextCursor: number, | | |
| 4230 | expandedCursor: number, | | |
| 4231 | cursorAdjacentToMention: boolean, | | |
| 4232 | terminalContextIds: string[], | | |
| 4233 | ) => { | | |
| 4234 | if (activePendingProgress?.activeQuestion && activePendingUserInput) { | | |
| 4235 | onChangeActivePendingUserInputCustomAnswer( | | |
| 4236 | activePendingProgress.activeQuestion.id, | | |
| 4237 | nextPrompt, | | |
| 4238 | nextCursor, | | |
| 4239 | expandedCursor, | | |
| 4240 | cursorAdjacentToMention, | | |
| 4241 | ); | | |
| 4242 | return; | | |
| 4243 | } | | |
| 4244 | promptRef.current = nextPrompt; | | |
| 4245 | setPrompt(nextPrompt); | | |
| 4246 | if (!terminalContextIdListsEqual(composerTerminalContexts, terminalContextIds)) { | | |
| 4247 | setComposerDraftTerminalContexts( | | |
| 4248 | composerDraftTarget, | | |
| 4249 | syncTerminalContextsByIds(composerTerminalContexts, terminalContextIds), | | |
| 4250 | ); | | |
| 4251 | } | | |
| 4252 | setComposerCursor(nextCursor); | | |
| 4253 | setComposerTrigger( | | |
| 4254 | cursorAdjacentToMention ? null : detectComposerTrigger(nextPrompt, expandedCursor), | | |
| 4255 | ); | | |
| 4256 | }, | | |
| 4257 | [ | | |
| 4258 | activePendingProgress?.activeQuestion, | | |
| 4259 | activePendingUserInput, | | |
| 4260 | composerTerminalContexts, | | |
| 4261 | onChangeActivePendingUserInputCustomAnswer, | | |
| 4262 | setPrompt, | | |
| 4263 | composerDraftTarget, | | |
| 4264 | setComposerDraftTerminalContexts, | | |
| 4265 | ], | | |
| 4266 | ); | | |
| 4267 | | | |
| 4268 | const onComposerCommandKey = ( | | |
| 4269 | key: "ArrowDown" | "ArrowUp" | "Enter" | "Tab", | | |
| 4270 | event: KeyboardEvent, | | |
| 4271 | ) => { | | |
| 4272 | if (key === "Tab" && event.shiftKey) { | | |
| 4273 | toggleInteractionMode(); | | |
| 4274 | return true; | | |
| 4275 | } | | |
| 4276 | | | |
| 4277 | const { trigger } = resolveActiveComposerTrigger(); | | |
| 4278 | const menuIsActive = composerMenuOpenRef.current || trigger !== null; | | |
| 4279 | | | |
| 4280 | if (menuIsActive) { | | |
| 4281 | const currentItems = composerMenuItemsRef.current; | | |
| 4282 | if (key === "ArrowDown" && currentItems.length > 0) { | | |
| 4283 | nudgeComposerMenuHighlight("ArrowDown"); | | |
| 4284 | return true; | | |
| 4285 | } | | |
| 4286 | if (key === "ArrowUp" && currentItems.length > 0) { | | |
| 4287 | nudgeComposerMenuHighlight("ArrowUp"); | | |
| 4288 | return true; | | |
| 4289 | } | | |
| 4290 | if (key === "Tab" || key === "Enter") { | | |
| 4291 | const selectedItem = activeComposerMenuItemRef.current ?? currentItems[0]; | | |
| 4292 | if (selectedItem) { | | |
| 4293 | onSelectComposerItem(selectedItem); | | |
| 4294 | return true; | | |
| 4295 | } | | |
| 4296 | } | | |
| 4297 | } | | |
| 4298 | | | |
| 4299 | if (key === "Enter" && !event.shiftKey) { | | |
| 4300 | void onSend(); | | |
| 4301 | return true; | | |
| 4302 | } | | |
| 4303 | return false; | | |
| 4304 | }; | | |
| 4305 | const onToggleWorkGroup = useCallback((groupId: string) => { | 3215 | const onToggleWorkGroup = useCallback((groupId: string) => { |
| 4306 | setExpandedWorkGroups((existing) => ({ | 3216 | setExpandedWorkGroups((existing) => ({ |
| 4307 | ...existing, | 3217 | ...existing, |
| ... | @@ -4311,7 +3221,6 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -4311,7 +3221,6 @@ export default function ChatView(props: ChatViewProps) { |
| 4311 | const onExpandTimelineImage = useCallback((preview: ExpandedImagePreview) => { | 3221 | const onExpandTimelineImage = useCallback((preview: ExpandedImagePreview) => { |
| 4312 | setExpandedImage(preview); | 3222 | setExpandedImage(preview); |
| 4313 | }, []); | 3223 | }, []); |
| 4314 | const expandedImageItem = expandedImage ? expandedImage.images[expandedImage.index] : null; | | |
| 4315 | const onOpenTurnDiff = useCallback( | 3224 | const onOpenTurnDiff = useCallback( |
| 4316 | (turnId: TurnId, filePath?: string) => { | 3225 | (turnId: TurnId, filePath?: string) => { |
| 4317 | if (!isServerThread) { | 3226 | if (!isServerThread) { |
| ... | @@ -4333,13 +3242,16 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -4333,13 +3242,16 @@ export default function ChatView(props: ChatViewProps) { |
| 4333 | }, | 3242 | }, |
| 4334 | [environmentId, isServerThread, navigate, threadId], | 3243 | [environmentId, isServerThread, navigate, threadId], |
| 4335 | ); | 3244 | ); |
| 4336 | const onRevertUserMessage = (messageId: MessageId) => { | 3245 | const onRevertUserMessage = useCallback( |
| 4337 | const targetTurnCount = revertTurnCountByUserMessageId.get(messageId); | 3246 | (messageId: MessageId) => { |
| 4338 | if (typeof targetTurnCount !== "number") { | 3247 | const targetTurnCount = revertTurnCountByUserMessageId.get(messageId); |
| 4339 | return; | 3248 | if (typeof targetTurnCount !== "number") { |
| 4340 | } | 3249 | return; |
| 4341 | void onRevertToTurnCount(targetTurnCount); | 3250 | } |
| 4342 | }; | 3251 | void onRevertToTurnCount(targetTurnCount); |
| | 3252 | }, |
| | 3253 | [onRevertToTurnCount, revertTurnCountByUserMessageId], |
| | 3254 | ); |
| 4343 | | 3255 | |
| 4344 | // Empty state: no active thread | 3256 | // Empty state: no active thread |
| 4345 | if (!activeThread) { | 3257 | if (!activeThread) { |
| ... | @@ -4374,9 +3286,7 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -4374,9 +3286,7 @@ export default function ChatView(props: ChatViewProps) { |
| 4374 | diffToggleShortcutLabel={diffPanelShortcutLabel} | 3286 | diffToggleShortcutLabel={diffPanelShortcutLabel} |
| 4375 | gitCwd={gitCwd} | 3287 | gitCwd={gitCwd} |
| 4376 | diffOpen={diffOpen} | 3288 | diffOpen={diffOpen} |
| 4377 | onRunProjectScript={(script) => { | 3289 | onRunProjectScript={runProjectScript} |
| 4378 | void runProjectScript(script); | | |
| 4379 | }} | | |
| 4380 | onAddProjectScript={saveProjectScript} | 3290 | onAddProjectScript={saveProjectScript} |
| 4381 | onUpdateProjectScript={updateProjectScript} | 3291 | onUpdateProjectScript={updateProjectScript} |
| 4382 | onDeleteProjectScript={deleteProjectScript} | 3292 | onDeleteProjectScript={deleteProjectScript} |
| ... | @@ -4456,392 +3366,71 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -4456,392 +3366,71 @@ export default function ChatView(props: ChatViewProps) { |
| 4456 | | 3366 | |
| 4457 | {/* Input bar */} | 3367 | {/* Input bar */} |
| 4458 | <div className={cn("px-3 pt-1.5 sm:px-5 sm:pt-2", isGitRepo ? "pb-1" : "pb-3 sm:pb-4")}> | 3368 | <div className={cn("px-3 pt-1.5 sm:px-5 sm:pt-2", isGitRepo ? "pb-1" : "pb-3 sm:pb-4")}> |
| 4459 | <form | 3369 | <ChatComposer |
| 4460 | ref={composerFormRef} | 3370 | ref={composerRef} |
| 4461 | onSubmit={onSend} | 3371 | composerDraftTarget={composerDraftTarget} |
| 4462 | className="mx-auto w-full min-w-0 max-w-208" | 3372 | environmentId={environmentId} |
| 4463 | data-chat-composer-form="true" | 3373 | routeKind={routeKind} |
| 4464 | > | 3374 | routeThreadRef={routeThreadRef} |
| 4465 | <div | 3375 | draftId={draftId} |
| 4466 | className={cn( | 3376 | activeThreadId={activeThreadId} |
| 4467 | "group rounded-[22px] p-px transition-colors duration-200", | 3377 | activeThreadEnvironmentId={activeThread?.environmentId} |
| 4468 | composerProviderState.composerFrameClassName, | 3378 | activeThread={activeThread} |
| 4469 | )} | 3379 | isServerThread={isServerThread} |
| 4470 | onDragEnter={onComposerDragEnter} | 3380 | isLocalDraftThread={isLocalDraftThread} |
| 4471 | onDragOver={onComposerDragOver} | 3381 | phase={phase} |
| 4472 | onDragLeave={onComposerDragLeave} | 3382 | isConnecting={isConnecting} |
| 4473 | onDrop={onComposerDrop} | 3383 | isSendBusy={isSendBusy} |
| 4474 | > | 3384 | isPreparingWorktree={isPreparingWorktree} |
| 4475 | <div | 3385 | activePendingApproval={activePendingApproval} |
| 4476 | className={cn( | 3386 | pendingApprovals={pendingApprovals} |
| 4477 | "rounded-[20px] border bg-card transition-colors duration-200 has-focus-visible:border-ring/45", | 3387 | pendingUserInputs={pendingUserInputs} |
| 4478 | isDragOverComposer ? "border-primary/70 bg-accent/30" : "border-border", | 3388 | activePendingProgress={activePendingProgress} |
| 4479 | composerProviderState.composerSurfaceClassName, | 3389 | activePendingResolvedAnswers={activePendingResolvedAnswers} |
| 4480 | )} | 3390 | activePendingIsResponding={activePendingIsResponding} |
| 4481 | > | 3391 | activePendingDraftAnswers={activePendingDraftAnswers} |
| 4482 | {activePendingApproval ? ( | 3392 | activePendingQuestionIndex={activePendingQuestionIndex} |
| 4483 | <div className="rounded-t-[19px] border-b border-border/65 bg-muted/20"> | 3393 | respondingRequestIds={respondingRequestIds} |
| 4484 | <ComposerPendingApprovalPanel | 3394 | showPlanFollowUpPrompt={showPlanFollowUpPrompt} |
| 4485 | approval={activePendingApproval} | 3395 | activeProposedPlan={activeProposedPlan} |
| 4486 | pendingCount={pendingApprovals.length} | 3396 | activePlan={activePlan as { turnId?: TurnId } | null} |
| 4487 | /> | 3397 | sidebarProposedPlan={sidebarProposedPlan as { turnId?: TurnId } | null} |
| 4488 | </div> | 3398 | planSidebarOpen={planSidebarOpen} |
| 4489 | ) : pendingUserInputs.length > 0 ? ( | 3399 | runtimeMode={runtimeMode} |
| 4490 | <div className="rounded-t-[19px] border-b border-border/65 bg-muted/20"> | 3400 | interactionMode={interactionMode} |
| 4491 | <ComposerPendingUserInputPanel | 3401 | lockedProvider={lockedProvider} |
| 4492 | pendingUserInputs={pendingUserInputs} | 3402 | providerStatuses={providerStatuses as ServerProvider[]} |
| 4493 | respondingRequestIds={respondingRequestIds} | 3403 | activeProjectDefaultModelSelection={activeProject?.defaultModelSelection} |
| 4494 | answers={activePendingDraftAnswers} | 3404 | activeThreadModelSelection={activeThread?.modelSelection} |
| 4495 | questionIndex={activePendingQuestionIndex} | 3405 | activeThreadActivities={activeThread?.activities} |
| 4496 | onToggleOption={onSelectActivePendingUserInputOption} | 3406 | resolvedTheme={resolvedTheme} |
| 4497 | onAdvance={onAdvanceActivePendingUserInput} | 3407 | settings={settings} |
| 4498 | /> | 3408 | gitCwd={gitCwd} |
| 4499 | </div> | 3409 | promptRef={promptRef} |
| 4500 | ) : showPlanFollowUpPrompt && activeProposedPlan ? ( | 3410 | composerImagesRef={composerImagesRef} |
| 4501 | <div className="rounded-t-[19px] border-b border-border/65 bg-muted/20"> | 3411 | composerTerminalContextsRef={composerTerminalContextsRef} |
| 4502 | <ComposerPlanFollowUpBanner | 3412 | shouldAutoScrollRef={shouldAutoScrollRef} |
| 4503 | key={activeProposedPlan.id} | 3413 | scheduleStickToBottom={scheduleStickToBottom} |
| 4504 | planTitle={proposedPlanTitle(activeProposedPlan.planMarkdown) ?? null} | 3414 | onSend={onSend} |
| 4505 | /> | 3415 | onInterrupt={onInterrupt} |
| 4506 | </div> | 3416 | onImplementPlanInNewThread={onImplementPlanInNewThread} |
| 4507 | ) : null} | 3417 | onRespondToApproval={onRespondToApproval} |
| 4508 | <div | 3418 | onSelectActivePendingUserInputOption={onSelectActivePendingUserInputOption} |
| 4509 | className={cn( | 3419 | onAdvanceActivePendingUserInput={onAdvanceActivePendingUserInput} |
| 4510 | "relative px-3 pb-2 sm:px-4", | 3420 | onPreviousActivePendingUserInputQuestion={onPreviousActivePendingUserInputQuestion} |
| 4511 | hasComposerHeader ? "pt-2.5 sm:pt-3" : "pt-3.5 sm:pt-4", | 3421 | onChangeActivePendingUserInputCustomAnswer={ |
| 4512 | )} | 3422 | onChangeActivePendingUserInputCustomAnswer |
| 4513 | > | 3423 | } |
| 4514 | {composerMenuOpen && !isComposerApprovalState && ( | 3424 | onProviderModelSelect={onProviderModelSelect} |
| 4515 | <div className="absolute inset-x-0 bottom-full z-20 mb-2 px-1"> | 3425 | toggleInteractionMode={toggleInteractionMode} |
| 4516 | <ComposerCommandMenu | 3426 | handleRuntimeModeChange={handleRuntimeModeChange} |
| 4517 | items={composerMenuItems} | 3427 | handleInteractionModeChange={handleInteractionModeChange} |
| 4518 | resolvedTheme={resolvedTheme} | 3428 | togglePlanSidebar={togglePlanSidebar} |
| 4519 | isLoading={isComposerMenuLoading} | 3429 | focusComposer={focusComposer} |
| 4520 | triggerKind={composerTriggerKind} | 3430 | scheduleComposerFocus={scheduleComposerFocus} |
| 4521 | activeItemId={activeComposerMenuItem?.id ?? null} | 3431 | setThreadError={setThreadError} |
| 4522 | onHighlightedItemChange={onComposerMenuItemHighlighted} | 3432 | onExpandImage={onExpandTimelineImage} |
| 4523 | onSelect={onSelectComposerItem} | 3433 | /> |
| 4524 | /> | | |
| 4525 | </div> | | |
| 4526 | )} | | |
| 4527 | | | |
| 4528 | {!isComposerApprovalState && | | |
| 4529 | pendingUserInputs.length === 0 && | | |
| 4530 | composerImages.length > 0 && ( | | |
| 4531 | <div className="mb-3 flex flex-wrap gap-2"> | | |
| 4532 | {composerImages.map((image) => ( | | |
| 4533 | <div | | |
| 4534 | key={image.id} | | |
| 4535 | className="relative h-16 w-16 overflow-hidden rounded-lg border border-border/80 bg-background" | | |
| 4536 | > | | |
| 4537 | {image.previewUrl ? ( | | |
| 4538 | <button | | |
| 4539 | type="button" | | |
| 4540 | className="h-full w-full cursor-zoom-in" | | |
| 4541 | aria-label={`Preview ${image.name}`} | | |
| 4542 | onClick={() => { | | |
| 4543 | const preview = buildExpandedImagePreview( | | |
| 4544 | composerImages, | | |
| 4545 | image.id, | | |
| 4546 | ); | | |
| 4547 | if (!preview) return; | | |
| 4548 | setExpandedImage(preview); | | |
| 4549 | }} | | |
| 4550 | > | | |
| 4551 | <img | | |
| 4552 | src={image.previewUrl} | | |
| 4553 | alt={image.name} | | |
| 4554 | className="h-full w-full object-cover" | | |
| 4555 | /> | | |
| 4556 | </button> | | |
| 4557 | ) : ( | | |
| 4558 | <div className="flex h-full w-full items-center justify-center px-1 text-center text-[10px] text-muted-foreground/70"> | | |
| 4559 | {image.name} | | |
| 4560 | </div> | | |
| 4561 | )} | | |
| 4562 | {nonPersistedComposerImageIdSet.has(image.id) && ( | | |
| 4563 | <Tooltip> | | |
| 4564 | <TooltipTrigger | | |
| 4565 | render={ | | |
| 4566 | <span | | |
| 4567 | role="img" | | |
| 4568 | aria-label="Draft attachment may not persist" | | |
| 4569 | className="absolute left-1 top-1 inline-flex items-center justify-center rounded bg-background/85 p-0.5 text-amber-600" | | |
| 4570 | > | | |
| 4571 | <CircleAlertIcon className="size-3" /> | | |
| 4572 | </span> | | |
| 4573 | } | | |
| 4574 | /> | | |
| 4575 | <TooltipPopup | | |
| 4576 | side="top" | | |
| 4577 | className="max-w-64 whitespace-normal leading-tight" | | |
| 4578 | > | | |
| 4579 | Draft attachment could not be saved locally and may be lost on | | |
| 4580 | navigation. | | |
| 4581 | </TooltipPopup> | | |
| 4582 | </Tooltip> | | |
| 4583 | )} | | |
| 4584 | <Button | | |
| 4585 | variant="ghost" | | |
| 4586 | size="icon-xs" | | |
| 4587 | className="absolute right-1 top-1 bg-background/80 hover:bg-background/90" | | |
| 4588 | onClick={() => removeComposerImage(image.id)} | | |
| 4589 | aria-label={`Remove ${image.name}`} | | |
| 4590 | > | | |
| 4591 | <XIcon /> | | |
| 4592 | </Button> | | |
| 4593 | </div> | | |
| 4594 | ))} | | |
| 4595 | </div> | | |
| 4596 | )} | | |
| 4597 | <ComposerPromptEditor | | |
| 4598 | ref={composerEditorRef} | | |
| 4599 | value={ | | |
| 4600 | isComposerApprovalState | | |
| 4601 | ? "" | | |
| 4602 | : activePendingProgress | | |
| 4603 | ? activePendingProgress.customAnswer | | |
| 4604 | : prompt | | |
| 4605 | } | | |
| 4606 | cursor={composerCursor} | | |
| 4607 | terminalContexts={ | | |
| 4608 | !isComposerApprovalState && pendingUserInputs.length === 0 | | |
| 4609 | ? composerTerminalContexts | | |
| 4610 | : [] | | |
| 4611 | } | | |
| 4612 | onRemoveTerminalContext={removeComposerTerminalContextFromDraft} | | |
| 4613 | onChange={onPromptChange} | | |
| 4614 | onCommandKeyDown={onComposerCommandKey} | | |
| 4615 | onPaste={onComposerPaste} | | |
| 4616 | placeholder={ | | |
| 4617 | isComposerApprovalState | | |
| 4618 | ? (activePendingApproval?.detail ?? | | |
| 4619 | "Resolve this approval request to continue") | | |
| 4620 | : activePendingProgress | | |
| 4621 | ? "Type your own answer, or leave this blank to use the selected option" | | |
| 4622 | : showPlanFollowUpPrompt && activeProposedPlan | | |
| 4623 | ? "Add feedback to refine the plan, or leave this blank to implement it" | | |
| 4624 | : phase === "disconnected" | | |
| 4625 | ? "Ask for follow-up changes or attach images" | | |
| 4626 | : "Ask anything, @tag files/folders, or use / to show available commands" | | |
| 4627 | } | | |
| 4628 | disabled={isConnecting || isComposerApprovalState} | | |
| 4629 | /> | | |
| 4630 | </div> | | |
| 4631 | | | |
| 4632 | {/* Bottom toolbar */} | | |
| 4633 | {activePendingApproval ? ( | | |
| 4634 | <div className="flex items-center justify-end gap-2 px-2.5 pb-2.5 sm:px-3 sm:pb-3"> | | |
| 4635 | <ComposerPendingApprovalActions | | |
| 4636 | requestId={activePendingApproval.requestId} | | |
| 4637 | isResponding={respondingRequestIds.includes( | | |
| 4638 | activePendingApproval.requestId, | | |
| 4639 | )} | | |
| 4640 | onRespondToApproval={onRespondToApproval} | | |
| 4641 | /> | | |
| 4642 | </div> | | |
| 4643 | ) : ( | | |
| 4644 | <div | | |
| 4645 | ref={composerFooterRef} | | |
| 4646 | data-chat-composer-footer="true" | | |
| 4647 | data-chat-composer-footer-compact={isComposerFooterCompact ? "true" : "false"} | | |
| 4648 | className={cn( | | |
| 4649 | "flex min-w-0 flex-nowrap items-center justify-between gap-2 overflow-hidden px-2.5 pb-2.5 sm:px-3 sm:pb-3", | | |
| 4650 | isComposerFooterCompact ? "gap-1.5" : "gap-2 sm:gap-0", | | |
| 4651 | )} | | |
| 4652 | > | | |
| 4653 | <div | | |
| 4654 | ref={composerFooterLeadingRef} | | |
| 4655 | className={cn( | | |
| 4656 | "flex min-w-0 flex-1 items-center", | | |
| 4657 | isComposerFooterCompact | | |
| 4658 | ? "gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden" | | |
| 4659 | : "gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden sm:min-w-max sm:overflow-visible", | | |
| 4660 | )} | | |
| 4661 | > | | |
| 4662 | {/* Provider/model picker */} | | |
| 4663 | <ProviderModelPicker | | |
| 4664 | compact={isComposerFooterCompact} | | |
| 4665 | provider={selectedProvider} | | |
| 4666 | model={selectedModelForPickerWithCustomFallback} | | |
| 4667 | lockedProvider={lockedProvider} | | |
| 4668 | providers={providerStatuses} | | |
| 4669 | modelOptionsByProvider={modelOptionsByProvider} | | |
| 4670 | {...(composerProviderState.modelPickerIconClassName | | |
| 4671 | ? { | | |
| 4672 | activeProviderIconClassName: | | |
| 4673 | composerProviderState.modelPickerIconClassName, | | |
| 4674 | } | | |
| 4675 | : {})} | | |
| 4676 | onProviderModelChange={onProviderModelSelect} | | |
| 4677 | /> | | |
| 4678 | | | |
| 4679 | {isComposerFooterCompact ? ( | | |
| 4680 | <CompactComposerControlsMenu | | |
| 4681 | activePlan={Boolean( | | |
| 4682 | activePlan || sidebarProposedPlan || planSidebarOpen, | | |
| 4683 | )} | | |
| 4684 | interactionMode={interactionMode} | | |
| 4685 | planSidebarOpen={planSidebarOpen} | | |
| 4686 | runtimeMode={runtimeMode} | | |
| 4687 | traitsMenuContent={providerTraitsMenuContent} | | |
| 4688 | onToggleInteractionMode={toggleInteractionMode} | | |
| 4689 | onTogglePlanSidebar={togglePlanSidebar} | | |
| 4690 | onRuntimeModeChange={handleRuntimeModeChange} | | |
| 4691 | /> | | |
| 4692 | ) : ( | | |
| 4693 | <> | | |
| 4694 | {providerTraitsPicker ? ( | | |
| 4695 | <> | | |
| 4696 | <Separator | | |
| 4697 | orientation="vertical" | | |
| 4698 | className="mx-0.5 hidden h-4 sm:block" | | |
| 4699 | /> | | |
| 4700 | {providerTraitsPicker} | | |
| 4701 | </> | | |
| 4702 | ) : null} | | |
| 4703 | | | |
| 4704 | <Separator | | |
| 4705 | orientation="vertical" | | |
| 4706 | className="mx-0.5 hidden h-4 sm:block" | | |
| 4707 | /> | | |
| 4708 | | | |
| 4709 | <Button | | |
| 4710 | variant="ghost" | | |
| 4711 | className="shrink-0 whitespace-nowrap px-2 text-muted-foreground/70 hover:text-foreground/80 sm:px-3" | | |
| 4712 | size="sm" | | |
| 4713 | type="button" | | |
| 4714 | onClick={toggleInteractionMode} | | |
| 4715 | title={ | | |
| 4716 | interactionMode === "plan" | | |
| 4717 | ? "Plan mode — click to return to normal build mode" | | |
| 4718 | : "Default mode — click to enter plan mode" | | |
| 4719 | } | | |
| 4720 | > | | |
| 4721 | <BotIcon /> | | |
| 4722 | <span className="sr-only sm:not-sr-only"> | | |
| 4723 | {interactionMode === "plan" ? "Plan" : "Build"} | | |
| 4724 | </span> | | |
| 4725 | </Button> | | |
| 4726 | | | |
| 4727 | <Separator | | |
| 4728 | orientation="vertical" | | |
| 4729 | className="mx-0.5 hidden h-4 sm:block" | | |
| 4730 | /> | | |
| 4731 | | | |
| 4732 | <Select | | |
| 4733 | value={runtimeMode} | | |
| 4734 | onValueChange={(value) => handleRuntimeModeChange(value!)} | | |
| 4735 | > | | |
| 4736 | <SelectTrigger | | |
| 4737 | variant="ghost" | | |
| 4738 | size="sm" | | |
| 4739 | aria-label="Runtime mode" | | |
| 4740 | title={runtimeModeOption.description} | | |
| 4741 | > | | |
| 4742 | <RuntimeModeIcon className="size-4" /> | | |
| 4743 | <SelectValue>{runtimeModeOption.label}</SelectValue> | | |
| 4744 | </SelectTrigger> | | |
| 4745 | <SelectPopup alignItemWithTrigger={false}> | | |
| 4746 | {runtimeModeOptions.map((mode) => { | | |
| 4747 | const option = runtimeModeConfig[mode]; | | |
| 4748 | const OptionIcon = option.icon; | | |
| 4749 | return ( | | |
| 4750 | <SelectItem key={mode} value={mode} className="min-w-64 py-2"> | | |
| 4751 | <div className="grid min-w-0 gap-0.5"> | | |
| 4752 | <span className="inline-flex items-center gap-1.5 font-medium text-foreground"> | | |
| 4753 | <OptionIcon className="size-3.5 shrink-0 text-muted-foreground" /> | | |
| 4754 | {option.label} | | |
| 4755 | </span> | | |
| 4756 | <span className="text-muted-foreground text-xs leading-4"> | | |
| 4757 | {option.description} | | |
| 4758 | </span> | | |
| 4759 | </div> | | |
| 4760 | </SelectItem> | | |
| 4761 | ); | | |
| 4762 | })} | | |
| 4763 | </SelectPopup> | | |
| 4764 | </Select> | | |
| 4765 | | | |
| 4766 | {activePlan || sidebarProposedPlan || planSidebarOpen ? ( | | |
| 4767 | <> | | |
| 4768 | <Separator | | |
| 4769 | orientation="vertical" | | |
| 4770 | className="mx-0.5 hidden h-4 sm:block" | | |
| 4771 | /> | | |
| 4772 | <Button | | |
| 4773 | variant="ghost" | | |
| 4774 | className={cn( | | |
| 4775 | "shrink-0 whitespace-nowrap px-2 sm:px-3", | | |
| 4776 | planSidebarOpen | | |
| 4777 | ? "text-blue-400 hover:text-blue-300" | | |
| 4778 | : "text-muted-foreground/70 hover:text-foreground/80", | | |
| 4779 | )} | | |
| 4780 | size="sm" | | |
| 4781 | type="button" | | |
| 4782 | onClick={togglePlanSidebar} | | |
| 4783 | title={ | | |
| 4784 | planSidebarOpen ? "Hide plan sidebar" : "Show plan sidebar" | | |
| 4785 | } | | |
| 4786 | > | | |
| 4787 | <ListTodoIcon /> | | |
| 4788 | <span className="sr-only sm:not-sr-only">Plan</span> | | |
| 4789 | </Button> | | |
| 4790 | </> | | |
| 4791 | ) : null} | | |
| 4792 | </> | | |
| 4793 | )} | | |
| 4794 | </div> | | |
| 4795 | | | |
| 4796 | {/* Right side: send / stop button */} | | |
| 4797 | <div | | |
| 4798 | ref={composerFooterActionsRef} | | |
| 4799 | data-chat-composer-actions="right" | | |
| 4800 | data-chat-composer-primary-actions-compact={ | | |
| 4801 | isComposerPrimaryActionsCompact ? "true" : "false" | | |
| 4802 | } | | |
| 4803 | className="flex shrink-0 flex-nowrap items-center justify-end gap-2" | | |
| 4804 | > | | |
| 4805 | {activeContextWindow ? ( | | |
| 4806 | <ContextWindowMeter usage={activeContextWindow} /> | | |
| 4807 | ) : null} | | |
| 4808 | {isPreparingWorktree ? ( | | |
| 4809 | <span className="text-muted-foreground/70 text-xs"> | | |
| 4810 | Preparing worktree... | | |
| 4811 | </span> | | |
| 4812 | ) : null} | | |
| 4813 | <ComposerPrimaryActions | | |
| 4814 | compact={isComposerPrimaryActionsCompact} | | |
| 4815 | pendingAction={ | | |
| 4816 | activePendingProgress | | |
| 4817 | ? { | | |
| 4818 | questionIndex: activePendingProgress.questionIndex, | | |
| 4819 | isLastQuestion: activePendingProgress.isLastQuestion, | | |
| 4820 | canAdvance: activePendingProgress.canAdvance, | | |
| 4821 | isResponding: activePendingIsResponding, | | |
| 4822 | isComplete: Boolean(activePendingResolvedAnswers), | | |
| 4823 | } | | |
| 4824 | : null | | |
| 4825 | } | | |
| 4826 | isRunning={phase === "running"} | | |
| 4827 | showPlanFollowUpPrompt={ | | |
| 4828 | pendingUserInputs.length === 0 && showPlanFollowUpPrompt | | |
| 4829 | } | | |
| 4830 | promptHasText={prompt.trim().length > 0} | | |
| 4831 | isSendBusy={isSendBusy} | | |
| 4832 | isConnecting={isConnecting} | | |
| 4833 | isPreparingWorktree={isPreparingWorktree} | | |
| 4834 | hasSendableContent={composerSendState.hasSendableContent} | | |
| 4835 | onPreviousPendingQuestion={onPreviousActivePendingUserInputQuestion} | | |
| 4836 | onInterrupt={() => void onInterrupt()} | | |
| 4837 | onImplementPlanInNewThread={() => void onImplementPlanInNewThread()} | | |
| 4838 | /> | | |
| 4839 | </div> | | |
| 4840 | </div> | | |
| 4841 | )} | | |
| 4842 | </div> | | |
| 4843 | </div> | | |
| 4844 | </form> | | |
| 4845 | </div> | 3434 | </div> |
| 4846 | | 3435 | |
| 4847 | {isGitRepo && ( | 3436 | {isGitRepo && ( |
| ... | @@ -4921,72 +3510,8 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -4921,72 +3510,8 @@ export default function ChatView(props: ChatViewProps) { |
| 4921 | /> | 3510 | /> |
| 4922 | ))} | 3511 | ))} |
| 4923 | | 3512 | |
| 4924 | {expandedImage && expandedImageItem && ( | 3513 | {expandedImage && ( |
| 4925 | <div | 3514 | <ExpandedImageDialog preview={expandedImage} onClose={closeExpandedImage} /> |
| 4926 | className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 px-4 py-6 [-webkit-app-region:no-drag]" | | |
| 4927 | role="dialog" | | |
| 4928 | aria-modal="true" | | |
| 4929 | aria-label="Expanded image preview" | | |
| 4930 | > | | |
| 4931 | <button | | |
| 4932 | type="button" | | |
| 4933 | className="absolute inset-0 z-0 cursor-zoom-out" | | |
| 4934 | aria-label="Close image preview" | | |
| 4935 | onClick={closeExpandedImage} | | |
| 4936 | /> | | |
| 4937 | {expandedImage.images.length > 1 && ( | | |
| 4938 | <Button | | |
| 4939 | type="button" | | |
| 4940 | size="icon" | | |
| 4941 | variant="ghost" | | |
| 4942 | className="absolute left-2 top-1/2 z-20 -translate-y-1/2 text-white/90 hover:bg-white/10 hover:text-white sm:left-6" | | |
| 4943 | aria-label="Previous image" | | |
| 4944 | onClick={() => { | | |
| 4945 | navigateExpandedImage(-1); | | |
| 4946 | }} | | |
| 4947 | > | | |
| 4948 | <ChevronLeftIcon className="size-5" /> | | |
| 4949 | </Button> | | |
| 4950 | )} | | |
| 4951 | <div className="relative isolate z-10 max-h-[92vh] max-w-[92vw]"> | | |
| 4952 | <Button | | |
| 4953 | type="button" | | |
| 4954 | size="icon-xs" | | |
| 4955 | variant="ghost" | | |
| 4956 | className="absolute right-2 top-2" | | |
| 4957 | onClick={closeExpandedImage} | | |
| 4958 | aria-label="Close image preview" | | |
| 4959 | > | | |
| 4960 | <XIcon /> | | |
| 4961 | </Button> | | |
| 4962 | <img | | |
| 4963 | src={expandedImageItem.src} | | |
| 4964 | alt={expandedImageItem.name} | | |
| 4965 | className="max-h-[86vh] max-w-[92vw] select-none rounded-lg border border-border/70 bg-background object-contain shadow-2xl" | | |
| 4966 | draggable={false} | | |
| 4967 | /> | | |
| 4968 | <p className="mt-2 max-w-[92vw] truncate text-center text-xs text-muted-foreground/80"> | | |
| 4969 | {expandedImageItem.name} | | |
| 4970 | {expandedImage.images.length > 1 | | |
| 4971 | ? ` (${expandedImage.index + 1}/${expandedImage.images.length})` | | |
| 4972 | : ""} | | |
| 4973 | </p> | | |
| 4974 | </div> | | |
| 4975 | {expandedImage.images.length > 1 && ( | | |
| 4976 | <Button | | |
| 4977 | type="button" | | |
| 4978 | size="icon" | | |
| 4979 | variant="ghost" | | |
| 4980 | className="absolute right-2 top-1/2 z-20 -translate-y-1/2 text-white/90 hover:bg-white/10 hover:text-white sm:right-6" | | |
| 4981 | aria-label="Next image" | | |
| 4982 | onClick={() => { | | |
| 4983 | navigateExpandedImage(1); | | |
| 4984 | }} | | |
| 4985 | > | | |
| 4986 | <ChevronRightIcon className="size-5" /> | | |
| 4987 | </Button> | | |
| 4988 | )} | | |
| 4989 | </div> | | |
| 4990 | )} | 3515 | )} |
| 4991 | </div> | 3516 | </div> |
| 4992 | ); | 3517 | ); |