All files / src/components/tools FileReadCard.tsx

92.3% Statements 24/26
85.71% Branches 36/42
100% Functions 6/6
92% Lines 23/25

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                80x     80x 80x 76x   4x 4x             80x 80x       45x                               80x 80x 80x 80x   80x         80x 80x 80x   80x 80x 80x   80x                                                                 247x                                           10x                                      
import { useState, type JSX, type ReactNode } from "react";
import { ChevronRight, FilePen, FileText } from "lucide-react";
import type { ToolCardProps } from "./ToolCardProps.js";
import { ICON_SM, ICON_MD } from "../../utils/iconSize.js";
import styles from "./toolCards.module.scss";
 
/** Extracts the file path from tool args (handles both `file_path` and `path` variants). */
function getFilePath(args: unknown): string {
  Iif (args === null || args === undefined || typeof args !== "object") {
    return "";
  }
  const a = args as Record<string, unknown>;
  if (typeof a.file_path === "string") {
    return a.file_path;
  }
  if (typeof a.path === "string") {
    return a.path;
  }
  return "";
}
 
/** Extracts the basename from a file path (handles both / and \ separators). */
function basename(filePath: string): string {
  const parts = filePath.split(/[/\\]/);
  return parts[parts.length - 1] || filePath;
}
 
/** Number of preview lines shown when collapsed. */
const PREVIEW_LINES: number = 5;
 
/** Extra props for FileReadCard to support write variant styling. */
interface FileReadCardProps extends ToolCardProps {
  /** When true, uses green accent and write icon instead of blue/read. */
  writeVariant?: boolean;
}
 
/** Renders a file read/write tool call with syntax-highlighted content preview. */
export function FileReadCard({
  tool,
  args,
  result,
  isError,
  writeVariant,
}: FileReadCardProps): JSX.Element {
  const [expanded, setExpanded] = useState(false);
  const filePath = getFilePath(args);
  const name = basename(filePath);
  const inProgress = result === undefined;
 
  const accentClass: string = isError
    ? styles.cardRed
    : writeVariant
      ? styles.cardGreen
      : styles.cardBlue;
  const accentColor: string = writeVariant ? "var(--accent-green)" : "var(--accent-blue)";
  const icon: ReactNode = writeVariant ? <FilePen size={ICON_MD} /> : <FileText size={ICON_MD} />;
  const testId: string = writeVariant ? "tool-card-file-write" : "tool-card-file-read";
 
  const lines = result?.split("\n") ?? [];
  const hasMore = lines.length > PREVIEW_LINES;
  const displayLines = expanded ? lines : lines.slice(0, PREVIEW_LINES);
 
  return (
    <div
      className={`${styles.card} ${accentClass} ${inProgress ? styles.inProgress : ""}`}
      data-testid={testId}
    >
      <div className={styles.header}>
        <span className={styles.icon}>{icon}</span>
        <span className={styles.toolName} style={{ color: accentColor }}>
          {tool}
        </span>
        {name && (
          <span className={styles.fileName} title={filePath}>
            {name}
          </span>
        )}
        {!inProgress && lines.length > 0 && (
          <>
            <span className={styles.spacer} />
            <span className={styles.badge}>{lines.length} lines</span>
          </>
        )}
      </div>
 
      {isError && result && (
        <pre className={styles.pre} data-testid="tool-card-error">
          {result}
        </pre>
      )}
 
      {!isError && !inProgress && lines.length > 0 && (
        <>
          <pre className={styles.pre} data-testid="tool-card-content">
            {displayLines.map((line, i) => (
              <span key={i} className={styles.diffLine}>
                <span
                  style={{
                    color: "var(--text-tertiary)",
                    userSelect: "none",
                    marginRight: "var(--space-sm)",
                    display: "inline-block",
                    width: "3ch",
                    textAlign: "right",
                  }}
                >
                  {i + 1}
                </span>
                {line}
              </span>
            ))}
          </pre>
          {hasMore && (
            <button
              type="button"
              className={styles.bodyToggle}
              onClick={() => {
                setExpanded((v) => !v);
              }}
              aria-expanded={expanded}
              data-testid="tool-card-toggle"
            >
              <span
                className={`${styles.chevron} ${expanded ? styles.chevronExpanded : ""}`}
                aria-hidden="true"
              >
                <ChevronRight size={ICON_SM} />
              </span>
              {expanded ? "collapse" : `${lines.length - PREVIEW_LINES} more lines`}
            </button>
          )}
        </>
      )}
    </div>
  );
}