feat(site): add transcript tool icons (#25724)

This commit is contained in:
Danielle Maywood
2026-05-27 14:43:14 +01:00
committed by GitHub
parent ecaf5e022b
commit 5603be19cc
26 changed files with 560 additions and 133 deletions
@@ -1,3 +1,4 @@
import { BrainIcon } from "lucide-react";
import { type FC, useEffect, useState } from "react";
import { Alert, AlertDescription, AlertTitle } from "#/components/Alert/Alert";
import { Link } from "#/components/Link/Link";
@@ -18,14 +19,18 @@ type ReconnectingStatus = Extract<LiveStatusModel, { phase: "reconnecting" }>;
const StatusPlaceholder: FC<{
text: string;
shimmer?: boolean;
}> = ({ text, shimmer = false }) => {
showThinkingIcon?: boolean;
}> = ({ text, shimmer = false, showThinkingIcon = false }) => {
return (
<div className="relative min-h-6">
{/* Reserve the final response height without exposing a selectable copy. */}
<Response aria-hidden className="invisible select-none">
{text}
</Response>
<div className="pointer-events-none absolute inset-0 flex items-baseline gap-2">
<div className="pointer-events-none absolute inset-0 flex items-center gap-2 text-content-secondary">
{showThinkingIcon && (
<BrainIcon className="size-4 shrink-0 stroke-[1.5] text-current" />
)}
{shimmer ? (
<Shimmer as="div" className="text-[13px] leading-relaxed">
{text}
@@ -54,6 +59,7 @@ const StartingPlaceholder: FC = () => {
<StatusPlaceholder
text={isDelayed ? DELAYED_STARTUP_TEXT : THINKING_TEXT}
shimmer={!isDelayed}
showThinkingIcon={!isDelayed}
/>
);
};
@@ -1,4 +1,9 @@
import { ChevronLeftIcon, ChevronRightIcon, PencilIcon } from "lucide-react";
import {
BrainIcon,
ChevronLeftIcon,
ChevronRightIcon,
PencilIcon,
} from "lucide-react";
import {
type FC,
Fragment,
@@ -160,13 +165,16 @@ const ReasoningDisclosure = memo<{
expanded={expanded}
onExpandedChange={(open) => setManualToggle(open)}
header={
isStreaming ? (
<Shimmer as="span" className="text-[13px]">
{title}
</Shimmer>
) : (
<span className="text-[13px]">{title}</span>
)
<>
<BrainIcon className="size-4 shrink-0 stroke-[1.5] text-current" />
{isStreaming ? (
<Shimmer as="span" className="text-[13px] leading-6">
{title}
</Shimmer>
) : (
<span className="text-[13px] leading-6">{title}</span>
)}
</>
}
>
{hasText && (
@@ -1,3 +1,4 @@
import { BrainIcon } from "lucide-react";
import type { FC } from "react";
import type { UrlTransform } from "streamdown";
import type * as TypesGen from "#/api/typesGenerated";
@@ -36,7 +37,8 @@ const hasTextOrReasoningBlock = (blocks: readonly RenderBlock[]): boolean =>
const StreamingThinkingPlaceholder: FC = () => (
<div data-transcript-row="" className="text-content-secondary">
<TranscriptRow className="w-full gap-2">
<Shimmer as="span" className="text-[13px] leading-relaxed">
<BrainIcon className="size-4 shrink-0 stroke-[1.5] text-current" />
<Shimmer as="span" className="text-[13px] leading-6">
Thinking
</Shimmer>
</TranscriptRow>
@@ -1,3 +1,4 @@
import { BrainIcon } from "lucide-react";
import type { ComponentPropsWithRef } from "react";
import { cn } from "#/utils/cn";
@@ -8,10 +9,13 @@ export const Thinking = ({ className, ref, ...props }: ThinkingProps) => {
<div
ref={ref}
className={cn(
"rounded-lg border border-border bg-surface-primary px-3 py-2 text-xs text-content-secondary",
"flex items-start gap-2 rounded-lg border border-border bg-surface-primary px-3 py-2 text-xs text-content-secondary",
className,
)}
{...props}
/>
>
<BrainIcon className="mt-0.5 size-4 shrink-0 stroke-[1.5] text-current" />
<div className="min-w-0 flex-1">{props.children}</div>
</div>
);
};
@@ -53,55 +53,55 @@ export const AdvisorTool: React.FC<AdvisorToolProps> = ({
defaultExpanded
headerClassName="items-start"
header={(expanded) => (
<>
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<div className="flex min-w-0 items-center gap-2 leading-4">
<ToolIcon
name="advisor"
isError={showError}
isRunning={isRunning}
/>
<ToolLabel
name="advisor"
args={{ question: questionText }}
result={resultType ? { type: resultType } : undefined}
/>
{isRunning && (
<span className="shrink-0 rounded-full border border-solid border-border-default px-2 text-[13px] leading-4 text-content-secondary">
{RUNNING_MESSAGE}
</span>
)}
{advisorModelText && (
<span className="min-w-0 truncate rounded-full border border-solid border-border-default px-2 text-[13px] leading-4 text-content-secondary">
{advisorModelText}
</span>
)}
{remainingUses !== undefined && (
<span className="shrink-0 rounded-full border border-solid border-border-default px-2 text-[13px] leading-4 text-content-secondary">
{remainingUses.toLocaleString("en-US")} uses left
</span>
)}
</div>
<span
className={cn(
"ml-6 block whitespace-normal break-words text-[13px]",
"font-normal leading-5 text-content-primary",
"[overflow-wrap:anywhere]",
!expanded && "line-clamp-2",
)}
>
{questionText}
</span>
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<div className="flex min-w-0 items-center gap-2 leading-4">
<ToolIcon
name="advisor"
isError={showError}
isRunning={isRunning}
/>
<ToolLabel
name="advisor"
args={{ question: questionText }}
result={resultType ? { type: resultType } : undefined}
/>
{isRunning && (
<span className="shrink-0 rounded-full border border-solid border-border-default px-2 text-[13px] leading-4 text-content-secondary">
{RUNNING_MESSAGE}
</span>
)}
{advisorModelText && (
<span className="min-w-0 truncate rounded-full border border-solid border-border-default px-2 text-[13px] leading-4 text-content-secondary">
{advisorModelText}
</span>
)}
{remainingUses !== undefined && (
<span className="shrink-0 rounded-full border border-solid border-border-default px-2 text-[13px] leading-4 text-content-secondary">
{remainingUses.toLocaleString("en-US")} uses left
</span>
)}
</div>
{showLimitReached ? (
<TriangleAlertIcon className="mt-0.5 size-3.5 shrink-0 text-content-warning" />
) : showError ? (
<CircleAlertIcon className="mt-0.5 size-3.5 shrink-0 text-content-destructive" />
) : isRunning ? (
<LoaderIcon className="mt-0.5 size-3.5 shrink-0 animate-spin motion-reduce:animate-none text-content-secondary" />
) : null}
</>
<span
className={cn(
"ml-6 block whitespace-normal break-words text-[13px]",
"font-normal leading-5 text-content-primary",
"[overflow-wrap:anywhere]",
!expanded && "line-clamp-2",
)}
>
{questionText}
</span>
</div>
)}
headerStatus={
showLimitReached ? (
<TriangleAlertIcon className="mt-0.5 size-3.5 shrink-0 text-content-warning" />
) : showError ? (
<CircleAlertIcon className="mt-0.5 size-3.5 shrink-0 text-content-destructive" />
) : isRunning ? (
<LoaderIcon className="mt-0.5 size-3.5 shrink-0 animate-spin motion-reduce:animate-none text-content-secondary" />
) : null
}
>
<ScrollArea
className="mt-1.5 rounded-md border border-solid border-border-default bg-surface-primary"
@@ -10,6 +10,7 @@ import { Input } from "#/components/Input/Input";
import { RadioGroup, RadioGroupItem } from "#/components/RadioGroup/RadioGroup";
import { cn } from "#/utils/cn";
import { TranscriptRow } from "../TranscriptRow";
import { ToolIcon } from "./ToolIcon";
import type { ToolStatus } from "./utils";
export type AskUserQuestion = {
@@ -539,8 +540,9 @@ export const AskUserQuestionTool: FC<AskUserQuestionToolProps> = ({
<div className="w-full">
<TranscriptRow
role="alert"
className="gap-1.5 text-[13px] text-content-secondary"
className="gap-2 text-[13px] text-content-secondary"
>
<ToolIcon name="ask_user_question" isError={isError} />
<TriangleAlertIcon
aria-label="Error"
className="size-3.5 shrink-0 text-content-secondary"
@@ -555,7 +557,16 @@ export const AskUserQuestionTool: FC<AskUserQuestionToolProps> = ({
return (
<div className="w-full">
{isRunning ? (
<TranscriptRow role="status" aria-live="polite" className="gap-1.5">
<TranscriptRow
role="status"
aria-live="polite"
className="gap-2 text-content-secondary"
>
<ToolIcon
name="ask_user_question"
isError={false}
isRunning={isRunning}
/>
<span className="text-[13px] text-content-secondary">
Asking for clarification...
</span>
@@ -677,7 +688,16 @@ export const AskUserQuestionTool: FC<AskUserQuestionToolProps> = ({
return (
<div className="w-full">
{isRunning && (
<TranscriptRow role="status" aria-live="polite" className="gap-1.5">
<TranscriptRow
role="status"
aria-live="polite"
className="gap-2 text-content-secondary"
>
<ToolIcon
name="ask_user_question"
isError={false}
isRunning={isRunning}
/>
<span className="text-[13px] text-content-secondary">
Asking for clarification...
</span>
@@ -8,6 +8,7 @@ import {
} from "#/components/Tooltip/Tooltip";
import { Response } from "../Response";
import { ToolCollapsible } from "./ToolCollapsible";
import { ToolIcon } from "./ToolIcon";
import type { ToolStatus } from "./utils";
/**
@@ -29,9 +30,18 @@ export const ChatSummarizedTool: React.FC<{
hasContent={hasSummary}
header={
<>
<span className="text-[13px]">
<ToolIcon
name="chat_summarized"
isError={isError}
isRunning={isRunning}
/>
<span className="text-[13px] leading-6">
{isRunning ? "Summarizing…" : "Summarized"}
</span>
</>
}
headerStatus={
<>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
@@ -8,6 +8,7 @@ import {
} from "#/components/Tooltip/Tooltip";
import { ImageLightbox } from "../../ImageLightbox";
import { ToolCollapsible } from "./ToolCollapsible";
import { ToolIcon } from "./ToolIcon";
import type { ToolStatus } from "./utils";
/**
@@ -39,9 +40,14 @@ export const ComputerTool: React.FC<{
defaultExpanded={hasImage}
header={
<>
<span className="text-[13px]">
<ToolIcon name="computer" isError={isError} isRunning={isRunning} />
<span className="text-[13px] leading-6">
{isRunning ? "Taking screenshot…" : "Screenshot"}
</span>
</>
}
headerStatus={
<>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
@@ -1,9 +1,4 @@
import {
ExternalLinkIcon,
LoaderIcon,
MonitorIcon,
TriangleAlertIcon,
} from "lucide-react";
import { ExternalLinkIcon, LoaderIcon, TriangleAlertIcon } from "lucide-react";
import type React from "react";
import { Link } from "react-router";
import {
@@ -12,6 +7,7 @@ import {
TooltipTrigger,
} from "#/components/Tooltip/Tooltip";
import { ToolCollapsible } from "./ToolCollapsible";
import { ToolIcon } from "./ToolIcon";
import { asRecord, asString, type ToolStatus } from "./utils";
import { WorkspaceBuildLogSection } from "./WorkspaceBuildLogSection";
@@ -72,8 +68,26 @@ export const CreateWorkspaceTool: React.FC<{
const header = (
<>
<MonitorIcon className="size-4 shrink-0 text-current" />
<span className="text-[13px]">{label}</span>
<ToolIcon
name="create_workspace"
isError={isError}
isRunning={isRunning}
/>
<span className="text-[13px] leading-6">{label}</span>
{workspaceLink && !isRunning && (
<Link
to={workspaceLink}
onClick={(e) => e.stopPropagation()}
className="ml-1 inline-flex align-middle text-content-secondary opacity-50 transition-opacity hover:opacity-100"
aria-label="View workspace"
>
<ExternalLinkIcon className="size-3" />
</Link>
)}
</>
);
const headerStatus = (
<>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
@@ -87,16 +101,6 @@ export const CreateWorkspaceTool: React.FC<{
{isRunning && (
<LoaderIcon className="size-3.5 shrink-0 animate-spin motion-reduce:animate-none text-current" />
)}
{workspaceLink && !isRunning && (
<Link
to={workspaceLink}
onClick={(e) => e.stopPropagation()}
className="ml-1 inline-flex align-middle text-content-secondary opacity-50 transition-opacity hover:opacity-100"
aria-label="View workspace"
>
<ExternalLinkIcon className="size-3" />
</Link>
)}
</>
);
@@ -104,6 +108,7 @@ export const CreateWorkspaceTool: React.FC<{
<div className="w-full">
<ToolCollapsible
header={header}
headerStatus={headerStatus}
hasContent={hasBuildLogs}
defaultExpanded={isRunning}
>
@@ -16,6 +16,7 @@ import {
resolveAgentDisplayState,
} from "./displayMode";
import { AgentDisplayModeToolCollapsible } from "./ToolCollapsible";
import { ToolIcon } from "./ToolIcon";
import {
DIFFS_FONT_STYLE,
type EditFilesFileEntry,
@@ -69,7 +70,12 @@ export const EditFilesTool: React.FC<{
autoDisplayState={EDIT_FILES_AUTO_DISPLAY_STATE}
header={
<>
<span className="text-[13px]">{label}</span>
<ToolIcon name="edit_files" isError={isError} isRunning={isRunning} />
<span className="text-[13px] leading-6">{label}</span>
</>
}
headerStatus={
<>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
@@ -26,6 +26,7 @@ import {
isAgentDisplayOpen,
resolveAgentDisplayState,
} from "./displayMode";
import { ToolIcon } from "./ToolIcon";
import {
formatShellDurationMs,
sanitizeExecuteModelIntent,
@@ -190,7 +191,8 @@ const ShellCommandLine: React.FC<{
const commandDisplay = summary || command;
return (
<>
<span className="block min-w-0 truncate text-[13px] font-normal text-current">
<ToolIcon name="execute" isError={false} />
<span className="block min-w-0 truncate text-[13px] font-normal leading-6 text-current">
{intentLabel ? (
<>
{intentLabel} using {commandDisplay}
@@ -7,6 +7,7 @@ import {
TooltipTrigger,
} from "#/components/Tooltip/Tooltip";
import { ToolCollapsible } from "./ToolCollapsible";
import { ToolIcon } from "./ToolIcon";
import { asRecord, asString, type ToolStatus } from "./utils";
/**
@@ -36,7 +37,16 @@ export const ListTemplatesTool: React.FC<{
hasContent={hasContent}
header={
<>
<span className="text-[13px]">{label}</span>
<ToolIcon
name="list_templates"
isError={isError}
isRunning={isRunning}
/>
<span className="text-[13px] leading-6">{label}</span>
</>
}
headerStatus={
<>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
@@ -16,6 +16,7 @@ import {
resolveAgentDisplayState,
} from "./displayMode";
import { AgentDisplayModeToolCollapsible } from "./ToolCollapsible";
import { ToolIcon } from "./ToolIcon";
import { COLLAPSED_OUTPUT_HEIGHT, signalTooltipLabel } from "./utils";
type ProcessOutputToolProps = {
@@ -77,8 +78,7 @@ const ProcessOutputToolInner: React.FC<ProcessOutputToolInnerProps> = ({
const toggleOutputExpansion = () => {
setOutputFullyExpanded((expanded) => !expanded);
};
const hasHeaderActions =
isRunning || Boolean(killedBySignal) || showExitCode || hasOutput;
const hasHeaderActions = Boolean(killedBySignal) || showExitCode || hasOutput;
return (
<AgentDisplayModeToolCollapsible
@@ -89,13 +89,24 @@ const ProcessOutputToolInner: React.FC<ProcessOutputToolInnerProps> = ({
ariaLabel={(expanded) =>
expanded ? "Collapse process output" : "Expand process output"
}
header={<span className="text-[13px]">Process output</span>}
header={
<>
<ToolIcon
name="process_output"
isError={isError}
isRunning={isRunning}
/>
<span className="text-[13px] leading-6">Process output</span>
</>
}
headerStatus={
isRunning ? (
<LoaderIcon className="size-3.5 shrink-0 animate-spin motion-reduce:animate-none text-content-secondary" />
) : undefined
}
headerActions={
hasHeaderActions ? (
<>
{isRunning && (
<LoaderIcon className="size-3.5 shrink-0 animate-spin motion-reduce:animate-none text-content-secondary" />
)}
{killedBySignal && !isRunning && (
<Tooltip>
<TooltipTrigger asChild>
@@ -11,6 +11,7 @@ import {
} from "#/components/Tooltip/Tooltip";
import { Response } from "../Response";
import { TranscriptRow } from "../TranscriptRow";
import { ToolIcon } from "./ToolIcon";
import type { ToolStatus } from "./utils";
export const ProposePlanTool: React.FC<{
@@ -73,8 +74,13 @@ export const ProposePlanTool: React.FC<{
return (
<div className="w-full">
<TranscriptRow className="gap-1.5 text-content-secondary">
<span className="text-[13px]">
<TranscriptRow className="gap-2 text-content-secondary">
<ToolIcon
name="propose_plan"
isError={effectiveError}
isRunning={isRunning}
/>
<span className="text-[13px] leading-6">
{isRunning ? `Proposing ${filename}…` : `Proposed ${filename}`}
</span>
{effectiveError && (
@@ -138,7 +144,7 @@ export const ProposePlanTool: React.FC<{
)
)}
{fetchLoading && (
<TranscriptRow className="gap-1.5 text-[13px] text-content-secondary">
<TranscriptRow className="gap-2 text-[13px] text-content-secondary">
<LoaderIcon className="size-3.5 animate-spin motion-reduce:animate-none" />
Loading plan…
</TranscriptRow>
@@ -10,6 +10,7 @@ import {
} from "#/components/Tooltip/Tooltip";
import { asRecord, asString } from "../runtimeTypeUtils";
import { ToolCollapsible } from "./ToolCollapsible";
import { ToolIcon } from "./ToolIcon";
import {
DIFFS_FONT_STYLE,
getFileViewerOptionsMinimal,
@@ -96,7 +97,12 @@ export const ReadFileTool: React.FC<{
onExpandedChange={onExpandedChange}
header={
<>
<span className="text-[13px]">{label}</span>
<ToolIcon name="read_file" isError={isError} isRunning={isRunning} />
<span className="text-[13px] leading-6">{label}</span>
</>
}
headerStatus={
<>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
@@ -8,6 +8,7 @@ import {
import type { MergedTool } from "../../ChatConversation/types";
import { getReadFileToolData, ReadFileTool } from "./ReadFileTool";
import { ToolCollapsible } from "./ToolCollapsible";
import { ToolIcon } from "./ToolIcon";
type ReadFileItem = {
id: string;
@@ -50,7 +51,12 @@ export const ReadFilesTool: FC<{
onExpandedChange={onExpandedChange}
header={
<>
<span className="text-[13px]">{label}</span>
<ToolIcon
name="read_file"
isError={isError}
isRunning={isRunning}
/>
<span className="text-[13px] leading-6">{label}</span>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
@@ -1,4 +1,4 @@
import { BookOpenIcon, LoaderIcon, TriangleAlertIcon } from "lucide-react";
import { LoaderIcon, TriangleAlertIcon } from "lucide-react";
import type React from "react";
import { ScrollArea } from "#/components/ScrollArea/ScrollArea";
import {
@@ -8,6 +8,7 @@ import {
} from "#/components/Tooltip/Tooltip";
import { Response } from "../Response";
import { ToolCollapsible } from "./ToolCollapsible";
import { ToolIcon } from "./ToolIcon";
import type { ToolStatus } from "./utils";
export const ReadSkillTool: React.FC<{
@@ -26,10 +27,14 @@ export const ReadSkillTool: React.FC<{
hasContent={hasContent}
header={
<>
<BookOpenIcon className="size-4 shrink-0 text-current" />
<span className="text-[13px]">
<ToolIcon name="read_skill" isError={isError} isRunning={isRunning} />
<span className="text-[13px] leading-6">
{isRunning ? `Reading ${label}…` : `Read ${label}`}
</span>
</>
}
headerStatus={
<>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
@@ -6,6 +6,7 @@ import {
TooltipTrigger,
} from "#/components/Tooltip/Tooltip";
import { TranscriptRow } from "../TranscriptRow";
import { ToolIcon } from "./ToolIcon";
import type { ToolStatus } from "./utils";
/**
@@ -27,8 +28,9 @@ export const ReadTemplateTool: React.FC<{
: "Read template";
return (
<TranscriptRow className="gap-1.5 text-content-secondary">
<span className="text-[13px]">{label}</span>
<TranscriptRow className="gap-2 text-content-secondary">
<ToolIcon name="read_template" isError={isError} isRunning={isRunning} />
<span className="text-[13px] leading-6">{label}</span>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
@@ -1,4 +1,4 @@
import { LoaderIcon, MonitorPlayIcon, TriangleAlertIcon } from "lucide-react";
import { LoaderIcon, TriangleAlertIcon } from "lucide-react";
import type { FC } from "react";
import {
Tooltip,
@@ -6,6 +6,7 @@ import {
TooltipTrigger,
} from "#/components/Tooltip/Tooltip";
import { ToolCollapsible } from "./ToolCollapsible";
import { ToolIcon } from "./ToolIcon";
import type { ToolStatus } from "./utils";
import { WorkspaceBuildLogSection } from "./WorkspaceBuildLogSection";
@@ -42,8 +43,16 @@ export const StartWorkspaceTool: FC<StartWorkspaceToolProps> = ({
const header = (
<>
<MonitorPlayIcon className="size-4 shrink-0 text-current" />
<span className="text-[13px]">{label}</span>
<ToolIcon
name="start_workspace"
isError={isError}
isRunning={isRunning}
/>
<span className="text-[13px] leading-6">{label}</span>
</>
);
const headerStatus = (
<>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
@@ -67,6 +76,7 @@ export const StartWorkspaceTool: FC<StartWorkspaceToolProps> = ({
<div className="w-full">
<ToolCollapsible
header={header}
headerStatus={headerStatus}
hasContent={hasBuildLogs}
defaultExpanded={isRunning}
>
@@ -69,7 +69,7 @@ function getSubagentLabel(
): React.ReactNode {
if (showDesktopPreview && toolStatus === "running") {
return (
<Shimmer as="span" className="text-[13px]">
<Shimmer as="span" className="text-[13px] leading-6">
Using the computer...
</Shimmer>
);
@@ -125,20 +125,22 @@ const SubagentStatusIcon: React.FC<{
const subagentCompleted = isSubagentSuccessStatus(subagentStatus);
const DefaultIcon = iconKind === "monitor" ? MonitorIcon : BotIcon;
if (isTimeout && !subagentCompleted) {
return <ClockIcon className="size-4 shrink-0 text-current" />;
return <ClockIcon className="size-4 shrink-0 stroke-[1.5] text-current" />;
}
if ((isError && !subagentCompleted) || toolStatus === "error") {
return <CircleXIcon className="size-4 shrink-0 text-current" />;
}
if (toolStatus === "running") {
if (showDesktopPreview) {
return <MonitorIcon className="size-4 shrink-0 text-current" />;
return (
<MonitorIcon className="size-4 shrink-0 stroke-[1.5] text-current" />
);
}
return (
<LoaderIcon className="size-4 shrink-0 animate-spin motion-reduce:animate-none text-content-link" />
);
}
return <DefaultIcon className="size-4 shrink-0 text-current" />;
return <DefaultIcon className="size-4 shrink-0 stroke-[1.5] text-current" />;
};
/**
@@ -10,6 +10,7 @@ import {
} from "storybook/test";
import { reactRouterParameters } from "storybook-addon-remix-react-router";
import { ChatWorkspaceContext } from "../../../context/ChatWorkspaceContext";
import { BlockList } from "../../ChatConversation/ConversationTimeline";
import { DesktopPanelContext } from "./DesktopPanelContext";
import { Tool } from "./Tool";
@@ -18,6 +19,10 @@ const executeIntentCommand = "npm test";
const longExecuteCommand =
"docker build --no-cache --build-arg NODE_ENV=production --build-arg API_URL=https://coder.example.com/api --build-arg SENTRY_DSN=https://example.com/sentry --build-arg FEATURE_FLAGS=agents,shell-tools --tag coder-agent:latest .";
// 1x1 solid coral (#FF6B6B) PNG encoded as base64.
const TEST_PNG_B64 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4n539HwAHFwLVF8kc1wAAAABJRU5ErkJggg==";
const getDiffsText = (element: HTMLElement) =>
Array.from(element.querySelectorAll("diffs-container"))
.map((container) => container.shadowRoot?.textContent ?? "")
@@ -48,6 +53,212 @@ const meta: Meta<typeof Tool> = {
export default meta;
type Story = StoryObj<typeof Tool>;
type ToolShowcaseItem = {
name: string;
status?: React.ComponentProps<typeof Tool>["status"];
args?: unknown;
result?: unknown;
isError?: boolean;
killedBySignal?: "kill" | "terminate";
modelIntent?: string;
parsedCommands?: readonly string[][];
subagentVariants?: Map<string, "general" | "explore" | "computer_use">;
};
const allToolShowcaseItems: ToolShowcaseItem[] = [
{
name: "execute",
args: { command: "pnpm check", model_intent: "Checking frontend" },
modelIntent: "Checking frontend",
parsedCommands: [["pnpm", "check"]],
result: {
output: "Checked 1799 files.",
wall_duration_ms: 2400,
exit_code: 0,
},
},
{
name: "process_output",
args: { process_id: "storybook-process" },
result: { output: "dev server ready on :6006" },
},
{
name: "process_list",
args: {},
result: {
processes: [
{
id: "storybook-process",
command: "pnpm storybook",
status: "running",
},
],
},
},
{
name: "process_signal",
args: { process_id: "storybook-process", signal: "terminate" },
result: { success: true },
},
{
name: "wait_for_external_auth",
args: { provider: "github" },
result: { provider_display_name: "GitHub", authenticated: true },
},
{
name: "read_file",
args: { path: "site/src/pages/AgentsPage/AgentChatPage.tsx" },
result: { content: "export const AgentChatPage = () => null;" },
},
{
name: "write_file",
args: { path: "docs/example.md", content: "# Example\n" },
result: { path: "docs/example.md" },
},
{
name: "edit_files",
args: {
files: [
{
path: "site/src/example.ts",
edits: [{ old_text: "foo", new_text: "bar" }],
},
],
},
result: { files: [{ path: "site/src/example.ts", status: "edited" }] },
},
{
name: "list_templates",
result: {
templates: [
{
id: "template-1",
name: "go-template",
display_name: "Go Development",
},
],
count: 1,
},
},
{
name: "read_template",
args: { template_id: "template-1" },
result: {
template: { name: "go-template", display_name: "Go Development" },
},
},
{
name: "create_workspace",
result: {
created: true,
workspace_name: "agent-icons",
build_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
},
},
{
name: "start_workspace",
result: {
started: true,
workspace_name: "agent-icons",
agent_status: "ready",
build_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
},
},
{
name: "chat_summarized",
result: { summary: "Earlier transcript content was compacted." },
},
{
name: "propose_plan",
args: { path: "/home/coder/.coder/plans/PLAN-example.md" },
result: { path: "/home/coder/.coder/plans/PLAN-example.md" },
},
{
name: "ask_user_question",
args: { questions: [] },
status: "running",
},
{
name: "advisor",
args: { question: "Which icon family should represent transcript tools?" },
result: { answer: "Use category-level icons for better scanning." },
},
{
name: "computer",
args: { action: "screenshot" },
result: { output: { type: "image", data: TEST_PNG_B64 } },
},
{
name: "read_skill",
args: { name: "deep-review" },
result: {
name: "deep-review",
content: "# Deep Review\nReview code carefully.",
},
},
{
name: "read_skill_file",
args: { name: "deep-review", path: "roles/security-reviewer.md" },
result: { content: "# Security Reviewer Role\nCheck auth boundaries." },
},
{
name: "spawn_agent",
args: { title: "Repository review", prompt: "Review the code." },
result: {
chat_id: "bot-child",
title: "Repository review",
status: "completed",
},
},
{
name: "wait_agent",
args: { chat_id: "bot-child" },
result: {
chat_id: "bot-child",
title: "Repository review",
status: "completed",
report: "No issues found.",
},
},
{
name: "message_agent",
args: { chat_id: "bot-child", message: "Check icon consistency." },
result: { chat_id: "bot-child", status: "completed" },
},
{
name: "close_agent",
args: { chat_id: "bot-child" },
result: { chat_id: "bot-child", status: "completed" },
},
{
name: "spawn_computer_use_agent",
args: { prompt: "Inspect the UI." },
result: { chat_id: "desktop-child", status: "completed" },
subagentVariants: new Map([["desktop-child", "computer_use"]]),
},
{
name: "read_file",
args: { path: "site/src/pages/AgentsPage/Missing.tsx" },
status: "error",
isError: true,
result: { error: "File not found" },
},
{
name: "create_workspace",
status: "running",
args: { workspace_name: "agent-icons" },
result: {
workspace_name: "agent-icons",
build_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
},
},
{
name: "unknown_tool",
args: { example: true },
result: { ok: true },
},
];
// ---------------------------------------------------------------------------
// Execute stories
// ---------------------------------------------------------------------------
@@ -2578,3 +2789,65 @@ export const CreateWorkspaceBuildFailed: Story = {
expect(canvas.getByText("Failed to create workspace")).toBeInTheDocument();
},
};
export const AllToolIconsTranscript: Story = {
render: () => (
<ChatWorkspaceContext value={{ workspaceId: "test-workspace-id" }}>
<DesktopPanelContext.Provider
value={{ desktopChatId: "desktop-child", onOpenDesktop: fn() }}
>
<div className="flex flex-col gap-2">
<BlockList
blocks={[
{
type: "thinking",
text: "Thinking\nReviewing the available tools and grouping them by category.",
},
]}
tools={[]}
keyPrefix="all-tool-icons-thinking"
/>
{allToolShowcaseItems.map((tool, index) => (
<Tool
key={`${tool.name}-${index}`}
name={tool.name}
status={tool.status ?? "completed"}
args={tool.args}
result={tool.result}
isError={tool.isError}
killedBySignal={tool.killedBySignal}
modelIntent={tool.modelIntent}
parsedCommands={tool.parsedCommands}
subagentVariants={tool.subagentVariants}
shellToolDisplayMode="always_collapsed"
codeDiffDisplayMode="always_collapsed"
showDesktopPreviews={false}
/>
))}
</div>
</DesktopPanelContext.Provider>
</ChatWorkspaceContext>
),
parameters: {
queries: [
{
key: ["workspace", "test-workspace-id"],
data: {
id: "test-workspace-id",
latest_build: {
id: "test-build-id",
status: "running",
},
},
},
{
key: [
"workspaceBuilds",
"a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"logs",
],
data: [],
},
],
},
};
@@ -938,6 +938,10 @@ const GenericToolRenderer: FC<ToolRendererProps> = ({
mcpSlug={mcpServer?.slug}
/>
)}
</>
);
const toolHeaderStatus = (
<>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
@@ -963,7 +967,11 @@ const GenericToolRenderer: FC<ToolRendererProps> = ({
);
return (
<ToolCollapsible hasContent={hasContent} header={toolHeader}>
<ToolCollapsible
hasContent={hasContent}
header={toolHeader}
headerStatus={toolHeaderStatus}
>
{toolContent}
</ToolCollapsible>
);
@@ -17,6 +17,7 @@ interface ToolCollapsibleProps {
children: ReactNode;
header: ToolCollapsibleHeader;
headerActions?: ReactNode;
headerStatus?: ReactNode;
hasContent?: boolean;
defaultExpanded?: boolean;
expanded?: boolean;
@@ -50,6 +51,7 @@ export const ToolCollapsible: FC<ToolCollapsibleProps> = ({
children,
header,
headerActions,
headerStatus,
hasContent = true,
defaultExpanded = false,
expanded: expandedProp,
@@ -88,6 +90,7 @@ export const ToolCollapsible: FC<ToolCollapsibleProps> = ({
onClick={toggleExpanded}
>
{renderedHeader}
{headerStatus}
<ChevronDownIcon
className={cn(
"size-3 shrink-0 text-current transition-transform",
@@ -105,6 +108,7 @@ export const ToolCollapsible: FC<ToolCollapsibleProps> = ({
)}
>
{renderedHeader}
{headerStatus}
</TranscriptRow>
);
@@ -1,13 +1,14 @@
import {
BookOpenIcon,
BadgeQuestionMarkIcon,
BotIcon,
ClipboardListIcon,
FileIcon,
FilePenIcon,
LightbulbIcon,
CompassIcon,
FilePenLineIcon,
FileTextIcon,
LogInIcon,
MonitorIcon,
PlayIcon,
PlusCircleIcon,
PowerIcon,
RouteIcon,
ServerIcon,
TerminalIcon,
WrenchIcon,
} from "lucide-react";
@@ -35,7 +36,12 @@ export const ToolIcon: React.FC<{
}> = ({ name, iconUrl, isRunning, serverName, subagentIconKind }) => {
const [imgError, setImgError] = useState(false);
const color = "text-current";
const base = cn("size-4 shrink-0", color, isRunning && "grayscale");
const base = cn(
"size-4 shrink-0",
color,
"stroke-[1.5]",
isRunning && "grayscale",
);
// If an MCP icon URL is provided and hasn't failed, render it.
// Strip colour so external icons match the monochrome lucide
@@ -93,28 +99,31 @@ export const ToolIcon: React.FC<{
case "process_list":
case "process_signal":
return <TerminalIcon className={base} />;
case "wait_for_external_auth":
return <LogInIcon className={base} />;
case "read_file":
case "list_templates":
case "read_template":
return <FileIcon className={base} />;
case "read_skill":
case "read_skill_file":
return <FileTextIcon className={base} />;
case "write_file":
case "edit_files":
return <FilePenIcon className={base} />;
return <FilePenLineIcon className={base} />;
case "list_templates":
case "read_template":
case "create_workspace":
return <PlusCircleIcon className={base} />;
return <ServerIcon className={base} />;
case "start_workspace":
return <PowerIcon className={base} />;
case "chat_summarized":
return <BotIcon className={base} />;
case "propose_plan":
return <ClipboardListIcon className={base} />;
return <RouteIcon className={base} />;
case "ask_user_question":
return <BadgeQuestionMarkIcon className={base} />;
case "advisor":
return <LightbulbIcon className={base} />;
return <CompassIcon className={base} />;
case "computer":
return <MonitorIcon className={base} />;
case "read_skill":
case "read_skill_file":
return <BookOpenIcon className={base} />;
case "start_workspace":
return <PlayIcon className={base} />;
default:
return <WrenchIcon className={base} />;
@@ -36,8 +36,8 @@ const WebSearchSources: FC<WebSearchSourcesProps> = ({ sources }) => {
hasContent={unique.length > 0}
header={
<>
<GlobeIcon className="size-4 shrink-0 text-current" />
<span className="text-[13px]">
<GlobeIcon className="size-4 shrink-0 stroke-[1.5] text-current" />
<span className="text-[13px] leading-6">
Searched <span className="text-content-secondary/60">{detail}</span>
</span>
</>
@@ -16,6 +16,7 @@ import {
resolveAgentDisplayState,
} from "./displayMode";
import { AgentDisplayModeToolCollapsible } from "./ToolCollapsible";
import { ToolIcon } from "./ToolIcon";
import {
DIFFS_FONT_STYLE,
getDiffViewerOptions,
@@ -53,7 +54,12 @@ export const WriteFileTool: React.FC<{
autoDisplayState={WRITE_FILE_AUTO_DISPLAY_STATE}
header={
<>
<span className="text-[13px]">{label}</span>
<ToolIcon name="write_file" isError={isError} isRunning={isRunning} />
<span className="text-[13px] leading-6">{label}</span>
</>
}
headerStatus={
<>
{isError && (
<Tooltip>
<TooltipTrigger asChild>