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

This commit is contained in:
Dotta
2026-04-24 15:50:32 -05:00
committed by GitHub
parent 9a8d219949
commit 5a0c1979cf
121 changed files with 9625 additions and 2044 deletions
@@ -110,4 +110,23 @@ describe("RunTranscriptView", () => {
expect(html).toMatch(/<li[^>]*>posted issue update<\/li>/);
expect(html).not.toContain("result");
});
it("windows large raw transcripts instead of rendering every entry at once", () => {
const entries: TranscriptEntry[] = Array.from({ length: 500 }, (_, index) => ({
kind: "stdout",
ts: `2026-03-12T00:${String(index % 60).padStart(2, "0")}:00.000Z`,
text: `line-${index}`,
}));
const html = renderToStaticMarkup(
<ThemeProvider>
<RunTranscriptView mode="raw" entries={entries} />
</ThemeProvider>,
);
expect(html).toContain("line-0");
expect(html).toContain("line-179");
expect(html).not.toContain("line-250");
expect(html).not.toContain("line-499");
});
});
@@ -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;
@@ -258,6 +258,34 @@ describe("useLiveRunTranscripts", () => {
container.remove();
});
it("starts persisted-log hydration from the newest bytes when the visible window is truncated", async () => {
function Harness() {
useLiveRunTranscripts({
companyId: "company-1",
runs: [{ id: "run-1", status: "running", adapterType: "codex_local", lastOutputBytes: 100_000 }],
enableRealtimeUpdates: false,
logReadLimitBytes: 64_000,
});
return null;
}
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
await act(async () => {
root.render(<Harness />);
await Promise.resolve();
});
expect(logMock).toHaveBeenCalledWith("run-1", 36_000, 64_000);
act(() => {
root.unmount();
});
container.remove();
});
it("rebuilds only the transcript for the run that receives live output", async () => {
function Harness() {
useLiveRunTranscripts({
@@ -16,6 +16,8 @@ export interface RunTranscriptSource {
status: string;
adapterType: string;
hasStoredOutput?: boolean;
logBytes?: number | null;
lastOutputBytes?: number | null;
}
interface UseLiveRunTranscriptsOptions {
@@ -35,6 +37,19 @@ function isTerminalStatus(status: string): boolean {
return status === "failed" || status === "timed_out" || status === "cancelled" || status === "succeeded";
}
function runKnownLogBytes(run: RunTranscriptSource): number | null {
const bytes = run.status === "queued"
? run.logBytes
: run.lastOutputBytes ?? run.logBytes;
return typeof bytes === "number" && Number.isFinite(bytes) && bytes > 0 ? bytes : null;
}
export function resolveInitialLogOffset(run: RunTranscriptSource, limitBytes: number): number {
const knownBytes = runKnownLogBytes(run);
if (knownBytes === null) return 0;
return Math.max(0, knownBytes - Math.max(0, limitBytes));
}
function parsePersistedLogContent(
runId: string,
content: string,
@@ -82,7 +97,11 @@ export function useLiveRunTranscripts({
const runsKey = useMemo(
() =>
runs
.map((run) => `${run.id}:${run.status}:${run.adapterType}:${run.hasStoredOutput === true ? "1" : "0"}`)
.map((run) => {
const logBytes = typeof run.logBytes === "number" ? run.logBytes : "";
const lastOutputBytes = typeof run.lastOutputBytes === "number" ? run.lastOutputBytes : "";
return `${run.id}:${run.status}:${run.adapterType}:${run.hasStoredOutput === true ? "1" : "0"}:${logBytes}:${lastOutputBytes}`;
})
.sort((a, b) => a.localeCompare(b))
.join(","),
[runs],
@@ -197,7 +216,7 @@ export function useLiveRunTranscripts({
if (missingTerminalLogRunIdsRef.current.has(run.id)) {
return;
}
const offset = logOffsetByRunRef.current.get(run.id) ?? 0;
const offset = logOffsetByRunRef.current.get(run.id) ?? resolveInitialLogOffset(run, logReadLimitBytes);
try {
const result = await heartbeatsApi.log(run.id, offset, logReadLimitBytes);
if (cancelled) return;