Type a question between pipes mid-sentence: a space after the closing pipes asks Haiku in the background and writes the answer back into your draft.

A Claude Code mod for questions in the middle of a sentence. Type a question between pipes while you write a prompt; a space after the closing pipes sends it to a small side session in the background, and the answer is written back into your draft in place, worded to continue the text before it.
The capital of Slovenia is || what is the capital of Slovenia || and it has about ...
Inside Claude Code:
/plugin install tangent --marketplace mihazip/hyperfocus
Restart Claude Code. Part of the hyperfocus collection.
|| question || then a space: the primary form. || is also logical OR, so it only fires when the question has 2+ words and the opener is not inside an unclosed backtick span or code fence (if (a || b || stays code).\\\ question \\\ and /// question /// then a space: aliases, on by default. They never fire by accident, so a one-word question is fine.⟦1 capital of Sloveni…⟧. Keep typing; the tag is swapped for the answer when it lands.Add as many opening and closing pairs as you like (the same string twice is fine): ;;;, >>> ... <<<, [[ ... ]]. Each is 2+ characters with no letters, digits or spaces, and needs a query of 2+ words like ||. The built-in ||, \\\ and /// can each be switched off, but at least one pair stays on.
/tangent delims opens a small picker pane: an on/off button per built-in, a remove button per custom pair, a field to add one (open close, close optional) and a done button. Tab or the arrows walk the rows, Enter presses, Esc only hands the keys back (done closes). The picker uses the Windows Terminal "Ubuntu" palette (THEME in hooks/register.tsx). The same things as text, for scripting:
/tangent delims add ;;; same string both sides
/tangent delims add >>> <<< different open and close
/tangent delims remove ;;; drops a custom pair
/tangent delims off || switch a built-in off (on to switch it back)
/tangent delims reset custom pairs cleared, built-ins all on
Plain /tangent lists the active delimiters. Pairs and switches are kept in the plugin's store ($.store, a file in your Claude Code config folder), so they survive restarts and claude plugin update. The plugin's options (open and close, kept in settings.json) add one more pair next to them. An older single pair saved by 0.2 is moved into the list on first start.
A short code picks the model and effort for one question. Put it right after the closing pipes (||your question||sm ) or at the start inside them (||sm your question|| ).
| letter | model |
|---|---|
| h | haiku |
| s | sonnet |
| o | opus |
| f | fable |
| letter | level |
|---|---|
| l | low |
| m | medium |
| h | high |
| x | xhigh |
| xx | max |
The level letter is optional (s alone is sonnet at high). Default is hh (haiku, high). Change it with /tangent <code>, e.g. /tangent sm; /tangent shows it. The default is kept across sessions.
Each question is one model call with no tools: up to ~20k characters of your draft before the spot plus 2k after it go in (about 5-6k tokens), and the reply is capped at 3000 tokens, so a side session stays near 10k tokens.
claude plugin validate .
claude plugin test .
MIT
hooks/register.tsx 515 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, ModelEffort, Register } from 'claude-code'
3
4import type { Job } from '../types'
5
6// || question || is the primary form; \\\ question \\\ and /// question /// are aliases. Users can add their own.
7export type Delims = [open: string, close: string][]
8export const DEFAULT_DELIMS: Delims = [
9 ['||', '||'],
10 ['\\\\\\', '\\\\\\'],
11 ['///', '///'],
12]
13// The aliases are never typed by accident; every other pair needs a query of 2+ words.
14const ALIASES = new Set(['\\\\\\', '///'])
15const MIN_WORDS = 2
16const MODELS: Record<string, string> = { h: 'haiku', s: 'sonnet', o: 'opus', f: 'fable' }
17const LEVELS: Record<string, ModelEffort> = { l: 'low', m: 'medium', h: 'high', x: 'xhigh', xx: 'max' }
18const CODE = /^([hsof])(xx|[lmhx])?$/
19const DEFAULT = 'hh'
20// Whole side session stays near 10k tokens: ~5k of draft in, 3k out.
21const CONTEXT_CHARS = 20000
22const MAX_TOKENS = 3000
23
24export const SYSTEM =
25 'You fill in one spot inside a draft the user is typing. ' +
26 'You get the draft before the spot, the spot itself (a question or an instruction), and the draft after it. ' +
27 'The draft around the spot is context only: never continue, finish or rephrase it, and never repeat the spot. ' +
28 'If the spot is a question or a request for information, your reply is the ANSWER: the actual facts, ' +
29 'compact, one to three sentences unless it asks for more. ' +
30 'Never answer by completing or restating the question, even when the draft around it is itself a question. ' +
31 'If the spot is an instruction to write something (for example "a friendly closing line"), write that text ' +
32 'so it flows into the draft: same language, voice and register, matching case and punctuation. ' +
33 'If it could be either, prefer answering. ' +
34 'Reply with only the text for the spot: no preamble, no quotes around it, no markdown, no notes to the user.'
35
36const nextId = atom({ plugin: 'tangent', key: 'nextId' } as const, 1)
37const jobs = atom({ plugin: 'tangent', key: 'jobs' } as const, {})
38const preset = atom({ plugin: 'tangent', key: 'preset' } as const, DEFAULT)
39const held = atom({ plugin: 'tangent', key: 'held' } as const, '')
40// Pairs the user added (store key 'pairs'), built-in opens switched off (store key 'off'), and the options pair.
41const custom = atom({ plugin: 'tangent', key: 'custom' } as const, [])
42const off = atom({ plugin: 'tangent', key: 'off' } as const, [])
43const extra = atom({ plugin: 'tangent', key: 'extra' } as const, [])
44const notice = atom({ plugin: 'tangent', key: 'notice' } as const, '')
45// Windows Terminal 'Ubuntu' scheme; change here for another palette
46const THEME = { bg: '#300A24', fg: '#EEEEEC', dim: '#D0CFCC', accent: '#E9AD0C', info: '#33C7DE', error: '#F66151' }
47
48const PANE = 'tangent-delims'
49
50export function parseCode(code: string): { model: string; effort: ModelEffort } | undefined {
51 const m = CODE.exec(code)
52 const model = MODELS[m?.[1] ?? '']
53 const effort = LEVELS[m?.[2] ?? 'h']
54 return model && effort ? { model, effort } : undefined
55}
56
57export function tag(id: number, q: string): string {
58 const short = q.length > 24 ? q.slice(0, 23).trimEnd() + '…' : q
59 return `⟦${id} ${short}⟧`
60}
61
62export function escapeRegex(s: string): string {
63 return s.replace(/[.*+?^${}()|[\]\\\/-]/g, '\\$&')
64}
65
66// A delimiter is 2+ characters with no letters and no whitespace.
67export function checkDelim(d: string): boolean {
68 return d.length >= 2 && !/[\p{L}\s]/u.test(d)
69}
70
71export function parseDelims(raw: unknown): Delims | undefined {
72 if (!Array.isArray(raw) || raw.length !== 2) return undefined
73 const [open, close] = raw
74 if (typeof open !== 'string' || typeof close !== 'string') return undefined
75 return checkDelim(open) && checkDelim(close) ? [[open, close]] : undefined
76}
77
78// Finds a closed <open>query<close>code just before `end` (where the space was typed).
79export function findQuery(
80 text: string,
81 end: number,
82 pairs: Delims = DEFAULT_DELIMS,
83): { start: number; query: string; code: string } | undefined {
84 const head = text.slice(0, end)
85 for (const [mark, shut] of pairs) {
86 const tail = new RegExp(`${escapeRegex(shut)}([a-z]{0,3})$`).exec(head)
87 if (!tail) continue
88 const found = between(head, mark, tail.index, tail[1] ?? '', !(ALIASES.has(mark) && mark === shut))
89 if (found) return found
90 }
91 return undefined
92}
93
94// True when `at` sits inside an unclosed ``` fence or `inline` span (spans end at a newline).
95export function inCode(text: string, at: number): boolean {
96 let fence = false
97 let inline = false
98 for (let i = 0; i < at; i++) {
99 if (text.startsWith('```', i)) {
100 fence = !fence
101 inline = false
102 i += 2
103 } else if (fence) continue
104 else if (text[i] === '`') inline = !inline
105 else if (text[i] === '\n') inline = false
106 }
107 return fence || inline
108}
109
110function between(
111 head: string,
112 mark: string,
113 close: number,
114 tailCode: string,
115 needWords: boolean,
116): { start: number; query: string; code: string } | undefined {
117 const open = head.lastIndexOf(mark, close - 1)
118 if (open < 0 || open + mark.length > close) return undefined
119 if (inCode(head, open)) return undefined
120 let query = head.slice(open + mark.length, close).trim()
121 let code = tailCode
122 // The code may also lead the query: \\\sh question\\\
123 const lead = /^([a-z]{1,3})\s+/.exec(query)
124 if (!code && lead?.[1] && parseCode(lead[1])) {
125 code = lead[1]
126 query = query.slice(lead[0].length)
127 }
128 if (!query || (code && !parseCode(code))) return undefined
129 // `a || b || ` is code, not a question.
130 if (needWords && query.split(/\s+/).length < MIN_WORDS) return undefined
131 return { start: open, query, code }
132}
133
134export function buildPrompt(job: Pick<Job, 'before' | 'query' | 'after'>): string {
135 return (
136 `<draft_before>\n${job.before}\n</draft_before>\n` +
137 `<spot>\n${job.query}\n</spot>\n` +
138 `<draft_after>\n${job.after}\n</draft_after>\n` +
139 'The draft is context only. If the spot asks a question or for information, reply with the answer, ' +
140 'not a continuation or rephrasing of the question. If it asks you to write text, write it so it fits the draft. ' +
141 'Reply with only the text for the spot.'
142 )
143}
144
145async function run($: EngineInterface, id: number): Promise<void> {
146 const job = (await read($, jobs))[String(id)]
147 if (!job) return
148 await showStatus($)
149 const r = await $.model.complete({
150 model: job.model,
151 system: SYSTEM,
152 prompt: buildPrompt(job),
153 maxTokens: MAX_TOKENS,
154 effort: job.effort,
155 timeoutMs: 90000,
156 })
157 const answer = r.isAnswered ? r.text.trim() : ''
158 await update($, jobs, all => ({
159 ...all,
160 [String(id)]: { ...job, answer, isDone: true, failure: r.isAnswered ? '' : r.reason },
161 }))
162 await place($, id)
163 await sendHeld($)
164 await showStatus($)
165}
166
167// Swaps the tag in the prompt box for the answer, or says where it went.
168async function place($: EngineInterface, id: number): Promise<void> {
169 const job = (await read($, jobs))[String(id)]
170 if (!job?.isDone) return
171 if ((await read($, held)).includes(job.tag)) return
172 const box = await $.prompt.read()
173 const at = box.text.indexOf(job.tag)
174 if (at < 0) {
175 if (job.answer) $.ui.toast(`tangent ${id}: ${job.answer.slice(0, 200)}`, { timeoutMs: 15000 })
176 return
177 }
178 const text = job.answer || `[${job.query}: ${job.failure}]`
179 await $.prompt.fill({
180 text: box.text.slice(0, at) + text + box.text.slice(at + job.tag.length),
181 mode: 'replace',
182 })
183 if (!job.answer) $.ui.toast(`tangent ${id} failed: ${job.failure}`)
184 await forget($, id)
185}
186
187async function forget($: EngineInterface, id: number): Promise<void> {
188 await update($, jobs, all => {
189 const rest = { ...all }
190 delete rest[String(id)]
191 return rest
192 })
193}
194
195async function showStatus($: EngineInterface): Promise<void> {
196 const busy = Object.values(await read($, jobs)).filter(j => !j.isDone).length
197 $.ui.status(busy ? `tangent: ${busy} running` : undefined)
198}
199
200async function start($: EngineInterface, text: string, found: { start: number; query: string; code: string }, end: number) {
201 const id = await read($, nextId)
202 await update($, nextId, n => n + 1)
203 const picked = parseCode(found.code || (await read($, preset))) ?? parseCode(DEFAULT)!
204 const job: Job = {
205 tag: tag(id, found.query),
206 query: found.query,
207 model: picked.model,
208 effort: picked.effort,
209 before: text.slice(0, found.start).slice(-CONTEXT_CHARS),
210 after: text.slice(end).slice(0, 2000),
211 answer: '',
212 failure: '',
213 isDone: false,
214 }
215 await update($, jobs, all => ({ ...all, [String(id)]: job }))
216 $.clock.after(0, () => void run($, id))
217 return job.tag
218}
219
220// Built-ins that are not switched off, then the user's pairs, then the options pair.
221export function activePairs(mine: Delims = [], disabled: string[] = [], fromOptions: Delims = []): Delims {
222 const all = [...DEFAULT_DELIMS.filter(([o]) => !disabled.includes(o)), ...mine, ...fromOptions]
223 return all.filter(([o], i) => all.findIndex(([p]) => p === o) === i)
224}
225
226async function active($: EngineInterface): Promise<Delims> {
227 return activePairs(await read($, custom), await read($, off), await read($, extra))
228}
229
230function showDelims(pairs: Delims): string {
231 return pairs.map(([o, c]) => `${o} question ${c}`).join(' or ')
232}
233
234function isBuiltin(open: string): boolean {
235 return DEFAULT_DELIMS.some(([o]) => o === open)
236}
237
238async function saveCustom($: EngineInterface, pairs: Delims): Promise<void> {
239 await update($, custom, () => pairs)
240 if (pairs.length) await $.store.set('pairs', pairs)
241 else await $.store.delete('pairs')
242}
243
244async function saveOff($: EngineInterface, opens: string[]): Promise<void> {
245 await update($, off, () => opens)
246 if (opens.length) await $.store.set('off', opens)
247 else await $.store.delete('off')
248}
249
250// "open close" (close optional) to a pair: adds it, or says why not.
251export async function addPair($: EngineInterface, open: string, close = open): Promise<string> {
252 const pair = parseDelims([open, close])
253 if (!pair) return 'Each delimiter is 2+ characters, no letters, digits or spaces, e.g. ;;; or >>> <<<.'
254 if (isBuiltin(open)) return `${open} is built in: switch it on instead.`
255 const mine = (await read($, custom)).filter(([o]) => o !== open)
256 await saveCustom($, [...mine, ...pair])
257 return `Added ${open} question ${close}.`
258}
259
260export async function removePair($: EngineInterface, open: string): Promise<string> {
261 const mine = await read($, custom)
262 if (!mine.some(([o]) => o === open)) return `No custom pair opens with ${open}.`
263 await saveCustom($, mine.filter(([o]) => o !== open))
264 return `Removed ${open}.`
265}
266
267// Switches a built-in on or off; at least one pair must stay enabled.
268export async function toggleBuiltin($: EngineInterface, open: string, on?: boolean): Promise<string> {
269 if (!isBuiltin(open)) return `${open} is not a built-in pair.`
270 const disabled = await read($, off)
271 const turnOn = on ?? disabled.includes(open)
272 if (!turnOn) {
273 const rest = activePairs(await read($, custom), [...disabled, open], await read($, extra))
274 if (!rest.length) return 'At least one pair must stay on.'
275 }
276 await saveOff($, turnOn ? disabled.filter(o => o !== open) : [...new Set([...disabled, open])])
277 return `${open} is ${turnOn ? 'on' : 'off'}.`
278}
279
280async function resetDelims($: EngineInterface): Promise<string> {
281 await saveCustom($, [])
282 await saveOff($, [])
283 return 'Custom pairs removed, built-ins all on.'
284}
285
286async function openPicker($: EngineInterface): Promise<void> {
287 await update($, notice, () => '')
288 await $.ui.open({ id: PANE, title: 'tangent delimiters', focus: true, rows: 14 })
289}
290
291// Runs a picker action and shows what it said in the pane's notice line.
292async function say($: EngineInterface, doing: Promise<string>): Promise<void> {
293 const said = await doing
294 await update($, notice, () => said)
295}
296
297// The Input holds "open close" (close optional).
298async function addFromInput($: EngineInterface, value: string): Promise<void> {
299 const [open, close, ...rest] = value.trim().split(/\s+/).filter(Boolean)
300 if (!open || rest.length) {
301 await update($, notice, () => 'Type an opening and optional closing delimiter, e.g. ;;; or >>> <<<.')
302 return
303 }
304 await say($, addPair($, open, close))
305}
306
307async function setDelims($: EngineInterface, args: string): Promise<{ text: string }> {
308 const [verb, first, second, ...rest] = args.trim().split(/\s+/).filter(Boolean)
309 let said = ''
310 if (!verb) {
311 await openPicker($)
312 return { text: 'tangent delimiters pane opened.' }
313 }
314 if (verb === 'reset' && !first) said = await resetDelims($)
315 else if (verb === 'add' && first && !rest.length) said = await addPair($, first, second)
316 else if (verb === 'remove' && first && !second) said = await removePair($, first)
317 else if ((verb === 'on' || verb === 'off') && first && !second) said = await toggleBuiltin($, first, verb === 'on')
318 else {
319 return {
320 text:
321 `Delimiters: ${showDelims(await active($))}\n` +
322 'Usage: /tangent delims (picker) | add <open> [close] | remove <open> | on <builtin> | off <builtin> | reset. ' +
323 'Each is 2+ characters, no letters or spaces, e.g. ;;; or >>> <<<.',
324 }
325 }
326 return { text: `tangent: ${said} Active: ${showDelims(await active($))}` }
327}
328
329// Reads the saved pairs and switches, moves an old single 'delims' pair into 'pairs' once,
330// and takes the plugin options as one more pair.
331async function loadDelims($: EngineInterface, opened: unknown, closed: unknown): Promise<void> {
332 const keep = (raw: unknown): Delims =>
333 Array.isArray(raw) ? raw.flatMap(p => parseDelims(p) ?? []).filter(([o]) => !isBuiltin(o)) : []
334 let mine = keep(await $.store.get('pairs'))
335 const rawOld = await $.store.get('delims')
336 if (rawOld !== undefined) {
337 const old = parseDelims(rawOld)
338 if (old && !isBuiltin(old[0][0]) && !mine.some(([o]) => o === old[0][0])) mine = [...mine, ...old]
339 await saveCustom($, mine)
340 await $.store.delete('delims')
341 } else {
342 await update($, custom, () => mine)
343 }
344 const saved = await $.store.get('off')
345 const disabled = Array.isArray(saved) ? saved.filter((o): o is string => typeof o === 'string' && isBuiltin(o)) : []
346 await update($, off, () => disabled)
347 const fromOptions = typeof opened === 'string' && opened ? parseDelims([opened, closed || opened]) : undefined
348 await update($, extra, () => (fromOptions && !isBuiltin(fromOptions[0][0]) ? fromOptions : []))
349 // never leave nothing enabled
350 if (!(await active($)).length) await saveOff($, [])
351}
352
353async function setPreset($: EngineInterface, args: string): Promise<{ text: string }> {
354 const code = args.trim().toLowerCase()
355 const sub = /^delim(?:iter)?s?(?=\s|$)/.exec(code)
356 if (sub) return setDelims($, args.trim().slice(sub[0].length))
357 if (!code) {
358 const p = parseCode(await read($, preset))!
359 return {
360 text:
361 `tangent default: ${p.model} ${p.effort}. Set with /tangent <code>, e.g. /tangent sm.\n` +
362 `Delimiters: ${showDelims(await active($))}. Edit them with /tangent delims (opens a picker), or add <open> [close], remove <open>, on/off <builtin>, reset.`,
363 }
364 }
365 const p = parseCode(code)
366 if (!p) {
367 return {
368 text:
369 'Usage: /tangent <code> sets the default; code is a model letter (h s o f) plus a level (l m h x, or xx for max), e.g. hm, sh, fxx.\n' +
370 'Or /tangent delims [add <open> [close] | remove <open> | on <builtin> | off <builtin> | reset] edits the delimiters.',
371 }
372 }
373 await update($, preset, () => code)
374 await $.store.set('preset', code)
375 return { text: `tangent default is now ${p.model} ${p.effort}.` }
376}
377
378// Puts finished answers in place of their tags; says how many still run.
379async function fillDone($: EngineInterface, text: string): Promise<{ text: string; running: number }> {
380 let out = text
381 let running = 0
382 for (const [id, j] of Object.entries(await read($, jobs))) {
383 if (!out.includes(j.tag)) continue
384 if (!j.isDone) {
385 running += 1
386 continue
387 }
388 out = out.replace(j.tag, j.answer || `[${j.query}]`)
389 await forget($, Number(id))
390 }
391 return { text: out, running }
392}
393
394// A prompt sent while answers ran is held; the last answer sends it.
395async function sendHeld($: EngineInterface): Promise<void> {
396 const draft = await read($, held)
397 if (!draft) return
398 const r = await fillDone($, draft)
399 if (r.running) return
400 await update($, held, () => '')
401 const box = await $.prompt.read()
402 if (box.text === draft) await $.prompt.fill({ text: '', mode: 'replace' })
403 await $.prompt.submit({ text: r.text, asUser: true })
404}
405
406export const register: Register = (on, options) => {
407 on('session.start', async ($, e, next) => {
408 const saved = await $.store.get('preset')
409 if (typeof saved === 'string' && parseCode(saved)) await update($, preset, () => saved)
410 await loadDelims($, options?.open, options?.close)
411 await $.command.register({
412 name: 'tangent',
413 description: 'Show or set the default model+level for || inline questions || (e.g. /tangent hm)',
414 })
415 return next(e)
416 })
417
418 on('command.run', { command: 'tangent' }, async ($, e) => setPreset($, e.args))
419
420 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
421 const { Box, Text, Button, Input } = $.ui.resolve(e)
422 const disabled = await read($, off)
423 const mine = await read($, custom)
424 const fromOptions = await read($, extra)
425 const note = await read($, notice)
426
427 const fg = { color: THEME.fg, backgroundColor: THEME.bg }
428
429 return (
430 <Box
431 flexDirection="column"
432 paddingX={1}
433 width={e.props.bodyColumns}
434 minHeight={e.props.scroll.bodyRows}
435 backgroundColor={THEME.bg}
436 >
437 <Text color={THEME.dim} backgroundColor={THEME.bg}>
438 Type || question || (a space after the closing mark asks). Tab or arrows move, Enter presses.
439 </Text>
440 {DEFAULT_DELIMS.map(([o, c]) => (
441 <Box flexDirection="row" width="100%" backgroundColor={THEME.bg}>
442 <Text {...fg}>{`${o} question ${c} `}</Text>
443 <Button
444 plain
445 key={`toggle:${o}`}
446 onPress={() => {
447 void say($, toggleBuiltin($, o))
448 }}
449 >
450 <Text {...fg}>{disabled.includes(o) ? 'off' : 'on'}</Text>
451 </Button>
452 </Box>
453 ))}
454 {mine.map(([o, c]) => (
455 <Box flexDirection="row" width="100%" backgroundColor={THEME.bg}>
456 <Text {...fg}>{`${o} question ${c} `}</Text>
457 <Button
458 plain
459 key={`remove:${o}`}
460 onPress={() => {
461 void say($, removePair($, o))
462 }}
463 >
464 <Text {...fg}>remove</Text>
465 </Button>
466 </Box>
467 ))}
468 {fromOptions.map(([o, c]) => (
469 <Text color={THEME.dim} backgroundColor={THEME.bg}>{`${o} question ${c} (from plugin options)`}</Text>
470 ))}
471 <Input
472 key="add"
473 placeholder="add a pair: open [close], e.g. ;;; or >>> <<<"
474 submitLabel="add"
475 autoFocus
476 value=""
477 onSubmit={(value: string) => {
478 void addFromInput($, value)
479 }}
480 />
481 {note !== '' && <Text color={THEME.error} backgroundColor={THEME.bg}>{note}</Text>}
482 <Button
483 plain
484 key="done"
485 onPress={() => {
486 void $.ui.close({ id: PANE })
487 }}
488 >
489 <Text {...fg}>done</Text>
490 </Button>
491 </Box>
492 )
493 })
494
495 on('prompt.edit', async ($, e, next) => {
496 const r = await next(e)
497 if (e.inputText !== ' ') return r
498 const end = r.cursor - 1
499 const found = findQuery(r.text, end, await active($))
500 if (!found) return r
501 const label = await start($, r.text, found, end + 1)
502 const text = r.text.slice(0, found.start) + label + ' ' + r.text.slice(end + 1)
503 return { text, cursor: found.start + label.length + 1 }
504 })
505
506 // The main thread waits for answers still running when the prompt is sent.
507 on('prompt.submit', async ($, e, next) => {
508 if (!e.text.includes('⟦')) return next(e)
509 const r = await fillDone($, e.text)
510 if (!r.running) return next({ ...e, text: r.text })
511 await update($, held, () => r.text)
512 return { drop: `tangent: waiting for ${r.running} answer(s), sends when done` }
513 }).catch(($, e, next) => next(e))
514}
515types/index.d.ts 27 lines1export type Job = {
2 tag: string
3 query: string
4 model: string
5 effort: 'low' | 'medium' | 'high' | 'xhigh' | 'max'
6 before: string
7 after: string
8 answer: string
9 failure: string
10 isDone: boolean
11}
12
13declare module 'claude-code' {
14 interface PluginState {
15 tangent: {
16 nextId: number
17 jobs: Record<string, Job>
18 preset: string
19 held: string
20 custom: [open: string, close: string][]
21 off: string[]
22 extra: [open: string, close: string][]
23 notice: string
24 }
25 }
26}
27