SLOPSHOPPER

compact-tools

Compact terminal tool output while Claude's native thinking display remains visible.

newpanebandrowscommand
v0.1.2no licenseupdated 2026-10-02AJclemendor/my-mods/plugins/compact-tools
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · compact-tools
› 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 › /compact-tools ⎿ compact-tools: Compact tools off. Thinking display is unchanged. ⟨Claude Code's own drawing⟩ [ Compact tools: OFF ] ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
⟨Claude Code's own drawing⟩ [ Compact tools: OFF ]
README

Compact tools

Compact Bash, Read, Edit, Write, and MCP output for Claude Code's terminal, with a visible ON/OFF toggle above the prompt. Run /compact-tools to toggle, or /compact-tools on and /compact-tools off.

Failed Bash calls show a compact red error row with their exit code when available and first diagnostic. Turn the mod off to see full error output.

MCP calls show the server and tool name without dumping their JSON arguments. Results show the first text line and a hidden-line count, or a count of non-text items. Failures are marked red, and interrupted calls keep their native interruption marker. The complete arguments and results still reach the tool and model and remain in the transcript; turning the mod off restores their full display.

Install sidebar-controls to move the ON/OFF button into a top-right sidebar. Closing the sidebar returns it above the prompt.

Install from the marketplace:

/plugin marketplace add AJclemendor/my-mods
/plugin install compact-tools@my-mods
/reload-plugins

Thinking visibility is configured separately in Claude's settings. See the marketplace README for setup, compatibility, and update instructions.

Source 2 files
hooks/compact-tools.mjs 200 lines
1const enabledState = { plugin: "compact-tools", key: "enabled" };
2const supported = new Set(["Bash", "Read", "Edit", "Write"]);
3
4export function register(on) {
5  on("session.start", async ($, e, next) => {
6    const result = await next(e);
7    await $.command.register({
8      name: "compact-tools",
9      description: "Toggle compact tool output; thinking stays in Claude's native display",
10      argumentHint: "[on|off]",
11      immediate: true,
12    });
13    return result;
14  });
15
16  on("command.run", { command: "compact-tools" }, async ($, e) => {
17    const arg = e.args.trim().toLowerCase();
18    if (arg && arg !== "on" && arg !== "off") {
19      return { text: "Usage: /compact-tools [on|off]" };
20    }
21    const { value = true } = await $.state.get(enabledState);
22    const enabled = arg ? arg === "on" : !value;
23    await $.state.set(enabledState, enabled);
24    return { text: `Compact tools ${enabled ? "on" : "off"}. Thinking display is unchanged.` };
25  });
26
27  on("ui.render", { component: ["AbovePrompt", "Pane"] }, async ($, e, next) => {
28    const inSidebar = e.component === "Pane" && e.requestId === "sidebar-controls";
29    if (!inSidebar && e.component !== "AbovePrompt") return next(e);
30    const original = await next(e);
31    if (e.surface !== "terminal" || e.props.hasSurvey) return original;
32    if (!inSidebar) {
33      const { value: sidebarVisible } = await $.state.get({ plugin: "sidebar-controls", key: "visible" });
34      if (sidebarVisible) return original;
35    }
36    const { value = true } = await $.state.get(enabledState);
37    const { Box, Button } = $.ui.resolve(e);
38    return Box({
39      flexDirection: "column",
40      children: [original, Button({
41        key: "toggle-compact-tools",
42        label: `Compact tools: ${value ? "ON" : "OFF"}`,
43        color: value ? "green" : "gray",
44        onPress: async () => {
45          const { value = true } = await $.state.get(enabledState);
46          await $.state.set(enabledState, !value);
47        },
48      })],
49    });
50  });
51
52  on("ui.render", { component: "ToolUse" }, async ($, e, next) => {
53    const p = e.props;
54    if (e.surface === "terminal" && isMcp(p.tool) && !p.isInterrupted) {
55      const { value = true } = await $.state.get(enabledState);
56      if (!value || isInterruption(p.output)) return next(e);
57      const failed = p.isErrored || p.output?.isError === true;
58      const name = p.tool.slice(5).split("__").map(part => part.replace(/_/g, " ")).join(" · ");
59      const label = `${failed ? "✗" : "●"} ${oneLine(name)} (MCP)${p.isRunning ? " · Running…" : ""}`;
60      return $.ui.resolve(e).Box({
61        flexDirection: "column",
62        marginTop: 1,
63        children: [row($, e, label, false, failed), ...(p.isRunning ? [] : [
64          row($, e, `  ⎿ ${failed ? "Error · " : ""}${mcpResultLabel(p.output, failed)}`, true, failed),
65        ])],
66      });
67    }
68    if (e.surface !== "terminal" || !supported.has(p.tool) || (p.isErrored && p.tool !== "Bash") || p.isInterrupted) {
69      return next(e);
70    }
71    const { value = true } = await $.state.get(enabledState);
72    if (!value) return next(e);
73    const label = toolLabel(p);
74    if (label === null) return next(e);
75    const result = p.isErrored ? bashErrorLabel(p.output) : resultLabel(p.tool, p.output);
76    if ((p.isErrored || p.output !== undefined) && result === null) return next(e);
77    const { Box } = $.ui.resolve(e);
78    return Box({
79      flexDirection: "column",
80      marginTop: 1,
81      children: [row($, e, label, false, p.isErrored), ...(result === null ? [] : [row($, e, `  ⎿ ${result}`, true, p.isErrored)])],
82    });
83  });
84
85  on("ui.render", { component: "ToolResult" }, async ($, e, next) => {
86    const p = e.props;
87    if (e.surface === "terminal" && isMcp(p.tool)) {
88      const { value = true } = await $.state.get(enabledState);
89      if (!value || isInterruption(p.output)) return next(e);
90      return $.ui.resolve(e).Box({});
91    }
92    if (e.surface !== "terminal" || !supported.has(p.tool) || (p.isErrored && p.tool !== "Bash")) return next(e);
93    const { value = true } = await $.state.get(enabledState);
94    if (!value) return next(e);
95    const label = p.isErrored ? bashErrorLabel(p.output) : resultLabel(p.tool, p.output);
96    if (label === null) return next(e);
97    // Both layouts receive the result on ToolUse; the standalone result would repeat it.
98    return $.ui.resolve(e).Box({});
99  });
100}
101
102function isMcp(tool) {
103  return tool.startsWith("mcp__");
104}
105
106function isInterruption(output) {
107  return typeof output === "string" && /^(?:Error:\s*)?\[(?:Request interrupted|Tool call )/i.test(output.trim());
108}
109
110function mcpResultLabel(output, failed) {
111  const blocks = Array.isArray(output) ? output : Array.isArray(output?.content) ? output.content : [];
112  const texts = typeof output === "string" ? [output] : blocks.filter(block => block?.type === "text" && typeof block.text === "string").map(block => block.text);
113  const lines = texts.flatMap(text => text.split(/\r?\n/)).map(oneLine).filter(Boolean);
114  const attachments = blocks.filter(block => block?.type !== "text").length;
115  const first = failed ? lines[0]?.replace(/^Error:\s*/i, "") : lines[0];
116  const fallback = failed ? "Tool failed" : attachments ? `${attachments} non-text ${attachments === 1 ? "item" : "items"}`
117    : output?.structuredContent || (output && !Array.isArray(output) && typeof output === "object" && !Array.isArray(output.content)) ? "Structured result" : "No output";
118  return `${first || fallback}${lines.length > 1 ? ` … +${lines.length - 1} lines` : ""}${lines.length && attachments ? ` · ${attachments} non-text ${attachments === 1 ? "item" : "items"}` : ""}`;
119}
120
121function toolLabel(p) {
122  const input = p.input;
123  if (!input || typeof input !== "object") return null;
124  const label = p.tool === "Bash" ? input.description || input.command : input.file_path;
125  if (typeof label !== "string") return null;
126  return `${p.isErrored ? "✗" : "●"} ${p.tool}(${oneLine(label)})${p.isRunning ? " · Running…" : ""}`;
127}
128
129function oneLine(value) {
130  return value
131    .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
132    .replace(/[\r\n\t]+/g, " ")
133    .replace(/[\x00-\x1f\x7f-\x9f]/g, "")
134    .trim()
135    .slice(0, 2000);
136}
137
138function row($, e, label, dimColor, isError = false) {
139  const { Box, Text } = $.ui.resolve(e);
140  return Box({
141    height: 1,
142    children: [Text({ dimColor: isError ? false : dimColor, color: isError ? "red" : undefined, wrap: "truncate-end", children: label })],
143  });
144}
145
146function linesIn(text) {
147  return text === "" ? 0 : text.replace(/\r?\n$/, "").split(/\r?\n/).length;
148}
149
150function lineCount(count) {
151  return `${count} ${count === 1 ? "line" : "lines"}`;
152}
153
154function bashErrorLabel(output) {
155  if (typeof output !== "string") {
156    const summary = resultLabel("Bash", output);
157    return summary === null ? null : `Error · ${summary}`;
158  }
159  if (/^(?:Error:\s*)?\[(?:Request interrupted|Tool call )/i.test(output.trim())) return null;
160  const lines = output.split(/\r?\n/).map(oneLine).filter(Boolean);
161  const exit = lines[0]?.match(/^(?:Error:\s*)?Exit code (\d+)$/i);
162  if (exit) lines.shift();
163  const detail = lines[0]?.replace(/^Error:\s*/i, "") || "Command failed";
164  return `${exit ? `Exit code ${exit[1]}` : "Error"} · ${detail}${lines.length > 1 ? ` … +${lines.length - 1} lines` : ""}`;
165}
166
167function resultLabel(tool, output) {
168  if (!output || typeof output !== "object") return null;
169  if (output.staged || output.interrupted) return null;
170  if (tool === "Bash") {
171    if (typeof output.stdout !== "string" || typeof output.stderr !== "string") return null;
172    const lines = [output.stderr, output.stdout].join("\n")
173      .split(/\r?\n/).map(oneLine).filter(Boolean);
174    const fallback = output.isImage ? "Image sent to Claude"
175      : output.backgroundTaskId ? "Running in background"
176      : output.bashEditDiff ? "File updated"
177      : output.returnCodeInterpretation || "No output";
178    return `${output.stderr.trim() ? "stderr · " : ""}${lines[0] || oneLine(fallback)}${lines.length > 1 ? ` … +${lines.length - 1} lines` : ""}`;
179  }
180  if (tool === "Edit") {
181    if (!Array.isArray(output.structuredPatch)) return null;
182    let added = 0, removed = 0;
183    for (const hunk of output.structuredPatch) {
184      for (const line of hunk.lines) {
185        if (line.startsWith("+")) added++;
186        if (line.startsWith("-")) removed++;
187      }
188    }
189    return `Added ${lineCount(added)}, removed ${lineCount(removed)}`;
190  }
191  if (tool === "Write") {
192    if (typeof output.content !== "string") return null;
193    return `Wrote ${lineCount(linesIn(output.content))}`;
194  }
195  if (tool === "Read" && output.type === "text" && typeof output.file?.numLines === "number") {
196    return `Read ${lineCount(output.file.numLines)}${output.file.truncatedByTokenCap ? " (partial)" : ""}`;
197  }
198  return null;
199}
200
types/index.d.ts 7 lines
1declare module "claude-code" {
2  interface PluginState {
3    "sidebar-controls": { visible: boolean };
4    "compact-tools": { enabled: boolean };
5  }
6}
7