| ... | ... | @@ -474,7 +474,9 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 474 | 474 | const clearComposerDraftContent = useComposerDraftStore((store) => store.clearComposerContent); |
| 475 | 475 | const clearDraftThread = useComposerDraftStore((store) => store.clearDraftThread); |
| 476 | 476 | const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); |
| 477 | | const draftThread = useComposerDraftStore((store) => store.draftThreadsByThreadId[threadId] ?? null); |
| 477 | const draftThread = useComposerDraftStore( |
| 478 | (store) => store.draftThreadsByThreadId[threadId] ?? null, |
| 479 | ); |
| 478 | 480 | const promptRef = useRef(prompt); |
| 479 | 481 | const [isDragOverComposer, setIsDragOverComposer] = useState(false); |
| 480 | 482 | const [expandedImage, setExpandedImage] = useState<ExpandedImagePreview | null>(null); |
| ... | ... | @@ -604,7 +606,9 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 604 | 606 | markThreadVisited, |
| 605 | 607 | ]); |
| 606 | 608 | |
| 607 | | const baseThreadModel = resolveModelSlug(activeThread?.model ?? activeProject?.model ?? DEFAULT_MODEL); |
| 609 | const baseThreadModel = resolveModelSlug( |
| 610 | activeThread?.model ?? activeProject?.model ?? DEFAULT_MODEL, |
| 611 | ); |
| 608 | 612 | const selectedModel = resolveModelSlug(composerDraft.model ?? baseThreadModel); |
| 609 | 613 | const selectedEffort = composerDraft.effort ?? DEFAULT_REASONING; |
| 610 | 614 | const modelOptions = getAppModelOptions(settings.customCodexModels, selectedModel); |
| ... | ... | @@ -614,8 +618,14 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 614 | 618 | const isWorking = phase === "running" || isSendBusy || isConnecting || isRevertingCheckpoint; |
| 615 | 619 | const nowIso = new Date(nowTick).toISOString(); |
| 616 | 620 | const threadActivities = activeThread?.activities ?? []; |
| 617 | | const workLogEntries = deriveWorkLogEntries(threadActivities, activeLatestTurn?.turnId ?? undefined); |
| 618 | | const latestTurnHasToolActivity = hasToolActivityForTurn(threadActivities, activeLatestTurn?.turnId); |
| 621 | const workLogEntries = deriveWorkLogEntries( |
| 622 | threadActivities, |
| 623 | activeLatestTurn?.turnId ?? undefined, |
| 624 | ); |
| 625 | const latestTurnHasToolActivity = hasToolActivityForTurn( |
| 626 | threadActivities, |
| 627 | activeLatestTurn?.turnId, |
| 628 | ); |
| 619 | 629 | const pendingApprovals = derivePendingApprovals(threadActivities); |
| 620 | 630 | useEffect(() => { |
| 621 | 631 | attachmentPreviewHandoffByMessageIdRef.current = attachmentPreviewHandoffByMessageId; |
| ... | ... | @@ -680,13 +690,22 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 680 | 690 | delete attachmentPreviewHandoffTimeoutByMessageIdRef.current[messageId]; |
| 681 | 691 | }, ATTACHMENT_PREVIEW_HANDOFF_TTL_MS); |
| 682 | 692 | }, []); |
| 683 | | const timelineMessages = (() => { |
| 684 | | const serverMessages = activeThread?.messages ?? []; |
| 693 | const serverMessages = activeThread?.messages; |
| 694 | const timelineMessages = useMemo(() => { |
| 695 | const messages = serverMessages ?? []; |
| 685 | 696 | const serverMessagesWithPreviewHandoff = |
| 686 | 697 | Object.keys(attachmentPreviewHandoffByMessageId).length === 0 |
| 687 | | ? serverMessages |
| 688 | | : serverMessages.map((message) => { |
| 689 | | if (message.role !== "user" || !message.attachments || message.attachments.length === 0) { |
| 698 | ? messages |
| 699 | : // Spread only fires for the few messages that actually changed; |
| 700 | // unchanged ones early-return their original reference. |
| 701 | // In-place mutation would break React's immutable state contract. |
| 702 | // oxlint-disable-next-line no-map-spread |
| 703 | messages.map((message) => { |
| 704 | if ( |
| 705 | message.role !== "user" || |
| 706 | !message.attachments || |
| 707 | message.attachments.length === 0 |
| 708 | ) { |
| 690 | 709 | return message; |
| 691 | 710 | } |
| 692 | 711 | const handoffPreviewUrls = attachmentPreviewHandoffByMessageId[message.id]; |
| ... | ... | @@ -724,7 +743,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 724 | 743 | return serverMessagesWithPreviewHandoff; |
| 725 | 744 | } |
| 726 | 745 | return [...serverMessagesWithPreviewHandoff, ...pendingMessages]; |
| 727 | | })(); |
| 746 | }, [serverMessages, attachmentPreviewHandoffByMessageId, optimisticUserMessages]); |
| 728 | 747 | const timelineEntries = deriveTimelineEntries(timelineMessages, workLogEntries); |
| 729 | 748 | const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } = |
| 730 | 749 | useTurnDiffSummaries(activeThread); |
| ... | ... | @@ -937,8 +956,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 937 | 956 | (activeThread.messages.length > 0 || |
| 938 | 957 | (activeThread.session !== null && activeThread.session.status !== "closed")), |
| 939 | 958 | ); |
| 940 | | const hasReachedTerminalLimit = |
| 941 | | terminalState.terminalIds.length >= MAX_THREAD_TERMINAL_COUNT; |
| 959 | const hasReachedTerminalLimit = terminalState.terminalIds.length >= MAX_THREAD_TERMINAL_COUNT; |
| 942 | 960 | const setThreadError = useCallback( |
| 943 | 961 | (targetThreadId: ThreadId | null, error: string | null) => { |
| 944 | 962 | if (!targetThreadId) return; |
| ... | ... | @@ -1390,12 +1408,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1390 | 1408 | return () => { |
| 1391 | 1409 | window.clearTimeout(timer); |
| 1392 | 1410 | }; |
| 1393 | | }, [ |
| 1394 | | activeThread?.id, |
| 1395 | | activeThread?.messages, |
| 1396 | | handoffAttachmentPreviews, |
| 1397 | | optimisticUserMessages, |
| 1398 | | ]); |
| 1411 | }, [activeThread?.id, activeThread?.messages, handoffAttachmentPreviews, optimisticUserMessages]); |
| 1399 | 1412 | |
| 1400 | 1413 | useEffect(() => { |
| 1401 | 1414 | promptRef.current = prompt; |
| ... | ... | @@ -1810,7 +1823,10 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1810 | 1823 | const shouldCreateWorktree = |
| 1811 | 1824 | isFirstMessage && envMode === "worktree" && !activeThread.worktreePath; |
| 1812 | 1825 | if (shouldCreateWorktree && !activeThread.branch) { |
| 1813 | | setStoreThreadError(threadIdForSend, "Select a base branch before sending in New worktree mode."); |
| 1826 | setStoreThreadError( |
| 1827 | threadIdForSend, |
| 1828 | "Select a base branch before sending in New worktree mode.", |
| 1829 | ); |
| 1814 | 1830 | return; |
| 1815 | 1831 | } |
| 1816 | 1832 | |
| ... | ... | @@ -1965,8 +1981,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1965 | 1981 | setSendPhase("sending-turn"); |
| 1966 | 1982 | const turnAttachments = await turnAttachmentsPromise; |
| 1967 | 1983 | const approvalPolicy = runtimeMode === "full-access" ? "never" : "on-request"; |
| 1968 | | const sandboxMode = |
| 1969 | | runtimeMode === "full-access" ? "danger-full-access" : "workspace-write"; |
| 1984 | const sandboxMode = runtimeMode === "full-access" ? "danger-full-access" : "workspace-write"; |
| 1970 | 1985 | await api.orchestration.dispatchCommand({ |
| 1971 | 1986 | type: "thread.turn.start", |
| 1972 | 1987 | commandId: newCommandId(), |
| ... | ... | @@ -2169,11 +2184,14 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2169 | 2184 | workspaceEntriesQuery.isLoading || |
| 2170 | 2185 | workspaceEntriesQuery.isFetching); |
| 2171 | 2186 | |
| 2172 | | const onPromptChange = useCallback((nextPrompt: string, nextCursor: number) => { |
| 2173 | | promptRef.current = nextPrompt; |
| 2174 | | setPrompt(nextPrompt); |
| 2175 | | setComposerCursor(nextCursor); |
| 2176 | | }, [setComposerCursor, setPrompt]); |
| 2187 | const onPromptChange = useCallback( |
| 2188 | (nextPrompt: string, nextCursor: number) => { |
| 2189 | promptRef.current = nextPrompt; |
| 2190 | setPrompt(nextPrompt); |
| 2191 | setComposerCursor(nextCursor); |
| 2192 | }, |
| 2193 | [setComposerCursor, setPrompt], |
| 2194 | ); |
| 2177 | 2195 | |
| 2178 | 2196 | const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { |
| 2179 | 2197 | if (composerMenuOpen && composerMenuItems.length > 0) { |
| ... | ... | @@ -2402,8 +2420,12 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2402 | 2420 | </span> |
| 2403 | 2421 | } |
| 2404 | 2422 | /> |
| 2405 | | <TooltipPopup side="top" className="max-w-64 whitespace-normal leading-tight"> |
| 2406 | | Draft attachment could not be saved locally and may be lost on navigation. |
| 2423 | <TooltipPopup |
| 2424 | side="top" |
| 2425 | className="max-w-64 whitespace-normal leading-tight" |
| 2426 | > |
| 2427 | Draft attachment could not be saved locally and may be lost on |
| 2428 | navigation. |
| 2407 | 2429 | </TooltipPopup> |
| 2408 | 2430 | </Tooltip> |
| 2409 | 2431 | )} |