@bismawy/pi-arnative 0.3.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +52 -46
- package/extensions/animations.ts +213 -0
- package/extensions/arnative.ts +410 -98
- package/extensions/footer.ts +334 -49
- package/extensions/section-headers.ts +11 -6
- package/extensions/timestamps.ts +1 -1
- package/extensions/tools.ts +19 -2
- package/extensions/ui-render-tweaks.ts +2 -1
- package/extensions/usage.ts +468 -78
- package/lib/ansi.ts +50 -0
- package/lib/box.ts +90 -5
- package/lib/shortcuts.ts +62 -13
- package/lib/spinner.ts +270 -0
- package/lib/usage-store.ts +96 -4
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-10-12
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `/arnative` → **Animations** picker and working-spinner presets (`lib/spinner.ts`, `extensions/animations.ts`): Diamond Glow (default), Diamond Pulse, Circle Pulse, Circle Wave, Shade Wave, Braille Column, Braille Level, Kanji Count, Suzhou Tally, Pi (system), Gemini (Google brand gradient), and Claude (pulsing mark loop).
|
|
7
|
+
- Automatic token cost estimation for unpriced models via pi's `models.json` declared rates (`lib/usage-store.ts`, `extensions/usage.ts`).
|
|
8
|
+
- Usage dashboard integration into `/arnative` → **Usage** alongside standalone `/usage` command (`extensions/arnative.ts`, `extensions/usage.ts`).
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Active session clock tracks duration while active and freezes after inactivity (`IDLE_PAUSE_MS`), ignoring non-keystroke inputs (`extensions/footer.ts`, `test/footer-minimal.test.ts`).
|
|
12
|
+
- Working spinner frames clear `renderIndicatorVerbatim` to apply theme `accent` color while preserving `accentSoft` for labels (`extensions/footer.ts`, `extensions/animations.ts`).
|
|
13
|
+
- Unified screen chrome and title format via `renderScreen` and `screenTitle` in `lib/box.ts`, with adaptive thinking-level border colors across all `/arnative` screens (`lib/box.ts`, `lib/ansi.ts`, `extensions/arnative.ts`, `extensions/usage.ts`).
|
|
14
|
+
- Back key `←` (`matchesBack`) standard across `/arnative` submenus and usage dashboard, stepping back without losing selection context (`lib/shortcuts.ts`, `extensions/arnative.ts`, `extensions/usage.ts`).
|
|
15
|
+
- Compact usage dashboard layout: removed `↑In`/`↓Out`/`Cache` columns from main table (moved to selected row detail line) and added mouse click toggle for provider rows (`extensions/usage.ts`).
|
|
16
|
+
- Aligned list picker layout to standard `Label · note` format, removing redundant `●` indicator column (`extensions/arnative.ts`).
|
|
17
|
+
- Header brand updated to `Arnative⠙ vX.Y.Z` using shared `BRAND` constant (`extensions/section-headers.ts`).
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Fixed spinner preset selection in `/arnative` → **Animations** by sharing active UI and theme references across extension module instances via `globalThis` (`extensions/animations.ts`).
|
|
21
|
+
- Fixed missing `animations.ts` in `scripts/preview.mjs` extension launch list (`scripts/preview.mjs`).
|
|
22
|
+
- Fixed box row truncation in `boxRow` to prevent right wall clipping on lines narrower than content (`lib/box.ts`).
|
|
23
|
+
- Fixed `boxRow` zero-column overflow when width is below 4 columns (`lib/box.ts`).
|
|
24
|
+
- Avoided repeated synchronous disk reads during usage summary collection by passing memoized price table (`lib/usage-store.ts`).
|
|
25
|
+
- Cleaned up active clock state initialization on fresh sessions (`extensions/footer.ts`).
|
|
26
|
+
- Bounded tool duration cache (`TIMINGS`) to 200 entries with FIFO eviction and cleared on session boundaries to prevent memory growth (`extensions/tools.ts`).
|
|
27
|
+
- Added short-circuit in `repairBubbleBg` to avoid string split/join allocations on lines without ANSI reset sequences (`extensions/timestamps.ts`).
|
|
28
|
+
- Added fast substring check in `shouldHideReloadBox` to bypass `stripAnsi` and array mapping across general containers (`extensions/ui-render-tweaks.ts`).
|
|
29
|
+
|
|
30
|
+
## [0.3.10] - 2026-10-11
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- The ponytail icon rendered as a blank cell on Windows. The status drew `U+EF51` (`fa-circle_radiation`), a codepoint the Windows JetBrainsMono Nerd Font build lists in its cmap but rasterizes with no outline, so the footer left an empty gap before `ponytail:`. It now draws `U+EF04` (`fa-horse`), which renders on both platforms. The cell also spends one space after the icon instead of two, matching `MCP:`. A self-check pins the codepoint, so an edit back to an invisible glyph fails `bun run test` instead of shipping silently (`extensions/footer.ts`).
|
|
34
|
+
|
|
3
35
|
## [0.3.9] - 2026-10-08
|
|
4
36
|
|
|
5
37
|
### Fixed
|
package/README.md
CHANGED
|
@@ -6,21 +6,35 @@ Refined aesthetics. Cohesive tools. Built for Pi.
|
|
|
6
6
|
[](https://www.npmjs.com/package/@bismawy/pi-arnative)
|
|
7
7
|
[](https://github.com/bismawy/pi-arnative)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
<img src="https://raw.githubusercontent.com/bismawy/pi-arnative/main/assets/pi-arnative.webp" alt="pi-arnative" width="100%">
|
|
10
10
|
|
|
11
11
|
## Overview
|
|
12
12
|
|
|
13
13
|
pi-arnative replaces pi’s default terminal chrome with a unified, cohesive visual aesthetic.
|
|
14
14
|
|
|
15
|
-
- Warm Themes: Cohesive, contrast-checked color palettes generated without drift.
|
|
16
|
-
- Tabbed Header:
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
- Warm Themes: Cohesive, contrast-checked OKLCH color palettes generated without drift.
|
|
16
|
+
- Tabbed Header: Compact box menu showing Directory, Model, Context, Skills, Extensions, and Shortcuts.
|
|
17
|
+
- Rounded Tool Boxes: Status, spinner, and duration for every tool call with expandable outputs.
|
|
18
|
+
- Enhanced Footer: 3-row status grid tracking branch, active duration (30s idle freeze), and token speed.
|
|
19
|
+
- Transcript Clock: Timestamps for conversation messages with clean bubble backgrounds.
|
|
20
|
+
- Usage Dashboard: Real-time token counts and cost tracking across all local sessions via `/usage`.
|
|
21
|
+
|
|
22
|
+
<details>
|
|
23
|
+
<summary><b>Configuration & Presets</b></summary>
|
|
24
|
+
|
|
25
|
+
### Working Spinner Presets
|
|
26
|
+
Configurable via `/arnative` → **Animations** (saved to `arnative-config.json`):
|
|
27
|
+
- `Diamond Glow` (default), `Diamond Pulse`, `Circle Pulse`, `Circle Wave`, `Shade Wave`
|
|
28
|
+
- `Braille Column`, `Braille Level`, `Kanji Count`, `Suzhou Tally`
|
|
29
|
+
- `Pi (system)`, `Gemini` (Google gradient sweep), and `Claude` (pulsing marks)
|
|
30
|
+
|
|
31
|
+
### Footer Presets
|
|
32
|
+
Configurable via `/arnative` → **Footers**:
|
|
33
|
+
- `Arnative (Full)` (default 3-row grid)
|
|
34
|
+
- `Arnative (Minimal)` (single collapsible head row; click or `ctrl+alt+m` to toggle)
|
|
35
|
+
- `Pi (system)` (built-in footer)
|
|
36
|
+
|
|
37
|
+
</details>
|
|
24
38
|
|
|
25
39
|
## Install
|
|
26
40
|
|
|
@@ -28,13 +42,12 @@ pi-arnative replaces pi’s default terminal chrome with a unified, cohesive vis
|
|
|
28
42
|
pi install npm:@bismawy/pi-arnative
|
|
29
43
|
```
|
|
30
44
|
|
|
31
|
-
|
|
45
|
+
Apply a theme via `/settings` → **Theme** → `arnative` (or variant). Requires a [Nerd Font](https://www.nerdfonts.com/font-downloads).
|
|
32
46
|
|
|
33
|
-
To test locally
|
|
47
|
+
To test locally:
|
|
34
48
|
```bash
|
|
35
49
|
pi --extension ./extensions/footer.ts
|
|
36
50
|
```
|
|
37
|
-
> **Prerequisite:** Requires a [Nerd Font](https://www.nerdfonts.com/font-downloads) (e.g. JetBrains Mono Nerd Font) for icons and rounded box borders.
|
|
38
51
|
|
|
39
52
|
## Shortcuts
|
|
40
53
|
|
|
@@ -42,75 +55,68 @@ pi --extension ./extensions/footer.ts
|
|
|
42
55
|
| --- | --- |
|
|
43
56
|
| `ctrl+alt+t` · `alt+t` | Cycle header tabs |
|
|
44
57
|
| `ctrl+alt+r` · `alt+r` | Quick reload (`/reload`) |
|
|
45
|
-
| `ctrl+alt+n` · `alt+n` | New session (`/new`)
|
|
46
|
-
| `ctrl+alt+m` · `alt+m` |
|
|
47
|
-
| `/usage` |
|
|
48
|
-
| `/arnative` | Settings menu
|
|
49
|
-
| Click tab | Open selected header tab |
|
|
58
|
+
| `ctrl+alt+n` · `alt+n` | New session (`/new`) |
|
|
59
|
+
| `ctrl+alt+m` · `alt+m` | Toggle Minimal footer |
|
|
60
|
+
| `/usage` | Token & cost dashboard (or `/arnative` → **Usage**) |
|
|
61
|
+
| `/arnative` | Settings menu (Themes, Headers, Footers, Animations, Usage) |
|
|
50
62
|
| Click tool / `ctrl+o` | Toggle tool output box |
|
|
63
|
+
| `←` | Back — step back from submenus/dashboard to menu |
|
|
51
64
|
|
|
52
|
-
>
|
|
53
|
-
>
|
|
54
|
-
> - **macOS:** uses `ctrl+option+…` (plain Option composes special characters).
|
|
55
|
-
> - **Herdr / tmux glitch:** if shortcuts feel delayed or type stray letters, set `PI_TUI_ESC_TIMEOUT=100` in your shell profile.
|
|
56
|
-
|
|
57
|
-
### Keybindings
|
|
58
|
-
|
|
59
|
-
Add to `~/.pi/agent/keybindings.json` to bind `app.session.new`:
|
|
65
|
+
<details>
|
|
66
|
+
<summary><b>Keybindings & Platform Notes</b></summary>
|
|
60
67
|
|
|
68
|
+
To bind `app.session.new` in `~/.pi/agent/keybindings.json`:
|
|
61
69
|
```json
|
|
62
70
|
{
|
|
63
71
|
"app.session.new": ["ctrl+alt+n", "alt+n"]
|
|
64
72
|
}
|
|
65
73
|
```
|
|
66
74
|
|
|
75
|
+
- **Windows / WSL:** Windows Terminal aliases `Ctrl+Alt` to AltGr; use `alt+…` (or disable `altGrAliasing` in WT profile).
|
|
76
|
+
- **macOS:** Uses `ctrl+option+…`.
|
|
77
|
+
- **tmux / Herdr:** If Esc has delay, set `PI_TUI_ESC_TIMEOUT=100` in your environment.
|
|
78
|
+
- **Navigation:** `←` navigates back to parent menus; `Esc` cancels or closes the active screen.
|
|
79
|
+
|
|
80
|
+
</details>
|
|
81
|
+
|
|
67
82
|
## Architecture
|
|
68
83
|
|
|
69
84
|
<details>
|
|
70
|
-
<summary><b>Extensions</b></summary>
|
|
85
|
+
<summary><b>Extensions & Components</b></summary>
|
|
71
86
|
|
|
72
87
|
| File | Role |
|
|
73
88
|
| --- | --- |
|
|
74
89
|
| `extensions/tools.ts` | Rounded tool boxes, spinners, and duration |
|
|
75
90
|
| `extensions/section-headers.ts` | Tabbed header and resource box (preset: `/arnative headers`) |
|
|
76
91
|
| `extensions/footer.ts` | 3-row status grid footer and boxed editor (preset: `/arnative footers`) |
|
|
77
|
-
| `extensions/
|
|
78
|
-
| `
|
|
92
|
+
| `extensions/animations.ts` | Working spinner presets and `/arnative` → Animations picker |
|
|
93
|
+
| `lib/spinner.ts` | Spinner preset catalog: frames, cadence, and painters |
|
|
94
|
+
| `extensions/arnative.ts` | Settings menu with live preview and navigation |
|
|
95
|
+
| `extensions/timestamps.ts` | Message timestamps and bubble backgrounds |
|
|
79
96
|
| `extensions/ui-render-tweaks.ts` | Contrast tweaks, selection style, and UI polish |
|
|
80
|
-
| `extensions/usage.ts` | `/usage`
|
|
97
|
+
| `extensions/usage.ts` | Token and cost dashboard (`/usage`) |
|
|
81
98
|
|
|
82
99
|
</details>
|
|
83
100
|
|
|
84
101
|
<details>
|
|
85
|
-
<summary><b>
|
|
102
|
+
<summary><b>Theme Variants</b></summary>
|
|
86
103
|
|
|
87
|
-
|
|
88
|
-
- Each theme is a few parameters (accent hue, chroma, canvas lightness, neutral tint) plus optional hue overrides for the strong palettes; all colors are derived as OKLCH from a shared ramp, so lightness/saturation stay consistent and no two `vars` collapse to the same value.
|
|
89
|
-
- `lib/color.ts` holds the OKLCH↔sRGB math and WCAG contrast used by both the generator and the self-check.
|
|
104
|
+
Theme definitions are generated via `themes/gen-themes.mjs` using OKLCH color math (`lib/color.ts`).
|
|
90
105
|
|
|
91
106
|
**Variants:** `sun` · `zinc` · `violet` · `emerald` · `matrix` · `cyberpunk` · `synthwave` · `gruvbox` · `nord` · `dracula`
|
|
92
107
|
|
|
93
108
|
</details>
|
|
94
109
|
|
|
95
110
|
<details>
|
|
96
|
-
<summary><b>Development</b></summary>
|
|
111
|
+
<summary><b>Development & Verification</b></summary>
|
|
97
112
|
|
|
98
113
|
```bash
|
|
99
|
-
npm test # Run self-checks
|
|
114
|
+
npm test # Run unit tests and self-checks
|
|
100
115
|
npm run themes # Rebuild all theme variants
|
|
101
116
|
```
|
|
102
117
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
**Releasing:** `scripts/release-check.mjs` is a read-only preflight (never writes, tags, pushes or publishes) with one stage per step of the release:
|
|
106
|
-
|
|
107
|
-
| Stage | Run before | Checks |
|
|
108
|
-
| :--- | :--- | :--- |
|
|
109
|
-
| `pre-tag` | `git tag` | clean tree on `main`, in sync, no open PRs, `CHANGELOG.md` entry, tag free, version unpublished |
|
|
110
|
-
| `pre-publish` | `npm publish` | the above, plus the tag exists **and is pushed**, and points at `HEAD` |
|
|
111
|
-
| `post-publish` | done | the above, plus the version is live on the registry, `latest` points at it and its tarball URL answers |
|
|
118
|
+
Pre-publish verification and tag validation are enforced via `scripts/release-check.mjs`.
|
|
112
119
|
|
|
113
|
-
`pre-publish` runs automatically through npm's `prepublishOnly` (after `npm test`), so a red build or a missing/unpushed tag aborts the publish (`npm error code 1`) instead of shipping a version whose tag was forgotten. The open-PR check uses `gh` when present and falls back to the public GitHub API otherwise. `npm run release:check` is the `pre-tag` shorthand. Exit codes: `0` pass, `1` a check failed, `2` bad usage.
|
|
114
120
|
</details>
|
|
115
121
|
|
|
116
122
|
## License
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animations — the working-indicator preset, owned outside the footer because it is not a
|
|
3
|
+
* footer concern: pi's loader spins in the boxed editor's top border and in the in-place
|
|
4
|
+
* status row whether or not an arnative footer is installed.
|
|
5
|
+
*
|
|
6
|
+
* `/arnative` → **Animations** picks a preset; this module installs it, repaints the live
|
|
7
|
+
* spinner on the spot, and hands the slot back to pi for `Pi (system)`. The frames, their
|
|
8
|
+
* cadence and their painter live in `lib/spinner.ts`; the picker UI lives here.
|
|
9
|
+
*
|
|
10
|
+
* The Loader's colour hooks are not reachable through pi's public indicator options, so this
|
|
11
|
+
* module reaches the live editor the footer publishes under `EDITOR_KEY`. That is the one
|
|
12
|
+
* coupling between the two, and it is a published symbol, not an import.
|
|
13
|
+
*/
|
|
14
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { loadChoice, saveChoice } from "../lib/preset-store.ts";
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_SPINNER_PRESET,
|
|
18
|
+
SPINNER_PRESETS,
|
|
19
|
+
SPINNER_PRESET_DEFS,
|
|
20
|
+
SPINNER_PRESET_KEY,
|
|
21
|
+
previewFrames,
|
|
22
|
+
spinnerPainter,
|
|
23
|
+
type SpinnerPreset,
|
|
24
|
+
} from "../lib/spinner.ts";
|
|
25
|
+
import { assert, isMain } from "../lib/check.ts";
|
|
26
|
+
|
|
27
|
+
/** The row divider, matching the one `arnative.ts` draws (`Label · note`). Kept local so
|
|
28
|
+
* this module does not reach into the picker just for a separator string. */
|
|
29
|
+
const ROW_DIVIDER = " · ";
|
|
30
|
+
|
|
31
|
+
/** The arnative editor instance, published by `footer.ts` so the live spinner is reachable. */
|
|
32
|
+
export const EDITOR_KEY = Symbol.for("pi-arnative.editor");
|
|
33
|
+
const SPINNER_PRESET_CONFIG_KEY = Symbol.for("pi-arnative.spinnerPreset");
|
|
34
|
+
|
|
35
|
+
/** The `setWorkingMessage`/`setWorkingIndicator` surface of `ctx.ui`. */
|
|
36
|
+
type WorkingUi = { setWorkingIndicator?: (options?: unknown) => void } | null | undefined;
|
|
37
|
+
|
|
38
|
+
/** The live theme, for the accent presets. A gradient preset ignores it. */
|
|
39
|
+
type ThemeLike = { fg?(color: string, text: string): string } | null;
|
|
40
|
+
|
|
41
|
+
// Active UI and theme are kept on globalThis so choices apply across reloads and multi-instance loads.
|
|
42
|
+
const UI_KEY = Symbol.for("pi-arnative.workingUi");
|
|
43
|
+
const THEME_KEY = Symbol.for("pi-arnative.workingTheme");
|
|
44
|
+
|
|
45
|
+
/** Record the session's `ctx.ui`, so a later preset pick can install through it. */
|
|
46
|
+
function workingUi(ui: WorkingUi): void {
|
|
47
|
+
(globalThis as Record<symbol, unknown>)[UI_KEY] = ui ?? undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The `ctx.ui` recorded at `session_start`, or null before one ran. */
|
|
51
|
+
function activeWorkingUi(): WorkingUi {
|
|
52
|
+
return ((globalThis as Record<symbol, unknown>)[UI_KEY] as WorkingUi) ?? null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Record the session's theme, so accent presets follow it instead of a stale colour. */
|
|
56
|
+
function workingTheme(theme: ThemeLike): void {
|
|
57
|
+
(globalThis as Record<symbol, unknown>)[THEME_KEY] = theme ?? undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The theme recorded at `session_start`; a gradient preset ignores it. */
|
|
61
|
+
function activeWorkingTheme(): ThemeLike {
|
|
62
|
+
return ((globalThis as Record<symbol, unknown>)[THEME_KEY] as ThemeLike) ?? null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function activeSpinnerPreset(): SpinnerPreset {
|
|
66
|
+
const stored =
|
|
67
|
+
(globalThis as Record<symbol, unknown>)[SPINNER_PRESET_CONFIG_KEY] ??
|
|
68
|
+
loadChoice(SPINNER_PRESET_KEY, SPINNER_PRESETS);
|
|
69
|
+
return SPINNER_PRESETS.includes(stored as SpinnerPreset) ? (stored as SpinnerPreset) : DEFAULT_SPINNER_PRESET;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Publish the choice for this process, write it to `arnative-config.json`, and install it on
|
|
74
|
+
* the running session right away — the picker previews by moving the cursor, so a choice that
|
|
75
|
+
* only took effect on the next `session_start` would look like the selection did nothing.
|
|
76
|
+
*/
|
|
77
|
+
export function setSpinnerPreset(preset: SpinnerPreset): void {
|
|
78
|
+
(globalThis as Record<symbol, unknown>)[SPINNER_PRESET_CONFIG_KEY] = preset;
|
|
79
|
+
saveChoice(SPINNER_PRESET_KEY, preset);
|
|
80
|
+
applySpinnerPreset(activeWorkingUi(), activeWorkingTheme());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Updates custom painter on live workingStatusIndicator if available. */
|
|
84
|
+
export function paintLiveIndicator(painter: (text: string) => string): void {
|
|
85
|
+
const editor = (globalThis as Record<symbol, any>)[EDITOR_KEY];
|
|
86
|
+
const indicator = editor?.workingStatusIndicator;
|
|
87
|
+
if (!indicator) return;
|
|
88
|
+
indicator.renderIndicatorVerbatim = false;
|
|
89
|
+
indicator.spinnerColorFn = painter;
|
|
90
|
+
indicator.updateDisplay?.();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Applies preset to working indicator or restores system default for 'Pi (system)'. */
|
|
94
|
+
export function applySpinnerPreset(ui: WorkingUi, theme: ThemeLike): void {
|
|
95
|
+
const preset = activeSpinnerPreset();
|
|
96
|
+
const def = SPINNER_PRESET_DEFS[preset];
|
|
97
|
+
const painter = spinnerPainter(preset, theme);
|
|
98
|
+
try {
|
|
99
|
+
if (def.frames.length === 0) ui?.setWorkingIndicator?.();
|
|
100
|
+
else ui?.setWorkingIndicator?.({ frames: [...def.frames], intervalMs: def.intervalMs });
|
|
101
|
+
} catch {
|
|
102
|
+
// older host without the API -> pi's own frames stay
|
|
103
|
+
}
|
|
104
|
+
paintLiveIndicator(painter);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Picker text. The description states the one seam: a preset applies to the next spinner. */
|
|
108
|
+
export const ANIMATIONS_PICKER_TEXT = {
|
|
109
|
+
title: "Animations - Arnative\u2819",
|
|
110
|
+
description: "The working spinner — applies on the next tool call.",
|
|
111
|
+
legend: "[↑↓] Preview [Enter] Select [Esc] Close",
|
|
112
|
+
} as const;
|
|
113
|
+
|
|
114
|
+
/** One row per preset: the name, then its note and the frames it actually cycles. The
|
|
115
|
+
* frames belong in the note — a user picking an animation wants to see the glyphs, and this
|
|
116
|
+
* is the screen where that is decided. */
|
|
117
|
+
export function animationRows(): Array<{ label: string; note: string }> {
|
|
118
|
+
return SPINNER_PRESETS.map((preset) => ({
|
|
119
|
+
label: preset,
|
|
120
|
+
note: `${SPINNER_PRESET_DEFS[preset].note} ${previewFrames(preset)}`,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The row as one flat string, for the headless fallback and the self-check. */
|
|
125
|
+
export function animationRowText(preset: SpinnerPreset): string {
|
|
126
|
+
return `${preset}${ROW_DIVIDER}${SPINNER_PRESET_DEFS[preset].note} ${previewFrames(preset)}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Register the extension: keep the installed preset in step with the session's theme. */
|
|
130
|
+
export default function registerAnimations(pi: ExtensionAPI): void {
|
|
131
|
+
pi.on("session_start", async (_event, ctx: ExtensionContext) => {
|
|
132
|
+
// A preset is installed per session so it survives `/reload` and a theme switch
|
|
133
|
+
// repaints the accent presets without re-selecting anything. Publish the surfaces first:
|
|
134
|
+
// `/arnative` → Animations re-applies through them the moment a row is chosen.
|
|
135
|
+
workingUi(ctx.ui);
|
|
136
|
+
workingTheme((ctx.ui as unknown as { theme?: ThemeLike })?.theme ?? null);
|
|
137
|
+
applySpinnerPreset(activeWorkingUi(), activeWorkingTheme());
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Self-check: `node extensions/animations.ts`. Async because the cross-instance assertion
|
|
142
|
+
// imports a second copy of this module.
|
|
143
|
+
if (isMain(import.meta.url)) {
|
|
144
|
+
await (async () => {
|
|
145
|
+
// The rows describe exactly the presets, and every row carries its own note.
|
|
146
|
+
const rows = animationRows();
|
|
147
|
+
assert(rows.length === SPINNER_PRESETS.length, "one row per preset");
|
|
148
|
+
assert(
|
|
149
|
+
rows.every((r) => r.note.length > 0),
|
|
150
|
+
"every row has a note",
|
|
151
|
+
);
|
|
152
|
+
assert(animationRowText("Diamond Glow").includes("\u2b25"), "the row text previews the frames");
|
|
153
|
+
|
|
154
|
+
// Installing a preset goes through pi's indicator API; `Pi (system)` passes nothing.
|
|
155
|
+
const calls: Array<Record<string, unknown> | undefined> = [];
|
|
156
|
+
const ui = { setWorkingIndicator: (options?: Record<string, unknown>) => calls.push(options) };
|
|
157
|
+
(globalThis as Record<symbol, unknown>)[SPINNER_PRESET_CONFIG_KEY] = "Diamond Glow";
|
|
158
|
+
applySpinnerPreset(ui, null);
|
|
159
|
+
assert(JSON.stringify(calls[0]?.frames) === JSON.stringify(SPINNER_PRESET_DEFS["Diamond Glow"].frames), "frames installed");
|
|
160
|
+
assert(calls[0]?.intervalMs === 200, "interval installed");
|
|
161
|
+
(globalThis as Record<symbol, unknown>)[SPINNER_PRESET_CONFIG_KEY] = "Pi (system)";
|
|
162
|
+
applySpinnerPreset(ui, null);
|
|
163
|
+
assert(calls.at(-1) === undefined, "Pi (system) passes no options -> pi restores its own spinner");
|
|
164
|
+
|
|
165
|
+
// A stored value that names no preset falls back instead of breaking the load.
|
|
166
|
+
(globalThis as Record<symbol, unknown>)[SPINNER_PRESET_CONFIG_KEY] = "Nope";
|
|
167
|
+
assert(activeSpinnerPreset() === DEFAULT_SPINNER_PRESET, "an unknown stored preset falls back to the default");
|
|
168
|
+
delete (globalThis as Record<symbol, unknown>)[SPINNER_PRESET_CONFIG_KEY];
|
|
169
|
+
|
|
170
|
+
// The painter seam: without a published editor there is nothing to paint, and that must
|
|
171
|
+
// be a no-op rather than a throw (the editor registers after the first session_start).
|
|
172
|
+
delete (globalThis as Record<symbol, unknown>)[EDITOR_KEY];
|
|
173
|
+
paintLiveIndicator((t) => t);
|
|
174
|
+
|
|
175
|
+
// Picking a preset must apply it NOW, not on the next session — and it must work when the
|
|
176
|
+
// picker calls a DIFFERENT module instance than the one that ran `session_start`. pi loads
|
|
177
|
+
// every manifest entry through its own loader while `arnative.ts` imports this module too,
|
|
178
|
+
// so there are two copies with separate module scope; only `globalThis` is shared. Keeping
|
|
179
|
+
// the surfaces on globalThis is what this pins, and a same-instance test would not catch it
|
|
180
|
+
// (the original bug passed such a test: the row moved, the spinner did not).
|
|
181
|
+
const pickCalls: Array<Record<string, unknown> | undefined> = [];
|
|
182
|
+
const pickUi = { setWorkingIndicator: (options?: Record<string, unknown>) => pickCalls.push(options) };
|
|
183
|
+
const handlers: Array<(e: unknown, c: unknown) => unknown> = [];
|
|
184
|
+
registerAnimations({ on: (_n: string, fn: (e: unknown, c: unknown) => unknown) => handlers.push(fn) } as never);
|
|
185
|
+
// Instance A runs session_start and records the ui on globalThis.
|
|
186
|
+
handlers[0]!({}, { ui: pickUi, theme: { fg: (_c: string, t: string) => t } });
|
|
187
|
+
pickCalls.length = 0;
|
|
188
|
+
// Instance B — a second copy of the module, like the one `arnative.ts` holds — does the pick.
|
|
189
|
+
const instanceB = await import(`./animations.ts?second=${Date.now()}`);
|
|
190
|
+
instanceB.setSpinnerPreset("Claude");
|
|
191
|
+
assert(pickCalls.length === 1, "a second module instance installs through the shared ui");
|
|
192
|
+
assert(pickCalls[0]?.frames?.[0] === "\u00b7", "the chosen preset's frames are installed, not the old ones");
|
|
193
|
+
assert(pickCalls[0]?.intervalMs === 120, "and its cadence (120ms)");
|
|
194
|
+
|
|
195
|
+
// Pi (system) still hands the slot back through the same path.
|
|
196
|
+
pickCalls.length = 0;
|
|
197
|
+
instanceB.setSpinnerPreset("Pi (system)");
|
|
198
|
+
assert(pickCalls[0] === undefined, "choosing Pi (system) restores pi's own spinner at once");
|
|
199
|
+
|
|
200
|
+
// With one, both the verbatim flag and the painter are set — this is what keeps the
|
|
201
|
+
// colour on a custom frame.
|
|
202
|
+
const indicator = { renderIndicatorVerbatim: true, spinnerColorFn: undefined as unknown, updateDisplay: () => {} };
|
|
203
|
+
let painted = 0;
|
|
204
|
+
const editor = { workingStatusIndicator: { ...indicator, updateDisplay: () => painted++ } };
|
|
205
|
+
(globalThis as Record<symbol, any>)[EDITOR_KEY] = editor;
|
|
206
|
+
paintLiveIndicator((t) => `<a>${t}`);
|
|
207
|
+
assert(editor.workingStatusIndicator.renderIndicatorVerbatim === false, "verbatim cleared, else pi skips the painter");
|
|
208
|
+
assert(painted === 1, "the live indicator repaints at once");
|
|
209
|
+
delete (globalThis as Record<symbol, any>)[EDITOR_KEY];
|
|
210
|
+
|
|
211
|
+
console.log("animations.ts self-check OK");
|
|
212
|
+
})();
|
|
213
|
+
}
|