feat(site): render read_skill body as markdown (#24069)

This commit is contained in:
Kyle Carberry
2026-04-07 11:50:21 -04:00
committed by GitHub
parent 6e5335df1e
commit cffc68df58
3 changed files with 127 additions and 0 deletions
@@ -0,0 +1,63 @@
import { BookOpenIcon, LoaderIcon, TriangleAlertIcon } from "lucide-react";
import type React from "react";
import { ScrollArea } from "#/components/ScrollArea/ScrollArea";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "#/components/Tooltip/Tooltip";
import { cn } from "#/utils/cn";
import { Response } from "../Response";
import { ToolCollapsible } from "./ToolCollapsible";
import type { ToolStatus } from "./utils";
export const ReadSkillTool: React.FC<{
label: string;
body: string;
status: ToolStatus;
isError: boolean;
errorMessage?: string;
}> = ({ label, body, status, isError, errorMessage }) => {
const hasContent = body.length > 0;
const isRunning = status === "running";
return (
<ToolCollapsible
className="w-full"
hasContent={hasContent}
header={
<>
<BookOpenIcon className="h-4 w-4 shrink-0 text-content-secondary" />
<span className={cn("text-sm", "text-content-secondary")}>
{isRunning ? `Reading ${label}…` : `Read ${label}`}
</span>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
</TooltipTrigger>
<TooltipContent>
{errorMessage || "Failed to read skill"}
</TooltipContent>
</Tooltip>
)}
{isRunning && (
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-content-secondary" />
)}
</>
}
>
{body && (
<ScrollArea
className="mt-1.5 rounded-md border border-solid border-border-default"
viewportClassName="max-h-64"
scrollBarClassName="w-1.5"
>
<div className="px-3 py-2">
<Response>{body}</Response>
</div>
</ScrollArea>
)}
</ToolCollapsible>
);
};
@@ -1342,6 +1342,12 @@ export const ReadSkillCompleted: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText(/Read skill deep-review/)).toBeInTheDocument();
// Expand the collapsible to verify markdown body renders.
const toggle = canvas.getByRole("button");
await userEvent.click(toggle);
await waitFor(() => {
expect(canvas.getByText("Deep Review Skill")).toBeInTheDocument();
});
},
};
@@ -1393,6 +1399,12 @@ export const ReadSkillFileCompleted: Story = {
expect(
canvas.getByText(/Read deep-review\/roles\/security-reviewer\.md/),
).toBeInTheDocument();
// Expand the collapsible to verify markdown content renders.
const toggle = canvas.getByRole("button");
await userEvent.click(toggle);
await waitFor(() => {
expect(canvas.getByText("Security Reviewer Role")).toBeInTheDocument();
});
},
};
@@ -23,6 +23,7 @@ import { ListTemplatesTool } from "./ListTemplatesTool";
import { ProcessOutputTool } from "./ProcessOutputTool";
import { ProposePlanTool } from "./ProposePlanTool";
import { ReadFileTool } from "./ReadFileTool";
import { ReadSkillTool } from "./ReadSkillTool";
import { ReadTemplateTool } from "./ReadTemplateTool";
import { SubagentTool } from "./SubagentTool";
import { ToolCollapsible } from "./ToolCollapsible";
@@ -210,6 +211,55 @@ const ReadFileRenderer: FC<ToolRendererProps> = ({
);
};
const ReadSkillRenderer: FC<ToolRendererProps> = ({
status,
args,
result,
isError,
}) => {
const parsedArgs = parseArgs(args);
const skillName = parsedArgs ? asString(parsedArgs.name) : "";
const rec = asRecord(result);
const body = rec ? asString(rec.body) : "";
return (
<ReadSkillTool
label={skillName ? `skill ${skillName}` : "skill"}
body={body}
status={status}
isError={isError}
errorMessage={rec ? asString(rec.error || rec.message) : undefined}
/>
);
};
const ReadSkillFileRenderer: FC<ToolRendererProps> = ({
status,
args,
result,
isError,
}) => {
const parsedArgs = parseArgs(args);
const skillName = parsedArgs ? asString(parsedArgs.name) : "";
const filePath = parsedArgs ? asString(parsedArgs.path) : "";
const label =
skillName && filePath
? `${skillName}/${filePath}`
: skillName || filePath || "skill file";
const rec = asRecord(result);
const content = rec ? asString(rec.content) : "";
return (
<ReadSkillTool
label={label}
body={content}
status={status}
isError={isError}
errorMessage={rec ? asString(rec.error || rec.message) : undefined}
/>
);
};
const WriteFileRenderer: FC<ToolRendererProps> = ({
status,
args,
@@ -667,6 +717,8 @@ const toolRenderers: Record<string, FC<ToolRendererProps>> = {
create_workspace: CreateWorkspaceRenderer,
list_templates: ListTemplatesRenderer,
read_template: ReadTemplateRenderer,
read_skill: ReadSkillRenderer,
read_skill_file: ReadSkillFileRenderer,
spawn_agent: SubagentRenderer,
wait_agent: SubagentRenderer,
message_agent: SubagentRenderer,