All files / src/components/tools FileReadCard.tsx

89.28% Statements 25/28
81.25% Branches 39/48
85.71% Functions 6/7
88.88% Lines 24/27

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                  80x     80x 80x 76x   4x 4x             80x 80x       45x                                 80x 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 { toFileUri } from "../../utils/fileUri.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,
  onOpenDocument,
}: FileReadCardProps): JSX.Element {
  const [expanded, setExpanded] = useState(false);
  const filePath = getFilePath(args);
  const name = basename(filePath);
  const inProgress = result === undefined;
  // Clickable only when the page wired an opener and the path is absolute (#1396).
  const fileUri = onOpenDocument ? toFileUri(filePath) : 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 &&
          (fileUri && onOpenDocument ? (
            <button
              type="button"
              className={styles.fileNameLink}
              title={`Open ${filePath}`}
              onClick={() => onOpenDocument(fileUri)}
              data-testid="tool-card-file-link"
            >
              {name}
            </button>
          ) : (
            <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>
  );
}