import {
ARTIFACT_PREVIEW,
ARTIFACT_ROW,
CARD_GAP,
FILE_HEADER,
FILE_HEADER_PAD_Y,
FILE_PREVIEW,
FILE_ROW,
SHOW_MORE_HEIGHT,
} from "./layout";
import type { Artifact, EditedFile } from "../../lib/timeline";
import { cn } from "../../lib/utils";
import { Button } from "../../ui/button";
import { buttonVariants } from "../../lib/button-variants";
import {
IconChevronDownMicro,
IconExternal,
IconFileDiff,
IconFileText,
IconHash,
IconIssue,
IconMessage,
IconPullRequest,
} from "../../icons";
const VM_PROJECT_PREFIX = "/workspace/project/";
type TurnSummaryProps = {
id: string;
artifacts: Artifact[];
files: EditedFile[];
artifactsExpanded: boolean;
filesExpanded: boolean;
onToggleArtifacts: () => void;
onToggleFiles: () => void;
onFileOpen?: (file: string) => void;
onEditedFileOpen?: (file: string) => void;
onLink?: (href: string) => void;
onReviewChanges?: () => void;
};
function TurnSummary({
id,
artifacts,
files,
artifactsExpanded,
filesExpanded,
onToggleArtifacts,
onToggleFiles,
onFileOpen,
onEditedFileOpen,
onLink,
onReviewChanges,
}: TurnSummaryProps) {
return (
{artifacts.length > 0 && (
)}
{files.length > 0 && (
)}
);
}
function ArtifactCard({
id,
artifacts,
expanded,
onToggle,
onFileOpen,
onLink,
}: {
id: string;
artifacts: Artifact[];
expanded: boolean;
onToggle: () => void;
onFileOpen?: (file: string) => void;
onLink?: (href: string) => void;
}) {
let visible = expanded ? artifacts : artifacts.slice(0, ARTIFACT_PREVIEW);
let extra = artifacts.length - ARTIFACT_PREVIEW;
return (
{visible.map((artifact, index) => (
0}
height={ARTIFACT_ROW}
onFileOpen={onFileOpen}
onLink={onLink}
/>
))}
{extra > 0 && (
)}
);
}
function ArtifactRow({
artifact,
separated,
height,
onFileOpen,
onLink,
}: {
artifact: Artifact;
separated: boolean;
height: number;
onFileOpen?: (file: string) => void;
onLink?: (href: string) => void;
}) {
let Icon = artifactIcon(artifact);
let actionable = Boolean(artifact.url || artifact.path && onFileOpen);
let actionLabel = "Open";
let content = (
<>
{artifact.meta}
{artifact.title}
{actionable && (
{actionLabel}
{artifact.url && }
)}
>
);
let classes = cn(
"flex w-full min-w-0 items-center gap-3 px-3 text-left transition-colors duration-150 focus-visible:outline-2 focus-visible:outline-ring/50 motion-reduce:transition-none",
"hover:bg-muted/50",
separated && "border-t border-border",
);
let style = { blockSize: height, boxSizing: "border-box" as const };
if (artifact.url) {
if (onLink) {
return (
);
}
return (
{content}
);
}
if (artifact.path && onFileOpen) {
return (
);
}
return (
{content}
);
}
function artifactIcon(artifact: Artifact) {
switch (artifact.kind) {
case "issue":
return IconIssue;
case "pull-request":
return IconPullRequest;
case "comment":
return IconMessage;
case "label":
return IconHash;
case "document":
return IconFileText;
}
}
function EditedFilesCard({
id,
files,
expanded,
onToggle,
onFileOpen,
onReviewChanges,
}: {
id: string;
files: EditedFile[];
expanded: boolean;
onToggle: () => void;
onFileOpen?: (file: string) => void;
onReviewChanges?: () => void;
}) {
let visible = expanded ? files : files.slice(0, FILE_PREVIEW);
let extra = files.length - FILE_PREVIEW;
let adds = sum(files, "adds");
let dels = sum(files, "dels");
let hasStats = adds > 0 || dels > 0;
return (
Edited {files.length} {files.length === 1 ? "file" : "files"}
{hasStats && (
+{adds}{" "}
-{dels}
)}
{onReviewChanges && (
)}
{visible.map(file => (
))}
{extra > 0 && (
)}
);
}
function FileRow({
file,
height,
onFileOpen,
}: {
file: EditedFile;
height: number;
onFileOpen?: (file: string) => void;
}) {
let stats = file.adds != null || file.dels != null;
let label = displayPath(file.path);
let content = (
<>
{label}
{stats && (
+{file.adds ?? 0}{" "}
-{file.dels ?? 0}
)}
>
);
let classes = cn(
"flex w-full min-w-0 items-center gap-3 px-[14px] text-left transition-colors duration-150 focus-visible:outline-2 focus-visible:outline-ring/50 motion-reduce:transition-none",
"hover:bg-muted/50",
);
let style = { blockSize: height, boxSizing: "border-box" as const };
if (!onFileOpen) return {content}
;
return (
);
}
function displayPath(path: string): string {
return path.startsWith(VM_PROJECT_PREFIX) ? path.slice(VM_PROJECT_PREFIX.length) : path;
}
function ShowMore({
id,
count,
label,
expanded,
onToggle,
}: {
id: string;
count: number;
label: string;
expanded: boolean;
onToggle: () => void;
}) {
let text = expanded ? "Show less" : `Show ${count} more ${label}${count === 1 ? "" : "s"}`;
return (
);
}
function sum(files: EditedFile[], key: "adds" | "dels"): number {
let value = 0;
for (let file of files) value += file[key] ?? 0;
return value;
}
export { TurnSummary };