From 9deb34558c8fb3a0b511f0a3d0fa42952ddd1369 Mon Sep 17 00:00:00 2001 From: Michael Matloka Date: Mon, 4 May 2026 20:20:59 +0200 Subject: [PATCH] feat(inbox): Session video via exported_asset_id; logs layout; cloud bootstrap - Signal session_problem replay loads export by ID only (no list fallback) - Per-relationship TaskLogs panels in inbox report detail - Cloud workspace bootstrap in TaskLogsPanel for cloud runs - useSessionViewState isCloud aligns with workspace row + latest_run env --- apps/code/src/renderer/api/posthogClient.ts | 22 ------ .../components/detail/ReportTaskLogs.tsx | 62 ++++++++-------- .../inbox/components/detail/SignalCard.tsx | 71 +++++++++++++------ .../sessions/hooks/useSessionViewState.ts | 6 +- .../task-detail/components/TaskLogsPanel.tsx | 54 ++++++++++++++ 5 files changed, 136 insertions(+), 79 deletions(-) diff --git a/apps/code/src/renderer/api/posthogClient.ts b/apps/code/src/renderer/api/posthogClient.ts index 8f64e5037..47a6db121 100644 --- a/apps/code/src/renderer/api/posthogClient.ts +++ b/apps/code/src/renderer/api/posthogClient.ts @@ -2721,28 +2721,6 @@ export class PostHogAPIClient { } } - /** Find an exported asset by session recording ID. */ - async findExportBySessionRecordingId( - projectId: number, - sessionRecordingId: string, - ): Promise { - const urlPath = `/api/projects/${projectId}/exports/`; - const url = new URL(`${this.api.baseUrl}${urlPath}`); - url.searchParams.set("session_recording_id", sessionRecordingId); - url.searchParams.set("export_format", "video/mp4"); - const response = await this.api.fetcher.fetch({ - method: "get", - url, - path: urlPath, - }); - if (!response.ok) return null; - const data = (await response.json()) as { - results?: Array<{ id: number; has_content: boolean }>; - }; - const match = data.results?.find((e) => e.has_content); - return match?.id ?? null; - } - /** Get the presigned content URL for an exported asset (e.g. rasterized recording). */ async getExportContentUrl( projectId: number, 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 a31617505..70140d840 100644 --- a/apps/code/src/renderer/features/inbox/components/detail/ReportTaskLogs.tsx +++ b/apps/code/src/renderer/features/inbox/components/detail/ReportTaskLogs.tsx @@ -11,7 +11,7 @@ import { } from "@phosphor-icons/react"; import { Button, Spinner, Text, Tooltip } from "@radix-ui/themes"; import type { SignalReportStatus, SignalReportTask, Task } from "@shared/types"; -import { useState } from "react"; +import { Fragment, useState } from "react"; type Relationship = SignalReportTask["relationship"]; @@ -291,6 +291,7 @@ export function ReportTaskLogs({ const expandedBar = expanded ? (bars.find((b) => b.relationship === expanded && b.task) ?? null) : null; + const totalBarsHeight = BAR_HEIGHT * bars.length; return ( @@ -323,10 +324,11 @@ export function ReportTaskLogs({ top: expandedBar ? "15%" : `calc(100% - ${totalBarsHeight}px)`, transition: "top 0.25s cubic-bezier(0.32, 0.72, 0, 1)", }} - className="pointer-events-none absolute right-0 bottom-0 left-0 flex flex-col border-t border-t-(--gray-6) bg-(--color-background)" + className="pointer-events-none absolute right-0 bottom-0 left-0 flex min-h-0 flex-col border-t border-t-(--gray-6) bg-(--color-background)" > - {/* Stacked header bars — one per task relationship. */} -
+ {/* Pipeline order: fixed bar positions; each task type gets its TaskLogsPanel + directly beneath its own header (not a shared pane under both bars). */} +
{bars.map((bar, index) => { const { relationship, @@ -346,7 +348,7 @@ export function ReportTaskLogs({ const hideStatusLabel = showRunAction && !task; const rowClassName = [ - "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", + "flex w-full shrink-0 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" @@ -436,7 +438,6 @@ export function ReportTaskLogs({ showRunAction ? ( // biome-ignore lint/a11y/useSemanticElements: a
diff --git a/apps/code/src/renderer/features/inbox/components/detail/SignalCard.tsx b/apps/code/src/renderer/features/inbox/components/detail/SignalCard.tsx index ea254b4fa..35a2b397f 100644 --- a/apps/code/src/renderer/features/inbox/components/detail/SignalCard.tsx +++ b/apps/code/src/renderer/features/inbox/components/detail/SignalCard.tsx @@ -10,8 +10,9 @@ import { QuestionIcon, TagIcon, } from "@phosphor-icons/react"; -import { Badge, Box, Flex, Text } from "@radix-ui/themes"; +import { Badge, Box, Button, Flex, Text } from "@radix-ui/themes"; import type { Signal, SignalFindingContent } from "@shared/types"; +import { useQueryClient } from "@tanstack/react-query"; import { useRef, useState } from "react"; const COLLAPSE_THRESHOLD = 300; @@ -542,11 +543,8 @@ function SessionProblemSignalCard({ )} - {extra.session_id && ( - + {extra.exported_asset_id != null && ( + )} state.projectId); + const queryClient = useQueryClient(); const videoRef = useRef(null); const videoQuery = useAuthenticatedQuery( - ["export-video", projectId, exportedAssetId, sessionId], + ["export-video", projectId, exportedAssetId], async (client) => { if (!projectId) return null; - let assetId: number | null = exportedAssetId ?? null; - // If no asset ID in the signal, look up the export by session_id - if (assetId == null) { - assetId = await client.findExportBySessionRecordingId( - projectId, - sessionId, - ); - if (assetId == null) return null; - } - return client.getExportContentUrl(projectId, assetId); + return client.getExportContentUrl(projectId, exportedAssetId); }, { enabled: !!projectId, staleTime: Infinity }, ); - if (videoQuery.isError || videoQuery.data === null) return null; - if (videoQuery.isLoading || videoQuery.data === undefined) { + const retryExportVideo = () => { + void queryClient.invalidateQueries({ + queryKey: ["export-video", projectId, exportedAssetId], + }); + }; + + if (videoQuery.isError) { + return ( + + Could not load recording. + + + + + ); + } + + if (videoQuery.data === null) { + return ( + + No playable export for this session yet. + + ); + } + + if (videoQuery.isPending || videoQuery.data === undefined) { return ( { + if (!isWorkspaceLoaded) return; + if (persistedWorkspace) return; + if (task.latest_run?.environment !== "cloud") return; + if (isSuspended) return; + if (isProvisioning) return; + + let cancelled = false; + void (async () => { + try { + await workspaceApi.create({ + taskId, + mainRepoPath: "", + folderId: "", + folderPath: "", + mode: "cloud", + }); + } catch (error) { + bootstrapCloudLog.warn("Cloud workspace bootstrap failed", { + taskId, + error, + }); + } + if (!cancelled) { + void queryClient.invalidateQueries( + trpcReact.workspace.getAll.pathFilter(), + ); + } + })(); + + return () => { + cancelled = true; + }; + }, [ + isProvisioning, + isSuspended, + isWorkspaceLoaded, + persistedWorkspace, + queryClient, + task.latest_run?.environment, + taskId, + trpcReact, + ]); + const handleRestoreWorktree = useCallback(async () => { await restoreTask(taskId); }, [taskId, restoreTask]);