From df32d3f8227becb14c890cce3cae6f580754667b Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Wed, 18 Feb 2026 21:02:25 +0530 Subject: [PATCH] enhance App and Chat components with improved state management for chat tabs, scroll position preservation, and active state handling; introduce new props for tool management and chat input behavior --- apps/x/apps/renderer/src/App.tsx | 369 +++++++++++++----- .../components/ai-elements/conversation.tsx | 24 +- .../components/ai-elements/prompt-input.tsx | 8 +- .../components/chat-input-with-mentions.tsx | 12 +- .../renderer/src/components/chat-sidebar.tsx | 227 ++++++----- 5 files changed, 417 insertions(+), 223 deletions(-) diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 4932cc6b..c47a841c 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -458,6 +458,8 @@ function ContentHeader({ } function App() { + type ShortcutPane = 'left' | 'right' + // File browser state (for Knowledge section) const [selectedPath, setSelectedPath] = useState(null) const [fileContent, setFileContent] = useState('') @@ -478,6 +480,7 @@ function App() { const [graphError, setGraphError] = useState(null) const [isChatSidebarOpen, setIsChatSidebarOpen] = useState(true) const [isRightPaneMaximized, setIsRightPaneMaximized] = useState(false) + const [activeShortcutPane, setActiveShortcutPane] = useState('left') const isMac = typeof navigator !== 'undefined' && navigator.platform.toLowerCase().includes('mac') const collapsedLeftPaddingPx = (isMac ? MACOS_TRAFFIC_LIGHTS_RESERVED_PX : 0) + @@ -534,16 +537,55 @@ function App() { const chatTabIdCounterRef = useRef(0) const newChatTabId = () => `chat-tab-${++chatTabIdCounterRef.current}` const chatDraftsRef = useRef(new Map()) + const chatScrollTopByTabRef = useRef(new Map()) + const [toolOpenByTab, setToolOpenByTab] = useState>>({}) const activeChatTabIdRef = useRef(activeChatTabId) activeChatTabIdRef.current = activeChatTabId - const handleDraftChange = useCallback((text: string) => { - const tabId = activeChatTabIdRef.current + const setChatDraftForTab = useCallback((tabId: string, text: string) => { if (text) { chatDraftsRef.current.set(tabId, text) } else { chatDraftsRef.current.delete(tabId) } }, []) + const isToolOpenForTab = useCallback((tabId: string, toolId: string): boolean => { + return toolOpenByTab[tabId]?.[toolId] ?? false + }, [toolOpenByTab]) + const setToolOpenForTab = useCallback((tabId: string, toolId: string, open: boolean) => { + setToolOpenByTab((prev) => { + const prevForTab = prev[tabId] ?? {} + if (prevForTab[toolId] === open) return prev + return { + ...prev, + [tabId]: { + ...prevForTab, + [toolId]: open, + }, + } + }) + }, []) + const getChatScrollContainer = useCallback((tabId: string): HTMLElement | null => { + if (typeof document === 'undefined') return null + const panel = document.querySelector( + `[data-chat-tab-panel="${tabId}"][aria-hidden="false"]` + ) + if (!panel) return null + const logRoot = panel.querySelector('[role="log"]') + if (!logRoot) return null + const children = Array.from(logRoot.children) as HTMLElement[] + for (const child of children) { + const style = window.getComputedStyle(child) + if (style.overflowY === 'auto' || style.overflowY === 'scroll') { + return child + } + } + return null + }, []) + const saveChatScrollForTab = useCallback((tabId: string) => { + const container = getChatScrollContainer(tabId) + if (!container) return + chatScrollTopByTabRef.current.set(tabId, container.scrollTop) + }, [getChatScrollContainer]) const getChatTabTitle = useCallback((tab: ChatTab) => { if (!tab.runId) return 'New chat' @@ -1701,6 +1743,7 @@ function App() { const tab = chatTabs.find(t => t.id === tabId) if (!tab) return if (tabId === activeChatTabId) return + saveChatScrollForTab(activeChatTabId) setActiveChatTabId(tabId) const restored = restoreChatTabState(tabId, tab.runId) if (tab.runId && processingRunIdsRef.current.has(tab.runId)) { @@ -1710,12 +1753,13 @@ function App() { if (!restored) { applyChatTab(tab) } - }, [chatTabs, activeChatTabId, applyChatTab, loadRun, restoreChatTabState]) + }, [chatTabs, activeChatTabId, applyChatTab, loadRun, restoreChatTabState, saveChatScrollForTab]) const closeChatTab = useCallback((tabId: string) => { if (chatTabs.length <= 1) return const idx = chatTabs.findIndex(t => t.id === tabId) if (idx === -1) return + saveChatScrollForTab(tabId) const nextTabs = chatTabs.filter(t => t.id !== tabId) setChatTabs(nextTabs) setChatViewStateByTab(prev => { @@ -1725,6 +1769,13 @@ function App() { return next }) chatDraftsRef.current.delete(tabId) + chatScrollTopByTabRef.current.delete(tabId) + setToolOpenByTab((prev) => { + if (!(tabId in prev)) return prev + const next = { ...prev } + delete next[tabId] + return next + }) if (tabId === activeChatTabId && nextTabs.length > 0) { const newIdx = Math.min(idx, nextTabs.length - 1) @@ -1737,7 +1788,81 @@ function App() { applyChatTab(newActiveTab) } } - }, [chatTabs, activeChatTabId, applyChatTab, loadRun, restoreChatTabState]) + }, [chatTabs, activeChatTabId, applyChatTab, loadRun, restoreChatTabState, saveChatScrollForTab]) + + useEffect(() => { + let cleanupScrollListener: (() => void) | undefined + let pollRaf: number | undefined + let restoreRafA: number | undefined + let restoreRafB: number | undefined + let restoreTimeout: ReturnType | undefined + let cancelled = false + + const restoreScrollTop = (container: HTMLElement, top: number) => { + const maxScroll = Math.max(0, container.scrollHeight - container.clientHeight) + const clampedTop = clampNumber(top, 0, maxScroll) + container.scrollTop = clampedTop + } + + const attach = (): boolean => { + if (cancelled) return true + const container = getChatScrollContainer(activeChatTabId) + if (!container) return false + + const savedTop = chatScrollTopByTabRef.current.get(activeChatTabId) + if (savedTop !== undefined) { + // Reinforce restoration across a couple frames because stick-to-bottom + // may schedule scroll adjustments during mount/resize. + restoreScrollTop(container, savedTop) + restoreRafA = requestAnimationFrame(() => { + restoreScrollTop(container, savedTop) + restoreRafB = requestAnimationFrame(() => { + restoreScrollTop(container, savedTop) + }) + }) + restoreTimeout = setTimeout(() => { + restoreScrollTop(container, savedTop) + }, 220) + } + + const onScroll = () => { + chatScrollTopByTabRef.current.set(activeChatTabId, container.scrollTop) + } + container.addEventListener('scroll', onScroll, { passive: true }) + cleanupScrollListener = () => { + chatScrollTopByTabRef.current.set(activeChatTabId, container.scrollTop) + container.removeEventListener('scroll', onScroll) + } + return true + } + + let attempts = 0 + const maxAttempts = 60 + const pollAttach = () => { + if (cancelled) return + if (attach()) return + if (attempts >= maxAttempts) return + attempts += 1 + pollRaf = requestAnimationFrame(pollAttach) + } + pollAttach() + + return () => { + cancelled = true + cleanupScrollListener?.() + if (pollRaf !== undefined) cancelAnimationFrame(pollRaf) + if (restoreRafA !== undefined) cancelAnimationFrame(restoreRafA) + if (restoreRafB !== undefined) cancelAnimationFrame(restoreRafB) + if (restoreTimeout !== undefined) clearTimeout(restoreTimeout) + } + }, [ + activeChatTabId, + selectedPath, + isGraphOpen, + isChatSidebarOpen, + isRightPaneMaximized, + getChatScrollContainer, + ]) // File tab operations const openFileInNewTab = useCallback((path: string) => { @@ -2086,13 +2211,18 @@ function App() { const handleTabKeyDown = (e: KeyboardEvent) => { const mod = e.metaKey || e.ctrlKey if (!mod) return - const inFileView = Boolean(selectedPath) + const rightPaneAvailable = Boolean((selectedPath || isGraphOpen) && isChatSidebarOpen) + const targetPane: ShortcutPane = rightPaneAvailable + ? (isRightPaneMaximized ? 'right' : activeShortcutPane) + : 'left' + const inFileView = targetPane === 'left' && Boolean(selectedPath) + const targetFileTabId = activeFileTabId ?? fileTabs.find((tab) => tab.path === selectedPath)?.id ?? null // Cmd+W — close active tab if (e.key === 'w') { e.preventDefault() - if (inFileView && activeFileTabId) { - closeFileTab(activeFileTabId) + if (inFileView && targetFileTabId) { + closeFileTab(targetFileTabId) } else { closeChatTab(activeChatTabId) } @@ -2120,7 +2250,7 @@ function App() { e.preventDefault() const direction = e.key === ']' ? 1 : -1 if (inFileView) { - const currentIdx = fileTabs.findIndex(t => t.id === activeFileTabId) + const currentIdx = fileTabs.findIndex(t => t.id === targetFileTabId) if (currentIdx === -1) return const nextIdx = (currentIdx + direction + fileTabs.length) % fileTabs.length switchFileTab(fileTabs[nextIdx].id) @@ -2135,7 +2265,7 @@ function App() { } document.addEventListener('keydown', handleTabKeyDown) return () => document.removeEventListener('keydown', handleTabKeyDown) - }, [selectedPath, chatTabs, fileTabs, activeChatTabId, activeFileTabId, closeChatTab, closeFileTab, switchChatTab, switchFileTab]) + }, [selectedPath, isGraphOpen, isChatSidebarOpen, isRightPaneMaximized, activeShortcutPane, chatTabs, fileTabs, activeChatTabId, activeFileTabId, closeChatTab, closeFileTab, switchChatTab, switchFileTab]) const toggleExpand = (path: string, kind: 'file' | 'dir') => { if (kind === 'file') { @@ -2496,7 +2626,7 @@ function App() { } }, [isGraphOpen, knowledgeFilePaths]) - const renderConversationItem = (item: ConversationItem) => { + const renderConversationItem = (item: ConversationItem, tabId: string) => { if (isChatMessage(item)) { if (item.role === 'user') { const { message, files } = parseAttachedFiles(item.content) @@ -2567,7 +2697,11 @@ function App() { const output = normalizeToolOutput(item.result, item.status) const input = normalizeToolInput(item.input) return ( - + setToolOpenForTab(tabId, item.id, open)} + > {!isRightPaneOnlyMode && ( - + setActiveShortcutPane('left')} + onFocusCapture={() => setActiveShortcutPane('left')} + > {/* Header - also serves as titlebar drag region, adjusts padding when sidebar collapsed */} { void navigateBack() }} @@ -2881,85 +3019,92 @@ function App() { ) : ( { navigateToFile(path) }}>
- {chatTabs.map((tab) => { - const isActive = tab.id === activeChatTabId - const tabState = getChatTabStateForRender(tab.id) - const tabHasConversation = tabState.conversation.length > 0 || tabState.currentAssistantMessage - const tabConversationContentClassName = tabHasConversation - ? "mx-auto w-full max-w-4xl pb-28" - : "mx-auto w-full max-w-4xl min-h-full items-center justify-center pb-0" - return ( -
- - - - {!tabHasConversation ? ( - -
- What are we working on? -
-
- ) : ( - <> - {tabState.conversation.map(item => { - const rendered = renderConversationItem(item) - if (isToolCall(item)) { - const permRequest = tabState.allPermissionRequests.get(item.id) - if (permRequest) { - const response = tabState.permissionResponses.get(item.id) || null - return ( - - {rendered} - handlePermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve')} - onDeny={() => handlePermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'deny')} - isProcessing={isActive && isProcessing} - response={response} - /> - - ) +
+ {chatTabs.map((tab) => { + const isActive = tab.id === activeChatTabId + const tabState = getChatTabStateForRender(tab.id) + const tabHasConversation = tabState.conversation.length > 0 || tabState.currentAssistantMessage + const tabConversationContentClassName = tabHasConversation + ? "mx-auto w-full max-w-4xl pb-28" + : "mx-auto w-full max-w-4xl min-h-full items-center justify-center pb-0" + return ( +
+ + + + {!tabHasConversation ? ( + +
+ What are we working on? +
+
+ ) : ( + <> + {tabState.conversation.map(item => { + const rendered = renderConversationItem(item, tab.id) + if (isToolCall(item)) { + const permRequest = tabState.allPermissionRequests.get(item.id) + if (permRequest) { + const response = tabState.permissionResponses.get(item.id) || null + return ( + + {rendered} + handlePermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve')} + onDeny={() => handlePermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'deny')} + isProcessing={isActive && isProcessing} + response={response} + /> + + ) + } } - } - return rendered - })} + return rendered + })} - {Array.from(tabState.pendingAskHumanRequests.values()).map((request) => ( - handleAskHumanResponse(request.toolCallId, request.subflow, response)} - isProcessing={isActive && isProcessing} - /> - ))} + {Array.from(tabState.pendingAskHumanRequests.values()).map((request) => ( + handleAskHumanResponse(request.toolCallId, request.subflow, response)} + isProcessing={isActive && isProcessing} + /> + ))} - {tabState.currentAssistantMessage && ( - - - {tabState.currentAssistantMessage} - - - )} + {tabState.currentAssistantMessage && ( + + + {tabState.currentAssistantMessage} + + + )} - {isActive && isProcessing && !tabState.currentAssistantMessage && ( - - - Thinking... - - - )} - - )} -
-
-
- ) - })} + {isActive && isProcessing && !tabState.currentAssistantMessage && ( + + + Thinking... + + + )} + + )} + + +
+ ) + })} +
@@ -2967,21 +3112,34 @@ function App() { {!hasConversation && ( )} - setPresetMessage(undefined)} - runId={runId} - initialDraft={chatDraftsRef.current.get(activeChatTabId)} - onDraftChange={handleDraftChange} - /> + {chatTabs.map((tab) => { + const isActive = tab.id === activeChatTabId + const tabState = getChatTabStateForRender(tab.id) + return ( +
+ setPresetMessage(undefined) : undefined} + runId={tabState.runId} + initialDraft={chatDraftsRef.current.get(tab.id)} + onDraftChange={(text) => setChatDraftForTab(tab.id, text)} + /> +
+ ) + })}
@@ -3017,14 +3175,17 @@ function App() { runId={runId} presetMessage={presetMessage} onPresetMessageConsumed={() => setPresetMessage(undefined)} - initialDraft={chatDraftsRef.current.get(activeChatTabId)} - onDraftChange={handleDraftChange} + getInitialDraft={(tabId) => chatDraftsRef.current.get(tabId)} + onDraftChangeForTab={setChatDraftForTab} pendingAskHumanRequests={pendingAskHumanRequests} allPermissionRequests={allPermissionRequests} permissionResponses={permissionResponses} onPermissionResponse={handlePermissionResponse} onAskHumanResponse={handleAskHumanResponse} + isToolOpenForTab={isToolOpenForTab} + onToolOpenChangeForTab={setToolOpenForTab} onOpenKnowledgeFile={(path) => { navigateToFile(path) }} + onActivate={() => setActiveShortcutPane('right')} /> )} {/* Rendered last so its no-drag region paints over the sidebar drag region */} diff --git a/apps/x/apps/renderer/src/components/ai-elements/conversation.tsx b/apps/x/apps/renderer/src/components/ai-elements/conversation.tsx index d9f36353..f1d514da 100644 --- a/apps/x/apps/renderer/src/components/ai-elements/conversation.tsx +++ b/apps/x/apps/renderer/src/components/ai-elements/conversation.tsx @@ -102,7 +102,7 @@ export const Conversation = ({ className, children, ...props }: ConversationProp * Must be used inside Conversation component. */ export const ScrollPositionPreserver = () => { - const { isAtBottom } = useStickToBottomContext(); + const { isAtBottom, scrollRef } = useStickToBottomContext(); const preservationContext = useContext(ScrollPreservationContext); const containerFoundRef = useRef(false); @@ -110,29 +110,13 @@ export const ScrollPositionPreserver = () => { useLayoutEffect(() => { if (containerFoundRef.current || !preservationContext) return; - // Find the scroll container (StickToBottom creates one) - // It's the first parent with overflow-y scroll/auto - const findScrollContainer = (): HTMLElement | null => { - const candidates = document.querySelectorAll('[role="log"]'); - for (const candidate of candidates) { - // The scroll container is a direct child of the role="log" element - const children = candidate.children; - for (const child of children) { - const style = window.getComputedStyle(child); - if (style.overflowY === 'auto' || style.overflowY === 'scroll') { - return child as HTMLElement; - } - } - } - return null; - }; - - const container = findScrollContainer(); + // Use the local StickToBottom scroll container for this conversation instance. + const container = scrollRef.current; if (container) { preservationContext.registerScrollContainer(container); containerFoundRef.current = true; } - }, [preservationContext]); + }, [preservationContext, scrollRef]); // Track engagement based on scroll position useEffect(() => { diff --git a/apps/x/apps/renderer/src/components/ai-elements/prompt-input.tsx b/apps/x/apps/renderer/src/components/ai-elements/prompt-input.tsx index c27ab5c3..98263434 100644 --- a/apps/x/apps/renderer/src/components/ai-elements/prompt-input.tsx +++ b/apps/x/apps/renderer/src/components/ai-elements/prompt-input.tsx @@ -931,7 +931,13 @@ export const PromptInputTextarea = ({ if (autoFocus || focusTrigger !== undefined) { // Small delay to ensure the element is fully mounted and visible const timer = setTimeout(() => { - textareaRef.current?.focus(); + const textarea = textareaRef.current; + if (!textarea) return; + try { + textarea.focus({ preventScroll: true }); + } catch { + textarea.focus(); + } }, 50); return () => clearTimeout(timer); } diff --git a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx index de35ac44..31bcba17 100644 --- a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx +++ b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx @@ -16,6 +16,7 @@ interface ChatInputInnerProps { onStop?: () => void isProcessing: boolean isStopping?: boolean + isActive: boolean presetMessage?: string onPresetMessageConsumed?: () => void runId?: string | null @@ -28,6 +29,7 @@ function ChatInputInner({ onStop, isProcessing, isStopping, + isActive, presetMessage, onPresetMessageConsumed, runId, @@ -72,6 +74,7 @@ function ChatInputInner({ }, [handleSubmit]) useEffect(() => { + if (!isActive) return const onDragOver = (e: DragEvent) => { if (e.dataTransfer?.types?.includes('Files')) { e.preventDefault() @@ -100,15 +103,15 @@ function ChatInputInner({ document.removeEventListener('dragover', onDragOver) document.removeEventListener('drop', onDrop) } - }, [controller]) + }, [controller, isActive]) return (
{isProcessing ? ( @@ -156,6 +159,7 @@ export interface ChatInputWithMentionsProps { onStop?: () => void isProcessing: boolean isStopping?: boolean + isActive?: boolean presetMessage?: string onPresetMessageConsumed?: () => void runId?: string | null @@ -171,6 +175,7 @@ export function ChatInputWithMentions({ onStop, isProcessing, isStopping, + isActive = true, presetMessage, onPresetMessageConsumed, runId, @@ -184,6 +189,7 @@ export function ChatInputWithMentions({ onStop={onStop} isProcessing={isProcessing} isStopping={isStopping} + isActive={isActive} presetMessage={presetMessage} onPresetMessageConsumed={onPresetMessageConsumed} runId={runId} diff --git a/apps/x/apps/renderer/src/components/chat-sidebar.tsx b/apps/x/apps/renderer/src/components/chat-sidebar.tsx index 397081d3..dfb155b4 100644 --- a/apps/x/apps/renderer/src/components/chat-sidebar.tsx +++ b/apps/x/apps/renderer/src/components/chat-sidebar.tsx @@ -55,6 +55,7 @@ interface ErrorMessage { type ConversationItem = ChatMessage | ToolCall | ErrorMessage type ChatTabViewState = { + runId: string | null conversation: ConversationItem[] currentAssistantMessage: string pendingAskHumanRequests: Map> @@ -160,14 +161,17 @@ interface ChatSidebarProps { runId?: string | null presetMessage?: string onPresetMessageConsumed?: () => void - initialDraft?: string - onDraftChange?: (text: string) => void + getInitialDraft?: (tabId: string) => string | undefined + onDraftChangeForTab?: (tabId: string, text: string) => void pendingAskHumanRequests?: Map> allPermissionRequests?: Map> permissionResponses?: Map onPermissionResponse?: (toolCallId: string, subflow: string[], response: 'approve' | 'deny') => void onAskHumanResponse?: (toolCallId: string, subflow: string[], response: string) => void + isToolOpenForTab?: (tabId: string, toolId: string) => boolean + onToolOpenChangeForTab?: (tabId: string, toolId: string, open: boolean) => void onOpenKnowledgeFile?: (path: string) => void + onActivate?: () => void } export function ChatSidebar({ @@ -195,14 +199,17 @@ export function ChatSidebar({ runId, presetMessage, onPresetMessageConsumed, - initialDraft, - onDraftChange, + getInitialDraft, + onDraftChangeForTab, pendingAskHumanRequests = new Map(), allPermissionRequests = new Map(), permissionResponses = new Map(), onPermissionResponse, onAskHumanResponse, + isToolOpenForTab, + onToolOpenChangeForTab, onOpenKnowledgeFile, + onActivate, }: ChatSidebarProps) { const [width, setWidth] = useState(() => getInitialPaneWidth(defaultWidth)) const [isResizing, setIsResizing] = useState(false) @@ -284,12 +291,14 @@ export function ChatSidebar({ }, [width, getMaxAllowedWidth]) const activeTabState = useMemo(() => ({ + runId: runId ?? null, conversation, currentAssistantMessage, pendingAskHumanRequests, allPermissionRequests, permissionResponses, }), [ + runId, conversation, currentAssistantMessage, pendingAskHumanRequests, @@ -297,6 +306,7 @@ export function ChatSidebar({ permissionResponses, ]) const emptyTabState = useMemo(() => ({ + runId: null, conversation: [], currentAssistantMessage: '', pendingAskHumanRequests: new Map(), @@ -309,7 +319,7 @@ export function ChatSidebar({ }, [activeChatTabId, activeTabState, chatTabStates, emptyTabState]) const hasConversation = activeTabState.conversation.length > 0 || Boolean(activeTabState.currentAssistantMessage) - const renderConversationItem = (item: ConversationItem) => { + const renderConversationItem = (item: ConversationItem, tabId: string) => { if (isChatMessage(item)) { return ( @@ -329,7 +339,11 @@ export function ChatSidebar({ const output = normalizeToolOutput(item.result, item.status) const input = normalizeToolInput(item.input) return ( - + onToolOpenChangeForTab?.(tabId, item.id, open)} + > @@ -367,6 +381,8 @@ export function ChatSidebar({ return (
{isMaximized ? : } - {isMaximized ? 'Restore two-pane view' : 'Maximize right pane'} + {isMaximized ? 'Restore two-pane view' : 'Maximize chat view'} )} @@ -431,80 +448,87 @@ export function ChatSidebar({ {})}>
- {chatTabs.map((tab) => { - const isActive = tab.id === activeChatTabId - const tabState = getTabState(tab.id) - const tabHasConversation = tabState.conversation.length > 0 || Boolean(tabState.currentAssistantMessage) - return ( -
- - - - {!tabHasConversation ? ( - -
Ask anything...
-
- ) : ( - <> - {tabState.conversation.map((item) => { - const rendered = renderConversationItem(item) - if (isToolCall(item) && onPermissionResponse) { - const permRequest = tabState.allPermissionRequests.get(item.id) - if (permRequest) { - const response = tabState.permissionResponses.get(item.id) || null - return ( - - {rendered} - onPermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve')} - onDeny={() => onPermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'deny')} - isProcessing={isActive && isProcessing} - response={response} - /> - - ) +
+ {chatTabs.map((tab) => { + const isActive = tab.id === activeChatTabId + const tabState = getTabState(tab.id) + const tabHasConversation = tabState.conversation.length > 0 || Boolean(tabState.currentAssistantMessage) + return ( +
+ + + + {!tabHasConversation ? ( + +
Ask anything...
+
+ ) : ( + <> + {tabState.conversation.map((item) => { + const rendered = renderConversationItem(item, tab.id) + if (isToolCall(item) && onPermissionResponse) { + const permRequest = tabState.allPermissionRequests.get(item.id) + if (permRequest) { + const response = tabState.permissionResponses.get(item.id) || null + return ( + + {rendered} + onPermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve')} + onDeny={() => onPermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'deny')} + isProcessing={isActive && isProcessing} + response={response} + /> + + ) + } } - } - return rendered - })} + return rendered + })} - {onAskHumanResponse && Array.from(tabState.pendingAskHumanRequests.values()).map((request) => ( - onAskHumanResponse(request.toolCallId, request.subflow, response)} - isProcessing={isActive && isProcessing} - /> - ))} + {onAskHumanResponse && Array.from(tabState.pendingAskHumanRequests.values()).map((request) => ( + onAskHumanResponse(request.toolCallId, request.subflow, response)} + isProcessing={isActive && isProcessing} + /> + ))} - {tabState.currentAssistantMessage && ( - - - {tabState.currentAssistantMessage} - - - )} + {tabState.currentAssistantMessage && ( + + + {tabState.currentAssistantMessage} + + + )} - {isActive && isProcessing && !tabState.currentAssistantMessage && ( - - - Thinking... - - - )} - - )} -
-
-
- ) - })} + {isActive && isProcessing && !tabState.currentAssistantMessage && ( + + + Thinking... + + + )} + + )} + + +
+ ) + })} +
@@ -512,24 +536,37 @@ export function ChatSidebar({ {!hasConversation && ( )} - { - setLocalPresetMessage(undefined) - onPresetMessageConsumed?.() - }} - runId={runId} - initialDraft={initialDraft} - onDraftChange={onDraftChange} - /> + {chatTabs.map((tab) => { + const isActive = tab.id === activeChatTabId + const tabState = getTabState(tab.id) + return ( +
+ { + setLocalPresetMessage(undefined) + onPresetMessageConsumed?.() + } : undefined} + runId={tabState.runId} + initialDraft={getInitialDraft?.(tab.id)} + onDraftChange={onDraftChangeForTab ? (text) => onDraftChangeForTab(tab.id, text) : undefined} + /> +
+ ) + })}