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

A private reference collection of public Claude Code mods, with a security-oriented analysis.
mods/Nothing here is installed or executed. All code is by its upstream authors under their own licences (MIT, one Apache-2.0); each folder keeps its original LICENSE.
hooks/register.tsx 197 lines1// 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}
197types/index.d.ts 8 lines1export 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