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

Skills for coding agents.
curl -fsSL shalom2552.github.io/agent-skills/install | bash
Or clone and run ./install. See ./install --help for options.
| Skill | Description |
|---|---|
docs-sync | Find and fix drift between docs and code. |
dotconf | Answer, change, and remember dotfile and app configs. |
read-only | Lock the session to read-only; change nothing. |
setup-sh25-skills | Install, manage and wire up shalom2552's agent-skills assets. |
system-audit | Read-only health, security and cleanliness audit of this machine. |
system-fix | Diagnose, fix, and document a system or config fault on this machine. |
to-prompt | Write a handoff prompt for a fresh agent. |
walk-with-me | Learn languages and idioms by coding with a coach. |
scripts/statusline.sh - status line for Claude Code.
Installing it also sets statusLine, from scripts/statusline.settings.json.
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.
| Setting | Description |
|---|---|
tmux-truecolor | Truecolor inside tmux, so mod hex colors render right. |
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
| Mod | Description |
|---|---|
spinner-words | Swap the spinner verbs for dev ones. |
tape | /tape pane of tool calls per turn. |
frame | Rounded frame around your prompts. |
beat | Turn time and usage as pills. |
hooks/register.tsx 192 lines1import { 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}
192types/index.d.ts 21 lines1export 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