Pick model and effort for subagents, workflow agents and new sessions, plus a saved delegation note

A Claude Code mod that lets you pick the model and effort Claude uses when it delegates work, and keeps a delegation note that goes with every request.
Run /delegation to open the side pane:
planning/review: opus high
coding: sonnet medium
search/reading: haiku low
Every picker defaults to auto, which leaves the choice to Claude. Reset all puts everything back to auto.
While agents, workflow agents or nested sessions are running, a line above the prompt shows what they run with (◆ Delegation Agents 2 · haiku Nested sessions 1 · sonnet · low). It stays hidden when nothing is running or every setting is on auto.
agent() call of the script.At the prompt of a terminal session:
/plugin install delegation --marketplace anas1412/claude-delegation
From a shell:
claude plugin install delegation --marketplace anas1412/claude-delegation
In the desktop app, where /plugin does not run: click + next to the prompt box, then Plugins, Add marketplace with anas1412/claude-delegation, then Add plugin.
claude plugin test .
MIT
hooks/register.tsx 306 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Prefs, Running } from '../types'
5
6const PANE = 'delegation'
7const AUTO = 'auto'
8const MODELS = [AUTO, 'fable', 'opus', 'sonnet', 'haiku']
9const EFFORTS = [AUTO, 'low', 'medium', 'high', 'xhigh', 'max']
10// start_session takes full ids, not aliases.
11const MODEL_IDS: Record<string, string> = {
12 fable: 'claude-fable-5-1',
13 opus: 'claude-opus-5-5',
14 sonnet: 'claude-sonnet-5-5',
15 haiku: 'claude-haiku-5-5',
16}
17const DEFAULTS: Prefs = {
18 agentModel: AUTO,
19 agentEffort: AUTO,
20 workflowModel: AUTO,
21 workflowEffort: AUTO,
22 sessionModel: AUTO,
23 sessionEffort: AUTO,
24 note: '',
25}
26
27type SessionRow = { startedBy?: string; isRunning?: boolean }
28
29type Group = {
30 kind: keyof Running
31 title: string
32 hint: string
33 model: keyof Prefs
34 effort: keyof Prefs
35}
36const GROUPS: Group[] = [
37 {
38 kind: 'agents',
39 title: 'Agents',
40 hint: 'Subagents Claude starts with the Agent tool. Enforced.',
41 model: 'agentModel',
42 effort: 'agentEffort',
43 },
44 {
45 kind: 'workflow',
46 title: 'Workflow agents',
47 hint: 'Each agent() in a workflow script. Sent as instructions: Workflow has no model field to enforce.',
48 model: 'workflowModel',
49 effort: 'workflowEffort',
50 },
51 {
52 kind: 'sessions',
53 title: 'Nested sessions',
54 hint: 'New sessions Claude starts. Enforced on fresh sessions, capped at this session’s model and effort; forks inherit.',
55 model: 'sessionModel',
56 effort: 'sessionEffort',
57 },
58]
59
60const prefs = atom({ plugin: 'delegation', key: 'prefs' } as const, DEFAULTS)
61const running = atom({ plugin: 'delegation', key: 'running' } as const, {
62 agents: 0,
63 workflow: 0,
64 sessions: 0,
65} as Running)
66
67const set = (b: EngineInterface, patch: Partial<Prefs>) =>
68 update(b, prefs, p => {
69 const next = { ...p, ...patch }
70 void b.store.set('prefs', next)
71 return next
72 })
73
74const pick = (v: string) => (v === AUTO ? undefined : v)
75
76const label = (p: Prefs, g: Group) =>
77 [p[g.model], p[g.effort]].filter(v => v !== AUTO).join(' · ') || AUTO
78
79const section = (p: Prefs) => {
80 const note = p.note.trim()
81 return [
82 'The user picked these models and effort levels for delegated work ("auto" means choose yourself). They override any default rule about which model agents use.',
83 `- Agent tool subagents: model ${p.agentModel}, effort ${p.agentEffort}`,
84 `- Workflow agents: model ${p.workflowModel}, effort ${p.workflowEffort}. When writing a Workflow script, put these in every agent() call's opts unless "auto".`,
85 `- New sessions (start_session): model ${p.sessionModel}, effort ${p.sessionEffort}`,
86 note &&
87 `The user's delegation note: follow it when splitting work into agents, workflow tasks or sessions (e.g. which model each kind of task gets):\n${note}`,
88 ]
89 .filter(Boolean)
90 .join('\n')
91}
92
93// Running counts. Subagent and workflow-agent loops show up as turns with an
94// agentId; only Agent-tool agents are in $.agent.list(). Nested sessions run
95// in their own process, so they are polled while any is running.
96const loops = new Map<string, 'agents' | 'workflow'>()
97let hasWorkflow = false
98let poll: { cancel: () => void } | undefined
99
100const recount = ($: EngineInterface) =>
101 update($, running, r => ({
102 ...r,
103 agents: [...loops.values()].filter(k => k === 'agents').length,
104 workflow: [...loops.values()].filter(k => k === 'workflow').length,
105 }))
106
107const pollSessions = async ($: EngineInterface) => {
108 const res = await $.mcp.call('ccd_session_mgmt', 'list_sessions', { linked: true })
109 const block = res.content[0]
110 const rows = block?.type === 'text' ? (JSON.parse(block.text ?? '[]') as SessionRow[]) : []
111 const sessions = rows.filter(r => r.startedBy && r.isRunning).length
112 await update($, running, r => ({ ...r, sessions }))
113 if (sessions === 0) {
114 poll?.cancel()
115 poll = undefined
116 }
117}
118
119export const register: Register = on => {
120 on('session.start', async ($, e, next) => {
121 const saved = (await $.store.get('prefs')) as Partial<Prefs> | undefined
122 if (saved) await update($, prefs, () => ({ ...DEFAULTS, ...saved }))
123 await $.command.register({
124 name: 'delegation',
125 description: 'Pick model/effort for agents, workflows and sessions',
126 })
127 return next(e)
128 })
129
130 on('command.run', { command: 'delegation' }, async $ => {
131 await $.ui.open({ id: PANE, title: 'Delegation' })
132 return { text: 'Delegation pane opened.' }
133 })
134
135 on('prompt.compose', async ($, e, next) => {
136 const out = await next(e)
137 const text = section(await read($, prefs))
138 return {
139 sections: [...out.sections, { id: 'delegation:prefs', text, scope: 'session' }],
140 }
141 })
142
143 on('tool.call', { tool: 'Agent' }, async ($, e, next) => {
144 const p = await read($, prefs)
145 const model = pick(p.agentModel) as typeof e.model
146 const effort = pick(p.agentEffort) as typeof e.effort
147 return next({ ...e, ...(model && { model }), ...(effort && { effort }) })
148 }).catch(($, e, next) => next(e))
149
150 on('tool.call', { tool: 'mcp__ccd_session__start_session' }, async ($, e, next) => {
151 const input = e as typeof e & { context?: string; proposal_id?: string }
152 // model/effort apply to fresh sessions only; a proposal starts exactly as proposed.
153 if (input.context === 'fork' || input.proposal_id) return next(e)
154 const p = await read($, prefs)
155 const model = pick(p.sessionModel)
156 const effort = pick(p.sessionEffort)
157 return next({
158 ...e,
159 ...(model && { model: MODEL_IDS[model] }),
160 ...(effort && { effort }),
161 })
162 }).catch(($, e, next) => next(e))
163
164 on('tool.call', { tool: 'Workflow' }, ($, e, next) => {
165 hasWorkflow = true
166 return next(e)
167 })
168
169 on('tool.call', { tool: 'mcp__ccd_session__start_session' }, async ($, e, next) => {
170 const ran = await next(e)
171 // ponytail: 20s poll of list_sessions; switch to an event if the app ever raises one.
172 poll ??= $.clock.every(20_000, () => void pollSessions($).catch(() => {}))
173 void pollSessions($).catch(() => {})
174 return ran
175 })
176
177 on('turn.start', async ($, e, next) => {
178 const id = e.agentId
179 if (id !== undefined) {
180 const isAgent = (await $.agent.list()).some(a => a.id === id)
181 // Unlisted loops are workflow agents or the engine's own forks (compaction).
182 if (isAgent || hasWorkflow) {
183 loops.set(id, isAgent ? 'agents' : 'workflow')
184 await recount($)
185 }
186 }
187 return next(e)
188 })
189
190 on('turn.complete', async ($, e, next) => {
191 if (e.agentId !== undefined && loops.delete(e.agentId)) await recount($)
192 return next(e)
193 })
194
195 // Above the prompt, only while delegated work runs and the user set something.
196 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
197 const p = await read($, prefs)
198 const r = await read($, running)
199 const active = GROUPS.filter(g => r[g.kind] > 0)
200 if (e.props.hasSurvey || active.length === 0 || JSON.stringify(p) === JSON.stringify(DEFAULTS)) {
201 return next(e)
202 }
203 const { Text } = $.ui.resolve(e)
204
205 return (
206 <Text>
207 <Text color="claude">◆</Text> Delegation{' '}
208 <Text dimColor>
209 {active.map(g => `${g.title} ${r[g.kind]} · ${label(p, g)}`).join(' ')}
210 {p.note.trim() ? ' · note on' : ''}
211 </Text>
212 </Text>
213 )
214 })
215
216 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
217 if (e.surface === 'mobile') {
218 const { Text } = $.ui.resolve(e)
219 return <Text dimColor>Open /delegation on desktop or terminal to edit.</Text>
220 }
221 const { Box, Text, Select, Input, Button } = $.ui.resolve(e)
222 const p = await read($, prefs)
223 const select = (key: keyof Prefs, title: string, list: string[]) => (
224 <Select
225 key={key}
226 label={title}
227 value={p[key]}
228 options={list.map(value => ({ value, label: value === AUTO ? 'Auto' : value }))}
229 onSelect={value => set($, { [key]: value })}
230 />
231 )
232 const hasNote = p.note.trim() !== ''
233 const isDefault = JSON.stringify(p) === JSON.stringify(DEFAULTS)
234
235 return (
236 <Box flexDirection="column" gap="md">
237 <Text dimColor size="footnote">
238 Choose how Claude delegates. Auto leaves the choice to Claude.
239 </Text>
240
241 {GROUPS.map(g => {
242 const value = label(p, g)
243 const isAuto = value === AUTO
244 return (
245 <Box
246 key={g.title}
247 flexDirection="column"
248 gap="sm"
249 padding="sm"
250 borderStyle="round"
251 borderColor={isAuto ? 'subtle' : 'claude'}
252 >
253 <Box flexDirection="row" justifyContent="space-between">
254 <Text weight="semibold">{g.title}</Text>
255 <Text size="caption" color={isAuto ? 'subtle' : 'claude'}>
256 {isAuto ? 'Auto' : value}
257 </Text>
258 </Box>
259 <Text dimColor size="caption">
260 {g.hint}
261 </Text>
262 <Box flexDirection="row" flexWrap="wrap" gap="md">
263 {select(g.model, 'Model', MODELS)}
264 {select(g.effort, 'Effort', EFFORTS)}
265 </Box>
266 </Box>
267 )
268 })}
269
270 <Box
271 flexDirection="column"
272 gap="sm"
273 padding="sm"
274 borderStyle="round"
275 borderColor={hasNote ? 'claude' : 'subtle'}
276 >
277 <Box flexDirection="row" justifyContent="space-between">
278 <Text weight="semibold">Delegation note</Text>
279 <Text size="caption" color={hasNote ? 'success' : 'subtle'}>
280 {hasNote ? 'Saved' : 'Empty'}
281 </Text>
282 </Box>
283 <Text dimColor size="caption">
284 Sent with every request. Say which model each kind of task should get.
285 </Text>
286 <Input
287 key="note"
288 rows={5}
289 value={p.note}
290 placeholder={'planning/review: opus high\ncoding: sonnet medium\nsearch/reading: haiku low'}
291 submitLabel="save"
292 onInput={note => set($, { note })}
293 onSubmit={note => set($, { note })}
294 />
295 </Box>
296
297 {!isDefault && (
298 <Box flexDirection="row" justifyContent="flex-end">
299 <Button key="reset" label="Reset all" onPress={() => set($, DEFAULTS)} />
300 </Box>
301 )}
302 </Box>
303 )
304 })
305}
306types/index.d.ts 18 lines1export type Prefs = {
2 agentModel: string
3 agentEffort: string
4 workflowModel: string
5 workflowEffort: string
6 sessionModel: string
7 sessionEffort: string
8 note: string
9}
10
11export type Running = { agents: number; workflow: number; sessions: number }
12
13declare module 'claude-code' {
14 interface PluginState {
15 delegation: { prefs: Prefs; running: Running }
16 }
17}
18