A band above the prompt: Fable thinks (Reverb, Council, Summit, Deepen), Opus and Sonnet act, and a token meter keeps Fable's share honest.

A band above the prompt. Fable thinks, Opus and Sonnet act, and a token meter keeps Fable's share honest.
At the prompt of a Claude Code terminal session:
/plugin install thinkfirst-ambition --marketplace DCXTribe/claude-code-think-first
From a shell:
claude plugin install thinkfirst-ambition --marketplace DCXTribe/claude-code-think-first
In the desktop app: click + next to the prompt box, then Plugins, Add marketplace with DCXTribe/claude-code-think-first, then Add plugin.
Type your task in the prompt box, then press one key (the band must have focus: click it or ctrl+x tab). The first press opens a panel that explains the function: what it does, what you get back, when it helps and what Use puts in your prompt. Nothing is sent yet. Press the same key again (or the panel's run button) to run it. Another number switches the panel to that function, and c closes it.
| Key | Function | One Fable call that returns… |
|---|---|---|
1 | Reverb | the task's key noun rewritten as the real verb/outcome, one line |
2 | Council | 3–5 lenses, one sentence each, Red Team always last |
3 | Summit | goal beyond the goal · hard target (a number) · outside judge |
4 | Deepen | the 4-things answers, then ## Rewritten task |
The reply lands in a card above the prompt. u replaces your draft with it (Deepen uses only the rewritten task), a appends it to your draft as it is now, x closes it. Then press Enter as usual: the main model and its subagents do the work.
h hides the band, t switches the meter between summary and raw counts. /ambition shows it again; /ambition reset clears the meter; /ambition deepen <task> (or reverb, council, summit) runs a function on the task you give it, without focusing the band. Only one Fable call runs at a time.
fable (Reverb/Council/Summit at high effort, Deepen at xhigh), with no history: it reads only what is in the prompt box.fable if they run there. A system-prompt section tells the main model to delegate hard pieces to opus and routine ones to sonnet, and never to Fable./ambition <fn> <task>) and that function's fixed instructions. No conversation history goes with it. Nothing runs until you press a key or run the command.[Reverb], [Council] or [Summit] block in your prompt came from Fable..claude-plugin/plugin.json manifest
hooks/hooks.json → register.tsx
hooks/register.tsx the band, the four functions, the meter, the routing hooks
types/index.d.ts state contract (card, busy, spend, isHidden, showRaw, armed)
tests/band.test.tsx the band's drawing
tests/fixes.test.tsx routing, /ambition <fn> <task>, append, the busy guard, the meter
tests/preview.test.tsx first press explains, second press runs, switch and cancel
Validate with claude plugin validate ., type-check with tsc -p . once the engine has laid .claude-plugin/types/, test with claude plugin test ..
Edit the top of hooks/register.tsx: THINKER (the model the band calls), FABLE_WARN_SHARE (the amber threshold), and the FN table (system prompts, effort, token caps and the explanation panel's text per function).
hooks/register.tsx 501 lines1import { atom, read, update } from 'claude-code'
2import type { Register, ModelUsage, EngineInterface } from 'claude-code'
3
4import type { Card, Fn, Lane, Spend } from '../types'
5
6// ---------------------------------------------------------------------------
7// State
8// ---------------------------------------------------------------------------
9
10const PLUGIN = 'thinkfirst-ambition' as const
11const emptyLane = (): Lane => ({ in: 0, out: 0, cached: 0, calls: 0 })
12const emptySpend = (): Spend => ({
13 fable: emptyLane(),
14 opus: emptyLane(),
15 sonnet: emptyLane(),
16 other: emptyLane(),
17 spawns: { fable: 0, opus: 0, sonnet: 0, other: 0 },
18})
19
20// $.state outlives a reload, so a meter written by an older version may lack
21// fields this one counts: fill them with zeros rather than draw NaN.
22const asLane = (l: Partial<Lane> | undefined): Lane => ({ ...emptyLane(), ...l })
23const asSpend = (s: Partial<Spend> | undefined): Spend => ({
24 fable: asLane(s?.fable),
25 opus: asLane(s?.opus),
26 sonnet: asLane(s?.sonnet),
27 other: asLane(s?.other),
28 spawns: { ...emptySpend().spawns, ...s?.spawns },
29})
30
31const card = atom({ plugin: 'thinkfirst-ambition', key: 'card' } as const, null)
32const busy = atom({ plugin: 'thinkfirst-ambition', key: 'busy' } as const, null)
33const spend = atom({ plugin: 'thinkfirst-ambition', key: 'spend' } as const, emptySpend())
34const isHidden = atom({ plugin: 'thinkfirst-ambition', key: 'isHidden' } as const, false)
35const showRaw = atom({ plugin: 'thinkfirst-ambition', key: 'showRaw' } as const, false)
36const armed = atom({ plugin: 'thinkfirst-ambition', key: 'armed' } as const, null)
37
38// ---------------------------------------------------------------------------
39// Models and routing
40// ---------------------------------------------------------------------------
41
42/** The thinker. Every band function is one call to this model. */
43const THINKER = 'fable'
44/** Fable's share of all tokens above which the meter turns amber. */
45const FABLE_WARN_SHARE = 0.2
46
47type LaneName = 'fable' | 'opus' | 'sonnet' | 'other'
48
49export const laneOf = (model: string | undefined): LaneName => {
50 const m = (model ?? '').toLowerCase()
51 if (/fable|mythos/.test(m)) return 'fable'
52 if (/opus/.test(m)) return 'opus'
53 if (/sonnet/.test(m)) return 'sonnet'
54 return 'other'
55}
56
57/** The executor a subagent asked to run on Fable is moved to. */
58const FABLE_REROUTE = 'opus'
59
60/**
61 * The model a spawn should run on instead, or undefined to leave it.
62 *
63 * A spawn that names no model inherits its parent's (unless its agent type
64 * sets one, which this hook cannot see), so a Fable parent means a Fable
65 * child. A fork, a workflow agent and a resumed run keep their model: the
66 * engine refuses to change it.
67 */
68export const reroute = (e: {
69 model?: string
70 parentModel: string
71 fork: boolean
72 workflow?: unknown
73 resume?: unknown
74}): string | undefined => {
75 if (e.fork || e.workflow !== undefined || e.resume !== undefined) return undefined
76 return laneOf(e.model ?? e.parentModel) === 'fable' ? FABLE_REROUTE : undefined
77}
78
79const addUsage = (s0: Spend, lane: LaneName, u: ModelUsage): Spend => {
80 const s = asSpend(s0)
81 return {
82 ...s,
83 [lane]: {
84 in: s[lane].in + u.input_tokens + u.cache_creation_input_tokens,
85 out: s[lane].out + u.output_tokens,
86 cached: s[lane].cached + u.cache_read_input_tokens,
87 calls: s[lane].calls + 1,
88 },
89 }
90}
91
92/**
93 * Billable tokens: cache reads are left out, since every executor turn
94 * rereads the whole transcript at a tenth of the price, and counting them
95 * made Fable's share look far smaller than what it costs.
96 */
97const total = (l: Lane) => l.in + l.out
98const k = (n: number) => (n >= 1000 ? `${Math.round(n / 100) / 10}k` : `${n}`)
99
100// ---------------------------------------------------------------------------
101// The four functions. Each is one Fable call, high effort, no history.
102// ---------------------------------------------------------------------------
103
104type Detail = {
105 /** One line: what the function is. */
106 what: string
107 /** What lands in the card. */
108 gives: string
109 /** When to reach for it. */
110 when: string
111 /** What Use (u) puts in the prompt box. */
112 use: string
113}
114
115const FN: Record<Fn, { label: string; hotkey: string; effort: 'high' | 'xhigh'; maxTokens: number; system: string; detail: Detail }> = {
116 reverb: {
117 label: 'Reverb',
118 hotkey: '1',
119 effort: 'high',
120 maxTokens: 300,
121 system: [
122 'The user will give you a task. Find its key noun (the deliverable: a deck, a dashboard, a report, an app)',
123 'and rewrite it as the real verb: the outcome that noun exists to cause, for whom, under what constraint.',
124 'Example: "QBR deck" becomes "getting busy people to believe a story about cause and effect and decide under uncertainty in 30 minutes."',
125 'Answer with ONE line in that shape and nothing else. No preamble, no quotes, no options.',
126 ].join(' '),
127 detail: {
128 what: 'Finds the key noun in your task (the deck, the dashboard, the report) and rewrites it as the real verb: the outcome it exists to cause, for whom, under what constraint.',
129 gives: 'One line. "QBR deck" becomes "getting busy people to believe a story about cause and effect and decide in 30 minutes."',
130 when: 'Your task names a thing to make and you want the executor aiming at what the thing is for.',
131 use: 'A [Reverb] block with that line.',
132 },
133 },
134 council: {
135 label: 'Council',
136 hotkey: '2',
137 effort: 'high',
138 maxTokens: 700,
139 system: [
140 'The user will give you a task. Assemble a council of 3 to 5 lenses that would each change how this task is done.',
141 'Lenses are methods and checks, not costumes: "do what a forensic accountant does", never "write as if you are".',
142 'The last seat is always Red Team: the person in the room who wants this to fail and asks the question that kills it.',
143 'Format: one line per lens, "Name — one sentence stating what that lens checks or changes for THIS task." Red Team last. Nothing else.',
144 ].join(' '),
145 detail: {
146 what: 'Assembles 3 to 5 lenses (methods and checks, not personas) that would each change how this task is done. Red Team always sits last.',
147 gives: 'One line per lens: its name and what it checks or changes for this task.',
148 when: 'The task has several ways to go wrong and you want them named before any work starts.',
149 use: 'A [Council] block with the lenses.',
150 },
151 },
152 summit: {
153 label: 'Summit',
154 hotkey: '3',
155 effort: 'high',
156 maxTokens: 400,
157 system: [
158 'The user will give you a task. Raise its altitude. Answer in exactly three labelled lines and nothing else:',
159 'Goal beyond the goal: what should actually happen next because this exists, the outcome, not the surface task.',
160 'Hard target: an ambitious but solvable, measurable bar, with a number or a binary (200 ms, crash-free, 3 decisions logged), never an adjective.',
161 'Outside judge: something other than the user or a model that will show the result is right (a tool measurement, a backtest, real-user numbers, a statistical test).',
162 ].join(' '),
163 detail: {
164 what: 'Raises the altitude of your task: the outcome behind it, a bar it must clear, and something outside you and the model that proves it cleared it.',
165 gives: 'Three labelled lines: Goal beyond the goal · Hard target (a number or a yes/no) · Outside judge (a test, a measurement, real numbers).',
166 when: 'You know what to do but not what "done well" means, or want a result you can check.',
167 use: 'A [Summit] block with the three lines.',
168 },
169 },
170 deepen: {
171 label: 'Deepen',
172 hotkey: '4',
173 effort: 'xhigh',
174 maxTokens: 1500,
175 system: [
176 'The user will give you a task and wants a deeper version of it, not a bigger one. Before rewriting it:',
177 '1. Say what a top professional in this field would do here that they are not asking for.',
178 '2. Name the rawest material that professional would start from instead of what has already been digested for them.',
179 '3. Name the specific methods and tools they would use.',
180 '4. Say what, other than you or them, would show the result is right.',
181 'Then rewrite their prompt to ask for exactly that.',
182 'Format: the four answers as a numbered list, two sentences each at most; then a line "## Rewritten task" and the rewritten prompt, written to be sent as-is to an executor model.',
183 'Insist on real instruments (code, data, profilers, tests), not better vocabulary. Do not pad.',
184 ].join(' '),
185 detail: {
186 what: 'Asks what a top professional would do here that you are not asking for: their rawest starting material, their methods and tools, and what would show the result is right. Then rewrites your prompt to ask for exactly that.',
187 gives: 'The four answers, then a "Rewritten task" ready to send as-is.',
188 when: 'The task is worth doing properly and your draft asks for the shallow version.',
189 use: 'The rewritten task alone, replacing your draft.',
190 },
191 },
192}
193
194const FN_ORDER: Fn[] = ['reverb', 'council', 'summit', 'deepen']
195
196/**
197 * What a card puts in the prompt box: Deepen's rewritten task alone, the
198 * others as a labelled block. Appending leaves the draft as it stands now.
199 */
200export const compose = (c: Card): string => {
201 if (c.fn === 'deepen') {
202 const i = c.text.indexOf('## Rewritten task')
203 return i >= 0 ? c.text.slice(i + '## Rewritten task'.length).trim() : c.text.trim()
204 }
205 return `[${FN[c.fn].label}]\n${c.text.trim()}`
206}
207
208// ---------------------------------------------------------------------------
209// Actions (top-level so they may take $)
210// ---------------------------------------------------------------------------
211
212// Held in the module, not $.state, so two presses in one tick cannot both
213// pass the check before either writes `busy`. A reload drops it, and the
214// call it guarded with it.
215let inFlight: Fn | null = null
216
217/**
218 * One Fable call per function, on `task` when given (`/ambition deepen …`),
219 * else on the draft in the prompt box. False when there was nothing to run on
220 * or a call was already running; the person has been told which.
221 */
222const run = async ($: EngineInterface, fn: Fn, task?: string): Promise<boolean> => {
223 if (inFlight !== null) {
224 $.ui.toast('Fable is already thinking.')
225 return false
226 }
227 const source = (task ?? (await $.prompt.read()).text).trim()
228 if (source.length === 0) {
229 $.ui.toast(`Type your task in the prompt first, then press ${FN[fn].hotkey}, or run /ambition ${fn} <task>.`)
230 return false
231 }
232 inFlight = fn
233 void think($, fn, source)
234 return true
235}
236
237const think = async ($: EngineInterface, fn: Fn, source: string) => {
238 const spec = FN[fn]
239 try {
240 await update($, busy, () => fn)
241 $.ui.status(`Fable · ${spec.label}…`)
242 const r = await $.model.complete({
243 model: THINKER,
244 system: spec.system,
245 prompt: source,
246 effort: spec.effort,
247 maxTokens: spec.maxTokens,
248 timeoutMs: 120_000,
249 })
250 await update($, spend, (s) => addUsage(s, 'fable', r.usage))
251 if (r.isAnswered) {
252 const next: Card = { fn, source, text: r.text }
253 await update($, card, () => next)
254 } else {
255 $.ui.toast(`${spec.label}: no answer from Fable (${r.reason}).`)
256 }
257 } catch (err) {
258 // Only a request the engine refuses to send rejects (a blocked model, say).
259 $.ui.toast(`${spec.label}: Fable call refused (${err instanceof Error ? err.message : String(err)}).`)
260 } finally {
261 inFlight = null
262 await update($, busy, () => null)
263 $.ui.status(undefined)
264 }
265}
266
267// First press of 1–4 shows what the function does; a second press of the
268// same key runs it. Another key moves the preview; c cancels it.
269const press = async ($: EngineInterface, fn: Fn) => {
270 if ((await read($, armed)) === fn) {
271 if (await run($, fn)) await update($, armed, () => null)
272 return
273 }
274 await update($, armed, () => fn)
275}
276
277// Taking a card: replace the draft, or append to the draft as it is now (not
278// as it was when the function ran), then close the card.
279const take = async ($: EngineInterface, mode: 'replace' | 'append') => {
280 const c = await read($, card)
281 if (c === null) return
282 const block = compose(c)
283 const hasDraft = mode === 'append' && (await $.prompt.read()).text.trim().length > 0
284 const filled = await $.prompt.fill(hasDraft ? { text: `\n\n${block}`, mode: 'append' } : { text: block, mode: 'replace' })
285 if (!filled.isFilled) {
286 $.ui.toast('Could not fill the prompt box.')
287 return
288 }
289 await update($, card, () => null)
290}
291
292// ---------------------------------------------------------------------------
293// Register
294// ---------------------------------------------------------------------------
295
296export const register: Register = (on) => {
297 on('session.start', async ($, e, next) => {
298 await $.command.register({
299 name: 'ambition',
300 description: 'Fable-ambition band: show, hide, reset the meter, or run reverb|council|summit|deepen [task]',
301 })
302 return next(e)
303 })
304
305 // /ambition [show|hide|reset] or /ambition <reverb|council|summit|deepen> [task]
306 on('command.run', { command: 'ambition' }, async ($, e) => {
307 const args = e.args.trim()
308 const arg = (args.split(/\s+/)[0] ?? '').toLowerCase()
309 const task = args.slice(arg.length).trim()
310 if (arg === 'hide') {
311 await update($, isHidden, () => true)
312 return { text: 'Ambition band hidden. /ambition show brings it back.' }
313 }
314 if (arg === 'reset') {
315 await update($, spend, () => emptySpend())
316 return { text: 'Token meter reset.' }
317 }
318 if ((FN_ORDER as string[]).includes(arg)) {
319 const fn = arg as Fn
320 await update($, isHidden, () => false)
321 // Submitting the command empties the prompt box, so the task comes with it.
322 if (task.length === 0) {
323 return { text: `Usage: /ambition ${fn} <task>. Or type the task in the prompt and press ${FN[fn].hotkey} in the band.` }
324 }
325 await update($, armed, () => null)
326 const started = await run($, fn, task)
327 return { text: started ? `${FN[fn].label} running on Fable.` : 'Fable is already thinking; try again when it answers.' }
328 }
329 await update($, isHidden, () => false)
330 return { text: 'Ambition band shown. Keys 1–4: Reverb, Council, Summit, Deepen.' }
331 })
332
333 // ---- Token tracking --------------------------------------------------------
334
335 // Every turn that ends with usage: the main thread and each subagent's.
336 on('turn.complete', async ($, e, next) => {
337 if (e.usage) {
338 const lane = laneOf(e.usage.model)
339 await update($, spend, (s) => addUsage(s, lane, e.usage!))
340 }
341 return next(e)
342 })
343
344 // Executors act, Fable thinks: a subagent that would run on Fable, named or
345 // inherited from a Fable parent, is moved to Opus. A failing hook is
346 // skipped and the spawn goes ahead unchanged, which is the fallback wanted.
347 on('agent.spawn', async ($, e, next) => {
348 const model = reroute(e)
349 if (model !== undefined) {
350 $.ui.toast(`Subagent would run on Fable; routed to ${model} (Fable only thinks here).`)
351 }
352 const r = await next(model === undefined ? e : { ...e, model })
353 if (r.deny === undefined) {
354 // Count by what the engine resolved, not what was asked.
355 const lane = laneOf(r.model)
356 await update($, spend, (s0) => {
357 const s = asSpend(s0)
358 return { ...s, spawns: { ...s.spawns, [lane]: s.spawns[lane] + 1 } }
359 })
360 }
361 return r
362 })
363
364 // Tell the main model the routing rule, so its own delegation follows it.
365 on('prompt.compose', async ($, e, next) => {
366 const r = await next(e)
367 return {
368 sections: [
369 ...r.sections,
370 {
371 id: `${PLUGIN}:routing`,
372 scope: 'session',
373 text: [
374 '# Fable-ambition routing',
375 'The person may prefix their prompt with a [Reverb], [Council] or [Summit] block, or send a rewritten task: these came from one Fable 5.1 call and are the plan. Treat them as the scope.',
376 'When you delegate with the Agent tool, use model "opus" for complex or high-stakes pieces and "sonnet" for routine or narrow ones. Do not spawn subagents on Fable.',
377 'Executors report back in a few lines: what changed, the check result, any blocker. Do not paste their full output back into the plan.',
378 ].join('\n'),
379 },
380 ],
381 }
382 })
383
384 // ---- The band ----------------------------------------------------------------
385
386 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
387 if (e.props.hasSurvey || (await read($, isHidden))) return next(e)
388
389 const { Box, Button, Text, Markdown } = $.ui.resolve(e)
390 const [c, b, s0, raw, arm] = await Promise.all([read($, card), read($, busy), read($, spend), read($, showRaw), read($, armed)])
391 const s = asSpend(s0)
392
393 const all = total(s.fable) + total(s.opus) + total(s.sonnet) + total(s.other)
394 const share = all === 0 ? 0 : total(s.fable) / all
395 const pct = Math.round(share * 100)
396 const cells = 10
397 const lit = all === 0 ? 0 : Math.max(share > 0 ? 1 : 0, Math.round(share * cells))
398 const bar = '▮'.repeat(lit) + '▯'.repeat(cells - lit)
399 const warn = share > FABLE_WARN_SHARE
400 const working = e.props.isWorking
401 // Another plugin's band (thinkfirst-skillcue's, say) may sit beneath: draw it
402 // under this one instead of hiding it.
403 const below = await next(e)
404
405 return (
406 <Box flexDirection="column" gap={0}>
407 {/* Row 1: the four functions + who is working */}
408 <Box gap={1}>
409 {FN_ORDER.map((fn) => (
410 <Button
411 key={fn}
412 hotkey={FN[fn].hotkey}
413 label={b === fn ? `${FN[fn].label}…` : FN[fn].label}
414 variant={arm === fn ? 'primary' : undefined}
415 dimColor={b !== null && b !== fn}
416 onPress={() => void press($, fn)}
417 />
418 ))}
419 <Text dimColor>│</Text>
420 <Text color={b !== null ? 'yellow' : undefined} dimColor={b === null}>
421 {b !== null ? '● Fable thinking' : '○ Fable'}
422 </Text>
423 <Text color={working ? 'cyan' : undefined} dimColor={!working}>
424 {working ? '● Opus/Sonnet acting' : '○ Opus/Sonnet'}
425 </Text>
426 <Button key="hide" plain dimColor label="hide" hotkey="h" onPress={() => update($, isHidden, () => true)} />
427 </Box>
428
429 {/* Row 2: the meter */}
430 <Box gap={1}>
431 <Text color={warn ? 'yellow' : 'green'} bold>
432 Fable {pct}% {bar}
433 </Text>
434 {raw ? (
435 <Text dimColor mono>
436 fable {k(total(s.fable))} ({s.fable.calls}) · opus {k(total(s.opus))} ({s.opus.calls}) · sonnet {k(total(s.sonnet))} ({s.sonnet.calls})
437 {total(s.other) > 0 ? ` · other ${k(total(s.other))}` : ''} · total {k(all)} · cache reads {k(s.fable.cached + s.opus.cached + s.sonnet.cached + s.other.cached)}
438 </Text>
439 ) : (
440 <Text dimColor mono>
441 fable {k(total(s.fable))} · opus {k(total(s.opus))} · sonnet {k(total(s.sonnet))} · total {k(all)} · spawns o{s.spawns.opus}/s{s.spawns.sonnet}{s.spawns.fable > 0 ? `/fable ${s.spawns.fable}` : ''}
442 </Text>
443 )}
444 <Button key="raw" plain dimColor label={raw ? 'less' : 'more'} hotkey="t" onPress={() => update($, showRaw, (v) => !v)} />
445 {warn && <Text color="yellow">Fable above {Math.round(FABLE_WARN_SHARE * 100)}%: let the executors run.</Text>}
446 </Box>
447
448 {/* Row 3: what the pressed function does, before it runs */}
449 {arm !== null && (
450 <Box key="detail" flexDirection="column" borderStyle="round" borderColor="cyan" paddingX={1} marginTop={1}>
451 <Text bold color="cyan">
452 {FN[arm].hotkey} · {FN[arm].label}{' '}
453 <Text dimColor>
454 · Fable, {FN[arm].effort} effort, reads only your prompt box
455 </Text>
456 </Text>
457 <Text>{FN[arm].detail.what}</Text>
458 <Text>
459 <Text bold>You get: </Text>
460 {FN[arm].detail.gives}
461 </Text>
462 <Text>
463 <Text bold>Best when: </Text>
464 {FN[arm].detail.when}
465 </Text>
466 <Text>
467 <Text bold>Use (u) puts in your prompt: </Text>
468 {FN[arm].detail.use}
469 </Text>
470 <Box gap={1} marginTop={1}>
471 <Button
472 key="run"
473 variant="primary"
474 label={`Press ${FN[arm].hotkey} again to run ${FN[arm].label}`}
475 onPress={() => void press($, arm)}
476 />
477 <Button key="cancel" hotkey="c" label="Cancel" onPress={() => update($, armed, () => null)} />
478 </Box>
479 </Box>
480 )}
481
482 {/* Row 4: the card, when a function has answered */}
483 {c !== null && (
484 <Box flexDirection="column" borderStyle="round" borderColor={c.fn === 'council' ? 'red' : 'yellow'} paddingX={1} marginTop={1}>
485 <Text bold color="yellow">
486 {FN[c.fn].label} <Text dimColor>· from Fable · {c.source.slice(0, 60)}{c.source.length > 60 ? '…' : ''}</Text>
487 </Text>
488 <Markdown text={c.text} />
489 <Box gap={1} marginTop={1}>
490 <Button key="use" variant="primary" hotkey="u" label="Use (replace prompt)" onPress={() => void take($, 'replace')} />
491 <Button key="append" hotkey="a" label="Append to prompt" onPress={() => void take($, 'append')} />
492 <Button key="close" role="dismiss" hotkey="x" label="Close" onPress={() => update($, card, () => null)} />
493 </Box>
494 </Box>
495 )}
496 {below}
497 </Box>
498 )
499 })
500}
501types/index.d.ts 42 lines1export type Fn = 'reverb' | 'council' | 'summit' | 'deepen'
2
3export type Card = {
4 fn: Fn
5 /** The prompt the person had typed when the function ran. */
6 source: string
7 /** Fable's reply. */
8 text: string
9}
10
11export type Lane = {
12 /** Billable input: uncached input plus cache writes. */
13 in: number
14 out: number
15 /** Input the prompt cache served; shown, but kept out of Fable's share. */
16 cached: number
17 calls: number
18}
19
20export type Spend = {
21 fable: Lane
22 opus: Lane
23 sonnet: Lane
24 other: Lane
25 /** Subagents the engine started, by the lane they run on. */
26 spawns: { fable: number; opus: number; sonnet: number; other: number }
27}
28
29declare module 'claude-code' {
30 interface PluginState {
31 'thinkfirst-ambition': {
32 card: Card | null
33 busy: Fn | null
34 spend: Spend
35 isHidden: boolean
36 showRaw: boolean
37 /** The function whose explanation is showing; its key again runs it. */
38 armed: Fn | null
39 }
40 }
41}
42