osdy-pi 1.6.1 → 1.8.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/README.md CHANGED
@@ -6,8 +6,11 @@ Osdy Pi gives [Pi](https://github.com/earendil-works/pi) a themed, responsive te
6
6
 
7
7
  ## Release highlights
8
8
 
9
+ The **1.8.0 release candidate** includes the changes below; npm publication is not yet confirmed.
10
+
9
11
  | Area | What ships |
10
12
  | --- | --- |
13
+ | New themes (included starting in 1.8.0) | Gruvbox Dark, Nord, Rosé Pine base dark, and Daniela Cute bring the bundle to 18 themes. |
11
14
  | Message cards | Pi-native colored user cards without emojis and plain assistant Markdown preceded by a separate `🦝` transcript row. |
12
15
  | Header and mascot | A theme-aware `neon` header and selectable `bts` mascot, both responsive and persisted independently. |
13
16
  | Accounts | `/osdy-account` switches Codex profiles in place with atomic activation, rollback, bounded auth files, and process-safe locks. |
@@ -42,7 +45,7 @@ Osdy Pi gives [Pi](https://github.com/earendil-works/pi) a themed, responsive te
42
45
  pi
43
46
  ```
44
47
 
45
- Osdy Pi's own extension and all **14 themes** are bundled with this installation—do not install them separately. Launch through `osdy-pi` (or `npm run pi:dev`) to activate a valid Osdy default account into Pi's shared agent directory before Pi starts. With no default, Pi remains unmanaged; run `/osdy-account` to create a profile and establish a default. On session start, Osdy Pi restores its persisted enabled state when a UI is available and preserves your selected Pi theme. When disabled, it leaves Gentle Shell (or Pi's native UI) untouched.
48
+ Osdy Pi's own extension and themes are bundled—do not install them separately. The **1.8.0 release candidate** bundles **18 themes**, including Gruvbox Dark, Nord, Rosé Pine base dark, and Daniela Cute starting in 1.8.0. Check your installed npm version; publication of 1.8.0 is not yet confirmed. Launch through `osdy-pi` (or `npm run pi:dev`) to activate a valid Osdy default account into Pi's shared agent directory before Pi starts. With no default, Pi remains unmanaged; run `/osdy-account` to create a profile and establish a default. On session start, Osdy Pi restores its persisted enabled state when a UI is available and preserves your selected Pi theme. When disabled, it leaves Gentle Shell (or Pi's native UI) untouched.
46
49
 
47
50
  ### Optional: isolated `osdy` command
48
51
 
@@ -132,13 +135,15 @@ The exclusions prevent Gentle's todo, questionnaire, and agents extensions from
132
135
 
133
136
  The model-facing `todo` tool supports `create`, `update`, `list`, `get`, `delete`, and `clear`. Tool results carry full task snapshots; Pi's current session branch is the TODO authority. Session switches and compaction replay the latest valid branch snapshot, rather than sharing one project-wide list. `/todos` opens a read-only, grouped modal (pending, in progress, completed) in interactive Pi; scroll with arrow or Page Up/Down keys and close with Esc or q. The refreshed modal adds a completed-task progress meter, theme-aware status groups and a scroll-position footer while keeping every task reachable. It is not an interactive Markdown editor. In non-terminal modes with UI support, it retains notification output. ODD's `odd/tasks/*.md` ledger and Engram are separate orchestration records: there is no automatic sync with these Pi TODOs.
134
137
 
135
- In interactive Pi, the persistent widget appears above the editor while visible tasks exist. By default it shows up to five task rows (excluding heading, overflow hint, and spacer), summarizes overflow, and keeps completed rows visible with crossed-out subjects on subsequent turns; expand tool output to view all rows. `ctrl+shift+t` collapses or expands the widget.
138
+ In interactive Pi, the persistent widget appears above the editor while visible tasks exist. Its compact preview shows up to three task rows (excluding heading, overflow hint, and spacer), prioritizes unfinished tasks on overflow, and crosses out completed subjects. In fullscreen Pi, left-click the heading to expand or return to preview; wheel over the expanded widget to move one task at a time beneath the fixed heading. The final task can reach the first task row even in a short dock. A heading-only or hidden dock cannot display tasks; use `/todos` for the full keyboard-accessible list. Regular mode stays compact; Pi's tool-output expansion does not change this widget. `ctrl+shift+t` retains the separate fully collapsed view.
139
+
140
+ Expansion and scroll position survive same-session refresh, tree navigation, and compaction, with offsets clamped as tasks change. Foreground session replacement and shutdown reset the view; background sessions do not affect it. Task data and `/todos` are unchanged.
136
141
 
137
142
  ### Try session TODO
138
143
 
139
- Use an installed 1.5.0-or-later package (or a checkout), ask the agent to track a task with `todo`, then run `/todos`. The widget shows up to five tasks by default; the modal shows the entire grouped list with arrow/Page Up/Down scrolling. If your `maxWidgetLines` configuration overrides the default, set it to `7` to show five task rows.
144
+ Use this checkout (or a package containing the three-task preview), ask the agent to track a task with `todo`, then run `/todos`. The modal shows the entire grouped list with arrow/Page Up/Down scrolling. `maxWidgetLines: 7` permits five task rows in the expanded fullscreen widget; the preview remains capped at three. Smaller budgets also constrain the preview. Fullscreen widget mouse handling uses Pi's public 0.99.1 component APIs.
140
145
 
141
- **Verification to date (not complete):** Prior feature checks passed 187 extension tests, 60 script tests, typecheck, and lint. A disposable Pi TUI with seeded tasks showed the modal's progress meter, grouped rows, scroll footer, and close behavior. Authenticated RPC turns exercised all six `todo` actions, and an authenticated TUI smoke observed widget and `/todos` updates. An RPC lifecycle check observed an empty new session and the completed task on switching back. Compaction replay remains unverified because Pi returned “Nothing to compact (session too small).” Language switching in the authenticated TODO flow remains unverified (an isolated TUI with both extensions did switch English to Spanish). Visual strikethrough of completed rows in the authenticated TUI remains unverified. Launch under an installed Osdy account profile remains unverified. These checks do not establish full feature parity or validate a published npm artifact.
146
+ **Verification to date (not complete):** The user confirmed fullscreen appearance and behavior via `pi:dev`. Automated PTY interaction passed 27 assertions, but fixture cleanup failed and the final task snapshot remains unverified; this was not a full end-to-end pass. Prior feature checks passed 187 extension tests, 60 script tests, typecheck, and lint. A disposable Pi TUI with seeded tasks showed the modal's progress meter, grouped rows, scroll footer, and close behavior. Authenticated RPC turns exercised all six `todo` actions, and an authenticated TUI smoke observed widget and `/todos` updates. An RPC lifecycle check observed an empty new session and the completed task on switching back. Compaction replay remains unverified because Pi returned “Nothing to compact (session too small).” Language switching in the authenticated TODO flow remains unverified (an isolated TUI with both extensions did switch English to Spanish). Visual strikethrough of completed rows in the authenticated TUI remains unverified. Launch under an installed Osdy account profile remains unverified. These checks do not establish full feature parity or validate a published npm artifact.
142
147
 
143
148
  ### TODO configuration and language
144
149
 
@@ -244,7 +249,7 @@ Before rename or removal, close this Pi process when it uses the target and **ma
244
249
 
245
250
  | Area | Included behavior |
246
251
  | --- | --- |
247
- | Themes | 14 built-in themes, including Osdy, Kanagawa, Dracula, Catppuccin, Matrix, and Lucent Orange palettes |
252
+ | Themes | 18 built-in themes, including Osdy, Kanagawa, Dracula, Gruvbox, Nord, Rosé Pine, Daniela Cute, Catppuccin, Matrix, and Lucent Orange palettes |
248
253
  | Header and mascot | Independently selectable `osdy-theme`/`neon` headers and `current`/`bts` mascots |
249
254
  | Messages | Native colored user cards without emojis and plain assistant Markdown with a separate assistant `🦝` row |
250
255
  | Input | Responsive auto editor by default, with selectable simple Pi-native or extended framed modes |
@@ -258,6 +263,11 @@ Before rename or removal, close this Pi process when it uses the target and **ma
258
263
 
259
264
  ### Themes
260
265
 
266
+ The bundle contains **18 themes**; Gruvbox Dark, Nord, Rosé Pine base dark, and Daniela Cute are included starting in **1.8.0**. To try the **1.8.0 release candidate** before npm publication is confirmed:
267
+
268
+ 1. Run `npm run pi:dev` from the 1.8.0 checkout, or `/reload` if that checkout is already loaded in Pi.
269
+ 2. Open `/settings` → **Theme** and select `osdy-pi-gruvbox-dark`, `osdy-pi-nord`, `osdy-pi-rose-pine`, `osdy-pi-daniela-cute`, or another name below.
270
+
261
271
  | Theme | Use |
262
272
  | --- | --- |
263
273
  | `osdy-pi-new` | Landing palette: cyan, violet, silver, and navy. |
@@ -268,6 +278,10 @@ Before rename or removal, close this Pi process when it uses the target and **ma
268
278
  | `osdy-pi-kanagawa-dragon` | Kanagawa Dragon dark palette. |
269
279
  | `osdy-pi-kanagawa-lotus` | Kanagawa Lotus light palette. |
270
280
  | `osdy-pi-dracula` | Dracula Classic dark palette. |
281
+ | `osdy-pi-gruvbox-dark` | [Gruvbox](https://github.com/morhetz/gruvbox#palette) dark palette: warm, retro colors on the medium-contrast background. |
282
+ | `osdy-pi-nord` | [Nord](https://www.nordtheme.com/docs/colors-and-palettes) dark palette: cool, restrained Polar Night, Snow Storm, Frost, and Aurora colors. |
283
+ | `osdy-pi-rose-pine` | [Rosé Pine](https://rosepinetheme.com/palette/ingredients/) base dark palette: soft colors, not Moon or Dawn. |
284
+ | `osdy-pi-daniela-cute` | Daniela Cute: navy panels/export page (`#0B1220`), blue cards (`#111F33`), borders (`#253A55`), blue accents (`#3584E4`), main text (`#D8E2EF`), secondary text (`#93A6BE`), and selection (`#193655`). Distinct status and syntax colors remain; the terminal background remains unchanged. |
271
285
  | `osdy-pi-catppuccin-latte` | Catppuccin Latte light palette. |
272
286
  | `osdy-pi-catppuccin-frappe` | Catppuccin Frappé dark palette. |
273
287
  | `osdy-pi-catppuccin-macchiato` | Catppuccin Macchiato dark palette. |
@@ -408,7 +408,8 @@ void test("every registered theme defines native user message colors", () => {
408
408
  readFileSync(new URL("../../package.json", import.meta.url), "utf8"),
409
409
  ) as { pi: { themes: string[] } };
410
410
 
411
- assert.equal(manifest.pi.themes.length, 14);
411
+ assert.equal(manifest.pi.themes.length, 18);
412
+ assert.equal(new Set(manifest.pi.themes).size, 18, "theme registrations must be unique");
412
413
  for (const themePath of manifest.pi.themes) {
413
414
  const theme = JSON.parse(
414
415
  readFileSync(
@@ -427,6 +428,29 @@ void test("every registered theme defines native user message colors", () => {
427
428
  }
428
429
  });
429
430
 
431
+ void test("new dark themes are explicitly registered with unique matching names", () => {
432
+ const manifest = JSON.parse(
433
+ readFileSync(new URL("../../package.json", import.meta.url), "utf8"),
434
+ ) as { pi: { themes: string[] } };
435
+ const names = manifest.pi.themes.map((path) => {
436
+ const theme = JSON.parse(readFileSync(new URL(`../../${path}`, import.meta.url), "utf8")) as { name: string };
437
+ assert.equal(path, `./themes/${theme.name}.json`);
438
+ return theme.name;
439
+ });
440
+ assert.equal(new Set(names).size, names.length, "theme names must be unique");
441
+ for (const name of ["osdy-pi-gruvbox-dark", "osdy-pi-nord", "osdy-pi-rose-pine", "osdy-pi-daniela-cute"]) {
442
+ assert.equal(manifest.pi.themes.filter((path) => path === `./themes/${name}.json`).length, 1);
443
+ assert.equal(names.filter((registered) => registered === name).length, 1);
444
+ }
445
+ });
446
+
447
+ void test("README documents all 18 themes including Daniela Cute", () => {
448
+ const readme = readFileSync(new URL("../../README.md", import.meta.url), "utf8");
449
+ assert.match(readme, /\*\*18 themes\*\*/);
450
+ assert.match(readme, /18 built-in themes/);
451
+ assert.match(readme, /\| `osdy-pi-daniela-cute` \| Daniela Cute/);
452
+ });
453
+
430
454
  void test("Tokyo Night retains its approved native user card colors", () => {
431
455
  const theme = JSON.parse(
432
456
  readFileSync(
@@ -5,6 +5,7 @@ import test from "node:test";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import ts from "typescript";
7
7
  import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
8
+ import type { Component, TuiMouseEvent, TuiMode } from "@earendil-works/pi-tui";
8
9
  import type { TodoConfig } from "./todo-config.js";
9
10
  import type { TodoI18n } from "./todo-i18n.js";
10
11
 
@@ -25,11 +26,11 @@ registerHooks({
25
26
  const { registerTodoWidget } = await import("./todo-widget.js");
26
27
  const { createTodoSessionStore } = await import("./todo-session.js");
27
28
  const { applyTodo } = await import("./todo-domain.js");
28
- const { visibleWidth } = await import("@earendil-works/pi-tui");
29
+ const { visibleWidth, Container } = await import("@earendil-works/pi-tui");
29
30
  const theme = { fg: (_color: string, text: string) => text, strikethrough: (text: string) => `~${text}~` };
30
31
  function harness(config: () => TodoConfig = () => ({}), i18n?: TodoI18n) {
31
32
  type Context = { hasUI: boolean; ui: typeof ui; sessionManager: { getSessionId(): string } };
32
- type Factory = (tui: { requestRender(force?: boolean): void }, theme: Theme) => { render(width: number): string[] };
33
+ type Factory = (tui: { mode: TuiMode; requestRender(force?: boolean): void }, theme: Theme) => Component;
33
34
  const handlers = new Map<string, ((event: { toolName?: string }, ctx: Context) => void)[]>();
34
35
  const shortcuts = new Map<string, (ctx: Context) => void>();
35
36
  const widgets: Array<{ key: string; factory: Factory | undefined; options: { placement: string } | undefined }> = [];
@@ -42,8 +43,27 @@ function harness(config: () => TodoConfig = () => ({}), i18n?: TodoI18n) {
42
43
  const emit = (name: string, context: Context, event: { toolName?: string } = {}) => { for (const handler of handlers.get(name) ?? []) handler(event, context); };
43
44
  const state = (id: string, tasks: { id: number; subject: string; status: "pending" | "in_progress" | "completed" | "deleted"; blockedBy?: number[]; activeForm?: string }[]) => store.set(id, { tasks, nextId: Math.max(1, ...tasks.map((t) => t.id + 1)) });
44
45
  const action = (id: string, kind: "create" | "clear", subject?: string) => store.set(id, applyTodo(store.get(id), kind, subject === undefined ? {} : { subject }).state);
45
- const lines = (width = 80): string[] => widgets.at(-1)?.factory?.({ requestRender(force?: boolean) { renders.push(force === true); } }, theme as Theme).render(width) ?? [];
46
- return { ui, widgets, renders, shortcuts, ctx, emit, state, action, lines };
46
+ const host = { mode: "fullscreen" as TuiMode, requestRender(force?: boolean) { renders.push(force === true); } };
47
+ let factory: Factory | undefined;
48
+ let component: Component | undefined;
49
+ const current = () => {
50
+ if (factory !== widgets.at(-1)?.factory) {
51
+ factory = widgets.at(-1)?.factory;
52
+ component = factory?.(host, theme as Theme);
53
+ }
54
+ return component;
55
+ };
56
+ const lines = (width = 80): string[] => current()?.render(width) ?? [];
57
+ const mouse = (change: Partial<TuiMouseEvent> = {}, clippedHeight?: number) => {
58
+ const child = current();
59
+ if (!child) return undefined;
60
+ const container = new Container(); container.addChild(child);
61
+ const rendered = container.render(80);
62
+ return container.handleMouse({ type: "click", button: "left", x: 0, y: 0,
63
+ screenX: 0, screenY: 0, width: 80, height: clippedHeight ?? rendered.length,
64
+ shift: false, alt: false, ctrl: false, ...change });
65
+ };
66
+ return { ui, host, widgets, renders, shortcuts, ctx, emit, state, action, lines, mouse, store };
47
67
  }
48
68
  const task = (id: number, status: "pending" | "in_progress" | "completed" | "deleted" = "pending", extra: { activeForm?: string; blockedBy?: number[] } = {}) => ({ id, status, subject: `Task ${id}`, ...extra });
49
69
 
@@ -102,24 +122,23 @@ void test("deleted tasks remain excluded while completed and pending tasks persi
102
122
  assert.deepEqual(h.lines(), []);
103
123
  });
104
124
 
105
- void test("default widget shows five task rows at the six-task boundary, with a truthful hint", () => {
125
+ void test("default widget shows three task rows at the six-task boundary, with a truthful hint", () => {
106
126
  const h = harness(); const c = h.ctx("main"); h.emit("session_start", c);
107
127
  h.state("main", Array.from({ length: 6 }, (_, i) => task(i + 1)));
108
128
  h.emit("tool_execution_end", c, { toolName: "todo" });
109
- assert.deepEqual(h.lines(), ["● Todos (0/6)", ...Array.from({ length: 5 }, (_, i) => `├─ ○ Task ${i + 1}`), "└─ +1 more (1 pending)", ""]);
110
- h.ui.expanded = true;
111
- assert.equal(h.lines().length, 8);
112
- assert.equal(h.lines()[6], "└─ ○ Task 6");
129
+ assert.deepEqual(h.lines(), ["● Todos (0/6)", ...Array.from({ length: 3 }, (_, i) => `├─ ○ Task ${i + 1}`), "└─ +3 more (3 pending)", ""]);
130
+ h.ui.expanded = true; // Pi's tool expansion is not the widget's display preference.
131
+ assert.equal(h.lines().length, 6);
113
132
  });
114
133
 
115
- void test("five or fewer default task rows keep completed subjects struck through without overflow", () => {
134
+ void test("three or fewer preview rows retain completed subjects and completed overflow counts", () => {
116
135
  const h = harness(); const c = h.ctx("main"); h.emit("session_start", c);
117
- h.state("main", [task(1, "completed"), ...Array.from({ length: 4 }, (_, i) => task(i + 2))]);
136
+ h.state("main", [task(1, "completed"), task(2), task(3)]);
118
137
  h.emit("tool_execution_end", c, { toolName: "todo" });
119
- assert.deepEqual(h.lines(), ["● Todos (1/5)", "├─ ✓ ~Task 1~", "├─ ○ Task 2", "├─ ○ Task 3", "├─ ○ Task 4", "└─ ○ Task 5", ""]);
138
+ assert.deepEqual(h.lines(), ["● Todos (1/3)", "├─ ✓ ~Task 1~", "├─ ○ Task 2", "└─ ○ Task 3", ""]);
120
139
  h.state("main", [task(1, "completed"), ...Array.from({ length: 5 }, (_, i) => task(i + 2, "completed"))]);
121
- assert.equal(h.lines().length, 8);
122
- assert.equal(h.lines().at(-2), "└─ +1 more (1 completed)");
140
+ assert.equal(h.lines().length, 6);
141
+ assert.equal(h.lines().at(-2), "└─ +3 more (3 completed)");
123
142
  assert.match(h.lines()[1] ?? "", /~Task 1~/);
124
143
  });
125
144
 
@@ -127,15 +146,16 @@ void test("default widget retains completed strikethrough when overflow contains
127
146
  const h = harness(); const c = h.ctx("main"); h.emit("session_start", c);
128
147
  h.state("main", [task(1, "completed"), ...Array.from({ length: 14 }, (_, i) => task(i + 2))]);
129
148
  h.emit("tool_execution_end", c, { toolName: "todo" });
130
- assert.deepEqual(h.lines(), ["● Todos (1/15)", ...Array.from({ length: 5 }, (_, i) => `├─ ○ Task ${i + 2}`), "└─ +10 more (1 completed, 9 pending)", ""]);
149
+ assert.deepEqual(h.lines(), ["● Todos (1/15)", ...Array.from({ length: 3 }, (_, i) => `├─ ○ Task ${i + 2}`), "└─ +12 more (1 completed, 11 pending)", ""]);
131
150
  h.ui.expanded = true;
132
- assert.equal(h.lines().length, 17);
151
+ assert.equal(h.lines().length, 6);
152
+ h.mouse();
133
153
  assert.equal(h.lines()[1], "├─ ✓ ~Task 1~");
134
154
  });
135
155
 
136
156
  void test("configured widget budget preserves small and boundary lists without hiding overflow", () => {
137
157
  for (const maxWidgetLines of [3, 7.5, 8]) {
138
- const slots = Math.floor(maxWidgetLines - 2);
158
+ const slots = Math.min(3, Math.floor(maxWidgetLines - 2));
139
159
  const h = harness(() => ({ maxWidgetLines })); const c = h.ctx("main"); h.emit("session_start", c);
140
160
  h.state("main", Array.from({ length: slots }, (_, i) => task(i + 1)));
141
161
  h.emit("tool_execution_end", c, { toolName: "todo" });
@@ -147,12 +167,12 @@ void test("configured widget budget preserves small and boundary lists without h
147
167
  }
148
168
  });
149
169
 
150
- void test("overflow drops completed first and expands on Pi tool expansion", () => {
170
+ void test("overflow drops completed first and ignores Pi tool expansion", () => {
151
171
  const h = harness(); const c = h.ctx("main"); h.emit("session_start", c);
152
172
  h.state("main", Array.from({ length: 15 }, (_, i) => task(i + 1, i % 3 === 0 ? "completed" : "pending")));
153
173
  h.emit("tool_execution_end", c, { toolName: "todo" });
154
- assert.equal(h.lines().length, 8); assert.match(h.lines()[6] ?? "", /\+10 more \(5 completed, 5 pending\)/);
155
- h.ui.expanded = true; assert.equal(h.lines().length, 17);
174
+ assert.equal(h.lines().length, 6); assert.match(h.lines()[4] ?? "", /\+12 more \(5 completed, 7 pending\)/);
175
+ h.ui.expanded = true; assert.equal(h.lines().length, 6);
156
176
  });
157
177
 
158
178
  void test("branch lifecycle retains completed rows and rebinds replacement UI safely", () => {
@@ -174,7 +194,7 @@ void test("overflow unfinished tail reports pending and hides completed first",
174
194
  const h = harness(); const c = h.ctx("main"); h.emit("session_start", c);
175
195
  h.state("main", [task(1, "completed"), ...Array.from({ length: 14 }, (_, i) => task(i + 2))]);
176
196
  h.emit("tool_execution_end", c, { toolName: "todo" });
177
- assert.match(h.lines()[6] ?? "", /\+10 more \(1 completed, 9 pending\)/);
197
+ assert.match(h.lines()[4] ?? "", /\+12 more \(1 completed, 11 pending\)/);
178
198
  assert.doesNotMatch(h.lines().join(" "), /Task 1(?!\d)/);
179
199
  });
180
200
 
@@ -185,7 +205,7 @@ void test("configured budget changes each render while key binds once and hint f
185
205
  h.emit("tool_execution_end", c, { toolName: "todo" });
186
206
  assert.equal(h.lines().length, 4);
187
207
  config = { maxWidgetLines: 8, collapseKey: "ctrl+g" };
188
- assert.equal(h.lines().length, 7);
208
+ assert.equal(h.lines().length, 6);
189
209
  assert.equal(h.shortcuts.has("ctrl+g"), false);
190
210
  h.shortcuts.get("alt+f5")?.(c);
191
211
  assert.match(h.lines()[1] ?? "", /ctrl\+g to expand/);
@@ -224,6 +244,84 @@ void test("widget reads live translations for heading, overflow, statuses and co
224
244
  assert.equal(h.lines()[1], "└─ ctrl+shift+t to expand");
225
245
  });
226
246
 
247
+ void test("fullscreen header alone toggles bounded expansion; regular mode stays compact", () => {
248
+ const h = harness(); const c = h.ctx("main");
249
+ h.state("main", Array.from({ length: 8 }, (_, i) => task(i + 1)));
250
+ h.emit("session_start", c);
251
+ const original = structuredClone(h.store.get("main"));
252
+ assert.equal(h.mouse({ button: "right" }), undefined);
253
+ assert.equal(h.mouse({ y: 1 }), undefined);
254
+ assert.equal(h.mouse({ type: "press" }), undefined);
255
+ assert.equal(h.mouse({ type: "wheel", wheelDelta: 1 }), undefined);
256
+ h.host.mode = "regular";
257
+ assert.equal(h.mouse(), undefined);
258
+ assert.equal(h.lines().length, 6);
259
+ h.host.mode = "fullscreen";
260
+ assert.equal(h.mouse()?.handled, true);
261
+ assert.equal(h.lines().length, 8); // bounded to five tasks, not all eight
262
+ assert.equal(h.mouse()?.handled, true);
263
+ assert.equal(h.lines().length, 6);
264
+ assert.deepEqual(h.store.get("main"), original);
265
+ });
266
+
267
+ void test("real Container intrinsic-height dispatch reaches the last task with only one dock task row", () => {
268
+ const h = harness(); const c = h.ctx("main");
269
+ h.state("main", Array.from({ length: 9 }, (_, i) => task(i + 1)));
270
+ h.emit("session_start", c); h.mouse({}, 2);
271
+ const original = structuredClone(h.store.get("main"));
272
+ for (let id = 1; id <= 9; id++) {
273
+ const clippedDock = h.lines().slice(0, 2);
274
+ assert.equal(clippedDock[0], "● Todos (0/9)");
275
+ assert.match(clippedDock[1] ?? "", new RegExp(`Task ${id}$`));
276
+ const result = h.mouse({ type: "wheel", button: "none", y: 1, wheelDelta: 7 }, 2);
277
+ assert.equal(result?.handled, true);
278
+ assert.ok((result?.target.height ?? 0) > 2); // Container forwards intrinsic height
279
+ }
280
+ assert.match(h.lines()[1] ?? "", /Task 9$/);
281
+ assert.equal(h.mouse({ type: "wheel", wheelDelta: 0 }, 2)?.handled, true);
282
+ assert.equal(h.mouse({ type: "wheel", wheelDelta: Number.NaN }, 2)?.handled, true);
283
+ assert.match(h.lines()[1] ?? "", /Task 9$/);
284
+ assert.equal(h.mouse({ type: "wheel", wheelDelta: -3 }, 2)?.handled, true);
285
+ assert.match(h.lines()[1] ?? "", /Task 8$/);
286
+ h.host.mode = "regular";
287
+ assert.equal(h.mouse({ type: "wheel", wheelDelta: 1 }), undefined);
288
+ assert.equal(h.lines().length, 6);
289
+ h.host.mode = "fullscreen";
290
+ assert.match(h.lines()[1] ?? "", /Task 8$/);
291
+ assert.deepEqual(h.store.get("main"), original);
292
+ });
293
+
294
+ void test("expanded offsets survive refresh and tree, clamp on deletion and reset on replacement/shutdown", () => {
295
+ const h = harness(); const c = h.ctx("main");
296
+ h.state("main", Array.from({ length: 7 }, (_, i) => task(i + 1)));
297
+ h.emit("session_start", c); h.mouse();
298
+ for (let i = 0; i < 6; i++) h.mouse({ type: "wheel", wheelDelta: 1 });
299
+ h.renders.length = 0;
300
+ h.emit("tool_execution_end", c, { toolName: "todo" });
301
+ assert.deepEqual(h.renders, [false]);
302
+ assert.match(h.lines()[1] ?? "", /Task 7$/);
303
+ h.emit("session_tree", c); h.emit("session_compact", c);
304
+ assert.match(h.lines()[1] ?? "", /Task 7$/);
305
+ h.state("main", [task(1, "completed"), task(2, "deleted"), task(3, "in_progress")]);
306
+ h.emit("tool_execution_end", c, { toolName: "todo" });
307
+ assert.equal(h.lines()[0], "● Todos (1/2)");
308
+ assert.match(h.lines()[1] ?? "", /Task 3$/);
309
+ assert.match(h.lines().at(-2) ?? "", /\+1 more \(1 completed\)/);
310
+ h.shortcuts.get("ctrl+shift+t")?.(c);
311
+ assert.match(h.lines()[1] ?? "", /to expand/);
312
+ assert.equal(h.mouse({ type: "wheel", wheelDelta: 1 }), undefined);
313
+ h.shortcuts.get("ctrl+shift+t")?.(c);
314
+ assert.match(h.lines()[1] ?? "", /Task 3$/);
315
+ const next = h.ctx("next"); // same foreground UI denotes session replacement
316
+ h.state("next", Array.from({ length: 7 }, (_, i) => task(i + 1)));
317
+ h.emit("session_start", next);
318
+ assert.equal(h.lines().length, 6);
319
+ h.mouse(); assert.match(h.lines()[1] ?? "", /Task 1$/);
320
+ h.emit("session_shutdown", c); assert.notEqual(h.widgets.at(-1)?.factory, undefined);
321
+ h.emit("session_shutdown", next); assert.equal(h.widgets.at(-1)?.factory, undefined);
322
+ h.emit("session_start", next); assert.equal(h.lines().length, 6);
323
+ });
324
+
227
325
  void test("headless and child sessions do not bind, refresh, collapse or dispose foreground", () => {
228
326
  const h = harness(); const main = h.ctx("main"), child = h.ctx("child", true, { ...h.ui });
229
327
  h.emit("session_start", h.ctx("headless", false)); h.state("headless", [task(1)]); h.emit("tool_execution_end", h.ctx("headless", false), { toolName: "todo" }); assert.equal(h.widgets.length, 0);
@@ -1,6 +1,6 @@
1
1
  // Widget behavior adapted independently from @juicesharp/rpiv-todo@2.11.0 (MIT); see root LICENSE.
2
2
  import type { ExtensionAPI, ExtensionContext, ExtensionUIContext, Theme } from "@earendil-works/pi-coding-agent";
3
- import { truncateToWidth, type TUI } from "@earendil-works/pi-tui";
3
+ import { truncateToWidth, type TUI, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
4
4
  import { COLLAPSE_KEY_OFF, getMaxWidgetLines, loadTodoConfig, resolveCollapseKey, type TodoConfig } from "./todo-config.js";
5
5
  import type { TodoTask } from "./todo-domain.js";
6
6
  import { todoI18n, type TodoI18n } from "./todo-i18n.js";
@@ -42,6 +42,9 @@ export function registerTodoWidget(pi: ExtensionAPI, store: Store, config: () =>
42
42
  let mounted = false;
43
43
  let tui: TUI | undefined;
44
44
  let collapsed = false;
45
+ let expanded = false;
46
+ let offset = 0;
47
+ const clampOffset = (length: number) => { offset = Math.max(0, Math.min(offset, length - 1)); };
45
48
  const visible = () => store.get(foreground ?? "").tasks.filter((task) => task.status !== "deleted");
46
49
  const render = (theme: Theme, width: number): string[] => {
47
50
  const tasks = visible();
@@ -55,8 +58,14 @@ export function registerTodoWidget(pi: ExtensionAPI, store: Store, config: () =>
55
58
  const hint = key === COLLAPSE_KEY_OFF ? i18n.t("overlay.collapsed", "collapsed") : i18n.t("overlay.expandHint", "{key} to expand").replace("{key}", key);
56
59
  return [heading, trunc(`${theme.fg("dim", "└─")} ${theme.fg("dim", hint)}`), ""];
57
60
  }
58
- const expanded = ui?.getToolsExpanded?.() === true;
59
- const { rows, completed, pending } = layout(tasks, expanded ? tasks.length + 1 : getMaxWidgetLines(config()) - 1);
61
+ clampOffset(tasks.length);
62
+ const slots = Math.max(1, Math.floor(getMaxWidgetLines(config()) - 2));
63
+ const viewport = expanded && tui?.mode === "fullscreen";
64
+ const rows = viewport ? tasks.slice(offset, offset + slots) : layout(tasks, Math.min(3, slots) + 1).rows;
65
+ const kept = new Set(rows);
66
+ const omitted = tasks.filter((task) => !kept.has(task));
67
+ const completed = omitted.filter((task) => task.status === "completed").length;
68
+ const pending = omitted.length - completed;
60
69
  const ids = tasks.some((task) => (task.blockedBy?.length ?? 0) > 0);
61
70
  const lines = [heading];
62
71
  for (const task of rows) {
@@ -72,14 +81,29 @@ export function registerTodoWidget(pi: ExtensionAPI, store: Store, config: () =>
72
81
  lines.push("");
73
82
  return lines;
74
83
  };
84
+ const handleMouse = (event: TuiMouseEvent): TuiMouseEventResult | undefined => {
85
+ if (tui?.mode !== "fullscreen" || !visible().length) return undefined;
86
+ if (event.type === "click" && event.button === "left" && event.y === 0) {
87
+ collapsed = false; expanded = !expanded; offset = 0;
88
+ } else if (event.type === "wheel" && expanded && !collapsed) {
89
+ // Container supplies intrinsic height, not the clipped dock height. Move one
90
+ // task per event, allowing the final task to reach the first row below heading.
91
+ const delta = event.wheelDelta ?? 0;
92
+ if (Number.isFinite(delta)) offset += Math.sign(delta);
93
+ clampOffset(visible().length);
94
+ } else return undefined;
95
+ tui.requestRender();
96
+ return { handled: true };
97
+ };
75
98
  const refresh = () => {
99
+ clampOffset(visible().length);
76
100
  if (!ui) return;
77
101
  if (!visible().length) {
78
102
  if (mounted) { ui.setWidget(KEY, undefined); mounted = false; tui = undefined; }
79
103
  } else if (!mounted) {
80
104
  ui.setWidget(KEY, (host, theme) => {
81
105
  tui = host;
82
- return { render: (width: number) => render(ui?.theme ?? theme, width), invalidate() {} };
106
+ return { render: (width: number) => render(ui?.theme ?? theme, width), handleMouse, invalidate() {} };
83
107
  }, { placement: "aboveEditor" });
84
108
  mounted = true;
85
109
  } else tui?.requestRender();
@@ -89,7 +113,10 @@ export function registerTodoWidget(pi: ExtensionAPI, store: Store, config: () =>
89
113
  pi.on("session_start", (_event, ctx) => {
90
114
  if (!ctx.hasUI) return;
91
115
  const session = id(ctx);
92
- if (foreground !== undefined && foreground !== session) return;
116
+ // A different session on another UI is a background child. The foreground
117
+ // UI reused for a new session is replacement; tree/compact keep preference.
118
+ if (foreground !== undefined && foreground !== session && ctx.ui !== ui) return;
119
+ if (foreground !== session) { collapsed = false; expanded = false; offset = 0; }
93
120
  if (ui && ui !== ctx.ui && mounted) ui.setWidget(KEY, undefined);
94
121
  if (ui !== ctx.ui) { mounted = false; tui = undefined; }
95
122
  foreground = session; ui = ctx.ui; refresh();
@@ -99,7 +126,7 @@ export function registerTodoWidget(pi: ExtensionAPI, store: Store, config: () =>
99
126
  pi.on("session_shutdown", (_event, ctx) => {
100
127
  if (!foregroundEvent(ctx)) return;
101
128
  if (mounted) ui?.setWidget(KEY, undefined);
102
- mounted = false; tui = undefined; ui = undefined; foreground = undefined; collapsed = false;
129
+ mounted = false; tui = undefined; ui = undefined; foreground = undefined; collapsed = false; expanded = false; offset = 0;
103
130
  });
104
131
  pi.on("tool_execution_end", (event, ctx) => {
105
132
  if (event.toolName === "todo" && event.isError !== true && foregroundEvent(ctx)) refresh();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osdy-pi",
3
- "version": "1.6.1",
3
+ "version": "1.8.0",
4
4
  "description": "Osdy Pi package: themes, header, and custom editor for Pi.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -41,6 +41,10 @@
41
41
  "./themes/osdy-pi-kanagawa-dragon.json",
42
42
  "./themes/osdy-pi-kanagawa-lotus.json",
43
43
  "./themes/osdy-pi-dracula.json",
44
+ "./themes/osdy-pi-gruvbox-dark.json",
45
+ "./themes/osdy-pi-nord.json",
46
+ "./themes/osdy-pi-rose-pine.json",
47
+ "./themes/osdy-pi-daniela-cute.json",
44
48
  "./themes/osdy-pi-catppuccin-latte.json",
45
49
  "./themes/osdy-pi-catppuccin-frappe.json",
46
50
  "./themes/osdy-pi-catppuccin-macchiato.json",
@@ -65,8 +69,8 @@
65
69
  }
66
70
  },
67
71
  "devDependencies": {
68
- "@earendil-works/pi-coding-agent": "^0.84.4",
69
- "@earendil-works/pi-tui": "^0.84.4",
72
+ "@earendil-works/pi-coding-agent": "0.99.1",
73
+ "@earendil-works/pi-tui": "0.99.1",
70
74
  "@eslint/js": "^9.39.1",
71
75
  "@types/node": "^25.8.0",
72
76
  "eslint": "^9.39.1",
@@ -0,0 +1,85 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
+ "name": "osdy-pi-daniela-cute",
4
+ "appearance": "dark",
5
+ "vars": {
6
+ "navy": "#0B1220",
7
+ "card": "#111F33",
8
+ "border": "#253A55",
9
+ "cobalt": "#3584E4",
10
+ "selection": "#193655",
11
+ "text": "#D8E2EF",
12
+ "muted": "#93A6BE",
13
+ "dim": "#7895BA",
14
+ "green": "#32D583",
15
+ "red": "#FF5364",
16
+ "yellow": "#FFC247",
17
+ "cyan": "#22D3EE",
18
+ "violet": "#A970FF",
19
+ "orange": "#FF963C",
20
+ "pink": "#F54DB8"
21
+ },
22
+ "colors": {
23
+ "accent": "cobalt",
24
+ "border": "border",
25
+ "borderAccent": "cobalt",
26
+ "borderMuted": "border",
27
+ "success": "green",
28
+ "error": "red",
29
+ "warning": "yellow",
30
+ "muted": "muted",
31
+ "dim": "dim",
32
+ "text": "text",
33
+ "thinkingText": "muted",
34
+ "selectedBg": "selection",
35
+ "scrollbarTrack": "card",
36
+ "scrollbarThumb": "cobalt",
37
+ "searchMatchBg": "yellow",
38
+ "searchMatchText": "navy",
39
+ "userMessageBg": "card",
40
+ "userMessageText": "text",
41
+ "customMessageBg": "card",
42
+ "customMessageText": "text",
43
+ "customMessageLabel": "cyan",
44
+ "toolPendingBg": "navy",
45
+ "toolSuccessBg": "navy",
46
+ "toolErrorBg": "navy",
47
+ "toolTitle": "cyan",
48
+ "toolOutput": "text",
49
+ "mdHeading": "cyan",
50
+ "mdLink": "cyan",
51
+ "mdLinkUrl": "muted",
52
+ "mdCode": "green",
53
+ "mdCodeBlock": "text",
54
+ "mdCodeBlockBorder": "border",
55
+ "mdQuote": "muted",
56
+ "mdQuoteBorder": "cobalt",
57
+ "mdHr": "border",
58
+ "mdListBullet": "orange",
59
+ "toolDiffAdded": "green",
60
+ "toolDiffRemoved": "red",
61
+ "toolDiffContext": "muted",
62
+ "syntaxComment": "dim",
63
+ "syntaxKeyword": "violet",
64
+ "syntaxFunction": "cyan",
65
+ "syntaxVariable": "text",
66
+ "syntaxString": "green",
67
+ "syntaxNumber": "orange",
68
+ "syntaxType": "yellow",
69
+ "syntaxOperator": "pink",
70
+ "syntaxPunctuation": "muted",
71
+ "thinkingOff": "dim",
72
+ "thinkingMinimal": "cobalt",
73
+ "thinkingLow": "cyan",
74
+ "thinkingMedium": "green",
75
+ "thinkingHigh": "violet",
76
+ "thinkingXhigh": "orange",
77
+ "thinkingMax": "pink",
78
+ "bashMode": "green"
79
+ },
80
+ "export": {
81
+ "pageBg": "navy",
82
+ "cardBg": "card",
83
+ "infoBg": "navy"
84
+ }
85
+ }
@@ -0,0 +1,88 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
+ "name": "osdy-pi-gruvbox-dark",
4
+ "appearance": "dark",
5
+ "vars": {
6
+ "bg0": "#282828",
7
+ "bg1": "#3c3836",
8
+ "bg2": "#504945",
9
+ "bg3": "#665c54",
10
+ "bg4": "#7c6f64",
11
+ "gray": "#928374",
12
+ "fg0": "#fbf1c7",
13
+ "fg1": "#ebdbb2",
14
+ "fg2": "#d5c4a1",
15
+ "fg3": "#bdae93",
16
+ "fg4": "#a89984",
17
+ "red": "#fb4934",
18
+ "green": "#b8bb26",
19
+ "yellow": "#fabd2f",
20
+ "blue": "#83a598",
21
+ "purple": "#d3869b",
22
+ "aqua": "#8ec07c",
23
+ "orange": "#fe8019"
24
+ },
25
+ "colors": {
26
+ "accent": "yellow",
27
+ "border": "gray",
28
+ "borderAccent": "orange",
29
+ "borderMuted": "bg2",
30
+ "success": "green",
31
+ "error": "red",
32
+ "warning": "yellow",
33
+ "muted": "fg4",
34
+ "dim": "gray",
35
+ "text": "fg1",
36
+ "thinkingText": "fg4",
37
+ "selectedBg": "bg2",
38
+ "scrollbarTrack": "bg1",
39
+ "scrollbarThumb": "gray",
40
+ "searchMatchBg": "yellow",
41
+ "searchMatchText": "bg0",
42
+ "userMessageBg": "bg1",
43
+ "userMessageText": "fg1",
44
+ "customMessageBg": "bg1",
45
+ "customMessageText": "fg1",
46
+ "customMessageLabel": "aqua",
47
+ "toolPendingBg": "bg0",
48
+ "toolSuccessBg": "bg1",
49
+ "toolErrorBg": "bg1",
50
+ "toolTitle": "yellow",
51
+ "toolOutput": "fg1",
52
+ "mdHeading": "yellow",
53
+ "mdLink": "blue",
54
+ "mdLinkUrl": "fg4",
55
+ "mdCode": "aqua",
56
+ "mdCodeBlock": "fg1",
57
+ "mdCodeBlockBorder": "gray",
58
+ "mdQuote": "fg3",
59
+ "mdQuoteBorder": "aqua",
60
+ "mdHr": "bg3",
61
+ "mdListBullet": "orange",
62
+ "toolDiffAdded": "green",
63
+ "toolDiffRemoved": "red",
64
+ "toolDiffContext": "fg3",
65
+ "syntaxComment": "gray",
66
+ "syntaxKeyword": "red",
67
+ "syntaxFunction": "green",
68
+ "syntaxVariable": "fg1",
69
+ "syntaxString": "green",
70
+ "syntaxNumber": "purple",
71
+ "syntaxType": "yellow",
72
+ "syntaxOperator": "orange",
73
+ "syntaxPunctuation": "fg1",
74
+ "thinkingOff": "gray",
75
+ "thinkingMinimal": "blue",
76
+ "thinkingLow": "aqua",
77
+ "thinkingMedium": "yellow",
78
+ "thinkingHigh": "orange",
79
+ "thinkingXhigh": "red",
80
+ "thinkingMax": "purple",
81
+ "bashMode": "green"
82
+ },
83
+ "export": {
84
+ "pageBg": "bg0",
85
+ "cardBg": "bg1",
86
+ "infoBg": "bg2"
87
+ }
88
+ }
@@ -0,0 +1,86 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
+ "name": "osdy-pi-nord",
4
+ "appearance": "dark",
5
+ "vars": {
6
+ "nord0": "#2e3440",
7
+ "nord1": "#3b4252",
8
+ "nord2": "#434c5e",
9
+ "nord3": "#4c566a",
10
+ "nord4": "#d8dee9",
11
+ "nord5": "#e5e9f0",
12
+ "nord6": "#eceff4",
13
+ "nord7": "#8fbcbb",
14
+ "nord8": "#88c0d0",
15
+ "nord9": "#81a1c1",
16
+ "nord10": "#5e81ac",
17
+ "nord11": "#bf616a",
18
+ "nord12": "#d08770",
19
+ "nord13": "#ebcb8b",
20
+ "nord14": "#a3be8c",
21
+ "nord15": "#b48ead"
22
+ },
23
+ "colors": {
24
+ "accent": "nord8",
25
+ "border": "nord3",
26
+ "borderAccent": "nord9",
27
+ "borderMuted": "nord2",
28
+ "success": "nord14",
29
+ "error": "nord11",
30
+ "warning": "nord13",
31
+ "muted": "nord9",
32
+ "dim": "nord3",
33
+ "text": "nord4",
34
+ "thinkingText": "nord9",
35
+ "selectedBg": "nord2",
36
+ "scrollbarTrack": "nord1",
37
+ "scrollbarThumb": "nord9",
38
+ "searchMatchBg": "nord13",
39
+ "searchMatchText": "nord0",
40
+ "userMessageBg": "nord1",
41
+ "userMessageText": "nord6",
42
+ "customMessageBg": "nord1",
43
+ "customMessageText": "nord4",
44
+ "customMessageLabel": "nord7",
45
+ "toolPendingBg": "nord0",
46
+ "toolSuccessBg": "nord1",
47
+ "toolErrorBg": "nord1",
48
+ "toolTitle": "nord8",
49
+ "toolOutput": "nord4",
50
+ "mdHeading": "nord8",
51
+ "mdLink": "nord8",
52
+ "mdLinkUrl": "nord9",
53
+ "mdCode": "nord14",
54
+ "mdCodeBlock": "nord4",
55
+ "mdCodeBlockBorder": "nord3",
56
+ "mdQuote": "nord9",
57
+ "mdQuoteBorder": "nord7",
58
+ "mdHr": "nord3",
59
+ "mdListBullet": "nord9",
60
+ "toolDiffAdded": "nord14",
61
+ "toolDiffRemoved": "nord11",
62
+ "toolDiffContext": "nord4",
63
+ "syntaxComment": "nord3",
64
+ "syntaxKeyword": "nord9",
65
+ "syntaxFunction": "nord8",
66
+ "syntaxVariable": "nord4",
67
+ "syntaxString": "nord14",
68
+ "syntaxNumber": "nord15",
69
+ "syntaxType": "nord7",
70
+ "syntaxOperator": "nord9",
71
+ "syntaxPunctuation": "nord6",
72
+ "thinkingOff": "nord3",
73
+ "thinkingMinimal": "nord10",
74
+ "thinkingLow": "nord7",
75
+ "thinkingMedium": "nord13",
76
+ "thinkingHigh": "nord12",
77
+ "thinkingXhigh": "nord11",
78
+ "thinkingMax": "nord15",
79
+ "bashMode": "nord14"
80
+ },
81
+ "export": {
82
+ "pageBg": "nord0",
83
+ "cardBg": "nord1",
84
+ "infoBg": "nord2"
85
+ }
86
+ }
@@ -0,0 +1,85 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
+ "name": "osdy-pi-rose-pine",
4
+ "appearance": "dark",
5
+ "vars": {
6
+ "base": "#191724",
7
+ "surface": "#1f1d2e",
8
+ "overlay": "#26233a",
9
+ "muted": "#6e6a86",
10
+ "subtle": "#908caa",
11
+ "text": "#e0def4",
12
+ "love": "#eb6f92",
13
+ "gold": "#f6c177",
14
+ "rose": "#ebbcba",
15
+ "pine": "#31748f",
16
+ "foam": "#9ccfd8",
17
+ "iris": "#c4a7e7",
18
+ "highlightLow": "#21202e",
19
+ "highlightMed": "#403d52",
20
+ "highlightHigh": "#524f67"
21
+ },
22
+ "colors": {
23
+ "accent": "iris",
24
+ "border": "subtle",
25
+ "borderAccent": "rose",
26
+ "borderMuted": "highlightMed",
27
+ "success": "foam",
28
+ "error": "love",
29
+ "warning": "gold",
30
+ "muted": "subtle",
31
+ "dim": "muted",
32
+ "text": "text",
33
+ "thinkingText": "subtle",
34
+ "selectedBg": "highlightMed",
35
+ "scrollbarTrack": "highlightLow",
36
+ "scrollbarThumb": "highlightHigh",
37
+ "searchMatchBg": "gold",
38
+ "searchMatchText": "base",
39
+ "userMessageBg": "highlightMed",
40
+ "userMessageText": "text",
41
+ "customMessageBg": "surface",
42
+ "customMessageText": "text",
43
+ "customMessageLabel": "foam",
44
+ "toolPendingBg": "surface",
45
+ "toolSuccessBg": "overlay",
46
+ "toolErrorBg": "overlay",
47
+ "toolTitle": "iris",
48
+ "toolOutput": "text",
49
+ "mdHeading": "iris",
50
+ "mdLink": "foam",
51
+ "mdLinkUrl": "subtle",
52
+ "mdCode": "rose",
53
+ "mdCodeBlock": "text",
54
+ "mdCodeBlockBorder": "muted",
55
+ "mdQuote": "subtle",
56
+ "mdQuoteBorder": "pine",
57
+ "mdHr": "highlightMed",
58
+ "mdListBullet": "love",
59
+ "toolDiffAdded": "foam",
60
+ "toolDiffRemoved": "love",
61
+ "toolDiffContext": "subtle",
62
+ "syntaxComment": "muted",
63
+ "syntaxKeyword": "pine",
64
+ "syntaxFunction": "rose",
65
+ "syntaxVariable": "text",
66
+ "syntaxString": "gold",
67
+ "syntaxNumber": "iris",
68
+ "syntaxType": "foam",
69
+ "syntaxOperator": "subtle",
70
+ "syntaxPunctuation": "subtle",
71
+ "thinkingOff": "muted",
72
+ "thinkingMinimal": "pine",
73
+ "thinkingLow": "foam",
74
+ "thinkingMedium": "gold",
75
+ "thinkingHigh": "rose",
76
+ "thinkingXhigh": "love",
77
+ "thinkingMax": "iris",
78
+ "bashMode": "foam"
79
+ },
80
+ "export": {
81
+ "pageBg": "base",
82
+ "cardBg": "surface",
83
+ "infoBg": "overlay"
84
+ }
85
+ }