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 | 1x 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>
);
}
|