| ... | ... | @@ -485,7 +485,6 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 485 | 485 | const [sendPhase, setSendPhase] = useState<SendPhase>("idle"); |
| 486 | 486 | const [isConnecting, _setIsConnecting] = useState(false); |
| 487 | 487 | const [isRevertingCheckpoint, setIsRevertingCheckpoint] = useState(false); |
| 488 | | const [envMode, setEnvMode] = useState<DraftThreadEnvMode>("local"); |
| 489 | 488 | const [isSwitchingRuntimeMode, setIsSwitchingRuntimeMode] = useState(false); |
| 490 | 489 | const [respondingRequestIds, setRespondingRequestIds] = useState<ApprovalRequestId[]>([]); |
| 491 | 490 | const [expandedWorkGroups, setExpandedWorkGroups] = useState<Record<string, boolean>>({}); |
| ... | ... | @@ -505,7 +504,6 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 505 | 504 | const textareaRef = useRef<HTMLTextAreaElement>(null); |
| 506 | 505 | const composerFormRef = useRef<HTMLFormElement>(null); |
| 507 | 506 | const composerFormHeightRef = useRef(0); |
| 508 | | const composerCommandInputRef = useRef<HTMLInputElement>(null); |
| 509 | 507 | const composerImagesRef = useRef<ComposerImageAttachment[]>([]); |
| 510 | 508 | const attachmentPreviewHandoffByMessageIdRef = useRef<Record<string, string[]>>({}); |
| 511 | 509 | const attachmentPreviewHandoffTimeoutByMessageIdRef = useRef<Record<string, number>>({}); |
| ... | ... | @@ -558,7 +556,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 558 | 556 | |
| 559 | 557 | const serverThread = threads.find((t) => t.id === threadId); |
| 560 | 558 | const fallbackDraftProject = projects.find((project) => project.id === draftThread?.projectId); |
| 561 | | const localDraftError = localDraftErrorsByThreadId[threadId] ?? null; |
| 559 | const localDraftError = serverThread ? null : (localDraftErrorsByThreadId[threadId] ?? null); |
| 562 | 560 | const localDraftThread = useMemo( |
| 563 | 561 | () => |
| 564 | 562 | draftThread |
| ... | ... | @@ -584,19 +582,6 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 584 | 582 | const latestTurnSettled = isLatestTurnSettled(activeLatestTurn, activeThread?.session ?? null); |
| 585 | 583 | const activeProject = projects.find((p) => p.id === activeThread?.projectId); |
| 586 | 584 | |
| 587 | | useEffect(() => { |
| 588 | | if (!serverThread) { |
| 589 | | return; |
| 590 | | } |
| 591 | | setLocalDraftErrorsByThreadId((existing) => { |
| 592 | | if (!(threadId in existing)) { |
| 593 | | return existing; |
| 594 | | } |
| 595 | | const { [threadId]: _removed, ...rest } = existing; |
| 596 | | return rest as Record<ThreadId, string | null>; |
| 597 | | }); |
| 598 | | }, [serverThread, threadId]); |
| 599 | | |
| 600 | 585 | useEffect(() => { |
| 601 | 586 | if (!activeThread?.id) return; |
| 602 | 587 | if (!latestTurnSettled) return; |
| ... | ... | @@ -618,27 +603,16 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 618 | 603 | const baseThreadModel = resolveModelSlug(activeThread?.model ?? activeProject?.model ?? DEFAULT_MODEL); |
| 619 | 604 | const selectedModel = resolveModelSlug(composerDraft.model ?? baseThreadModel); |
| 620 | 605 | const selectedEffort = composerDraft.effort ?? DEFAULT_REASONING; |
| 621 | | const modelOptions = useMemo( |
| 622 | | () => getAppModelOptions(settings.customCodexModels, selectedModel), |
| 623 | | [selectedModel, settings.customCodexModels], |
| 624 | | ); |
| 606 | const modelOptions = getAppModelOptions(settings.customCodexModels, selectedModel); |
| 625 | 607 | const phase = derivePhase(activeThread?.session ?? null); |
| 626 | 608 | const isSendBusy = sendPhase !== "idle"; |
| 627 | 609 | const isPreparingWorktree = sendPhase === "preparing-worktree"; |
| 628 | 610 | const isWorking = phase === "running" || isSendBusy || isConnecting || isRevertingCheckpoint; |
| 629 | 611 | const nowIso = new Date(nowTick).toISOString(); |
| 630 | 612 | const threadActivities = activeThread?.activities ?? []; |
| 631 | | const workLogEntries = useMemo( |
| 632 | | () => deriveWorkLogEntries(threadActivities, activeLatestTurn?.turnId ?? undefined), |
| 633 | | [activeLatestTurn?.turnId, threadActivities], |
| 634 | | ); |
| 635 | | const latestTurnHasToolActivity = useMemo(() => { |
| 636 | | return hasToolActivityForTurn(threadActivities, activeLatestTurn?.turnId); |
| 637 | | }, [activeLatestTurn?.turnId, threadActivities]); |
| 638 | | const pendingApprovals = useMemo( |
| 639 | | () => derivePendingApprovals(threadActivities), |
| 640 | | [threadActivities], |
| 641 | | ); |
| 613 | const workLogEntries = deriveWorkLogEntries(threadActivities, activeLatestTurn?.turnId ?? undefined); |
| 614 | const latestTurnHasToolActivity = hasToolActivityForTurn(threadActivities, activeLatestTurn?.turnId); |
| 615 | const pendingApprovals = derivePendingApprovals(threadActivities); |
| 642 | 616 | useEffect(() => { |
| 643 | 617 | attachmentPreviewHandoffByMessageIdRef.current = attachmentPreviewHandoffByMessageId; |
| 644 | 618 | }, [attachmentPreviewHandoffByMessageId]); |
| ... | ... | @@ -655,6 +629,11 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 655 | 629 | attachmentPreviewHandoffByMessageIdRef.current = {}; |
| 656 | 630 | setAttachmentPreviewHandoffByMessageId({}); |
| 657 | 631 | }, []); |
| 632 | useEffect(() => { |
| 633 | return () => { |
| 634 | clearAttachmentPreviewHandoffs(); |
| 635 | }; |
| 636 | }, [clearAttachmentPreviewHandoffs]); |
| 658 | 637 | const handoffAttachmentPreviews = useCallback((messageId: MessageId, previewUrls: string[]) => { |
| 659 | 638 | if (previewUrls.length === 0) return; |
| 660 | 639 | |
| ... | ... | @@ -686,14 +665,15 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 686 | 665 | } |
| 687 | 666 | setAttachmentPreviewHandoffByMessageId((existing) => { |
| 688 | 667 | if (!(messageId in existing)) return existing; |
| 689 | | const { [messageId]: _removed, ...rest } = existing; |
| 690 | | attachmentPreviewHandoffByMessageIdRef.current = rest; |
| 691 | | return rest; |
| 668 | const next = { ...existing }; |
| 669 | delete next[messageId]; |
| 670 | attachmentPreviewHandoffByMessageIdRef.current = next; |
| 671 | return next; |
| 692 | 672 | }); |
| 693 | 673 | delete attachmentPreviewHandoffTimeoutByMessageIdRef.current[messageId]; |
| 694 | 674 | }, ATTACHMENT_PREVIEW_HANDOFF_TTL_MS); |
| 695 | 675 | }, []); |
| 696 | | const timelineMessages = useMemo(() => { |
| 676 | const timelineMessages = (() => { |
| 697 | 677 | const serverMessages = activeThread?.messages ?? []; |
| 698 | 678 | const serverMessagesWithPreviewHandoff = |
| 699 | 679 | Object.keys(attachmentPreviewHandoffByMessageId).length === 0 |
| ... | ... | @@ -737,11 +717,8 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 737 | 717 | return serverMessagesWithPreviewHandoff; |
| 738 | 718 | } |
| 739 | 719 | return [...serverMessagesWithPreviewHandoff, ...pendingMessages]; |
| 740 | | }, [activeThread?.messages, attachmentPreviewHandoffByMessageId, optimisticUserMessages]); |
| 741 | | const timelineEntries = useMemo( |
| 742 | | () => deriveTimelineEntries(timelineMessages, workLogEntries), |
| 743 | | [timelineMessages, workLogEntries], |
| 744 | | ); |
| 720 | })(); |
| 721 | const timelineEntries = deriveTimelineEntries(timelineMessages, workLogEntries); |
| 745 | 722 | const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } = |
| 746 | 723 | useTurnDiffSummaries(activeThread); |
| 747 | 724 | const turnDiffSummaryByAssistantMessageId = useMemo(() => { |
| ... | ... | @@ -752,7 +729,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 752 | 729 | } |
| 753 | 730 | return byMessageId; |
| 754 | 731 | }, [turnDiffSummaries]); |
| 755 | | const revertTurnCountByUserMessageId = useMemo(() => { |
| 732 | const revertTurnCountByUserMessageId = (() => { |
| 756 | 733 | const byUserMessageId = new Map<MessageId, number>(); |
| 757 | 734 | for (let index = 0; index < timelineEntries.length; index += 1) { |
| 758 | 735 | const entry = timelineEntries[index]; |
| ... | ... | @@ -783,9 +760,9 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 783 | 760 | } |
| 784 | 761 | |
| 785 | 762 | return byUserMessageId; |
| 786 | | }, [inferredCheckpointTurnCountByTurnId, timelineEntries, turnDiffSummaryByAssistantMessageId]); |
| 763 | })(); |
| 787 | 764 | |
| 788 | | const completionSummary = useMemo(() => { |
| 765 | const completionSummary = (() => { |
| 789 | 766 | if (!latestTurnSettled) return null; |
| 790 | 767 | if (!activeLatestTurn?.startedAt) return null; |
| 791 | 768 | if (!activeLatestTurn.completedAt) return null; |
| ... | ... | @@ -793,13 +770,8 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 793 | 770 | |
| 794 | 771 | const elapsed = formatElapsed(activeLatestTurn.startedAt, activeLatestTurn.completedAt); |
| 795 | 772 | return elapsed ? `Worked for ${elapsed}` : null; |
| 796 | | }, [ |
| 797 | | activeLatestTurn?.completedAt, |
| 798 | | activeLatestTurn?.startedAt, |
| 799 | | latestTurnHasToolActivity, |
| 800 | | latestTurnSettled, |
| 801 | | ]); |
| 802 | | const completionDividerBeforeEntryId = useMemo(() => { |
| 773 | })(); |
| 774 | const completionDividerBeforeEntryId = (() => { |
| 803 | 775 | if (!latestTurnSettled) return null; |
| 804 | 776 | if (!activeLatestTurn?.startedAt) return null; |
| 805 | 777 | if (!activeLatestTurn.completedAt) return null; |
| ... | ... | @@ -823,13 +795,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 823 | 795 | } |
| 824 | 796 | } |
| 825 | 797 | return inRangeMatch ?? fallbackMatch; |
| 826 | | }, [ |
| 827 | | activeLatestTurn?.completedAt, |
| 828 | | activeLatestTurn?.startedAt, |
| 829 | | completionSummary, |
| 830 | | latestTurnSettled, |
| 831 | | timelineEntries, |
| 832 | | ]); |
| 798 | })(); |
| 833 | 799 | const gitCwd = activeThread?.worktreePath ?? activeProject?.cwd ?? null; |
| 834 | 800 | const composerTrigger = useMemo( |
| 835 | 801 | () => detectComposerTrigger(prompt, composerCursor), |
| ... | ... | @@ -918,15 +884,17 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 918 | 884 | [nonPersistedComposerImageIds], |
| 919 | 885 | ); |
| 920 | 886 | const keybindings = keybindingsQuery.data ?? EMPTY_KEYBINDINGS; |
| 887 | const activeProjectCwd = activeProject?.cwd ?? null; |
| 888 | const activeThreadWorktreePath = activeThread?.worktreePath ?? null; |
| 921 | 889 | const threadTerminalRuntimeEnv = useMemo(() => { |
| 922 | | if (!activeProject?.cwd) return {}; |
| 890 | if (!activeProjectCwd) return {}; |
| 923 | 891 | return projectScriptRuntimeEnv({ |
| 924 | 892 | project: { |
| 925 | | cwd: activeProject.cwd, |
| 893 | cwd: activeProjectCwd, |
| 926 | 894 | }, |
| 927 | | worktreePath: activeThread?.worktreePath ?? null, |
| 895 | worktreePath: activeThreadWorktreePath, |
| 928 | 896 | }); |
| 929 | | }, [activeProject?.cwd, activeThread?.worktreePath]); |
| 897 | }, [activeProjectCwd, activeThreadWorktreePath]); |
| 930 | 898 | // Default true while loading to avoid toolbar flicker. |
| 931 | 899 | const isGitRepo = branchesQuery.data?.isRepo ?? true; |
| 932 | 900 | const splitTerminalShortcutLabel = useMemo( |
| ... | ... | @@ -1109,20 +1077,24 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1109 | 1077 | worktreePath: options?.worktreePath ?? activeThread.worktreePath ?? null, |
| 1110 | 1078 | ...(options?.env ? { extraEnv: options.env } : {}), |
| 1111 | 1079 | }); |
| 1080 | const openTerminalInput: Parameters<typeof api.terminal.open>[0] = shouldCreateNewTerminal |
| 1081 | ? { |
| 1082 | threadId: activeThreadId, |
| 1083 | terminalId: targetTerminalId, |
| 1084 | cwd: targetCwd, |
| 1085 | env: runtimeEnv, |
| 1086 | cols: SCRIPT_TERMINAL_COLS, |
| 1087 | rows: SCRIPT_TERMINAL_ROWS, |
| 1088 | } |
| 1089 | : { |
| 1090 | threadId: activeThreadId, |
| 1091 | terminalId: targetTerminalId, |
| 1092 | cwd: targetCwd, |
| 1093 | env: runtimeEnv, |
| 1094 | }; |
| 1112 | 1095 | |
| 1113 | 1096 | try { |
| 1114 | | await api.terminal.open({ |
| 1115 | | threadId: activeThreadId, |
| 1116 | | terminalId: targetTerminalId, |
| 1117 | | cwd: targetCwd, |
| 1118 | | env: runtimeEnv, |
| 1119 | | ...(shouldCreateNewTerminal |
| 1120 | | ? { |
| 1121 | | cols: SCRIPT_TERMINAL_COLS, |
| 1122 | | rows: SCRIPT_TERMINAL_ROWS, |
| 1123 | | } |
| 1124 | | : {}), |
| 1125 | | }); |
| 1097 | await api.terminal.open(openTerminalInput); |
| 1126 | 1098 | await api.terminal.write({ |
| 1127 | 1099 | threadId: activeThreadId, |
| 1128 | 1100 | terminalId: targetTerminalId, |
| ... | ... | @@ -1140,12 +1112,14 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1140 | 1112 | activeThread, |
| 1141 | 1113 | activeThreadId, |
| 1142 | 1114 | gitCwd, |
| 1143 | | isLocalDraftThread, |
| 1144 | 1115 | isServerThread, |
| 1145 | 1116 | setTerminalOpen, |
| 1146 | 1117 | setThreadError, |
| 1147 | 1118 | storeNewTerminal, |
| 1148 | 1119 | storeSetActiveTerminal, |
| 1120 | terminalState.activeTerminalId, |
| 1121 | terminalState.runningTerminalIds, |
| 1122 | terminalState.terminalIds, |
| 1149 | 1123 | ], |
| 1150 | 1124 | ); |
| 1151 | 1125 | const persistProjectScripts = useCallback( |
| ... | ... | @@ -1369,24 +1343,6 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1369 | 1343 | scheduleStickToBottom(); |
| 1370 | 1344 | }, [phase, scheduleStickToBottom, timelineEntries]); |
| 1371 | 1345 | |
| 1372 | | useEffect(() => { |
| 1373 | | setExpandedWorkGroups({}); |
| 1374 | | }, [activeThread?.id]); |
| 1375 | | |
| 1376 | | useEffect(() => { |
| 1377 | | if (!composerMenuOpen) { |
| 1378 | | setComposerHighlightedItemId(null); |
| 1379 | | return; |
| 1380 | | } |
| 1381 | | setComposerHighlightedItemId((existing) => |
| 1382 | | existing && composerMenuItems.some((item) => item.id === existing) ? existing : null, |
| 1383 | | ); |
| 1384 | | }, [composerMenuItems, composerMenuOpen]); |
| 1385 | | |
| 1386 | | useEffect(() => { |
| 1387 | | setIsRevertingCheckpoint(false); |
| 1388 | | }, [activeThread?.id]); |
| 1389 | | |
| 1390 | 1346 | useEffect(() => { |
| 1391 | 1347 | if (!activeThread?.id || terminalState.terminalOpen) return; |
| 1392 | 1348 | const frame = window.requestAnimationFrame(() => { |
| ... | ... | @@ -1402,16 +1358,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1402 | 1358 | }, [composerImages]); |
| 1403 | 1359 | |
| 1404 | 1360 | useEffect(() => { |
| 1405 | | if (!activeThread?.id) { |
| 1406 | | clearAttachmentPreviewHandoffs(); |
| 1407 | | setOptimisticUserMessages((existing) => { |
| 1408 | | for (const message of existing) { |
| 1409 | | revokeUserMessagePreviewUrls(message); |
| 1410 | | } |
| 1411 | | return []; |
| 1412 | | }); |
| 1413 | | return; |
| 1414 | | } |
| 1361 | if (!activeThread?.id) return; |
| 1415 | 1362 | if (activeThread.messages.length === 0) { |
| 1416 | 1363 | return; |
| 1417 | 1364 | } |
| ... | ... | @@ -1420,7 +1367,11 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1420 | 1367 | if (removedMessages.length === 0) { |
| 1421 | 1368 | return; |
| 1422 | 1369 | } |
| 1423 | | setOptimisticUserMessages((existing) => existing.filter((message) => !serverIds.has(message.id))); |
| 1370 | const timer = window.setTimeout(() => { |
| 1371 | setOptimisticUserMessages((existing) => |
| 1372 | existing.filter((message) => !serverIds.has(message.id)), |
| 1373 | ); |
| 1374 | }, 0); |
| 1424 | 1375 | for (const removedMessage of removedMessages) { |
| 1425 | 1376 | const previewUrls = collectUserMessageBlobPreviewUrls(removedMessage); |
| 1426 | 1377 | if (previewUrls.length > 0) { |
| ... | ... | @@ -1429,10 +1380,12 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1429 | 1380 | } |
| 1430 | 1381 | revokeUserMessagePreviewUrls(removedMessage); |
| 1431 | 1382 | } |
| 1383 | return () => { |
| 1384 | window.clearTimeout(timer); |
| 1385 | }; |
| 1432 | 1386 | }, [ |
| 1433 | 1387 | activeThread?.id, |
| 1434 | 1388 | activeThread?.messages, |
| 1435 | | clearAttachmentPreviewHandoffs, |
| 1436 | 1389 | handoffAttachmentPreviews, |
| 1437 | 1390 | optimisticUserMessages, |
| 1438 | 1391 | ]); |
| ... | ... | @@ -1441,21 +1394,6 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1441 | 1394 | promptRef.current = prompt; |
| 1442 | 1395 | }, [prompt]); |
| 1443 | 1396 | |
| 1444 | | useEffect(() => { |
| 1445 | | clearAttachmentPreviewHandoffs(); |
| 1446 | | setOptimisticUserMessages((existing) => { |
| 1447 | | for (const message of existing) { |
| 1448 | | revokeUserMessagePreviewUrls(message); |
| 1449 | | } |
| 1450 | | return []; |
| 1451 | | }); |
| 1452 | | setSendPhase("idle"); |
| 1453 | | setComposerHighlightedItemId(null); |
| 1454 | | dragDepthRef.current = 0; |
| 1455 | | setIsDragOverComposer(false); |
| 1456 | | setExpandedImage(null); |
| 1457 | | }, [clearAttachmentPreviewHandoffs, threadId]); |
| 1458 | | |
| 1459 | 1397 | useEffect(() => { |
| 1460 | 1398 | let cancelled = false; |
| 1461 | 1399 | void (async () => { |
| ... | ... | @@ -1463,9 +1401,10 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1463 | 1401 | clearComposerDraftPersistedAttachments(threadId); |
| 1464 | 1402 | return; |
| 1465 | 1403 | } |
| 1404 | const getPersistedAttachmentsForThread = () => |
| 1405 | useComposerDraftStore.getState().draftsByThreadId[threadId]?.persistedAttachments ?? []; |
| 1466 | 1406 | try { |
| 1467 | | const currentPersistedAttachments = |
| 1468 | | useComposerDraftStore.getState().draftsByThreadId[threadId]?.persistedAttachments ?? []; |
| 1407 | const currentPersistedAttachments = getPersistedAttachmentsForThread(); |
| 1469 | 1408 | const existingPersistedById = new Map( |
| 1470 | 1409 | currentPersistedAttachments.map((attachment) => [attachment.id, attachment]), |
| 1471 | 1410 | ); |
| ... | ... | @@ -1497,8 +1436,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1497 | 1436 | syncComposerDraftPersistedAttachments(threadId, serialized); |
| 1498 | 1437 | } catch { |
| 1499 | 1438 | const currentImageIds = new Set(composerImages.map((image) => image.id)); |
| 1500 | | const fallbackPersistedAttachments = |
| 1501 | | useComposerDraftStore.getState().draftsByThreadId[threadId]?.persistedAttachments ?? []; |
| 1439 | const fallbackPersistedAttachments = getPersistedAttachmentsForThread(); |
| 1502 | 1440 | const fallbackPersistedIds = fallbackPersistedAttachments |
| 1503 | 1441 | .map((attachment) => attachment.id) |
| 1504 | 1442 | .filter((id) => currentImageIds.has(id)); |
| ... | ... | @@ -1571,19 +1509,11 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1571 | 1509 | }, [closeExpandedImage, expandedImage, navigateExpandedImage]); |
| 1572 | 1510 | |
| 1573 | 1511 | const activeWorktreePath = activeThread?.worktreePath; |
| 1574 | | |
| 1575 | | useEffect(() => { |
| 1576 | | if (!activeThread?.id) return; |
| 1577 | | if (activeWorktreePath) { |
| 1578 | | setEnvMode("worktree"); |
| 1579 | | return; |
| 1580 | | } |
| 1581 | | if (isLocalDraftThread) { |
| 1582 | | setEnvMode(draftThread?.envMode ?? "local"); |
| 1583 | | return; |
| 1584 | | } |
| 1585 | | setEnvMode("local"); |
| 1586 | | }, [activeThread?.id, activeWorktreePath, draftThread?.envMode, isLocalDraftThread]); |
| 1512 | const envMode: DraftThreadEnvMode = activeWorktreePath |
| 1513 | ? "worktree" |
| 1514 | : isLocalDraftThread |
| 1515 | ? (draftThread?.envMode ?? "local") |
| 1516 | : "local"; |
| 1587 | 1517 | |
| 1588 | 1518 | // Auto-resize textarea |
| 1589 | 1519 | useEffect(() => { |
| ... | ... | @@ -1609,7 +1539,8 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1609 | 1539 | const current = Boolean(terminalState.terminalOpen); |
| 1610 | 1540 | |
| 1611 | 1541 | if (!previous && current) { |
| 1612 | | setTerminalFocusRequestId((value) => value + 1); |
| 1542 | terminalOpenByThreadRef.current[activeThreadId] = current; |
| 1543 | return; |
| 1613 | 1544 | } else if (previous && !current) { |
| 1614 | 1545 | terminalOpenByThreadRef.current[activeThreadId] = current; |
| 1615 | 1546 | const frame = window.requestAnimationFrame(() => { |
| ... | ... | @@ -1922,7 +1853,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1922 | 1853 | let turnStartSucceeded = false; |
| 1923 | 1854 | let nextThreadBranch = activeThread.branch; |
| 1924 | 1855 | let nextThreadWorktreePath = activeThread.worktreePath; |
| 1925 | | try { |
| 1856 | await (async () => { |
| 1926 | 1857 | // On first message: lock in branch + create worktree if needed. |
| 1927 | 1858 | if (baseBranchForWorktree) { |
| 1928 | 1859 | setSendPhase("preparing-worktree"); |
| ... | ... | @@ -1948,12 +1879,29 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1948 | 1879 | } |
| 1949 | 1880 | } |
| 1950 | 1881 | |
| 1951 | | const titleSeed = |
| 1952 | | trimmed || |
| 1953 | | (composerImagesSnapshot.length > 0 |
| 1954 | | ? `Image: ${composerImagesSnapshot[0]?.name ?? "attachment"}` |
| 1955 | | : "New thread"); |
| 1882 | let firstComposerImageName: string | null = null; |
| 1883 | if (composerImagesSnapshot.length > 0) { |
| 1884 | const firstComposerImage = composerImagesSnapshot[0]; |
| 1885 | if (firstComposerImage) { |
| 1886 | firstComposerImageName = firstComposerImage.name; |
| 1887 | } |
| 1888 | } |
| 1889 | let titleSeed = trimmed; |
| 1890 | if (!titleSeed) { |
| 1891 | if (firstComposerImageName) { |
| 1892 | titleSeed = `Image: ${firstComposerImageName}`; |
| 1893 | } else { |
| 1894 | titleSeed = "New thread"; |
| 1895 | } |
| 1896 | } |
| 1956 | 1897 | const title = truncateTitle(titleSeed); |
| 1898 | let threadCreateModel = selectedModel; |
| 1899 | if (!threadCreateModel) { |
| 1900 | threadCreateModel = activeProject.model; |
| 1901 | } |
| 1902 | if (!threadCreateModel) { |
| 1903 | threadCreateModel = DEFAULT_MODEL; |
| 1904 | } |
| 1957 | 1905 | |
| 1958 | 1906 | if (isLocalDraftThread) { |
| 1959 | 1907 | await api.orchestration.dispatchCommand({ |
| ... | ... | @@ -1962,7 +1910,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1962 | 1910 | threadId: threadIdForSend, |
| 1963 | 1911 | projectId: activeProject.id, |
| 1964 | 1912 | title, |
| 1965 | | model: selectedModel ?? activeProject.model ?? DEFAULT_MODEL, |
| 1913 | model: threadCreateModel, |
| 1966 | 1914 | branch: nextThreadBranch, |
| 1967 | 1915 | worktreePath: nextThreadWorktreePath, |
| 1968 | 1916 | createdAt: activeThread.createdAt, |
| ... | ... | @@ -1970,16 +1918,30 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1970 | 1918 | createdServerThreadForLocalDraft = true; |
| 1971 | 1919 | } |
| 1972 | 1920 | |
| 1973 | | const setupScript = baseBranchForWorktree |
| 1974 | | ? setupProjectScript(activeProject.scripts) |
| 1975 | | : null; |
| 1976 | | if (setupScript && (isServerThread || createdServerThreadForLocalDraft)) { |
| 1977 | | await runProjectScript(setupScript, { |
| 1978 | | worktreePath: nextThreadWorktreePath, |
| 1979 | | rememberAsLastInvoked: false, |
| 1980 | | allowLocalDraftThread: createdServerThreadForLocalDraft, |
| 1981 | | ...(nextThreadWorktreePath ? { cwd: nextThreadWorktreePath } : {}), |
| 1982 | | }); |
| 1921 | let setupScript: ProjectScript | null = null; |
| 1922 | if (baseBranchForWorktree) { |
| 1923 | setupScript = setupProjectScript(activeProject.scripts); |
| 1924 | } |
| 1925 | if (setupScript) { |
| 1926 | let shouldRunSetupScript = false; |
| 1927 | if (isServerThread) { |
| 1928 | shouldRunSetupScript = true; |
| 1929 | } else { |
| 1930 | if (createdServerThreadForLocalDraft) { |
| 1931 | shouldRunSetupScript = true; |
| 1932 | } |
| 1933 | } |
| 1934 | if (shouldRunSetupScript) { |
| 1935 | const setupScriptOptions: Parameters<typeof runProjectScript>[1] = { |
| 1936 | worktreePath: nextThreadWorktreePath, |
| 1937 | rememberAsLastInvoked: false, |
| 1938 | allowLocalDraftThread: createdServerThreadForLocalDraft, |
| 1939 | }; |
| 1940 | if (nextThreadWorktreePath) { |
| 1941 | setupScriptOptions.cwd = nextThreadWorktreePath; |
| 1942 | } |
| 1943 | await runProjectScript(setupScript, setupScriptOptions); |
| 1944 | } |
| 1983 | 1945 | } |
| 1984 | 1946 | |
| 1985 | 1947 | // Auto-title from first message |
| ... | ... | @@ -2018,7 +1980,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2018 | 1980 | if (isFirstMessage) { |
| 2019 | 1981 | clearDraftThread(threadIdForSend); |
| 2020 | 1982 | } |
| 2021 | | } catch (err) { |
| 1983 | })().catch(async (err: unknown) => { |
| 2022 | 1984 | if (createdServerThreadForLocalDraft && !turnStartSucceeded) { |
| 2023 | 1985 | await api.orchestration |
| 2024 | 1986 | .dispatchCommand({ |
| ... | ... | @@ -2050,7 +2012,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2050 | 2012 | threadIdForSend, |
| 2051 | 2013 | err instanceof Error ? err.message : "Failed to send message.", |
| 2052 | 2014 | ); |
| 2053 | | } |
| 2015 | }); |
| 2054 | 2016 | sendInFlightRef.current = false; |
| 2055 | 2017 | setSendPhase("idle"); |
| 2056 | 2018 | }; |
| ... | ... | @@ -2074,23 +2036,21 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2074 | 2036 | setRespondingRequestIds((existing) => |
| 2075 | 2037 | existing.includes(requestId) ? existing : [...existing, requestId], |
| 2076 | 2038 | ); |
| 2077 | | try { |
| 2078 | | await api.orchestration.dispatchCommand({ |
| 2039 | await api.orchestration |
| 2040 | .dispatchCommand({ |
| 2079 | 2041 | type: "thread.approval.respond", |
| 2080 | 2042 | commandId: newCommandId(), |
| 2081 | 2043 | threadId: activeThreadId, |
| 2082 | 2044 | requestId, |
| 2083 | 2045 | decision, |
| 2084 | 2046 | createdAt: new Date().toISOString(), |
| 2047 | }) |
| 2048 | .catch((err: unknown) => { |
| 2049 | setStoreThreadError( |
| 2050 | activeThreadId, |
| 2051 | err instanceof Error ? err.message : "Failed to submit approval decision.", |
| 2052 | ); |
| 2085 | 2053 | }); |
| 2086 | | } catch (err) { |
| 2087 | | setStoreThreadError( |
| 2088 | | activeThreadId, |
| 2089 | | err instanceof Error ? err.message : "Failed to submit approval decision.", |
| 2090 | | ); |
| 2091 | | } finally { |
| 2092 | | setRespondingRequestIds((existing) => existing.filter((id) => id !== requestId)); |
| 2093 | | } |
| 2094 | 2054 | setRespondingRequestIds((existing) => existing.filter((id) => id !== requestId)); |
| 2095 | 2055 | }, |
| 2096 | 2056 | [activeThreadId, setStoreThreadError], |
| ... | ... | @@ -2130,7 +2090,6 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2130 | 2090 | ); |
| 2131 | 2091 | const onEnvModeChange = useCallback( |
| 2132 | 2092 | (mode: DraftThreadEnvMode) => { |
| 2133 | | setEnvMode(mode); |
| 2134 | 2093 | if (isLocalDraftThread) { |
| 2135 | 2094 | setDraftThreadContext(threadId, { envMode: mode }); |
| 2136 | 2095 | } |
| ... | ... | @@ -2260,16 +2219,13 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 2260 | 2219 | }, |
| 2261 | 2220 | [navigate, threadId], |
| 2262 | 2221 | ); |
| 2263 | | const onRevertUserMessage = useCallback( |
| 2264 | | (messageId: MessageId) => { |
| 2265 | | const targetTurnCount = revertTurnCountByUserMessageId.get(messageId); |
| 2266 | | if (typeof targetTurnCount !== "number") { |
| 2267 | | return; |
| 2268 | | } |
| 2269 | | void onRevertToTurnCount(targetTurnCount); |
| 2270 | | }, |
| 2271 | | [onRevertToTurnCount, revertTurnCountByUserMessageId], |
| 2272 | | ); |
| 2222 | const onRevertUserMessage = (messageId: MessageId) => { |
| 2223 | const targetTurnCount = revertTurnCountByUserMessageId.get(messageId); |
| 2224 | if (typeof targetTurnCount !== "number") { |
| 2225 | return; |
| 2226 | } |
| 2227 | void onRevertToTurnCount(targetTurnCount); |
| 2228 | }; |
| 2273 | 2229 | |
| 2274 | 2230 | // Empty state: no active thread |
| 2275 | 2231 | if (!activeThread) { |