Shows the main chat, helper agents, their models, tasks and skills for the current turn.

<img width="1096" height="808" alt="Screenshot 2026-10-19 at 8 25 29 PM" src="https://github.com/user-attachments/assets/b22cc708-5a70-4333-b3ba-79517a3b7603" />
Mods for Claude Code that add a band above the prompt, a side pane or a slash command. Each is its own plugin, so you can install only the ones you want.
| Plugin | Band | What it does | Command | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
band-rule | line | Draws a dim line above the bands to separate them from Claude's output. List it before the other mods (see below). | none | |||||||||
cache-clock | CACHE | Shows whether the prompt cache is still warm, the time left, % reused and the cold-rewrite cost, with a Compact button. Toasts shortly before the cache goes cold. | `/cache-clock on \ | off \ | status` | |||||||
usage-gauge | USAGE | Plan usage windows (5h, week, others) with reset times, the session cost and a pace warning. Toasts at 75% and 90%. | /usage-gauge (opens a pane), `on \ | off \ | warn <a> <b> \ | status` | ||||||
usage-limits | footer | Session (5h) and week (7d) rate-limit bars in the prompt footer. | none | |||||||||
auto-handoff | CONTEXT | Watches how full the chat is and saves a HANDOFF.md when it reaches a threshold, then offers to clear and resume. | `/auto-handoff on \ | off \ | now \ | resume \ | threshold <n> \ | mode auto\ | ask \ | hide \ | show \ | status` |
agent-watch | AGENTS | Shows the main chat and helper agents with their models and tasks, plus the skills that ran this turn. Opens an Agents pane. | /agent-watch (opens the Agents pane), `on \ | off \ | clear \ | help` |
Bands stack in the order their plugins appear in enabledPlugins in ~/.claude/settings.json, first on top. To reorder, move the lines and run /reload-plugins; claude plugin install adds new entries at the end. band-rule draws its line above the whole stack only when it is listed first. The author's order, top to bottom: band-rule, agent-watch, auto-handoff, cache-clock, usage-gauge, usage-limits. The last one draws in the footer, so its place doesn't matter. The Band column is the title each mod shows in its band.
Each band is one row. In a narrow terminal the end of the row is cut off with … instead of wrapping.
usage-gauge and usage-limits both show plan usage, so enable only one.
In auto mode, auto-handoff sends the handoff prompt as soon as the threshold is crossed, even while Claude is mid-task. The default ask mode only offers the button.
cache-clock has a cacheMinutes option (5 or 60, default 60). Other settings are set through each mod's commands and persist between sessions.
Install a mod from the marketplace:
/plugin install cache-clock --marketplace Abdallah-Abdelazim/claude-code-mods
Run it once per mod, changing the plugin name. From a local clone, use --marketplace ~/path/to/claude-code-mods instead.
To try one without installing: claude --plugin-dir plugins/<name>.
These mods use Claude Code's function-hook plugin API, which is in early access; they were written against Claude Code 2.1.303. Where an organization manages Claude Code, plugins you install don't run the session.compact, skill.prompt and prompt.compose hooks.
Each folder under plugins/ is a standalone mod: .claude-plugin/plugin.json, hooks/, tsconfig.json, a *.test.ts and, for a mod that keeps state, types/index.d.ts. See CLAUDE.md for the conventions and the release steps.
claude plugin validate plugins/<name>
claude plugin test plugins/<name>
The engine writes .claude-plugin/types/ inside a mod when it loads it; the folder is git-ignored.
hooks/register.tsx 240 lines1import { atom, read, update } from 'claude-code'
2import type { AgentSpawnInput, AgentSpawnResult, EngineInterface as Engine, Register } from 'claude-code'
3
4import type { WatchAgent } from '../types'
5import { CHIP_WIDTH, SKILL_COLOR, bandSegs, cut, markOf, modelColor, modelTag } from './layout'
6
7const KEY = 'agent-watch'
8const PANE = 'agent-watch'
9const HELP = 'Usage: /agent-watch (opens the Agents pane) | on | off | clear | help'
10
11const isOnA = atom({ plugin: 'agent-watch', key: 'isOn' } as const, true)
12const agentsA = atom({ plugin: 'agent-watch', key: 'agents' } as const, [])
13const skillsA = atom({ plugin: 'agent-watch', key: 'skills' } as const, [])
14const modelsA = atom({ plugin: 'agent-watch', key: 'models' } as const, {})
15const timelineA = atom({ plugin: 'agent-watch', key: 'timeline' } as const, [])
16const turnStartA = atom({ plugin: 'agent-watch', key: 'turnStart' } as const, null)
17const activeA = atom({ plugin: 'agent-watch', key: 'active' } as const, 'main')
18const fmtTokens = (n: number) => (n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : `${n}`)
19
20async function logEvent($: Engine, text: string) {
21 const at = await $.clock.now()
22 await update($, timelineA, t => [...t, { at, text }].slice(-60))
23}
24
25async function patch($: Engine, id: string, fn: (a: WatchAgent) => WatchAgent) {
26 await update($, agentsA, rows => rows.map(r => (r.id === id ? fn(r) : r)))
27}
28
29async function loadOn($: Engine) {
30 const raw = (await $.store.get(KEY)) as { isOn?: boolean } | null
31 await update($, isOnA, () => raw?.isOn ?? true)
32}
33
34async function setOn($: Engine, isOn: boolean) {
35 await $.store.set(KEY, { isOn })
36 await update($, isOnA, () => isOn)
37}
38
39async function clearAll($: Engine) {
40 await update($, agentsA, () => [])
41 await update($, skillsA, () => [])
42 await update($, modelsA, () => ({}))
43 await update($, timelineA, () => [])
44 await update($, turnStartA, () => null)
45}
46
47async function startTurn($: Engine, text: string) {
48 const now = await $.clock.now()
49 const main: WatchAgent = {
50 id: 'main', parent: null, type: 'main', task: text.split('\n')[0] ?? '',
51 model: null, status: 'running', steps: 0, calls: 0, tokens: 0, skills: [],
52 }
53 await clearAll($)
54 await update($, agentsA, () => [main])
55 await update($, turnStartA, () => now)
56 await update($, activeA, () => 'main')
57 await logEvent($, 'turn started')
58}
59
60async function recordSpawn($: Engine, e: AgentSpawnInput, r: AgentSpawnResult) {
61 const parent = e.parentAgentId ?? 'main'
62 const id = r.agentId ?? e.tool_use_id
63 const task = e.description || e.prompt.split('\n')[0] || ''
64 const row: WatchAgent = {
65 id, parent, type: e.subagentType, task, model: r.model ?? null,
66 status: r.deny === undefined ? 'running' : 'failed', steps: 0, calls: 0, tokens: 0, skills: [],
67 }
68 await update($, agentsA, rows => [...rows.filter(x => x.id !== id), row])
69 await logEvent($, `${e.subagentType} ${r.deny === undefined ? 'started' : 'refused'}`)
70}
71
72async function recordStep($: Engine, agentId: string | undefined, model: string, tokens: number) {
73 const id = agentId ?? 'main'
74 await patch($, id, a => ({ ...a, model, steps: a.steps + 1, tokens: a.tokens + tokens }))
75 await update($, modelsA, m => {
76 const prev = m[model] ?? { steps: 0, tokens: 0 }
77 return { ...m, [model]: { steps: prev.steps + 1, tokens: prev.tokens + tokens } }
78 })
79}
80
81async function recordComplete($: Engine, agentId: string | undefined, isFailed: boolean) {
82 const id = agentId ?? 'main'
83 const status = isFailed ? 'failed' : 'done'
84 const row = (await read($, agentsA)).find(a => a.id === id)
85 await patch($, id, a => ({ ...a, status }))
86 if (row) await logEvent($, `${row.type} ${status}`)
87}
88
89// A skill counts once per turn, under the agent that was running when it loaded.
90async function recordSkill($: Engine, skill: string) {
91 if ((await read($, skillsA)).includes(skill)) return
92 const id = await read($, activeA)
93 await update($, skillsA, s => [...s, skill])
94 await patch($, id, a => ({ ...a, skills: [...a.skills, skill] }))
95 await logEvent($, `skill ${skill}`)
96}
97
98export const register: Register = on => {
99 on('session.start', async ($, e, next) => {
100 await $.command.register({
101 name: 'agent-watch',
102 description: 'Show which agents, models and skills are working this turn.',
103 argumentHint: 'on | off | clear | help',
104 })
105 await loadOn($)
106
107 return next(e)
108 })
109
110 on('turn.start', async ($, e, next) => {
111 if (e.agentId === undefined) await startTurn($, e.text)
112
113 return next(e)
114 })
115
116 on('turn.step', async function* ($, e, next) {
117 await update($, activeA, () => e.agentId ?? 'main')
118 const result = yield* next(e)
119 const u = result.usage
120 await recordStep($, e.agentId, u?.model ?? e.model, u ? u.input_tokens + u.output_tokens + u.cache_read_input_tokens + u.cache_creation_input_tokens : 0)
121
122 return result
123 })
124
125 on('tool.call', async ($, e, next) => {
126 await patch($, e.agentId ?? 'main', a => ({ ...a, calls: a.calls + 1 }))
127
128 return next(e)
129 }).catch(($, e, next) => next(e))
130
131 on('agent.spawn', async ($, e, next) => {
132 const result = await next(e)
133 await recordSpawn($, e, result)
134
135 return result
136 }).catch(($, e, next) => next(e))
137
138 on('skill.prompt', async ($, e, next) => {
139 await recordSkill($, e.skill)
140
141 return next(e)
142 })
143
144 on('turn.complete', async ($, e, next) => {
145 await recordComplete($, e.agentId, e.isAborted || 'refusal' in e)
146
147 return next(e)
148 })
149
150 on('command.run', { command: 'agent-watch' }, async ($, e) => {
151 const arg = e.args.trim()
152 if (arg === '') {
153 await $.ui.open({ id: PANE, title: 'Agents' })
154 return { text: 'Agents pane opened.' }
155 }
156 if (arg === 'on' || arg === 'off') {
157 await setOn($, arg === 'on')
158 return { text: `Agent watch ${arg}.` }
159 }
160 if (arg === 'clear') {
161 await clearAll($)
162 return { text: 'Agent watch cleared.' }
163 }
164
165 return { text: HELP }
166 })
167
168 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
169 const below = await next(e)
170 const agents = await read($, agentsA)
171 if (!(await read($, isOnA))) return below
172
173 const { Box, Text } = $.ui.resolve(e)
174 const segs =
175 agents.length === 0
176 ? [{ text: ' idle', dim: true }]
177 : bandSegs(agents, await read($, skillsA), e.props.bodyColumns - CHIP_WIDTH)
178
179 return (
180 <Box flexDirection="column">
181 <Box>
182 <Box flexShrink={0}><Text bold color="#0b0b0f" backgroundColor="#bb9af7"> ◎ AGENTS </Text></Box>
183 <Text wrap="truncate-end">
184 {segs.map(s => (
185 <Text color={s.color} dimColor={s.dim}>{s.text}</Text>
186 ))}
187 </Text>
188 </Box>
189 {below}
190 </Box>
191 )
192 })
193
194 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
195 const { Box, Text } = $.ui.resolve(e)
196 const agents = await read($, agentsA)
197 const models = await read($, modelsA)
198 const timeline = await read($, timelineA)
199 const turnStart = await read($, turnStartA)
200
201 const rows: { a: WatchAgent; depth: number }[] = []
202 const walk = (parent: string | null, depth: number) => {
203 for (const a of agents.filter(x => x.parent === parent)) {
204 rows.push({ a, depth })
205 walk(a.id, depth + 1)
206 }
207 }
208 walk(null, 0)
209
210 return (
211 <Box flexDirection="column">
212 {agents.length === 0 && <Text dimColor>No agents yet this turn.</Text>}
213 {rows.map(({ a, depth }) => {
214 const pad = ' '.repeat(depth)
215 const mark = markOf(a.status)
216 return (
217 <Box flexDirection="column">
218 <Text bold>{pad}{a.type} — {a.task || 'no task'} <Text color={mark.color}>{mark.text}</Text></Text>
219 <Text dimColor>
220 {pad}{' '}{modelTag(a.model).label} · {a.steps} steps · {a.calls} calls · {fmtTokens(a.tokens)} tokens
221 {a.skills.length ? ` · skills: ${a.skills.join(', ')}` : ''}
222 </Text>
223 </Box>
224 )
225 })}
226 {Object.keys(models).length > 0 && <Text bold>Models</Text>}
227 {Object.entries(models).map(([id, m]) => (
228 <Text color={modelColor(id)}>{' '}{modelTag(id).label}{' '}{m.steps} steps · {fmtTokens(m.tokens)} tokens</Text>
229 ))}
230 {e.surface === 'terminal' && timeline.length > 0 && <Text bold>Timeline</Text>}
231 {e.surface === 'terminal' &&
232 timeline.slice(-12).map(t => {
233 const s = Math.max(0, Math.round((t.at - (turnStart ?? t.at)) / 1000))
234 return <Text dimColor>{' '}+{Math.floor(s / 60)}:{String(s % 60).padStart(2, '0')} {cut(t.text, 60)}</Text>
235 })}
236 </Box>
237 )
238 })
239}
240hooks/layout.ts 76 lines1import type { WatchAgent } from '../types'
2
3export type Seg = { text: string; color?: string; dim?: boolean }
4
5const MODEL_COLOR: Record<string, string> = {
6 fable: '#d16bff',
7 opus: '#ff8c1a',
8 sonnet: '#3d9bff',
9 haiku: '#19c3a6',
10}
11const MARK = { running: '⟳', done: '✓', failed: '✗' } as const
12const MARK_COLOR = { running: '#f59e0b', done: '#22c55e', failed: '#ef4444' } as const
13export const SKILL_COLOR = '#a3e635'
14export const CHIP_WIDTH = 11 // " ◎ AGENTS ", every band chip is 11 cells
15
16// claude-opus-5-5 -> opus·5.5; a dated id keeps only the version.
17export function modelTag(id: string | null) {
18 if (!id) return { family: '?', label: '?' }
19 const m = /(fable|opus|sonnet|haiku)(?:-(\d+)(?:-(\d{1,2}))?)?/.exec(id)
20 if (!m) return { family: id, label: id }
21 const ver = m[2] ? (m[3] ? `${m[2]}.${m[3]}` : m[2]) : ''
22 return { family: m[1] ?? id, label: ver ? `${m[1]}·${ver}` : (m[1] ?? id) }
23}
24
25export const modelColor = (id: string | null) => MODEL_COLOR[modelTag(id).family]
26export const markOf = (s: WatchAgent['status']) => ({ text: MARK[s], color: MARK_COLOR[s] })
27
28export const cut = (task: string, max = 22) => (task.length > max ? `${task.slice(0, max - 1)}…` : task)
29
30function agentSegs(a: WatchAgent, showTask: boolean): Seg[] {
31 const tag = modelTag(a.model)
32 const mark = markOf(a.status)
33 const segs: Seg[] = [{ text: a.type }, { text: ` ${tag.label}`, color: MODEL_COLOR[tag.family] }]
34 if (showTask && a.task && a.id !== 'main') segs.push({ text: ` "${cut(a.task)}"`, dim: true })
35 segs.push({ text: ` ${mark.text}`, color: mark.color })
36 return segs
37}
38
39const width = (segs: Seg[]) => segs.reduce((n, s) => n + s.text.length, 0)
40
41// Too wide: drop task text from the oldest helpers, then fold the newest into "+N more",
42// then trim skills. The first helper's task and one skill always stay.
43export function bandSegs(agents: WatchAgent[], skills: string[], avail: number): Seg[] {
44 const [main, ...helpers] = agents
45 if (!main) return []
46 const keep = Math.max(0, helpers.findIndex(h => h.task))
47 const showTask = helpers.map(() => true)
48 let hidden = 0
49 let nSkills = skills.length
50
51 const build = () => {
52 const segs = agentSegs(main, false).map((s, i) => (i === 0 ? { ...s, text: ` ${s.text}` } : s))
53 helpers.slice(0, helpers.length - hidden).forEach((h, i) => {
54 segs.push({ text: i === 0 ? ' │ ' : ' · ', dim: true }, ...agentSegs(h, showTask[i] ?? false))
55 })
56 if (hidden) segs.push({ text: ` +${hidden} more`, dim: true })
57 if (skills.length) {
58 segs.push({ text: ' │ skills:', dim: true })
59 skills.slice(0, nSkills).forEach(s => segs.push({ text: ` ${s}`, color: SKILL_COLOR }))
60 if (nSkills < skills.length) segs.push({ text: ` +${skills.length - nSkills}`, dim: true })
61 }
62 return segs
63 }
64
65 let segs = build()
66 while (width(segs) > avail) {
67 const i = showTask.findIndex((s, j) => s && j !== keep)
68 if (i >= 0) showTask[i] = false
69 else if (helpers.length - hidden - 1 > keep) hidden += 1
70 else if (nSkills > 1) nSkills -= 1
71 else break
72 segs = build()
73 }
74 return segs
75}
76types/index.d.ts 30 lines1export type WatchStatus = 'running' | 'done' | 'failed'
2export type WatchAgent = {
3 id: string
4 parent: string | null
5 type: string
6 task: string
7 model: string | null
8 status: WatchStatus
9 steps: number
10 calls: number
11 tokens: number
12 skills: string[]
13}
14export type WatchModel = { steps: number; tokens: number }
15export type WatchEvent = { at: number; text: string }
16
17declare module 'claude-code' {
18 interface PluginState {
19 'agent-watch': {
20 isOn: boolean
21 agents: WatchAgent[]
22 skills: string[]
23 models: Record<string, WatchModel>
24 timeline: WatchEvent[]
25 turnStart: number | null
26 active: string
27 }
28 }
29}
30