SLOPSHOPPER

tape

/tape: a live pane of every tool call, grouped by turn, with status, target and duration.

newpaneguardcommand
v0.1.0MITupdated 2026-10-11shalom2552/agent-skills/mods/tape
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · tape
│ ┃ Tape ✕ › fix the failing auth test and add an audit log call │ ┃ Turn 1 · 9 calls │ ┃ ✓ Bash cat .env 0.0s ⏺ Read(src/auth.ts) │ ┃ ✓ Bash rm -rf build && git push --force ⎿ Read 6 lines │ ┃ ✓ Bash git status --porcelain 0.0s ⏺ Update(src/auth.ts) │ ┃ ✗ Bash bun test 0.0s ⎿ Added 2 lines, removed 1 line │ ┃ ✓ Write src/cache.ts 0.0s ⏺ Bash(bun test) │ ┃ ✓ Write src/audit.ts 0.0s ⎿ 3 pass, 1 fail │ ┃ ✓ Edit src/auth.ts 0.0s │ ┃ ✓ Grep refresh\( 0.0s ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ ✓ Read src/auth.ts 0.0s │ ┃ ✻ Worked for 42s · done 4:20 PM │ │ › /tape │ ⎿ tape: Tape opened. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Tape
Turn 1 · 9 calls ✓ Bash cat .env 0.0s ✓ Bash rm -rf build && git push --force origin main 0 ✓ Bash git status --porcelain 0.0s ✗ Bash bun test 0.0s ✓ Write src/cache.ts 0.0s ✓ Write src/audit.ts 0.0s ✓ Edit src/auth.ts 0.0s ✓ Grep refresh\( 0.0s ✓ Read src/auth.ts 0.0s
README

Agent Skills

Skills for coding agents.

Install

curl -fsSL shalom2552.github.io/agent-skills/install | bash

Or clone and run ./install. See ./install --help for options.

Skills

SkillDescription
docs-syncFind and fix drift between docs and code.
dotconfAnswer, change, and remember dotfile and app configs.
read-onlyLock the session to read-only; change nothing.
setup-sh25-skillsInstall, manage and wire up shalom2552's agent-skills assets.
system-auditRead-only health, security and cleanliness audit of this machine.
system-fixDiagnose, fix, and document a system or config fault on this machine.
to-promptWrite a handoff prompt for a fresh agent.
walk-with-meLearn languages and idioms by coding with a coach.

Scripts

scripts/statusline.sh - status line for Claude Code.

Installing it also sets statusLine, from scripts/statusline.settings.json.

Settings

Small JSON files that ./install merges into the scope's settings.json, so no hand edits.

  • settings/<name>.json stands alone.
  • <stem>.settings.json beside a skill or script is bound to it and applies each time that entry installs. {{path}} in it becomes the entry's installed path.
./install settings --scope global -y

To add a setting, add a file. To remove one, delete the file and re-run ./install. Only settings still holding the installed values are removed; one you changed by hand stays. Needs jq.

SettingDescription
tmux-truecolorTruecolor inside tmux, so mod hex colors render right.

Mods

Claude Code plugins in mods/. ./install mods adds this repo as the shalom2552 marketplace and installs them; without the installer:

/plugin install <mod> --marketplace shalom2552/agent-skills
ModDescription
spinner-wordsSwap the spinner verbs for dev ones.
tape/tape pane of tool calls per turn.
frameRounded frame around your prompts.
beatTurn time and usage as pills.
Source 2 files
hooks/register.tsx 192 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { TapeCall, TapeStatus, TapeTurn } from '../types'
5
6const PANE = 'tape'
7const MAX_CALLS = 300
8const MAX_TURNS = 50
9const TARGET_KEYS = ['command', 'file_path', 'notebook_path', 'pattern', 'url', 'query', 'skill', 'description', 'prompt']
10
11const calls = atom({ plugin: 'tape', key: 'calls' } as const, [])
12const turns = atom({ plugin: 'tape', key: 'turns' } as const, [])
13
14const ICON: Record<TapeStatus, { glyph: string; color: string }> = {
15  running: { glyph: '●', color: 'warning' },
16  ok: { glyph: '✓', color: 'success' },
17  error: { glyph: '✗', color: 'error' },
18  denied: { glyph: '⊘', color: 'error' },
19}
20
21export const firstLine = (text: string) => text.split('\n').find(l => l.trim().length > 0)?.trim() ?? ''
22
23const TAG = /<\/?[a-z][\w-]*(?:\s[^<>]*)?>/gi
24
25export const labelOf = (text: string) => {
26  for (const line of text.split('\n')) {
27    const trimmed = line.trim()
28    const label = /^<\/?[a-z]/i.test(trimmed) ? trimmed.replace(TAG, '').trim() : trimmed
29    if (label.length > 0) return label
30  }
31  return ''
32}
33
34export const targetOf = (input: Record<string, unknown>, cwd: string) => {
35  for (const key of TARGET_KEYS) {
36    const value = input[key]
37    if (typeof value === 'string' && value.length > 0) {
38      const line = firstLine(value)
39      return line.startsWith(`${cwd}/`) ? line.slice(cwd.length + 1) : line
40    }
41  }
42  return ''
43}
44
45export const formatSeconds = (ms: number) => (ms < 10_000 ? `${(ms / 1000).toFixed(1)}s` : `${Math.round(ms / 1000)}s`)
46
47const shortTool = (tool: string) => (tool.startsWith('mcp__') ? tool.split('__').slice(2).join('__') || tool : tool)
48
49export const register: Register = on => {
50  let turn = 0
51
52  on('session.start', async ($, e, next) => {
53    const started = await next(e)
54    await $.command.register({
55      name: 'tape',
56      description: 'Toggle a live pane of tool calls, grouped by turn',
57      immediate: true,
58    })
59    const known = await read($, turns)
60    turn = known[known.length - 1]?.n ?? 0
61    return started
62  })
63
64  on('command.run', { command: 'tape' }, async $ => {
65    const isOpen = (await $.ui.panes()).some(pane => pane.id === PANE)
66    if (isOpen) {
67      await $.ui.close({ id: PANE })
68      return { text: 'Tape closed.' }
69    }
70    await $.ui.open({ id: PANE, title: 'Tape' })
71    return { text: 'Tape opened.' }
72  })
73
74  on('session.end', async ($, e, next) => {
75    if (e.reason === 'clear') {
76      turn = 0
77      await update($, calls, () => [])
78      await update($, turns, () => [])
79    }
80    return next(e)
81  })
82
83  on('turn.start', async ($, e, next) => {
84    turn += 1
85    const entry: TapeTurn = { n: turn, prompt: labelOf(e.text) }
86    await update($, turns, list => [...list, entry].slice(-MAX_TURNS))
87    return next(e)
88  })
89
90  on('tool.call', async ($, e, next) => {
91    const call: TapeCall = {
92      id: e.tool_use_id,
93      turn,
94      tool: shortTool(String(e.tool)),
95      target: targetOf(e as unknown as Record<string, unknown>, await $.session.cwd()),
96      isAgent: e.agentId !== undefined,
97      startedAt: await $.clock.now(),
98      status: 'running',
99    }
100    await update($, calls, list => [...list, call].slice(-MAX_CALLS))
101
102    let status: TapeStatus = 'error'
103    try {
104      const ran = await next(e)
105      status = ran.deny !== undefined ? 'denied' : ran.isError === true ? 'error' : 'ok'
106      return ran
107    } finally {
108      const endedAt = await $.clock.now()
109      await update($, calls, list => list.map(one => (one.id === call.id ? { ...one, status, endedAt } : one)))
110    }
111  })
112
113  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
114    const { Box, Text } = $.ui.resolve(e)
115    const list = await read($, calls)
116    const known = await read($, turns)
117
118    if (list.length === 0) {
119      return (
120        <Box flexDirection="column">
121          <Text dimColor>No tool calls yet.</Text>
122        </Box>
123      )
124    }
125
126    const order: number[] = []
127    const byTurn = new Map<number, TapeCall[]>()
128    for (const call of list) {
129      const group = byTurn.get(call.turn)
130      if (group === undefined) {
131        byTurn.set(call.turn, [call])
132        order.push(call.turn)
133      } else {
134        group.push(call)
135      }
136    }
137
138    const width = e.props.bodyColumns
139    const toolWidth = 10
140    const timeWidth = 7
141
142    return (
143      <Box flexDirection="column">
144        {order.reverse().map(n => {
145          const group = byTurn.get(n) ?? []
146          const prompt = known.find(t => t.n === n)?.prompt ?? ''
147          return (
148            <Box key={`turn-${n}`} flexDirection="column" marginBottom={1}>
149              <Box flexDirection="row">
150                <Text color="claude" bold>
151                  {n === 0 ? 'Before first turn' : `Turn ${n}`}
152                </Text>
153                <Text dimColor> · {group.length} call{group.length === 1 ? '' : 's'}</Text>
154                {prompt.length > 0 && width >= 40 && (
155                  <Box flexGrow={1} flexShrink={1} marginLeft={1}>
156                    <Text dimColor italic wrap="truncate-end">
157                      {prompt}
158                    </Text>
159                  </Box>
160                )}
161              </Box>
162              {[...group].reverse().map(call => {
163                const icon = ICON[call.status]
164                return (
165                  <Box key={call.id} flexDirection="row">
166                    <Box width={2} flexShrink={0}>
167                      <Text color={icon.color}>{icon.glyph}</Text>
168                    </Box>
169                    <Box width={toolWidth} flexShrink={0}>
170                      <Text bold={!call.isAgent} dimColor={call.isAgent} wrap="truncate-end">
171                        {call.isAgent ? `↳${call.tool}` : call.tool}
172                      </Text>
173                    </Box>
174                    <Box flexGrow={1} flexShrink={1}>
175                      <Text dimColor={call.status !== 'running'} wrap="truncate-end">
176                        {call.target}
177                      </Text>
178                    </Box>
179                    <Box width={timeWidth} flexShrink={0} justifyContent="flex-end">
180                      <Text dimColor>{call.endedAt === undefined ? '…' : formatSeconds(call.endedAt - call.startedAt)}</Text>
181                    </Box>
182                  </Box>
183                )
184              })}
185            </Box>
186          )
187        })}
188      </Box>
189    )
190  })
191}
192
types/index.d.ts 21 lines
1export type TapeStatus = 'running' | 'ok' | 'error' | 'denied'
2
3export type TapeCall = {
4  id: string
5  turn: number
6  tool: string
7  target: string
8  isAgent: boolean
9  startedAt: number
10  endedAt?: number
11  status: TapeStatus
12}
13
14export type TapeTurn = { n: number; prompt: string }
15
16declare module 'claude-code' {
17  interface PluginState {
18    tape: { calls: TapeCall[]; turns: TapeTurn[] }
19  }
20}
21