[codex] Add runtime lifecycle recovery and live issue visibility (#4419)

This commit is contained in:
Dotta authored and GitHub committed 2026-04-24 15:50:32 -05:00
1 parent 9a8d219949
commit 5a0c1979cf
121 files changed
+9625 -2044

No files matched your search

@@ -1,4 +1,4 @@
import { useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import type { TranscriptEntry } from "../../adapters";
import { MarkdownBody } from "../MarkdownBody";
import { cn, formatTokens } from "../../lib/utils";
@@ -16,6 +16,11 @@ import {
export type TranscriptMode = "nice" | "raw";
export type TranscriptDensity = "comfortable" | "compact";
const RAW_VIRTUALIZATION_THRESHOLD = 300;
const RAW_OVERSCAN_ROWS = 40;
const RAW_ESTIMATED_ROW_HEIGHT = 36;
const RAW_INITIAL_ROWS = 180;
interface RunTranscriptViewProps {
entries: TranscriptEntry[];
mode?: TranscriptMode;
@@ -1347,6 +1352,34 @@ function TranscriptStdoutRow({
);
}
function findScrollParent(element: HTMLElement): HTMLElement | Window {
let current = element.parentElement;
while (current) {
const style = window.getComputedStyle(current);
if (/(auto|scroll)/.test(style.overflowY) && current.scrollHeight > current.clientHeight) {
return current;
}
current = current.parentElement;
}
return window;
}
function rawEntryContent(entry: TranscriptEntry): string {
if (entry.kind === "tool_call") {
return `${entry.name}\n${formatToolPayload(entry.input)}`;
}
if (entry.kind === "tool_result") {
return formatToolPayload(entry.content);
}
if (entry.kind === "result") {
return `${entry.text}\n${formatTokens(entry.inputTokens)} / ${formatTokens(entry.outputTokens)} / $${entry.costUsd.toFixed(6)}`;
}
if (entry.kind === "init") {
return `model=${entry.model}${entry.sessionId ? ` session=${entry.sessionId}` : ""}`;
}
return entry.text;
}
function RawTranscriptView({
entries,
density,
@@ -1355,11 +1388,63 @@ function RawTranscriptView({
density: TranscriptDensity;
}) {
const compact = density === "compact";
const listRef = useRef<HTMLDivElement | null>(null);
const shouldVirtualize = entries.length > RAW_VIRTUALIZATION_THRESHOLD;
const [range, setRange] = useState(() => ({
start: 0,
end: Math.min(entries.length, shouldVirtualize ? RAW_INITIAL_ROWS : entries.length),
}));
useEffect(() => {
if (!shouldVirtualize) {
setRange({ start: 0, end: entries.length });
return;
}
const list = listRef.current;
if (!list) return;
const scrollParent = findScrollParent(list);
const updateRange = () => {
const scrollElement: HTMLElement | null = scrollParent === window ? null : (scrollParent as HTMLElement);
const scrollerTop = scrollElement ? scrollElement.getBoundingClientRect().top : 0;
const scrollerHeight = scrollElement ? scrollElement.clientHeight : window.innerHeight;
const listTop = list.getBoundingClientRect().top;
const visibleTop = Math.max(0, scrollerTop - listTop);
const visibleBottom = Math.max(visibleTop + scrollerHeight, 0);
const nextStart = Math.max(0, Math.floor(visibleTop / RAW_ESTIMATED_ROW_HEIGHT) - RAW_OVERSCAN_ROWS);
const nextEnd = Math.min(
entries.length,
Math.ceil(visibleBottom / RAW_ESTIMATED_ROW_HEIGHT) + RAW_OVERSCAN_ROWS,
);
setRange((current) => (
current.start === nextStart && current.end === nextEnd
? current
: { start: nextStart, end: nextEnd }
));
};
updateRange();
const frame = window.requestAnimationFrame(updateRange);
scrollParent.addEventListener("scroll", updateRange, { passive: true });
window.addEventListener("resize", updateRange);
return () => {
window.cancelAnimationFrame(frame);
scrollParent.removeEventListener("scroll", updateRange);
window.removeEventListener("resize", updateRange);
};
}, [entries.length, shouldVirtualize]);
const visibleEntries = shouldVirtualize ? entries.slice(range.start, range.end) : entries;
const topSpacer = shouldVirtualize ? range.start * RAW_ESTIMATED_ROW_HEIGHT : 0;
const bottomSpacer = shouldVirtualize ? Math.max(0, entries.length - range.end) * RAW_ESTIMATED_ROW_HEIGHT : 0;
return (
<div className={cn("font-mono", compact ? "space-y-1 text-[11px]" : "space-y-1.5 text-xs")}>
{entries.map((entry, idx) => (
<div ref={listRef} className={cn("font-mono", compact ? "space-y-1 text-[11px]" : "space-y-1.5 text-xs")}>
{topSpacer > 0 && <div aria-hidden="true" style={{ height: topSpacer }} />}
{visibleEntries.map((entry, idx) => (
<div
key={`${entry.kind}-${entry.ts}-${idx}`}
key={`${entry.kind}-${entry.ts}-${range.start + idx}`}
className={cn(
"grid gap-x-3",
"grid-cols-[auto_1fr]",
@@ -1369,18 +1454,11 @@ function RawTranscriptView({
{entry.kind}
</span>
<pre className="min-w-0 whitespace-pre-wrap break-words text-foreground/80">
{entry.kind === "tool_call"
? `${entry.name}\n${formatToolPayload(entry.input)}`
: entry.kind === "tool_result"
? formatToolPayload(entry.content)
: entry.kind === "result"
? `${entry.text}\n${formatTokens(entry.inputTokens)} / ${formatTokens(entry.outputTokens)} / $${entry.costUsd.toFixed(6)}`
: entry.kind === "init"
? `model=${entry.model}${entry.sessionId ? ` session=${entry.sessionId}` : ""}`
: entry.text}
{rawEntryContent(entry)}
</pre>
</div>
))}
{bottomSpacer > 0 && <div aria-hidden="true" style={{ height: bottomSpacer }} />}
</div>
);
}
@@ -1396,7 +1474,10 @@ export function RunTranscriptView({
className,
thinkingClassName,
}: RunTranscriptViewProps) {
const blocks = useMemo(() => normalizeTranscript(entries, streaming), [entries, streaming]);
const blocks = useMemo(
() => (mode === "raw" ? [] : normalizeTranscript(entries, streaming)),
[entries, mode, streaming],
);
const visibleBlocks = limit ? blocks.slice(-limit) : blocks;
const visibleEntries = limit ? entries.slice(-limit) : entries;