SLOPSHOPPER

cw-pane

Live pane of driver-seat cheap workers: running tasks, cost, fail and escalation rates from workers.tsv

newpanecommandstatustimer
v0.1.0no licenseupdated 2026-10-10ianwilson97/claude-mods/cw-pane
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · cw-pane
│ ┃ Cheap workers ✕ › fix the failing auth test and add an audit log call │ ┃ No .driver-seat/workers.tsv in this project. │ ⏺ 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 │ │ › /cw-pane │ ⎿ cw-pane: Cheap workers pane opened. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Cheap workers
No .driver-seat/workers.tsv in this project.
README

ianwilson-mods

Claude Code mods (function-hook plugins). Each folder is one plugin; .claude-plugin/marketplace.json makes this repo a marketplace.

ModWhat it does
cheap-worker/worker spins up Claude Code workers from a picker; /workers lists, messages and closes them. Team mode lets workers find each other (ListAgents) and talk (SendMessage).
agent-progress/progress: animated progress bar per agent (main, subagents, workers) from their task lists.
agent-map/map: project file tree; red 🔨 = an agent is editing it, green ✓ = done, ready for review.
context-barContext-window breakdown above the prompt.
usage-meter5-hour / weekly usage in the footer.
quick-settings⚙ footer button for model and effort.
skins/skins: colour themes (Tokyo Night, Dracula, Gruvbox, …).
cw-pane, driver-seat-guard, evidence-bandFor the driver-seat workflow; skip them elsewhere.

Install

/plugin marketplace add ianwilson97/claude-mods
/plugin install cheap-worker@ianwilson-mods
/plugin install agent-progress@ianwilson-mods
/plugin install agent-map@ianwilson-mods

On the machine where this folder is the live copy (loaded through CLAUDE_CODE_PLUGIN_DIRS), don't also install from the marketplace: each mod would load twice.

cheap-worker: two switches

Set per machine in /config (or pluginConfigs.cheap-worker.options in ~/.claude/settings.json):

OptionValues
provideropenrouter (default)Any OpenRouter model; key read from $CW_OPENROUTER_API_KEY (export it in ~/.zshenv). Workers skip user settings, hooks and plugins.
bedrockClaude only (Sonnet / Opus / Haiku). Workers keep your user settings, so the Bedrock env there (CLAUDE_CODE_USE_BEDROCK, AWS_REGION, AWS_PROFILE, …) applies. No call to openrouter.ai.
launchpanes (default)All workers tiled in tmux session cw: tmux new -A -s cw.
tabsEach worker gets a tmux session of its own and opens in a background tab: a cmux tab when Claude Code runs inside cmux, else a Ghostty tab (focus stays where you were).

Work laptop: provider = bedrock, launch = tabs.

Reaching a worker: crew

The mod installs crew into ~/.local/bin (that folder must be on your PATH; it never overwrites a different crew).

crew            # list running workers, pick one by number
crew ab12c      # attach to cw-ab12c

If Ghostty can't be scripted (macOS asks once to let Claude control Ghostty; a managed Mac may refuse), the worker still runs: press CMD+T and run crew. Closing a tab only detaches; the worker keeps running until you close it from /workers.

Needs: tmux; for auto-opened tabs, cmux or Ghostty 1.3+.

First run on a new machine: checks

  1. /plugin lists the mods as enabled. Managed settings can block marketplaces or plugins; if so, nothing loads.
  2. /worker, launch one with task say hi; a Ghostty tab opens (or the toast says CMD+T, then crew …).
  3. Turn on team, launch two workers, then from /workers send one a message: messaging is Claude Code's own (ListAgents / SendMessage) and works under Bedrock unless policy turns it off.
Source 2 files
hooks/register.tsx 98 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { JobStats, Row, Summary } from '../types'
5
6const PANE = 'cw'
7const DIR = '.driver-seat'
8const summary = atom({ plugin: 'cw-pane', key: 'summary' } as const, null as Summary | null)
9
10// The driver-seat rule: revisit the model split at 20%+ escalated after 10 runs of a job.
11const isHot = (j: JobStats) => j.runs >= 10 && j.escalated / j.runs >= 0.2
12
13export function summarize(tsv: string, tasks: readonly string[], results: readonly string[]): Summary {
14  const lines = tsv.trim().split('\n').slice(1).filter(Boolean)
15  const rows: Row[] = lines.map(line => {
16    const c = line.split('\t')
17    return {
18      taskId: c[1] ?? '?',
19      job: c[2] ?? '?',
20      cost: Number(c[8]) || 0,
21      verdict: c[9] ?? '?',
22      escalated: c[10] === '1',
23    }
24  })
25  const jobs = new Map<string, JobStats>()
26  for (const r of rows) {
27    const j = jobs.get(r.job) ?? { job: r.job, runs: 0, fails: 0, escalated: 0, cost: 0 }
28    jobs.set(r.job, {
29      ...j,
30      runs: j.runs + 1,
31      fails: j.fails + (r.verdict === 'fail' ? 1 : 0),
32      escalated: j.escalated + (r.escalated ? 1 : 0),
33      cost: j.cost + r.cost,
34    })
35  }
36  const done = new Set(results)
37  return {
38    running: tasks.filter(t => !done.has(t)).map(t => t.replace(/\.md$/, '')),
39    recent: rows.slice(-8).reverse(),
40    jobs: [...jobs.values()],
41    cost: rows.reduce((sum, r) => sum + r.cost, 0),
42  }
43}
44
45async function refresh($: EngineInterface) {
46  const tsv = await $.fs.read(`${DIR}/workers.tsv`).catch(() => null)
47  if (typeof tsv !== 'string') return update($, summary, () => null)
48  const names = async (d: string) =>
49    (await $.fs.list(`${DIR}/${d}`).catch(() => [])).filter(e => e.name.endsWith('.md')).map(e => e.name)
50  const next = summarize(tsv, await names('tasks'), await names('results'))
51  await update($, summary, () => next)
52  $.ui.status(next.running.length > 0 ? `cw: ${next.running.length} running` : undefined)
53}
54
55export const register: Register = on => {
56  on('session.start', async ($, e, next) => {
57    await $.command.register({ name: 'cw-pane', description: 'Show driver-seat cheap workers in a pane' })
58    await refresh($)
59    $.clock.every(5000, () => refresh($)) // ponytail: polls fs; switch to a watcher if it ever lags
60    return next(e)
61  })
62
63  on('command.run', { command: 'cw-pane' }, async $ => {
64    await refresh($)
65    await $.ui.open({ id: PANE, title: 'Cheap workers' })
66    return { text: 'Cheap workers pane opened.' }
67  })
68
69  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
70    const { Box, Text } = $.ui.resolve(e)
71    const s = await read($, summary)
72    if (s === null) return <Text dimColor>No .driver-seat/workers.tsv in this project.</Text>
73    const pct = (n: number, d: number) => (d === 0 ? '0%' : `${Math.round((100 * n) / d)}%`)
74    return (
75      <Box flexDirection="column">
76        <Text bold>Running ({s.running.length})</Text>
77        {s.running.length === 0 && <Text dimColor>  none</Text>}
78        {s.running.map(t => (
79          <Text key={`run-${t}`} color="yellow">  ● {t}</Text>
80        ))}
81        <Text bold>Jobs · total ${s.cost.toFixed(2)}</Text>
82        {s.jobs.map(j => (
83          <Text key={`job-${j.job}`} color={isHot(j) ? 'red' : undefined}>
84            {'  '}{j.job.padEnd(9)} {String(j.runs).padStart(3)} runs · fail {pct(j.fails, j.runs)} · esc{' '}
85            {pct(j.escalated, j.runs)} · ${j.cost.toFixed(2)}{isHot(j) ? '  ← revisit model' : ''}
86          </Text>
87        ))}
88        <Text bold>Recent</Text>
89        {s.recent.map((r, i) => (
90          <Text key={`row-${i}`} dimColor={r.verdict === 'pass'} color={r.verdict === 'fail' ? 'red' : undefined}>
91            {'  '}{r.verdict.padEnd(4)} {r.taskId} ${r.cost.toFixed(3)}{r.escalated ? ' ↑esc' : ''}
92          </Text>
93        ))}
94      </Box>
95    )
96  })
97}
98
types/index.d.ts 10 lines
1export type Row = { taskId: string; job: string; verdict: string; escalated: boolean; cost: number }
2export type JobStats = { job: string; runs: number; fails: number; escalated: number; cost: number }
3export type Summary = { running: string[]; recent: Row[]; jobs: JobStats[]; cost: number }
4
5declare module 'claude-code' {
6  interface PluginState {
7    'cw-pane': { summary: Summary | null }
8  }
9}
10