SLOPSHOPPER

aside

/aside <task>: a background worker (Haiku; "opus:" for Opus) does a side task with tools while the chat goes on; its answer opens in a pane with Send to chat.

newpanecommandtoastpromptagents
v0.1.0MITupdated 2026-10-21dante32683/claude-code-mods/aside
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · aside
│ ┃ Aside ✕ › fix the failing auth test and add an audit log call │ ┃ No side tasks yet. Start one with /aside │ ┃ <task>. ⏺ 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 │ │ › /aside │ ⎿ aside: Usage: /aside <task>, or /aside opus: <task>. The Aside p │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Aside
No side tasks yet. Start one with /aside <task>.
README

claude-code-mods

A few mods for Claude Code that I use every day. They work in the terminal and in the desktop app.

agent-crabs

Install

From a shell:

claude plugin marketplace add dante32683/claude-code-mods
claude plugin install agent-crabs@dante32683-mods

Swap agent-crabs for any mod below. In the desktop app, click + next to the prompt box, then Plugins, add the marketplace dante32683/claude-code-mods, and pick the mods you want.

Mods load when a chat starts, so open a new chat after installing.

Mods

agent-crabs

A little Clawd above the prompt for each subagent that's running, with what it's doing right now. The crab acts it out: reading a book, walking around with a magnifying glass while it searches, writing on paper while it edits, jogging while a command runs, holding a globe while it's on the web, and thinking in between. It throws confetti when the subagent finishes and sits under a rain cloud if it fails. /crabs-demo plays every animation.

The animated crabs show in the desktop app; the terminal gets a small text crab.

usage-context

A pane with your plan's 5-hour and weekly usage, how fast you're using it, how much per hour or day you can spend and still make the reset, and what's filling the context window. While the pane is closed, the usage bars sit above the prompt. /meter shows or hides the pane. It saves the last reading to statusline-limits.json in your Claude folder.

compact-plus

Better compaction. Before each compaction it saves the whole conversation to a searchable file in ~/.claude/compact-history/ and tells Claude where it is, so details the summary drops can still be found. Your own messages are kept word for word, the summary is written as a handoff, instruction files are re-read afterward, and when a turn ends close to the auto-compact limit it compacts right away instead of mid-task. History older than 30 days is deleted. Options: turnEndPercent and verbatimUserTokens.

repo-peers

Tells Claude when other Claude chats are working in the same repo (same checkout or another worktree) and which files they've edited, so it can coordinate before work collides. Windows only for now.

aside

/aside <task> hands a side task to a background worker while your chat keeps going. Unlike /btw, the worker has tools. It sees the last few messages for context, and its answer opens in a pane where you can send it to the chat or dismiss it. Haiku by default; /aside opus: <task> uses Opus.

bash-fixes

Windows Git Bash only. It fixes a few ways Claude's Bash tool breaks there: very long commands get cut off or mangled, rg patterns starting with / get turned into Windows paths, sed -i doesn't say when nothing changed, and rm "${DIR:?}"/... keeps asking for approval.

Credits

agent-crabs takes its idea from hamzafer's agent-radar (MIT), and Clawd's shape from Codrops' article on reverse-engineering Claude's mascot animations. Clawd is Anthropic's mascot; this is a fan project, not affiliated with Anthropic. compact-plus's summary style is modeled on OpenAI's Codex CLI.

License

MIT

Source 2 files
hooks/register.ts 140 lines
1// aside: /aside <task> hands a side task to a background worker while the
2// chat goes on. Unlike /btw it has tools, so it can look things up, run
3// commands and edit files. Its answer opens in the Aside pane instead of
4// landing in the chat; "Send to chat" posts it as your message when you want
5// the main thread to see it. It is immediate: typed mid-turn it starts the
6// worker at once instead of queuing behind (or interrupting) the main turn.
7import { atom, read, update } from 'claude-code'
8import type { Register, EngineInterface } from 'claude-code'
9import type { AsideJob } from '../types'
10
11const PANE = 'aside'
12const TITLE = 'Aside'
13const jobs = atom({ plugin: 'aside', key: 'jobs' } as const, [])
14const CONTEXT_CHARS = 8000
15
16const WORKER_PROMPT = `You are a side worker. The user started you with /aside while their main chat with Claude keeps going, so do the task without waiting on that chat.
17
18- Do the task fully with your tools, then answer in a few short paragraphs or a compact list: what you found or changed, with file paths and line numbers where they help.
19- Don't commit, push, send messages, or act outside this machine unless the task says to.
20- The main chat may be editing the same repo. Change only what the task needs, and say exactly which files you touched.
21- If the task is unclear, make the most reasonable reading, say which one you took, and do it.`
22
23export function parseTask(args: string): { task: string; model: 'haiku' | 'opus' } {
24  const m = /^\s*opus:\s*/i.exec(args)
25  return m ? { task: args.slice(m[0].length).trim(), model: 'opus' } : { task: args.trim(), model: 'haiku' }
26}
27
28export async function recentContext($: EngineInterface): Promise<string> {
29  let msgs
30  try {
31    msgs = await $.session.messages()
32  } catch {
33    return ''
34  }
35  const out: string[] = []
36  let room = CONTEXT_CHARS
37  for (let i = msgs.length - 1; i >= 0 && room > 0; i--) {
38    const m = msgs[i]!
39    const text = m.text.trim()
40    if (!text || text.startsWith('<')) continue
41    const cut = text.length > 1500 ? text.slice(0, 1500) + ' ...' : text
42    out.unshift(`${m.role === 'user' ? 'User' : 'Claude'}: ${cut}`)
43    room -= cut.length
44  }
45  return out.join('\n\n')
46}
47
48// One notification block that names one of our workers: the answer is in the
49// pane already, so the main chat doesn't need to hear about it.
50export function isOurNotice(text: string, ids: string[]): boolean {
51  const blocks = text.match(/<task-notification>/g)?.length ?? 0
52  return blocks === 1 && ids.some(id => text.includes(id))
53}
54
55const setJob = ($: EngineInterface, id: string, patch: Partial<AsideJob>) =>
56  update($, jobs, list => list.map(j => (j.id === id ? { ...j, ...patch } : j)))
57
58export const register: Register = on => {
59  on('session.start', async ($, e, next) => {
60    const r = await next(e)
61    await $.command.register({ name: 'aside', description: 'Hand a side task to a background worker; its answer opens in a pane', argumentHint: '<task>  (start with "opus:" for Opus)', immediate: true })
62    await $.agent.register({ name: 'worker', description: 'Side worker for /aside (Haiku)', prompt: WORKER_PROMPT, model: 'haiku', effort: 'xhigh' })
63    await $.agent.register({ name: 'worker-opus', description: 'Side worker for /aside (Opus)', prompt: WORKER_PROMPT, model: 'opus', effort: 'medium' })
64    return r
65  })
66
67  // Only /aside starts these workers; Claude's Agent tool doesn't offer them.
68  on('agent.offer', { agent: 'aside:worker' }, () => ({ isOffered: false }))
69  on('agent.offer', { agent: 'aside:worker-opus' }, () => ({ isOffered: false }))
70
71  on('command.run', { command: 'aside' }, async ($, e) => {
72    const { task, model } = parseTask(e.args)
73    if (!task) {
74      await $.ui.open({ id: PANE, title: TITLE })
75      return { text: 'Usage: /aside <task>, or /aside opus: <task>. The Aside pane shows earlier answers.' }
76    }
77    const context = await recentContext($)
78    const prompt = `Task:\n${task}` + (context ? `\n\nThe main chat's recent messages, for context (don't continue that work unless the task asks):\n\n${context}` : '')
79    const r = await $.agent.spawn({ prompt, description: 'aside: ' + task.slice(0, 50), subagentType: model === 'opus' ? 'aside:worker-opus' : 'aside:worker' })
80    const id = 'agentId' in r ? r.agentId : undefined
81    if (!id) return { text: `Couldn't start the side worker: ${'deny' in r ? r.deny : 'no agent id'}` }
82    await update($, jobs, list => [...list, { id, task, model, status: 'running' as const, answer: '' }].slice(-20))
83    await $.ui.open({ id: PANE, title: TITLE })
84    return { text: `Side worker (${model}) started: ${task}` }
85  })
86
87  on('turn.complete', async ($, e, next) => {
88    const r = await next(e)
89    if (!e.agentId) return r
90    const list = await read($, jobs)
91    const job = list.find(j => j.id === e.agentId)
92    if (!job) return r
93    const failed = e.reason !== 'answer'
94    await setJob($, job.id, { status: failed ? 'failed' : 'done', answer: e.answer || (failed ? `Ended: ${e.reason}` : '') })
95    $.ui.toast(`Aside ${failed ? 'stopped' : 'done'}: ${job.task.slice(0, 60)}`)
96    await $.ui.open({ id: PANE, title: TITLE })
97    return r
98  }).catch(($, e, next) => next(e))
99
100  on('prompt.submit', async ($, e, next) => {
101    if (e.origin?.kind !== 'task-notification') return next(e)
102    const list = await read($, jobs)
103    if (isOurNotice(e.text, list.map(j => j.id))) return { drop: 'Side worker finished; its answer is in the Aside pane.' }
104    return next(e)
105  }).catch(($, e, next) => next(e))
106
107  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
108    const { Box, Text, Button, Markdown } = $.ui.resolve(e)
109    const list = (await read($, jobs)).slice().reverse()
110    if (!list.length) return Box({ children: [Text({ dimColor: true, children: ['No side tasks yet. Start one with /aside <task>.'] })] })
111    return Box({
112      flexDirection: 'column',
113      gap: 1,
114      children: list.map(j =>
115        Box({
116          key: 'job-' + j.id,
117          flexDirection: 'column',
118          children: [
119            Text({ bold: true, children: [j.task] }),
120            Text({ dimColor: true, children: [`${j.model} · ${j.status === 'running' ? 'working…' : j.status}`] }),
121            ...(j.answer ? [Markdown({ key: 'ans-' + j.id, text: j.answer })] : []),
122            Box({
123              flexDirection: 'row',
124              gap: 1,
125              children: [
126                ...(j.answer && j.status === 'done'
127                  ? [Button({ key: 'send-' + j.id, label: 'Send to chat', variant: 'primary', onPress: () => void $.prompt.submit({ text: `Side worker's answer to "${j.task}":\n\n${j.answer}`, asUser: true }).catch(() => {}) })]
128                  : []),
129                ...(j.status !== 'running'
130                  ? [Button({ key: 'dismiss-' + j.id, label: 'Dismiss', variant: 'secondary', onPress: () => void update($, jobs, l => l.filter(x => x.id !== j.id)) })]
131                  : []),
132              ],
133            }),
134          ],
135        }),
136      ),
137    })
138  })
139}
140
types/index.d.ts 16 lines
1// The side tasks this session started, newest last. `id` is the worker's
2// agentId; `answer` is its final message once it ends.
3export type AsideJob = {
4  id: string
5  task: string
6  model: 'haiku' | 'opus'
7  status: 'running' | 'done' | 'failed'
8  answer: string
9}
10
11declare module 'claude-code' {
12  interface PluginState {
13    aside: { jobs: AsideJob[] }
14  }
15}
16