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…

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.
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.
/aside opens the pane./aside what changed so far? opens it and asks a question.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.
Configure this plugin through Claude's plugin settings. Its settings identity is aside@claude-code-mods.
| Option | Default | Behavior |
|---|---|---|
liveFallback | true | Before a completed transcript snapshot exists, answer through a plain completion over bounded transcript text; off queues the question for the end of a turn |
liveModel | haiku | Model for live fallback answers; forks use the session's model |
maxHistory | 8 | Earlier side exchanges included with a question |
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.
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.
MIT. Original: JayDoubleu. Restyled fork maintained by Sruthik Issac.
hooks/register.ts 327 lines1import 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}
327hooks/presets.ts 99 lines1type 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