SLOPSHOPPER

thinking-band

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

newband
v0.1.0MITupdated 2026-10-04orfevre-34/thinking-band
A shopper browsing a rack in a slop shop
README

thinking-band

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)
╭──────────────────────────────────────────────╮
│ >                                            │
╰──────────────────────────────────────────────╯

What it does

  • Shows only the latest thinking block and keeps its tail in view while it streams. A new block replaces the previous one.
  • Keeps showing the last thought while tools run.
  • Hides when Claude starts writing its reply. You can make it wait for the end of the turn instead (see Settings).
  • Always hides when the turn ends, interrupts (Esc) included.
  • Covers the main conversation only. Subagent thinking is never shown, and a subagent finishing does not hide the main thread's thinking.
  • Leaves everything else as it was:
  • Whatever other mods draw in the band stays where it is. This mod only adds its box on top.
  • It only observes the response stream and passes every chunk and the step result through unchanged.
  • Claude Code's own thinking view (Ctrl+O) is untouched.

Requirements

  • Claude Code 2.1.287 or newer (function-hook mods). Developed and tested on 2.1.289.
  • "showThinkingSummaries": true in your ~/.claude/settings.json. Without it, thinking arrives empty and the band never shows.
  • An interactive session: the terminal (CLI) or the desktop app's Code tab. Headless runs (claude -p) receive empty thinking and have no band, so nothing is shown there.

Install

From the marketplace

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.

From a local folder

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" }
}

Settings

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 }
    }
  }
}
OptionValuesDefaultMeaning
hideOntext \turn-endtexttext: 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.
maxLinesnumber6The most thinking rows to show. The box frame and header take three more rows. The band's available height can lower this further.

Development

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:

  • thinking → tool → reply → done
  • the turn-end mode
  • interrupts
  • subagents mixed into the main turn
  • the row limit and line wrapping
  • leaving other mods' band content intact

Layout:

.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

Notes

  • The thinking text is the summarized thinking the model returns, the same text Ctrl+O shows.
  • Lines are wrapped by display width. Wide characters (CJK, full-width forms, emoji with Emoji_Presentation) count as two cells. ZWJ emoji sequences are counted wider than they render.

License

MIT

Source 2 files
hooks/register.tsx 135 lines
1import 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}
135
types/index.d.ts 9 lines
1// メイン会話の最新の思考ブロック。'' は非表示。
2export type ThinkingText = string
3
4declare module 'claude-code' {
5  interface PluginState {
6    'thinking-band': { text: ThinkingText }
7  }
8}
9