From abc5991177e6ec4ef79c93093b018f10217f69ba Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sun, 24 May 2026 10:22:35 -0400 Subject: [PATCH] Add Copy/Download actions for loaded stage events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two icon buttons on the stage events toolbar, to the right of the model usage label: copy all loaded events as pretty-printed JSON, or download them as JSONL. Both read from the existing SWR cache, so no new API surface — for in-flight stages they're a snapshot of what the client has fetched so far. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/fabro-web/app/routes/run-stages.tsx | 77 ++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/apps/fabro-web/app/routes/run-stages.tsx b/apps/fabro-web/app/routes/run-stages.tsx index d10756a24..7503f0c3a 100644 --- a/apps/fabro-web/app/routes/run-stages.tsx +++ b/apps/fabro-web/app/routes/run-stages.tsx @@ -1,8 +1,10 @@ import { useEffect, useMemo, useState } from "react"; import { useParams } from "react-router"; import { + ArrowDownTrayIcon, ChevronDownIcon, ChevronRightIcon, + ClipboardDocumentIcon, CpuChipIcon, } from "@heroicons/react/16/solid"; import { CircleStackIcon, ClockIcon } from "@heroicons/react/20/solid"; @@ -1234,6 +1236,66 @@ function EventsTabToggle({ ); } +function EventExportActions({ + events, + runId, + stageId, + className, +}: { + events: EventEnvelope[]; + runId: string; + stageId: string; + className?: string; +}) { + const [copied, setCopied] = useState(false); + const disabled = events.length === 0; + const buttonClass = + "inline-flex size-6 items-center justify-center rounded text-fg-muted transition-colors hover:bg-overlay hover:text-fg-2 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-teal-500 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent disabled:hover:text-fg-muted"; + return ( +
+ + +
+ ); +} + function EventsToolbar({ tab, renderer, @@ -1250,6 +1312,9 @@ function EventsToolbar({ filteredCount, totalCount, providerUsed, + events, + runId, + stageId, }: { tab: EventsTab; renderer: StageRenderer; @@ -1266,6 +1331,9 @@ function EventsToolbar({ filteredCount: number; totalCount: number; providerUsed: StageModelUsage | null; + events: EventEnvelope[]; + runId: string; + stageId: string; }) { // Filters apply to: the agent transcript (filter event kinds) and the Debug // tab (filter event categories). Specialized renderers (human, parallel, @@ -1348,6 +1416,12 @@ function EventsToolbar({ {modelUsageLabel} )} + {tab === "primary" && renderer === "command" && commandTurn && ( )} @@ -1560,6 +1634,9 @@ export default function RunStages() { filteredCount={effectiveTab === "primary" ? filteredTurns.length : filteredDebugEvents.length} totalCount={effectiveTab === "primary" ? turns.length : debugEvents.length} providerUsed={selectedStage.providerUsed} + events={stageEventsQuery.data ?? []} + runId={id ?? ""} + stageId={selectedStageId ?? ""} /> {effectiveTab === "debug" && (