Live Runs
@@ -147,6 +147,7 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
density="compact"
limit={8}
streaming={isActive}
+ collapseStdout
emptyMessage={hasOutputForRun(run.id) ? "Waiting for transcript parsing..." : "Waiting for run output..."}
/>
diff --git a/ui/src/components/transcript/RunTranscriptView.tsx b/ui/src/components/transcript/RunTranscriptView.tsx
index 298bf6c7..8aec4911 100644
--- a/ui/src/components/transcript/RunTranscriptView.tsx
+++ b/ui/src/components/transcript/RunTranscriptView.tsx
@@ -21,6 +21,7 @@ interface RunTranscriptViewProps {
density?: TranscriptDensity;
limit?: number;
streaming?: boolean;
+ collapseStdout?: boolean;
emptyMessage?: string;
className?: string;
}
@@ -70,6 +71,11 @@ type TranscriptBlock =
status: "running" | "completed" | "error";
}>;
}
+ | {
+ type: "stdout";
+ ts: string;
+ text: string;
+ }
| {
type: "event";
ts: string;
@@ -480,13 +486,16 @@ function normalizeTranscript(entries: TranscriptEntry[], streaming: boolean): Tr
continue;
}
- blocks.push({
- type: "event",
- ts: entry.ts,
- label: "stdout",
- tone: "neutral",
- text: entry.text,
- });
+ if (previous?.type === "stdout") {
+ previous.text += previous.text.endsWith("\n") || entry.text.startsWith("\n") ? entry.text : `\n${entry.text}`;
+ previous.ts = entry.ts;
+ } else {
+ blocks.push({
+ type: "stdout",
+ ts: entry.ts,
+ text: entry.text,
+ });
+ }
}
return groupCommandBlocks(blocks);
@@ -859,6 +868,44 @@ function TranscriptEventRow({
);
}
+function TranscriptStdoutRow({
+ block,
+ density,
+ collapseByDefault,
+}: {
+ block: Extract
;
+ density: TranscriptDensity;
+ collapseByDefault: boolean;
+}) {
+ const [open, setOpen] = useState(!collapseByDefault);
+
+ return (
+
+
+
+ stdout
+
+
+
+ {open && (
+
+ {block.text}
+
+ )}
+
+ );
+}
+
function RawTranscriptView({
entries,
density,
@@ -903,6 +950,7 @@ export function RunTranscriptView({
density = "comfortable",
limit,
streaming = false,
+ collapseStdout = false,
emptyMessage = "No transcript yet.",
className,
}: RunTranscriptViewProps) {
@@ -937,6 +985,9 @@ export function RunTranscriptView({
{block.type === "thinking" && }
{block.type === "tool" && }
{block.type === "command_group" && }
+ {block.type === "stdout" && (
+
+ )}
{block.type === "activity" && }
{block.type === "event" && }
diff --git a/ui/src/components/transcript/useLiveRunTranscripts.ts b/ui/src/components/transcript/useLiveRunTranscripts.ts
index acc5b082..993663d8 100644
--- a/ui/src/components/transcript/useLiveRunTranscripts.ts
+++ b/ui/src/components/transcript/useLiveRunTranscripts.ts
@@ -46,7 +46,7 @@ function parsePersistedLogContent(
ts,
stream,
chunk,
- dedupeKey: `persisted:${runId}:${ts}:${stream}:${chunk}`,
+ dedupeKey: `log:${runId}:${ts}:${stream}:${chunk}`,
});
} catch {
// Ignore malformed log rows.
@@ -202,6 +202,7 @@ export function useLiveRunTranscripts({
if (event.type === "heartbeat.run.log") {
const chunk = readString(payload["chunk"]);
if (!chunk) return;
+ const ts = readString(payload["ts"]) ?? event.createdAt;
const stream =
readString(payload["stream"]) === "stderr"
? "stderr"
@@ -209,10 +210,10 @@ export function useLiveRunTranscripts({
? "system"
: "stdout";
appendChunks(runId, [{
- ts: event.createdAt,
+ ts,
stream,
chunk,
- dedupeKey: `socket:log:${runId}:${event.createdAt}:${stream}:${chunk}`,
+ dedupeKey: `log:${runId}:${ts}:${stream}:${chunk}`,
}]);
return;
}