.agents/skills/builtin-tool/references/ui/render.md
Lifecycle: rendered once the result arrives (after Placeholder/Streaming hand off). Sits below the Inspector header.
Skip if the API is read-only or the result is just text — the framework already shows the executor's content string. Add a Render only when there's a structured artifact worth seeing: a card, a chart, a diff, a list of files.
BuiltinRenderProps<Args, State, Content>)interface BuiltinRenderProps<Arguments = any, State = any, Content = any> {
apiName?: string;
args: Arguments; // final params from the LLM
content: Content; // executor's content string (or parsed)
identifier?: string;
messageId: string; // for store lookups
pluginError?: any; // from BuiltinToolResult.error
pluginState?: State; // executor's state
toolCallId?: string;
}
Pattern A — Single-file Render (web-browsing CrawlSinglePage):
// client/Render/CrawlSinglePage.tsx
import type { BuiltinRenderProps, CrawlPluginState, CrawlSinglePageQuery } from '@lobechat/types';
import { memo } from 'react';
import PageContent from './PageContent';
const CrawlSinglePage = memo<BuiltinRenderProps<CrawlSinglePageQuery, CrawlPluginState>>(
({ messageId, pluginState, args }) => (
<PageContent messageId={messageId} results={pluginState?.results} urls={[args?.url]} />
),
);
export default CrawlSinglePage;
Pattern B — Folder with subcomponents (web-browsing Search):
client/Render/Search/
├── index.tsx # composes the subcomponents, handles error states
├── ConfigForm.tsx # appears when pluginError.type === 'PluginSettingsInvalid'
├── SearchQuery.tsx # editable query header
└── SearchResult.tsx # result list
Use Pattern B when the Render has internal state (editing mode, expanded items), error variants, or is large enough to benefit from splitting.
Renders are the canonical place to surface pluginError because the chat doesn't auto-render typed errors:
if (pluginError) {
if (pluginError?.type === 'PluginSettingsInvalid') {
return <ConfigForm id={messageId} provider={pluginError.body?.provider} />;
}
return (
<Alert
title={pluginError?.message}
type="error"
extra={<Highlighter language="json">{JSON.stringify(pluginError.body, null, 2)}</Highlighter>}
/>
);
}
null if there's nothing useful to draw yet (avoids empty cards during stream).pluginState for server-truth (ids, counts, server-assigned status) and args for what the LLM asked. Combine — neither alone is enough.@lobehub/ui/base-ui (createModal, useModalContext, confirmModal) — see the modal skill.client/Render/index.tsimport type { BuiltinRender } from '@lobechat/types';
import { TaskApiName } from '../../types';
import CreateTaskRender from './CreateTask';
import RunTasksRender from './RunTasks';
export const TaskRenders: Record<string, BuiltinRender> = {
[TaskApiName.createTask]: CreateTaskRender as BuiltinRender,
[TaskApiName.runTasks]: RunTasksRender as BuiltinRender,
/* only the APIs with rich result UI — others fall back to text content */
};
export { default as CreateTaskRender } from './CreateTask';
export { default as RunTasksRender } from './RunTasks';
If the Render should hide for certain results (e.g. ClaudeCode's TodoWrite hides when the agent is mid-stream), add a RenderDisplayControl to packages/builtin-tools/src/displayControls.ts. See ClaudeCodeRenderDisplayControls for the pattern.