SLOPSHOPPER

inline-thinking

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

newrows
A shopper browsing a rack in a slop shop
README

inline-thinking

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.

Setup

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.

Install

At the prompt of a terminal session:

/plugin install inline-thinking --marketplace astrosteveo/astro-plugins

Test

claude plugin validate plugins/inline-thinking
claude plugin test plugins/inline-thinking
Source 2 files
hooks/register.tsx 141 lines
1import { 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}
141
types/index.d.ts 19 lines
1/** 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