Draws Claude's thinking in the chat, above the reply or tool call it led to, in place of the folded "Thought for 4s".

Draws Claude's thinking in the chat, above the reply or tool call it led to. Without it, Claude Code folds the thinking into a "Thought for 4s" line that only ctrl+o or --verbose opens.
Each thinking block draws as Claude Code draws one under --verbose: a dim italic ∴ in the gutter with the text beside it. It sits above the row that came next. That row is a reply, a tool call's own row, or a folded run of calls ("Thought for 4s, read 3 files"), which shows the thinking before each of its calls in order. Markdown in the text, mostly bold headings, is stripped so it doesn't show as stars.
Rows where Claude Code already draws thinking are left alone: under --verbose (Verbose output in /config), in the ctrl+o transcript and in unfolded groups. A subagent's thinking stays in its own transcript. Thinking cut off by a new prompt draws nowhere. Only the terminal draws it; other surfaces are left as they are.
Known gap: in the ctrl+o transcript, thinking before a reply draws twice, once by Claude Code and once by the mod. A reply's row doesn't say when ctrl+o is open, as a tool row's does, so the mod can't tell.
What draws is the summary of the thinking that the API sends back. The API sends it only when ~/.claude/settings.json has:
"showThinkingSummaries": true
Without it there is nothing to draw.
At the prompt of a terminal session:
/plugin install inline-thinking --marketplace astrosteveo/astro-plugins
claude plugin validate plugins/inline-thinking
claude plugin test plugins/inline-thinkinghooks/register.tsx 141 lines1import { atom, memberOf, read, update } from 'claude-code'
2import type { EngineInterface, Register, RenderInput, RenderNode, SessionAppendInput } from 'claude-code'
3
4const pending = atom({ plugin: 'inline-thinking', key: 'pending' } as const, [], { shape: 'pending-1' })
5const thoughts = atom({ plugin: 'inline-thinking', key: 'thoughts' } as const, [])
6const isVerbose = atom({ plugin: 'inline-thinking', key: 'isVerbose' } as const, false)
7
8// Each thinking block waits for the next thing the model does, a reply or a
9// tool call, and is filed under that row's id: the row draws it above itself.
10async function file($: EngineInterface, e: SessionAppendInput) {
11 const said = e.message.content.flatMap(block =>
12 block.type === 'thinking' && typeof block.thinking === 'string' && block.thinking.trim() !== ''
13 ? [block.thinking.trim()]
14 : [],
15 )
16 if (said.length > 0) {
17 await update($, pending, list => [...list, ...said])
18 }
19 const done = e.message.content.find(block => block.type === 'text' || block.type === 'tool_use')
20 if (done === undefined) {
21 return
22 }
23 const held = await read($, pending)
24 if (held.length === 0) {
25 return
26 }
27 const id = done.type === 'tool_use' && typeof done.id === 'string' ? done.id : e.uuid
28 await update($, memberOf(thoughts, { requestId: id }), () => held)
29 await update($, pending, () => [])
30}
31
32// The summary's own markup, bold headings mostly, would show as stars.
33function paragraphs(thought: string) {
34 return thought
35 .split(/\n\s*\n/)
36 .map(part => part.replace(/\*\*|__/g, '').replace(/\s+/g, ' ').trim())
37 .filter(part => part !== '')
38}
39
40// Drawn as the engine draws a thinking block under --verbose: a dim italic ∴
41// in the two-column gutter, the text beside it.
42function drawn($: EngineInterface, e: RenderInput, said: string[], row: RenderNode) {
43 const { Box, Text } = $.ui.resolve(e)
44
45 return (
46 <Box flexDirection="column">
47 {said.map((thought, n) => (
48 <Box key={`thought-${n}`} flexDirection="row" marginTop={1} width="100%">
49 <Box minWidth={2}>
50 <Text dimColor italic>
51 ∴
52 </Text>
53 </Box>
54 <Box flexDirection="column" flexGrow={1} gap={1}>
55 {paragraphs(thought).map((part, m) => (
56 <Text key={`part-${m}`} dimColor italic>
57 {part}
58 </Text>
59 ))}
60 </Box>
61 </Box>
62 ))}
63 {row}
64 </Box>
65 )
66}
67
68export const register: Register = on => {
69 on('session.start', async ($, e, next) => {
70 const started = await next(e)
71 const rows = await $.config.list()
72 const verbose = rows.find(row => row.key === 'verbose')?.value === true
73 await update($, isVerbose, () => verbose)
74
75 return started
76 })
77
78 on('config.set', { key: 'verbose' }, async ($, e, next) => {
79 const set = await next(e)
80 if (set.deny === undefined) {
81 await update($, isVerbose, () => set.value === true)
82 }
83
84 return set
85 })
86
87 // A subagent's rows draw in its own transcript, not here.
88 on('session.append', { door: 'response' }, async ($, e, next) => {
89 if (e.agentId === undefined) {
90 await file($, e)
91 }
92
93 return next(e)
94 })
95
96 // Thinking that nothing followed (an Esc mid-thought) ends with the turn.
97 on('session.append', { door: 'prompt' }, async ($, e, next) => {
98 await update($, pending, () => [])
99
100 return next(e)
101 })
102
103 on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
104 const said = await read($, memberOf(thoughts, e))
105 if (e.surface !== 'terminal' || said.length === 0 || (await read($, isVerbose))) {
106 return next(e)
107 }
108
109 return drawn($, e, said, await next(e))
110 })
111
112 // The ctrl+o transcript and --verbose draw thinking themselves.
113 on('ui.render', { component: 'ToolUse' }, async ($, e, next) => {
114 const said = await read($, memberOf(thoughts, e))
115 if (e.surface !== 'terminal' || said.length === 0 || e.props.isExpanded === true) {
116 return next(e)
117 }
118
119 return drawn($, e, said, await next(e))
120 })
121
122 // A folded run of calls draws the thinking before each of them above its
123 // one line ("Thought for 15s, read 3 files").
124 on('ui.render', { component: 'ToolGroup' }, async ($, e, next) => {
125 if (e.surface !== 'terminal' || e.props.isExpanded) {
126 return next(e)
127 }
128 const said: string[] = []
129 for (const call of e.props.calls) {
130 if (call.tool_use_id !== undefined) {
131 said.push(...(await read($, memberOf(thoughts, { requestId: call.tool_use_id }))))
132 }
133 }
134 if (said.length === 0) {
135 return next(e)
136 }
137
138 return drawn($, e, said, await next(e))
139 })
140}
141types/index.d.ts 19 lines1/** One thinking block's text, as the model's summary of it came back. */
2export type Thought = string
3
4declare module 'claude-code' {
5 interface PluginState {
6 'inline-thinking': {
7 /** Thinking the model did since the last thing it did: not drawn yet. */
8 pending: Shaped<Thought[]>
9 /**
10 * The thinking drawn above one row: a reply's by its row id, a tool
11 * call's by its tool_use_id.
12 */
13 thoughts: StateFamily<Thought[]>
14 /** Verbose mode is on, so the engine draws thinking itself. */
15 isVerbose: boolean
16 }
17 }
18}
19