SLOPSHOPPER

reels-while-you-wait

Opens Instagram Reels in a small centered Safari window while Claude works, autoscrolls, and closes it when Claude finishes.

newcommandtoastpromptprocess
v0.2.5no licenseupdated 2026-10-09jemsley06/reels-while-you-wait
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · reels-while-you-wait
› fix the failing auth test and add an audit log call ⏺ 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-while-you-wait: Reels while you wait: off ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

reels-while-you-wait

A Claude Code mod for macOS. When you submit a prompt, it opens Instagram Reels in a small window centered on your screen (in Safari, so your Chrome windows stay put), scrolls to the next reel each time one finishes, and closes the window and hands focus back to you when Claude is done.

Install

In a Claude Code terminal session:

/plugin install reels-while-you-wait --marketplace jemsley06/reels-while-you-wait

Answer y to add the marketplace, then press Enter for the user scope.

One-time setup

  1. Safari autoscroll: Safari → Settings → Advanced → tick Show features for web developers, then Settings → Developer → turn on Allow JavaScript from Apple Events. Without it, Reels opens and closes but won't scroll.
  2. Instagram: log in to Instagram in Safari.
  3. macOS permissions: the first run asks to let Claude control Safari and System Events. Allow both.

Turning it on and off

Type /reels to toggle it. Your choice is remembered across sessions.

Notes

  • Prompts that start with / (slash commands) never open Reels.
  • If Claude is in macOS full-screen mode, the Reels window can't float over it; use a normal window.
  • The window is a fixed 420×760, centered on the screen you're working on.
Source 2 files
hooks/register.ts 112 lines
1import type { Register } from 'claude-code'
2import { AUTOSCROLL_JS } from './autoscroll'
3
4const OPEN = `use framework "AppKit"
5use scripting additions
6set prev to path to frontmost application as text
7set wasRunning to application "Safari" is running
8-- \`launch\` starts Safari without the Start Page window a plain start opens
9if not wasRunning then tell application "Safari" to launch
10-- the screen holding the focused window; center a phone-sized 420x760 window on it
11set {{sx, sy}, {sw, sh}} to current application's NSScreen's mainScreen's frame() as list
12set x to sx + (sw - 420) div 2
13set y to (sh - 760) div 2
14tell application "Safari"
15  set existing to id of every window
16  make new document with properties {URL:"https://www.instagram.com/reels/"}
17  -- find our window as the new one on Instagram: right after creation \`front window\` can still be one of yours
18  set w to missing value
19  repeat 50 times
20    repeat with o in (every window)
21      if (id of o) is not in existing and (URL of current tab of o as text) contains "instagram.com" then set w to contents of o
22    end repeat
23    if w is not missing value then exit repeat
24    delay 0.1
25  end repeat
26  set bounds of w to {x, y, x + 420, y + 760}
27  set wid to id of w
28  -- Safari can still open its Start Page a moment after starting; close it so only Reels shows
29  if not wasRunning then
30    delay 1
31    repeat with o in (every window whose id is not wid)
32      if (URL of current tab of o as text) is in {"favorites://", "about:blank", "missing value"} then close o
33    end repeat
34  end if
35end tell
36-- not \`activate\`: that raises every Safari window over Claude; this raises only the new (key) one
37tell application "System Events" to set frontmost of process "Safari" to true
38return (wid as text) & linefeed & prev & linefeed & wasRunning`
39
40// every step checks Safari is running first: a \`tell\` to a quit Safari relaunches it, restoring old windows
41const INJECT = `on run argv
42  if application "Safari" is running then tell application "Safari" to do JavaScript (item 2 of argv) in current tab of (first window whose id is (item 1 of argv as integer))
43end run`
44
45const CLOSE = `on run argv
46  if not (application "Safari" is running) then return
47  -- read before closing: are you still watching, or have you moved on to something else?
48  tell application "System Events" to set stillWatching to frontmost of process "Safari"
49  tell application "Safari"
50    -- we started Safari: quit it unless you have a real page open (decided before closing: closes settle late)
51    set yours to 0
52    if item 3 of argv is "false" then
53      repeat with o in (every window whose id is not (item 1 of argv as integer))
54        if (URL of current tab of o as text) is not in {"favorites://", "about:blank", "missing value"} then set yours to yours + 1
55      end repeat
56    end if
57    if item 3 of argv is "false" and yours is 0 then
58      quit
59    else
60      close (every window whose id is (item 1 of argv as integer))
61    end if
62  end tell
63  if stillWatching then tell application (item 2 of argv) to activate
64end run`
65
66export const register: Register = on => {
67  // the open in flight, resolving to [windowId, prevApp, wasRunning]; awaited at turn end so a short turn can't miss it
68  // shortcut: it lives in a module variable, so a mod reload mid-turn leaves that window open
69  let opening: Promise<string[] | undefined> | undefined
70
71  on('session.start', async ($, e, next) => {
72    await $.command.register({ name: 'reels', description: 'Turn Reels-while-you-wait on or off' })
73    return next(e)
74  })
75
76  on('command.run', { command: 'reels' }, async $ => {
77    const isOff = !(await $.store.get('off'))
78    await $.store.set('off', isOff)
79    return { text: `Reels while you wait: ${isOff ? 'off' : 'on'}` }
80  })
81
82  on('prompt.submit', async ($, e, next) => {
83    // shortcut: skips every slash command (they may never end a turn, which would strand the window); skills invoked by slash won't get reels
84    if (!opening && !e.text.startsWith('/') && !(await $.store.get('off'))) {
85      opening = $.process.run(['osascript', '-e', OPEN]).then(opened => {
86        if (opened.exitCode === 0) return opened.stdout.trim().split('\n')
87        $.ui.toast(`reels: ${opened.stderr.trim()}`)
88      }).catch(err => void $.ui.toast(`reels: ${err}`))
89      const mine = opening
90      void opening.then(async win => {
91        if (!win || opening !== mine) return // turn already ended
92        await $.clock.sleep(5000) // let Instagram's page mount its <video>s
93        if (opening !== mine) return
94        const injected = await $.process.run(['osascript', '-e', INJECT, win[0], AUTOSCROLL_JS])
95        if (injected.exitCode !== 0)
96          $.ui.toast('reels: autoscroll needs Safari → Settings → Developer → Allow JavaScript from Apple Events')
97      }).catch(() => {}) // the window may already be closed when a turn is short
98    }
99    return next(e)
100  }).catch(($, e, next) => next(e)) // never block your prompt over a reels hiccup
101
102  on('turn.complete', async ($, e, next) => {
103    const result = await next(e)
104    if (!e.agentId && opening) {
105      const win = await opening
106      opening = undefined
107      if (win) await $.process.run(['osascript', '-e', CLOSE, ...win]).catch(() => {})
108    }
109    return result
110  })
111}
112
hooks/autoscroll.ts 13 lines
1// Injected into the Reels tab. Reels loop instead of firing 'ended', so "finished" = playhead near the end.
2export const AUTOSCROLL_JS = `(() => {
3  if (window.__reelScroll) return;
4  const done = new WeakSet();
5  window.__reelScroll = setInterval(() => {
6    const vids = [...document.querySelectorAll('video')];
7    const v = vids.find(v => !v.paused && v.duration && !done.has(v));
8    if (!v || v.currentTime < v.duration - 0.35) return;
9    done.add(v);
10    vids[vids.indexOf(v) + 1]?.scrollIntoView({ behavior: 'smooth', block: 'center' });
11  }, 250);
12})()`
13