diff --git a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/__tests__/file-viewer.test.tsx b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/__tests__/file-viewer.test.tsx index ca66a764a..573dabc69 100644 --- a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/__tests__/file-viewer.test.tsx +++ b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/__tests__/file-viewer.test.tsx @@ -118,6 +118,47 @@ describe('FileViewer', () => { expect(downloadButtons[0].hasAttribute('disabled')).toBe(false); }); + describe('isActive prop controls polling', () => { + it('polls when isActive and session is Running', () => { + mockUseWorkspaceFile.mockReturnValue({ + data: 'hello', + isLoading: false, + error: null, + } as ReturnType); + + render(); + + const opts = mockUseWorkspaceFile.mock.calls.at(-1)?.[3]; + expect(opts?.refetchInterval).toBe(5000); + }); + + it('does not poll when isActive is false even if session is Running', () => { + mockUseWorkspaceFile.mockReturnValue({ + data: 'hello', + isLoading: false, + error: null, + } as ReturnType); + + render(); + + const opts = mockUseWorkspaceFile.mock.calls.at(-1)?.[3]; + expect(opts?.refetchInterval).toBe(false); + }); + + it('does not poll when session is not Running regardless of isActive', () => { + mockUseWorkspaceFile.mockReturnValue({ + data: 'hello', + isLoading: false, + error: null, + } as ReturnType); + + render(); + + const opts = mockUseWorkspaceFile.mock.calls.at(-1)?.[3]; + expect(opts?.refetchInterval).toBe(false); + }); + }); + describe('download uses direct link instead of triggerDownload', () => { it('downloads via direct workspace API link, not triggerDownload', () => { mockUseWorkspaceFile.mockReturnValue({ diff --git a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/file-viewer.tsx b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/file-viewer.tsx index 9e7a54a32..e69ae5d8a 100644 --- a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/file-viewer.tsx +++ b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/file-viewer.tsx @@ -12,6 +12,8 @@ type FileViewerProps = { sessionName: string; filePath: string; sessionPhase?: string; + /** whether this tab is the currently visible one (gates polling to avoid background fetches) */ + isActive?: boolean; }; /** Displays a workspace file with download and refresh controls. */ @@ -20,6 +22,7 @@ export function FileViewer({ sessionName, filePath, sessionPhase, + isActive = true, }: FileViewerProps) { const { data: content, @@ -27,11 +30,9 @@ export function FileViewer({ error, refetch, } = useWorkspaceFile(projectName, sessionName, filePath, { - // Refetch when tab is first opened refetchOnMount: true, - // Only poll while actively viewing this file tab (component is mounted) AND session is running - // Automatically stops when switching to another tab (component unmounts) - refetchInterval: sessionPhase === "Running" ? 5000 : false, + // only poll when the tab is visible and session is running + refetchInterval: isActive && sessionPhase === "Running" ? 5000 : false, }); const fileName = filePath.split("/").pop() ?? "file"; diff --git a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/task-transcript-viewer.tsx b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/task-transcript-viewer.tsx index 535888c66..ed647fb78 100644 --- a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/task-transcript-viewer.tsx +++ b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/task-transcript-viewer.tsx @@ -15,6 +15,8 @@ type TaskTranscriptViewerProps = { sessionName: string taskId: string task?: BackgroundTask + /** whether this tab is the currently visible one (gates polling to avoid background fetches) */ + isActive?: boolean } export function TaskTranscriptViewer({ @@ -22,13 +24,14 @@ export function TaskTranscriptViewer({ sessionName, taskId, task, + isActive = true, }: TaskTranscriptViewerProps) { const isRunning = task?.status === "running" const { data, isLoading, error, refetch, isFetching } = useQuery({ queryKey: ["task-output", projectName, sessionName, taskId], queryFn: () => getTaskOutput(projectName, sessionName, taskId), - refetchInterval: isRunning ? 5000 : false, + refetchInterval: isActive && isRunning ? 5000 : false, }) const messages = useMemo( diff --git a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx index c7a91f074..2a6b6d2d0 100755 --- a/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx +++ b/components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx @@ -1578,113 +1578,136 @@ export default function ProjectSessionDetailPage({ ); } - // Chat/FileViewer/TaskTranscript content rendering helper + // all tab content is rendered simultaneously and toggled via CSS `hidden` + // so scroll position, input state, and react query cache are preserved across tab switches const renderMainContent = () => { - if (fileTabs.activeTab.type === "task") { - const task = aguiState.backgroundTasks.get(fileTabs.activeTab.taskId); - return ( - - ); - } + const isChatActive = fileTabs.activeTab.type === "chat"; - if (fileTabs.activeTab.type === "file") { - return ( - - ); - } - - // Chat view return ( - - - {repoChanging && ( -
- - - Updating Repositories... - -

Please wait while repositories are being updated. This may take 10-20 seconds...

-
-
-
- )} - -
- {(phase === "Creating" || phase === "Pending") && ( -
- + {/* chat -- always mounted, hidden when a file/task tab is active */} +
+ + + {repoChanging && ( +
+ + + Updating Repositories... + +

Please wait while repositories are being updated. This may take 10-20 seconds...

+
+
+
+ )} + +
+ {(phase === "Creating" || phase === "Pending") && ( +
+ +
+ )} + + username={currentUser?.username || currentUser?.displayName || "anonymous"} + initialPrompt={session?.spec?.initialPrompt} + activeWorkflow={workflowManagement.activeWorkflow || undefined} + messages={streamMessages} + traceId={langfuseTraceId || undefined} + messageFeedback={aguiState.messageFeedback} + > + Promise.resolve(sendChat())} + onSendToolAnswer={sendToolAnswer} + onInterrupt={aguiInterrupt} + onGoToResults={() => {}} + onContinue={handleContinue} + workflowMetadata={workflowMetadata} + onCommandClick={handleCommandClick} + isRunActive={isRunActive} + queuedMessages={sessionQueue.messages} + hasRealMessages={hasRealMessages} + onCancelQueuedMessage={sessionQueue.cancelMessage} + onUpdateQueuedMessage={sessionQueue.updateMessage} + onClearQueue={sessionQueue.clearMessages} + agentName={agentName} + onAddRepository={handleOpenContextModal} + onUploadFile={handleOpenUploadModal} + projectName={projectName} + workflowSlot={ + setCustomWorkflowDialogOpen(true)} + /> + } + /> +
- )} - +
+
+ + {/* file tabs -- one FileViewer per open tab, only the active one is visible */} + {fileTabs.openTabs.map((tab) => { + const isActive = fileTabs.activeTab.type === "file" && fileTabs.activeTab.path === tab.path; + return ( +
- Promise.resolve(sendChat())} - onSendToolAnswer={sendToolAnswer} - onInterrupt={aguiInterrupt} - onGoToResults={() => {}} - onContinue={handleContinue} - workflowMetadata={workflowMetadata} - onCommandClick={handleCommandClick} - isRunActive={isRunActive} - queuedMessages={sessionQueue.messages} - hasRealMessages={hasRealMessages} - onCancelQueuedMessage={sessionQueue.cancelMessage} - onUpdateQueuedMessage={sessionQueue.updateMessage} - onClearQueue={sessionQueue.clearMessages} - agentName={agentName} - onAddRepository={handleOpenContextModal} - onUploadFile={handleOpenUploadModal} + setCustomWorkflowDialogOpen(true)} - /> - } + sessionName={sessionName} + filePath={tab.path} + sessionPhase={phase} + isActive={isActive} /> - -
- - +
+ ); + })} + + {/* task tabs -- same pattern */} + {fileTabs.openTaskTabs.map((tab) => { + const isActive = fileTabs.activeTab.type === "task" && fileTabs.activeTab.taskId === tab.taskId; + const task = aguiState.backgroundTasks.get(tab.taskId); + return ( +
+ +
+ ); + })} + ); };