All files / src/components/display VirtualEventItem.tsx

57.5% Statements 69/120
54.54% Branches 6/11
40% Functions 2/5
57.5% Lines 69/120

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 1721x 1x 1x 1x     1x     6x 6x 6x 6x 6x 6x 6x   6x 6x   6x   6x   6x 6x                                                                                                                                                                           1x 1x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x   6x   6x 6x     6x 6x   6x 6x 6x   6x 2x 1x 1x 4x   6x 6x 6x 6x 6x 6x 6x 6x 6x 6x 6x   6x 6x 6x 6x 6x 6x 6x 6x 6x   6x 1x 1x  
import { memo, useCallback, type JSX } from "react";
import { EventRenderer } from "./EventRenderer.js";
import { EventHoverRow } from "./EventHoverRow.js";
import { isContentBearingEvent, getEventCopyText } from "../../utils/eventContent.js";
import type { DisplayEvent } from "../../utils/sessionEvents.js";
import type { SessionEvent } from "../../hooks/types.js";
import styles from "./EventStream.module.scss";
 
/** Build a descriptive label for the selection checkbox aria-label. */
function buildCheckboxLabel(event: SessionEvent): string {
  const time = new Date(event.timestamp).toLocaleTimeString();
  switch (event.eventType) {
    case "text":
    case "output":
      return `Select message from assistant at ${time}`;
    case "user_input":
      return `Select message from user at ${time}`;
    case "tool_result":
    case "tool_use":
      return `Select tool event at ${time}`;
    case "error":
      return `Select error at ${time}`;
    default:
      return `Select event at ${time}`;
  }
}
 
/** Props for the virtualized event item. */
export interface VirtualEventItemProps {
  /** The display event to render. */
  event: DisplayEvent;
  /** Original index in the chronological events array (used for selection). */
  originalIndex: number;
  /** Whether multi-select mode is active. */
  isSelecting: boolean;
  /** Whether this event is currently selected. */
  isSelected: boolean;
  /** Called when the user clicks the Select hover action. */
  onSelect: (originalIndex: number) => void;
  /** Called when the user toggles selection (checkbox click or row click). */
  onToggle: (originalIndex: number, shiftKey: boolean) => void;
  /** Called after a successful single-event copy from the hover row. */
  onCopied: () => void;
  /** Sandbox proxy origin URL for rendering MCP Apps widget events. */
  sandboxProxyUrl?: string;
  /** Open a file in the live-docs pane. */
  onOpenDocument?: (uri: string) => void;
  /** Whether this event is newly appended (triggers entry animation). */
  isNew: boolean;
  /** Whether the stream is in reversed (newest-at-top) mode. */
  isReversed: boolean;
}
 
/** Custom equality check for React.memo — avoids re-rendering unchanged items. */
/** Compare toolUseCtx by fields (pairToolEvents recreates these objects each render). */
function toolCtxEqual(a: DisplayEvent["toolUseCtx"], b: DisplayEvent["toolUseCtx"]): boolean {
  if (a === b) {
    return true;
  }
  if (!a || !b) {
    return false;
  }
  return (
    a.tool === b.tool &&
    a.detailedResult === b.detailedResult &&
    JSON.stringify(a.args) === JSON.stringify(b.args)
  );
}
 
/** Compare events by stable fields that affect rendering. */
function eventsEqual(a: DisplayEvent, b: DisplayEvent): boolean {
  if (a === b) {
    return true;
  }
  return (
    a.sessionId === b.sessionId &&
    a.timestamp === b.timestamp &&
    a.eventType === b.eventType &&
    a.content === b.content &&
    a.toolCallId === b.toolCallId &&
    a.toolError === b.toolError &&
    a.raw === b.raw &&
    a.settled === b.settled &&
    toolCtxEqual(a.toolUseCtx, b.toolUseCtx)
  );
}
 
function arePropsEqual(
  prev: Readonly<VirtualEventItemProps>,
  next: Readonly<VirtualEventItemProps>,
): boolean {
  return (
    eventsEqual(prev.event, next.event) &&
    prev.originalIndex === next.originalIndex &&
    prev.isSelecting === next.isSelecting &&
    prev.isSelected === next.isSelected &&
    prev.onSelect === next.onSelect &&
    prev.onToggle === next.onToggle &&
    prev.onCopied === next.onCopied &&
    prev.sandboxProxyUrl === next.sandboxProxyUrl &&
    prev.onOpenDocument === next.onOpenDocument &&
    prev.isNew === next.isNew &&
    prev.isReversed === next.isReversed
  );
}
 
/**
 * Memoized event row for the virtualized EventStream.
 * Wraps EventHoverRow + EventRenderer. Measurement and positioning are
 * handled by the parent VirtualList component.
 */
export const VirtualEventItem: React.NamedExoticComponent<VirtualEventItemProps> = memo(
  function VirtualEventItem({
    event,
    originalIndex,
    isSelecting,
    isSelected,
    onSelect,
    onToggle,
    onCopied,
    sandboxProxyUrl,
    onOpenDocument,
    isNew,
    isReversed,
  }: VirtualEventItemProps): JSX.Element {
    const handleSelect = useCallback(() => {
      onSelect(originalIndex);
    }, [onSelect, originalIndex]);
 
    const handleToggle = useCallback(
      (shiftKey: boolean) => {
        onToggle(originalIndex, shiftKey);
      },
      [onToggle, originalIndex],
    );
 
    const copyText = getEventCopyText(event);
    const contentBearing = isContentBearingEvent(event);
    const checkboxLabel = buildCheckboxLabel(event);
 
    const animationClass = isNew
      ? isReversed
        ? styles.eventFadeInReversed
        : styles.eventFadeIn
      : undefined;
 
    return (
      <div className={animationClass}>
        <EventHoverRow
          copyText={copyText}
          isContentBearing={contentBearing}
          isSelecting={isSelecting}
          isSelected={isSelected}
          checkboxLabel={checkboxLabel}
          onSelect={handleSelect}
          onToggle={handleToggle}
          onCopied={onCopied}
        >
          <EventRenderer
            event={event}
            toolUseCtx={event.toolUseCtx}
            settled={event.settled}
            sandboxProxyUrl={sandboxProxyUrl}
            onOpenDocument={onOpenDocument}
          />
        </EventHoverRow>
      </div>
    );
  },
  arePropsEqual,
);