mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(site): render read_skill body as markdown (#24069)
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user