Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof useWorkspaceFile>);

render(<FileViewer {...defaultProps} sessionPhase="Running" isActive={true} />);

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<typeof useWorkspaceFile>);

render(<FileViewer {...defaultProps} sessionPhase="Running" isActive={false} />);

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<typeof useWorkspaceFile>);

render(<FileViewer {...defaultProps} sessionPhase="Completed" isActive={true} />);

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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -20,18 +22,17 @@ export function FileViewer({
sessionName,
filePath,
sessionPhase,
isActive = true,
}: FileViewerProps) {
const {
data: content,
isLoading,
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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,23 @@ 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({
projectName,
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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<TaskTranscriptViewer
projectName={projectName}
sessionName={sessionName}
taskId={fileTabs.activeTab.taskId}
task={task}
/>
);
}
const isChatActive = fileTabs.activeTab.type === "chat";

if (fileTabs.activeTab.type === "file") {
return (
<FileViewer
projectName={projectName}
sessionName={sessionName}
filePath={fileTabs.activeTab.path}
sessionPhase={phase}
/>
);
}

// Chat view
return (
<Card className="relative flex-1 flex flex-col overflow-hidden py-0 border-0 rounded-none">
<CardContent className="px-6 pt-0 pb-0 flex-1 flex flex-col overflow-hidden">
{repoChanging && (
<div className="absolute inset-0 bg-background/90 backdrop-blur-sm z-10 flex items-center justify-center rounded-lg">
<Alert className="max-w-md mx-4">
<Loader2 className="h-4 w-4 animate-spin" />
<AlertTitle>Updating Repositories...</AlertTitle>
<AlertDescription>
<p>Please wait while repositories are being updated. This may take 10-20 seconds...</p>
</AlertDescription>
</Alert>
</div>
)}

<div className="relative flex flex-col flex-1 overflow-hidden">
{(phase === "Creating" || phase === "Pending") && (
<div className="absolute inset-0 bg-background/80 backdrop-blur-sm z-10">
<SessionStartingEvents
<>
{/* chat -- always mounted, hidden when a file/task tab is active */}
<div className={cn("relative flex-1 flex flex-col overflow-hidden", !isChatActive && "hidden")}>
<Card className="relative flex-1 flex flex-col overflow-hidden py-0 border-0 rounded-none">
<CardContent className="px-6 pt-0 pb-0 flex-1 flex flex-col overflow-hidden">
{repoChanging && (
<div className="absolute inset-0 bg-background/90 backdrop-blur-sm z-10 flex items-center justify-center rounded-lg">
<Alert className="max-w-md mx-4">
<Loader2 className="h-4 w-4 animate-spin" />
<AlertTitle>Updating Repositories...</AlertTitle>
<AlertDescription>
<p>Please wait while repositories are being updated. This may take 10-20 seconds...</p>
</AlertDescription>
</Alert>
</div>
)}

<div className="relative flex flex-col flex-1 overflow-hidden">
{(phase === "Creating" || phase === "Pending") && (
<div className="absolute inset-0 bg-background/80 backdrop-blur-sm z-10">
<SessionStartingEvents
projectName={projectName}
sessionName={sessionName}
/>
</div>
)}
<FeedbackProvider
projectName={projectName}
sessionName={sessionName}
/>
username={currentUser?.username || currentUser?.displayName || "anonymous"}
initialPrompt={session?.spec?.initialPrompt}
activeWorkflow={workflowManagement.activeWorkflow || undefined}
messages={streamMessages}
traceId={langfuseTraceId || undefined}
messageFeedback={aguiState.messageFeedback}
>
<MessagesTab
session={session}
streamMessages={streamMessages}
chatInput={chatInput}
setChatInput={setChatInput}
onSendChat={() => 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={
<WorkflowSelector
sessionPhase={session?.status?.phase}
activeWorkflow={workflowManagement.activeWorkflow}
activeWorkflowDetails={
workflowManagement.activeWorkflow === "custom" && session?.spec?.activeWorkflow
? {
gitUrl: session.spec.activeWorkflow.gitUrl,
branch: session.spec.activeWorkflow.branch || "main",
path: session.spec.activeWorkflow.path || "",
}
: undefined
}
selectedWorkflow={workflowManagement.selectedWorkflow}
workflowActivating={workflowManagement.workflowActivating}
ootbWorkflows={ootbWorkflows}
onWorkflowChange={handleWorkflowChange}
onLoadCustom={() => setCustomWorkflowDialogOpen(true)}
/>
}
/>
</FeedbackProvider>
</div>
)}
<FeedbackProvider
projectName={projectName}
sessionName={sessionName}
username={currentUser?.username || currentUser?.displayName || "anonymous"}
initialPrompt={session?.spec?.initialPrompt}
activeWorkflow={workflowManagement.activeWorkflow || undefined}
messages={streamMessages}
traceId={langfuseTraceId || undefined}
messageFeedback={aguiState.messageFeedback}
</CardContent>
</Card>
</div>

{/* 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 (
<div
key={tab.path}
className={cn("flex-1 flex flex-col overflow-hidden", !isActive && "hidden")}
>
<MessagesTab
session={session}
streamMessages={streamMessages}
chatInput={chatInput}
setChatInput={setChatInput}
onSendChat={() => 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}
<FileViewer
projectName={projectName}
workflowSlot={
<WorkflowSelector
sessionPhase={session?.status?.phase}
activeWorkflow={workflowManagement.activeWorkflow}
activeWorkflowDetails={
workflowManagement.activeWorkflow === "custom" && session?.spec?.activeWorkflow
? {
gitUrl: session.spec.activeWorkflow.gitUrl,
branch: session.spec.activeWorkflow.branch || "main",
path: session.spec.activeWorkflow.path || "",
}
: undefined
}
selectedWorkflow={workflowManagement.selectedWorkflow}
workflowActivating={workflowManagement.workflowActivating}
ootbWorkflows={ootbWorkflows}
onWorkflowChange={handleWorkflowChange}
onLoadCustom={() => setCustomWorkflowDialogOpen(true)}
/>
}
sessionName={sessionName}
filePath={tab.path}
sessionPhase={phase}
isActive={isActive}
/>
</FeedbackProvider>
</div>
</CardContent>
</Card>
</div>
);
})}

{/* 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 (
<div
key={tab.taskId}
className={cn("flex-1 flex flex-col overflow-hidden", !isActive && "hidden")}
>
<TaskTranscriptViewer
projectName={projectName}
sessionName={sessionName}
taskId={tab.taskId}
task={task}
isActive={isActive}
/>
</div>
);
})}
</>
);
};

Expand Down