osdy-pi 1.11.9 → 1.11.11
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 +31 -17
- package/extensions/osdy-pi/account-profiles.test.ts +43 -1
- package/extensions/osdy-pi/account-profiles.ts +11 -5
- package/extensions/osdy-pi/animation.test.ts +29 -2
- package/extensions/osdy-pi/constants.ts +48 -0
- package/extensions/osdy-pi/control-center-account.test.ts +81 -0
- package/extensions/osdy-pi/control-center-account.ts +1 -1
- package/extensions/osdy-pi/control-center-preferences.test.ts +4 -2
- package/extensions/osdy-pi/control-center-preferences.ts +5 -1
- package/extensions/osdy-pi/control-center.test.ts +16 -2
- package/extensions/osdy-pi/editor-settings.test.ts +13 -1
- package/extensions/osdy-pi/runtime.test.ts +254 -44
- package/extensions/osdy-pi/runtime.ts +68 -51
- package/extensions/osdy-pi/types.ts +1 -1
- package/extensions/osdy-pi/ui.test.ts +56 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Osdy Pi gives [Pi](https://github.com/earendil-works/pi) a themed, responsive te
|
|
|
12
12
|
|
|
13
13
|
| Area | What ships |
|
|
14
14
|
| --- | --- |
|
|
15
|
-
| Control Center | `/osdyConfig` opens ten categories, including confirmed TODO/Agents provider selection and explicit per-profile Codex quota previews. |
|
|
15
|
+
| Control Center | **Ctrl+Option+O** or `/osdyConfig` opens ten categories, including confirmed TODO/Agents provider selection and explicit per-profile Codex quota previews. |
|
|
16
16
|
| Local analytics (1.10.0) | `/osdy-usage` opens Summary, Profiles, Models, and History pages for recorded tokens and estimated cost—not billing or subscription quota. |
|
|
17
17
|
| Themes introduced in 1.11.0 | Spider-Man Classic, Miles Morales, and Spider-Verse add three distinct dark palettes. |
|
|
18
18
|
| Themes introduced in 1.9.0 | Halloween and Halloween Killer add two dark palettes. |
|
|
@@ -20,7 +20,7 @@ Osdy Pi gives [Pi](https://github.com/earendil-works/pi) a themed, responsive te
|
|
|
20
20
|
| Message cards | Pi-native colored user cards without emojis and plain assistant Markdown preceded by a separate `🦝` transcript row. |
|
|
21
21
|
| Header and mascot | Independent header/mascot choices, an installed-package version label, and mascot-only fallback when full artwork cannot fit. |
|
|
22
22
|
| Accounts | `/osdy-account` switches Codex profiles in place with atomic activation, rollback, bounded auth files, and process-safe locks. |
|
|
23
|
-
| Quota | `/usage`
|
|
23
|
+
| Quota | **Ctrl+Option+U** or `/usage` opens subscription quota; compact bars emphasize remaining Codex quota at warning (40% or less) and error (15% or less) thresholds. |
|
|
24
24
|
| TODO (starting in 1.5.0, opt-in) | Default-off first-party `todo` snapshots track the Pi session branch; `/todos` reads them without editing. ODD Markdown remains separate. |
|
|
25
25
|
|
|
26
26
|
## Prerequisites
|
|
@@ -57,19 +57,30 @@ Osdy Pi's own extension and themes are bundled—do not install them separately.
|
|
|
57
57
|
|
|
58
58
|
### Control Center in Pi
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
Press **Ctrl+Option+O** or run **`/osdyConfig`** in interactive Pi to open the Osdy Control Center. The ten categories—**Theme, Header, Mascot, Editor, Git, Sounds, Account, Usage, TODO, and Agents**—provide inline controls or data in the same modal. Existing commands remain available.
|
|
61
|
+
|
|
62
|
+
| Shortcut | Panel | Command fallback |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| **Ctrl+Option+O** | Osdy Control Center | `/osdyConfig` |
|
|
65
|
+
| **Ctrl+Option+U** | Active Codex subscription quota—not local token/cost analytics | `/usage` |
|
|
66
|
+
|
|
67
|
+
On macOS, **Option = Alt** in Pi's shortcut names: these bindings register as `ctrl+alt+o` and `ctrl+alt+u`. On other platforms, use **Ctrl+Alt+O** and **Ctrl+Alt+U**. Your terminal must send Option as **Alt/Meta** and support **Ctrl+Alt chords**; registration does not guarantee actual hardware delivery. Check **`/hotkeys`** for bindings, and use **`/osdyConfig`** or **`/usage`** if the terminal, OS, or another extension intercepts or cannot deliver the chord.
|
|
68
|
+
|
|
69
|
+
**Ctrl+Option+O capability limits:** Account **Switch**, **Set default**, and **Clear default** refuse changes while Pi is busy; retry when idle. The same actions through `/osdyConfig` retain idle waiting, as does switching through `/osdy-account`. Read-only account quota previews remain available while busy. Successful TODO/Agents selection from Ctrl+Option+O saves settings, closes/disposes the panel, releases its editor hold, then asks you to run **`/reload` manually** (or restart Pi). From `/osdyConfig`, the same sequence still reloads automatically once; restart Pi if reload fails.
|
|
70
|
+
|
|
71
|
+
Ctrl+Option+U opens the existing `/usage` dashboard with the same refresh behavior; it does not open `/osdy-usage`.
|
|
61
72
|
|
|
62
73
|
- Use **↑/↓**, **Home/End**, or **Page Up/Down** to navigate; **Tab** or **←/→** switches between categories and details. **Enter** selects. Single blank lines separate the heading, summary, compact options, selected-item help, status, and action hints—not individual options. Gaps yield on short viewports so the selected option and controls remain reachable. Press **?** to expand/collapse the selected help and view notes; resize if expanded help is clipped. Confirmation effects remain complete, and **?** never toggles help during confirmation, path editing, loading, querying, or saving.
|
|
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
|
|
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
|
|
74
|
+
- 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 automatic command reload or the Ctrl+Option+O manual-reload notice described above; 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.
|
|
75
|
+
- 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 automatic command reload or the Ctrl+Option+O manual-reload notice described above; restart Pi if reload fails. Legacy `/osdy-pi agents setup|on|off|status` is unchanged.
|
|
65
76
|
- 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.
|
|
77
|
+
- 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
78
|
- 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
79
|
- 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
80
|
- **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.
|
|
70
81
|
- **Sounds** shows each event's saved and effective path/source. Configure opens a single-line path editor **inside this overlay**: enter a readable `.mp3`/`.wav` path, then **Enter** to validate and save immediately, or **Esc** to cancel editing. Relative paths resolve against the project directory and save as absolute paths. Clear removes only that event's saved path from the existing v1 audio store (`<agent-dir>/extensions/osdy-pi/audio-notifications.json`). Notifications read this store afresh; refresh failures are reported separately from save failures.
|
|
71
82
|
- Sounds' master enablement is **unavailable**: the existing schema has paths, not an enable switch or presets. Startup `--osdy-pi-sound-<event>` flags override saved paths, so **clearing a saved path does not disable flag-configured playback**. Only **Test effective** explicitly plays a sound; opening categories and configuring paths never play. Tests report unavailable files/platforms or playback failures; playback remains macOS/Windows only. The legacy `/osdy-pi sound setup` wizard is preserved.
|
|
72
|
-
- **Account** shows one row per validated profile, marked **Active** and **Default** where applicable. Select a profile, then use its visible context bar: **v View usage**, **s Switch** (inactive profiles), **d Set default**, and **c Clear default** (when a default exists). **v** or **Enter** explicitly queries that profile's stored Codex credentials without activating it; no second action list appears. Opening, navigating or highlighting never queries; stored profiles are never polled. A dedicated quota view reuses `/usage` dashboard content: Session/Weekly cards, additional bucket groups, remaining-capacity bars, resets, and the quotas/account summary with plan and credits when available. Identity belongs to the selected stored profile; unknown provider/model metadata is labeled honestly. Checked and updated times use local time; absent data is unknown, not zero. Refresh/Back controls and query feedback stay pinned while long dashboard content scrolls. **r Refresh** repeats the selected profile query; **s Switch**, when available, confirms activation of the currently viewed inactive profile. Cancelling that confirmation preserves the quota view and scroll position. **b Back** or **Esc** returns to the overview with the profile selected, without closing the overlay. In quota details, **↑/↓** selects Refresh/Back and **Enter** activates it; **Page Up/Down** and **Home/End** scroll long quota data. Switch, set default, and clear default require an **in-screen confirmation** with Cancel selected initially; **Esc** cancels confirmation without closing the overlay. Single blank separators group the confirmation title, complete effects, choices, and instructions; gaps yield on short viewports. Resize if the full confirmation cannot fit—acceptance stays hidden and blocked. Switching waits for Pi to become idle
|
|
83
|
+
- **Account** shows one row per validated profile, marked **Active** and **Default** where applicable. Select a profile, then use its visible context bar: **v View usage**, **s Switch** (inactive profiles), **d Set default**, and **c Clear default** (when a default exists). **v** or **Enter** explicitly queries that profile's stored Codex credentials without activating it; no second action list appears. Opening, navigating or highlighting never queries; stored profiles are never polled. A dedicated quota view reuses `/usage` dashboard content: Session/Weekly cards, additional bucket groups, remaining-capacity bars, resets, and the quotas/account summary with plan and credits when available. Identity belongs to the selected stored profile; unknown provider/model metadata is labeled honestly. Checked and updated times use local time; absent data is unknown, not zero. Refresh/Back controls and query feedback stay pinned while long dashboard content scrolls. **r Refresh** repeats the selected profile query; **s Switch**, when available, confirms activation of the currently viewed inactive profile. Cancelling that confirmation preserves the quota view and scroll position. **b Back** or **Esc** returns to the overview with the profile selected, without closing the overlay. In quota details, **↑/↓** selects Refresh/Back and **Enter** activates it; **Page Up/Down** and **Home/End** scroll long quota data. Switch, set default, and clear default require an **in-screen confirmation** with Cancel selected initially; **Esc** cancels confirmation without closing the overlay. Single blank separators group the confirmation title, complete effects, choices, and instructions; gaps yield on short viewports. Resize if the full confirmation cannot fit—acceptance stays hidden and blocked. Switching through `/osdyConfig` waits for Pi to become idle; Ctrl+Option+O refuses busy account changes. Successful switching activates through the existing backend and refreshes subscription usage. Login remains Pi-owned (`/login`); create/rename/remove remain available in `/osdy-account`. Category navigation never changes profiles or credentials.
|
|
73
84
|
- **Usage** shows the runtime's cached active Codex subscription quota (remaining-capacity bars and percentages, window, reset/fetch times) and the existing local history owner's recorded token/cache totals, cost gaps, and model detail. Select **Range: day/week/month** for the current local calendar period; toggle **Accounts: all/current** for local analytics (unmanaged when no valid current profile exists). Quota always belongs to the active account, not the analytics filter; local token/cost totals never become quota percentages. Select **Refresh quota and local history** explicitly; rendering and filters never fetch. Empty, loading, unavailable, and limited/incomplete coverage are reported honestly. Estimates are not billing or subscription quota. `/usage` and `/osdy-usage` remain unchanged.
|
|
74
85
|
- **Esc** closes without undoing live or saved changes, except when cancelling a confirmation/input or returning from Account quota details. Account read-only queries are cancelled on Back/Esc, category exit or close; late results are ignored even if cancellation is not honored. Other in-flight operations still finish and are not claimed cancelled. Late results cannot replace a closed overlay or another category. On narrow terminals, categories and details share the viewport; use Tab to switch.
|
|
75
86
|
|
|
@@ -300,7 +311,7 @@ Before rename or removal, close this Pi process when it uses the target and **ma
|
|
|
300
311
|
| Area | Included behavior |
|
|
301
312
|
| --- | --- |
|
|
302
313
|
| 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 |
|
|
314
|
+
| Header and mascot | Independently selectable `osdy-theme`/`neon` headers and `current`/`bts`/`osdy-halloween` mascots |
|
|
304
315
|
| Messages | Native colored user cards without emojis and plain assistant Markdown with a separate assistant `🦝` row |
|
|
305
316
|
| Input | Responsive auto editor by default, with selectable simple Pi-native or extended framed modes |
|
|
306
317
|
| 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 +377,12 @@ Header and mascot choices are independent:
|
|
|
366
377
|
| Element | Choices | Command |
|
|
367
378
|
| --- | --- | --- |
|
|
368
379
|
| Header | `osdy-theme` (default), `neon` | `/osdy-pi header osdy-theme|neon` |
|
|
369
|
-
| Mascot | `current` (default), `bts` | `/osdy-pi mascot current
|
|
380
|
+
| Mascot | `current` (default), `bts`, `osdy-halloween` | `/osdy-pi mascot current\|bts\|osdy-halloween` |
|
|
370
381
|
|
|
371
382
|
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
383
|
|
|
384
|
+
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.
|
|
385
|
+
|
|
373
386
|
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
387
|
|
|
375
388
|
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.
|
|
@@ -400,21 +413,21 @@ The enabled state, editor mode, working-tree visibility preference, header, and
|
|
|
400
413
|
|
|
401
414
|
| Group | Command |
|
|
402
415
|
| --- | --- |
|
|
403
|
-
| Control Center | `/osdyConfig` (interactive terminal UI) |
|
|
416
|
+
| Control Center | **Ctrl+Option+O** or `/osdyConfig` (interactive terminal UI) |
|
|
404
417
|
| Main | `/osdy-pi` |
|
|
405
418
|
| Main | `/osdy-pi enable\|disable\|on\|off\|status` |
|
|
406
419
|
| Accounts | `/osdy-account` |
|
|
407
420
|
| TODO selection | `/osdy-pi todo on\|off\|status` |
|
|
408
421
|
| Session TODO (opt-in, starting in 1.5.0) | `/todos` (read-only) |
|
|
409
422
|
| Header | `/osdy-pi header osdy-theme\|neon\|status` |
|
|
410
|
-
| Mascot | `/osdy-pi mascot current\|bts\|status` |
|
|
423
|
+
| Mascot | `/osdy-pi mascot current\|bts\|osdy-halloween\|status` |
|
|
411
424
|
| Editor | `/osdy-pi editor auto\|extended\|simple\|on\|off\|toggle\|status` |
|
|
412
425
|
| Working tree | `/osdy-pi working-tree on\|off\|toggle\|status` |
|
|
413
426
|
| Working tree | `/osdy-pi working-tree position top\|bottom\|status` |
|
|
414
427
|
| Audio | `/osdy-pi sound setup` |
|
|
415
428
|
| Diff | `/osdy-pi diff` |
|
|
416
429
|
| Package | `/osdy-pi uninstall` |
|
|
417
|
-
| Codex subscription | `/usage` |
|
|
430
|
+
| Codex subscription | **Ctrl+Option+U** or `/usage` |
|
|
418
431
|
| Local usage analytics (starting in 1.10.0) | `/osdy-usage` |
|
|
419
432
|
|
|
420
433
|
`/osdy-pi uninstall` finds a unique Osdy Pi package in Pi's configured package list and asks you to confirm its exact source and user/project scope before calling Pi's `remove` command. Run it from a trusted project with interactive UI. Git registrations are checked against Pi's host/path identity; a local registration must point to this running extension's package root and have a matching manifest. Ambiguous or changed registrations, unrecognized sources, and cancelled confirmation remove nothing. It removes only the Pi package registration, not Osdy profiles, accounts, the globally installed CLI, or other packages. Restart Pi afterward to unload the extension. If no unique source is found, inspect `pi list` and use `pi remove <source> [-l]` manually.
|
|
@@ -423,13 +436,13 @@ The enabled state, editor mode, working-tree visibility preference, header, and
|
|
|
423
436
|
|
|
424
437
|
### Codex subscription usage
|
|
425
438
|
|
|
426
|
-
|
|
439
|
+
Press **Ctrl+Option+U** or run `/usage` to open the same Codex subscription dashboard for the active managed `openai-codex` profile and model. Press `r` to refresh; `esc` or `q` closes it. The dashboard shows these controls at the bottom.
|
|
427
440
|
|
|
428
441
|
The main windows are labeled **Session** and **Weekly** (the API/domain remains primary/secondary). Their remaining-capacity bars are full at 100% remaining and empty at 0%; labels use the active theme's bold accent, Session uses the accent fill, Weekly uses `mdLink`, and empty segments are muted. The remaining percentage stays muted above 40%, changes to the theme's warning color at 40% or less, and changes to its error color at 15% or less. The same thresholds apply in the `/usage` dashboard and compact footer/editor quota bars. A double themed frame, section dividers, and spacing separate the display. Detail cards show each duration and relative, local, and UTC reset times. Plan, availability, credits/reset count, and additional buckets appear only when the service supplies them; absent optional values are omitted.
|
|
429
442
|
|
|
430
443
|
At wide widths, the modal pairs the Session and Weekly detail cards. Below 72 content columns, cards and bars stack and account/model data wraps. The native footer and extended editor also show compact Session/Weekly remaining-capacity bars below model and thinking metadata whenever a current Codex snapshot exists. Wide widths combine those bars; narrow widths stack them. Additional buckets appear in `/usage` and the selected-profile Control Center quota view, not compact editor/footer bars.
|
|
431
444
|
|
|
432
|
-
Active quota loads when an enabled session starts and refreshes when `/usage` opens or `r` is pressed. It also refreshes while idle on a **60-second** schedule, only after settings are loaded, with Osdy enabled, an interactive terminal UI, an `openai-codex` model selected, and no quota request already in flight. Automatic failures double the retry delay up to **10 minutes**; a successful automatic query restores the 60-second interval. This scheduler never polls stored profiles.
|
|
445
|
+
Active quota loads when an enabled session starts and refreshes when **Ctrl+Option+U** or `/usage` opens the dashboard, or `r` is pressed. It also refreshes while idle on a **60-second** schedule, only after settings are loaded, with Osdy enabled, an interactive terminal UI, an `openai-codex` model selected, and no quota request already in flight. Automatic failures double the retry delay up to **10 minutes**; a successful automatic query restores the 60-second interval. This scheduler never polls stored profiles.
|
|
433
446
|
|
|
434
447
|
Automatic refresh retains the last snapshot during loading and on error, so an older reading can remain visible until a successful query replaces it. Manual refresh clears the previous snapshot before authentication resolves and supersedes any in-flight automatic request. Account switching refreshes active quota through that same clearing path. Session replacement and shutdown stop the timer, cancel pending requests and clear quota state; late results are ignored.
|
|
435
448
|
|
|
@@ -604,13 +617,14 @@ This launches `pi` from `PATH`, loads this checkout with `-e`, and uses `.pi-dev
|
|
|
604
617
|
The explicit argument bypasses the launcher's default-account activation; Pi's `--no-session` disables session persistence, **not settings writes or provider access**.
|
|
605
618
|
An isolated agent directory does **not** make custom audio paths or real provider accounts harmless. Use intentional disposable test profiles/files for profile writes, switches, default confirmations, and audio tests; do not assume an isolated account store makes real credentials safe.
|
|
606
619
|
|
|
607
|
-
- Open `/osdyConfig` in regular and fullscreen Pi. Check arrows/Home/End/Page Up/Down, Tab or left/right, Enter, and Esc; shrink and restore the terminal, checking both panes remain reachable.
|
|
620
|
+
- Check `/hotkeys` lists `ctrl+alt+o` and `ctrl+alt+u`. On macOS, configure Option to send Alt/Meta and verify the terminal supports Ctrl+Alt chords; actual Ctrl+Option delivery still needs a hardware check. Open both **Ctrl+Option+O** and `/osdyConfig` in regular and fullscreen Pi (Ctrl+Alt+O on other platforms); use the slash command if the chord is intercepted or unsupported. Check arrows/Home/End/Page Up/Down, Tab or left/right, Enter, and Esc; shrink and restore the terminal, checking both panes remain reachable. With disposable TODO/Agents settings, verify Ctrl+Option+O asks for manual `/reload` only after closing, while `/osdyConfig` reloads automatically.
|
|
608
621
|
- In disposable settings, change a visual preference, wait for **Saved globally**, close and reopen, then reload/restart to check persistence. Esc does not undo changes; distinguish save/application errors from success.
|
|
609
|
-
- Account (manual): with disposable profiles only, check Cancel-first switch/set-default/clear-default confirmations, Esc cancellation, idle switching, and current/default labels. Check one row per profile with **v/s/d/c** context actions and **Enter** usage fallback, without a second action list. From **v** quota details, check eligible **s Switch** opens the same Cancel-first confirmation, with blank separators between the title/effects, choices, and instructions; Cancel/Esc must retain the quota view and scroll position. Check gaps yield on short viewports without hiding safety content or enabling acceptance when the full confirmation cannot fit. Check remaining-capacity bars, **r Refresh**, **b Back/Esc** returning to the selected profile, long-view scrolling, no query on navigation, no stored-profile polling, and cancellation on exit. Create/rename/remove stay in `/osdy-account`, login in `/login`.
|
|
622
|
+
- Account (manual): with disposable profiles only, check Cancel-first switch/set-default/clear-default confirmations, Esc cancellation, idle switching, and current/default labels. Verify Ctrl+Option+O refuses all three changes while busy, succeeds when idle, and `/osdyConfig` still waits for idle. Check one row per profile with **v/s/d/c** context actions and **Enter** usage fallback, without a second action list. From **v** quota details, check eligible **s Switch** opens the same Cancel-first confirmation, with blank separators between the title/effects, choices, and instructions; Cancel/Esc must retain the quota view and scroll position. Check gaps yield on short viewports without hiding safety content or enabling acceptance when the full confirmation cannot fit. Check remaining-capacity bars, **r Refresh**, **b Back/Esc** returning to the selected profile, long-view scrolling, no query on navigation, no stored-profile polling, and cancellation on exit. Create/rename/remove stay in `/osdy-account`, login in `/login`.
|
|
623
|
+
- Subscription quota (manual): compare **Ctrl+Option+U** and `/usage` (Ctrl+Alt+U on other platforms), including **r Refresh** and Esc/q closing. Verify actual terminal chord delivery; use `/usage` if intercepted or unsupported.
|
|
610
624
|
- Usage (manual): check day/week/month and all/current filters, empty/unavailable/limited states, and explicit refresh. Authenticated quota and post-switch account correctness need an intentionally authorized live provider check; local estimates are not billing.
|
|
611
625
|
- Sounds (manual): use disposable readable `.mp3`/`.wav` files to check configure, cancel, clear, and startup-flag precedence. Only choose **Test effective** when playback is intended; actual macOS/Windows playback still needs a live check.
|
|
612
626
|
|
|
613
|
-
Live TUI appearance, real account switching/authenticated quota, and actual audio playback remain unverified; this checklist is not a recorded pass.
|
|
627
|
+
The user confirmed physical Ctrl+Option+O and Ctrl+Option+U delivery on one Mac terminal; other terminals may intercept or not support these chords. Live TUI appearance, real account switching/authenticated quota, and actual audio playback remain unverified; this checklist is not a recorded full pass.
|
|
614
628
|
|
|
615
629
|
Install a local checkout into Pi with:
|
|
616
630
|
|
|
@@ -460,12 +460,14 @@ void test("manages profile rename and permanent removal prompts through authorit
|
|
|
460
460
|
|
|
461
461
|
void test("switches in place after idle without spawning or shutting down", async () => {
|
|
462
462
|
const events: string[] = [];
|
|
463
|
+
let idle = false;
|
|
463
464
|
await switchAccountInPlace(
|
|
464
465
|
{
|
|
465
|
-
isIdle: () =>
|
|
466
|
+
isIdle: () => idle,
|
|
466
467
|
waitForIdle: () =>
|
|
467
468
|
Promise.resolve().then(() => {
|
|
468
469
|
events.push("idle");
|
|
470
|
+
idle = true;
|
|
469
471
|
}),
|
|
470
472
|
ui: {
|
|
471
473
|
notify: (message: string) => events.push(message),
|
|
@@ -495,6 +497,46 @@ void test("switches in place after idle without spawning or shutting down", asyn
|
|
|
495
497
|
]);
|
|
496
498
|
});
|
|
497
499
|
|
|
500
|
+
void test("base-context account switches require idle without a command wait hook", async () => {
|
|
501
|
+
const previous = process.env.OSDY_PI_PROFILE_NAME;
|
|
502
|
+
try {
|
|
503
|
+
for (const idle of [false, true]) {
|
|
504
|
+
process.env.OSDY_PI_PROFILE_NAME = "personal";
|
|
505
|
+
const events: string[] = [];
|
|
506
|
+
const notices: string[] = [];
|
|
507
|
+
const ctx = { isIdle: () => idle, ui: {
|
|
508
|
+
notify: (message: string) => notices.push(message),
|
|
509
|
+
select: () => Promise.resolve(undefined), input: () => Promise.resolve(undefined),
|
|
510
|
+
} };
|
|
511
|
+
assert.equal("waitForIdle" in ctx, false);
|
|
512
|
+
const switched = await switchAccountInPlace(ctx, "work",
|
|
513
|
+
profile => { events.push(`activate:${profile}`); return Promise.resolve(); },
|
|
514
|
+
() => { events.push("refresh"); return Promise.resolve(); },
|
|
515
|
+
() => { events.push("render"); });
|
|
516
|
+
assert.equal(switched, idle);
|
|
517
|
+
assert.deepEqual(events, idle ? ["activate:work", "render", "refresh"] : []);
|
|
518
|
+
assert.equal(process.env.OSDY_PI_PROFILE_NAME, idle ? "work" : "personal");
|
|
519
|
+
assert.match(notices.join(" "), idle ? /Switched to work/ : /require Pi to be idle/);
|
|
520
|
+
}
|
|
521
|
+
} finally {
|
|
522
|
+
if (previous === undefined) delete process.env.OSDY_PI_PROFILE_NAME;
|
|
523
|
+
else process.env.OSDY_PI_PROFILE_NAME = previous;
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
void test("a command wait hook cannot activate an account if Pi remains busy", async () => {
|
|
528
|
+
const events: string[] = [];
|
|
529
|
+
const switched = await switchAccountInPlace({ isIdle: () => false,
|
|
530
|
+
waitForIdle: () => { events.push("wait"); return Promise.resolve(); },
|
|
531
|
+
ui: { notify: message => events.push(message), select: () => Promise.resolve(undefined), input: () => Promise.resolve(undefined) },
|
|
532
|
+
}, "work", () => { events.push("activate"); return Promise.resolve(); },
|
|
533
|
+
() => { events.push("refresh"); return Promise.resolve(); }, () => { events.push("render"); });
|
|
534
|
+
assert.equal(switched, false);
|
|
535
|
+
assert.equal(events[0], "wait");
|
|
536
|
+
assert.match(events[1] ?? "", /require Pi to be idle/);
|
|
537
|
+
assert.equal(events.length, 2);
|
|
538
|
+
});
|
|
539
|
+
|
|
498
540
|
void test("keeps the current account when in-place activation fails", async () => {
|
|
499
541
|
const notices: string[] = [];
|
|
500
542
|
let renderRequested = false;
|
|
@@ -12,10 +12,9 @@ function sameProfile(left: string | undefined, right: string | undefined): boole
|
|
|
12
12
|
return left?.toLowerCase() === right?.toLowerCase();
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export type AccountContext = Pick<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
> & {
|
|
15
|
+
export type AccountContext = Pick<ExtensionContext, "isIdle"> & {
|
|
16
|
+
/** Only slash-command contexts can wait; base shortcut contexts must already be idle. */
|
|
17
|
+
waitForIdle?: ExtensionCommandContext["waitForIdle"];
|
|
19
18
|
shutdown?: ExtensionCommandContext["shutdown"];
|
|
20
19
|
sessionManager?: Pick<
|
|
21
20
|
ExtensionCommandContext["sessionManager"],
|
|
@@ -128,7 +127,14 @@ export async function switchAccountInPlace(
|
|
|
128
127
|
);
|
|
129
128
|
return false;
|
|
130
129
|
}
|
|
131
|
-
if (!ctx.isIdle()) await ctx.waitForIdle();
|
|
130
|
+
if (!ctx.isIdle() && ctx.waitForIdle) await ctx.waitForIdle();
|
|
131
|
+
if (!ctx.isIdle()) {
|
|
132
|
+
ctx.ui.notify(
|
|
133
|
+
"Account changes require Pi to be idle. Wait for active work to finish and retry.",
|
|
134
|
+
"warning",
|
|
135
|
+
);
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
132
138
|
try {
|
|
133
139
|
await activate(profile);
|
|
134
140
|
} catch {
|
|
@@ -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. 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 {
|
|
@@ -138,6 +138,87 @@ void test("Bound Account reuses idle switching, quota refresh, and authoritative
|
|
|
138
138
|
}
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
+
void test("Bound base-context Account mutations refuse busy Pi and work when idle", async () => {
|
|
142
|
+
const previous = process.env.OSDY_PI_PROFILE_NAME;
|
|
143
|
+
try {
|
|
144
|
+
for (const idle of [false, true]) {
|
|
145
|
+
process.env.OSDY_PI_PROFILE_NAME = "personal";
|
|
146
|
+
const events: string[] = [];
|
|
147
|
+
const commands: string[][] = [];
|
|
148
|
+
const ctx = { isIdle: () => idle, ui: {
|
|
149
|
+
notify: (text: string) => events.push(text),
|
|
150
|
+
select: () => Promise.reject(new Error("dialog forbidden")), input: () => Promise.reject(new Error("dialog forbidden")),
|
|
151
|
+
} };
|
|
152
|
+
assert.equal("waitForIdle" in ctx, false);
|
|
153
|
+
const service = bindControlCenterAccount(ctx,
|
|
154
|
+
() => { events.push("refresh"); return Promise.resolve(); }, () => { events.push("render"); },
|
|
155
|
+
{ profiles: () => Promise.resolve(["work", "personal"]),
|
|
156
|
+
activate: name => { events.push(`activate:${name}`); return Promise.resolve(); },
|
|
157
|
+
run: args => { commands.push(args); return Promise.resolve({ code: 0, stdout: "No default account.", stderr: "" }); } });
|
|
158
|
+
assert.equal((await service.apply({ kind: "account-switch", profile: "work" })).failed, !idle);
|
|
159
|
+
assert.equal((await service.apply({ kind: "account-default", profile: "work" })).failed, !idle);
|
|
160
|
+
assert.equal((await service.apply({ kind: "account-default", profile: undefined })).failed, !idle);
|
|
161
|
+
assert.deepEqual(commands, idle ? [["account", "default", "work"], ["account", "default", "--clear"]] : []);
|
|
162
|
+
assert.equal(events.includes("activate:work"), idle);
|
|
163
|
+
assert.equal(events.includes("render"), idle);
|
|
164
|
+
assert.equal(events.includes("refresh"), idle);
|
|
165
|
+
assert.equal(process.env.OSDY_PI_PROFILE_NAME, idle ? "work" : "personal");
|
|
166
|
+
}
|
|
167
|
+
} finally {
|
|
168
|
+
if (previous === undefined) delete process.env.OSDY_PI_PROFILE_NAME;
|
|
169
|
+
else process.env.OSDY_PI_PROFILE_NAME = previous;
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
void test("Bound command Account defaults wait for idle and recheck idle and runtime freshness", async () => {
|
|
174
|
+
for (const outcome of ["idle", "busy", "stale", "rejected"] as const) {
|
|
175
|
+
let idle = false;
|
|
176
|
+
let current = true;
|
|
177
|
+
const events: string[] = [];
|
|
178
|
+
const service = bindControlCenterAccount({ isIdle: () => idle,
|
|
179
|
+
waitForIdle: () => {
|
|
180
|
+
events.push("wait");
|
|
181
|
+
idle = outcome !== "busy";
|
|
182
|
+
current = outcome !== "stale";
|
|
183
|
+
return outcome === "rejected" ? Promise.reject(new Error("wait failed")) : Promise.resolve();
|
|
184
|
+
},
|
|
185
|
+
ui: { notify: () => {}, select: () => Promise.resolve(undefined), input: () => Promise.resolve(undefined) },
|
|
186
|
+
}, () => { throw new Error("refresh forbidden"); }, () => {},
|
|
187
|
+
{ profiles: () => Promise.resolve(["work"]), run: args => {
|
|
188
|
+
events.push(args.join(" ")); return Promise.resolve({ code: 0, stdout: "", stderr: "" });
|
|
189
|
+
} }, { isCurrent: () => current });
|
|
190
|
+
assert.equal((await service.apply({ kind: "account-default", profile: "work" })).failed, outcome !== "idle");
|
|
191
|
+
assert.deepEqual(events, outcome === "idle" ? ["wait", "account default work"] : ["wait"]);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
void test("Bound base-context Account rechecks idle after asynchronous profile validation", async () => {
|
|
196
|
+
const previous = process.env.OSDY_PI_PROFILE_NAME;
|
|
197
|
+
process.env.OSDY_PI_PROFILE_NAME = "personal";
|
|
198
|
+
let idle = true;
|
|
199
|
+
let finish: (profiles: string[]) => void = () => {};
|
|
200
|
+
const events: string[] = [];
|
|
201
|
+
const service = bindControlCenterAccount({ isIdle: () => idle,
|
|
202
|
+
ui: { notify: () => {}, select: () => Promise.resolve(undefined), input: () => Promise.resolve(undefined) },
|
|
203
|
+
}, () => { events.push("refresh"); return Promise.resolve(); }, () => { events.push("render"); },
|
|
204
|
+
{ profiles: () => new Promise(resolve => { finish = resolve; }),
|
|
205
|
+
activate: () => { events.push("activate"); return Promise.resolve(); },
|
|
206
|
+
run: () => { events.push("default"); return Promise.resolve({ code: 0, stdout: "", stderr: "" }); } });
|
|
207
|
+
try {
|
|
208
|
+
for (const kind of ["account-switch", "account-default"] as const) {
|
|
209
|
+
idle = true;
|
|
210
|
+
const applying = service.apply({ kind, profile: "work" });
|
|
211
|
+
idle = false;
|
|
212
|
+
finish(["work"]);
|
|
213
|
+
assert.equal((await applying).failed, true);
|
|
214
|
+
}
|
|
215
|
+
assert.deepEqual(events, []);
|
|
216
|
+
} finally {
|
|
217
|
+
if (previous === undefined) delete process.env.OSDY_PI_PROFILE_NAME;
|
|
218
|
+
else process.env.OSDY_PI_PROFILE_NAME = previous;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
141
222
|
void test("Bound Account validates live external active names without rendering unsafe metadata", async () => {
|
|
142
223
|
const previous = process.env.OSDY_PI_PROFILE_NAME;
|
|
143
224
|
const service = bindControlCenterAccount({ isIdle: () => true, waitForIdle: () => Promise.resolve(),
|
|
@@ -136,7 +136,7 @@ export function bindControlCenterAccount(ctx: AccountContext, refreshUsage: () =
|
|
|
136
136
|
await backend.activate!(name);
|
|
137
137
|
}, refreshUsage, requestRender) : Promise.resolve(false),
|
|
138
138
|
setDefault: async profile => {
|
|
139
|
-
if (!ctx.isIdle()) await ctx.waitForIdle();
|
|
139
|
+
if (!ctx.isIdle() && ctx.waitForIdle) await ctx.waitForIdle();
|
|
140
140
|
if (!(options.isCurrent?.() ?? true) || !ctx.isIdle()) return false;
|
|
141
141
|
return (await backend.run(["account", "default", profile ?? "--clear"])).code === 0;
|
|
142
142
|
},
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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", "
|
|
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,
|