Skip to content

Commit da3b501

Browse files
pfbyjyclaude
andauthored
Merge task overview and trial QA into unified panel (#1065)
* Open a task overview instead of the first trial from experiments Clicking a task in an experiment used to jump straight into its first trial; the task pane's default view was the static checks alone. Both now land on a task overview that gathers the task's QA in one place: - New TaskOverviewPanel: QA verdict (in panes without their own verdict card), the static checks, and the trial-level QA aggregated across the shown version's trials — classification counts, deduped findings (by the server's content-hash action-item ids), and per-trial classifications, each linking to the trial that surfaced it. - Trial links open in the caller's own drawer when the trial is addressable there, falling back to the task page deep link (?version=&trial=) otherwise. - The experiment grid's task click opens task mode (the overview) rather than promoting to the first trial; "View trials" and the overview's links drill in. - SeverityGroups accepts an optional renderItemFooter so aggregated findings can append their "seen in" trial links. The overview replaces the "Static checks" entry at the top of the task pane sidebar; probes stay excluded and the public share view (which hides analysis) is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRu1mqJXR7HzspFFCsP2fs * Merge the static checks into the overview's unified findings The overview no longer carries a standalone "Static checks" section. Source-audit findings and trial-QA action items now land in one Findings list, deduped by the server's content-hash ids, with each item tagged by its sources: a "Source audit" chip and/or links to the trials that surfaced it. The audit's lifecycle survives in the section header — status summary, cost, and the Run/Re-run audit button, with the same loading/unknown-state guard that kept a misread "unaudited" from wiping real findings. StaticChecksPanel had no other consumers, so the component is deleted; its staticCheckState state machine moves into the overview panel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRu1mqJXR7HzspFFCsP2fs * Join audit and trial findings through links_to, not ids Bugbot caught that the overview's finding dedupe could never merge across sources: the server's content-hash ids include the analyzer source, so an audit (pre_trial) row and a trial (post_trial) row for the same defect always hash differently. The real cross-source join is links_to, which the panel now reads from both places: a trial's exploitation assessments put exploiting trials on the audit row's "seen in" list, and a post-trial action item that links to an audit finding folds into that row instead of duplicating it. Trials are deduped per row so a trial contributing through both paths appears once. Also stop the clean-state copy from vouching for trial QA that hasn't run: without any analyzed trials it now only speaks for the source audit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRu1mqJXR7HzspFFCsP2fs * Keep the overview's trial aggregation on the pinned version Bugbot caught that the overview received version={checksVersion?.version ?? null}: while /detail was loading (or after it failed) the prop collapsed to null — the "aggregate every version" value — even when the host pane already had a pinned taskVersion. The pinned version now wins outright, and without a pin the prop stays undefined until /detail resolves, which the panel renders as a waiting state instead of a briefly version-spanning aggregation; a dead /detail says the version could not be resolved. Only a loaded task with no versions is genuinely unscoped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRu1mqJXR7HzspFFCsP2fs * Open overview trial links in the drawer, never the task page The overview's trial links only opened in place when the trial was already in the host's loaded trial list, and routed to the task page otherwise. On the experiment page that list streams in progressively, so the lookup usually missed and a click navigated away from an experiment whose drawer can render the trial on the same page. The overview hands the full trial row to the callback, so both hosts now always open it in their own drawer — a grid/version-list match is still preferred so the per-group trial nav lines up, and an unmatched trial opens with no group index. The task-page deep link remains only as the no-callback fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRu1mqJXR7HzspFFCsP2fs * Make the overview's trial data cheap: scope, seed, and calm the polling Opening any task fetched every trial the task ever had — all versions, all experiments, each with its full analysis payload — and re-fetched it on a 5s loop whenever an analysis sat pending, which is most tasks with QA enabled. That made the whole page feel slow. - GET /tasks/{task_id}/trials gains a `version` filter (inner join to task_versions), so the overview's fetch carries one version's rows instead of the task's entire history; the Next proxy forwards it and the panel waits for the resolved version before fetching at all. Deploy skew is safe: an older backend ignores the param and the panel's client-side version filter still applies. - The panel seeds from the trials the host already has in memory (grid rows / task page), so the aggregation paints immediately; the scoped fetch then replaces the seed to add action items and exploitation. Seeded rows also cover a failed fetch instead of an error flash. - Polling drops to 15s and stops revalidating on focus. Also addresses this round's Bugbot findings: a /detail load failure now shows the error above whatever trial-side findings exist instead of hiding them, and the rows handed to SeverityGroups carry trial ids rather than full trial objects so the per-item copy button no longer serializes nested trial payloads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRu1mqJXR7HzspFFCsP2fs * Treat an empty trial seed as no seed Bugbot caught that an empty seed array slipped past the loading gate: displayTrials became [] while the scoped fetch was still in flight, so the pane flashed "No trials" for a task whose rows simply weren't in the host's memory yet. Only a non-empty seed may paint; an empty one falls back to the loading state until the fetch answers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRu1mqJXR7HzspFFCsP2fs * Scope the overview's trial QA to the host's own trials In an experiment drawer the aggregation pulled from the task-wide trials fetch, so trials from other experiments showed up in a context that should only speak for its own rows. The host's trial set is now the authoritative scope: the experiment drawer and task page pass their own rows, and the fetch only enriches those by id with the fields the compact payload omits (action items, exploitation) — it can never widen the set. An empty scope renders as loading while the host is still streaming trials and as an honest "no trials" once it isn't. Panes with no host scope (none remain today) fall back to the fetched version-scoped set. Also drops the backend `version` filter from this branch — that, its test, and the core helper change move to their own PR so this one stays a pure side-panel change. The frontend still sends the param; a backend without it ignores it, and the scope semantics above no longer depend on it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRu1mqJXR7HzspFFCsP2fs --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9b13139 commit da3b501

8 files changed

Lines changed: 917 additions & 226 deletions

File tree

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,20 @@ export function TaskDetailClient({
878878
[orderedTrials, trialGroups]
879879
);
880880

881+
// A trial link from the task overview's aggregated QA. Always opens in
882+
// this page's drawer: the overview hands over the full trial row, so a
883+
// trial the current version list doesn't carry still renders in place
884+
// instead of routing away. The version-list match is preferred so the
885+
// per-group trial nav lines up.
886+
const handleOpenTrialFromOverview = useCallback(
887+
(trial: Trial): boolean => {
888+
const match = orderedTrials.find((t) => t.id === trial.id);
889+
handleSelectTrial(match ?? trial);
890+
return true;
891+
},
892+
[orderedTrials, handleSelectTrial]
893+
);
894+
881895
const handleOpenTaskFiles = useCallback(() => {
882896
unresolvedTrialParamRef.current = false;
883897
setDrawer({
@@ -1406,7 +1420,11 @@ export function TaskDetailClient({
14061420
isOpen={true}
14071421
onClose={() => {}}
14081422
taskId={null}
1423+
// Scopes the overview's trial aggregation; this pane renders
1424+
// no header, so none of the task-driven header UI appears.
1425+
task={task}
14091426
staticChecksTaskId={task.id}
1427+
onOpenTrial={handleOpenTrialFromOverview}
14101428
filesUrl={`/api/tasks/${task.id}/files`}
14111429
taskVersion={selectedVersion?.version}
14121430
initialFilePath={taskPaneFile}
@@ -1424,6 +1442,7 @@ export function TaskDetailClient({
14241442
taskId={task.id}
14251443
task={task}
14261444
taskVersion={selectedVersion?.version}
1445+
onOpenTrial={handleOpenTrialFromOverview}
14271446
initialFilePath={taskPaneFile}
14281447
selectedLines={taskPaneLines}
14291448
onSelectLinesChange={setTaskPaneLines}

frontend/src/app/api/tasks/[task_id]/trials/route.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,17 @@ export async function GET(
1616

1717
const { task_id } = await params;
1818

19-
const probe = new URL(request.url).searchParams.get("probe");
20-
const queryParams = probe !== null ? { probe } : undefined;
21-
const url = getBackendUrl("tasks", `/${task_id}/trials`, queryParams);
19+
const incoming = new URL(request.url).searchParams;
20+
const probe = incoming.get("probe");
21+
const version = incoming.get("version");
22+
const queryParams: Record<string, string> = {};
23+
if (probe !== null) queryParams.probe = probe;
24+
if (version !== null) queryParams.version = version;
25+
const url = getBackendUrl(
26+
"tasks",
27+
`/${task_id}/trials`,
28+
Object.keys(queryParams).length > 0 ? queryParams : undefined,
29+
);
2230
const res = await fetch(url, {
2331
cache: "no-store",
2432
headers: getAuthHeaders(token),

frontend/src/components/experiment-detail-view.tsx

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1571,6 +1571,30 @@ export function ExperimentDetailView({
15711571
});
15721572
};
15731573

1574+
// A trial link from the task overview's aggregated QA. Always opens in
1575+
// this drawer: the overview hands over the full trial row, so even a
1576+
// trial the grid hasn't streamed in yet (or one gathered from another
1577+
// experiment) renders in place — never a navigation away. A grid match
1578+
// is still preferred so the per-group trial nav lines up.
1579+
const handleOpenTrialFromOverview = useCallback(
1580+
(trial: Trial): boolean => {
1581+
if (!drawerState) return false;
1582+
const { trialGroups, orderedTrials } = buildTrialGroups(drawerState.task);
1583+
const trialIndex = orderedTrials.findIndex((t) => t.id === trial.id);
1584+
cancelPendingDeepLink();
1585+
setDrawerState({
1586+
...drawerState,
1587+
mode: "trial",
1588+
trial: trialIndex >= 0 ? orderedTrials[trialIndex] : trial,
1589+
trialIndex: trialIndex >= 0 ? trialIndex : null,
1590+
orderedTrials,
1591+
trialGroups,
1592+
});
1593+
return true;
1594+
},
1595+
[drawerState, buildTrialGroups, cancelPendingDeepLink]
1596+
);
1597+
15741598
return (
15751599
<>
15761600
{isInitialLoading ? (
@@ -1690,19 +1714,18 @@ export function ExperimentDetailView({
16901714
onTaskSelect={(task, context) => {
16911715
cancelPendingDeepLink();
16921716
const { trialGroups, orderedTrials } = buildTrialGroups(task);
1693-
// If the task has trials, jump straight into the first one
1694-
// so the user immediately sees results alongside the task
1695-
// definition. They can navigate back with the in-drawer
1696-
// "View task" control.
1697-
const firstTrial = orderedTrials[0] ?? null;
1717+
// Land on the task overview — task-level QA plus the
1718+
// aggregated trial QA — never on a specific trial. Trials
1719+
// are one click away via "View trials" or the overview's
1720+
// per-trial links.
16981721
setDrawerState({
16991722
isOpen: true,
1700-
mode: firstTrial ? "trial" : "task",
1723+
mode: "task",
17011724
task,
17021725
taskIndex: context.taskIndex,
17031726
orderedTasks: context.orderedTasks,
1704-
trial: firstTrial,
1705-
trialIndex: firstTrial ? 0 : null,
1727+
trial: null,
1728+
trialIndex: null,
17061729
orderedTrials,
17071730
trialGroups,
17081731
});
@@ -1727,7 +1750,13 @@ export function ExperimentDetailView({
17271750
isOpen={true}
17281751
onClose={() => {}}
17291752
taskId={null}
1753+
// The task prop scopes the overview's trial aggregation to
1754+
// this experiment's rows; this pane renders no header, so
1755+
// none of the task-driven header UI appears.
1756+
task={drawerState.task}
17301757
staticChecksTaskId={drawerState.task.id}
1758+
onOpenTrial={handleOpenTrialFromOverview}
1759+
overviewTrialsLoading={isLoadingTrials}
17311760
filesUrl={`${apiBaseUrl}/tasks/${drawerState.task.id}/files`}
17321761
taskVersion={resolveExperimentTaskVersion(drawerState.task)}
17331762
initialFilePath={taskPaneFile}
@@ -1769,6 +1798,8 @@ export function ExperimentDetailView({
17691798
? handleNavigateToFirstTrial
17701799
: undefined
17711800
}
1801+
onOpenTrial={handleOpenTrialFromOverview}
1802+
overviewTrialsLoading={isLoadingTrials}
17721803
initialFilePath={taskPaneFile}
17731804
selectedLines={taskPaneLines}
17741805
onSelectLinesChange={setTaskPaneLines}

frontend/src/components/qa-report/action-items.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { ReactNode } from "react";
2+
13
import { cn } from "@/lib/utils";
24
import { AnalysisProse } from "@/components/analysis-prose";
35
import type { PreTrialFinding } from "@/lib/types";
@@ -17,10 +19,12 @@ function ActionItemDetail({
1719
item,
1820
itemKey,
1921
onFeedback,
22+
renderItemFooter,
2023
}: {
2124
item: PreTrialFinding;
2225
itemKey: string;
2326
onFeedback?: (r: FeedbackRecord) => void;
27+
renderItemFooter?: (item: PreTrialFinding, itemKey: string) => ReactNode;
2428
}) {
2529
const where = findingLocation(item);
2630
return (
@@ -75,6 +79,8 @@ function ActionItemDetail({
7579
</div>
7680
) : null}
7781

82+
{renderItemFooter?.(item, itemKey)}
83+
7884
{onFeedback ? (
7985
<FeedbackControl
8086
label={`action item: ${item.title ?? itemKey}`}
@@ -101,12 +107,15 @@ export function SeverityGroups({
101107
onFeedback,
102108
className,
103109
tierEffects,
110+
renderItemFooter,
104111
}: {
105112
items: PreTrialFinding[];
106113
onFeedback?: (r: FeedbackRecord) => void;
107114
className?: string;
108115
/** Per-tier effect line; the default narrates trial classification. */
109116
tierEffects?: Partial<Record<string, string>>;
117+
/** Extra content under an item — e.g. links to the trials that surfaced it. */
118+
renderItemFooter?: (item: PreTrialFinding, itemKey: string) => ReactNode;
110119
}) {
111120
const groups = TIER_ORDER.map((tier) => ({
112121
tier,
@@ -155,6 +164,7 @@ export function SeverityGroups({
155164
item={item}
156165
itemKey={key}
157166
onFeedback={onFeedback}
167+
renderItemFooter={renderItemFooter}
158168
/>
159169
</li>
160170
);

frontend/src/components/static-checks-panel.tsx

Lines changed: 0 additions & 168 deletions
This file was deleted.

0 commit comments

Comments
 (0)