Thinking Control Flow
[Created by Codex: 019bd742-f29e-70a1-ae7a-70c54d7277f4] [Edited by Codex: 019bd742-f29e-70a1-ae7a-70c54d7277f4]
Last verified: claude-code-2.1.11, 2026-01-20
5-minute Fix TL;DR
If thinking “doesn’t stream,” it’s almost always collapsed UI. Apply this one-liner:
<details open={isCurrentlyThinking || isExpanded}>
Table of Contents
- Key Insight
- Quick Fixes (Solution First)
- Persistence Options
- Data Flow Diagram
- Validation Checklist
- Per-block vs Global Expansion
- Thinking Token Budget Mapping
- Debug Commands + Expected Output
- Diagnostics Table
- Warnings + Rollback
- Thinking Code Line Map
- Re-verify Line Numbers
Key Insight
Thinking already streams; it looks “non‑streaming” because <details> is collapsed by default. Fixing the open logic reveals live deltas.
Quick Fixes (Solution First)
1) Auto‑expand while streaming (shows live deltas)
File: src/public/webview/index.js
Function: xZe
Line: ~238487
// BEFORE
<details open={isExpanded}>
// AFTER (streaming visible, user toggle still respected)
<details open={isCurrentlyThinking || isExpanded}>
Line anchors from investigation:
xZerender block atsrc/public/webview/index.js:238487.- Streaming deltas append at
src/public/webview/index.js:171450. content_block_deltatriggersupdated()atsrc/public/webview/index.js:171377.- Default expand state initialized at
src/public/webview/index.js:240540.
2) Enable thinking by default for new sessions
File: src/server.py
Where: init response and launch fallback
# init_response (search for "init_response")
"thinkingLevel": "default_on"
# launch (search for "launch" or "thinking_level")
thinking_level = options.get("thinkingLevel") or "default_on"
Important: If you keep seeing thinkingLevel: "off" in logs, you are likely hitting an older process. Start with --kill-port or verify the PID bound to 8787.
3) Always expanded (never collapse)
<details open={true}>
4) Hide thinking until complete (no streaming view)
if (isCurrentlyThinking) return null;
Persistence Options (keep user preference across reloads)
A) Use acquireVsCodeApi state (recommended)
File: src/public/webview/index.js
Where: initialize thinkingLevelOverride
// NOTE: This code must run inside the session class (Vk) where
// `this.thinkingLevelOverride` is defined. Do not paste at module scope.
// Read on boot (inside session constructor or init)
const api = acquireVsCodeApi();
const stored = api.getState()?.thinkingLevelOverride;
if (stored && !this.thinkingLevelOverride.value) {
this.thinkingLevelOverride.value = stored;
}
// Write on toggle (inside setThinkingLevel method)
api.setState({ ...api.getState(), thinkingLevelOverride: nextLevel });
B) Use localStorage (shim‑only)
Exact code to persist thinking preference using localStorage
File: src/public/webview/index.js
Where: near the toggle handler and session init
// NOTE: Same constraint—must be inside session class (Vk) for `this` access.
// Read on boot (inside session constructor or init)
const storedThinkingLevel = localStorage.getItem("thinkingLevelOverride");
if (storedThinkingLevel && !this.thinkingLevelOverride.value) {
this.thinkingLevelOverride.value = storedThinkingLevel;
}
// Write on toggle (inside setThinkingLevel method)
localStorage.setItem("thinkingLevelOverride", nextLevel);
Note: src/public/vscode-shim.js already stores acquireVsCodeApi().setState in
localStorage under claude-webview-state, so either approach works.
Data Flow Diagram (ASCII)
CLI (stream-json) Shim (server.py) Webview (index.js)
| stream_event | from-extension | window.postMessage
| content_block_start ----> io_message ------------> assembler.processStreamEvent
| type: thinking | xZe renders <details>
| content_block_delta ---> io_message ------------> yet(): thinking_delta
| thinking_delta updated() bumps key
Sequence Diagrams
[Edited by Opus: 933941fe-4c97-4da0-a807-96b2b63e9f03]
1. Session Init (how thinking level is set on startup)
┌─────────┐ ┌─────────────┐ ┌─────────────┐
│ Browser │ │ server.py │ │ index.js │
└────┬────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│ GET / │ │
│─────────────────────>│ │
│ │ │
│ index.html + JS │ │
│<─────────────────────│ │
│ │ │
│ EventSource /events │ │
│─────────────────────>│ │
│ │ │
│ SSE: init_response │ │
│ {thinkingLevel: │ │
│ "default_on"} │ │
│<─────────────────────│ │
│ │ │
│ │ postMessage │
│ │ {type:"response", │
│ │ response:{type: │
│ │ "init_response"}} │
│ │ ─────────────────────> │
│ │ │
│ │ │ store in
│ │ │ state.thinkingLevel
│ │ ▼
Key file:line refs:
server.py:1091— init_response thinkingLevelindex.js:242940— message bridge receives postMessageindex.js:171553— computed thinkingLevel stored
2. Thinking Stream (deltas flowing from CLI to UI)
┌─────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────┐
│ CLI │ │ server.py │ │ vscode-shim │ │ index.js│
│(claude) │ │ (SSE) │ │ .js │ │ (xZe) │
└────┬────┘ └──────┬──────┘ └──────┬──────┘ └────┬────┘
│ │ │ │
│ stream_event │ │ │
│ content_block_start │ │
│ {type:"thinking"} │ │ │
│──────────────────>│ │ │
│ │ │ │
│ │ SSE: io_message │ │
│ │ wrapped in │ │
│ │ {type:"from-extension"} │
│ │────────────────────>│ │
│ │ │ │
│ │ │ postMessage │
│ │ │──────────────────>│
│ │ │ │
│ │ │ │ assembler
│ │ │ │ creates block
│ │ │ ▼
│ stream_event │ │ │
│ content_block_delta │ │
│ {thinking_delta: │ │ │
│ "Let me..."} │ │ │
│──────────────────>│ │ │
│ │ │ │
│ │ SSE: io_message │ │
│ │────────────────────>│ │
│ │ │ postMessage │
│ │ │──────────────────>│
│ │ │ │
│ │ │ │ yet() appends
│ │ │ │ i.thinking +=
│ │ │ │ updated()
│ │ │ ▼
│ (repeat deltas) │ │ │
│ ═══════════════════════════════════════════════════════════>│
│ │ │ │
│ stream_event │ │ │
│ content_block_stop│ │ │
│──────────────────>│ │ │
│ │────────────────────>│──────────────────>│
│ │ │ │
│ │ │ │ isCurrently
│ │ │ │ Thinking=false
│ │ │ ▼
Key file:line refs:
index.js:171377— content_block_delta triggersupdated()index.js:171450—thinking_deltaappended toi.thinkingindex.js:238487—xZerenders<details open={isCurrentlyThinking || isExpanded}>
3. Toggle Thinking Level (user clicks toggle)
┌─────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────┐
│ User │ │ index.js │ │ server.py │ │ CLI │
│ (click) │ │ (webview) │ │ (shim) │ │(claude) │
└────┬────┘ └──────┬──────┘ └──────┬──────┘ └────┬────┘
│ │ │ │
│ click toggle │ │ │
│──────────────────>│ │ │
│ │ │ │
│ │ postMessage │ │
│ │ {type:"request", │ │
│ │ request:{type: │ │
│ │ "set_thinking_level", │
│ │ thinkingLevel:"off"}} │
│ │────────────────────>│ │
│ │ │ │
│ │ │ control_request │
│ │ │ set_max_thinking │
│ │ │ _tokens: 0 │
│ │ │──────────────────>│
│ │ │ │
│ │ │ │ (thinking
│ │ │ │ disabled)
│ │ │ ▼
│ │ │ │
│ │ SSE: response │ │
│ │ {type:"response", │ │
│ │ response:{type: │ │
│ │ "set_thinking_level_response"}} │
│ │<────────────────────│ │
│ │ │ │
│ │ update │ │
│ │ thinkingLevelOverride │
│ ▼ │ │
Key file:line refs:
index.js:171849—setThinkingLevelupdates overrideserver.py:1276—set_thinking_levelhandlerserver.py:499—max_thinking_tokensmapping (off=0)
4. Launch New Session (with thinking level)
┌─────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────┐
│ User │ │ index.js │ │ server.py │ │ CLI │
│ (send) │ │ (webview) │ │ (shim) │ │(spawned)│
└────┬────┘ └──────┬──────┘ └──────┬──────┘ └────┬────┘
│ │ │ │
│ submit prompt │ │ │
│──────────────────>│ │ │
│ │ │ │
│ │ postMessage │ │
│ │ {type:"launch_claude", │
│ │ prompt:"...", │ │
│ │ thinkingLevel: │ │
│ │ "default_on"} │ │
│ │────────────────────>│ │
│ │ │ │
│ │ │ spawn CLI │
│ │ │ --max-thinking- │
│ │ │ tokens 31999 │
│ │ │──────────────────>│
│ │ │ │
│ │ │ (CLI starts) │
│ │ │ ▼
│ │ │ │
│ │ │ stream begins │
│ │ │<══════════════════│
│ │<════════════════════│ │
│<══════════════════│ │ │
Key file:line refs:
server.py:699— launch fallbackthinking_level = options.get("thinkingLevel") or "default_on"- CLI spawn includes
--max-thinking-tokensbased on level
5. UI Render Decision (xZe component)
┌─────────────────────────────────┐
│ xZe(props) │
│ isCurrentlyThinking: bool │
│ isExpanded: bool (global) │
│ thinking: string │
└───────────────┬─────────────────┘
│
┌───────────────▼───────────────┐
│ thinking.trim() === "" ? │
└───────────────┬───────────────┘
yes │ │ no
▼ ▼
┌──────────────┐ ┌──────────────────────────┐
│ return null │ │ <details │
│ (no render) │ │ open={isCurrently │
└──────────────┘ │ Thinking || │
│ isExpanded}> │
│ <summary>Thinking │
│ </summary> │
│ <div>{thinking}</div> │
│ </details> │
└──────────────────────────┘
│
┌────────────────────────┼────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ isCurrently │ │ !isCurrently │ │ !isCurrently │
│ Thinking=true │ │ Thinking && │ │ Thinking && │
│ │ │ isExpanded=true │ │ isExpanded=false│
│ OPEN (streaming │ │ OPEN (user │ │ CLOSED (user │
│ visible) │ │ expanded) │ │ collapsed) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Key insight: The || in open={isCurrentlyThinking || isExpanded} ensures streaming is always visible, while still respecting user toggle after completion.
Validation Checklist
-
thinking_deltaappears incli_to_extension.jsonl -
init_responseincludesthinkingLevel: "default_on" -
launch_claudeincludesthinkingLevel: "default_on" - CLI spawn args include
--max-thinking-tokens -
<details open=...>renders streaming thinking as expected
Per-block vs Global Expansion
Current behavior: global state areThinkingBlocksExpanded lives in the session view
and is passed to all thinking blocks.
Per-block approach (untested/theoretical): track expanded state per block key.
Key changes (conceptual, untested—not a complete patch):
- Change state from boolean to Map:
// BEFORE
let [S, E] = (0, qe.useState)(!1)
// AFTER
let [S, E] = (0, qe.useState)(new Map())
- Change toggle function to use blockKey:
function xc(blockKey) {
E((prev) => {
let next = new Map(prev);
next.set(blockKey, !(next.get(blockKey) ?? false));
return next;
});
}
- Wire blockKey into xZe (additional work required):
// In xZe props:
isExpanded={areThinkingBlocksExpanded.get(blockKey)} => setAreThinkingBlocksExpanded(blockKey)}
// where blockKey = content.key or message_index + block_index
Note: This requires threading blockKey through the component tree—not shown here.
Thinking Token Budget Mapping
Current src/server.py:max_thinking_tokens() implementation:
| thinkingLevel | max_thinking_tokens |
|---|---|
| off | 0 |
| (any other) | 31999 |
Note: The current code treats ALL non-off values as 31999. If you want granular levels (low/medium/high), you must modify max_thinking_tokens() to handle them:
def max_thinking_tokens(level: str) -> int:
return {
"off": 0,
"low": 10000,
"medium": 20000,
"default_on": 31999,
"high": 31999,
}.get(level, 31999)
Debug Commands (use these first)
# Check init_response thinkingLevel
rg -n "init_response|thinkingLevel" \
~/centralized-logs/claude-vscode-shim/extension_to_webview.jsonl | tail -n 1
# Check launch_claude thinkingLevel
rg -n "launch_claude|thinkingLevel" \
~/centralized-logs/claude-vscode-shim/webview_to_extension.jsonl | tail -n 1
# Confirm deltas exist
rg -n "thinking_delta|content_block_start\".*\"thinking\"" \
~/centralized-logs/claude-vscode-shim/cli_to_extension.jsonl | head -n 5
# Verify server PID bound to 8787
lsof -nP -iTCP:8787 -sTCP:LISTEN
Debug Commands: Expected Output (Examples)
- init_response:
- Expected:
"thinkingLevel":"default_on"
- Expected:
- launch_claude:
- Expected:
"thinkingLevel":"default_on"
- Expected:
- thinking_delta:
- Expected: lines containing
"thinking_delta"andcontent_block_starttype"thinking"
- Expected: lines containing
- lsof:
- Expected: one
pythonprocess listening on127.0.0.1:8787
- Expected: one
Diagnostics Table
| Symptom | Likely Cause | Fix |
|---|---|---|
| No thinking blocks at all | init default is "off" | Set thinkingLevel in init_response and launch fallback |
| Thinking exists but hidden | <details> collapsed |
Use `open={isCurrentlyThinking |
| Toggle appears on but no deltas | CLI not getting tokens | Verify --max-thinking-tokens arg and control_request |
| Changes don’t show up | Old server process | Use --kill-port or check PID with lsof |
Common Mistakes
- Editing
src/server.pybut testing an old server process on 8787. - Assuming streaming is broken when the UI is simply collapsed.
- Setting init defaults but forgetting the launch fallback.
- Editing the webview bundle in production extension (updates will overwrite changes).
- Assuming line numbers are stable across extension versions.
Warnings + Rollback
Warnings:
- Do not edit the minified extension bundle directly in production; updates overwrite changes.
- Line numbers shift between extension releases; re-verify after upgrades.
Rollback:
- Revert the
<details open=...>change to restore default collapsed behavior. - Set
thinkingLevelback to"off"ininit_responseand launch fallback to disable defaults.
Thinking Code Line Map (file:line pairs)
src/public/webview/index.js:171377content_block_delta ->updated()src/public/webview/index.js:171450thinking_deltaappendsrc/public/webview/index.js:170934lhcontent block class (key, updated)src/public/webview/index.js:238487xZethinking block rendersrc/public/webview/index.js:238562Zymemo render for content blockssrc/public/webview/index.js:240540session view state init for expand togglesrc/public/webview/index.js:171553computedthinkingLevelsrc/public/webview/index.js:171849setThinkingLevelupdates overridesrc/public/webview/index.js:242940message bridge fromwindow.postMessagesrc/public/index.css:8766thinking summary styles (class.A)src/public/index.css:8780thinking toggle styles (class.Kt)src/public/index.css:8785thinking content styles (class.Jt)src/server.py:1091init_responsethinkingLevelsrc/server.py:699launch fallbackthinking_levelsrc/server.py:499max_thinking_tokenssrc/server.py:1276set_thinking_levelhandler
Re-verify Line Numbers
Run after upgrading the extension bundle:
rg -n "xZe|thinking_delta|content_block_delta|thinkingLevelOverride" src/public/webview/index.js
rg -n "thinkingLevel|max_thinking_tokens|set_thinking_level" src/server.py
rg -n "\\.A\\b|\\.Kt\\b|\\.Jt\\b" src/public/index.css