SLOPSHOPPER

reels

YouTube Shorts in a terminal pane: plays while Claude works, pauses when Claude is done.

newpanecommandprocessnetwork
★ 167v0.1.3MITupdated 2026-10-08hamzafer/claude-code-mods/mods/reels
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · reels
│ ┃ reels ✕ › fix the failing auth test and add an audit log call │ ┃ Waiting for the first frame… │ ┃ ⏺ 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 │ ┃ │ ┃ › /reels │ ┃ ⎿ reels: Reels needs Playwright once: npm install --prefix /Users/ │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ │ ┃ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · reels
Waiting for the first frame… [ Prev ] [ Next ] [ Mute ] [ Stop ] Needs Playwright: see /reels
README

<h1 align="center">Claude Code mods</h1>

<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT"></a> <a href="https://github.com/hamzafer/claude-code-mods/actions/workflows/ci.yml"><img src="https://github.com/hamzafer/claude-code-mods/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>

<a href="#-install">Install</a> · <a href="#-the-mods">All mods</a> · <a href="docs/mods.md">Docs</a> · <a href="https://claude.dev/blog/getting-started-with-claude-code-mods/">What are mods?</a>

<table> <tr> <td align="center" width="33%"><a href="docs/mods.md#-context-bar"><img src="images/context-bar.png" alt="context-bar: Claude Code context window usage as a stacked bar, a color per category" width="260"></a><br>📊 <b>context-bar</b><br>what fills your context</td> <td align="center" width="33%"><a href="docs/mods.md#-review-watch"><img src="images/review-watch.png" alt="review-watch: live lines for running Codex and subagent code reviews in Claude Code" width="260"></a><br>🔍 <b>review-watch</b><br>running code reviews, live</td> <td align="center" width="33%"><a href="docs/mods.md#-md-preview"><img src="images/md-preview.png" alt="md-preview: Markdown that Claude Code edits, rendered like GitHub next to the diff" width="260"></a><br>📝 <b>md-preview</b><br>Markdown rendered like GitHub</td> </tr> <tr> <td align="center" width="33%"><a href="docs/mods.md#-blast-radius"><img src="images/gallery/blast-radius.png" alt="blast-radius: a Claude Code hook holds rm -rf and lists the files it would delete" width="260"></a><br>💥 <b>blast-radius</b><br>see what <code>rm -rf</code> would delete</td> <td align="center" width="33%"><a href="docs/mods.md#-now-playing"><img src="images/now-playing.png" alt="now-playing: Spotify track, progress bar and synced lyrics inside Claude Code" width="260"></a><br>🎵 <b>now-playing</b><br>Spotify and its lyrics, live</td> <td align="center" width="33%"><a href="docs/mods.md#-reels-and-snake"><img src="images/reels-demo.gif" alt="reels: YouTube Shorts in a Claude Code pane while it works" width="260"></a><br>📱 <b>reels</b><br>Shorts while Claude works</td> </tr> <tr> <td align="center" width="33%"><a href="docs/mods.md#-where-am-i"><img src="images/gallery/where-am-i.png" alt="where-am-i: the session goal, current step and what waits on you, above the Claude Code prompt" width="260"></a><br>📍 <b>where-am-i</b><br>goal, now, waiting on you</td> <td align="center" width="33%"><a href="docs/mods.md#-lines-above-the-prompt"><img src="images/gallery/lines.png" alt="token-weather, usage-meter and other Claude Code status lines stacked above the prompt" width="260"></a><br>🌦️ <b>token-weather and friends</b><br>lines above the prompt</td> <td align="center" width="33%"><a href="#mission-control"><img src="images/gallery/mission-control.png" alt="mission-control: Claude Code subagents, tool calls and the files they touch, live" width="260"></a><br>🛰️ <b>mission-control</b><br>agents and the code they touch</td> </tr> </table>

🚀 Install

Add the marketplace once, then install any mod by name:

claude plugin marketplace add hamzafer/claude-code-mods
claude plugin install context-bar@claude-code-mods

Or install the general-purpose set in one go:

for m in context-bar token-weather usage-meter where-am-i next-steps agent-radar review-watch replay-theater md-preview blast-radius mission-control; do
  claude plugin install "$m@claude-code-mods"
done

Restart Claude Code after installing. To try one without installing:

git clone https://github.com/hamzafer/claude-code-mods && cd claude-code-mods
claude --plugin-dir mods/context-bar

Needs Claude Code 2.1.287+. A few mods need more (Chrome, gh, a connector); the tables say which.

🧩 The mods

👀 See what's happening

ModWhat it doesCommand
🛰️mission-controlLive map of agents, tool calls and the code they touch/mission
📊context-barYour context window as one stacked bar, a color per category, with token counts and where it compacts/context-bar
🌦️token-weatherContext fill from Clear to Compact soon, plus a prompt-cache countdown
⏱️cache-clockA prompt-cache line under your status line, from Claude Code's own figures: time left, hit rate and misses, and the tokens your next message re-caches once it goes cold. Needs Node and Claude Code 2.1.251+/cache-clock setup
📍where-am-iGoal, doing now, waiting on you, next step/where
➡️next-steps2 or 3 likely next prompts after each turn, one key to draft one1 2 3, 0 hides
💰usage-meter5-hour and 7-day plan usage, the reset countdown and the session's cost
💳openai-balanceYour OpenAI API credit: an estimated balance with a gauge, today's spend, where the money mostly went, and the last call. Needs an OpenAI organization Admin key/openai-balance
📡agent-radarOne live line per running subagent/radar
🔍review-watchOne live line per running code review (Codex or a review subagent) with the model, target, elapsed time and Codex's latest output. A toast lists the findings when it ends
🌐browser-lanesWhether this session has a browser, and who holds it/browser
🕌prayer-timesThe current prayer and how long is left, the next one, and zawal. Computed on your computer, Hanafi or standard Asr/prayers
🎬replay-theaterSteps through the last turn's edits, one diff at a time/replay
📝md-previewRenders the Markdown files Claude edits like GitHub does, with before and after side by side. Needs Chrome and a terminal that shows images/md

🛡️ Guard your repo

ModWhat it doesCommand
💥blast-radiusHolds rm -r, force pushes and migrations, shows what they'd delete, cancels after 60 s with no answer

💸 Spend less

ModWhat it doesCommand
🔀switchboardPicks the model for each subagent that doesn't name one, with OpenAI's Decisions API or Jev, from its short label only. Shows what every subagent cost/route

🔧 My setup (fork and adapt)

These are built around my own tools and rules. Fork them and change the rules to yours.

ModWhat it doesCommand
👀glanceOne line with what needs you: next meeting, PRs, Linear issues, Slack DMs. Needs gh and the Google Calendar, Linear and Slack connectors/glance
🚦merge-gateHolds gh pr merge until CI is green and Codex reviewed once. Needs gh and the Codex CLI. Reviews run on one fixed model; change it to yours/gate
📏rulebook-guardEnforces my writing and git rules: rewrites em dashes, asks before --amend, unformatted pushes, emails and phone numbers in notes
💾session-saverSaves where you left off, shows it on resume. Needs unpause/park [note]

🎮 For fun (opt-in)

ModWhat it doesCommand
📱reelsYouTube Shorts while Claude works, pauses when it's done/reels
🐍snakeSnake while Claude works/snake
🎵now-playingWhat Spotify is playing, with a progress bar, the lyric being sung, and ⏮ ⏸ ⏭ buttons. Needs macOS and the Spotify app/music

<a id="mission-control"></a>

🛰️ Flagship: mission-control

Every subagent, every tool call and every file they touch, in a pane next to the chat. Shown at 4x: two subagents building a logout feature across four files.

mission-control at 4x: two subagents and a logout feature landing across four files

Install it like any mod, restart, and type /mission (or /mission code to open the code map). q closes it.

  • 🤖 w shows the agents and every tool call, live
  • 🗺️ c shows the code map, with import arrows
  • 🔵 Blue while the agent reads a file
  • 🟠 Orange while it writes
  • 🟢 Green when done, with one line on what changed

The Code view also needs macOS, Google Chrome and a terminal that shows images (Ghostty, kitty, iTerm2). The Who view works everywhere.

📚 More

Source 2 files
hooks/register.tsx 197 lines
1// Reels: YouTube Shorts in a pane, mirrored from a hidden Chrome (helper/reels.cjs).
2// It plays while Claude works and pauses when Claude is done. Nothing starts until /reels.
3import { atom, read, update } from 'claude-code'
4import type { EngineInterface, Register } from 'claude-code'
5
6import type { ReelsStatus } from '../types'
7
8const PANE = 'reels'
9const IMAGE = 'reel'
10const FOOTER_ROWS = 2 // the controls and the status line under the picture
11
12const status = atom({ plugin: 'reels', key: 'status' } as const, 'off' as ReelsStatus)
13const isMuted = atom({ plugin: 'reels', key: 'isMuted' } as const, false)
14
15// The newest frame, the picture's mounted size and whether Claude is working; a reload starts them over.
16const live: { frame: { file: string; n: number } | null; size: { columns: number; rows: number } | null; isWorking: boolean } = {
17  frame: null,
18  size: null,
19  isWorking: false,
20}
21
22export const register: Register = on => {
23  on('session.start', async ($, e, next) => {
24    const r = await next(e)
25    await $.command.register({ name: 'reels', description: 'Play Shorts in a pane while Claude works; /reels login signs in, /reels stop ends it' })
26    return r
27  })
28
29  on('command.run', { command: 'reels' }, async ($, e) => {
30    const arg = e.args.trim().toLowerCase()
31    const p = await paths($)
32    if (arg === 'stop') {
33      await send($, '/quit')
34      await update($, status, () => 'off')
35      await $.ui.close({ id: PANE })
36      return { text: 'Reels stopped.' }
37    }
38    if (!(await $.fs.exists(`${p.modules}/playwright`))) {
39      await update($, status, () => 'missing')
40      return { text: `Reels needs Playwright once: npm install --prefix ${p.dir} playwright@1.63.0` }
41    }
42    if (arg === 'login') {
43      await send($, '/quit') // the profile opens in one Chrome at a time
44      void drain($.process.spawn({ argv: helperArgv($, p, 'login'), env: { NODE_PATH: p.modules } })).catch(() => {})
45      return { text: 'A YouTube window opened. Accept cookies or sign in, close it, then run /reels.' }
46    }
47    await $.ui.open({ id: PANE, title: 'Reels', focus: true })
48    if (!(await send($, '/state'))) await start($, p)
49    if (live.isWorking) await send($, '/play')
50    return { text: 'Reels is on. It plays while Claude works.' }
51  })
52
53  on('turn.start', async ($, e, next) => {
54    live.isWorking = true
55    if (await send($, '/play')) await update($, status, () => 'playing')
56    return next(e)
57  })
58
59  on('turn.complete', async ($, e, next) => {
60    const r = await next(e)
61    if (!e.agentId) {
62      live.isWorking = false
63      if (await send($, '/pause')) await update($, status, () => 'paused')
64    }
65    return r
66  })
67
68  on('session.end', async ($, e, next) => {
69    await send($, '/quit')
70    return next(e)
71  })
72
73  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
74    if (e.surface !== 'terminal') {
75      const { Text } = $.ui.resolve(e)
76      return <Text dimColor>Reels draws pictures in the terminal only.</Text>
77    }
78    const { Box, Text, Button, Image } = $.ui.resolve(e)
79    const now = await read($, status)
80    const muted = await read($, isMuted)
81
82    // A 9:16 picture: terminal cells are about twice as tall as wide.
83    const rows = Math.max(4, Math.min(255, e.props.scroll.bodyRows - FOOTER_ROWS))
84    const columns = Math.max(4, Math.min(255, e.props.bodyColumns, Math.round((rows * 9) / 8)))
85    live.size = { columns, rows }
86
87    const line = {
88      off: 'Run /reels to start',
89      starting: 'Starting Chrome…',
90      paused: "⏸ Claude's done, your turn",
91      playing: '▶ Playing while Claude works',
92      consent: 'YouTube wants cookie consent: run /reels login once',
93      missing: 'Needs Playwright: see /reels',
94      failed: 'Chrome did not start: run claude --debug',
95    }[now]
96
97    return (
98      <Box flexDirection="column">
99        {live.frame ? (
100          <Image key={IMAGE} source={{ file: live.frame.file, format: 'png', generation: live.frame.n }} columns={columns} rows={rows} alt="YouTube Shorts" />
101        ) : (
102          <Box height={rows}>
103            <Text dimColor>{now === 'off' ? ' ' : 'Waiting for the first frame…'}</Text>
104          </Box>
105        )}
106        <Box flexDirection="row" gap={1}>
107          <Button key="prev" label="Prev" hotkey="k" onPress={() => void send($, '/prev')} />
108          <Button key="next" label="Next" hotkey="j" variant="primary" onPress={() => void send($, '/next')} />
109          <Button
110            key="mute"
111            label={muted ? 'Unmute' : 'Mute'}
112            hotkey="m"
113            onPress={async () => {
114              if (await send($, '/mute')) await update($, isMuted, m => !m)
115            }}
116          />
117          <Button key="stop" label="Stop" hotkey="x" role="dismiss" onPress={() => void stopAll($)} />
118        </Box>
119        <Text dimColor={now !== 'paused'} color={now === 'paused' ? 'yellow' : undefined}>{line}</Text>
120      </Box>
121    )
122  })
123}
124
125async function start($: EngineInterface, p: Paths) {
126  await update($, status, () => 'starting')
127  // A child for the session's life: the loop ends with the helper or with this module.
128  void (async () => {
129    const child = $.process.spawn({ argv: helperArgv($, p, 'pane'), env: { NODE_PATH: p.modules } })
130    let rest = ''
131    for await (const piece of child) {
132      if (!('stream' in piece)) break
133      if (piece.stream === 'stderr') {
134        $.ui.log(piece.text, { to: 'debug' })
135        continue
136      }
137      const lines = (rest + piece.text).split('\n')
138      rest = lines.pop() ?? ''
139      for (const line of lines) await onLine($, line)
140    }
141    await update($, status, s => (s === 'consent' ? s : 'off'))
142  })().catch(async err => {
143    $.ui.log(`reels: helper did not start: ${String(err)}`, { to: 'debug' })
144    await update($, status, () => 'failed').catch(() => {})
145  })
146}
147
148async function onLine($: EngineInterface, line: string) {
149  if (line.startsWith('F ')) {
150    const [, n, file] = line.split(' ')
151    if (!file) return
152    live.frame = { file, n: Number(n) }
153    if (live.size) {
154      await $.ui.blit({ requestId: PANE, key: IMAGE, source: { file, format: 'png', generation: live.frame.n }, ...live.size }).catch(() => {})
155    }
156  } else if (line === 'R ready') {
157    await update($, status, () => (live.isWorking ? 'playing' : 'paused'))
158  } else if (line === 'C consent') {
159    await update($, status, () => 'consent')
160  }
161}
162
163type Paths = { dir: string; socket: string; profile: string; frames: string; modules: string }
164
165async function paths($: EngineInterface): Promise<Paths> {
166  const home = (await $.env.get('HOME')) ?? ''
167  const dir = `${home}/.claude-mods/reels`
168  return { dir, socket: `${dir}/ctl.sock`, profile: `${dir}/profile`, frames: `${dir}/frames`, modules: `${dir}/node_modules` }
169}
170
171function helperArgv($: EngineInterface, p: Paths, mode: 'pane' | 'login') {
172  return ['/usr/bin/env', 'node', `${$.plugin.root}/helper/reels.cjs`, p.socket, p.profile, p.frames, mode]
173}
174
175// Sends one command to the helper; false when it is not running.
176async function send($: EngineInterface, path: string) {
177  const { socket } = await paths($)
178  try {
179    const r = await $.http.fetch(`http://reels${path}`, { method: path === '/state' ? 'GET' : 'POST', socketPath: socket })
180    return r.ok
181  } catch {
182    return false
183  }
184}
185
186async function stopAll($: EngineInterface) {
187  await send($, '/quit')
188  await update($, status, () => 'off')
189  await $.ui.close({ id: PANE })
190}
191
192async function drain(stream: AsyncIterable<unknown>) {
193  for await (const _ of stream) {
194    // The login window's output is not needed; the loop keeps it alive until it closes.
195  }
196}
197
types/index.d.ts 8 lines
1export type ReelsStatus = 'off' | 'starting' | 'paused' | 'playing' | 'consent' | 'missing' | 'failed'
2
3declare module 'claude-code' {
4  interface PluginState {
5    'reels': { status: ReelsStatus; isMuted: boolean }
6  }
7}
8