From b238089e2dbd227adeb58215d92d5303cf9ce36c Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Thu, 19 Feb 2026 11:43:22 +0530 Subject: [PATCH 1/8] fix: broken thinking indicator --- apps/x/apps/renderer/src/App.tsx | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 6e79bec8..9cc66879 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -624,23 +624,28 @@ function App() { processingRunIdsRef.current = processingRunIds }, [processingRunIds]) - // Sync active run streaming UI with background tracking + // Sync active run streaming UI with background processing tracking. + // Depend on both runId and processingRunIds so we don't miss late/early event ordering. useEffect(() => { if (!runId) { setIsProcessing(false) + setIsStopping(false) + setStopClickedAt(null) setCurrentAssistantMessage('') return } - const isRunProcessing = processingRunIdsRef.current.has(runId) + const isRunProcessing = processingRunIds.has(runId) setIsProcessing(isRunProcessing) if (isRunProcessing) { const buffer = streamingBuffersRef.current.get(runId) setCurrentAssistantMessage(buffer?.assistant ?? '') } else { + setIsStopping(false) + setStopClickedAt(null) setCurrentAssistantMessage('') streamingBuffersRef.current.delete(runId) } - }, [runId]) + }, [runId, processingRunIds]) // Load directory tree const loadDirectory = useCallback(async () => { @@ -1163,7 +1168,14 @@ function App() { break case 'start': + setProcessingRunIds(prev => { + if (prev.has(event.runId)) return prev + const next = new Set(prev) + next.add(event.runId) + return next + }) if (!isActiveRun) return + setIsProcessing(true) setCurrentAssistantMessage('') setModelUsage(null) break @@ -1171,12 +1183,20 @@ function App() { case 'llm-stream-event': { const llmEvent = event.event + // Fallback: if processing-start is missed/out-of-order, stream activity still means run is active. + setProcessingRunIds(prev => { + if (prev.has(event.runId)) return prev + const next = new Set(prev) + next.add(event.runId) + return next + }) if (!isActiveRun) { if (llmEvent.type === 'text-delta' && llmEvent.delta) { appendStreamingBuffer(event.runId, llmEvent.delta) } return } + setIsProcessing(true) if (llmEvent.type === 'text-delta' && llmEvent.delta) { appendStreamingBuffer(event.runId, llmEvent.delta) setCurrentAssistantMessage(prev => prev + llmEvent.delta) From 73da27eec6ca4631b5f7cb29be876b01f1bbaa09 Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Thu, 19 Feb 2026 15:04:11 +0530 Subject: [PATCH 2/8] feat: add search button to FixedSidebarToggle and integrate onOpenSearch prop --- apps/x/apps/renderer/src/App.tsx | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 9cc66879..8ed5e7b7 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -224,6 +224,7 @@ function FixedSidebarToggle({ canNavigateBack, canNavigateForward, onNewChat, + onOpenSearch, leftInsetPx, }: { onNavigateBack: () => void @@ -231,6 +232,7 @@ function FixedSidebarToggle({ canNavigateBack: boolean canNavigateForward: boolean onNewChat: () => void + onOpenSearch: () => void leftInsetPx: number }) { const { toggleSidebar, state } = useSidebar() @@ -257,6 +259,15 @@ function FixedSidebarToggle({ > + {/* Back / Forward navigation */} {isCollapsed && ( <> @@ -2769,14 +2780,6 @@ function App() { onCloseTab={closeChatTab} /> )} - {selectedPath && (
{isSaving ? ( @@ -3078,6 +3081,7 @@ function App() { canNavigateBack={canNavigateBack} canNavigateForward={canNavigateForward} onNewChat={handleNewChatTab} + onOpenSearch={() => setIsSearchOpen(true)} leftInsetPx={isMac ? MACOS_TRAFFIC_LIGHTS_RESERVED_PX : 0} /> From 06444e5db209e23ab88d2d8cc2eefe48a1d8e886 Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Thu, 19 Feb 2026 15:13:22 +0530 Subject: [PATCH 3/8] fix: update titlebar button configurations for better layout --- apps/x/apps/renderer/src/App.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 8ed5e7b7..3ff5e433 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -100,8 +100,8 @@ const TITLEBAR_BUTTON_PX = 32 const TITLEBAR_BUTTON_GAP_PX = 4 const TITLEBAR_HEADER_GAP_PX = 8 const TITLEBAR_TOGGLE_MARGIN_LEFT_PX = 12 -const TITLEBAR_BUTTONS_COLLAPSED = 4 -const TITLEBAR_BUTTON_GAPS_COLLAPSED = 3 +const TITLEBAR_BUTTONS_COLLAPSED = 5 +const TITLEBAR_BUTTON_GAPS_COLLAPSED = 4 const clampNumber = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value)) @@ -320,7 +320,7 @@ function ContentHeader({ "titlebar-drag-region flex h-10 shrink-0 items-stretch border-b border-border px-3 bg-sidebar transition-[padding] duration-200 ease-linear overflow-hidden", // When the sidebar is collapsed the content area shifts left, so we need enough left padding // to avoid overlapping the fixed traffic-lights/toggle/back/forward controls. - isCollapsed && !collapsedLeftPaddingPx && "pl-[168px]" + isCollapsed && !collapsedLeftPaddingPx && "pl-[196px]" )} style={isCollapsed && collapsedLeftPaddingPx ? { paddingLeft: collapsedLeftPaddingPx } : undefined} > From f61404cbcde3155238cae66642dfcd1fe3e49770 Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Thu, 19 Feb 2026 15:30:26 +0530 Subject: [PATCH 4/8] feat: implement graph tab functionality and enhance tab management - Introduced a new graph tab view with dedicated handling for file tabs. - Updated tab navigation logic to support switching between regular and graph tabs. - Added a new prop `allowSingleTabClose` to the TabBar component for improved tab closing behavior. - Refactored existing tab management functions to accommodate the new graph tab functionality. --- apps/x/apps/renderer/src/App.tsx | 127 ++++++++++++------ .../apps/renderer/src/components/tab-bar.tsx | 4 +- 2 files changed, 88 insertions(+), 43 deletions(-) diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 3ff5e433..047b4203 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -5,7 +5,6 @@ import { RunEvent, ListRunsResponse } from '@x/shared/src/runs.js'; import type { LanguageModelUsage, ToolUIPart } from 'ai'; import './App.css' import z from 'zod'; -import { Button } from './components/ui/button'; import { CheckIcon, LoaderIcon, PanelLeftIcon, Expand, Shrink, X, ChevronLeftIcon, ChevronRightIcon, SquarePen, SearchIcon } from 'lucide-react'; import { cn } from '@/lib/utils'; import { MarkdownEditor } from './components/markdown-editor'; @@ -102,6 +101,7 @@ const TITLEBAR_HEADER_GAP_PX = 8 const TITLEBAR_TOGGLE_MARGIN_LEFT_PX = 12 const TITLEBAR_BUTTONS_COLLAPSED = 5 const TITLEBAR_BUTTON_GAPS_COLLAPSED = 4 +const GRAPH_TAB_PATH = '__rowboat_graph_view__' const clampNumber = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value)) @@ -134,6 +134,8 @@ const getBaseName = (path: string) => { return file.replace(/\.md$/i, '') } +const isGraphTabPath = (path: string) => path === GRAPH_TAB_PATH + const normalizeUsage = (usage?: Partial | null): LanguageModelUsage | null => { if (!usage) return null const hasNumbers = Object.values(usage).some((value) => typeof value === 'number') @@ -504,6 +506,7 @@ function App() { const newFileTabId = () => `file-tab-${++fileTabIdCounterRef.current}` const getFileTabTitle = useCallback((tab: FileTab) => { + if (isGraphTabPath(tab.path)) return 'Graph View' return tab.path.split('/').pop()?.replace(/\.md$/i, '') || tab.path }, []) @@ -1789,12 +1792,14 @@ function App() { const existingTab = fileTabs.find(t => t.path === path) if (existingTab) { setActiveFileTabId(existingTab.id) + setIsGraphOpen(false) setSelectedPath(path) return } const id = newFileTabId() setFileTabs(prev => [...prev, { id, path }]) setActiveFileTabId(id) + setIsGraphOpen(false) setSelectedPath(path) }, [fileTabs]) @@ -1802,12 +1807,24 @@ function App() { const tab = fileTabs.find(t => t.id === tabId) if (!tab) return setActiveFileTabId(tabId) + setSelectedBackgroundTask(null) + setExpandedFrom(null) + // If chat-only maximize is active, drop back to a visible knowledge layout. + if (isRightPaneMaximized) { + setIsRightPaneMaximized(false) + } + if (isGraphTabPath(tab.path)) { + setSelectedPath(null) + setIsGraphOpen(true) + return + } + setIsGraphOpen(false) setSelectedPath(tab.path) - }, [fileTabs]) + }, [fileTabs, isRightPaneMaximized]) const closeFileTab = useCallback((tabId: string) => { const closingTab = fileTabs.find(t => t.id === tabId) - if (closingTab) { + if (closingTab && !isGraphTabPath(closingTab.path)) { removeEditorCacheForPath(closingTab.path) initialContentByPathRef.current.delete(closingTab.path) if (editorPathRef.current === closingTab.path) { @@ -1819,15 +1836,23 @@ function App() { // Last file tab - close it and go back to chat setActiveFileTabId(null) setSelectedPath(null) + setIsGraphOpen(false) return [] } const idx = prev.findIndex(t => t.id === tabId) + if (idx === -1) return prev const next = prev.filter(t => t.id !== tabId) if (tabId === activeFileTabId && next.length > 0) { const newIdx = Math.min(idx, next.length - 1) const newActiveTab = next[newIdx] setActiveFileTabId(newActiveTab.id) - setSelectedPath(newActiveTab.path) + if (isGraphTabPath(newActiveTab.path)) { + setSelectedPath(null) + setIsGraphOpen(true) + } else { + setIsGraphOpen(false) + setSelectedPath(newActiveTab.path) + } } return next }) @@ -1918,22 +1943,62 @@ function App() { return [...stack, entry] }, []) + const ensureFileTabForPath = useCallback((path: string) => { + const existingTab = fileTabs.find((tab) => tab.path === path) + if (existingTab) { + setActiveFileTabId(existingTab.id) + return + } + + if (activeFileTabId) { + const activeTab = fileTabs.find((tab) => tab.id === activeFileTabId) + if (activeTab && !isGraphTabPath(activeTab.path)) { + setFileTabs((prev) => prev.map((tab) => ( + tab.id === activeFileTabId ? { ...tab, path } : tab + ))) + return + } + } + + const id = newFileTabId() + setFileTabs((prev) => [...prev, { id, path }]) + setActiveFileTabId(id) + }, [fileTabs, activeFileTabId]) + + const ensureGraphFileTab = useCallback(() => { + const existingGraphTab = fileTabs.find((tab) => isGraphTabPath(tab.path)) + if (existingGraphTab) { + setActiveFileTabId(existingGraphTab.id) + return + } + const id = newFileTabId() + setFileTabs((prev) => [...prev, { id, path: GRAPH_TAB_PATH }]) + setActiveFileTabId(id) + }, [fileTabs]) + const applyViewState = useCallback(async (view: ViewState) => { switch (view.type) { case 'file': setSelectedBackgroundTask(null) setIsGraphOpen(false) setExpandedFrom(null) - setIsChatSidebarOpen(true) - setIsRightPaneMaximized(false) + // Preserve split vs knowledge-max mode when navigating knowledge files. + // Only exit chat-only maximize, because that would hide the selected file. + if (isRightPaneMaximized) { + setIsRightPaneMaximized(false) + } setSelectedPath(view.path) + ensureFileTabForPath(view.path) return case 'graph': setSelectedBackgroundTask(null) setSelectedPath(null) setExpandedFrom(null) setIsGraphOpen(true) - setIsRightPaneMaximized(false) + ensureGraphFileTab() + if (isRightPaneMaximized) { + setIsRightPaneMaximized(false) + } return case 'task': setSelectedPath(null) @@ -1955,7 +2020,7 @@ function App() { } return } - }, [handleNewChat, loadRun]) + }, [ensureFileTabForPath, ensureGraphFileTab, handleNewChat, isRightPaneMaximized, loadRun]) const navigateToView = useCallback(async (nextView: ViewState) => { const current = currentViewState @@ -2024,22 +2089,8 @@ function App() { }, [viewHistory.forward, currentViewState]) const navigateToFile = useCallback((path: string) => { - // If already open in a file tab, switch to it - const existingTab = fileTabs.find(t => t.path === path) - if (existingTab) { - switchFileTab(existingTab.id) - return - } - // Update current file tab or create one if none exists - if (activeFileTabId) { - setFileTabs(prev => prev.map(t => t.id === activeFileTabId ? { ...t, path } : t)) - } else { - const id = newFileTabId() - setFileTabs(prev => [...prev, { id, path }]) - setActiveFileTabId(id) - } void navigateToView({ type: 'file', path }) - }, [navigateToView, fileTabs, activeFileTabId, switchFileTab]) + }, [navigateToView]) const navigateToFullScreenChat = useCallback(() => { // Only treat this as navigation when coming from another view @@ -2130,8 +2181,13 @@ function App() { 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 + const inFileView = targetPane === 'left' && Boolean(selectedPath || isGraphOpen) + const selectedKnowledgePath = isGraphOpen ? GRAPH_TAB_PATH : selectedPath + const targetFileTabId = activeFileTabId ?? ( + selectedKnowledgePath + ? (fileTabs.find((tab) => tab.path === selectedKnowledgePath)?.id ?? null) + : null + ) // Cmd+W — close active tab if (e.key === 'w') { @@ -2760,7 +2816,7 @@ function App() { canNavigateForward={canNavigateForward} collapsedLeftPaddingPx={collapsedLeftPaddingPx} > - {selectedPath && fileTabs.length >= 1 ? ( + {(selectedPath || isGraphOpen) && fileTabs.length >= 1 ? ( t.id} onSwitchTab={switchFileTab} onCloseTab={closeFileTab} + allowSingleTabClose={fileTabs.length === 1 && isGraphOpen} /> ) : ( )} - {!selectedPath && isGraphOpen && ( - - )} {!selectedPath && !isGraphOpen && expandedFrom && ( - {isChatSidebarOpen - ? (selectedPath ? "Maximize knowledge view" : "Maximize main view") - : "Restore two-pane view"} + {isChatSidebarOpen ? "Maximize knowledge view" : "Restore two-pane view"} )} diff --git a/apps/x/apps/renderer/src/components/tab-bar.tsx b/apps/x/apps/renderer/src/components/tab-bar.tsx index 4abe8175..744f578d 100644 --- a/apps/x/apps/renderer/src/components/tab-bar.tsx +++ b/apps/x/apps/renderer/src/components/tab-bar.tsx @@ -21,6 +21,7 @@ interface TabBarProps { onSwitchTab: (tabId: string) => void onCloseTab: (tabId: string) => void layout?: 'fill' | 'scroll' + allowSingleTabClose?: boolean } export function TabBar({ @@ -32,6 +33,7 @@ export function TabBar({ onSwitchTab, onCloseTab, layout = 'fill', + allowSingleTabClose = false, }: TabBarProps) { return (
({ )} {title} - {tabs.length > 1 && ( + {(allowSingleTabClose || tabs.length > 1) && ( Date: Thu, 19 Feb 2026 15:36:26 +0530 Subject: [PATCH 5/8] feat: make sure graph view opens in maximised pane --- apps/x/apps/renderer/src/App.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 047b4203..60ce4b32 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -2357,6 +2357,11 @@ function App() { } }, openGraph: () => { + // From chat-only landing state, open graph directly in full knowledge view. + if (!selectedPath && !isGraphOpen && !selectedBackgroundTask) { + setIsChatSidebarOpen(false) + setIsRightPaneMaximized(false) + } void navigateToView({ type: 'graph' }) }, expandAll: () => setExpandedPaths(new Set(collectDirPaths(tree))), @@ -2422,7 +2427,7 @@ function App() { onOpenInNewTab: (path: string) => { openFileInNewTab(path) }, - }), [tree, selectedPath, workspaceRoot, navigateToFile, navigateToView, openFileInNewTab, fileTabs, closeFileTab, removeEditorCacheForPath]) + }), [tree, selectedPath, isGraphOpen, selectedBackgroundTask, workspaceRoot, navigateToFile, navigateToView, openFileInNewTab, fileTabs, closeFileTab, removeEditorCacheForPath]) // Handler for when a voice note is created/updated const handleVoiceNoteCreated = useCallback(async (notePath: string) => { From e8d8332e34c1979dc0ad2a9336505e9408fdc6c5 Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Thu, 19 Feb 2026 15:49:45 +0530 Subject: [PATCH 6/8] feat: enhance knowledge tree navigation and visibility auto-open the folder chain to reveal the active file in sidebar when a knowledge file is opened --- apps/x/apps/renderer/src/App.tsx | 29 ++++++++ .../src/components/sidebar-content.tsx | 66 ++++++++++++++----- 2 files changed, 80 insertions(+), 15 deletions(-) diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 60ce4b32..df8bd3dc 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -134,6 +134,16 @@ const getBaseName = (path: string) => { return file.replace(/\.md$/i, '') } +const getAncestorDirectoryPaths = (path: string): string[] => { + const parts = path.split('/').filter(Boolean) + if (parts.length <= 2) return [] + const ancestors: string[] = [] + for (let i = 1; i < parts.length - 1; i++) { + ancestors.push(parts.slice(0, i + 1).join('/')) + } + return ancestors +} + const isGraphTabPath = (path: string) => path === GRAPH_TAB_PATH const normalizeUsage = (usage?: Partial | null): LanguageModelUsage | null => { @@ -597,6 +607,25 @@ function App() { } }, [selectedPath]) + // Keep active file visible in the Knowledge tree by auto-expanding its ancestor folders. + useEffect(() => { + if (!selectedPath) return + const ancestorDirs = getAncestorDirectoryPaths(selectedPath) + if (ancestorDirs.length === 0) return + + setExpandedPaths((prev) => { + let changed = false + const next = new Set(prev) + for (const dirPath of ancestorDirs) { + if (!next.has(dirPath)) { + next.add(dirPath) + changed = true + } + } + return changed ? next : prev + }) + }, [selectedPath]) + // Keep runIdRef in sync with runId state (for use in event handlers to avoid stale closures) useEffect(() => { runIdRef.current = runId diff --git a/apps/x/apps/renderer/src/components/sidebar-content.tsx b/apps/x/apps/renderer/src/components/sidebar-content.tsx index 6a4bec64..fb890ecb 100644 --- a/apps/x/apps/renderer/src/components/sidebar-content.tsx +++ b/apps/x/apps/renderer/src/components/sidebar-content.tsx @@ -820,6 +820,36 @@ function KnowledgeSection({ onVoiceNoteCreated?: (path: string) => void }) { const isExpanded = expandedPaths.size > 0 + const treeContainerRef = React.useRef(null) + + useEffect(() => { + if (!selectedPath) return + + let cancelled = false + let rafId: number | null = null + let attempts = 0 + const maxAttempts = 20 + + const revealActiveFile = () => { + if (cancelled) return + const container = treeContainerRef.current + if (!container) return + const activeRow = container.querySelector('[data-knowledge-active="true"]') + if (activeRow) { + activeRow.scrollIntoView({ block: "nearest", inline: "nearest" }) + return + } + if (attempts >= maxAttempts) return + attempts += 1 + rafId = requestAnimationFrame(revealActiveFile) + } + + rafId = requestAnimationFrame(revealActiveFile) + return () => { + cancelled = true + if (rafId !== null) cancelAnimationFrame(rafId) + } + }, [selectedPath, expandedPaths, tree]) const quickActions = [ { icon: FilePlus, label: "New Note", action: () => actions.createNote() }, @@ -865,18 +895,20 @@ function KnowledgeSection({
- - {tree.map((item, index) => ( - - ))} - +
+ + {tree.map((item, index) => ( + + ))} + +
@@ -935,7 +967,7 @@ function Tree({ try { await actions.rename(item.path, trimmedName, isDir) toast('Renamed successfully', 'success') - } catch (err) { + } catch { toast('Failed to rename', 'error') } } @@ -950,7 +982,7 @@ function Tree({ try { await actions.remove(item.path) toast('Moved to trash', 'success') - } catch (err) { + } catch { toast('Failed to delete', 'error') } } @@ -1045,7 +1077,11 @@ function Tree({ return ( - + { From bf5f6f16de69350e46534eb14007ec1bf10e1aef Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Thu, 19 Feb 2026 16:19:13 +0530 Subject: [PATCH 7/8] feat: update chat and app UI with new maximize/minimize icons --- apps/x/apps/renderer/src/App.tsx | 51 ++++++++++++++----- .../renderer/src/components/chat-sidebar.tsx | 4 +- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index df8bd3dc..b810af9b 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -5,7 +5,7 @@ import { RunEvent, ListRunsResponse } from '@x/shared/src/runs.js'; import type { LanguageModelUsage, ToolUIPart } from 'ai'; import './App.css' import z from 'zod'; -import { CheckIcon, LoaderIcon, PanelLeftIcon, Expand, Shrink, X, ChevronLeftIcon, ChevronRightIcon, SquarePen, SearchIcon } from 'lucide-react'; +import { CheckIcon, LoaderIcon, PanelLeftIcon, Maximize2, Minimize2, ChevronLeftIcon, ChevronRightIcon, SquarePen, SearchIcon } from 'lucide-react'; import { cn } from '@/lib/utils'; import { MarkdownEditor } from './components/markdown-editor'; import { ChatSidebar } from './components/chat-sidebar'; @@ -1655,8 +1655,10 @@ function App() { const restoreChatTabState = useCallback((tabId: string, fallbackRunId: string | null): boolean => { const cached = chatViewStateByTabRef.current[tabId] if (!cached) return false + // Ignore stale cache snapshots that don't match the tab's current run binding. + if (cached.runId !== fallbackRunId) return false - const resolvedRunId = cached.runId ?? fallbackRunId + const resolvedRunId = fallbackRunId setRunId(resolvedRunId) setConversation(cached.conversation) setCurrentAssistantMessage(cached.currentAssistantMessage) @@ -1678,6 +1680,8 @@ function App() { const openChatInNewTab = useCallback((targetRunId: string) => { const existingTab = chatTabs.find(t => t.runId === targetRunId) if (existingTab) { + // Cancel stale in-flight loads from previously focused tabs. + loadRunRequestIdRef.current += 1 setActiveChatTabId(existingTab.id) const restored = restoreChatTabState(existingTab.id, existingTab.runId) if (processingRunIdsRef.current.has(targetRunId) || !restored) { @@ -1696,6 +1700,8 @@ function App() { if (!tab) return if (tabId === activeChatTabId) return saveChatScrollForTab(activeChatTabId) + // Cancel stale in-flight loads from previously focused tabs. + loadRunRequestIdRef.current += 1 setActiveChatTabId(tabId) const restored = restoreChatTabState(tabId, tab.runId) if (tab.runId && processingRunIdsRef.current.has(tab.runId)) { @@ -1732,6 +1738,8 @@ function App() { if (tabId === activeChatTabId && nextTabs.length > 0) { const newIdx = Math.min(idx, nextTabs.length - 1) const newActiveTab = nextTabs[newIdx] + // Cancel stale in-flight loads from the closing tab. + loadRunRequestIdRef.current += 1 setActiveChatTabId(newActiveTab.id) const restored = restoreChatTabState(newActiveTab.id, newActiveTab.runId) if (newActiveTab.runId && processingRunIdsRef.current.has(newActiveTab.runId)) { @@ -2776,7 +2784,6 @@ function App() { onSelectRun: (runIdToLoad) => { if (selectedPath || isGraphOpen) { setIsChatSidebarOpen(true) - setIsRightPaneMaximized(false) } // If already open in a chat tab, switch to it @@ -2886,15 +2893,35 @@ function App() { ) : null}
)} + {!selectedPath && !isGraphOpen && !selectedTask && ( + + + + + New chat tab + + )} {!selectedPath && !isGraphOpen && expandedFrom && ( - + + + + + Restore two-pane view + )} {(selectedPath || isGraphOpen) && ( @@ -2905,7 +2932,7 @@ function App() { className="titlebar-no-drag flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground transition-colors -mr-1 self-center shrink-0" aria-label={isChatSidebarOpen ? "Maximize knowledge view" : "Restore two-pane view"} > - {isChatSidebarOpen ? : } + {isChatSidebarOpen ? : } diff --git a/apps/x/apps/renderer/src/components/chat-sidebar.tsx b/apps/x/apps/renderer/src/components/chat-sidebar.tsx index 44fdbafd..1e8610dd 100644 --- a/apps/x/apps/renderer/src/components/chat-sidebar.tsx +++ b/apps/x/apps/renderer/src/components/chat-sidebar.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { Expand, Shrink, SquarePen } from 'lucide-react' +import { Maximize2, Minimize2, SquarePen } from 'lucide-react' import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' @@ -394,7 +394,7 @@ export function ChatSidebar({ className="titlebar-no-drag my-1 mr-2 h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground" aria-label={isMaximized ? 'Restore two-pane view' : 'Maximize chat view'} > - {isMaximized ? : } + {isMaximized ? : } From 1cad68e60fde22e14601fbc13d1b50433fef80af Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Thu, 19 Feb 2026 23:58:27 +0530 Subject: [PATCH 8/8] feat: enhance sidebar behavior with collapsible left pane and optimized chat sidebar transitions --- apps/x/apps/renderer/src/App.tsx | 10 +++++++--- apps/x/apps/renderer/src/components/chat-sidebar.tsx | 8 +++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index b810af9b..e18c9d47 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -2748,6 +2748,7 @@ function App() { : null const isRightPaneContext = Boolean(selectedPath || isGraphOpen) const isRightPaneOnlyMode = isRightPaneContext && isChatSidebarOpen && isRightPaneMaximized + const shouldCollapseLeftPane = isRightPaneOnlyMode const openMarkdownTabs = React.useMemo(() => { const markdownTabs = fileTabs.filter(tab => tab.path.endsWith('.md')) if (selectedPath?.endsWith('.md')) { @@ -2843,9 +2844,13 @@ function App() { backgroundTasks={backgroundTasks} selectedBackgroundTask={selectedBackgroundTask} /> - {!isRightPaneOnlyMode && ( setActiveShortcutPane('left')} onFocusCapture={() => setActiveShortcutPane('left')} > @@ -3136,7 +3141,6 @@ function App() { )} - )} {/* Chat sidebar - shown when viewing files/graph */} {isRightPaneContext && ( diff --git a/apps/x/apps/renderer/src/components/chat-sidebar.tsx b/apps/x/apps/renderer/src/components/chat-sidebar.tsx index 1e8610dd..05e498f7 100644 --- a/apps/x/apps/renderer/src/components/chat-sidebar.tsx +++ b/apps/x/apps/renderer/src/components/chat-sidebar.tsx @@ -154,6 +154,8 @@ export function ChatSidebar({ const paneRef = useRef(null) const startXRef = useRef(0) const startWidthRef = useRef(0) + const prevIsMaximizedRef = useRef(isMaximized) + const justToggledMaximize = prevIsMaximizedRef.current !== isMaximized const getMaxAllowedWidth = useCallback(() => { if (typeof window === 'undefined') return MAX_WIDTH @@ -183,6 +185,10 @@ export function ChatSidebar({ setShowContent(false) }, [isOpen]) + useEffect(() => { + prevIsMaximizedRef.current = isMaximized + }, [isMaximized]) + useEffect(() => { if (typeof window === 'undefined') return try { @@ -343,7 +349,7 @@ export function ChatSidebar({ onFocusCapture={onActivate} className={cn( 'relative flex min-w-0 flex-col overflow-hidden border-l border-border bg-background', - !isResizing && 'transition-[width] duration-200 ease-linear' + !isResizing && !justToggledMaximize && 'transition-[width] duration-200 ease-linear' )} style={paneStyle} >