@tiagojct/subsub 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/web.js CHANGED
@@ -13,20 +13,15 @@
13
13
  * - Without an open page for a while (and with nothing waiting), the server
14
14
  * stops by itself, so a shortcut can start it without a terminal.
15
15
  */
16
- import { spawn } from "node:child_process";
17
16
  import { randomBytes, timingSafeEqual } from "node:crypto";
18
- import { createReadStream, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, appendFileSync } from "node:fs";
17
+ import { createReadStream, existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync, appendFileSync } from "node:fs";
19
18
  import { createServer } from "node:http";
20
19
  import { homedir } from "node:os";
21
- import { basename, dirname, extname, join, normalize, relative, resolve, sep } from "node:path";
22
- import { StringDecoder } from "node:string_decoder";
20
+ import { basename, dirname, extname, join, normalize, relative, sep } from "node:path";
23
21
  import { agentDirFor, chooseCwd, PACKAGE_DIR, packageVersion } from "./cli.js";
24
- import { configPath, expand, loadConfig, RECOMMENDED_MODELS, readConfigFile, saveConfig, uiLanguage, withStarbuck } from "./config.js";
22
+ import { configPath, loadConfig, RECOMMENDED_MODELS, readConfigFile, saveConfig, uiLanguage, withStarbuck } from "./config.js";
25
23
  import { TESTED_MODELS } from "./init.js";
26
24
  import { KEY_PROVIDERS, saveApiKey } from "./keys.js";
27
- import { NOTE_DIRS, obsidianRoot, SETTINGS_DIR } from "./layout.js";
28
- import { field, frontMatter } from "./notes.js";
29
- import { realish, within } from "./paths.js";
30
25
  export function parseWebArgs(args) {
31
26
  const o = { port: 0, open: true, librarian: false, here: false, idleMinutes: 10, piArgs: [] };
32
27
  for (let i = 0; i < args.length; i++) {
@@ -51,127 +46,12 @@ export function parseWebArgs(args) {
51
46
  export { uiLanguage } from "./config.js";
52
47
  // ---------------------------------------------------------------- providers (API keys)
53
48
  export { KEY_PROVIDERS, saveApiKey } from "./keys.js";
54
- /** pi as a child process in RPC mode. Strict JSONL: records end at LF only. */
55
- export class Agent {
56
- proc;
57
- decoder = new StringDecoder("utf8");
58
- buf = "";
59
- n = 0;
60
- pending = new Map();
61
- listeners = new Set();
62
- stderr = "";
63
- onExit;
64
- command;
65
- args;
66
- cwd;
67
- env;
68
- constructor(command, args, cwd, env) {
69
- this.command = command;
70
- this.args = args;
71
- this.cwd = cwd;
72
- this.env = env;
73
- }
74
- start(extra = []) {
75
- this.buf = "";
76
- this.decoder = new StringDecoder("utf8");
77
- const proc = spawn(this.command, [...this.args, ...extra], { cwd: this.cwd, env: this.env, stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
78
- this.proc = proc;
79
- proc.stdout.on("data", (chunk) => this.feed(this.decoder.write(chunk)));
80
- proc.stderr.on("data", (d) => {
81
- this.stderr = (this.stderr + d.toString()).slice(-20_000);
82
- });
83
- proc.once("exit", (code) => {
84
- if (this.proc !== proc)
85
- return;
86
- this.proc = undefined;
87
- for (const p of this.pending.values()) {
88
- clearTimeout(p.timer);
89
- p.reject(new Error(`pi stopped (code ${code}). ${this.stderr.slice(-500)}`));
90
- }
91
- this.pending.clear();
92
- this.onExit?.(code);
93
- });
94
- }
95
- feed(text) {
96
- this.buf += text;
97
- let i;
98
- while ((i = this.buf.indexOf("\n")) >= 0) {
99
- const line = this.buf.slice(0, i).replace(/\r$/, "");
100
- this.buf = this.buf.slice(i + 1);
101
- if (!line.trim())
102
- continue;
103
- let rec;
104
- try {
105
- rec = JSON.parse(line);
106
- }
107
- catch {
108
- continue;
109
- }
110
- if (rec.type === "response" && typeof rec.id === "string" && this.pending.has(rec.id)) {
111
- const p = this.pending.get(rec.id);
112
- this.pending.delete(rec.id);
113
- clearTimeout(p.timer);
114
- if (rec.success === false)
115
- p.reject(new Error(rec.error ?? `${rec.command} failed`));
116
- else
117
- p.resolve(rec.data);
118
- continue;
119
- }
120
- for (const l of this.listeners)
121
- l(rec);
122
- }
123
- }
124
- on(l) {
125
- this.listeners.add(l);
126
- return () => this.listeners.delete(l);
127
- }
128
- write(rec) {
129
- const stdin = this.proc?.stdin;
130
- if (!stdin || !stdin.writable)
131
- throw new Error("pi is not running");
132
- stdin.write(`${JSON.stringify(rec)}\n`);
133
- }
134
- request(cmd, timeoutMs = 30_000) {
135
- const id = `w${++this.n}`;
136
- return new Promise((resolvePromise, reject) => {
137
- const timer = setTimeout(() => {
138
- this.pending.delete(id);
139
- reject(new Error(`pi did not answer ${String(cmd.type)} in time`));
140
- }, timeoutMs);
141
- this.pending.set(id, { resolve: resolvePromise, reject, timer });
142
- try {
143
- this.write({ ...cmd, id });
144
- }
145
- catch (err) {
146
- clearTimeout(timer);
147
- this.pending.delete(id);
148
- reject(err);
149
- }
150
- });
151
- }
152
- async stop() {
153
- const proc = this.proc;
154
- if (!proc)
155
- return;
156
- this.proc = undefined;
157
- for (const p of this.pending.values()) {
158
- clearTimeout(p.timer);
159
- p.reject(new Error("pi was stopped"));
160
- }
161
- this.pending.clear();
162
- await new Promise((done) => {
163
- const t = setTimeout(() => {
164
- proc.kill("SIGKILL");
165
- done();
166
- }, 3000);
167
- proc.once("exit", () => {
168
- clearTimeout(t);
169
- done();
170
- });
171
- proc.kill();
172
- });
173
- }
174
- }
49
+ // ---------------------------------------------------------------- files (shared with the terminal screens)
50
+ import { allowedFile, listNotes, openExternal, openFile, OPENABLE, wikilinkTarget } from "./files.js";
51
+ export { allowedFile, listNotes, openExternal, OPENABLE, wikilinkTarget };
52
+ // ---------------------------------------------------------------- pi in RPC mode (src/rpc.ts)
53
+ import { Agent, findLibrary, isDialog, Side, withHints } from "./rpc.js";
54
+ export { Agent };
175
55
  // ---------------------------------------------------------------- helpers
176
56
  const TYPES = {
177
57
  ".html": "text/html; charset=utf-8",
@@ -186,41 +66,6 @@ const TYPES = {
186
66
  const NOTE_TYPES = new Set([".md", ".markdown"]);
187
67
  /** Images that a note shows (a figure cut out of a PDF, for example). */
188
68
  const IMAGE_TYPES = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
189
- /** Files that the Open button may hand to the system (never programs or scripts). */
190
- export const OPENABLE = new Set([".md", ".markdown", ".txt", ".pdf", ".bib", ".csv", ".qmd"]);
191
- /** A file inside the working folder or the Sub-Sub folder, judged by its real path, so a link cannot point outside. */
192
- export function allowedFile(raw, cwd, vault) {
193
- // expand() resolves against the process folder: use it for "~" only.
194
- const target = resolve(cwd, raw.startsWith("~") ? expand(raw) : raw);
195
- const real = realish(target);
196
- const inside = within(real, realish(cwd)) || Boolean(vault && within(real, realish(vault)));
197
- return inside && existsSync(target) && statSync(target).isFile() ? target : undefined;
198
- }
199
- /** The note a [[wikilink]] names: the first <folder>/<name>.md in the Sub-Sub folder, as Obsidian would find it. */
200
- export function wikilinkTarget(name, root) {
201
- const file = name.endsWith(".md") ? name : `${name}.md`;
202
- return ["", ...NOTE_DIRS, SETTINGS_DIR].map((d) => join(root, d, file)).find((f) => existsSync(f));
203
- }
204
- /** The notes in the Sub-Sub folders, newest first (dot folders such as Research/.plans left out). */
205
- export function listNotes(root, max = 300) {
206
- const out = [];
207
- for (const d of [...NOTE_DIRS, SETTINGS_DIR]) {
208
- let names = [];
209
- try {
210
- names = readdirSync(join(root, d), { recursive: true });
211
- }
212
- catch {
213
- continue;
214
- }
215
- for (const n of names) {
216
- if (!n.endsWith(".md") || n.split(/[\\/]/).some((x) => x.startsWith(".")))
217
- continue;
218
- const path = join(root, d, n);
219
- out.push({ path, rel: join(d, n), modified: statSync(path).mtimeMs });
220
- }
221
- }
222
- return out.sort((a, b) => b.modified - a.modified).slice(0, max);
223
- }
224
69
  const CSP = [
225
70
  "default-src 'none'",
226
71
  "script-src 'self'",
@@ -232,14 +77,6 @@ const CSP = [
232
77
  "form-action 'none'",
233
78
  "frame-ancestors 'none'",
234
79
  ].join("; ");
235
- function readText(path) {
236
- try {
237
- return readFileSync(path, "utf8");
238
- }
239
- catch {
240
- return "";
241
- }
242
- }
243
80
  function same(a, b) {
244
81
  const x = Buffer.from(a);
245
82
  const y = Buffer.from(b);
@@ -285,20 +122,6 @@ function alive(pid) {
285
122
  return err.code === "EPERM";
286
123
  }
287
124
  }
288
- /** Open a URL or a file with the system's default program. */
289
- export function openExternal(target) {
290
- const opts = { detached: true, stdio: "ignore", windowsHide: true };
291
- let child;
292
- if (process.platform === "darwin")
293
- child = spawn("open", [target], opts);
294
- // rundll32 hands the target to the default program without cmd.exe parsing it.
295
- else if (process.platform === "win32")
296
- child = spawn("rundll32", ["url.dll,FileProtocolHandler", target], opts);
297
- else
298
- child = spawn("xdg-open", [target], opts);
299
- child.on("error", () => { });
300
- child.unref();
301
- }
302
125
  /**
303
126
  * Start the web server and pi. Resolves when both are listening; the caller
304
127
  * decides whether to open a browser. Used by `subsub web` and by the tests.
@@ -321,7 +144,7 @@ export async function startWeb(opts, env = process.env, log = () => { }) {
321
144
  const clients = new Set();
322
145
  const dialogs = new Map();
323
146
  let subsubState = null;
324
- let lastFind = null;
147
+ const side = new Side();
325
148
  let busy = false;
326
149
  let lastSeen = Date.now();
327
150
  let restarting = false;
@@ -345,27 +168,14 @@ export async function startWeb(opts, env = process.env, log = () => { }) {
345
168
  }
346
169
  agent.on((ev) => {
347
170
  if (ev.type === "extension_ui_request") {
348
- if (["select", "confirm", "input", "editor"].includes(ev.method)) {
171
+ if (isDialog(ev)) {
349
172
  dialogs.set(ev.id, ev);
350
173
  broadcast(ev);
351
174
  }
352
- else if (ev.method === "setStatus" && ev.statusKey === "subsub.web") {
353
- try {
354
- subsubState = ev.statusText ? JSON.parse(ev.statusText) : null;
355
- }
356
- catch {
357
- subsubState = null;
358
- }
175
+ else if (side.take(ev) === "state") {
176
+ subsubState = side.state;
359
177
  broadcast({ type: "subsub_state", state: subsubState });
360
178
  }
361
- else if (ev.method === "setStatus" && ev.statusKey === "subsub.find") {
362
- try {
363
- lastFind = ev.statusText ? JSON.parse(ev.statusText) : null;
364
- }
365
- catch {
366
- lastFind = null;
367
- }
368
- }
369
179
  else if (ev.method === "notify") {
370
180
  broadcast(ev);
371
181
  }
@@ -409,13 +219,7 @@ export async function startWeb(opts, env = process.env, log = () => { }) {
409
219
  out.session = await agent.request({ type: "get_state" });
410
220
  lastSession = out.session.sessionFile ?? lastSession;
411
221
  out.messages = (await agent.request({ type: "get_messages" })).messages;
412
- const commands = (await agent.request({ type: "get_commands" })).commands;
413
- // RPC has no argument hints: read them from the prompt files.
414
- out.commands = commands.map((c) => {
415
- const front = c.source === "prompt" && c.sourceInfo?.path ? frontMatter(readText(c.sourceInfo.path)) : undefined;
416
- const hint = front && field(front, "argument-hint");
417
- return hint ? { ...c, hint } : c;
418
- });
222
+ out.commands = withHints((await agent.request({ type: "get_commands" })).commands);
419
223
  }
420
224
  catch (err) {
421
225
  out.error = err.message;
@@ -697,8 +501,7 @@ export async function startWeb(opts, env = process.env, log = () => { }) {
697
501
  const q = (url.searchParams.get("q") ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
698
502
  if (!q)
699
503
  return send(res, 200, { q, items: [] });
700
- await agent.request({ type: "prompt", message: `/subsub-find ${q}` });
701
- return send(res, 200, lastFind?.q === q ? lastFind : { q, items: [] });
504
+ return send(res, 200, await findLibrary(agent, side, q));
702
505
  }
703
506
  case "POST /api/session/name": {
704
507
  const body = await readJson(req);
@@ -741,10 +544,7 @@ export async function startWeb(opts, env = process.env, log = () => { }) {
741
544
  // Documents only: a file that the model wrote must never be run.
742
545
  if (!OPENABLE.has(extname(target).toLowerCase()))
743
546
  return send(res, 403, { error: "Sub-Sub opens only notes and documents (.md, .txt, .pdf, .bib, .csv, .qmd)." });
744
- if (vault && within(target, vault) && obsidianRoot(vault))
745
- openExternal(`obsidian://open?path=${encodeURIComponent(target)}`);
746
- else
747
- openExternal(target);
547
+ openFile(target, vault);
748
548
  return send(res, 200, { ok: true });
749
549
  }
750
550
  case "POST /api/quit":
package/dist/words.js ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The plain words of the web view (actions, suggestions, names of tools, in English and Portuguese), for the terminal screens.
3
+ * web/i18n.js is a browser module with no imports; it is loaded here as it is, so the two views never differ.
4
+ */
5
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
6
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
7
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
8
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
9
+ });
10
+ }
11
+ return path;
12
+ };
13
+ import { join } from "node:path";
14
+ import { pathToFileURL } from "node:url";
15
+ import { PACKAGE_DIR } from "./cli.js";
16
+ let words;
17
+ /** A dynamic import: a static one would pull a file outside src/ into the TypeScript build. */
18
+ export function loadWords() {
19
+ words ??= import(__rewriteRelativeImportExtension(pathToFileURL(join(PACKAGE_DIR, "web", "i18n.js")).href));
20
+ return words;
21
+ }
22
+ /** What a tool is called while it runs ("zotero_find_items" is "search the library"); unknown tools keep their name. */
23
+ export function toolWord(w, lang, name) {
24
+ const short = name.replace(/^(zotero|scholar)_/, "");
25
+ return w.TOOLS[lang][short] ?? w.TOOLS.en[short] ?? name;
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiagojct/subsub",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "Sub-Sub: a research assistant that works from your own Zotero library, in the browser or the terminal, built on pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ // Writes the pi themes of Sub-Sub (themes/subsub-<name>-<scheme>.json) from the colours in the front matter of
3
+ // DESIGN.md and the role map in scripts/plate.json. The Researcher uses Prussian blue as its accent (glauca), the
4
+ // Librarian madder (try-works). Errors have no colour of their own, removals are muted ink (the strike-through is
5
+ // drawn by the code), and every meaning also has a glyph or a word, so the themes work for readers who do not see the washes.
6
+ //
7
+ // node scripts/make-themes.mjs
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { fileURLToPath, pathToFileURL } from "node:url";
11
+
12
+ const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
13
+ const SCHEMA = "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json";
14
+
15
+ /** The `colors:` block of the DESIGN.md front matter as {token: "#rrggbb"}. */
16
+ export function designColors(markdown) {
17
+ const colors = {};
18
+ let inBlock = false;
19
+ for (const line of markdown.split("\n")) {
20
+ if (/^colors:\s*$/.test(line)) inBlock = true;
21
+ else if (inBlock && /^\S/.test(line)) break;
22
+ else if (inBlock) {
23
+ const m = line.match(/^\s+([a-z0-9-]+):\s*"(#[0-9a-fA-F]{6})"\s*$/);
24
+ if (m) colors[m[1]] = m[2].toLowerCase();
25
+ }
26
+ }
27
+ return colors;
28
+ }
29
+
30
+
31
+ const CUBE = [0, 95, 135, 175, 215, 255];
32
+ /** The RGB of an xterm-256 colour index from 16 to 255 (the 16 system colours depend on the terminal). */
33
+ export function xterm256(i) {
34
+ if (i >= 232) return [8 + (i - 232) * 10, 8 + (i - 232) * 10, 8 + (i - 232) * 10];
35
+ const n = i - 16;
36
+ return [CUBE[Math.floor(n / 36)], CUBE[Math.floor(n / 6) % 6], CUBE[n % 6]];
37
+ }
38
+ const toRgb = (h) => [1, 3, 5].map((k) => parseInt(h.slice(k, k + 2), 16));
39
+ const luminance = (rgb) => {
40
+ const [r, g, b] = rgb.map((v) => v / 255).map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4));
41
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
42
+ };
43
+ export function contrastRatio(a, b) {
44
+ const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
45
+ return (hi + 0.05) / (lo + 0.05);
46
+ }
47
+
48
+ /** The xterm-256 index nearest to a colour that reaches `min` contrast against `against` (the closest index with enough contrast, or the one with the most). */
49
+ export function nearest256(hex, against, min) {
50
+ const want = toRgb(hex);
51
+ const ranked = [];
52
+ for (let i = 16; i < 256; i++) {
53
+ const c = xterm256(i);
54
+ ranked.push({ i, d: (c[0] - want[0]) ** 2 * 0.3 + (c[1] - want[1]) ** 2 * 0.59 + (c[2] - want[2]) ** 2 * 0.11, k: against ? contrastRatio(c, against) : Infinity });
55
+ }
56
+ ranked.sort((a, b) => a.d - b.d);
57
+ return (ranked.find((r) => r.k >= min) ?? ranked.sort((a, b) => b.k - a.k)[0]).i;
58
+ }
59
+
60
+ // Contrast that each colour variable needs against the terminal's own background: text 4.5, lines and bullets 3, rules none.
61
+ const NEEDS = { ink: 4.5, muted: 4.5, faint: 4.5, prussian: 4.5, madder: 4.5, sap: 4.5, gamboge: 4.5, mark: 3, rule: 0 };
62
+ // Roles whose background is left to the terminal in the 256-colour themes: a wash that is quantised to the cube loses its contrast.
63
+ const DEFAULT_BG = ["userMessageBg", "customMessageBg", "toolPendingBg", "toolSuccessBg", "toolErrorBg", "searchMatchBg"];
64
+
65
+ /** The same theme with xterm-256 indexes, for terminals without truecolor. */
66
+ function to256(theme) {
67
+ const v = theme.vars;
68
+ const sheet = toRgb(v.sheet);
69
+ const vars = {};
70
+ for (const name of Object.keys(NEEDS)) vars[name] = nearest256(v[name], sheet, NEEDS[name]);
71
+ vars.accent = vars[Object.keys(NEEDS).find((n) => v[n] === v.accent)];
72
+ // The selection needs the text colour to stay readable on it.
73
+ vars.wash = nearest256(v.wash, xterm256(vars.ink), 4.5);
74
+ const colors = { ...theme.colors };
75
+ for (const r of DEFAULT_BG) colors[r] = "";
76
+ return { ...theme, name: `${theme.name}-256`, vars, colors, export: theme.export };
77
+ }
78
+
79
+ /** {"subsub-glauca-dark": theme, "subsub-glauca-dark-256": theme, ...} for every theme and scheme of the plate file. */
80
+ export function buildThemes(designMd, plate) {
81
+ const design = designColors(designMd);
82
+ const out = {};
83
+ for (const [name, mode] of Object.entries(plate.themes)) {
84
+ for (const scheme of ["dark", "light"]) {
85
+ const vars = {};
86
+ for (const [v, key] of Object.entries(plate.tokens[scheme])) {
87
+ if (!design[key]) throw new Error(`DESIGN.md has no colour "${key}" (${scheme} ${v})`);
88
+ vars[v] = design[key];
89
+ }
90
+ vars.accent = vars[mode.accent];
91
+ vars.wash = vars[mode.wash];
92
+ const colors = {};
93
+ for (const [role, ref] of Object.entries(plate.roles)) colors[role] = ref === "$accent" ? "accent" : ref === "$wash" ? "wash" : ref;
94
+ const id = `subsub-${name}-${scheme}`;
95
+ out[id] = { $schema: SCHEMA, name: id, vars, colors, export: { pageBg: vars.sheet, cardBg: vars.field, infoBg: vars.sunk } };
96
+ out[`${id}-256`] = to256(out[id]);
97
+ }
98
+ }
99
+ return out;
100
+ }
101
+
102
+ function main() {
103
+ const design = fs.readFileSync(path.join(ROOT, "DESIGN.md"), "utf8");
104
+ const plate = JSON.parse(fs.readFileSync(path.join(ROOT, "scripts", "plate.json"), "utf8"));
105
+ fs.mkdirSync(path.join(ROOT, "themes"), { recursive: true });
106
+ for (const [id, theme] of Object.entries(buildThemes(design, plate))) {
107
+ fs.writeFileSync(path.join(ROOT, "themes", `${id}.json`), JSON.stringify(theme, null, "\t") + "\n");
108
+ console.log(`themes/${id}.json`);
109
+ }
110
+ }
111
+
112
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) main();
@@ -0,0 +1,35 @@
1
+ {
2
+ "tokens": {
3
+ "light": {
4
+ "sheet": "sheet", "sunk": "sunk-field", "field": "plate-field", "ink": "copperplate-ink", "muted": "ink-muted", "faint": "ink-faint", "rule": "fine-rule", "mark": "plate-mark",
5
+ "prussian": "prussian-blue", "prussianWash": "prussian-wash", "madder": "madder", "madderWash": "madder-wash", "sap": "sap-green", "sapWash": "sap-wash", "gamboge": "gamboge", "gambogeWash": "gamboge-wash"
6
+ },
7
+ "dark": {
8
+ "sheet": "steel-ground", "sunk": "steel-sunk", "field": "steel-field", "ink": "steel-line", "muted": "steel-muted", "faint": "steel-faint", "rule": "steel-fine-rule", "mark": "steel-plate-mark",
9
+ "prussian": "steel-prussian", "prussianWash": "steel-prussian-wash", "madder": "steel-madder", "madderWash": "steel-madder-wash", "sap": "steel-sap", "sapWash": "steel-sap-wash", "gamboge": "steel-gamboge", "gambogeWash": "steel-gamboge-wash"
10
+ }
11
+ },
12
+ "themes": {
13
+ "glauca": { "accent": "prussian", "wash": "prussianWash" },
14
+ "try-works": { "accent": "madder", "wash": "madderWash" }
15
+ },
16
+ "roles": {
17
+ "text": "ink", "userMessageText": "ink", "customMessageText": "ink", "toolTitle": "ink", "mdHeading": "ink", "mdCode": "ink", "mdCodeBlock": "ink", "error": "ink",
18
+ "muted": "muted", "toolOutput": "muted", "mdQuote": "muted", "toolDiffContext": "muted", "thinkingText": "muted", "toolDiffRemoved": "muted",
19
+ "dim": "faint", "mdLinkUrl": "faint", "syntaxComment": "faint",
20
+ "border": "mark", "mdQuoteBorder": "mark", "mdListBullet": "mark",
21
+ "borderMuted": "rule", "mdHr": "rule", "mdCodeBlockBorder": "rule", "scrollbarTrack": "rule", "thinkingOff": "rule",
22
+ "scrollbarThumb": "faint",
23
+ "accent": "$accent", "borderAccent": "$accent", "bashMode": "$accent", "customMessageLabel": "$accent", "thinkingXhigh": "$accent", "thinkingMax": "$accent",
24
+ "mdLink": "prussian", "thinkingHigh": "prussian",
25
+ "success": "sap", "toolDiffAdded": "sap",
26
+ "warning": "gamboge",
27
+ "selectedBg": "$wash",
28
+ "searchMatchBg": "gambogeWash", "searchMatchText": "ink",
29
+ "userMessageBg": "field", "toolSuccessBg": "field",
30
+ "customMessageBg": "sunk", "toolErrorBg": "sunk",
31
+ "toolPendingBg": "gambogeWash",
32
+ "syntaxKeyword": "prussian", "syntaxNumber": "prussian", "syntaxFunction": "ink", "syntaxVariable": "ink", "syntaxType": "ink", "syntaxString": "muted", "syntaxOperator": "muted", "syntaxPunctuation": "faint",
33
+ "thinkingMinimal": "faint", "thinkingLow": "mark", "thinkingMedium": "muted"
34
+ }
35
+ }