From 3cdda5b71f4857ad6ce942c6d1faf10f0671e122 Mon Sep 17 00:00:00 2001 From: Oliver Browne Date: Tue, 28 Apr 2026 17:19:09 +0000 Subject: [PATCH 1/4] refactor: clean up ReportTaskLogs and fix bar expand/collapse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract BarRow + buildBars out of the inline render, drive bar order from a single BAR_ORDER constant, and use a uniform
for every interactive bar so clicking the same bar in the expanded state reliably collapses it — and so research expands cleanly when an implementation task is also present. Generated-By: PostHog Code Task-Id: 6b2b677f-e9c9-4028-b37e-d92158107040 --- .../components/detail/ReportTaskLogs.tsx | 434 +++++++++--------- 1 file changed, 217 insertions(+), 217 deletions(-) diff --git a/apps/code/src/renderer/features/inbox/components/detail/ReportTaskLogs.tsx b/apps/code/src/renderer/features/inbox/components/detail/ReportTaskLogs.tsx index c0e64fc2c..e1267ffcd 100644 --- a/apps/code/src/renderer/features/inbox/components/detail/ReportTaskLogs.tsx +++ b/apps/code/src/renderer/features/inbox/components/detail/ReportTaskLogs.tsx @@ -14,15 +14,17 @@ import type { SignalReportStatus, SignalReportTask, Task } from "@shared/types"; import { useState } from "react"; type Relationship = SignalReportTask["relationship"]; +type BarRelationship = Extract; -const RELATIONSHIP_LABELS: Record = { - repo_selection: "Repository selection", +const RELATIONSHIP_LABELS: Record = { research: "Research task", implementation: "Implementation task", }; -// We display relationships in this order, top to bottom. -const DISPLAYED_RELATIONSHIPS: Relationship[] = ["implementation", "research"]; +// Bars are rendered top-to-bottom in this order. +const BAR_ORDER: BarRelationship[] = ["research", "implementation"]; + +const BAR_HEIGHT = 38; interface ReportTaskData { task: Task; @@ -39,20 +41,15 @@ function useReportTasks(reportId: string, reportStatus: SignalReportStatus) { ["inbox", "report-tasks", reportId], async (client) => { const reportTasks = await client.getSignalReportTasks(reportId); - const relevant = reportTasks.filter((rt) => - DISPLAYED_RELATIONSHIPS.includes(rt.relationship), + const relevant = reportTasks.filter((rt): rt is SignalReportTask => + (BAR_ORDER as Relationship[]).includes(rt.relationship), ); - const tasks = await Promise.all( + return Promise.all( relevant.map(async (rt) => { const task = (await client.getTask(rt.task_id)) as unknown as Task; return { task, relationship: rt.relationship }; }), ); - return tasks.sort( - (a, b) => - DISPLAYED_RELATIONSHIPS.indexOf(a.relationship) - - DISPLAYED_RELATIONSHIPS.indexOf(b.relationship), - ); }, { enabled: !!reportId, @@ -165,20 +162,194 @@ export function getTaskPrUrl(task: Task): string | null { return null; } -const BAR_HEIGHT = 38; - interface Bar { - relationship: Relationship; + relationship: BarRelationship; task: Task | null; summary: BarSummary; /** Tooltip shown on hover (e.g. pipeline status explanation). */ tooltip?: string; - /** When set, render a run-action button with this label instead of (or alongside) the status label. */ + /** When set, render a run-action button with this label alongside (or instead of) the status label. */ runActionLabel?: string; /** PR URL produced by the implementation task, if available. */ prUrl?: string | null; } +function buildBars({ + researchTask, + implementationTask, + reportStatus, + isLoading, + onRunInCloud, +}: { + researchTask: Task | null; + implementationTask: Task | null; + reportStatus: SignalReportStatus; + isLoading: boolean; + onRunInCloud?: () => void; +}): Bar[] { + const isPendingInput = reportStatus === "pending_input"; + const runActionLabel = onRunInCloud + ? isPendingInput + ? "Provide input for PR" + : "Create PR" + : undefined; + + const bars: Bar[] = []; + for (const relationship of BAR_ORDER) { + if (relationship === "research") { + if (researchTask) { + bars.push({ + relationship: "research", + task: researchTask, + summary: getTaskStatusSummary(researchTask), + }); + } else { + const { summary, tooltip } = getResearchPendingSummary( + reportStatus, + isLoading, + ); + bars.push({ relationship: "research", task: null, summary, tooltip }); + } + } else if (implementationTask) { + bars.push({ + relationship: "implementation", + task: implementationTask, + summary: getTaskStatusSummary(implementationTask), + prUrl: getTaskPrUrl(implementationTask), + // Once the impl task exists, only surface the run button when the + // agent is awaiting user input (the user has to provide context to + // unstick the PR). Otherwise the bar is purely informational. + runActionLabel: isPendingInput ? runActionLabel : undefined, + }); + } else if (reportStatus === "ready" || isPendingInput) { + bars.push({ + relationship: "implementation", + task: null, + summary: { + label: "Not started", + color: "var(--gray-9)", + icon: , + }, + runActionLabel, + }); + } + } + return bars; +} + +interface BarRowProps { + bar: Bar; + index: number; + isExpanded: boolean; + onToggle: () => void; + onRunAction?: () => void; +} + +function BarRow({ + bar, + index, + isExpanded, + onToggle, + onRunAction, +}: BarRowProps) { + const { relationship, task, summary, tooltip, runActionLabel, prUrl } = bar; + const isInteractive = !!task; + const showRunAction = !!runActionLabel; + const hideStatusLabel = showRunAction && !task; + + const className = [ + "flex w-full items-center gap-2 bg-transparent px-2 @md:px-3 @lg:px-4 @xl:px-5 @2xl:px-6 @3xl:px-8 @4xl:px-10 @5xl:px-12 py-2 text-left transition-colors", + index > 0 ? "border-gray-5 border-t" : "", + isInteractive + ? "cursor-pointer hover:bg-gray-2" + : showRunAction + ? "cursor-default" + : "cursor-default opacity-70", + isExpanded && isInteractive ? "bg-gray-2" : "", + ] + .filter(Boolean) + .join(" "); + + const inner = ( + <> + {summary.icon} + + {RELATIONSHIP_LABELS[relationship]} + + {hideStatusLabel ? ( + + ) : ( + + {prUrl + ? summary.label + : relationship === "implementation" && + (task?.latest_run?.status === "queued" || + task?.latest_run?.status === "in_progress") + ? "Working on a PR…" + : summary.label} + + )} + {prUrl && } + {showRunAction && ( + + )} + {isInteractive && ( + + + + )} + + ); + + // Render every interactive bar as a
rather than a + // - )} - {isInteractive && ( - - - - )} - - ); - - const row = isInteractive ? ( - showRunAction ? ( - // biome-ignore lint/a11y/useSemanticElements: a - ) - ) : ( -
- {rowInner} -
- ); - - return tooltip ? ( - - {row} - - ) : ( - row - ); - })} + {bars.map((bar, index) => ( + toggle(bar.relationship)} + onRunAction={onRunInCloud} + /> + ))}
- {/* Expanded logs body — only rendered for the selected task. */}
{expandedBar?.task && ( From 96167ec302eb756aaf6eb71deef733cabae0e171 Mon Sep 17 00:00:00 2001 From: Oliver Browne Date: Tue, 28 Apr 2026 17:47:03 +0000 Subject: [PATCH 2/4] fix: drop pointer-events trickery so clicking the bar collapses it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The card had `pointer-events-none` with `pointer-events-auto` re-enabled on the bars container. That click-through dance was unnecessary (the card has no empty area — bars + TaskLogsPanel fill it) and was what was preventing clicks from reliably reaching the bar in the expanded state. Removed it on the card, the bars container, and the expanded body, leaving everything inside the card with default pointer-events. Adds a focused unit test that asserts: same-bar click collapses, the research bar expands when a completed implementation task is also present, and clicking a different bar while one is expanded switches. Generated-By: PostHog Code Task-Id: 6b2b677f-e9c9-4028-b37e-d92158107040 --- .../components/detail/ReportTaskLogs.test.tsx | 137 ++++++++++++++++++ .../components/detail/ReportTaskLogs.tsx | 13 +- 2 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 apps/code/src/renderer/features/inbox/components/detail/ReportTaskLogs.test.tsx diff --git a/apps/code/src/renderer/features/inbox/components/detail/ReportTaskLogs.test.tsx b/apps/code/src/renderer/features/inbox/components/detail/ReportTaskLogs.test.tsx new file mode 100644 index 000000000..3166244b8 --- /dev/null +++ b/apps/code/src/renderer/features/inbox/components/detail/ReportTaskLogs.test.tsx @@ -0,0 +1,137 @@ +import { Theme } from "@radix-ui/themes"; +import type { SignalReportStatus, Task } from "@shared/types"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import type { ReactElement } from "react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const reportTasksRef: { current: { task: Task; relationship: string }[] } = { + current: [], +}; + +vi.mock("@hooks/useAuthenticatedQuery", () => ({ + useAuthenticatedQuery: () => ({ + data: reportTasksRef.current, + isLoading: false, + }), +})); + +vi.mock("@features/task-detail/components/TaskLogsPanel", () => ({ + TaskLogsPanel: ({ taskId }: { taskId: string }) => ( +
{`logs for ${taskId}`}
+ ), +})); + +vi.mock("@features/inbox/components/utils/ReportImplementationPrLink", () => ({ + ReportImplementationPrLink: ({ prUrl }: { prUrl: string }) => ( + e.stopPropagation()}> + pr-link + + ), +})); + +import { ReportTaskLogs } from "./ReportTaskLogs"; + +function makeTask(overrides: Partial = {}): Task { + return { + id: overrides.id ?? "task-1", + latest_run: overrides.latest_run ?? { + status: "completed", + stage: null, + output: null, + }, + ...overrides, + } as Task; +} + +function renderWithTheme(ui: ReactElement) { + return render({ui}); +} + +describe("ReportTaskLogs", () => { + beforeEach(() => { + reportTasksRef.current = []; + }); + + it("clicking the same bar that was clicked to expand collapses it", async () => { + const user = userEvent.setup(); + reportTasksRef.current = [ + { + task: makeTask({ id: "research-1" }), + relationship: "research", + }, + { + task: makeTask({ id: "impl-1" }), + relationship: "implementation", + }, + ]; + + renderWithTheme( + , + ); + + const researchBar = screen.getByRole("button", { name: /Research task/i }); + + await user.click(researchBar); + expect(screen.getByTestId("task-logs-panel")).toHaveTextContent( + "logs for research-1", + ); + + await user.click(researchBar); + expect(screen.queryByTestId("task-logs-panel")).not.toBeInTheDocument(); + }); + + it("research bar expands when implementation task is also present and completed", async () => { + const user = userEvent.setup(); + reportTasksRef.current = [ + { task: makeTask({ id: "research-1" }), relationship: "research" }, + { + task: makeTask({ id: "impl-1" }), + relationship: "implementation", + }, + ]; + + renderWithTheme( + , + ); + + await user.click(screen.getByRole("button", { name: /Research task/i })); + + expect(screen.getByTestId("task-logs-panel")).toHaveTextContent( + "logs for research-1", + ); + }); + + it("clicking a different bar while one is expanded switches to the new bar", async () => { + const user = userEvent.setup(); + reportTasksRef.current = [ + { task: makeTask({ id: "research-1" }), relationship: "research" }, + { task: makeTask({ id: "impl-1" }), relationship: "implementation" }, + ]; + + renderWithTheme( + , + ); + + await user.click(screen.getByRole("button", { name: /Research task/i })); + expect(screen.getByTestId("task-logs-panel")).toHaveTextContent( + "logs for research-1", + ); + + await user.click( + screen.getByRole("button", { name: /Implementation task/i }), + ); + expect(screen.getByTestId("task-logs-panel")).toHaveTextContent( + "logs for impl-1", + ); + }); +}); diff --git a/apps/code/src/renderer/features/inbox/components/detail/ReportTaskLogs.tsx b/apps/code/src/renderer/features/inbox/components/detail/ReportTaskLogs.tsx index e1267ffcd..beb3cdd7a 100644 --- a/apps/code/src/renderer/features/inbox/components/detail/ReportTaskLogs.tsx +++ b/apps/code/src/renderer/features/inbox/components/detail/ReportTaskLogs.tsx @@ -426,16 +426,18 @@ export function ReportTaskLogs({ /> {/* Sliding card — animates `top` to avoid a Chromium layout - bug with `transform` on absolute elements in flex+scroll. */} + bug with `transform` on absolute elements in flex+scroll. + The card itself captures clicks (so they reach the bars and the + TaskLogsPanel below); the scrim handles click-outside-to-close. */}
-
+
{bars.map((bar, index) => ( -
+
{expandedBar?.task && ( Date: Tue, 28 Apr 2026 17:59:39 +0000 Subject: [PATCH 3/4] fix: use a real + ); + } else if (isInteractive) { + row = ( + // biome-ignore lint/a11y/useSemanticElements: needs to nest the run-action