SLOPSHOPPER

aside

Read-only side chat: /aside opens a pane beside the transcript where you ask about the session so far; answers come from a tool-less fork of the session's own…

newpanecommandmodel
v0.1.2MITupdated 2026-10-09sruthik27/claude-foundry/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 │ ┃ ◆ aside read-only · catpp │ ┃ ──────────────────────────────────────────── ⏺ Read(src/auth.ts) │ ┃ Ask about the session so far. No tools, noth ⎿ Read 6 lines │ ┃ into the main thread. ⏺ Update(src/auth.ts) │ ┃ ⎿ Added 2 lines, removed 1 line │ ┃ ╭─────────────────────────────────────────── ⏺ Bash(bun test) │ ┃ │ ctrl+x tab focuses the pane ⏎ ask ⎿ 3 pass, 1 fail │ ┃ ╰─────────────────────────────────────────── │ ┃ [ clear ] [ close ] Es ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /aside │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · aside
◆ aside read-only · catppuccin-mocha ─────────────────────────────────────────────────────── Ask about the session so far. No tools, nothing goes into the main thread. ╭─────────────────────────────────────────────────────╮ │ ctrl+x tab focuses the pane ⏎ ask │ ╰─────────────────────────────────────────────────────╯ [ clear ] [ close ] Esc to prompt
README

Aside

A read-only side chat about your Claude Code session, restyled to follow Lumen's theme. Fork of JayDoubleu/aside; the original MIT copyright is retained.

Install

claude plugin install aside --marketplace sruthik27/claude-foundry

Requires Claude Code 2.1.296+. Disable upstream aside@aside first if you already have it installed, then reload plugins or restart. Lumen is optional; the pane has a native Markdown fallback.

Use

  • /aside opens the pane.
  • /aside what changed so far? opens it and asks a question.
  • Enter submits the pane's input; Esc returns focus to the main composer.
  • The pane's Clear and Close buttons manage its local conversation.

Answers come from a tool-less fork of the completed session transcript. A question asked during a later turn sees the last completed snapshot. Side questions and answers are not appended to the main model's conversation.

Options

Configure this plugin through Claude's plugin settings. Its settings identity is aside@claude-code-mods.

OptionDefaultBehavior
liveFallbacktrueBefore a completed transcript snapshot exists, answer through a plain completion over bounded transcript text; off queues the question for the end of a turn
liveModelhaikuModel for live fallback answers; forks use the session's model
maxHistory8Earlier side exchanges included with a question

Model usage and access

Questions make model calls and consume your own plan/API usage. Live fallback sends bounded transcript text as fresh input; forks can use the session prefix cache. Actual costs depend on model and provider.

The side-chat model cannot call tools or edit files. The mod itself reads Claude settings to match the theme and reads session messages for fallback answers. "Read-only" describes the side conversation, not an operating-system sandbox.

Development

node --test mods/aside/tests/register.test.ts
claude plugin validate --strict mods/aside/.claude-plugin/plugin.json

The Node tests use a fake engine. This fork does not ship or claim a live-model eval suite. See validation boundaries and installation guide.

Licence

MIT. Original: JayDoubleu. Restyled fork maintained by Sruthik Issac.

Source 2 files
hooks/register.ts 327 lines
1import type { On, PluginOptions } from 'claude-code'
2import { PRESETS } from './presets.ts'
3
4declare module 'claude-code' {
5  interface EngineInterface {
6    lumen: { markdown: (args: { surface: RenderSurface; text: string; columns: number }) => Promise<RenderElement | undefined> }
7  }
8}
9
10// Restyled fork of JayDoubleu/aside (MIT): colours follow the lumen theme.
11const theme: Record<string, string> = { name: 'catppuccin-mocha', ...(PRESETS as Record<string, Record<string, string>>)['catppuccin-mocha'] }
12async function loadTheme($: any): Promise<void> {
13  try {
14    const home = (await $.env.get('HOME')) ?? ''
15    const cfg = JSON.parse(await $.fs.read(`${home}/.claude/settings.json`))
16    const name = String((cfg?.pluginConfigs?.['lumen@claude-code-mods'] ?? cfg?.pluginConfigs?.['lumen@inline'] ?? cfg?.pluginConfigs?.['prismantis@prismantis'])?.options?.theme ?? 'catppuccin-mocha')
17    const p = (PRESETS as Record<string, Record<string, string>>)[name]
18    if (p) Object.assign(theme, p, { name })
19  } catch { /* keep current theme */ }
20}
21let spin = 0
22// Input draws one line and cuts long text with "…"; it exposes no cursor or scroll, so the pane
23// echoes the END of a long draft on one dim line under it (typing happens at the end).
24let draft = ''
25let tailFrom = 30
26const SPIN = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
27
28
29/**
30 * aside: a read-only side chat beside the transcript.
31 *
32 * `/aside [question]` opens the pane (focused when the composer is empty);
33 * Tab puts the focus ring on the pane's input, Enter asks, Esc hands the keys
34 * back to the composer, ctrl+x tab focuses the pane again.
35 *
36 * Each question is one `$.model.fork` over the session's own transcript as
37 * of the last completed turn: tool-less, off the transcript, sharing the
38 * main thread's prompt cache; it answers during a later turn too, without
39 * seeing the turn in flight. A fork takes exactly one user message, so the
40 * side chat's own history is rendered into each prompt.
41 *
42 * Until the session's first turn has completed there is no snapshot to fork
43 * (`$.model.fork` resolves null at once). Then, with `liveFallback` on, the
44 * question is answered by `$.model.complete` over the text of
45 * `$.session.messages()`: immediate, uncached, without the session's system
46 * prompt, and marked "live" in the pane. With it off, the question queues
47 * and a fork answers it when the turn ends.
48 *
49 * What this module calls on `$` (the static listing `claude plugin validate`
50 * prints): clock.now, command.register, model.complete, model.fork,
51 * session.messages, ui.close, ui.invalidate, ui.open, ui.resolve. All reads
52 * and drawing: no fs, process, http, store, tool or prompt verbs, so the mod
53 * cannot reach the working tree, the network, or the main thread's prompt.
54 */
55
56type Usage = { input_tokens: number; output_tokens: number; cache_read_input_tokens: number; cache_creation_input_tokens: number }
57
58type Exchange = {
59  q: string
60  a: string | null
61  askedAt: number
62  ms?: number
63  note?: string
64  kind?: 'fork' | 'live'
65  queued?: true
66}
67
68type Host = {
69  fork: (prompt: string) => Promise<{ text: string; usage: Usage } | null>
70  messages: () => Promise<readonly { role: 'user' | 'assistant'; text: string }[]>
71  complete: (prompt: string) => Promise<string>
72  invalidate: () => void
73  open: () => Promise<void>
74  close: () => Promise<void>
75  now: () => number | Promise<number>
76}
77
78export const PANE_ID = 'aside'
79export const DEFAULTS = { liveFallback: true, liveModel: 'haiku', maxHistory: 8 } as const
80export const MAX_CHARS_PER_ANSWER = 1200
81export const LIVE_TRANSCRIPT_CHARS = 60000
82const LIVE_SYSTEM =
83  'You answer read-only side questions about a Claude Code session from its transcript. No tools; do not continue the task; answer briefly in plain prose.'
84
85const clip = (text: string): string =>
86  text.length > MAX_CHARS_PER_ANSWER ? `${text.slice(0, MAX_CHARS_PER_ANSWER)}…` : text
87
88export function register(on: On, options: PluginOptions = {}) {
89  const liveFallback = typeof options.liveFallback === 'boolean' ? options.liveFallback : DEFAULTS.liveFallback
90  const liveModel = typeof options.liveModel === 'string' && options.liveModel !== '' ? options.liveModel : DEFAULTS.liveModel
91  const maxHistory =
92    typeof options.maxHistory === 'number' && options.maxHistory >= 1 ? Math.floor(options.maxHistory) : DEFAULTS.maxHistory
93
94  let host: Host | null = null
95  let history: Exchange[] = []
96  let queued: Exchange[] = []
97  let inFlight = 0
98
99  const promptOf = (question: string): string => {
100    const prior = history
101      .filter(x => x.a !== null && x.kind !== undefined)
102      .slice(-maxHistory)
103      .map(x => `Q: ${x.q}\nA: ${x.a}`)
104      .join('\n\n')
105    return [
106      'This is a read-only side question about the conversation above. Answer from the transcript so far.',
107      'Do not use tools, do not propose edits, do not continue the main task; answer briefly in plain prose.',
108      prior === '' ? '' : `Earlier side questions and their answers:\n\n${prior}`,
109      `Side question: ${question}`,
110    ]
111      .filter(part => part !== '')
112      .join('\n\n')
113  }
114
115  async function answerLive(h: Host, entry: Exchange): Promise<void> {
116    const messages = await h.messages()
117    if (messages.length === 0) {
118      entry.a = '(nothing to ask about yet: the transcript is empty)'
119      return
120    }
121    let rendered = messages.map(m => `${m.role.toUpperCase()}: ${m.text}`).join('\n\n')
122    if (rendered.length > LIVE_TRANSCRIPT_CHARS) rendered = `…${rendered.slice(-LIVE_TRANSCRIPT_CHARS)}`
123    const text = await h.complete(`Transcript so far:\n\n${rendered}\n\n${promptOf(entry.q)}`)
124    entry.a = clip(text)
125    entry.kind = 'live'
126    entry.note = `${rendered.length} chars of transcript sent uncached`
127  }
128
129  function run(h: Host, entry: Exchange): void {
130    inFlight += 1
131    void h
132      .fork(promptOf(entry.q))
133      .then(async r => {
134        if (r !== null) {
135          entry.a = clip(r.text)
136          entry.kind = 'fork'
137          entry.note = `read ${r.usage.cache_read_input_tokens} · new ${r.usage.input_tokens + r.usage.cache_creation_input_tokens} · out ${r.usage.output_tokens}`
138          return
139        }
140        if (liveFallback) {
141          await answerLive(h, entry)
142          return
143        }
144        entry.queued = true
145        queued.push(entry)
146      })
147      .catch((err: unknown) => {
148        entry.a = `(no answer: ${err instanceof Error ? err.message : String(err)})`
149      })
150      .then(async () => {
151        if (entry.a !== null) entry.ms = (await h.now()) - entry.askedAt
152        inFlight -= 1
153        h.invalidate()
154      })
155  }
156
157  function ask(question: string): void {
158    const h = host
159    const q = question.trim()
160    if (!h || q === '') return
161    const entry: Exchange = { q, a: null, askedAt: 0 }
162    void Promise.resolve(h.now()).then(t => { entry.askedAt = t })
163    history = [...history, entry]
164    h.invalidate()
165    run(h, entry)
166  }
167
168  on('session.start', async ($, e, next) => {
169    await loadTheme($)
170    host = {
171      // Current engine answers {isAnswered, text, usage | reason}; adapt to what aside expects.
172      fork: async prompt => {
173        const r = await $.model.fork({ prompt })
174        if (r.isAnswered) return { text: r.text, usage: r.usage }
175        if (r.reason === 'nothing-to-fork') return null
176        throw new Error('error' in r ? String((r as { error: unknown }).error) : r.reason)
177      },
178      messages: () => $.session.messages(),
179      complete: async prompt => {
180        const r = await $.model.complete({ model: liveModel, maxTokens: 400, system: LIVE_SYSTEM, prompt })
181        if (r.isAnswered) return r.text
182        throw new Error('error' in r ? String((r as { error: unknown }).error) : r.reason)
183      },
184      invalidate: () => $.ui.invalidate('ui.render'),
185      open: async () => { await $.ui.open({ id: PANE_ID, title: 'aside', focus: true }) },
186      close: () => $.ui.close({ id: PANE_ID }),
187      now: () => $.clock.now(),
188    }
189    await $.command.register({
190      name: 'aside',
191      description: 'Read-only side chat about this session (opens a pane; Tab focuses its input)',
192      argumentHint: '[question]',
193      immediate: true,
194    })
195    return next(e)
196  })
197
198  on('command.run', { command: 'aside' }, async ($, e, next) => {
199    if (!host) return next(e)
200    await loadTheme($)
201    await host.open()
202    ask(e.args)
203    return {}
204  })
205
206  // Questions that found no snapshot (liveFallback off) are answered by a
207  // fork as soon as the turn in flight ends.
208  on('turn.complete', ($, e, next) => {
209    const h = host
210    const waiting = queued
211    queued = []
212    if (h) {
213      for (const entry of waiting) {
214        entry.queued = undefined
215        run(h, entry)
216      }
217    }
218    return next(e)
219  })
220
221  on('ui.render', { component: 'Pane' }, async ($, e, next) => {
222    if (e.requestId !== PANE_ID) return next(e)
223    const ui = $.ui.resolve(e)
224    if (!('Input' in ui)) return next(e)
225    const { Box, Text, Input, Button } = ui
226    const width = Math.max(24, e.props.bodyColumns - 1)
227    const t = theme
228    tailFrom = width - 14
229    const shown = history.slice(-maxHistory)
230    const body = async (text: string) => {
231      try {
232        const d = await $.lumen.markdown({ surface: e.surface, text, columns: width - 2 })
233        if (d) return d
234      } catch { /* lumen absent */ }
235      return Text({ wrap: 'wrap', color: t.codeText, children: text })
236    }
237    const rows = await Promise.all(shown.map(async (x, i) => {
238      const pending = x.a === null
239      const meta = !pending && x.ms !== undefined
240        ? `${x.kind ?? 'error'} · ${(x.ms / 1000).toFixed(1)}s${x.note ? ` · ${x.note}` : ''}` : null
241      return Box({
242        key: `x${i}`, flexDirection: 'column', marginTop: i === 0 ? 0 : 1, width,
243        children: [
244          Box({ justifyContent: 'flex-end', width, children: [
245            Box({ borderStyle: 'round', borderColor: t.accent, paddingX: 1, flexShrink: 1, children: [
246              Text({ wrap: 'wrap', color: t.strong, children: x.q }),
247            ] }),
248          ] }),
249          Text({ color: t.accent, bold: true, children: '◇ aside' }),
250          Box({ paddingLeft: 2, flexDirection: 'column', width, children: [
251            pending
252              ? Text({ color: t.codeComment, italic: true, children: `${SPIN[(spin++) % SPIN.length]} ${x.queued ? 'waiting for this turn to end…' : 'thinking…'}` })
253              : await body(x.a as string),
254            meta ? Text({ color: t.codeComment, children: meta }) : null,
255          ] }),
256        ],
257      })
258    }))
259    const count = history.length === 0 ? 'read-only' : `${history.length} asked${inFlight ? ` · ${inFlight} pending` : ''}`
260    return Box({
261      flexDirection: 'column', width,
262      children: [
263        Box({ justifyContent: 'space-between', width, children: [
264          Text({ color: t.heading, bold: true, children: '◆ aside' }),
265          Text({ color: t.codeComment, children: `${count} · ${t.name}` }),
266        ] }),
267        Text({ color: t.rule, children: '─'.repeat(width) }),
268        history.length === 0
269          ? Text({ color: t.codeComment, wrap: 'wrap', children: 'Ask about the session so far. No tools, nothing goes into the main thread.' })
270          : null,
271        ...rows,
272        Box({
273          // Explicit width: an Input wraps only when its box is sized; a shrink-to-fit box truncates it.
274          marginTop: 1, flexDirection: 'column', borderStyle: 'round', width,
275          borderColor: e.props.isFocused ? t.accent : t.rule, paddingX: 1,
276          children: [
277            Input({
278              key: 'q',
279              placeholder: e.props.isFocused ? 'ask about this session…' : 'ctrl+x tab focuses the pane',
280              submitLabel: 'ask', onSubmit: () => undefined,
281            }),
282            draft.length > tailFrom
283              ? Text({ wrap: 'truncate-start', color: t.codeComment, children: `…${draft.slice(-(width - 6))}` })
284              : null,
285          ],
286        }),
287        Box({ flexDirection: 'row', gap: 1, justifyContent: 'space-between', width, children: [
288          Box({ flexDirection: 'row', gap: 1, children: [
289            Button({ key: 'clear', label: 'clear', onPress: () => undefined }),
290            Button({ key: 'close', label: 'close', onPress: () => undefined }),
291          ] }),
292          Text({ color: t.codeComment, children: 'Esc to prompt' }),
293        ] }),
294      ],
295    })
296  })
297
298  // The pane's events are answered here rather than in the element closures:
299  // a closure handle belongs to one drawing, and a submit that lands during a
300  // redraw is dropped by core ("no handler is held under handle N"), while the
301  // hook still sees the event and its value.
302  on('ui.input', { plugin: 'aside', element: 'q' }, ($, e, next) => {
303    if (e.kind !== 'submit') {
304      const long = draft.length > tailFrom || e.value.length > tailFrom
305      draft = e.value
306      if (long) host?.invalidate()
307      return next(e)
308    }
309    draft = ''
310    ask(e.value)
311    return { element: e.element, value: e.value }
312  })
313
314  on('ui.press', { plugin: 'aside' }, ($, e, next) => {
315    if (e.element === 'clear') {
316      history = []
317      host?.invalidate()
318      return { element: e.element }
319    }
320    if (e.element === 'close') {
321      void host?.close()
322      return { element: e.element }
323    }
324    return next(e)
325  })
326}
327
hooks/presets.ts 99 lines
1type Theme = Record<string, string> // copied from lumen (MIT; originally from prismantis)
2
3export const PRESETS = {
4  'catppuccin-mocha': {
5    accent: '#cba6f7', heading: '#f9e2af', tableHeader: '#f9e2af', strong: '#ffffff', emphasis: '#f5c2e7',
6    inlineCode: '#89dceb', codeText: '#cdd6f4', codeCommand: '#89b4fa', codeFlag: '#f38ba8', codeString: '#a6e3a1',
7    codeComment: '#6c7086', link: '#89b4fa', path: '#89dceb', number: '#a6e3a1', quote: '#9399b2',
8    rule: '#45475a', tableRule: '#585b70', bullet: '#cba6f7', diagram: '#89b4fa', diagramText: '#cdd6f4',
9  },
10  'catppuccin-latte': {
11    accent: '#8839ef', heading: '#df8e1d', tableHeader: '#df8e1d', strong: '#11111b', emphasis: '#ea76cb',
12    inlineCode: '#04a5e5', codeText: '#4c4f69', codeCommand: '#1e66f5', codeFlag: '#d20f39', codeString: '#40a02b',
13    codeComment: '#9ca0b0', link: '#1e66f5', path: '#209fb5', number: '#40a02b', quote: '#6c6f85',
14    rule: '#bcc0cc', tableRule: '#acb0be', bullet: '#8839ef', diagram: '#1e66f5', diagramText: '#4c4f69',
15  },
16  dracula: {
17    accent: '#bd93f9', heading: '#bd93f9', tableHeader: '#f1fa8c', strong: '#f8f8f2', emphasis: '#ff79c6',
18    inlineCode: '#8be9fd', codeText: '#f8f8f2', codeCommand: '#50fa7b', codeFlag: '#ff79c6', codeString: '#f1fa8c',
19    codeComment: '#6272a4', link: '#8be9fd', path: '#ffb86c', number: '#bd93f9', quote: '#6272a4',
20    rule: '#44475a', tableRule: '#6272a4', bullet: '#ff79c6', diagram: '#bd93f9', diagramText: '#f8f8f2',
21  },
22  nord: {
23    accent: '#88c0d0', heading: '#88c0d0', tableHeader: '#ebcb8b', strong: '#eceff4', emphasis: '#b48ead',
24    inlineCode: '#8fbcbb', codeText: '#d8dee9', codeCommand: '#88c0d0', codeFlag: '#81a1c1', codeString: '#a3be8c',
25    codeComment: '#4c566a', link: '#88c0d0', path: '#8fbcbb', number: '#b48ead', quote: '#81a1c1',
26    rule: '#3b4252', tableRule: '#4c566a', bullet: '#88c0d0', diagram: '#81a1c1', diagramText: '#d8dee9',
27  },
28  'tokyo-night': {
29    accent: '#bb9af7', heading: '#7dcfff', tableHeader: '#e0af68', strong: '#c0caf5', emphasis: '#f7768e',
30    inlineCode: '#7dcfff', codeText: '#a9b1d6', codeCommand: '#7aa2f7', codeFlag: '#bb9af7', codeString: '#9ece6a',
31    codeComment: '#565f89', link: '#73daca', path: '#2ac3de', number: '#ff9e64', quote: '#9aa5ce',
32    rule: '#414868', tableRule: '#565f89', bullet: '#bb9af7', diagram: '#7aa2f7', diagramText: '#c0caf5',
33  },
34  'gruvbox-dark': {
35    accent: '#d3869b', heading: '#fabd2f', tableHeader: '#fabd2f', strong: '#fbf1c7', emphasis: '#d3869b',
36    inlineCode: '#8ec07c', codeText: '#ebdbb2', codeCommand: '#83a598', codeFlag: '#fe8019', codeString: '#b8bb26',
37    codeComment: '#928374', link: '#83a598', path: '#8ec07c', number: '#d3869b', quote: '#a89984',
38    rule: '#3c3836', tableRule: '#504945', bullet: '#fe8019', diagram: '#83a598', diagramText: '#ebdbb2',
39  },
40  'gruvbox-light': {
41    accent: '#8f3f71', heading: '#b57614', tableHeader: '#b57614', strong: '#282828', emphasis: '#8f3f71',
42    inlineCode: '#427b58', codeText: '#3c3836', codeCommand: '#076678', codeFlag: '#af3a03', codeString: '#79740e',
43    codeComment: '#928374', link: '#076678', path: '#427b58', number: '#8f3f71', quote: '#7c6f64',
44    rule: '#ebdbb2', tableRule: '#a89984', bullet: '#af3a03', diagram: '#076678', diagramText: '#3c3836',
45  },
46  'rose-pine': {
47    accent: '#c4a7e7', heading: '#c4a7e7', tableHeader: '#f6c177', strong: '#e0def4', emphasis: '#ebbcba',
48    inlineCode: '#9ccfd8', codeText: '#e0def4', codeCommand: '#9ccfd8', codeFlag: '#eb6f92', codeString: '#f6c177',
49    codeComment: '#6e6a86', link: '#c4a7e7', path: '#9ccfd8', number: '#ebbcba', quote: '#908caa',
50    rule: '#26233a', tableRule: '#6e6a86', bullet: '#eb6f92', diagram: '#c4a7e7', diagramText: '#e0def4',
51  },
52  'rose-pine-dawn': {
53    accent: '#907aa9', heading: '#907aa9', tableHeader: '#ea9d34', strong: '#464261', emphasis: '#d7827e',
54    inlineCode: '#56949f', codeText: '#464261', codeCommand: '#56949f', codeFlag: '#b4637a', codeString: '#ea9d34',
55    codeComment: '#9893a5', link: '#907aa9', path: '#286983', number: '#d7827e', quote: '#797593',
56    rule: '#f2e9e1', tableRule: '#9893a5', bullet: '#b4637a', diagram: '#907aa9', diagramText: '#464261',
57  },
58  everforest: {
59    accent: '#a7c080', heading: '#e69875', tableHeader: '#dbbc7f', strong: '#d3c6aa', emphasis: '#d699b6',
60    inlineCode: '#83c092', codeText: '#d3c6aa', codeCommand: '#7fbbb3', codeFlag: '#e67e80', codeString: '#a7c080',
61    codeComment: '#859289', link: '#7fbbb3', path: '#83c092', number: '#d699b6', quote: '#9da9a0',
62    rule: '#475258', tableRule: '#7a8478', bullet: '#e69875', diagram: '#7fbbb3', diagramText: '#d3c6aa',
63  },
64  'github-dark': {
65    accent: '#d2a8ff', heading: '#79c0ff', tableHeader: '#ffa657', strong: '#e6edf3', emphasis: '#d2a8ff',
66    inlineCode: '#79c0ff', codeText: '#e6edf3', codeCommand: '#d2a8ff', codeFlag: '#ff7b72', codeString: '#a5d6ff',
67    codeComment: '#7d8590', link: '#58a6ff', path: '#7ee787', number: '#79c0ff', quote: '#7d8590',
68    rule: '#30363d', tableRule: '#8b949e', bullet: '#ff7b72', diagram: '#58a6ff', diagramText: '#e6edf3',
69  },
70  'github-light': {
71    accent: '#8250df', heading: '#0550ae', tableHeader: '#953800', strong: '#1f2328', emphasis: '#8250df',
72    inlineCode: '#0550ae', codeText: '#1f2328', codeCommand: '#8250df', codeFlag: '#cf222e', codeString: '#0a3069',
73    codeComment: '#6e7781', link: '#0969da', path: '#116329', number: '#0550ae', quote: '#656d76',
74    rule: '#d0d7de', tableRule: '#6e7781', bullet: '#cf222e', diagram: '#0969da', diagramText: '#1f2328',
75  },
76  'one-dark': {
77    accent: '#c678dd', heading: '#e06c75', tableHeader: '#e5c07b', strong: '#d19a66', emphasis: '#c678dd',
78    inlineCode: '#56b6c2', codeText: '#abb2bf', codeCommand: '#61afef', codeFlag: '#e06c75', codeString: '#98c379',
79    codeComment: '#5c6370', link: '#61afef', path: '#56b6c2', number: '#d19a66', quote: '#828997',
80    rule: '#3a3f4b', tableRule: '#5c6370', bullet: '#c678dd', diagram: '#61afef', diagramText: '#abb2bf',
81  },
82  'solarized-dark': {
83    accent: '#6c71c4', heading: '#b58900', tableHeader: '#b58900', strong: '#eee8d5', emphasis: '#d33682',
84    inlineCode: '#2aa198', codeText: '#93a1a1', codeCommand: '#268bd2', codeFlag: '#cb4b16', codeString: '#859900',
85    codeComment: '#586e75', link: '#268bd2', path: '#2aa198', number: '#859900', quote: '#657b83',
86    rule: '#073642', tableRule: '#586e75', bullet: '#6c71c4', diagram: '#268bd2', diagramText: '#93a1a1',
87  },
88  'solarized-light': {
89    accent: '#6c71c4', heading: '#b58900', tableHeader: '#b58900', strong: '#073642', emphasis: '#d33682',
90    inlineCode: '#2aa198', codeText: '#586e75', codeCommand: '#268bd2', codeFlag: '#cb4b16', codeString: '#859900',
91    codeComment: '#93a1a1', link: '#268bd2', path: '#2aa198', number: '#859900', quote: '#839496',
92    rule: '#eee8d5', tableRule: '#93a1a1', bullet: '#6c71c4', diagram: '#268bd2', diagramText: '#586e75',
93  },
94  mono: {} as Theme,
95} satisfies Record<string, Theme>
96
97export type PresetName = keyof typeof PRESETS
98export const PRESET_NAMES = Object.keys(PRESETS) as PresetName[]
99