| author | |
| committer | |
| log | 32fe80d1647ea9fb466fe5fab67b74fea78f011b |
| tree | 28562e51f579392a7960571431efca89630d3f0b |
| parent | 6e93d42023a61f638d7a83c50083a18040f136c1 |
- Add setTerminalFocusRequestId call when terminal opens via toggle so the
terminal panel receives focus automatically
- Call clearTerminalState when deleting threads in the Sidebar and add
removeOrphanedTerminalStates to prune stale entries after read model sync
- Track optimisticUserMessages via ref and revoke their blob URLs in the
unmount cleanup effect to prevent memory leaks
Co-authored-by: Julius Marminge <juliusmarminge@users.noreply.github.com>4 files changed, 31 insertions(+), 0 deletions(-)
apps/web/src/components/ChatView.tsx+6| ... | @@ -479,6 +479,8 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -479,6 +479,8 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 479 | const [isDragOverComposer, setIsDragOverComposer] = useState(false); | 479 | const [isDragOverComposer, setIsDragOverComposer] = useState(false); |
| 480 | const [expandedImage, setExpandedImage] = useState<ExpandedImagePreview | null>(null); | 480 | const [expandedImage, setExpandedImage] = useState<ExpandedImagePreview | null>(null); |
| 481 | const [optimisticUserMessages, setOptimisticUserMessages] = useState<ChatMessage[]>([]); | 481 | const [optimisticUserMessages, setOptimisticUserMessages] = useState<ChatMessage[]>([]); |
| 482 | const optimisticUserMessagesRef = useRef(optimisticUserMessages); | ||
| 483 | optimisticUserMessagesRef.current = optimisticUserMessages; | ||
| 482 | const [localDraftErrorsByThreadId, setLocalDraftErrorsByThreadId] = useState< | 484 | const [localDraftErrorsByThreadId, setLocalDraftErrorsByThreadId] = useState< |
| 483 | Record<ThreadId, string | null> | 485 | Record<ThreadId, string | null> |
| 484 | >({}); | 486 | >({}); |
| ... | @@ -634,6 +636,9 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -634,6 +636,9 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 634 | useEffect(() => { | 636 | useEffect(() => { |
| 635 | return () => { | 637 | return () => { |
| 636 | clearAttachmentPreviewHandoffs(); | 638 | clearAttachmentPreviewHandoffs(); |
| 639 | for (const message of optimisticUserMessagesRef.current) { | ||
| 640 | revokeUserMessagePreviewUrls(message); | ||
| 641 | } | ||
| 637 | }; | 642 | }; |
| 638 | }, [clearAttachmentPreviewHandoffs]); | 643 | }, [clearAttachmentPreviewHandoffs]); |
| 639 | const handoffAttachmentPreviews = useCallback((messageId: MessageId, previewUrls: string[]) => { | 644 | const handoffAttachmentPreviews = useCallback((messageId: MessageId, previewUrls: string[]) => { |
| ... | @@ -1542,6 +1547,7 @@ export default function ChatView({ threadId }: ChatViewProps) { | ... | @@ -1542,6 +1547,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1542 | 1547 | ||
| 1543 | if (!previous && current) { | 1548 | if (!previous && current) { |
| 1544 | terminalOpenByThreadRef.current[activeThreadId] = current; | 1549 | terminalOpenByThreadRef.current[activeThreadId] = current; |
| 1550 | setTerminalFocusRequestId((value) => value + 1); | ||
| 1545 | return; | 1551 | return; |
| 1546 | } else if (previous && !current) { | 1552 | } else if (previous && !current) { |
| 1547 | terminalOpenByThreadRef.current[activeThreadId] = current; | 1553 | terminalOpenByThreadRef.current[activeThreadId] = current; |
apps/web/src/components/Sidebar.tsx+3| ... | @@ -241,6 +241,7 @@ export default function Sidebar() { | ... | @@ -241,6 +241,7 @@ export default function Sidebar() { |
| 241 | ); | 241 | ); |
| 242 | const getDraftThread = useComposerDraftStore((store) => store.getDraftThread); | 242 | const getDraftThread = useComposerDraftStore((store) => store.getDraftThread); |
| 243 | const terminalStateByThreadId = useTerminalStateStore((state) => state.terminalStateByThreadId); | 243 | const terminalStateByThreadId = useTerminalStateStore((state) => state.terminalStateByThreadId); |
| 244 | const clearTerminalState = useTerminalStateStore((state) => state.clearTerminalState); | ||
| 244 | const setProjectDraftThreadId = useComposerDraftStore((store) => store.setProjectDraftThreadId); | 245 | const setProjectDraftThreadId = useComposerDraftStore((store) => store.setProjectDraftThreadId); |
| 245 | const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); | 246 | const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext); |
| 246 | const clearProjectDraftThreadId = useComposerDraftStore( | 247 | const clearProjectDraftThreadId = useComposerDraftStore( |
| ... | @@ -578,6 +579,7 @@ export default function Sidebar() { | ... | @@ -578,6 +579,7 @@ export default function Sidebar() { |
| 578 | }); | 579 | }); |
| 579 | clearComposerDraftForThread(threadId); | 580 | clearComposerDraftForThread(threadId); |
| 580 | clearProjectDraftThreadById(thread.projectId, thread.id); | 581 | clearProjectDraftThreadById(thread.projectId, thread.id); |
| 582 | clearTerminalState(threadId); | ||
| 581 | if (shouldNavigateToFallback) { | 583 | if (shouldNavigateToFallback) { |
| 582 | if (fallbackThreadId) { | 584 | if (fallbackThreadId) { |
| 583 | void navigate({ | 585 | void navigate({ |
| ... | @@ -619,6 +621,7 @@ export default function Sidebar() { | ... | @@ -619,6 +621,7 @@ export default function Sidebar() { |
| 619 | appSettings.confirmThreadDelete, | 621 | appSettings.confirmThreadDelete, |
| 620 | clearComposerDraftForThread, | 622 | clearComposerDraftForThread, |
| 621 | clearProjectDraftThreadById, | 623 | clearProjectDraftThreadById, |
| 624 | clearTerminalState, | ||
| 622 | markThreadUnread, | 625 | markThreadUnread, |
| 623 | navigate, | 626 | navigate, |
| 624 | projects, | 627 | projects, |
apps/web/src/routes/__root.tsx+9| ... | @@ -143,11 +143,20 @@ function EventRouter() { | ... | @@ -143,11 +143,20 @@ function EventRouter() { |
| 143 | let syncing = false; | 143 | let syncing = false; |
| 144 | let pending = false; | 144 | let pending = false; |
| 145 | 145 | ||
| 146 | const removeOrphanedTerminalStates = | ||
| 147 | useTerminalStateStore.getState().removeOrphanedTerminalStates; | ||
| 148 | |||
| 146 | const flushSnapshotSync = async (): Promise<void> => { | 149 | const flushSnapshotSync = async (): Promise<void> => { |
| 147 | const snapshot = await api.orchestration.getSnapshot(); | 150 | const snapshot = await api.orchestration.getSnapshot(); |
| 148 | if (disposed) return; | 151 | if (disposed) return; |
| 149 | latestSequence = Math.max(latestSequence, snapshot.snapshotSequence); | 152 | latestSequence = Math.max(latestSequence, snapshot.snapshotSequence); |
| 150 | syncServerReadModel(snapshot); | 153 | syncServerReadModel(snapshot); |
| 154 | const activeThreadIds = new Set( | ||
| 155 | snapshot.threads | ||
| 156 | .filter((t) => t.deletedAt === null) | ||
| 157 | .map((t) => t.id), | ||
| 158 | ); | ||
| 159 | removeOrphanedTerminalStates(activeThreadIds); | ||
| 151 | if (pending) { | 160 | if (pending) { |
| 152 | pending = false; | 161 | pending = false; |
| 153 | await flushSnapshotSync(); | 162 | await flushSnapshotSync(); |
apps/web/src/terminalStateStore.ts+13| ... | @@ -461,6 +461,7 @@ interface TerminalStateStoreState { | ... | @@ -461,6 +461,7 @@ interface TerminalStateStoreState { |
| 461 | hasRunningSubprocess: boolean, | 461 | hasRunningSubprocess: boolean, |
| 462 | ) => void; | 462 | ) => void; |
| 463 | clearTerminalState: (threadId: ThreadId) => void; | 463 | clearTerminalState: (threadId: ThreadId) => void; |
| 464 | removeOrphanedTerminalStates: (activeThreadIds: Set<ThreadId>) => void; | ||
| 464 | } | 465 | } |
| 465 | 466 | ||
| 466 | export const useTerminalStateStore = create<TerminalStateStoreState>()( | 467 | export const useTerminalStateStore = create<TerminalStateStoreState>()( |
| ... | @@ -505,6 +506,18 @@ export const useTerminalStateStore = create<TerminalStateStoreState>()( | ... | @@ -505,6 +506,18 @@ export const useTerminalStateStore = create<TerminalStateStoreState>()( |
| 505 | ), | 506 | ), |
| 506 | clearTerminalState: (threadId) => | 507 | clearTerminalState: (threadId) => |
| 507 | updateTerminal(threadId, () => createDefaultThreadTerminalState()), | 508 | updateTerminal(threadId, () => createDefaultThreadTerminalState()), |
| 509 | removeOrphanedTerminalStates: (activeThreadIds) => | ||
| 510 | set((state) => { | ||
| 511 | const orphanedIds = Object.keys(state.terminalStateByThreadId).filter( | ||
| 512 | (id) => !activeThreadIds.has(id as ThreadId), | ||
| 513 | ); | ||
| 514 | if (orphanedIds.length === 0) return state; | ||
| 515 | const next = { ...state.terminalStateByThreadId }; | ||
| 516 | for (const id of orphanedIds) { | ||
| 517 | delete next[id as ThreadId]; | ||
| 518 | } | ||
| 519 | return { terminalStateByThreadId: next }; | ||
| 520 | }), | ||
| 508 | }; | 521 | }; |
| 509 | }, | 522 | }, |
| 510 | { | 523 | { |