# All files / src/components/tools FileReadCard.tsx

5.93% Statements 7/118
100% Branches 0/0
0% Functions 0/3
5.93% Lines 7/118

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
    1521x
    1x

    1x
    1x
    1x

    1x

    1x

     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 {
      if (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>
      );
    }