Skip to content

Commit b9b2582

Browse files
authored
perf(frontend): load only the selected trial on drawer open (#1283)
1 parent 86f1275 commit b9b2582

5 files changed

Lines changed: 74 additions & 95 deletions

File tree

frontend/e2e/critical-react-subtree.spec.ts

Lines changed: 33 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -460,31 +460,31 @@ test.describe("critical task and trial subtree", () => {
460460
const taskOpenResponse = page.waitForResponse(taskOpenPattern);
461461
taskOpenGate.release();
462462
await taskOpenResponse;
463-
const taskDetailRequest = page.waitForRequest(taskDetailPattern);
464-
await page.getByRole("button", { name: "View task files" }).click();
465-
await taskDetailRequest;
466-
expect(requestCount(requests, taskDetailPattern)).toBe(1);
467-
await expect(
468-
page.getByRole("button", { name: "Rerun trials" })
469-
).toBeDisabled();
470-
await expect(
471-
page.getByRole("button", { name: "Rerun QA", exact: true })
472-
).toBeDisabled();
473-
await page.keyboard.press("Escape");
474-
await expect(
475-
page.getByRole("button", { name: "Rerun QA", exact: true })
476-
).toBeHidden();
477-
478463
const trialButton = page.getByRole("button", { name: "trial-p1 Fail" });
479464
await expect(trialButton).toBeVisible();
480465

481466
const trialDetailPattern = new RegExp(`/api/trials/${TRIAL_ID}(?:\\?|$)`);
467+
const taskTrialsPattern = new RegExp(
468+
`/api/tasks/${TASK_ID}/trials(?:\\?|$)`
469+
);
470+
const taskFilesPattern = new RegExp(
471+
`/api/tasks/${TASK_ID}/files(?:/|\\?|$)`
472+
);
473+
const analysisLogPattern = new RegExp(
474+
`/api/trials/${TRIAL_ID}/analysis-log(?:\\?|$)`
475+
);
476+
const trialFilesPattern = new RegExp(
477+
`/api/trials/${TRIAL_ID}/files(?:/|\\?|$)`
478+
);
479+
const trajectoryPattern = new RegExp(
480+
`/api/trials/${TRIAL_ID}/trajectory(?:/|\\?|$)`
481+
);
482482
const trialDetailRequest = page.waitForRequest(trialDetailPattern);
483483
await trialButton.click();
484484
await trialDetailRequest;
485485
expect(requestCount(requests, trialDetailPattern)).toBe(1);
486-
// Both canonical requests are blocked. The snapshot still paints Summary,
487-
// but every mutation waits for the full trial resource.
486+
// The lightweight /open row paints Summary, but mutations wait for the
487+
// selected trial resource. The hidden task pane owns no network work.
488488
await expect(page.getByRole("tab", { name: "Summary" })).toBeVisible();
489489
await expect(
490490
page.getByRole("button", { name: "Retry Trial" })
@@ -494,34 +494,30 @@ test.describe("critical task and trial subtree", () => {
494494
).toBeDisabled();
495495
await expect(page.getByText("Loading latest trial state.")).toBeVisible();
496496

497+
await page.waitForTimeout(300);
498+
expect(requestCount(requests, taskDetailPattern)).toBe(0);
499+
expect(requestCount(requests, taskTrialsPattern)).toBe(0);
500+
expect(requestCount(requests, taskFilesPattern)).toBe(0);
501+
expect(requestCount(requests, analysisLogPattern)).toBe(0);
502+
expect(requestCount(requests, trialFilesPattern)).toBe(0);
503+
expect(requestCount(requests, trajectoryPattern)).toBe(0);
504+
505+
const taskDetailRequest = page.waitForRequest(taskDetailPattern);
506+
await page.getByRole("button", { name: "Show task" }).click();
507+
await taskDetailRequest;
508+
expect(requestCount(requests, taskDetailPattern)).toBe(1);
509+
await expect.poll(() => requestCount(requests, taskTrialsPattern)).toBe(1);
510+
expect(requestCount(requests, taskFilesPattern)).toBe(0);
497511
taskDetailGate.release();
498-
// The open drawer adopts the canonical task list instead of retaining its
499-
// snapshot copy. That list also contributes the selected trial's report.
512+
513+
trialDetailGate.release();
500514
await expect(
501515
page.getByRole("heading", { name: "GOOD FAILURE", exact: true })
502516
).toBeVisible();
503-
const nextTrialButton = page.getByRole("button", { name: "Next trial" });
504-
await expect(nextTrialButton).toBeEnabled();
505517
const analysisLogDisclosure = page
506518
.locator("summary")
507519
.filter({ hasText: "Analysis log" });
508520
await expect(analysisLogDisclosure).toBeVisible();
509-
510-
await page.waitForTimeout(300);
511-
const analysisLogPattern = new RegExp(
512-
`/api/trials/${TRIAL_ID}/analysis-log(?:\\?|$)`
513-
);
514-
const trialFilesPattern = new RegExp(
515-
`/api/trials/${TRIAL_ID}/files(?:/|\\?|$)`
516-
);
517-
const trajectoryPattern = new RegExp(
518-
`/api/trials/${TRIAL_ID}/trajectory(?:/|\\?|$)`
519-
);
520-
expect(requestCount(requests, analysisLogPattern)).toBe(0);
521-
expect(requestCount(requests, trialFilesPattern)).toBe(0);
522-
expect(requestCount(requests, trajectoryPattern)).toBe(0);
523-
524-
trialDetailGate.release();
525521
await expect(
526522
page.getByRole("button", { name: "Re-run analysis" })
527523
).toBeEnabled();
@@ -559,14 +555,6 @@ test.describe("critical task and trial subtree", () => {
559555
page.getByRole("button", { name: "Retry Trial" })
560556
).toBeEnabled();
561557

562-
const probeDetailPattern = new RegExp(
563-
`/api/trials/${PROBE_TRIAL_ID}(?:\\?|$)`
564-
);
565-
const probeDetailRequest = page.waitForRequest(probeDetailPattern);
566-
await nextTrialButton.click();
567-
await probeDetailRequest;
568-
await expect(page.getByText("probe-p1", { exact: true })).toBeVisible();
569-
570558
await page.goto("/tasks");
571559
const failedTaskLink = page.getByRole("link", {
572560
name: "P1 failed detail task",

frontend/e2e/tasks-view.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ test.describe("authenticated task view", () => {
311311
return url.pathname === `/api/tasks/${TASK_ID}/files`;
312312
});
313313
await page.goto(`/tasks/${TASK_ID}?drawer=task`);
314+
await page.getByRole("button", { name: "Files", exact: true }).click();
314315

315316
const response = await listingResponse;
316317
const url = new URL(response.url());
@@ -469,6 +470,7 @@ test.describe("authenticated task view", () => {
469470
});
470471

471472
await page.goto(`/tasks/${TASK_ID}?drawer=task`);
473+
await page.getByRole("button", { name: "Files", exact: true }).click();
472474
await page.getByRole("button", { name: "readme.txt" }).click();
473475
await expect(page.getByText("text preview loaded")).toBeVisible();
474476
await expect(page.getByText(/Showing first 100\.0 KB/)).toBeVisible();

frontend/src/app/(app)/tasks/[task_id]/task-detail-client.tsx

Lines changed: 9 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import Link from "next/link";
44
import dynamic from "next/dynamic";
55
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
6-
import useSWR from "swr";
76
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
87
import { Badge } from "@/components/ui/badge";
98
import { Button } from "@/components/ui/button";
@@ -25,7 +24,6 @@ import { TaskVerdictBadge } from "@/components/task-verdict-badge";
2524
import { UnifiedDrawerWrapper } from "@/components/unified-drawer-wrapper";
2625
import { ExperimentsList } from "@/components/experiments-list";
2726
import { QaCostSuffix } from "@/components/qa-cost-suffix";
28-
import { fetcher } from "@/lib/api";
2927
import { getExperimentAgentKey } from "@/lib/experiment-agent-grouping";
3028
import {
3129
formatCostUsd,
@@ -42,11 +40,6 @@ import {
4240
STATUS_CONFIG,
4341
} from "@/lib/status-config";
4442
import { summarizeTrials, type TrialAggregate } from "@/lib/trial-aggregation";
45-
import {
46-
isBrowseTaskDetail,
47-
taskDetailValue,
48-
type TaskDetailResource,
49-
} from "@/lib/task-detail-resource";
5043
import type {
5144
Task,
5245
TaskOpenAgentModelSummary,
@@ -662,7 +655,6 @@ export function TaskDetailClient({
662655
agentCards,
663656
defaultVersionError,
664657
defaultVersionId,
665-
detailKey,
666658
error,
667659
explicitVersionMissing,
668660
handleSelectVersion,
@@ -693,42 +685,22 @@ export function TaskDetailClient({
693685
[selectedVersion, trialsForVersion]
694686
);
695687
const [drawer, setDrawer] = useState<DrawerState | null>(null);
696-
const [drawerShowTask, setDrawerShowTask] = useState(true);
688+
const [drawerShowTask, setDrawerShowTask] = useState(false);
697689
const [drawerShowTrial, setDrawerShowTrial] = useState(true);
698-
const { data: drawerDetailResource, isLoading: isDrawerDetailLoading } =
699-
useSWR<TaskDetailResource>(drawer ? detailKey : null, fetcher, {
700-
revalidateOnFocus: false,
701-
revalidateOnMount: true,
702-
});
703-
const canonicalDrawerDetailResource =
704-
drawerDetailResource && !isBrowseTaskDetail(drawerDetailResource)
705-
? drawerDetailResource
706-
: undefined;
707-
const canonicalDrawerDetail = taskDetailValue(canonicalDrawerDetailResource);
708-
const drawerTask = canonicalDrawerDetail?.task ?? task;
709-
const drawerTrialsForVersion = useMemo(() => {
710-
const canonicalTrials = canonicalDrawerDetail?.task.trials;
711-
if (!canonicalTrials || selectedVersionId === null) {
712-
return trialsForVersion;
713-
}
714-
return canonicalTrials.filter(
715-
(trial) => trial.task_version_id === selectedVersionId
716-
);
717-
}, [canonicalDrawerDetail?.task.trials, selectedVersionId, trialsForVersion]);
718690
const drawerTrialGroups = useMemo(
719691
() =>
720692
agentCards.map((card) => ({
721693
agent: card.key,
722694
model: card.summary.model,
723-
trials: drawerTrialsForVersion.filter(
695+
trials: trialsForVersion.filter(
724696
(trial) =>
725697
getExperimentAgentKey(
726698
normalizedAgentModel(trial),
727699
modelScopedAgents
728700
) === card.key
729701
),
730702
})),
731-
[agentCards, drawerTrialsForVersion, modelScopedAgents]
703+
[agentCards, modelScopedAgents, trialsForVersion]
732704
);
733705
const drawerOrderedTrials = useMemo(
734706
() => drawerTrialGroups.flatMap((group) => group.trials),
@@ -774,6 +746,7 @@ export function TaskDetailClient({
774746
// The user (or hydration) is driving the drawer now; any unresolved
775747
// deep-link trial param no longer needs preserving.
776748
unresolvedTrialParamRef.current = false;
749+
setDrawerShowTask(false);
777750
setDrawer({ mode: "trial", fallbackTrial: trial });
778751
}, []);
779752

@@ -892,6 +865,7 @@ export function TaskDetailClient({
892865
drawerHydratedRef.current = true;
893866
hydrationOpeningRef.current = true;
894867
handleSelectTrial(previewTrial);
868+
if (urlTaskFile || urlTaskPane) setDrawerShowTask(true);
895869
return;
896870
}
897871
if (isDeepLinkTrialLoading) return;
@@ -911,6 +885,7 @@ export function TaskDetailClient({
911885
}
912886
hydrationOpeningRef.current = true;
913887
handleSelectTrial(fetchedDeepLinkTrial);
888+
if (urlTaskFile || urlTaskPane) setDrawerShowTask(true);
914889
return;
915890
}
916891

@@ -1394,15 +1369,11 @@ export function TaskDetailClient({
13941369
taskId={null}
13951370
// Scopes the overview's trial aggregation; this pane renders
13961371
// no header, so none of the task-driven header UI appears.
1397-
task={drawerTask ?? task}
1372+
task={task}
13981373
staticChecksTaskId={task.id}
1399-
taskDetail={canonicalDrawerDetailResource}
14001374
onOpenTrial={handleOpenTrialFromOverview}
14011375
filesUrl={`/api/tasks/${task.id}/files`}
14021376
loadFilesLazily
1403-
overviewTrialsLoading={
1404-
isDrawerDetailLoading || canonicalDrawerDetail == null
1405-
}
14061377
taskVersion={selectedVersion?.version}
14071378
initialFilePath={taskPaneFile}
14081379
selectedLines={taskPaneLines}
@@ -1419,12 +1390,8 @@ export function TaskDetailClient({
14191390
activePane={activeTaskPane}
14201391
onActivePaneChange={selectTaskPane}
14211392
taskId={task.id}
1422-
task={drawerTask ?? task}
1423-
taskDetail={canonicalDrawerDetailResource}
1393+
task={task}
14241394
loadFilesLazily
1425-
overviewTrialsLoading={
1426-
isDrawerDetailLoading || canonicalDrawerDetail == null
1427-
}
14281395
taskVersion={selectedVersion?.version}
14291396
onOpenTrial={handleOpenTrialFromOverview}
14301397
initialFilePath={taskPaneFile}
@@ -1452,7 +1419,7 @@ export function TaskDetailClient({
14521419
isOpen={true}
14531420
onClose={() => setDrawer(null)}
14541421
trial={drawerTrial}
1455-
task={drawerTask ?? task}
1422+
task={task}
14561423
orderedTrials={drawerOrderedTrials}
14571424
trialIndex={drawerTrialIndex >= 0 ? drawerTrialIndex : null}
14581425
trialGroups={drawerTrialGroups}

frontend/src/components/task-files-panel.tsx

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ export function TaskFilesPanel({
416416
isOpen,
417417
onClose,
418418
taskId,
419-
task,
419+
task: taskSnapshot,
420420
orderedTasks,
421421
taskIndex,
422422
onNavigate,
@@ -450,8 +450,8 @@ export function TaskFilesPanel({
450450
// panes (which pass taskId={null}); staticChecksTaskId supplies the id there.
451451
const effectiveChecksTaskId = taskId ?? staticChecksTaskId ?? null;
452452
// The pre_trial_* fields live on the version summaries of /detail, not on
453-
// the plain task endpoint. The task page uses the same key, so SWR shares
454-
// the cache there.
453+
// the plain task endpoint. Task cards seed this key from their browse rows;
454+
// SWR replaces that snapshot with the full response when this pane mounts.
455455
const checksKey =
456456
effectiveChecksTaskId && showAnalysis !== false
457457
? taskDetailKey(effectiveChecksTaskId, baseUrl)
@@ -481,6 +481,9 @@ export function TaskFilesPanel({
481481
},
482482
});
483483
const checksDetail = taskDetailValue(checksResource);
484+
const task = cancelExperimentId
485+
? taskSnapshot
486+
: (checksDetail?.task ?? taskSnapshot);
484487
const actionsReady =
485488
checksResource !== undefined && !isBrowseTaskDetail(checksResource);
486489
// Scoped panes (the experiment drawer) pin the version whose files are on
@@ -933,7 +936,7 @@ export function TaskFilesPanel({
933936

934937
// Fetch root file list when panel opens
935938
useEffect(() => {
936-
if (!isOpen || (!taskId && !filesUrl)) {
939+
if (!isOpen || activePane !== "file" || (!taskId && !filesUrl)) {
937940
return;
938941
}
939942

@@ -1023,6 +1026,7 @@ export function TaskFilesPanel({
10231026
};
10241027
}, [
10251028
isOpen,
1029+
activePane,
10261030
taskId,
10271031
filesUrl,
10281032
resolvedFilesUrl,
@@ -1530,11 +1534,30 @@ export function TaskFilesPanel({
15301534
<span className="truncate">Capabilities</span>
15311535
</button>
15321536
) : null}
1537+
<button
1538+
type="button"
1539+
onClick={() => onActivePaneChange?.("file")}
1540+
aria-current={activePane === "file" ? "page" : undefined}
1541+
className={`flex w-full items-center gap-1.5 rounded px-2 py-1 text-left text-sm ${
1542+
activePane === "file"
1543+
? "bg-primary/20 text-primary"
1544+
: "hover:bg-muted/50 cursor-pointer"
1545+
}`}
1546+
title="Browse task files"
1547+
>
1548+
<FolderOpen
1549+
className="h-3.5 w-3.5 shrink-0"
1550+
aria-hidden="true"
1551+
/>
1552+
<span className="truncate">Files</span>
1553+
</button>
15331554
</div>
15341555
)}
1535-
<div className="text-muted-foreground px-2 py-2 font-mono text-[10px] font-semibold tracking-wide uppercase sm:text-xs">
1536-
Files
1537-
</div>
1556+
{!taskPaneExists ? (
1557+
<div className="text-muted-foreground px-2 py-2 font-mono text-[10px] font-semibold tracking-wide uppercase sm:text-xs">
1558+
Files
1559+
</div>
1560+
) : null}
15381561
{listingError ? (
15391562
<p className="text-muted-foreground px-2 py-2 text-xs">
15401563
Unable to load files: {listingError}

frontend/src/lib/use-task-open-reader.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,6 @@ export function useTaskOpenReader(
319319
agentCards,
320320
defaultVersionError,
321321
defaultVersionId,
322-
detailKey,
323322
error,
324323
exactAgentModels,
325324
explicitVersionMissing,

0 commit comments

Comments
 (0)