diff --git a/bun.lock b/bun.lock index 0509cc8701..7f6bb4006a 100644 --- a/bun.lock +++ b/bun.lock @@ -1197,6 +1197,7 @@ "@shikijs/langs": "^4.0.2", "@shikijs/themes": "^4.0.2", "@tanstack/react-router": "catalog:", + "@tanstack/react-virtual": "^3.14.3", "@tanstack/virtual-file-routes": "^1.162.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -2888,6 +2889,8 @@ "@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="], + "@tanstack/react-virtual": ["@tanstack/react-virtual@3.14.3", "", { "dependencies": { "@tanstack/virtual-core": "3.17.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-k/cnHPVaOfn46hSbiY6n4Dzf4QjCGWSF40zR5QIIYUqPAjpA6TN7InfYmcMiDVQGP2iUn9xsRbAl8u1v3UmeVQ=="], + "@tanstack/router-cli": ["@tanstack/router-cli@1.167.17", "", { "dependencies": { "@tanstack/router-generator": "1.167.17", "chokidar": "^5.0.0", "yargs": "^17.7.2" }, "bin": { "tsr": "bin/tsr.cjs" } }, "sha512-kws9PNdspkHbeZG6aLDjjpgH/hg7Q565BdSW9hHrjKmXPq/at7OKxQnA76aw9dJiJqy9OmST2RdOKlMEog43+g=="], "@tanstack/router-core": ["@tanstack/router-core@1.168.15", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-Wr0424NDtD8fT/uALobMZ9DdcfsTyXtW5IPR++7zvW8/7RaIOeaqXpVDId8ywaGtqPWLWOfaUg2zUtYtukoXYA=="], @@ -2910,6 +2913,8 @@ "@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="], + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.1", "", {}, "sha512-VZyW2Uiml5tmBZwPGrSD3Sz73OxzljQMCmzYHsUTPEuTsERf5xwa+uWb01xEzkz3ZSYTjj8NEb/mKHvgKxyZdA=="], + "@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.162.0", "", {}, "sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA=="], "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], diff --git a/packages/plugins/execution-history/src/react/RunsPage.tsx b/packages/plugins/execution-history/src/react/RunsPage.tsx index 6c4173e0f7..be95ed4d1e 100644 --- a/packages/plugins/execution-history/src/react/RunsPage.tsx +++ b/packages/plugins/execution-history/src/react/RunsPage.tsx @@ -10,7 +10,7 @@ import { LiveButton } from "./live-button"; import { LiveDivider } from "./live-row"; import { RefreshButton } from "./refresh-button"; import { RunListRow } from "./run-row"; -import { RunsShell, RUNS_TABLE_COLSPAN } from "./shell"; +import { RunsShell, RUNS_TABLE_COLSPAN, type RunsShellRow } from "./shell"; import { RunsTimelineChart } from "./timeline-chart"; import { emptyRunsFilters, @@ -146,37 +146,40 @@ export function RunsPage() { [isDescending, view.rows, view.liveCutoffId], ); - const rowItems: React.ReactNode[] = []; + // Run rows + the interleaved live divider, as keyed entries the shell windows. + // Elements for off-screen runs are cheap descriptors — only the rows the + // windowing mounts actually run their per-row hooks / portals. + const rows: RunsShellRow[] = []; view.rows.forEach((run, index) => { if (index === cutoffIndex && cutoffIndex > 0) { - rowItems.push(); + rows.push({ key: "live-divider", node: }); } - rowItems.push( - 0 && index >= cutoffIndex} - columns={columns} - onSelect={() => setSelected(run.executionId)} - />, - ); + rows.push({ + key: run.executionId, + node: ( + 0 && index >= cutoffIndex} + columns={columns} + onSelect={() => setSelected(run.executionId)} + /> + ), + }); }); - if (view.isLoadMoreError) { - rowItems.push( - - - - Failed to load more - - - - , - ); - } + const loadMoreError = view.isLoadMoreError ? ( + + + + Failed to load more + + + + + ) : undefined; const toolbar = (
@@ -226,7 +229,7 @@ export function RunsPage() { /> ) : undefined; - const body = view.isLoading ? ( + const statusRow = view.isLoading ? ( Loading runs… @@ -238,9 +241,7 @@ export function RunsPage() { Unable to load runs. - ) : ( - rowItems - ); + ) : undefined; // Prev/next drawer navigation mirrors the ArrowUp/ArrowDown keyboard handler: // index-1 is the row above (newer in descending sort), index+1 the row below. @@ -286,9 +287,11 @@ export function RunsPage() {
} railCollapsed={railCollapsed} - > - {body} - + rows={rows} + statusRow={statusRow} + loadMoreError={loadMoreError} + scrollRestoreId="execution-history-runs" + /> - {lines.map((line, index) => ( + `${index}-${line.slice(0, 24)}`} + estimateSize={() => 28} + overscan={16} + className="min-h-0 flex-1 px-5 py-4" + aria-label="Run logs" + renderItem={(line) => (
           {line}
         
- ))} - + )} + /> ); } @@ -117,14 +123,15 @@ function ToolCallItem(props: { readonly call: ToolCallRow; readonly windowStart: number; readonly windowEnd: number; + readonly expanded: boolean; + readonly onToggle: () => void; }) { - const [expanded, setExpanded] = useState(false); return (
- {expanded && ( + {props.expanded && (
{props.call.status === "failed" && props.call.errorText ? ( @@ -157,9 +164,20 @@ function ToolCallItem(props: { } function ToolCallsTab(props: { readonly run: RunRow; readonly toolCalls: readonly ToolCallRow[] }) { + // Expanded state is lifted out of the row so it survives windowing (a row + // scrolled out and back keeps its expansion). + const [expanded, setExpanded] = useState>(() => new Set()); const windowStart = props.run.startedAt; + const toggle = useCallback((id: string) => { + setExpanded((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); + else next.add(id); + return next; + }); + }, []); if (props.toolCalls.length === 0) { - return

No tool calls recorded.

; + return

No tool calls recorded.

; } // Derive the window end from the tool-call data as well as `run.completedAt` // so a fresher tool call can't outrun a stale run end and clamp its bar to a @@ -175,16 +193,25 @@ function ToolCallsTab(props: { readonly run: RunRow; readonly toolCalls: readonl ); const windowEnd = Math.max(props.run.completedAt ?? Date.now(), callsEnd); return ( -
- {props.toolCalls.map((call) => ( - - ))} -
+ call.toolCallId} + estimateSize={() => 52} + overscan={12} + className="min-h-0 flex-1 px-5 py-4" + aria-label="Tool calls" + renderItem={(call) => ( +
+ toggle(call.toolCallId)} + /> +
+ )} + /> ); } @@ -313,85 +340,87 @@ function DetailContent(props: { Logs - - -
- - - - {statusLabel(run.status)} - - - - {run.durationMs != null ? formatDuration(run.durationMs) : "—"} - - - {run.actorId !== null ? ( - - {actorLabel} - {run.actorKind !== null ? ( - · {run.actorKind} - ) : null} + + +
+
+ + + + {statusLabel(run.status)} - ) : ( - — - )} - - - - - - {run.completedAt != null ? ( - - ) : ( - formatDateTime(run.completedAt) - )} - -
- -
- - - via {trigger.label} - - · tools {run.toolCallCount} -
- - - - {props.errorText && ( -
-

Error

-
-                  {props.errorText}
-                
+ + + {run.durationMs != null ? formatDuration(run.durationMs) : "—"} + + + {run.actorId !== null ? ( + + {actorLabel} + {run.actorKind !== null ? ( + · {run.actorKind} + ) : null} + + ) : ( + — + )} + + + + + + {run.completedAt != null ? ( + + ) : ( + formatDateTime(run.completedAt) + )} +
- )} - {props.interactions.length > 0 && ( -
-

- {interactionsHeading} -

- {props.interactions.map((interaction) => ( - - ))} +
+ + + via {trigger.label} + + · tools {run.toolCallCount}
- )} - - - - - - - - + + + + {props.errorText && ( +
+

Error

+
+                    {props.errorText}
+                  
+
+ )} + + {props.interactions.length > 0 && ( +
+

+ {interactionsHeading} +

+ {props.interactions.map((interaction) => ( + + ))} +
+ )} +
+ + + + + + + +
); diff --git a/packages/plugins/execution-history/src/react/shell.tsx b/packages/plugins/execution-history/src/react/shell.tsx index 31300e7f47..abdf1ffe9b 100644 --- a/packages/plugins/execution-history/src/react/shell.tsx +++ b/packages/plugins/execution-history/src/react/shell.tsx @@ -1,18 +1,21 @@ import * as React from "react"; import { cn } from "@executor-js/react/lib/utils"; +import { useWindowedRows } from "@executor-js/react/components/virtual-list"; // --------------------------------------------------------------------------- -// RunsShell — two-column layout (filter rail + main pane) with infinite scroll. +// RunsShell — two-column layout (filter rail + main pane) with a windowed, +// infinite-scrolling table. // // The left