/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.

A few mods for Claude Code that I use every day. They work in the terminal and in the desktop app.
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.
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.
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.
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.
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 <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.
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.
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.
MIT
hooks/register.ts 140 lines1// 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}
140types/index.d.ts 16 lines1// 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