History

Disable virtualization for the current in-progress turn

When isWorking, rows belonging to the active turn (from the last user message onward) are rendered in normal document flow instead of through the virtualizer. This prevents height estimation mismatches during streaming that cause layout collisions.

Cursor Agent committed 8 months ago

Commit 87cfb961 parent 21c823e1 file · +306 −272Browse files

Unified · Split

apps/web/src/components/ChatView.tsx+306−272
@@ -2446,11 +2446,31 @@ const MessagesTimeline = memo(function MessagesTimeline({ · 2445 unmodified lines
24462446 return nextRows;
24472447 }, [timelineEntries, completionDividerBeforeEntryId, isWorking]);
24482448
2449+ // Split rows: completed turns are virtualized, current in-progress turn is not.
2450+ // The current turn starts at the last user message when actively working.
2451+ const currentTurnStartIndex = useMemo(() => {
2452+ if (!isWorking) return rows.length;
2453+ for (let i = rows.length - 1; i >= 0; i--) {
2454+ const row = rows[i];
2455+ if (row?.kind === "message" && row.message.role === "user") return i;
2456+ }
2457+ return rows.length;
2458+ }, [rows, isWorking]);
2459+
2460+ const virtualizedRows = useMemo(
2461+ () => rows.slice(0, currentTurnStartIndex),
2462+ [rows, currentTurnStartIndex],
2463+ );
2464+ const currentTurnRows = useMemo(
2465+ () => rows.slice(currentTurnStartIndex),
2466+ [rows, currentTurnStartIndex],
2467+ );
2468+
24492469 const rowVirtualizer = useVirtualizer({
2450− count: rows.length,
2470+ count: virtualizedRows.length,
24512471 getScrollElement: () => scrollContainerRef.current,
24522472 estimateSize: (index: number) => {
2453− const row = rows[index];
2473+ const row = virtualizedRows[index];
24542474 if (!row) return 96;
24552475 if (row.kind === "work") return 112;
24562476 if (row.kind === "working") return 40;
@@ -2472,292 +2492,306 @@ const MessagesTimeline = memo(function MessagesTimeline({ · 15 unmodified lines
24722492 );
24732493 }
24742494
2475− return (
2476− <div
2477− className="relative mx-auto w-full min-w-0 max-w-3xl overflow-x-hidden"
2478− style={{ height: `${rowVirtualizer.getTotalSize()}px` }}
2479− >
2480− {virtualRows.map((virtualRow: VirtualItem) => {
2481− const row = rows[virtualRow.index];
2482− if (!row) return null;
2483−
2484− return (
2485− <div
2486− key={row.id}
2487− data-index={virtualRow.index}
2488− ref={rowVirtualizer.measureElement}
2489− className="absolute left-0 top-0 w-full"
2490− style={{ transform: `translateY(${virtualRow.start}px)` }}
2491− >
2492− <div className="pb-4">
2493− {row.kind === "work" &&
2494− (() => {
2495− const groupId = row.id;
2496− const groupedEntries = row.groupedEntries;
2497− const isExpanded = expandedWorkGroups[groupId] ?? false;
2498− const hasOverflow = groupedEntries.length > MAX_VISIBLE_WORK_LOG_ENTRIES;
2499− const visibleEntries =
2500− hasOverflow && !isExpanded
2501− ? groupedEntries.slice(-MAX_VISIBLE_WORK_LOG_ENTRIES)
2502− : groupedEntries;
2503− const hiddenCount = groupedEntries.length - visibleEntries.length;
2504− const onlyToolEntries = groupedEntries.every((entry) => entry.tone === "tool");
2505− const groupLabel = onlyToolEntries
2506− ? groupedEntries.length === 1
2507− ? "Tool call"
2508− : `Tool calls (${groupedEntries.length})`
2509− : groupedEntries.length === 1
2510− ? "Work event"
2511− : `Work log (${groupedEntries.length})`;
2512−
2495+ const renderRowContent = (row: TimelineRow) => (
2496+ <div className="pb-4">
2497+ {row.kind === "work" &&
2498+ (() => {
2499+ const groupId = row.id;
2500+ const groupedEntries = row.groupedEntries;
2501+ const isExpanded = expandedWorkGroups[groupId] ?? false;
2502+ const hasOverflow = groupedEntries.length > MAX_VISIBLE_WORK_LOG_ENTRIES;
2503+ const visibleEntries =
2504+ hasOverflow && !isExpanded
2505+ ? groupedEntries.slice(-MAX_VISIBLE_WORK_LOG_ENTRIES)
2506+ : groupedEntries;
2507+ const hiddenCount = groupedEntries.length - visibleEntries.length;
2508+ const onlyToolEntries = groupedEntries.every((entry) => entry.tone === "tool");
2509+ const groupLabel = onlyToolEntries
2510+ ? groupedEntries.length === 1
2511+ ? "Tool call"
2512+ : `Tool calls (${groupedEntries.length})`
2513+ : groupedEntries.length === 1
2514+ ? "Work event"
2515+ : `Work log (${groupedEntries.length})`;
2516+
2517+ return (
2518+ <div className="rounded-lg border border-border/80 bg-card/45 px-3 py-2">
2519+ <div className="mb-1.5 flex items-center justify-between gap-3">
2520+ <p className="text-[10px] uppercase tracking-[0.12em] text-muted-foreground/65">
2521+ {groupLabel}
2522+ </p>
2523+ {hasOverflow && (
2524+ <button
2525+ type="button"
2526+ className="text-[10px] uppercase tracking-[0.12em] text-muted-foreground/55 transition-colors duration-150 hover:text-muted-foreground/80"
2527+ onClick={() => onToggleWorkGroup(groupId)}
2528+ >
2529+ {isExpanded ? "Show less" : `Show ${hiddenCount} more`}
2530+ </button>
2531+ )}
2532+ </div>
2533+ <div className="space-y-1">
2534+ {visibleEntries.map((workEntry) => (
2535+ <div
2536+ key={`work-row:${workEntry.id}`}
2537+ className="flex items-start gap-2 py-0.5"
2538+ >
2539+ <span className="mt-[7px] h-1.5 w-1.5 shrink-0 rounded-full bg-muted-foreground/30" />
2540+ <p
2541+ className={`py-[2px] text-[11px] leading-relaxed ${workToneClass(workEntry.tone)}`}
2542+ >
2543+ {workEntry.detail ? (
2544+ <>
2545+ {workEntry.label}
2546+ <span
2547+ className="ml-1.5 inline-block max-w-[70ch] truncate align-bottom font-mono text-[11px] opacity-60"
2548+ title={workEntry.detail}
2549+ >
2550+ {workEntry.detail}
2551+ </span>
2552+ </>
2553+ ) : (
2554+ workEntry.label
2555+ )}
2556+ </p>
2557+ </div>
2558+ ))}
2559+ </div>
2560+ </div>
2561+ );
2562+ })()}
2563+
2564+ {row.kind === "message" &&
2565+ row.message.role === "user" &&
2566+ (() => {
2567+ const userImages = row.message.attachments ?? [];
2568+ const canRevertAgentWork = revertTurnCountByUserMessageId.has(row.message.id);
2569+ return (
2570+ <div className="flex justify-end">
2571+ <div className="group relative max-w-[80%] rounded-2xl rounded-br-sm border border-border bg-secondary px-4 py-3">
2572+ {userImages.length > 0 && (
2573+ <div className="mb-2 grid max-w-[420px] grid-cols-2 gap-2">
2574+ {userImages.map(
2575+ (image: NonNullable<TimelineMessage["attachments"]>[number]) => (
2576+ <div
2577+ key={image.id}
2578+ className="overflow-hidden rounded-lg border border-border/80 bg-background/70"
2579+ >
2580+ {image.previewUrl ? (
2581+ <img
2582+ src={image.previewUrl}
2583+ alt={image.name}
2584+ className="h-full max-h-[220px] w-full cursor-zoom-in object-cover"
2585+ onClick={() =>
2586+ onImageExpand({ src: image.previewUrl!, name: image.name })
2587+ }
2588+ />
2589+ ) : (
2590+ <div className="flex min-h-[72px] items-center justify-center px-2 py-3 text-center text-[11px] text-muted-foreground/70">
2591+ {image.name}
2592+ </div>
2593+ )}
2594+ </div>
2595+ ),
2596+ )}
2597+ </div>
2598+ )}
2599+ {row.message.text && (
2600+ <pre className="whitespace-pre-wrap wrap-break-word font-mono text-sm leading-relaxed text-foreground">
2601+ {row.message.text}
2602+ </pre>
2603+ )}
2604+ <div className="mt-1.5 flex items-center justify-end gap-2">
2605+ <div className="flex items-center gap-1.5 opacity-0 transition-opacity duration-200 focus-within:opacity-100 group-hover:opacity-100">
2606+ {row.message.text && <MessageCopyButton text={row.message.text} />}
2607+ {canRevertAgentWork && (
2608+ <Button
2609+ type="button"
2610+ size="xs"
2611+ variant="outline"
2612+ disabled={isRevertingCheckpoint || isWorking}
2613+ onClick={() => onRevertUserMessage(row.message.id)}
2614+ title="Revert to this message"
2615+ >
2616+ <Undo2Icon className="size-3" />
2617+ </Button>
2618+ )}
2619+ </div>
2620+ <p className="text-right text-[10px] text-muted-foreground/30">
2621+ {formatTimestamp(row.message.createdAt)}
2622+ </p>
2623+ </div>
2624+ </div>
2625+ </div>
2626+ );
2627+ })()}
2628+
2629+ {row.kind === "message" &&
2630+ row.message.role === "assistant" &&
2631+ (() => {
2632+ const messageText =
2633+ row.message.text || (row.message.streaming ? "" : "(empty response)");
2634+ return (
2635+ <>
2636+ {row.showCompletionDivider && (
2637+ <div className="my-3 flex items-center gap-3">
2638+ <span className="h-px flex-1 bg-border" />
2639+ <span className="rounded-full border border-border bg-background px-2.5 py-1 text-[10px] uppercase tracking-[0.14em] text-muted-foreground/80">
2640+ {completionSummary ? `Response • ${completionSummary}` : "Response"}
2641+ </span>
2642+ <span className="h-px flex-1 bg-border" />
2643+ </div>
2644+ )}
2645+ <div className="min-w-0 px-1 py-0.5">
2646+ <ChatMarkdown text={messageText} cwd={markdownCwd} />
2647+ {(() => {
2648+ const turnSummary = turnDiffSummaryByAssistantMessageId.get(
2649+ row.message.id,
2650+ );
2651+ if (!turnSummary) return null;
2652+ const checkpointFiles = turnSummary.files;
2653+ if (checkpointFiles.length === 0) return null;
2654+ const summaryStat = checkpointFiles.reduce(
2655+ (acc, file) => {
2656+ if (
2657+ typeof file.additions !== "number" ||
2658+ typeof file.deletions !== "number"
2659+ ) {
2660+ return acc;
2661+ }
2662+ return {
2663+ additions: acc.additions + file.additions,
2664+ deletions: acc.deletions + file.deletions,
2665+ };
2666+ },
2667+ { additions: 0, deletions: 0 },
2668+ );
2669+ const changedFileCountLabel = String(checkpointFiles.length);
25132670 return (
2514− <div className="rounded-lg border border-border/80 bg-card/45 px-3 py-2">
2515− <div className="mb-1.5 flex items-center justify-between gap-3">
2671+ <div className="mt-2 rounded-lg border border-border/80 bg-card/45 p-2.5">
2672+ <div className="mb-1.5 flex items-center justify-between gap-2">
25162673 <p className="text-[10px] uppercase tracking-[0.12em] text-muted-foreground/65">
2517− {groupLabel}
2674+ <span>Changed files ({changedFileCountLabel})</span>
2675+ {(summaryStat.additions > 0 || summaryStat.deletions > 0) && (
2676+ <>
2677+ <span className="mx-1">•</span>
2678+ <span className="text-success">+{summaryStat.additions}</span>
2679+ <span className="mx-0.5 text-muted-foreground/70">/</span>
2680+ <span className="text-destructive">
2681+ -{summaryStat.deletions}
2682+ </span>
2683+ </>
2684+ )}
25182685 </p>
2519− {hasOverflow && (
2686+ <Button
2687+ type="button"
2688+ size="xs"
2689+ variant="outline"
2690+ onClick={() =>
2691+ onOpenTurnDiff(turnSummary.turnId, checkpointFiles[0]?.path)
2692+ }
2693+ >
2694+ View diff
2695+ </Button>
2696+ </div>
2697+ <div className="flex flex-wrap gap-1.5">
2698+ {checkpointFiles.map((file) => (
25202699 <button
2700+ key={`${turnSummary.turnId}:${file.path}`}
25212701 type="button"
2522− className="text-[10px] uppercase tracking-[0.12em] text-muted-foreground/55 transition-colors duration-150 hover:text-muted-foreground/80"
2523− onClick={() => onToggleWorkGroup(groupId)}
2524− >
2525− {isExpanded ? "Show less" : `Show ${hiddenCount} more`}
2526− </button>
2527− )}
2528− </div>
2529− <div className="space-y-1">
2530− {visibleEntries.map((workEntry) => (
2531− <div
2532− key={`work-row:${workEntry.id}`}
2533− className="flex items-start gap-2 py-0.5"
2702+ className="rounded-md border border-border/70 bg-background/70 px-2 py-1 font-mono text-[11px] text-muted-foreground/80 transition-colors hover:border-border hover:text-foreground/90"
2703+ onClick={() => onOpenTurnDiff(turnSummary.turnId, file.path)}
25342704 >
2535− <span className="mt-[7px] h-1.5 w-1.5 shrink-0 rounded-full bg-muted-foreground/30" />
2536− <p
2537− className={`py-[2px] text-[11px] leading-relaxed ${workToneClass(workEntry.tone)}`}
2538− >
2539− {workEntry.detail ? (
2705+ {(() => {
2706+ const stat =
2707+ typeof file.additions === "number" &&
2708+ typeof file.deletions === "number"
2709+ ? {
2710+ additions: file.additions,
2711+ deletions: file.deletions,
2712+ }
2713+ : null;
2714+ if (!stat) {
2715+ return file.path;
2716+ }
2717+ return (
25402718 <>
2541− {workEntry.label}
2542− <span
2543− className="ml-1.5 inline-block max-w-[70ch] truncate align-bottom font-mono text-[11px] opacity-60"
2544− title={workEntry.detail}
2545− >
2546− {workEntry.detail}
2719+ <span>{file.path}</span>
2720+ <span className="ml-1 text-muted-foreground/70">(</span>
2721+ <span className="text-success">+{stat.additions}</span>
2722+ <span className="mx-0.5 text-muted-foreground/70">/</span>
2723+ <span className="text-destructive">
2724+ -{stat.deletions}
25472725 </span>
2726+ <span className="text-muted-foreground/70">)</span>
25482727 </>
2549− ) : (
2550− workEntry.label
2551− )}
2552− </p>
2553− </div>
2728+ );
2729+ })()}
2730+ </button>
25542731 ))}
25552732 </div>
25562733 </div>
25572734 );
25582735 })()}
2736+ <p className="mt-1.5 text-[10px] text-muted-foreground/30">
2737+ {formatMessageMeta(
2738+ row.message.createdAt,
2739+ row.message.streaming
2740+ ? formatElapsed(row.message.createdAt, nowIso)
2741+ : formatElapsed(row.message.createdAt, row.message.completedAt),
2742+ )}
2743+ </p>
2744+ </div>
2745+ </>
2746+ );
2747+ })()}
2748+
2749+ {row.kind === "working" && (
2750+ <div className="flex items-center gap-2 py-0.5 pl-1.5">
2751+ <span className="h-1.5 w-1.5 shrink-0 rounded-full bg-muted-foreground/30" />
2752+ <div className="flex items-center pt-1">
2753+ <span className="inline-flex items-center gap-[3px]">
2754+ <span className="h-1 w-1 rounded-full bg-muted-foreground/30 animate-pulse" />
2755+ <span className="h-1 w-1 rounded-full bg-muted-foreground/30 animate-pulse [animation-delay:200ms]" />
2756+ <span className="h-1 w-1 rounded-full bg-muted-foreground/30 animate-pulse [animation-delay:400ms]" />
2757+ </span>
2758+ </div>
2759+ </div>
2760+ )}
2761+ </div>
2762+ );
25592763
2560− {row.kind === "message" &&
2561− row.message.role === "user" &&
2562− (() => {
2563− const userImages = row.message.attachments ?? [];
2564− const canRevertAgentWork = revertTurnCountByUserMessageId.has(row.message.id);
2565− return (
2566− <div className="flex justify-end">
2567− <div className="group relative max-w-[80%] rounded-2xl rounded-br-sm border border-border bg-secondary px-4 py-3">
2568− {userImages.length > 0 && (
2569− <div className="mb-2 grid max-w-[420px] grid-cols-2 gap-2">
2570− {userImages.map(
2571− (image: NonNullable<TimelineMessage["attachments"]>[number]) => (
2572− <div
2573− key={image.id}
2574− className="overflow-hidden rounded-lg border border-border/80 bg-background/70"
2575− >
2576− {image.previewUrl ? (
2577− <img
2578− src={image.previewUrl}
2579− alt={image.name}
2580− className="h-full max-h-[220px] w-full cursor-zoom-in object-cover"
2581− onClick={() =>
2582− onImageExpand({ src: image.previewUrl!, name: image.name })
2583− }
2584− />
2585− ) : (
2586− <div className="flex min-h-[72px] items-center justify-center px-2 py-3 text-center text-[11px] text-muted-foreground/70">
2587− {image.name}
2588− </div>
2589− )}
2590− </div>
2591− ),
2592− )}
2593− </div>
2594− )}
2595− {row.message.text && (
2596− <pre className="whitespace-pre-wrap wrap-break-word font-mono text-sm leading-relaxed text-foreground">
2597− {row.message.text}
2598− </pre>
2599− )}
2600− <div className="mt-1.5 flex items-center justify-end gap-2">
2601− <div className="flex items-center gap-1.5 opacity-0 transition-opacity duration-200 focus-within:opacity-100 group-hover:opacity-100">
2602− {row.message.text && <MessageCopyButton text={row.message.text} />}
2603− {canRevertAgentWork && (
2604− <Button
2605− type="button"
2606− size="xs"
2607− variant="outline"
2608− disabled={isRevertingCheckpoint || isWorking}
2609− onClick={() => onRevertUserMessage(row.message.id)}
2610− title="Revert to this message"
2611− >
2612− <Undo2Icon className="size-3" />
2613− </Button>
2614− )}
2615− </div>
2616− <p className="text-right text-[10px] text-muted-foreground/30">
2617− {formatTimestamp(row.message.createdAt)}
2618− </p>
2619− </div>
2620− </div>
2621− </div>
2622− );
2623− })()}
2624−
2625− {row.kind === "message" &&
2626− row.message.role === "assistant" &&
2627− (() => {
2628− const messageText =
2629− row.message.text || (row.message.streaming ? "" : "(empty response)");
2630− return (
2631− <>
2632− {row.showCompletionDivider && (
2633− <div className="my-3 flex items-center gap-3">
2634− <span className="h-px flex-1 bg-border" />
2635− <span className="rounded-full border border-border bg-background px-2.5 py-1 text-[10px] uppercase tracking-[0.14em] text-muted-foreground/80">
2636− {completionSummary ? `Response • ${completionSummary}` : "Response"}
2637− </span>
2638− <span className="h-px flex-1 bg-border" />
2639− </div>
2640− )}
2641− <div className="min-w-0 px-1 py-0.5">
2642− <ChatMarkdown text={messageText} cwd={markdownCwd} />
2643− {(() => {
2644− const turnSummary = turnDiffSummaryByAssistantMessageId.get(
2645− row.message.id,
2646− );
2647− if (!turnSummary) return null;
2648− const checkpointFiles = turnSummary.files;
2649− if (checkpointFiles.length === 0) return null;
2650− const summaryStat = checkpointFiles.reduce(
2651− (acc, file) => {
2652− if (
2653− typeof file.additions !== "number" ||
2654− typeof file.deletions !== "number"
2655− ) {
2656− return acc;
2657− }
2658− return {
2659− additions: acc.additions + file.additions,
2660− deletions: acc.deletions + file.deletions,
2661− };
2662− },
2663− { additions: 0, deletions: 0 },
2664− );
2665− const changedFileCountLabel = String(checkpointFiles.length);
2666− return (
2667− <div className="mt-2 rounded-lg border border-border/80 bg-card/45 p-2.5">
2668− <div className="mb-1.5 flex items-center justify-between gap-2">
2669− <p className="text-[10px] uppercase tracking-[0.12em] text-muted-foreground/65">
2670− <span>Changed files ({changedFileCountLabel})</span>
2671− {(summaryStat.additions > 0 || summaryStat.deletions > 0) && (
2672− <>
2673− <span className="mx-1">•</span>
2674− <span className="text-success">+{summaryStat.additions}</span>
2675− <span className="mx-0.5 text-muted-foreground/70">/</span>
2676− <span className="text-destructive">
2677− -{summaryStat.deletions}
2678− </span>
2679− </>
2680− )}
2681− </p>
2682− <Button
2683− type="button"
2684− size="xs"
2685− variant="outline"
2686− onClick={() =>
2687− onOpenTurnDiff(turnSummary.turnId, checkpointFiles[0]?.path)
2688− }
2689− >
2690− View diff
2691− </Button>
2692− </div>
2693− <div className="flex flex-wrap gap-1.5">
2694− {checkpointFiles.map((file) => (
2695− <button
2696− key={`${turnSummary.turnId}:${file.path}`}
2697− type="button"
2698− className="rounded-md border border-border/70 bg-background/70 px-2 py-1 font-mono text-[11px] text-muted-foreground/80 transition-colors hover:border-border hover:text-foreground/90"
2699− onClick={() => onOpenTurnDiff(turnSummary.turnId, file.path)}
2700− >
2701− {(() => {
2702− const stat =
2703− typeof file.additions === "number" &&
2704− typeof file.deletions === "number"
2705− ? {
2706− additions: file.additions,
2707− deletions: file.deletions,
2708− }
2709− : null;
2710− if (!stat) {
2711− return file.path;
2712− }
2713− return (
2714− <>
2715− <span>{file.path}</span>
2716− <span className="ml-1 text-muted-foreground/70">(</span>
2717− <span className="text-success">+{stat.additions}</span>
2718− <span className="mx-0.5 text-muted-foreground/70">/</span>
2719− <span className="text-destructive">
2720− -{stat.deletions}
2721− </span>
2722− <span className="text-muted-foreground/70">)</span>
2723− </>
2724− );
2725− })()}
2726− </button>
2727− ))}
2728− </div>
2729− </div>
2730− );
2731− })()}
2732− <p className="mt-1.5 text-[10px] text-muted-foreground/30">
2733− {formatMessageMeta(
2734− row.message.createdAt,
2735− row.message.streaming
2736− ? formatElapsed(row.message.createdAt, nowIso)
2737− : formatElapsed(row.message.createdAt, row.message.completedAt),
2738− )}
2739− </p>
2740− </div>
2741− </>
2742− );
2743− })()}
2764+ return (
2765+ <div className="mx-auto w-full min-w-0 max-w-3xl overflow-x-hidden">
2766+ {/* Virtualized completed turns */}
2767+ {virtualizedRows.length > 0 && (
2768+ <div
2769+ className="relative"
2770+ style={{ height: `${rowVirtualizer.getTotalSize()}px` }}
2771+ >
2772+ {virtualRows.map((virtualRow: VirtualItem) => {
2773+ const row = virtualizedRows[virtualRow.index];
2774+ if (!row) return null;
2775+
2776+ return (
2777+ <div
2778+ key={row.id}
2779+ data-index={virtualRow.index}
2780+ ref={rowVirtualizer.measureElement}
2781+ className="absolute left-0 top-0 w-full"
2782+ style={{ transform: `translateY(${virtualRow.start}px)` }}
2783+ >
2784+ {renderRowContent(row)}
2785+ </div>
2786+ );
2787+ })}
2788+ </div>
2789+ )}
27442790
2745− {row.kind === "working" && (
2746− <div className="flex items-center gap-2 py-0.5 pl-1.5">
2747− <span className="h-1.5 w-1.5 shrink-0 rounded-full bg-muted-foreground/30" />
2748− <div className="flex items-center pt-1">
2749− <span className="inline-flex items-center gap-[3px]">
2750− <span className="h-1 w-1 rounded-full bg-muted-foreground/30 animate-pulse" />
2751− <span className="h-1 w-1 rounded-full bg-muted-foreground/30 animate-pulse [animation-delay:200ms]" />
2752− <span className="h-1 w-1 rounded-full bg-muted-foreground/30 animate-pulse [animation-delay:400ms]" />
2753− </span>
2754− </div>
2755− </div>
2756− )}
2757− </div>
2758− </div>
2759− );
2760− })}
2791+ {/* Current in-progress turn rendered in normal flow (no virtualization) */}
2792+ {currentTurnRows.map((row) => (
2793+ <div key={row.id}>{renderRowContent(row)}</div>
2794+ ))}
27612795 </div>
27622796 );
27632797 });