mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 08:22:20 +02:00
[codex] Add runtime lifecycle recovery and live issue visibility (#4419)
This commit is contained in:
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;
|
||||
|
||||
|
||||
Reference in new issue
Block a user