Thinking steps
An assistant’s reasoning and tool calls as a trace that opens while it runs and says how long it took.
Done #
- Planned the answer done 0.9s
- Searched the web done 2.3sFound four sources on tide tables.
- Read the sources done 1.1s
export const ThinkingStepsStyle = forSlots(ThinkingStepsSlots)(Recipes.ThinkingSteps({}))
export const THOUGHT: ReadonlyArray<ThinkingStep> = [
{ id: 'plan', title: 'Planned the answer', state: 'done', duration: 900 },
{
id: 'search',
title: 'Searched the web',
state: 'done',
duration: 2300,
detail: 'Found four sources on tide tables.',
},
{ id: 'read', title: 'Read the sources', state: 'done', duration: 1100 },
]
// Shut once done, saying how long it took; a press opens it. `open` is the
// Model's: none follows the work, and the press sends what the reader asked.
export const thinkingSteps = <Message>(
style: NamedStyle<typeof ThinkingStepsSlots>,
open: Option.Option<boolean>,
onToggle: (open: boolean) => Message,
h: HtmlBuilder<Message>,
): Html =>
h.div(
[h.Style({ inlineSize: 'min(28rem, 100%)' })],
[ThinkingSteps.view({ id: 'preview-thought', steps: THOUGHT, open, onToggle, style }, h)],
)Running #
- Searched the web done 2.3s
- Fetching tides.example failed 0.4s
- Reading 3 sources running
- Write the answer pending
export const THINKING: ReadonlyArray<ThinkingStep> = [
{ id: 'search', title: 'Searched the web', state: 'done', duration: 2300 },
{ id: 'fetch', title: 'Fetching tides.example', state: 'failed', duration: 400 },
{ id: 'read', title: 'Reading 3 sources', state: 'running' },
{ id: 'write', title: 'Write the answer', state: 'pending' },
]
// Open while it runs, its summary the running step's title, shimmering.
export const thinkingStepsRunning = <Message>(
style: NamedStyle<typeof ThinkingStepsSlots>,
open: Option.Option<boolean>,
onToggle: (open: boolean) => Message,
h: HtmlBuilder<Message>,
): Html =>
h.div(
[h.Style({ inlineSize: 'min(28rem, 100%)' })],
[ThinkingSteps.view({ id: 'preview-thinking', steps: THINKING, open, onToggle, style }, h)],
)Tool calls #
→ { "city": "Lisbon" }
← { "high": 24, "sky": "clear" }→ { "day": "Saturday" }
← 2 events- Work calendar done 0.5s
- Home calendar done 0.7s
export const ToolStepsStyle = forSlots(ThinkingStepsSlots)(
Recipes.ThinkingSteps({ variant: 'card' }),
)
const toolCall = <Message>(h: HtmlBuilder<Message>, input: string, output: string): Html =>
h.pre(
[h.Style({ margin: '0', whiteSpace: 'pre-wrap', fontSize: '0.8125rem' })],
[`→ ${input}`, h.br([]), `← ${output}`],
)
/** Built per builder: a tool call's detail is markup. */
export const toolSteps = <Message>(h: HtmlBuilder<Message>): ReadonlyArray<ThinkingStep> => [
{
id: 'weather',
title: 'get_weather',
state: 'done',
duration: 640,
detail: toolCall(h, '{ "city": "Lisbon" }', '{ "high": 24, "sky": "clear" }'),
},
{
id: 'calendar',
title: 'list_events',
state: 'done',
duration: 1200,
detail: toolCall(h, '{ "day": "Saturday" }', '2 events'),
steps: [
{ id: 'calendar-work', title: 'Work calendar', state: 'done', duration: 500 },
{ id: 'calendar-home', title: 'Home calendar', state: 'done', duration: 700 },
],
},
]
// Each tool call's input and output under its own row, which a press shows
// and hides (`Accordion.toggled(openSteps, id, { multiple: true })`).
export const thinkingStepsTools = <Message>(
style: NamedStyle<typeof ThinkingStepsSlots>,
open: Option.Option<boolean>,
onToggle: (open: boolean) => Message,
openSteps: ReadonlyArray<string>,
onToggleStep: (id: string) => Message,
h: HtmlBuilder<Message>,
): Html =>
h.div(
[h.Style({ inlineSize: 'min(28rem, 100%)' })],
[
ThinkingSteps.view(
{
id: 'preview-tools',
steps: toolSteps(h),
open,
onToggle,
summary: 'Used 2 tools',
openSteps,
onToggleStep,
style,
},
h,
),
],
)Usage
Each example above names the function that drew it, over the theme module Create a theme writes. The preview passes it the style picked above instead of its default.