osdy-pi 1.11.8 → 1.11.10

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
@@ -63,7 +63,7 @@ Run **`/osdyConfig`** in interactive Pi to open the Osdy Control Center. The ten
63
63
  - TODO shows configured selection, current eligibility, and actually loaded SDK registration separately. Opt-in/out requires an idle session and a Cancel-first inline confirmation naming the target, owned Gentle filter effects, and reload. Successful selection closes/disposes the panel and releases its editor hold before reloading once; restart Pi if reload fails. Rejections stay in the panel. No installs or task-history changes occur; legacy `/osdy-pi todo on|off|status` remains available.
64
64
  - Agents shows concise **Joker / Gentle** choices without activating anything. With detail focus, visible lowercase-only **j Joker / g Gentle** letters open the existing confirmation directly; only available, unambiguous rows get shortcuts. **↑/↓ + Enter** remains the fallback. Profile and Agent selections survive category navigation; unavailable choices fall back to the current row. Letters never bypass category focus, confirmation, path input, loading, querying, or saving. Selection is idle-only and Cancel-first confirmed, naming the real target, possible Joker installation, owned agent filters, and preserved unrelated resources. Normal personal Pi only: `pi:dev` and isolated/override sessions are blocked, never redirected to personal settings. Success closes/disposes the panel and releases its editor hold before reloading once; restart Pi if reload fails. Legacy `/osdy-pi agents setup|on|off|status` is unchanged.
65
65
  - Theme lists Pi's available themes and marks the current theme. Explicit **Page x/N** pages contain at most **8 themes**, fewer in short viewports or with expanded help. **Page Up/Down** changes pages, **Home/End** reaches the first/last theme, and arrows cross page boundaries. Selection survives category navigation and resizing; resizing adjusts the page around the selected theme. A successful selection applies live and saves globally in the active Pi agent directory; project settings can override it at startup. Failures appear in the modal.
66
- - Header offers **osdy-theme / neon**; Mascot offers **current / Bts**; Editor offers **auto / extended / simple**, with the current preference marked. Editor also shows the effective mode: small terminals stay simple/native even with extended selected.
66
+ - Header offers **osdy-theme / neon**; Mascot offers **current / Bts / Osdy-Halloween**; Editor offers **auto / extended / simple**, with the current preference marked. Editor also shows the effective mode: small terminals stay simple/native even with extended selected.
67
67
  - Preferences save immediately through the same runtime and global Osdy settings store as `/osdy-pi` commands (`<agent-dir>/extensions/osdy-pi/settings.json`). Header/Mascot apply live without closing the panel. Editor replacement waits until all overlays close; if another overlay remains, the next existing responsive watcher tick applies the latest choice. The effective mode reports the actually mounted editor while queued. Opening a category only reads values; while Osdy is disabled, choices save for later without enabling it.
68
68
  - A selection shows **Saving** until persistence finishes and **Saved globally** only on success. Failed saves retain the live preference but report **could not be saved**; select again to retry. Application errors report **Not saved**, without claiming the runtime change succeeded. Duplicate selections are ignored while saving.
69
69
  - **Git** shows the current branch (or detached HEAD), changed-entry count/clean status, and the current widget position as a read-only session detail. Working-tree enablement applies and saves through the same owner as `/osdy-pi working-tree`; Git inspection never writes Git. `/osdy-pi diff` and position commands are unchanged.
@@ -300,7 +300,7 @@ Before rename or removal, close this Pi process when it uses the target and **ma
300
300
  | Area | Included behavior |
301
301
  | --- | --- |
302
302
  | Themes | 23 built-in themes, including Osdy, Kanagawa, Dracula, Gruvbox, Nord, Rosé Pine, Daniela Cute, Halloween, Halloween Killer, Catppuccin, Matrix, Lucent Orange, and three Spider-Man-inspired palettes |
303
- | Header and mascot | Independently selectable `osdy-theme`/`neon` headers and `current`/`bts` mascots |
303
+ | Header and mascot | Independently selectable `osdy-theme`/`neon` headers and `current`/`bts`/`osdy-halloween` mascots |
304
304
  | Messages | Native colored user cards without emojis and plain assistant Markdown with a separate assistant `🦝` row |
305
305
  | Input | Responsive auto editor by default, with selectable simple Pi-native or extended framed modes |
306
306
  | Status | Six activity labels with compact per-state glyphs and a shared theme-color pulse, responsive footer metrics, dynamic extension statuses, and Codex subscription quota with low-capacity emphasis |
@@ -366,10 +366,12 @@ Header and mascot choices are independent:
366
366
  | Element | Choices | Command |
367
367
  | --- | --- | --- |
368
368
  | Header | `osdy-theme` (default), `neon` | `/osdy-pi header osdy-theme|neon` |
369
- | Mascot | `current` (default), `bts` | `/osdy-pi mascot current|bts` |
369
+ | Mascot | `current` (default), `bts`, `osdy-halloween` | `/osdy-pi mascot current\|bts\|osdy-halloween` |
370
370
 
371
371
  Both commands update the UI immediately. Add `status` instead of a choice to inspect the current selection. Choices persist in the global Osdy Pi settings across reloads and sessions. The old top-level style commands and `classic` header aliases are no longer registered.
372
372
 
373
+ Choose **Osdy-Halloween** in `/osdyConfig` → **Mascot**, or run `/osdy-pi mascot osdy-halloween`. It dresses the same Current raccoon in ivory fangs and a high-collared dark/crimson vampire cape. Its glow follows the active theme's `accent`, `mdHeading`, and `mdLink` colors; no Halloween theme is required. Current remains the default, and Current/Bts artwork and version-1 settings compatibility are unchanged.
374
+
373
375
  The header also shows **`osdy-pi v<version>`** at the top right in the theme's accent color, read from the installed Osdy package—not Pi's host version or the workspace manifest. It uses an empty first row or adds a row without overwriting artwork; invalid/unreadable versions or labels wider than the terminal are omitted.
374
376
 
375
377
  In normal mode, the selected header and mascot render side by side when both fit. The Neon header derives its highlights from the active theme rather than using one fixed palette. Each mascot keeps its own tone map while its animated edge glow can resolve through theme colors. Header and mascot scaling remain independent, so any combination follows the same responsive layout rules.
@@ -407,7 +409,7 @@ The enabled state, editor mode, working-tree visibility preference, header, and
407
409
  | TODO selection | `/osdy-pi todo on\|off\|status` |
408
410
  | Session TODO (opt-in, starting in 1.5.0) | `/todos` (read-only) |
409
411
  | Header | `/osdy-pi header osdy-theme\|neon\|status` |
410
- | Mascot | `/osdy-pi mascot current\|bts\|status` |
412
+ | Mascot | `/osdy-pi mascot current\|bts\|osdy-halloween\|status` |
411
413
  | Editor | `/osdy-pi editor auto\|extended\|simple\|on\|off\|toggle\|status` |
412
414
  | Working tree | `/osdy-pi working-tree on\|off\|toggle\|status` |
413
415
  | Working tree | `/osdy-pi working-tree position top\|bottom\|status` |
@@ -492,14 +494,14 @@ While work is active, Osdy Pi centers an activity label and compact animated gly
492
494
 
493
495
  | Source | Example | Meaning |
494
496
  | --- | --- | --- |
495
- | Validated public `agent` argument | `explore · Working...` | Generic fallback, not measured progress or a claimed editing operation. This provider-neutral fallback works with Gentle Agents, which currently emits no live child progress. |
496
- | Public foreground progress from Joker (`pi-subagents-j0k3r`) | `scout · Exploring...` | Named `read` activity selects both the Exploring label and its existing glyph frames, overriding fallback. |
497
+ | Validated public `agent` argument | `Working...` | Generic fallback, not measured progress or a claimed editing operation. This provider-neutral fallback works with Gentle Agents, which currently emits no live child progress. |
498
+ | Public foreground progress from Joker (`pi-subagents-j0k3r`) | `Exploring...` | Named `read` activity selects both the Exploring label and its existing glyph frames, overriding fallback. |
497
499
 
498
- Live child activity uses the same six states and corresponding animated glyphs listed above, retaining the agent prefix: `scout · Thinking...`, `Exploring...`, `Verifying...`, `Working...`, `Delegating...`, or `Executing...`. Both public thinking and response streaming select Thinking; model labels, reasoning and response text are never read. Named reads/searches/direct documentation tools select Exploring, `lens_diagnostics` selects Verifying, edits/writes select Working, and `subagent_run` selects Delegating. Only named checks qualify: public tool metadata lacks arguments, so `bash`, wrappers needing selectors, and unknown tool names select Executing. Tool completion/failure events classify the reported operation while the child is running; they do not claim whole-task completion.
500
+ Live child activity uses the same six states and corresponding animated glyphs listed above, without agent names: `Thinking...`, `Exploring...`, `Verifying...`, `Working...`, `Delegating...`, or `Executing...`. Both public thinking and response streaming select Thinking; model labels, reasoning and response text are never read. Named reads/searches/direct documentation tools select Exploring, `lens_diagnostics` selects Verifying, edits/writes select Working, and `subagent_run` selects Delegating. Only named checks qualify: public tool metadata lacks arguments, so `bash`, wrappers needing selectors, and unknown tool names select Executing. Tool completion/failure events classify the reported operation while the child is running; they do not claim whole-task completion.
499
501
 
500
- The fallback is `<agent> · Working...` **with the Working glyph**, regardless of optional `label` metadata. Missing or empty tool names supply no usable operation; named unknown tools instead select Executing. A continuation with an absent agent uses `Subagent`, never an inferred identity; missing/invalid run identity or explicitly invalid continuation identity fails closed to ordinary parent activity unless a live snapshot supplies its own validated public agent. Full tasks, prompts, context, content, trails and transcripts are never read or displayed.
502
+ The fallback is `Working...` **with the Working glyph**, regardless of optional `label` metadata. Missing or empty tool names supply no usable operation; named unknown tools instead select Executing. A continuation with an absent agent validates the generic `Subagent` identity without displaying it, never inferring an identity; missing/invalid run identity or explicitly invalid continuation identity fails closed to ordinary parent activity unless a live snapshot supplies its own validated public agent. Full tasks, prompts, context, content, trails and transcripts are never read or displayed.
501
503
 
502
- For multiple running children, the indicator uses the first child in snapshot order with a compact additional-child count. Missing, malformed, terminal or no-running-child progress restores both the agent's generic fallback text and Working glyph, or ordinary parent activity when validated agent identity is unavailable. Labels are bounded, control-safe and clipped to terminal width. Matching tool end, agent/session end and reload cleanup discard per-call state.
504
+ For multiple running children, the indicator uses the first child in snapshot order with a compact additional-child count (`(+N)`). Missing, malformed, terminal or no-running-child progress restores both the generic `Working...` fallback text and Working glyph, or ordinary parent activity when validated agent identity is unavailable. Labels are bounded, control-safe and clipped to terminal width. Matching tool end, agent/session end and reload cleanup discard per-call state.
503
505
 
504
506
  **Background limitation:** agent fallbacks appear only while their parent call is active. There is no persistent background live feed, polling or task cache.
505
507
 
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
2
2
  import { registerHooks } from "node:module";
3
3
  import test from "node:test";
4
4
  // @ts-expect-error Node's native TypeScript runner resolves test-only TypeScript source imports.
5
- import { HEADER_VARIANTS } from "./constants.ts";
5
+ import { HEADER_VARIANTS, mascotForChoice } from "./constants.ts";
6
6
  import type { SimpleTheme } from "./types.js";
7
7
 
8
8
  registerHooks({
@@ -19,7 +19,7 @@ registerHooks({
19
19
  });
20
20
 
21
21
  // @ts-expect-error Node's native TypeScript runner resolves test-only TypeScript source imports.
22
- const { colorAsciiCharacter } = await import("./animation.ts");
22
+ const { colorAsciiCharacter, animateAsciiLineWithToneMap } = await import("./animation.ts");
23
23
 
24
24
  class RecordingTheme implements SimpleTheme {
25
25
  readonly calls: Array<{ name: string; text: string }> = [];
@@ -74,6 +74,33 @@ void test("Neon renders main and dark pink through bounded accent styling", () =
74
74
  assert.equal(`${main}${dark.join("")}`.includes("\u001B[48;"), false);
75
75
  });
76
76
 
77
+ void test("Osdy-Halloween resolves edge glow against the live theme in static and animated frames", () => {
78
+ const { art, tonePalette } = mascotForChoice("osdy-halloween");
79
+ assert.deepEqual([tonePalette.p, tonePalette.c, tonePalette.v], ["accent", "mdHeading", "mdLink"]);
80
+ let themeId = "first";
81
+ const calls: string[] = [];
82
+ const theme: SimpleTheme = { fg(name, text) { calls.push(name); return `<${themeId}:${name}:${text}>`; } };
83
+ for (const style of ["static", "animated"] as const) {
84
+ for (const frame of [0, 7, 28]) {
85
+ const render = () => art.mascot.map((line, row) =>
86
+ animateAsciiLineWithToneMap(line, art.toneMap[row] ?? "", row, frame, theme, tonePalette, style)).join("\n");
87
+ themeId = "first";
88
+ const first = render();
89
+ themeId = "second";
90
+ const second = render();
91
+ assert.notEqual(first, second);
92
+ assert.equal(first.replaceAll("first:", "second:"), second, "only theme-resolved colors change");
93
+ assert.ok(second.includes("\u001B[38;2;"), "fixed body/costume foreground colors remain");
94
+ assert.ok(!second.includes("\u001B[48;"), "transparent background");
95
+ }
96
+ }
97
+ assert.deepEqual([...new Set(calls)].sort(), ["accent", "mdHeading", "mdLink"].sort());
98
+ for (let row = 0; row < art.mascot.length; row++) {
99
+ const occupied = Array.from(art.mascot[row] ?? "").flatMap((glyph, column) => glyph === " " ? [] : [column]);
100
+ assert.deepEqual(occupied.slice(-3).reverse().map((column) => art.toneMap[row]?.[column]), ["p", "c", "v"].slice(0, occupied.length));
101
+ }
102
+ });
103
+
77
104
  void test("Neon never maps a tone to mdHeading or mdLink", () => {
78
105
  for (const color of Object.values(neonPalette())) {
79
106
  if (typeof color === "string") {
@@ -457,6 +457,52 @@ function addRightEdgeGlow(art: MascotArt): MascotArt {
457
457
  };
458
458
  }
459
459
 
460
+ // Transparent overlay at Current's mouth/shoulders (row 17 onward).
461
+ // ▓ = dark cape, ▒ = crimson lining, █/▼ = ivory fangs. Keep the face and tail.
462
+ const VAMPIRE_COSTUME = [
463
+ " ▓ ▓",
464
+ " ▓▒▓ █ █ ▓▒▒▓",
465
+ " ▓▒▒▒▓ ▼ ▼ ▓▒▒▒▒▓",
466
+ " ▓▒▒▒▒▒▒▓ ▓▒▒▒▒▒▒▓",
467
+ " ▓▓▒▒▒▒▒▓▓ ▓▓▒▒▒▒▒▓▓",
468
+ " ▓▓▓▒▒▒▓▓ ▓▓▒▒▒▓▓▓",
469
+ " ▓▓▓▒▒▒▓▓ ▓▓▒▒▓▓▓",
470
+ " ▓▓▓▒▒▒▓▓ ▓▓▒▒▒▓▓",
471
+ " ▓▓▓▒▒▒▓▓ ▓▓▒▒▓▓",
472
+ " ▓▓▓▒▒▒▓▓ ▓▓▒▒▓▓",
473
+ " ▓▓▓▒▒▒▓▓ ▓▓▒▒▓▓",
474
+ " ▓▓▓▒▒▒▓▓ ▓▓▒▒▓▓",
475
+ " ▓▓▓▒▒▒▓▓ ▓▓▒▒▓▓",
476
+ " ▓▓▓▓▒▒▓▓ ▓▓▒▒▓▓",
477
+ " ▓▓▓▓▒▒▓▓ ▓▓▒▓▓",
478
+ " ▓▓▓▓▓▓ ▓▓▓",
479
+ ] as const;
480
+
481
+ function addVampireCostume(art: MascotArt): MascotArt {
482
+ const rows = art.mascot.map((line, row) => {
483
+ const glyphs = Array.from(line);
484
+ // Reserve b for lining; retain the few original eye glints in ivory h.
485
+ const tones = Array.from((art.toneMap[row] ?? "").replaceAll("b", "h"));
486
+ for (const [column, glyph] of Array.from(VAMPIRE_COSTUME[row - 17] ?? "").entries()) {
487
+ if (glyph === " ") continue;
488
+ glyphs[column] = glyph;
489
+ if (glyph === "▓") tones[column] = "d";
490
+ else if (glyph === "▒") tones[column] = "b";
491
+ else tones[column] = "h";
492
+ }
493
+ return { mascot: glyphs.join(""), toneMap: tones.join("") };
494
+ });
495
+ return { mascot: rows.map((row) => row.mascot), toneMap: rows.map((row) => row.toneMap) };
496
+ }
497
+
498
+ const HALLOWEEN_MASCOT_TONES: MascotTonePalette = {
499
+ ...HTML_MASCOT_TONES,
500
+ b: "#9F2342",
501
+ };
502
+ const HALLOWEEN_MASCOT = addRightEdgeGlow(addVampireCostume({
503
+ mascot: HTML_MASCOT,
504
+ toneMap: HTML_MASCOT_MAP,
505
+ }));
460
506
  const ROSE_MASCOT = addRightEdgeGlow({
461
507
  mascot: HTML_MASCOT,
462
508
  toneMap: HTML_MASCOT_MAP,
@@ -468,6 +514,7 @@ const BTS_MASCOT_ART = addRightEdgeGlow({
468
514
  for (const [art, palette] of [
469
515
  [ROSE_MASCOT, HTML_MASCOT_TONES],
470
516
  [BTS_MASCOT_ART, BTS_MASCOT_TONES],
517
+ [HALLOWEEN_MASCOT, HALLOWEEN_MASCOT_TONES],
471
518
  ] as const) {
472
519
  assertRawHexPalette(palette);
473
520
  assertMascotStructure(art, palette);
@@ -481,6 +528,7 @@ export type MascotConfig = {
481
528
  export const MASCOTS: Record<MascotChoice, MascotConfig> = {
482
529
  current: { art: ROSE_MASCOT, tonePalette: HTML_MASCOT_TONES },
483
530
  bts: { art: BTS_MASCOT_ART, tonePalette: BTS_MASCOT_TONES },
531
+ "osdy-halloween": { art: HALLOWEEN_MASCOT, tonePalette: HALLOWEEN_MASCOT_TONES },
484
532
  };
485
533
 
486
534
  export function mascotForChoice(choice: MascotChoice): MascotConfig {
@@ -21,7 +21,7 @@ const snapshot: VisualPreferenceSnapshot = { enabled: true, headerVariant: "osdy
21
21
  void test("preference rows expose every catalog value and exactly one current indicator", () => {
22
22
  for (const [category, field, values] of [
23
23
  ["Header", "headerVariant", ["osdy-theme", "neon"]],
24
- ["Mascot", "mascot", ["current", "bts"]],
24
+ ["Mascot", "mascot", ["current", "bts", "osdy-halloween"]],
25
25
  ["Editor", "editorMode", ["auto", "extended", "simple"]],
26
26
  ] as const) {
27
27
  for (const value of values) {
@@ -30,7 +30,9 @@ void test("preference rows expose every catalog value and exactly one current in
30
30
  assert.deepEqual(detail.rows.map((row) => "value" in row.action ? row.action.value : undefined), values);
31
31
  assert.equal(detail.rows.filter((row) => row.current).length, 1);
32
32
  assert.equal(detail.rows.find((row) => row.current)?.action.kind, category.toLowerCase());
33
- assert.match(detail.summary, new RegExp(`Current: ${value === "bts" ? "Bts" : value}`));
33
+ const label = value === "bts" ? "Bts" : value === "osdy-halloween" ? "Osdy-Halloween" : value;
34
+ assert.match(detail.summary, new RegExp(`Current: ${label}`));
35
+ assert.equal(detail.rows.find((row) => row.current)?.label, label);
34
36
  }
35
37
  }
36
38
  });
@@ -17,7 +17,11 @@ export interface ControlCenterPreferences {
17
17
  }
18
18
 
19
19
  export function visualPreferenceLabel(action: VisualPreferenceAction): string {
20
- return action.kind === "mascot" && action.value === "bts" ? "Bts" : action.value;
20
+ if (action.kind === "mascot") {
21
+ if (action.value === "bts") return "Bts";
22
+ if (action.value === "osdy-halloween") return "Osdy-Halloween";
23
+ }
24
+ return action.value;
21
25
  }
22
26
 
23
27
  /** Explicit view adaptation only; application and persistence belong to runtime. */
@@ -336,7 +336,7 @@ void test("inline preferences show all supported values and live current/effecti
336
336
  const f = preferenceFixture();
337
337
  for (const [category, choices, current] of [
338
338
  ["Header", ["osdy-theme", "neon"], "osdy-theme"],
339
- ["Mascot", ["current", "Bts"], "current"],
339
+ ["Mascot", ["current", "Bts", "Osdy-Halloween"], "current"],
340
340
  ["Editor", ["auto", "extended", "simple"], "auto"],
341
341
  ] as const) {
342
342
  f.panel.handleInput(down);
@@ -350,6 +350,20 @@ void test("inline preferences show all supported values and live current/effecti
350
350
  assert.deepEqual(f.applied, []);
351
351
  });
352
352
 
353
+ void test("Mascot selects Osdy-Halloween and reports its exact saved label", async () => {
354
+ const f = preferenceFixture();
355
+ f.panel.handleInput(down);
356
+ f.panel.handleInput(down);
357
+ f.panel.handleInput(right);
358
+ f.panel.handleInput("\x1b[F");
359
+ f.panel.handleInput("\r");
360
+ assert.deepEqual(f.applied, [{ kind: "mascot", value: "osdy-halloween" }]);
361
+ assert.match(f.panel.render(100).join("\n"), /Saving.*Osdy-Halloween/);
362
+ f.complete(true);
363
+ await Promise.resolve();
364
+ assert.match(f.panel.render(100).join("\n"), /Saved globally: Osdy-Halloween/);
365
+ });
366
+
353
367
  void test("changing categories resets selection to their current row, even after a longer theme list", () => {
354
368
  const f = preferenceFixture();
355
369
  f.panel.handleInput(right);
@@ -427,7 +441,7 @@ void test("inline preference rendering stays bounded at narrow widths and differ
427
441
  f.panel.handleInput(down);
428
442
  f.panel.handleInput(right);
429
443
  f.panel.handleInput("\x1b[F");
430
- assert.match(f.panel.render(48).join("\n"), new RegExp(`> ${["neon", "Bts", "simple"][category]}`));
444
+ assert.match(f.panel.render(48).join("\n"), new RegExp(`> ${["neon", "Osdy-Halloween", "simple"][category]}`));
431
445
  for (const height of [1, 4, 7, 9, 18]) {
432
446
  f.resize(height);
433
447
  for (const width of [1, 8, 24, 48, 80]) {
@@ -97,7 +97,7 @@ void test("editor settings normalize only supported versioned modes", () => {
97
97
  });
98
98
 
99
99
  void test("mascot choices preserve the version-one current default, migrate raccoon to Bts, and persist Bts", async () => {
100
- assert.deepEqual(MASCOT_CHOICES, ["current", "bts"]);
100
+ assert.deepEqual(MASCOT_CHOICES, ["current", "bts", "osdy-halloween"]);
101
101
  assert.equal(
102
102
  normalizeEditorSettings({ version: 1, mascot: "invalid" }).mascot,
103
103
  "current",
@@ -129,6 +129,18 @@ void test("mascot choices preserve the version-one current default, migrate racc
129
129
  assert.equal((await store.load()).mascot, "bts");
130
130
  });
131
131
 
132
+ void test("Osdy-Halloween round-trips through a fresh version-one settings store", async () => {
133
+ const fileSystem = new MemoryEditorSettingsFileSystem();
134
+ const path = "/agent/extensions/osdy-pi/settings.json";
135
+ const settings = normalizeEditorSettings({ version: 1, mascot: "osdy-halloween" });
136
+ assert.equal(settings.mascot, "osdy-halloween");
137
+ await createEditorSettingsStore(path, fileSystem).save(settings);
138
+ const persisted: unknown = JSON.parse(fileSystem.content ?? "{}");
139
+ assert.deepEqual(persisted, { ...settings, version: 1 });
140
+ assert.deepEqual(await createEditorSettingsStore(path, fileSystem).load(), settings);
141
+ assert.equal(normalizeEditorSettings({ version: 2, mascot: "osdy-halloween" }).mascot, "current");
142
+ });
143
+
132
144
  void test("enabled defaults safely and persists globally", async () => {
133
145
  assert.equal(
134
146
  normalizeEditorSettings({ version: 1, editorMode: "simple" }).enabled,
@@ -152,6 +152,7 @@ void test("shared visual actions apply immediately, save the complete settings,
152
152
  for (const enabled of [true, false]) {
153
153
  for (const action of [
154
154
  { kind: "header", value: "neon" }, { kind: "mascot", value: "bts" },
155
+ { kind: "mascot", value: "osdy-halloween" },
155
156
  { kind: "editor", value: "simple" }, { kind: "editor", value: "extended" },
156
157
  { kind: "editor", value: "auto" },
157
158
  ] as const satisfies readonly VisualPreferenceAction[]) {
@@ -903,6 +904,7 @@ void test("registered legacy commands and osdyConfig share live values and exist
903
904
  for (const [args, expected, field, value] of [
904
905
  ["header neon", "osdy-pi header: neon", "headerVariant", "neon"],
905
906
  ["mascot bts", "osdy-pi mascot: Bts", "mascot", "bts"],
907
+ ["mascot osdy-halloween", "osdy-pi mascot: Osdy-Halloween", "mascot", "osdy-halloween"],
906
908
  ["editor off", "osdy-pi editor mode: simple", "editorMode", "simple"],
907
909
  ["editor on", "osdy-pi editor mode: extended", "editorMode", "extended"],
908
910
  ["editor toggle", "osdy-pi editor mode: simple", "editorMode", "simple"],
@@ -916,7 +918,9 @@ void test("registered legacy commands and osdyConfig share live values and exist
916
918
  assert.equal(writes.at(-1)?.workingTreeEnabled, false);
917
919
  assert.equal(notices.at(-1)?.message, "osdy-pi working tree disabled");
918
920
  const before = writes.length;
919
- for (const args of ["working-tree status", "header status", "mascot status", "editor status", "header invalid", "mascot bts extra", "editor simple extra"]) await legacy.handler(args, ctx);
921
+ await legacy.handler("mascot status", ctx);
922
+ assert.deepEqual(notices.at(-1), { message: "osdy-pi mascot: Osdy-Halloween", level: "info" });
923
+ for (const args of ["working-tree status", "header status", "mascot status", "editor status", "header invalid", "mascot bts extra", "mascot osdy-halloween extra", "mascot invalid", "editor simple extra"]) await legacy.handler(args, ctx);
920
924
  assert.equal(writes.length, before, "status and invalid inputs never save");
921
925
  const beforeModalGitCalls = gitCalls.length;
922
926
  const beforeModalNotices = notices.length;
@@ -1348,15 +1352,19 @@ void test("uninstall command is offered and wired through guarded Pi CLI removal
1348
1352
  assert.match(source, /pi\.exec\("pi", \["remove", source/);
1349
1353
  });
1350
1354
 
1351
- void test("mascot command completes current, Bts, and status while persisting with immediate refresh", () => {
1355
+ void test("mascot command completes all three choices and status while persisting with immediate refresh", () => {
1352
1356
  assert.deepEqual(getOsdyCommandCompletions("mascot"), [
1353
1357
  { value: "mascot current", label: "mascot current" },
1354
1358
  { value: "mascot bts", label: "mascot bts" },
1359
+ { value: "mascot osdy-halloween", label: "mascot osdy-halloween" },
1355
1360
  { value: "mascot status", label: "mascot status" },
1356
1361
  ]);
1357
1362
  assert.deepEqual(getOsdyCommandCompletions("mascot b"), [
1358
1363
  { value: "mascot bts", label: "mascot bts" },
1359
1364
  ]);
1365
+ assert.deepEqual(getOsdyCommandCompletions("mascot o"), [
1366
+ { value: "mascot osdy-halloween", label: "mascot osdy-halloween" },
1367
+ ]);
1360
1368
  assert.deepEqual(getOsdyCommandCompletions("mascot d"), []);
1361
1369
 
1362
1370
  const source = readFileSync(new URL("./runtime.ts", import.meta.url), "utf8");
@@ -128,6 +128,7 @@ function isMascotChoice(value: string): value is MascotChoice {
128
128
  }
129
129
 
130
130
  function mascotLabel(mascot: MascotChoice): string {
131
+ if (mascot === "osdy-halloween") return "Osdy-Halloween";
131
132
  return mascot === "bts" ? "Bts" : mascot;
132
133
  }
133
134
 
@@ -44,7 +44,7 @@ export const HEADER_VARIANT_CHOICES = ["osdy-theme", "neon"] as const;
44
44
 
45
45
  export type HeaderVariant = (typeof HEADER_VARIANT_CHOICES)[number];
46
46
 
47
- export const MASCOT_CHOICES = ["current", "bts"] as const;
47
+ export const MASCOT_CHOICES = ["current", "bts", "osdy-halloween"] as const;
48
48
 
49
49
  export type MascotChoice = (typeof MASCOT_CHOICES)[number];
50
50
 
@@ -1,4 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
+ import { createHash } from "node:crypto";
2
3
  import { existsSync, readFileSync } from "node:fs";
3
4
  import { registerHooks } from "node:module";
4
5
  import { fileURLToPath } from "node:url";
@@ -244,12 +245,57 @@ void test("selected headers render through foreground-only tone-map animation",
244
245
  void test("mascot selection wires Bts art through the responsive renderer", () => {
245
246
  const ui = readFileSync(new URL("./ui.ts", import.meta.url), "utf8");
246
247
 
247
- assert.deepEqual(Object.keys(MASCOTS), ["current", "bts"]);
248
+ assert.deepEqual(Object.keys(MASCOTS), ["current", "bts", "osdy-halloween"]);
248
249
  assert.ok(mascotForChoice("bts"));
249
250
  assert.match(ui, /const mascot = mascotForChoice\(state\.mascot\);/);
250
251
  assert.match(ui, /scaleMascot\(\s*mascot\.art,/);
251
252
  });
252
253
 
254
+ void test("Current and Bts retain their original glyphs, tone maps, and palettes", () => {
255
+ for (const [choice, fingerprint] of [
256
+ ["current", "5d399dc6d6cf7f88dce300ca224d157e23e21186b1991a4f57b65fe60c34d8ac"],
257
+ ["bts", "e2040efe00cf709b799834d5183ddae3273cfb515b8dfa0f63ae1b9c5def71ec"],
258
+ ] as const) {
259
+ assert.equal(createHash("sha256").update(JSON.stringify(MASCOTS[choice])).digest("hex"), fingerprint);
260
+ }
261
+ });
262
+
263
+ void test("Osdy-Halloween preserves Current's face and tail beneath a bounded vampire costume", () => {
264
+ const { art, tonePalette } = mascotForChoice("osdy-halloween");
265
+ const current = MASCOTS.current.art;
266
+ assert.deepEqual(art.mascot.slice(0, 17), current.mascot.slice(0, 17), "same ears, mask, eyes and snout");
267
+ for (let row = 21; row < current.mascot.length; row++) {
268
+ assert.equal(art.mascot[row]?.slice(44), current.mascot[row]?.slice(44), "original striped tail");
269
+ }
270
+ assert.notDeepEqual(art.mascot.slice(17), current.mascot.slice(17));
271
+ const fangs = art.mascot.slice(17, 20).join("\n");
272
+ assert.match(fangs, /▼[^▼\n]+▼/, "two ivory fang tips separated by the original muzzle");
273
+ assert.equal([...fangs].filter((glyph) => glyph === "▼").length, 2, "one-column fang tips");
274
+ assert.equal([...fangs].filter((glyph) => glyph === "█").length, 2, "one-column stems keep fangs two rows tall");
275
+ assert.equal(art.mascot.length, current.mascot.length);
276
+ assert.deepEqual(art.mascot.map((row) => row.length), current.mascot.map((row) => row.length));
277
+ assert.equal(art.toneMap[17]?.[8], "d", "raised collar has a dark outer edge");
278
+ assert.equal(art.toneMap[20]?.[12], "b", "collar has crimson lining");
279
+ assert.equal(tonePalette.b, "#9F2342");
280
+ assert.equal(tonePalette.d, MASCOTS.current.tonePalette.d);
281
+ for (const [width, rows] of [[100, 100], [48, 28], [32, 20], [1, 1]] as const) {
282
+ const scaled = scaleMascot(art, width, rows);
283
+ assert.ok(scaled.mascot.length <= rows);
284
+ assert.equal(scaled.mascot.length, scaled.toneMap.length);
285
+ if (width >= 32) assert.match(scaled.mascot.join("\n"), /▼+[^▼\n]+▼+/, "both fangs survive responsive sampling");
286
+ for (let row = 0; row < scaled.mascot.length; row++) {
287
+ const glyphs = Array.from(scaled.mascot[row] ?? "");
288
+ const tones = Array.from(scaled.toneMap[row] ?? "");
289
+ assert.ok(visibleWidth(scaled.mascot[row] ?? "") <= width);
290
+ assert.equal(glyphs.length, tones.length);
291
+ for (let column = 0; column < glyphs.length; column++) {
292
+ if (glyphs[column] !== " ") assert.ok(Object.keys(tonePalette).includes(tones[column] ?? ""));
293
+ if (glyphs[column] === "▼") assert.equal(tones[column], "h", "fangs retain ivory rather than glow/lining tones");
294
+ }
295
+ }
296
+ }
297
+ });
298
+
253
299
  void test("Bts retains the 60 by 26 raccoon glyph structure and transparent tone cells", () => {
254
300
  const art = MASCOTS.bts.art;
255
301
  assert.equal(art.mascot.length, 26);
@@ -9,24 +9,24 @@ registerHooks({ resolve(specifier, context, nextResolve) {
9
9
  return nextResolve(specifier, context);
10
10
  } });
11
11
  const { childActivityState: activity, childAssignmentState: assignment } = await import("./working-child-activity.js");
12
- const working = (agent: string) => ({ activity: "working", label: `${agent} · Working...` });
12
+ const working = () => ({ activity: "working", label: "Working..." });
13
13
 
14
14
  void test("fallback uses only validated public agent identity regardless of optional label", () => {
15
- assert.deepEqual(assignment("subagent_run", { agent: "explore", label: "map footer data" }), working("explore"));
15
+ assert.deepEqual(assignment("subagent_run", { agent: "explore", label: "map footer data" }), working());
16
16
  for (const args of [undefined, null, [], "scout", {}, { agent: 1 }, { agent: "\u0000\n" }, { agent: "a".repeat(4097) }, { label: "secret" }])
17
17
  assert.equal(assignment("subagent_run", args), undefined);
18
18
  for (const label of [undefined, null, 1, [], "", "\u0000", "x".repeat(4097)])
19
- assert.deepEqual(assignment("subagent_run", { agent: "scout", label, task: "SECRET", prompt: "SECRET", context: "SECRET" }), working("scout"));
19
+ assert.deepEqual(assignment("subagent_run", { agent: "scout", label, task: "SECRET", prompt: "SECRET", context: "SECRET" }), working());
20
20
  const metadata = { agent: "scout", get label() { throw new Error("label must not be read"); },
21
21
  get task() { throw new Error("task must not be read"); }, get prompt() { throw new Error("prompt must not be read"); },
22
22
  get context() { throw new Error("context must not be read"); } };
23
- assert.deepEqual(assignment("subagent_run", metadata), working("scout"));
23
+ assert.deepEqual(assignment("subagent_run", metadata), working());
24
24
  assert.equal(assignment("read", { agent: "scout" }), undefined);
25
- assert.deepEqual(assignment("subagent_continue", { id: "scout" }), working("Subagent"));
25
+ assert.deepEqual(assignment("subagent_continue", { id: "scout" }), working());
26
26
  for (const agent of [42, null, "", "\u0000"])
27
27
  assert.equal(assignment("subagent_continue", { agent }), undefined);
28
- assert.deepEqual(assignment("subagent_run", { agent: "\u001b[31m探索\u001b[0m\n\u202eagent" }), working("探索 agent"));
29
- assert.deepEqual(assignment("subagent_run", { agent: "🙂".repeat(1000) }), working("🙂".repeat(32)));
28
+ assert.deepEqual(assignment("subagent_run", { agent: "\u001b[31m探索\u001b[0m\n\u202eagent" }), working());
29
+ assert.deepEqual(assignment("subagent_run", { agent: "🙂".repeat(1000) }), working());
30
30
  });
31
31
 
32
32
  function task(agent = "scout", kind = "tool_running", tool_names: unknown = ["read"]) {
@@ -54,18 +54,18 @@ void test("public operations return structured six-state activity and label", ()
54
54
  ];
55
55
  for (const kind of ["tool_running", "tool_completed", "tool_failed"])
56
56
  for (const [tool, state, label] of cases)
57
- assert.deepEqual(activity(progress(task("scout", kind, [tool]))), { activity: state, label: `scout · ${label}` });
57
+ assert.deepEqual(activity(progress(task("scout", kind, [tool]))), { activity: state, label });
58
58
  for (const kind of ["thinking", "streaming_response"])
59
- assert.deepEqual(activity(progress(task("scout", kind))), { activity: "thinking", label: "scout · Thinking..." });
59
+ assert.deepEqual(activity(progress(task("scout", kind))), { activity: "thinking", label: "Thinking..." });
60
60
  });
61
61
 
62
62
  void test("multiple children select first running snapshot child and validate all children", () => {
63
63
  assert.deepEqual(activity(progress({ agent: "old", status: "completed" }, task("first"), task("second"), task("third"))),
64
- { activity: "exploring", label: "first · Exploring... (+2)" });
65
- assert.deepEqual(activity(progress(task("first", "tool_running", ["read", "write"]))), { activity: "exploring", label: "first · Exploring..." });
64
+ { activity: "exploring", label: "Exploring... (+2)" });
65
+ assert.deepEqual(activity(progress(task("first", "tool_running", ["read", "write"]))), { activity: "exploring", label: "Exploring..." });
66
66
  assert.equal(activity(progress(task(), task("bad", "tool_running", ["read", 1]))), undefined);
67
67
  assert.equal(activity(progress(...Array.from({ length: 65 }, () => task()))), undefined);
68
- assert.equal(activity(progress(...Array.from({ length: 64 }, () => task())))?.label, "scout · Exploring... (+63)");
68
+ assert.equal(activity(progress(...Array.from({ length: 64 }, () => task())))?.label, "Exploring... (+63)");
69
69
  });
70
70
 
71
71
  void test("terminal, missing, malformed and unsupported progress has no usable activity", () => {
@@ -90,16 +90,16 @@ void test("oversized snapshot agent is rejected despite otherwise valid live act
90
90
  assert.equal(activity(progress(task("a".repeat(4097)))), undefined);
91
91
  });
92
92
 
93
- void test("snapshot agent at the input bound remains valid and is truncated for display", () => {
93
+ void test("snapshot agent at the input bound remains valid without appearing in the label", () => {
94
94
  assert.deepEqual(activity(progress(task("a".repeat(4096)))),
95
- { activity: "exploring", label: `${"a".repeat(32)} · Exploring...` });
95
+ { activity: "exploring", label: "Exploring..." });
96
96
  });
97
97
 
98
98
  void test("external names are single-line, control-free and bounded before classification", () => {
99
99
  const dirty = "\u001b[31mAgent\u001b[0m\n\t\u202eName\u0007";
100
100
  assert.deepEqual(activity(progress(task(dirty, "tool_running", ["\u001b]0;title\u0007read\nfile"]))),
101
- { activity: "executing", label: "Agent Name · Executing..." });
101
+ { activity: "executing", label: "Executing..." });
102
102
  assert.deepEqual(activity(progress(task("a".repeat(1000), "tool_running", ["b".repeat(1000)]))),
103
- { activity: "executing", label: `${"a".repeat(32)} · Executing...` });
103
+ { activity: "executing", label: "Executing..." });
104
104
  assert.equal(activity(progress(task("\u0000\n"))), undefined);
105
105
  });
@@ -4,8 +4,8 @@ import { classifyWorkingActivity, WORKING_ACTIVITY_LABELS } from "./working-acti
4
4
 
5
5
  export type ChildWorkingState = { activity: WorkingActivity; label: string };
6
6
 
7
- function childState(agent: string, activity: WorkingActivity): ChildWorkingState {
8
- return { activity, label: `${agent} · ${WORKING_ACTIVITY_LABELS[activity]}` };
7
+ function childState(activity: WorkingActivity): ChildWorkingState {
8
+ return { activity, label: WORKING_ACTIVITY_LABELS[activity] };
9
9
  }
10
10
 
11
11
  function record(value: unknown): value is Record<string, unknown> {
@@ -27,7 +27,7 @@ export function childAssignmentState(toolName: string, args: unknown): ChildWork
27
27
  if (!record(args)) return undefined;
28
28
  const agent = args.agent === undefined && toolName === "subagent_continue" ? "Subagent" : safeLabel(args.agent, 32);
29
29
  if (!agent) return undefined;
30
- return childState(agent, "working");
30
+ return childState("working");
31
31
  }
32
32
 
33
33
  function observableActivity(value: unknown): WorkingActivity | undefined {
@@ -63,7 +63,7 @@ export function childActivityState(partialResult: unknown): ChildWorkingState |
63
63
  const agent = safeLabel(task.agent, 32);
64
64
  const activity = record(task.live_activity) ? observableActivity(task.live_activity.current) : undefined;
65
65
  if (!agent || !activity) return undefined;
66
- first ??= childState(agent, activity);
66
+ first ??= childState(activity);
67
67
  running++;
68
68
  }
69
69
  if (!first) return undefined;
@@ -122,7 +122,7 @@ void test("child states select matching text and every existing glyph frame", (t
122
122
  t.mock.timers.enable({ apis: ["setInterval"] });
123
123
  const { working, controller: c } = fixture();
124
124
  c.onToolStart("subagent_run", "child", { agent: "scout" });
125
- assertDisplay(working, "working", "scout · Working...");
125
+ assertDisplay(working, "working", "Working...");
126
126
  const cases: Array<[string, string | undefined, WorkingActivity, string]> = [
127
127
  ["thinking", undefined, "thinking", "Thinking..."],
128
128
  ["streaming_response", undefined, "thinking", "Thinking..."],
@@ -135,7 +135,7 @@ void test("child states select matching text and every existing glyph frame", (t
135
135
  for (const [kind, tool, activity, label] of cases) {
136
136
  c.onToolUpdate("subagent_run", "child", { details: { tasks: [{ agent: "scout", status: "running",
137
137
  live_activity: { current: { kind, label: "IGNORED", tool_names: tool ? [tool] : undefined } } }] } });
138
- assertDisplay(working, activity, `scout · ${label}`);
138
+ assertDisplay(working, activity, label);
139
139
  }
140
140
  c.onShutdown();
141
141
  });
@@ -145,21 +145,21 @@ void test("child updates respect tool identity and latest remaining tool ownersh
145
145
  const { working, controller: c } = fixture();
146
146
  c.onToolStart("subagent_run", "a");
147
147
  c.onToolUpdate("subagent_run", "a", childProgress("scout"));
148
- assertDisplay(working, "exploring", "scout · Exploring...");
148
+ assertDisplay(working, "exploring", "Exploring...");
149
149
  c.onToolStart("subagent_run", "b");
150
150
  c.onToolUpdate("subagent_run", "b", childProgress("writer", "edit"));
151
151
  c.onToolUpdate("subagent_run", "a", childProgress("scout", "grep"));
152
152
  c.onToolUpdate("read", "b", childProgress("wrong"));
153
153
  c.onToolUpdate("subagent_run", "missing", childProgress("late"));
154
154
  c.onToolEnd("missing");
155
- assertDisplay(working, "working", "writer · Working...");
155
+ assertDisplay(working, "working", "Working...");
156
156
  c.onToolStart("read", "ordinary");
157
157
  c.onToolUpdate("subagent_run", "ordinary", childProgress("wrong"));
158
158
  assert.equal(working.label, "Exploring...");
159
- c.onToolEnd("ordinary"); assertDisplay(working, "working", "writer · Working...");
160
- c.onToolEnd("b"); assertDisplay(working, "exploring", "scout · Exploring...");
159
+ c.onToolEnd("ordinary"); assertDisplay(working, "working", "Working...");
160
+ c.onToolEnd("b"); assertDisplay(working, "exploring", "Exploring...");
161
161
  c.onToolUpdate("subagent_run", "b", childProgress("late"));
162
- assertDisplay(working, "exploring", "scout · Exploring...");
162
+ assertDisplay(working, "exploring", "Exploring...");
163
163
  c.onToolUpdate("subagent_run", "a", childProgress("scout", "read", "completed"));
164
164
  assert.equal(working.label, "Delegating...");
165
165
  c.onToolUpdate("subagent_run", "a", childProgress("scout"));
@@ -187,16 +187,16 @@ void test("agent fallback appears at start and resets after unavailable, malform
187
187
  t.mock.timers.enable({ apis: ["setInterval"] });
188
188
  const { working, controller: c } = fixture();
189
189
  c.onToolStart("subagent_run", "a", { agent: "explore", label: "map footer data", task: "SECRET TASK", prompt: "SECRET PROMPT" });
190
- assertDisplay(working, "working", "explore · Working...");
190
+ assertDisplay(working, "working", "Working...");
191
191
  c.onToolUpdate("subagent_run", "a", childProgress("explore"));
192
- assertDisplay(working, "exploring", "explore · Exploring...");
192
+ assertDisplay(working, "exploring", "Exploring...");
193
193
  for (const update of [undefined, {}, { details: { tasks: null } }, { details: { tasks: [{ agent: "explore", status: "running" }] } }, childProgress("explore", "read", "completed"), childProgress("explore", ""),
194
194
  { details: { tasks: [{ agent: "explore", status: "running", live_activity: { current: { kind: "tool_running", tool_names: [] } } }] } },
195
195
  { details: { tasks: [{ agent: "explore", status: "running", live_activity: { current: { kind: "tool_running" } } }] } }]) {
196
196
  c.onToolUpdate("subagent_run", "a", childProgress("explore"));
197
- assertDisplay(working, "exploring", "explore · Exploring...");
197
+ assertDisplay(working, "exploring", "Exploring...");
198
198
  c.onToolUpdate("subagent_run", "a", update);
199
- assertDisplay(working, "working", "explore · Working...");
199
+ assertDisplay(working, "working", "Working...");
200
200
  }
201
201
  c.onToolEnd("a"); assert.equal(working.label, "Thinking...");
202
202
  });
@@ -205,28 +205,28 @@ void test("continuations use generic identity without guessing and preserve call
205
205
  t.mock.timers.enable({ apis: ["setInterval"] });
206
206
  const { working, controller: c } = fixture();
207
207
  c.onToolStart("subagent_run", "a", { agent: "scout" });
208
- assert.equal(working.label, "scout · Working...");
208
+ assert.equal(working.label, "Working...");
209
209
  c.onToolStart("subagent_continue", "b", { id: "a", label: "check footer" });
210
- assert.equal(working.label, "Subagent · Working...");
210
+ assert.equal(working.label, "Working...");
211
211
  c.onToolStart("subagent_continue", "a", { agent: "wrong", label: "wrong" });
212
212
  c.onToolUpdate("subagent_run", "b", childProgress("wrong"));
213
213
  c.onToolUpdate("subagent_continue", "missing", childProgress("wrong"));
214
- assert.equal(working.label, "Subagent · Working...");
214
+ assert.equal(working.label, "Working...");
215
215
  c.onToolUpdate("subagent_continue", "b", childProgress("writer", "edit"));
216
- assertDisplay(working, "working", "writer · Working...");
216
+ assertDisplay(working, "working", "Working...");
217
217
  c.onToolUpdate("subagent_run", "a", childProgress("scout"));
218
- assertDisplay(working, "working", "writer · Working...");
218
+ assertDisplay(working, "working", "Working...");
219
219
  c.onToolUpdate("subagent_continue", "b", {});
220
- assert.equal(working.label, "Subagent · Working...");
221
- c.onToolEnd("b"); assertDisplay(working, "exploring", "scout · Exploring...");
220
+ assert.equal(working.label, "Working...");
221
+ c.onToolEnd("b"); assertDisplay(working, "exploring", "Exploring...");
222
222
  c.onToolUpdate("subagent_continue", "b", childProgress("late"));
223
- c.onToolUpdate("subagent_run", "a", {}); assert.equal(working.label, "scout · Working...");
223
+ c.onToolUpdate("subagent_run", "a", {}); assert.equal(working.label, "Working...");
224
224
  c.onToolEnd("a");
225
225
  c.onToolStart("subagent_continue", "c", { agent: "writer", label: "finish" });
226
- assert.equal(working.label, "writer · Working...");
226
+ assert.equal(working.label, "Working...");
227
227
  c.onToolEnd("c");
228
228
  c.onToolStart("subagent_continue", "d", { id: "c", task: "SECRET" });
229
- assert.equal(working.label, "Subagent · Working...");
229
+ assert.equal(working.label, "Working...");
230
230
  c.onToolEnd("d");
231
231
  });
232
232
 
@@ -237,7 +237,7 @@ void test("identity failures stay parent-owned and live snapshots retain validat
237
237
  c.onToolStart("subagent_run", "run", args);
238
238
  assertDisplay(working, "delegating", "Delegating...");
239
239
  c.onToolUpdate("subagent_run", "run", childProgress("public"));
240
- assertDisplay(working, "exploring", "public · Exploring...");
240
+ assertDisplay(working, "exploring", "Exploring...");
241
241
  c.onToolUpdate("subagent_run", "run", {});
242
242
  assertDisplay(working, "delegating", "Delegating...");
243
243
  c.onToolEnd("run");
@@ -246,17 +246,17 @@ void test("identity failures stay parent-owned and live snapshots retain validat
246
246
  assertDisplay(working, "executing", "Executing...");
247
247
  c.onToolEnd("continue");
248
248
  c.onToolStart("subagent_continue", "continue", {});
249
- assertDisplay(working, "working", "Subagent · Working...");
249
+ assertDisplay(working, "working", "Working...");
250
250
  c.onToolUpdate("subagent_continue", "continue", { details: { tasks: [
251
251
  { agent: "first", status: "running", live_activity: { current: { kind: "tool_running", tool_names: ["read"] } } },
252
252
  { agent: "second", status: "running", live_activity: { current: { kind: "thinking" } } },
253
253
  ] } });
254
- assertDisplay(working, "exploring", "first · Exploring... (+1)");
254
+ assertDisplay(working, "exploring", "Exploring... (+1)");
255
255
  c.onToolStart("subagent_run", "continue", { agent: "wrong" });
256
256
  c.onToolUpdate("subagent_run", "continue", childProgress("wrong"));
257
- assertDisplay(working, "exploring", "first · Exploring... (+1)");
257
+ assertDisplay(working, "exploring", "Exploring... (+1)");
258
258
  c.onToolUpdate("subagent_continue", "continue", {});
259
- assertDisplay(working, "working", "Subagent · Working...");
259
+ assertDisplay(working, "working", "Working...");
260
260
  c.onShutdown();
261
261
  });
262
262
 
@@ -265,7 +265,7 @@ void test("assignment metadata is cleared on agent and session cleanup", (t) =>
265
265
  const { working, controller: c } = fixture();
266
266
  for (const cleanup of [() => c.onAgentEnd(), () => c.onShutdown()]) {
267
267
  c.onToolStart("subagent_run", "a", { agent: "scout", label: "map" });
268
- assert.equal(working.label, "scout · Working...");
268
+ assert.equal(working.label, "Working...");
269
269
  cleanup(); c.onToolUpdate("subagent_run", "a", childProgress("late"));
270
270
  c.refreshWorking(); assert.equal(working.label, "Thinking...");
271
271
  assert.equal(working.active, false); assert.equal(working.timer, undefined);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osdy-pi",
3
- "version": "1.11.8",
3
+ "version": "1.11.10",
4
4
  "description": "Osdy Pi package: themes, header, and custom editor for Pi.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -83,7 +83,7 @@
83
83
  "typescript": "^5.9.3",
84
84
  "typescript-eslint": "^8.46.3"
85
85
  },
86
- "releaseSource": "f0af5964701aaf9022bb79d1ff2d25dbc389b7dc",
86
+ "releaseSource": "96f670c11788dfb1a336c664a266b8581546ff59",
87
87
  "repository": {
88
88
  "type": "git",
89
89
  "url": "git+https://github.com/OsdyOrtiz/Osdy-Pi.git"