Shows Claude's latest thinking block above the prompt while a turn runs

A Claude Code mod that keeps Claude's latest thinking visible in a band above the prompt while a turn runs. You no longer have to press Ctrl+O to see what Claude is thinking right now.
╭──────────────────────────────────────────────╮
│ ✻ Thinking │
│ I should check the types first, then… │
│ The probe showed thinking arrives live. │
│ Next I'll update the render hook ▍ │
╰──────────────────────────────────────────────╯
(other mods' rows in the band stay below)
╭──────────────────────────────────────────────╮
│ > │
╰──────────────────────────────────────────────╯
Esc) included.Ctrl+O) is untouched."showThinkingSummaries": true in your ~/.claude/settings.json. Without it, thinking arrives empty and the band never shows.claude -p) receive empty thinking and have no band, so nothing is shown there.This repository is also a plugin marketplace. Add it and install the plugin:
claude plugin marketplace add orfevre-34/thinking-band
claude plugin install thinking-band@thinking-band
You can also do this inside Claude Code with /plugin marketplace add orfevre-34/thinking-band, then install it from /plugin.
To update later:
claude plugin marketplace update thinking-band
claude plugin update thinking-band@thinking-band
Then restart Claude Code, or run /reload-plugins in a running session.
Clone the repository and point Claude Code at the folder:
git clone https://github.com/orfevre-34/thinking-band.git
claude --plugin-dir /path/to/thinking-band
Where you cannot pass a flag (for example, a session the desktop app starts), list the folder in CLAUDE_CODE_PLUGIN_DIRS. You can set it in the process environment or in the env block of ~/.claude/settings.json:
{
"env": { "CLAUDE_CODE_PLUGIN_DIRS": "/path/to/thinking-band" }
}
Both options show up as rows in /config. They can also be set in settings.json:
{
"pluginConfigs": {
"thinking-band": {
"options": { "hideOn": "turn-end", "maxLines": 8 }
}
}
}
| Option | Values | Default | Meaning | |
|---|---|---|---|---|
hideOn | text \ | turn-end | text | text: hide as soon as reply text starts streaming. It comes back if Claude thinks again later in the turn. turn-end: keep showing until the turn ends. |
maxLines | number | 6 | The most thinking rows to show. The box frame and header take three more rows. The band's available height can lower this further. |
claude plugin validate . # manifest and hooks module, as the engine reads them
claude plugin test . # tests/*.test.tsx against the engine
The tests cover these cases, on both the terminal and desktop surfaces:
turn-end modeLayout:
.claude-plugin/plugin.json manifest and settings (userConfig)
hooks/register.tsx the hooks module
types/index.d.ts the $.state contract
tests/ claude plugin test suite
Ctrl+O shows.Emoji_Presentation) count as two cells. ZWJ emoji sequences are counted wider than they render.hooks/register.tsx 135 lines1import type { EngineInterface, Register } from 'claude-code'
2
3// メイン会話の最新の思考ブロック。'' のとき非表示。
4const THINKING = { plugin: 'thinking-band', key: 'text' } as const
5
6const ACCENT = '#D97757'
7const CURSOR = '▍'
8// 枠(上下・左右)と見出し、左右の余白とカーソルの分
9const CHROME_ROWS = 3
10const CHROME_COLUMNS = 6
11
12function show($: EngineInterface, text: string) {
13 return $.state.set(THINKING, text)
14}
15
16// 端末上の表示幅(全角 2、それ以外 1)。
17// ponytail: CJK・全角の主要範囲と Emoji_Presentation だけの近似。結合文字や ZWJ 絵文字はずれるので、困ったら east-asian-width 表に置き換える
18function charWidth(ch: string) {
19 return /[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-가-힣豈-︰-﹏-⦆¢-₩\u{20000}-\u{3FFFD}\p{Emoji_Presentation}]/u.test(ch) ? 2 : 1
20}
21
22function strWidth(s: string) {
23 let w = 0
24 for (const ch of s) w += charWidth(ch)
25 return w
26}
27
28// text を幅 width で折り返し、空行を除いた末尾 rows 行を返す。英文は空白で折り返す。
29function tailRows(text: string, width: number, rows: number) {
30 const out: string[] = []
31 for (const para of text.split('\n')) {
32 let row = ''
33 let w = 0
34 for (const ch of para) {
35 const cw = charWidth(ch)
36 if (w + cw > width) {
37 const cut = row.lastIndexOf(' ')
38 out.push(cut > 0 ? row.slice(0, cut) : row)
39 row = cut > 0 ? row.slice(cut + 1) : ''
40 w = strWidth(row)
41 }
42 row += ch
43 w += cw
44 }
45 out.push(row)
46 }
47 return out.map(r => r.trim()).filter(r => r !== '').slice(-rows)
48}
49
50export const register: Register = (on, options) => {
51 const hideOnText = options.hideOn !== 'turn-end'
52 const maxLines = Math.max(1, Math.floor(Number(options.maxLines)) || 6)
53
54 // リロード直後や新しいターンに前回の思考を残さない
55 on('session.start', async ($, e, next) => {
56 await show($, '')
57 return next(e)
58 })
59
60 on('turn.start', async ($, e, next) => {
61 await show($, '')
62 return next(e)
63 })
64
65 // 観測のみ: チャンクは受け取ったものをそのまま流す
66 on('turn.step', async function* ($, e, next) {
67 if (e.agentId) return yield* next(e)
68
69 let index = -1
70 let thinking = ''
71 let hidden = false
72 const stream = next(e)
73 for await (const c of stream) {
74 if (c.kind === 'thinking') {
75 if (c.index !== index) {
76 index = c.index
77 thinking = ''
78 }
79 thinking += c.text
80 if (thinking.trim()) {
81 hidden = false
82 await show($, thinking)
83 }
84 } else if (c.kind === 'text' && hideOnText && !hidden) {
85 hidden = true
86 await show($, '')
87 }
88 yield c
89 }
90 // 何も返さないと上位に結果が届かないため、下位の結果をそのまま返す
91 return await stream.result
92 })
93
94 // 中断(reason: 'aborted')を含むすべてのターン終了で消す。サブエージェントのターン終了は無視する
95 on('turn.complete', async ($, e, next) => {
96 if (!e.agentId) await show($, '')
97 return next(e)
98 })
99
100 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
101 // 他の mod やエンジンが描いたものは残し、その上に足す
102 const below = await next(e)
103 const { value: text = '' } = await $.state.get(THINKING)
104 // 枠の上下と見出しの 3 行を除いた残りが本文の行数
105 const lines = Math.min(maxLines, e.props.maxRows - CHROME_ROWS)
106 if (!text || !e.props.isWorking || e.props.hasSurvey || lines <= 0) return below
107
108 const rows = tailRows(text, Math.max(10, e.props.bodyColumns - CHROME_COLUMNS), lines)
109 const { Box, Text } = $.ui.resolve(e)
110
111 // 古い行は薄く、最新の行だけ少し明るくしてカーソルを付ける
112 return (
113 <Box flexDirection="column">
114 <Box key="thinking" flexDirection="column" borderStyle="round" borderColor="gray" borderDimColor paddingX={1}>
115 <Text color={ACCENT} bold>
116 ✻ Thinking
117 </Text>
118 {rows.map((row, i) =>
119 i === rows.length - 1 ? (
120 <Text key={`row${i}`} color="gray" italic wrap="truncate-end">
121 {row} <Text color={ACCENT}>{CURSOR}</Text>
122 </Text>
123 ) : (
124 <Text key={`row${i}`} color="gray" dimColor italic wrap="truncate-end">
125 {row}
126 </Text>
127 ),
128 )}
129 </Box>
130 {below}
131 </Box>
132 )
133 })
134}
135types/index.d.ts 9 lines1// メイン会話の最新の思考ブロック。'' は非表示。
2export type ThinkingText = string
3
4declare module 'claude-code' {
5 interface PluginState {
6 'thinking-band': { text: ThinkingText }
7 }
8}
9