SLOPSHOPPER

live-thinking

Stream Claude's thinking summaries inline in the conversation, with an ON/OFF toggle above the prompt.

newpanebandspinnerrowscommand
v0.1.2no licenseupdated 2026-10-02AJclemendor/my-mods/plugins/live-thinking
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · live-thinking
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /live-thinking ⎿ live-thinking: Live thinking off. ⟨Claude Code's own drawing⟩ [ Live thinking: OFF ] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⟨Claude Code's own drawing⟩ [ Live thinking: OFF ]
README

Live thinking

See Claude's thinking summary stream inline in the conversation, before the completed block appears.

/plugin marketplace add AJclemendor/my-mods
/plugin install live-thinking@my-mods
/reload-plugins

The Live thinking: ON/OFF button above the prompt controls the preview. You can also use /live-thinking, /live-thinking on, or /live-thinking off. Each session starts ON. It works independently of compact-tools, and both plugins can be loaded together.

Install sidebar-controls to move the ON/OFF button into a top-right sidebar. Thinking remains inline in the conversation; closing the sidebar returns the button above the prompt.

The preview appears after the latest user message, assistant message, or tool result. Only the toggle occupies the area above the prompt. The preview clears when the response moves to text or a tool call, finishes, fails, or is cancelled. Claude's completed thinking block remains in the normal transcript. The preview shows the main conversation's thinking; it does not mix in background subagent streams.

Enable thinking summaries

This plugin displays the summary chunks Claude supplies. It cannot display thinking text that the provider omits or has not sent yet. Enable native thinking summaries by merging these keys into your existing ~/.claude/settings.json and restarting Claude:

{
  "showThinkingSummaries": true,
  "verbose": true
}

For a single session, use claude --verbose --thinking-display summarized. See the marketplace README if your Claude build has not enabled the mod runtime.

Behavior and validation

The plugin observes turn.step and passes every chunk and the final result through unchanged. It observes session.append to position the preview without changing stored messages. It does not make model requests, modify prompts, alter thinking signatures, or write thinking to files. It retains the latest 8,000 characters for the temporary preview; the normal response stream is not truncated.

The plugin targets Claude Code 2.1.287's terminal API. Its tests cover inline rendering, incremental updates before response completion, block transitions, toggling, subagent isolation, overlapping responses, errors, cancellation, bounded previews, and preserving the response stream. Offline terminal replays verified placement after user messages, assistant text, and real Read tool results in classic and fullscreen layouts, including alongside compact-tools. Run the runtime tests with:

claude plugin validate ./plugins/live-thinking --strict
claude plugin test ./plugins/live-thinking
Source 2 files
hooks/live-thinking.mjs 164 lines
1const enabledState = { plugin: "live-thinking", key: "enabled" };
2const previewState = { plugin: "live-thinking", key: "preview" };
3const anchorState = { plugin: "live-thinking", key: "anchor" };
4const MAX_PREVIEW_CHARS = 8000;
5
6export function register(on) {
7  on("session.start", async ($, e, next) => {
8    const result = await next(e);
9    await $.state.set(previewState, null);
10    await $.state.set(anchorState, null);
11    await $.command.register({
12      name: "live-thinking",
13      description: "Toggle live thinking inline in the conversation",
14      argumentHint: "[on|off]",
15      immediate: true,
16    });
17    return result;
18  });
19
20  on("session.append", async ($, e, next) => {
21    const result = await next(e);
22    if (e.agentId || result.deny) return result;
23    const message = result.message ?? e.message;
24    const blocks = message.content;
25    if (message.type === "user" || message.type === "assistant") {
26      const last = blocks.at(-1);
27      if (last?.type === "tool_result") {
28        await $.state.set(anchorState, { kind: "tool", id: last.tool_use_id });
29      } else if (last?.type === "tool_use") {
30        await $.state.set(anchorState, { kind: "tool", id: last.id });
31      } else if (last?.type === "text" && !message.isMeta) {
32        await $.state.set(anchorState, { kind: message.type, id: e.uuid });
33      }
34    }
35    return result;
36  });
37
38  on("command.run", { command: "live-thinking" }, async ($, e) => {
39    const arg = e.args.trim().toLowerCase();
40    if (arg && arg !== "on" && arg !== "off") return { text: "Usage: /live-thinking [on|off]" };
41    const { value = true } = await $.state.get(enabledState);
42    const enabled = arg ? arg === "on" : !value;
43    await $.state.set(enabledState, enabled);
44    return { text: `Live thinking ${enabled ? "on" : "off"}.` };
45  });
46
47  on("turn.step", async function* ($, e, next) {
48    if (e.agentId) return yield* next(e);
49    let version = null;
50    let text = "";
51    let block = null;
52
53    async function publish(value, first = false) {
54      if (!first && version === null) return;
55      try {
56        const written = await $.state.set(previewState, value, first ? undefined : { ifVersion: version });
57        version = written.isSet ? written.version : null;
58      } catch {
59        // A failed preview must not drop a response chunk or stop the model stream.
60        version = null;
61      }
62    }
63
64    await publish(null, true);
65    const stream = next(e);
66    let completed = false;
67    try {
68      while (true) {
69        const entry = await stream.next();
70        if (entry.done) {
71          completed = true;
72          return entry.value;
73        }
74        const chunk = entry.value;
75        if (chunk.kind === "thinking" && chunk.text) {
76          if (chunk.index !== block) {
77            text = "";
78            block = chunk.index;
79          }
80          text = previewTail(text + chunk.text);
81          await publish({ text });
82        } else if (text && (chunk.kind === "text" || chunk.kind === "tool" || chunk.kind === "stop")) {
83          text = "";
84          block = null;
85          await publish(null);
86        }
87        yield chunk;
88      }
89    } finally {
90      try {
91        if (!completed) await stream.return();
92      } finally {
93        // Only the stream that still owns this version can clear the preview.
94        await publish(null);
95      }
96    }
97  });
98
99  on("ui.render", { component: ["AbovePrompt", "Pane"] }, async ($, e, next) => {
100    const inSidebar = e.component === "Pane" && e.requestId === "sidebar-controls";
101    if (!inSidebar && e.component !== "AbovePrompt") return next(e);
102    const original = await next(e);
103    if (e.surface !== "terminal" || e.props.hasSurvey || e.props.view?.agentId) return original;
104    if (!inSidebar) {
105      const { value: sidebarVisible } = await $.state.get({ plugin: "sidebar-controls", key: "visible" });
106      if (sidebarVisible) return original;
107    }
108    const { value: enabled = true } = await $.state.get(enabledState);
109    const { Box, Button } = $.ui.resolve(e);
110    const children = [original, Button({
111      key: "toggle-live-thinking",
112      label: `Live thinking: ${enabled ? "ON" : "OFF"}`,
113      color: enabled ? "cyan" : "gray",
114      onPress: async () => {
115        const { value = true } = await $.state.get(enabledState);
116        await $.state.set(enabledState, !value);
117      },
118    })];
119    return Box({ flexDirection: "column", children });
120  });
121
122  on("ui.render", async ($, e, next) => {
123    if (e.surface !== "terminal" || !["UserMessage", "AssistantMessage", "ToolResult", "ToolGroup"].includes(e.component)) {
124      return next(e);
125    }
126    const { value: anchor } = await $.state.get(anchorState);
127    if (!isAnchor(e, anchor)) return next(e);
128    const original = await next(e);
129    const { value: enabled = true } = await $.state.get(enabledState);
130    const { value: preview } = await $.state.get(previewState);
131    if (!enabled || !preview?.text) return original;
132    const visible = previewTail(preview.text
133      .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
134      .replace(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/g, "")
135      .replace(/\t/g, "  "));
136    const { Box, Text } = $.ui.resolve(e);
137    return Box({
138      flexDirection: "column",
139      children: [original, Box({
140        marginTop: 1,
141        flexDirection: "row",
142        children: [Text({ dimColor: true, children: "∴ " }), Box({
143          key: "thinking-preview",
144          flexShrink: 1,
145          children: [Text({ dimColor: true, children: visible })],
146        })],
147      })],
148    });
149  });
150}
151
152function isAnchor(e, anchor) {
153  if (!anchor) return false;
154  if (anchor.kind === "user") return e.component === "UserMessage" && e.requestId === anchor.id;
155  if (anchor.kind === "assistant") return e.component === "AssistantMessage" && e.requestId === anchor.id;
156  if (e.component === "ToolResult") return e.props.tool_use_id === anchor.id;
157  return e.component === "ToolGroup" && e.props.calls.some(call => call.tool_use_id === anchor.id);
158}
159
160function previewTail(text) {
161  const tail = text.slice(-MAX_PREVIEW_CHARS);
162  return /^[\uDC00-\uDFFF]/.test(tail) ? tail.slice(1) : tail;
163}
164
types/index.d.ts 11 lines
1declare module "claude-code" {
2  interface PluginState {
3    "sidebar-controls": { visible: boolean };
4    "live-thinking": {
5      enabled: boolean;
6      preview: { text: string } | null;
7      anchor: { kind: "user" | "assistant" | "tool"; id: string } | null;
8    };
9  }
10}
11