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