SLOPSHOPPER

tool-skins

Reskins the transcript's tool rows: an icon and a colour per tool, from a theme palette. /skin <name> or /skin off.

newrowscommand
v1.0.0no licenseupdated 2026-10-09Fraktalis/claude-mods/plugins/tool-skins
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · tool-skins
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ✎ Edit app/src/auth.ts ⏺ 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 › /skin ⎿ tool-skins: Skin: dracula. Available: dracula, off. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Tool row
✎ Edit app/src/auth.ts
README

claude-mods

My Claude Code mods, as a plugin marketplace (alex-mods). One folder per mod under plugins/.

ModWhat it does
savvy-progressEmber progress bar above the prompt for a batch of subagents, and an Agents pane: crab mascot per agent, live status, per-agent progress bar, model, tokens, estimated cost, elapsed time.
file-treeFiles pane with the project tree: files Claude reads (blue), modifies (orange) or creates (purple) light up with a shimmer, and turn green once committed, in every repo they live in.
tool-skinsReskins the transcript's tool rows: an icon and a palette colour per tool (dracula skin from kevinvn1709/vscode-dracula-color-theme), the row's result still drawn by Claude Code.
cache-taxPrompt-cache state in the status line; stops a message that would re-read the whole conversation on a cold cache and shows what it costs; /keepwarm keeps the cache warm while you are away.

Install on this machine (reads straight from this folder)

claude plugin marketplace add E:/WS/claude-mods
claude plugin install savvy-progress@alex-mods --scope user
claude plugin install file-tree@alex-mods --scope user
claude plugin install cache-tax@alex-mods --scope user
claude plugin install tool-skins@alex-mods --scope user

Installed this way, Claude Code reads the plugin from this folder, so an edit here reaches a session with /reload-plugins (no reinstall, no version bump).

Install from GitHub (another machine)

/plugin install savvy-progress --marketplace Fraktalis/claude-mods

Then update later with claude plugin update savvy-progress@alex-mods after pushing a new version.

Working on a mod

claude plugin validate plugins/savvy-progress
claude plugin test plugins/savvy-progress

Bump version in the mod's .claude-plugin/plugin.json for each release that GitHub installs should pick up.

savvy-progress

  • /savvy opens the Agents pane; /savvy-skin <type|*> <crab|developer|designer|researcher|architect|ghost> picks a mascot.
  • Registers a progress tool (mcp__savvy-progress__progress); each subagent is asked to report done/total/step with it. The main loop can also call it, with title to name the batch.
  • Cost is estimated from each request's token usage at the model's list price (table in hooks/register.tsx, PRICES); edit it when prices change.

file-tree

  • /tree opens the Files pane (it also opens by itself on the first file Claude edits or creates); /tree-clear forgets what was touched.
  • States: ◦ read, ● modified, + new, ✓ committed. Reads never downgrade a state; editing a committed file makes it modified again.
  • A successful git commit run by Claude re-checks git status in every repository holding a modified or new file; commits made in a terminal are picked up on the next prompt.
  • Folders leading to touched files open by themselves; node_modules, .git and the like are listed dimmed and stay closed. Touched only hides the rest. Files outside the session's folder are listed under Elsewhere.

cache-tax

  • Status line: 🔥 cache 42m while warm, 🧊 cache cold · next msg ≈ $2.40 once the TTL has passed since the main conversation's last request.
  • On a cold cache, a message whose context is above the threshold (20k tokens by default) is stopped once with the cold and warm cost; the text is put back in the box, and Enter again within 2 min sends it.
  • Cost: cold re-writes the whole context to the cache (2x input price for a 1h TTL, 1.25x for 5m); warm reads it at the cache-read price. Prices in hooks/register.ts (PRICES), same table as savvy-progress.
  • /cache shows the details; /keepwarm on|off sends a one-line ping ~1m30 before expiry, at most maxPings times in a row (never while Claude works or while you type). Each ping costs a cache read and a one-word reply.
  • Options (plugin config): ttl (1h or 5m), minTokens, maxPings. The TTL is not detected: switch it to 5m in usage overage.

tool-skins

  • Redraws each tool row's header (ToolUse): icon + tool name in its palette colour, the call's argument (last 3 path segments, or the one-line command), … running, ✗ error, Interrupted. The result under the row (ToolResult: diffs, Bash output) is still Claude Code's own.
  • /skin lists skins, /skin <name> switches, /skin off hands the rows back; the choice is kept across sessions. dracula is on by default.
  • dracula comes from themes/dracula-color-theme.json of github.com/kevinvn1709/vscode-dracula-color-theme, a Darcula-style palette despite its name: keyword #CC7832 → Bash, function #FFC66D → Edit, string #6A8759 → Write, number #6897BB → Read, type #4EC9B0 → Grep/Glob, property #9876AA → Agent, info #6796e6 → web tools, debug #b267e6 → MCP tools, on #212122.
  • A new skin is one more entry in THEMES (hooks/register.tsx). The name claude-skins is reserved for Anthropic, hence tool-skins.
Source 2 files
hooks/register.tsx 148 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4const skin = atom({ plugin: 'tool-skins', key: 'skin' } as const, null)
5
6// ---------- themes ----------
7
8export type Look = { icon: string; color: string; bold?: boolean }
9
10export type Theme = {
11  /** Where the palette comes from, shown by /skin. */
12  source: string
13  /** Row background; absent leaves the transcript's own. */
14  background?: string
15  text: string
16  muted: string
17  running: string
18  error: string
19  tools: Record<string, Look>
20  /** MCP tools (`mcp__server__tool`). */
21  mcp: Look
22  fallback: Look
23}
24
25/**
26 * "dracula" from github.com/kevinvn1709/vscode-dracula-color-theme
27 * (themes/dracula-color-theme.json): despite the name, a Darcula-style palette.
28 * Token colours mapped to tools by role: keywords -> Bash, functions -> Edit,
29 * strings -> Write, numbers -> Read, types -> search, properties -> Agent.
30 */
31const DRACULA: Theme = {
32  source: 'github.com/kevinvn1709/vscode-dracula-color-theme',
33  background: '#212122', // editor.background
34  text: '#A9B7C6', // variables / plain text
35  muted: '#808080', // comment
36  running: '#cd9731', // token.warn-token
37  error: '#f44747', // invalid / token.error-token
38  tools: {
39    Bash: { icon: '❯', color: '#CC7832', bold: true }, // keyword (bold)
40    Read: { icon: '▤', color: '#6897BB' }, // constant.numeric
41    Edit: { icon: '✎', color: '#FFC66D' }, // entity.name.function
42    MultiEdit: { icon: '✎', color: '#FFC66D' },
43    NotebookEdit: { icon: '✎', color: '#FFC66D' },
44    Write: { icon: '✚', color: '#6A8759' }, // string
45    Grep: { icon: '⌕', color: '#4EC9B0' }, // entity.name.type
46    Glob: { icon: '✱', color: '#4EC9B0' },
47    WebFetch: { icon: '⇣', color: '#6796e6' }, // token.info-token
48    WebSearch: { icon: '⌕', color: '#6796e6' },
49    Agent: { icon: '◆', color: '#9876AA' }, // variable / property
50    TodoWrite: { icon: '☑', color: '#808080' },
51    TaskCreate: { icon: '☐', color: '#808080' },
52    TaskUpdate: { icon: '☑', color: '#808080' },
53    Skill: { icon: '✦', color: '#d7ba7d' }, // entity.name.tag.css
54    ToolSearch: { icon: '⌕', color: '#BABABA' }, // entity.other.attribute-name
55  },
56  mcp: { icon: '⧉', color: '#b267e6' }, // token.debug-token
57  fallback: { icon: '•', color: '#d4d4d4' }, // editor.foreground
58}
59
60export const THEMES: Record<string, Theme> = { dracula: DRACULA }
61const DEFAULT_SKIN = 'dracula'
62
63export const lookOf = (theme: Theme, tool: string): Look => theme.tools[tool] ?? (tool.startsWith('mcp__') ? theme.mcp : theme.fallback)
64
65/** `mcp__obsidian__read_note` -> `obsidian · read_note`; built-ins stay as they are. */
66export const toolLabel = (tool: string) => {
67  const m = /^mcp__(.+?)__(.+)$/.exec(tool)
68  return m ? `${m[1]} · ${m[2]}` : tool
69}
70
71/** The one argument worth showing for a call, shortened. */
72export const argOf = (input: unknown, max = 90) => {
73  const i = (input ?? {}) as Record<string, unknown>
74  const pick = [i.file_path, i.notebook_path, i.command, i.pattern, i.url, i.query, i.description, i.skill, i.subject, i.prompt].find(
75    v => typeof v === 'string' && v.trim() !== '',
76  ) as string | undefined
77  if (!pick) return ''
78  const one = pick.replace(/\s+/g, ' ').trim()
79  // a path: keep the last three segments
80  const isPath = (typeof i.file_path === 'string' || typeof i.notebook_path === 'string') && pick === (i.file_path ?? i.notebook_path)
81  const shown = isPath ? one.replace(/\\/g, '/').split('/').slice(-3).join('/') : one
82  return shown.length > max ? `${shown.slice(0, max - 1)}…` : shown
83}
84
85// ---------- hooks ----------
86
87export const register: Register = on => {
88  on('session.start', async ($, e, next) => {
89    await $.command.register({
90      name: 'skin',
91      description: 'Reskin tool rows: /skin <name>, /skin off, /skin to list',
92      argumentHint: `<${[...Object.keys(THEMES), 'off'].join('|')}>`,
93    })
94    // never chosen yet: start on the default skin; an explicit /skin off is kept (null)
95    const saved = (await $.store.get('skin')) as string | null | undefined
96    const start = saved === undefined ? DEFAULT_SKIN : saved === null || THEMES[saved] ? saved : DEFAULT_SKIN
97    await update($, skin, () => start)
98    return next(e)
99  })
100
101  on('command.run', { command: 'skin' }, async ($, e) => {
102    const arg = e.args.trim().toLowerCase()
103    const names = Object.keys(THEMES)
104    if (!arg) {
105      const cur = await read($, skin)
106      return { text: `Skin: ${cur ?? 'off'}. Available: ${names.join(', ')}, off.` }
107    }
108    if (arg !== 'off' && !THEMES[arg]) return { text: `No skin "${arg}". Available: ${names.join(', ')}, off.` }
109    const next = arg === 'off' ? null : arg
110    await update($, skin, () => next)
111    await $.store.set('skin', next)
112    return { text: next ? `Skin "${next}" on (palette from ${THEMES[next]!.source}).` : 'Skins off: the engine draws its own rows.' }
113  })
114
115  on('ui.render', { component: 'ToolUse' }, async ($, e, next) => {
116    const name = await read($, skin)
117    const theme = name ? THEMES[name] : undefined
118    if (!theme) return next(e)
119
120    const { Box, Text } = $.ui.resolve(e)
121    const p = e.props
122    const look = lookOf(theme, p.tool)
123    const arg = argOf(p.input)
124    const status = p.isInterrupted ? (
125      <Text color={theme.muted}> Interrupted</Text>
126    ) : p.isErrored ? (
127      <Text color={theme.error}> ✗</Text>
128    ) : p.isRunning ? (
129      <Text color={theme.running}> …</Text>
130    ) : null
131
132    return (
133      <Box flexDirection="row" backgroundColor={theme.background} paddingX={1}>
134        <Text color={look.color} bold={look.bold}>
135          {look.icon} {toolLabel(p.tool)}
136        </Text>
137        {arg ? (
138          <Text color={theme.text} wrap="truncate-end">
139            {' '}
140            {arg}
141          </Text>
142        ) : null}
143        {status}
144      </Box>
145    )
146  })
147}
148
types/index.d.ts 12 lines
1/** A skin's name, a key of THEMES in hooks/register.tsx. */
2export type SkinName = string
3
4declare module 'claude-code' {
5  interface PluginState {
6    'tool-skins': {
7      /** The active skin; null draws the engine's own rows. */
8      skin: SkinName | null
9    }
10  }
11}
12