osdy-pi 0.1.4 → 0.1.5

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
@@ -10,15 +10,23 @@ Visit the Osdy landing page: [landing-osdy.vercel.app](https://landing-osdy.verc
10
10
 
11
11
  - **Dark theme:** `osdy-pi-dark`, enabled by default when the package starts.
12
12
  - **Light theme:** `osdy-pi-light`, with the same Osdy palette adapted for light terminals.
13
- - **Custom header:** centered Osdy-Pi ASCII branding with responsive status metadata.
13
+ - **Simple theme:** `osdy-pi-simple`, a blue/red/slate console theme for the full Pi interface.
14
+ - **Custom header:** two selectable header styles with responsive status metadata: `osdy-theme` (default) and `classic`.
14
15
  - **Custom editor:** full-width framed input area with model, thinking, token, cost, and context status.
15
- - **Clean layout:** the built-in footer/working row is hidden while Osdy Pi is enabled to avoid duplicated UI.
16
+ - **Custom working indicator:** a dedicated working widget/spinner appears above the text box, outside the editor frame.
17
+ - **Clean layout:** the built-in working row is hidden while Osdy Pi is enabled to avoid duplicated UI.
16
18
 
17
19
  <img width="1280" height="433" alt="image" src="https://github.com/user-attachments/assets/20c7624d-9ad8-4494-97fb-6b6d81aaf328" />
18
20
 
19
21
  ## Install in Pi
20
22
 
21
- Install the package directly from GitHub:
23
+ Install the published package from npm:
24
+
25
+ ```bash
26
+ pi install npm:osdy-pi
27
+ ```
28
+
29
+ You can also install it directly from GitHub:
22
30
 
23
31
  ```bash
24
32
  pi install git:github.com/OsdyOrtiz/Osdy-Pi
@@ -45,6 +53,7 @@ Then select one of these theme names:
45
53
  ```text
46
54
  osdy-pi-dark
47
55
  osdy-pi-light
56
+ osdy-pi-simple
48
57
  ```
49
58
 
50
59
  You can also set it in your Pi `settings.json`:
@@ -55,7 +64,7 @@ You can also set it in your Pi `settings.json`:
55
64
  }
56
65
  ```
57
66
 
58
- Use `osdy-pi-light` instead if you prefer the light version.
67
+ Use `osdy-pi-light` if you prefer the light version, or `osdy-pi-simple` if you want the blue/red/slate palette across the whole console.
59
68
 
60
69
  ## Commands
61
70
 
@@ -65,11 +74,20 @@ Osdy Pi includes a small command group:
65
74
  /osdy-pi enable
66
75
  /osdy-pi disable
67
76
  /osdy-pi status
77
+ /osdy-pi osdy-theme
78
+ /osdy-pi classic
79
+ /osdy-pi-osdy-theme
80
+ /osdy-pi-classic
68
81
  ```
69
82
 
70
83
  - `enable` applies the dark Osdy theme, custom header, custom editor, and clean layout.
71
84
  - `disable` restores Pi's built-in header, editor, footer, and working visibility, then switches back to the previous theme or `dark`.
72
- - `status` shows whether the Osdy Pi UI is currently enabled.
85
+ - `status` shows whether the Osdy Pi UI is currently enabled, including the active style.
86
+ - `osdy-theme` is the default OsdyTheme header with pink, cyan, and purple styling, plus the mascot glow on the right edge.
87
+ - `classic` keeps the previous classic header shape with the shared mascot.
88
+ - `/osdy-pi-osdy-theme` and `/osdy-pi-classic` are direct aliases.
89
+
90
+ After changing a local extension, run `/reload` or restart Pi so the updated commands are registered.
73
91
 
74
92
  ## Local install
75
93
 
@@ -92,11 +110,24 @@ pi install /absolute/path/to/Osdy-Pi
92
110
  ```text
93
111
  themes/osdy-pi-dark.json
94
112
  themes/osdy-pi-light.json
113
+ themes/osdy-pi-simple.json
95
114
  extensions/osdy-pi.ts
115
+ extensions/osdy-pi/
96
116
  ```
97
117
 
118
+ `extensions/osdy-pi.ts` is the package entrypoint. The implementation lives in the modular `extensions/osdy-pi/` folder (runtime, UI, metrics, working controller, animation, border, and formatting helpers).
119
+
98
120
  The Pi manifest is declared in `package.json` through `pi.themes` and `pi.extensions`, so Pi can discover the themes and extension after installation.
99
121
 
122
+ ## Development
123
+
124
+ If you are working on the package locally, you can run:
125
+
126
+ ```bash
127
+ npm run typecheck
128
+ npm run lint
129
+ ```
130
+
100
131
  ## Uninstall or turn off
101
132
 
102
133
  To temporarily turn off the custom UI inside Pi:
@@ -1,5 +1,6 @@
1
1
  import { ANIMATION_ENABLED } from "./constants.js";
2
2
  import type { AnimationMode, SimpleTheme } from "./types.js";
3
+ import type { MascotTonePalette } from "./constants.js";
3
4
  import { positiveModulo } from "./utils.js";
4
5
 
5
6
  export type AsciiAnimationStyle = "static" | "animated";
@@ -57,3 +58,42 @@ export function animateAsciiLine(
57
58
  })
58
59
  .join("");
59
60
  }
61
+
62
+ export function animateAsciiLineWithToneMap(
63
+ line: string,
64
+ toneMap: string,
65
+ lineIndex: number,
66
+ frame: number,
67
+ theme: SimpleTheme,
68
+ palette: MascotTonePalette,
69
+ style: AsciiAnimationStyle,
70
+ ): string {
71
+ return Array.from(line)
72
+ .map((char, charIndex) => {
73
+ if (char === " ") return char;
74
+ const tone = toneMap[charIndex];
75
+ if (
76
+ tone !== "b" &&
77
+ tone !== "h" &&
78
+ tone !== "l" &&
79
+ tone !== "m" &&
80
+ tone !== "d" &&
81
+ tone !== "p" &&
82
+ tone !== "c" &&
83
+ tone !== "v"
84
+ ) {
85
+ return char;
86
+ }
87
+ const baseColor = palette[tone];
88
+ if (style === "static") return theme.fg(baseColor, char);
89
+ const wave = positiveModulo(charIndex + lineIndex * 2 - frame * 5, 44);
90
+ const highlightColor =
91
+ tone === "b" || tone === "h" || tone === "p" ? palette.h : palette.l;
92
+ const trailColor = tone === "d" ? palette.m : palette.b;
93
+ return theme.fg(
94
+ getAnimatedAsciiColor(wave, baseColor, highlightColor, trailColor),
95
+ char,
96
+ );
97
+ })
98
+ .join("");
99
+ }
@@ -1,4 +1,5 @@
1
1
  import { visibleWidth } from "@earendil-works/pi-tui";
2
+ import type { HeaderVariant } from "./types.js";
2
3
 
3
4
  export const THEME_NAME = "osdy-pi-dark";
4
5
  export const ANIMATION_ENABLED = true;
@@ -20,37 +21,83 @@ export const WORKING_WIDGET_KEY = "osdy-pi-working";
20
21
  export const MASCOT_MIN_ROWS = 34;
21
22
  export const MASCOT_GAP = 0;
22
23
 
23
- export const MASCOT = [
24
- " %% %%%@@ @@%%% @%%",
25
- " %= --- -=#% @@@@@@@@@%%%-- ----#%",
26
- " %- -*#@ =+**-------:-**- %%#*--#%",
27
- " %- -*#%*-----------------*%#*--#%",
28
- " %+-#*---------------------**-+%",
29
- " %#=--=- ==-------= ----=*%",
30
- " %+-- ***** ------ **** -=*%",
31
- " @%% ****%% %*=-----**% %****# #%%@",
32
- " @%# ******%%%%*=--==-**%%%%*****#--%",
33
- " %%#*******##*# %%%% **%#*******%%@",
34
- " %%%#****** %@%%@% ******#%@%",
35
- " %%%:: ::#%@ %%%%%",
36
- " %%%**::::::::::%%% %%#-****%%",
37
- " %#---: : :: ::-=+%%%%%=-::::+*#%",
38
- " %%----- : ::----*%%%*****-:::#%",
39
- " #*------ : ::-----**#+-:=+***=-*%",
40
- " %%%+*=+------ ::------+*=+%-:::-*##+%%",
41
- " %*---**-----* **----+*--=+%=---=*#%",
42
- " %**+==%*----*==%=----##==+*%#+--*%%",
43
- " %%#%#%@+=+++%%++++==%##%#%#%%%%",
44
- " ----=+%%%#%%#%*##%###*#%%##%%==---------",
45
- " ##****=*****==",
24
+ const HTML_MASCOT = [
25
+ " ▓▓▓▓▒ ",
26
+ " ░ ▓▓▓ ▒▓ ",
27
+ " ░░░▓▓▓ ▓▓ ",
28
+ " ▒ ▒ ▒▒ ▒ ░░ ▓▓▓▓ ░▒▓▓ ",
29
+ "▓▓▓▓▓▓▓▓░░░░░ ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ ▒▒ ░░▒▓ ",
30
+ "▓▓ ▓▓▓▓▓░░░ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░ ░░▒▒ ",
31
+ "▓▓░ ▒▓▓▓▒░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▒▒▒▒▒▒▒░░ ░▒▒ ",
32
+ " ▓▒░░ ▒░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓█▓▓█▓▓▓▓▓▒▒▒░▒▒░ ",
33
+ " ▓▒░░ ░▒▒▒▒▒▓▓▓▓▓▒█▒▒▒▒▒▓▓▓▓ ░░░ ▓▓▓▓▒▒▒░░ ",
34
+ " ▒▒░░ ░▒▒▒▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▓▓ ░ ▓ ▒▒░░ ",
35
+ " ▒▒▒▒░▒▒▓▓▓▓░ ░▓▒▒▒▒▒ ░▒▒█▓ ▒▓▓▓░▒ ",
36
+ " ░ ░▒▒▓▓▒ ▓░░ ░░ ░ ▓▓▓▒ ",
37
+ " ░░▒▓░ ░▒░▓▓ ░░ ░▓▓▒ ",
38
+ " ░▒ ▒ ░▒▒▒ ▒▒▒▒ ░ ▒▒▒ ",
39
+ " ░▒▓▓░ ▓▓░░░ █▓ ▒ ░▒▒▒ ",
40
+ " ▒▓▓▓░ ▓▓ ▓▓▓▓▓▓▓▓▓ ░░▒ ",
41
+ " ▓▓▓░ ░ ▓▓▓▓▓▓▒▓▓▓▓ ▓▓▓▓▓▓▓▒░░░░ ░ ",
42
+ " ▒▒▒▒ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒░░░░ ",
43
+ " ▓▒▒░░ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒░░ ▓ ",
44
+ " ░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒ ░░░ ",
45
+ " ░ ░░░░░░░░░░░▒▒░░ ░ ░ ░ ▓ ",
46
+ " ░░░░░▒▒▒▒░░▒░ ▒▒▒▒▒░░ ░░▒▒▒░░░░░░░░ ",
47
+ " ▒▒▒▒▒░░░▒▒▒▒▒▒▒▒▒▒▒▒░░░ ▒░░▒▒▒▒▓▓▓▓░░░░ ",
48
+ " ░▒▒▒▒▒░ ░░▒▒▒▒▒▒▒▒░░░ ░░▒▓▓▓▓▒▒░░ ",
49
+ " ▓░░░░░░ ░ ░░░░░░░░░░░ ░░░░░░▓▒▒▒▒ ░",
50
+ " ░░░▒▒▓▓▓▓▓▒░░░░░░░░░░ ░░▒▒▒▒░░░░░░░▒▒░░ ",
51
+ " ░▒▒▒▓▓▓▓▓▓▓▓▓▓▒▒▒▒░░░ ░▒▒▒▓▓▒░░░ ░░░▒ ",
52
+ " ▒░▒▒▒▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒░░░ ░▒▒▓▓▒░ ░ ",
53
+ " ░░▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒▒░░░░ ░▒▒▒▒▒ ░ ",
54
+ " ▒░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░ ▒▒░▒ ",
55
+ " ░░░░░░░░░░▒░░▒░▒▒░░░░ ░ ░░░░ ",
56
+ " ░░░░ ░░░░░░░░░░░ ░░░░ ",
57
+ " ░ ░░░░░░ ▓ ",
58
+ " ░░░░░ ░░░░░░ ",
59
+ " ░ ░ ",
46
60
  ] as const;
47
61
 
48
- export const MASCOT_WIDTH = MASCOT.reduce(
49
- (maxWidth, line) => Math.max(maxWidth, visibleWidth(line)),
50
- 0,
51
- );
62
+ const HTML_MASCOT_MAP = [
63
+ " hhhhl ",
64
+ " dmmhhhmmlhm ",
65
+ " dmmmhhhdmmmmhh ",
66
+ " ldldllddmdldmmmmhhlhddmmmlhh ",
67
+ "hhhhhhhhmmmlm llllllllllllllllllmlldddmmmll ",
68
+ "hhmmmmlhhhhmmmdllllllllllllllllllllllllmddmmmll ",
69
+ "hhmmmddlhhhlmlllllllllllllllllllhlllllllllmmlld ",
70
+ " llmmmdddlmllllllllllllllllllhbhhbhhhhhllllllm ",
71
+ " dllmmmddllllllhhhhhlblllllhhhhmmmmmmhhhhlllmm ",
72
+ " dllmmmlllllhhhhhhhhhhllllhhmmmmmmmmmddddhdlllm ",
73
+ " lllllllhhhhmmmmmmmhllllldmdlllbhmmmmddddlhhhlldd ",
74
+ " mdlllhhlmmdhmmmddmmdmmddmmmmdldddddddddmmmhhhl ",
75
+ " lllhmddmlmhhmmmmmddmmmddddddddddddddddmmdmmmhhl ",
76
+ " mlldldddmlllddddddddlllldmddddddddddddmmmddmlll ",
77
+ " llhhmddddmmdddddddddhhmmmdbhdldddddddddmmddmlll ",
78
+ " llhhmmmdddddddddddddhhddddmhhhhhhhhhmmdddddlml ",
79
+ " hhhmmmmmddddddddldhhhhhhlhhhhmhhhhhhhlmmmmmm ",
80
+ " llllmddmddddddddhhhhhhhhhhlhhhhhhhlllmmmm ",
81
+ " hllllmddddddmhhhhhhhhhhhhhllllllmmmh ",
82
+ " dmmmmmmmmlllllllllllllmdmmm ",
83
+ " mmmmmmmmmmmmmmmlllmm ",
84
+ " dlmmmmllllmmlmmllllllm dmllllmmmmmmmm ",
85
+ " lllllmmlllllllllllllmmm lmllllllllhlmmmmm ",
86
+ " mlllllmdmlllllllllmmmd mddmmmmlhhhlllmmmmm ",
87
+ " hmmmmmmdmdmmmmmmmmmmm dddmmmmmmmmlllllmmdmm",
88
+ " mmmlllllhllllmmmmmmmm dmmllllmmmmmmmlllmdm ",
89
+ " mlllhhhhhhhhlllllllmmdddmlllhllmmmmmmlmml ",
90
+ " llllllhhhhhhhllllllllmmdddmmlllllmmmmdmmd ",
91
+ " mllllllllhhllllllllllmmmddmmmlllllmdddmm ",
92
+ " lmllmlllllllllllllllmmmmmddmmmllllmdddd ",
93
+ " lmmmmmmllllmllllllmmmdmdddddmmmmmddmd ",
94
+ " dmmmmmmd dmmmmmmmmmmmmddddmmmmmdd ",
95
+ " dmmmmmd dmmmmmmd h ",
96
+ " mmmmmmmm dmmmmmmm ",
97
+ " dmmmmmm ",
98
+ ] as const;
52
99
 
53
- export const HEADER = [
100
+ const HEADER_CLASSIC = [
54
101
  "░█████╗░░██████╗██████╗░██╗░░░██╗░░░░░░██████╗░██╗",
55
102
  "██╔══██╗██╔════╝██╔══██╗╚██╗░██╔╝░░░░░░██╔══██╗ ║",
56
103
  "██║░░██║╚█████╗░██║░░██║░╚████╔╝░█████╗██████╔╝██║",
@@ -64,11 +111,227 @@ export const HEADER = [
64
111
  " ╱╱╱╱╱╱╱╰╯ </>",
65
112
  ] as const;
66
113
 
67
- export const HEADER_WIDTH = HEADER.reduce(
68
- (maxWidth, line) => Math.max(maxWidth, visibleWidth(line)),
69
- 0,
114
+ const HEADER_SIMPLE = [
115
+ " ███████ █████ ███████████ ███ ",
116
+ " ███░░░░░███ ░░███ ░░███░░░░░███ ░░░ ",
117
+ " ███ ░░███ █████ ███████ █████ ████ ░███ ░███ ████ ",
118
+ "░███ ░███ ███░░ ███░░███ ░░███ ░███ ██████████ ░██████████ ░░███ ",
119
+ "░███ ░███░░█████ ░███ ░███ ░███ ░███ ░░░░░░░░░░ ░███░░░░░░ ░███ ",
120
+ "░░███ ███ ░░░░███░███ ░███ ░███ ░███ ░███ ░███ ",
121
+ " ░░░███████░ ██████ ░░████████ ░░███████ █████ █████",
122
+ " ░░░░░░░ ░░░░░░ ░░░░░░░░ ░░░░░███ ░░░░░ ░░░░░ ",
123
+ " ███ ░███",
124
+ " ░░██████",
125
+ " ░░░░░░ </>",
126
+ ] as const;
127
+
128
+ type HeaderPalette = {
129
+ baseColor: string;
130
+ highlightColor: string;
131
+ trailColor: string;
132
+ };
133
+
134
+ export type MascotToneKey = "b" | "h" | "l" | "m" | "d" | "p" | "c" | "v";
135
+
136
+ export type MascotTonePalette = Record<MascotToneKey, string>;
137
+
138
+ type HeaderVariantConfig = {
139
+ label: string;
140
+ header: readonly string[];
141
+ headerMap?: readonly string[];
142
+ headerTonePalette?: MascotTonePalette;
143
+ minRowsForFull?: number;
144
+ fallbackHeader?: readonly string[];
145
+ fallbackLinePalette?: (lineIndex: number) => HeaderPalette;
146
+ mascot?: readonly string[];
147
+ mascotMap?: readonly string[];
148
+ linePalette: (lineIndex: number) => HeaderPalette;
149
+ mascotPalette: HeaderPalette;
150
+ mascotTonePalette?: MascotTonePalette;
151
+ };
152
+
153
+ const CLASSIC_PALETTE: HeaderPalette = {
154
+ baseColor: "accent",
155
+ highlightColor: "mdHeading",
156
+ trailColor: "mdLink",
157
+ };
158
+
159
+ const CLASSIC_LINK_PALETTE: HeaderPalette = {
160
+ baseColor: "mdLink",
161
+ highlightColor: "mdHeading",
162
+ trailColor: "mdLink",
163
+ };
164
+
165
+ const OSDY_THEME_PINK_PALETTE: HeaderPalette = {
166
+ baseColor: "accent",
167
+ highlightColor: "mdHeading",
168
+ trailColor: "mdLink",
169
+ };
170
+
171
+ const OSDY_THEME_CYAN_PALETTE: HeaderPalette = {
172
+ baseColor: "mdLink",
173
+ highlightColor: "accent",
174
+ trailColor: "mdHeading",
175
+ };
176
+
177
+ const OSDY_THEME_PURPLE_PALETTE: HeaderPalette = {
178
+ baseColor: "mdHeading",
179
+ highlightColor: "mdLink",
180
+ trailColor: "accent",
181
+ };
182
+
183
+ const OSDY_THEME_MASCOT_PALETTE: HeaderPalette = {
184
+ baseColor: "mdLink",
185
+ highlightColor: "accent",
186
+ trailColor: "mdHeading",
187
+ };
188
+
189
+ const HTML_MASCOT_TONES: MascotTonePalette = {
190
+ b: "mascotBg",
191
+ h: "mascotBright",
192
+ l: "mascotLight",
193
+ m: "mascotMid",
194
+ d: "mascotDark",
195
+ p: "htmlPink",
196
+ c: "mdLink",
197
+ v: "mdHeading",
198
+ };
199
+
200
+ type MascotArt = {
201
+ mascot: readonly string[];
202
+ toneMap: readonly string[];
203
+ };
204
+
205
+ function sampleIndex(
206
+ outputIndex: number,
207
+ outputCount: number,
208
+ inputCount: number,
209
+ ): number {
210
+ if (outputCount <= 1) return 0;
211
+ return Math.min(
212
+ inputCount - 1,
213
+ Math.round((outputIndex * (inputCount - 1)) / (outputCount - 1)),
214
+ );
215
+ }
216
+
217
+ function sampleLine(
218
+ line: string,
219
+ outputWidth: number,
220
+ inputWidth: number,
221
+ ): string {
222
+ const chars = Array.from(line.padEnd(inputWidth, " "));
223
+ return Array.from({ length: outputWidth }, (_value, outputIndex) => {
224
+ const inputIndex = sampleIndex(outputIndex, outputWidth, inputWidth);
225
+ return chars[inputIndex] ?? " ";
226
+ }).join("");
227
+ }
228
+
229
+ function scaleMascotArt(
230
+ mascot: readonly string[],
231
+ toneMap: readonly string[],
232
+ scale: number,
233
+ ): MascotArt {
234
+ const inputRows = Math.min(mascot.length, toneMap.length);
235
+ const inputWidth = Math.max(
236
+ ...mascot.slice(0, inputRows).map((line) => Array.from(line).length),
237
+ ...toneMap.slice(0, inputRows).map((line) => Array.from(line).length),
238
+ );
239
+ const outputRows = Math.max(1, Math.round(inputRows * scale));
240
+ const outputWidth = Math.max(1, Math.round(inputWidth * scale));
241
+
242
+ return Array.from({ length: outputRows }, (_value, outputIndex) => {
243
+ const inputIndex = sampleIndex(outputIndex, outputRows, inputRows);
244
+ return {
245
+ mascot: sampleLine(mascot[inputIndex] ?? "", outputWidth, inputWidth),
246
+ toneMap: sampleLine(toneMap[inputIndex] ?? "", outputWidth, inputWidth),
247
+ };
248
+ }).reduce<MascotArt>(
249
+ (accumulator, line) => ({
250
+ mascot: [...accumulator.mascot, line.mascot],
251
+ toneMap: [...accumulator.toneMap, line.toneMap],
252
+ }),
253
+ { mascot: [], toneMap: [] },
254
+ );
255
+ }
256
+
257
+ function addRightEdgeGlow(
258
+ mascot: readonly string[],
259
+ toneMap: readonly string[],
260
+ ): readonly string[] {
261
+ return toneMap.map((lineMap, lineIndex) => {
262
+ const chars = Array.from(lineMap);
263
+ const mascotLine = mascot[lineIndex] ?? "";
264
+ const rightEdgeIndex = Math.max(
265
+ ...Array.from(mascotLine).map((char, index) =>
266
+ char === " " ? -1 : index,
267
+ ),
268
+ );
269
+ if (rightEdgeIndex < 0) return lineMap;
270
+ const glowTones = ["p", "c", "v"] as const;
271
+ for (let offset = 0; offset < glowTones.length; offset += 1) {
272
+ const glowTone = glowTones[offset];
273
+ const index = rightEdgeIndex - offset;
274
+ if (
275
+ glowTone &&
276
+ index >= 0 &&
277
+ index < chars.length &&
278
+ chars[index] !== " "
279
+ ) {
280
+ chars[index] = glowTone;
281
+ }
282
+ }
283
+ return chars.join("");
284
+ });
285
+ }
286
+
287
+ const OSDY_THEME_MASCOT_ART = scaleMascotArt(HTML_MASCOT, HTML_MASCOT_MAP, 0.9);
288
+ const OSDY_THEME_MASCOT_MAP = addRightEdgeGlow(
289
+ OSDY_THEME_MASCOT_ART.mascot,
290
+ OSDY_THEME_MASCOT_ART.toneMap,
70
291
  );
71
292
 
293
+ function osdyThemePalette(lineIndex: number): HeaderPalette {
294
+ if (lineIndex < 4) return OSDY_THEME_PINK_PALETTE;
295
+ if (lineIndex < 8) return OSDY_THEME_CYAN_PALETTE;
296
+ return OSDY_THEME_PURPLE_PALETTE;
297
+ }
298
+
299
+ export const HEADER_VARIANTS: Record<HeaderVariant, HeaderVariantConfig> = {
300
+ "osdy-theme": {
301
+ label: "OsdyTheme",
302
+ header: HEADER_SIMPLE,
303
+ mascot: OSDY_THEME_MASCOT_ART.mascot,
304
+ mascotMap: OSDY_THEME_MASCOT_MAP,
305
+ linePalette: osdyThemePalette,
306
+ mascotPalette: OSDY_THEME_MASCOT_PALETTE,
307
+ mascotTonePalette: HTML_MASCOT_TONES,
308
+ },
309
+ classic: {
310
+ label: "Classic",
311
+ header: HEADER_CLASSIC,
312
+ mascot: HTML_MASCOT,
313
+ mascotMap: HTML_MASCOT_MAP,
314
+ linePalette: (lineIndex) =>
315
+ lineIndex >= 6 ? CLASSIC_LINK_PALETTE : CLASSIC_PALETTE,
316
+ mascotPalette: OSDY_THEME_MASCOT_PALETTE,
317
+ mascotTonePalette: HTML_MASCOT_TONES,
318
+ },
319
+ };
320
+
321
+ export function headerWidth(variant: HeaderVariant): number {
322
+ return HEADER_VARIANTS[variant].header.reduce(
323
+ (maxWidth, line) => Math.max(maxWidth, visibleWidth(line)),
324
+ 0,
325
+ );
326
+ }
327
+
328
+ export function mascotWidth(variant: HeaderVariant): number {
329
+ return (HEADER_VARIANTS[variant].mascot ?? []).reduce(
330
+ (maxWidth, line) => Math.max(maxWidth, visibleWidth(line)),
331
+ 0,
332
+ );
333
+ }
334
+
72
335
  export const HEADER_FALLBACK = [
73
336
  "OSDY - PI",
74
337
  "</> Compilador de ideas",
@@ -95,7 +95,7 @@ export function disableOsdyPi(ctx: ExtensionContext, state: OsdyState): void {
95
95
 
96
96
  export function notifyStatus(ctx: ExtensionContext, state: OsdyState): void {
97
97
  ctx.ui.notify(
98
- `osdy-pi ${state.enabled ? "enabled" : "disabled"} · theme ${ctx.ui.theme.name ?? "unknown"} · animation ${asciiAnimationMode()} · ${modelLabel(ctx)} · ${usageLabel(ctx).trim()}`,
98
+ `osdy-pi ${state.enabled ? "enabled" : "disabled"} · theme ${ctx.ui.theme.name ?? "unknown"} · style ${state.headerVariant} · animation ${asciiAnimationMode()} · ${modelLabel(ctx)} · ${usageLabel(ctx).trim()}`,
99
99
  "info",
100
100
  );
101
101
  }
@@ -3,7 +3,7 @@ import type {
3
3
  ExtensionContext,
4
4
  } from "@earendil-works/pi-coding-agent";
5
5
  import { applyOsdyPi, disableOsdyPi, notifyStatus } from "./runtime-helpers.js";
6
- import type { OsdyState, WorkingWidgetState } from "./types.js";
6
+ import type { HeaderVariant, OsdyState, WorkingWidgetState } from "./types.js";
7
7
  import {
8
8
  createWorkingController,
9
9
  type WorkingController,
@@ -61,8 +61,14 @@ function loadSessionMetadata(
61
61
  });
62
62
  }
63
63
 
64
- function parseCommandAction(args: string): string {
65
- return args.trim().split(/\s+/, 1)[0] || "status";
64
+ function parseCommandArgs(args: string): string[] {
65
+ return args.trim().split(/\s+/).filter(Boolean);
66
+ }
67
+
68
+ const HEADER_VARIANTS = ["osdy-theme", "classic"] as const;
69
+
70
+ function isHeaderVariant(value: string): value is HeaderVariant {
71
+ return HEADER_VARIANTS.includes(value as HeaderVariant);
66
72
  }
67
73
 
68
74
  function enableOsdyPi(
@@ -87,6 +93,18 @@ function disableOsdyPiCommand(
87
93
  disableOsdyPi(ctx, state);
88
94
  }
89
95
 
96
+ function setHeaderVariant(
97
+ pi: ExtensionAPI,
98
+ ctx: ExtensionContext,
99
+ state: OsdyState,
100
+ workingState: WorkingWidgetState,
101
+ variant: HeaderVariant,
102
+ ): void {
103
+ state.headerVariant = variant;
104
+ if (state.enabled) applyOsdyPi(pi, ctx, state, workingState);
105
+ ctx.ui.notify(`osdy-pi style: ${variant}`, "info");
106
+ }
107
+
90
108
  function registerCommand(
91
109
  pi: ExtensionAPI,
92
110
  state: OsdyState,
@@ -94,14 +112,21 @@ function registerCommand(
94
112
  controller: WorkingController,
95
113
  ): void {
96
114
  pi.registerCommand("osdy-pi", {
97
- description: "Manage the Osdy Pi experience: enable, disable, or status.",
115
+ description:
116
+ "Manage the Osdy Pi experience: enable, disable, status, or style.",
98
117
  getArgumentCompletions(prefix: string) {
99
- return ["enable", "disable", "status"]
100
- .filter((value) => value.startsWith(prefix.trim()))
101
- .map((value) => ({ value, label: value }));
118
+ const trimmed = prefix.trim();
119
+ const parts = parseCommandArgs(trimmed);
120
+ if (parts.length <= 1) {
121
+ const valuePrefix = parts[0] ?? "";
122
+ return ["enable", "disable", "status", ...HEADER_VARIANTS]
123
+ .filter((value) => value.startsWith(valuePrefix))
124
+ .map((value) => ({ value, label: value }));
125
+ }
126
+ return null;
102
127
  },
103
128
  handler: (args, ctx) => {
104
- const action = parseCommandAction(args);
129
+ const [action = "status"] = parseCommandArgs(args);
105
130
  if (action === "enable") {
106
131
  enableOsdyPi(pi, ctx, state, workingState, controller);
107
132
  return Promise.resolve();
@@ -114,14 +139,36 @@ function registerCommand(
114
139
  notifyStatus(ctx, state);
115
140
  return Promise.resolve();
116
141
  }
117
- ctx.ui.notify("Usage: /osdy-pi enable | disable | status", "warning");
142
+ if (isHeaderVariant(action)) {
143
+ setHeaderVariant(pi, ctx, state, workingState, action);
144
+ return Promise.resolve();
145
+ }
146
+ ctx.ui.notify(
147
+ "Usage: /osdy-pi enable | disable | status | osdy-theme | classic",
148
+ "warning",
149
+ );
118
150
  return Promise.resolve();
119
151
  },
120
152
  });
153
+
154
+ for (const variant of HEADER_VARIANTS) {
155
+ pi.registerCommand(`osdy-pi-${variant}`, {
156
+ description: `Switch Osdy Pi header to ${variant}.`,
157
+ handler: (_args, ctx) => {
158
+ setHeaderVariant(pi, ctx, state, workingState, variant);
159
+ return Promise.resolve();
160
+ },
161
+ });
162
+ }
121
163
  }
122
164
 
123
165
  export function registerOsdyPi(pi: ExtensionAPI): void {
124
- const state: OsdyState = { enabled: true, gitLabel: "-", agentsLabel: "-" };
166
+ const state: OsdyState = {
167
+ enabled: true,
168
+ headerVariant: "osdy-theme",
169
+ gitLabel: "-",
170
+ agentsLabel: "-",
171
+ };
125
172
  const workingState: WorkingWidgetState = {
126
173
  active: false,
127
174
  label: "Working...",
@@ -2,8 +2,11 @@ import type { TUI } from "@earendil-works/pi-tui";
2
2
 
3
3
  export type AnimationMode = "off" | "intro" | "continuous";
4
4
 
5
+ export type HeaderVariant = "osdy-theme" | "classic";
6
+
5
7
  export type OsdyState = {
6
8
  enabled: boolean;
9
+ headerVariant: HeaderVariant;
7
10
  previousThemeName?: string;
8
11
  gitLabel: string;
9
12
  agentsLabel: string;
@@ -12,18 +12,21 @@ import {
12
12
  truncateToWidth,
13
13
  visibleWidth,
14
14
  } from "@earendil-works/pi-tui";
15
- import { animateAsciiLine, asciiAnimationMode } from "./animation.js";
15
+ import {
16
+ animateAsciiLine,
17
+ animateAsciiLineWithToneMap,
18
+ asciiAnimationMode,
19
+ } from "./animation.js";
16
20
  import { fitBorder } from "./border.js";
17
21
  import {
18
22
  ANIMATION_INTERVAL_MS,
19
- HEADER,
23
+ headerWidth,
20
24
  HEADER_FALLBACK,
21
- HEADER_WIDTH,
25
+ HEADER_VARIANTS,
22
26
  INTRO_ANIMATION_FRAMES,
23
- MASCOT,
24
27
  MASCOT_GAP,
25
28
  MASCOT_MIN_ROWS,
26
- MASCOT_WIDTH,
29
+ mascotWidth,
27
30
  WORKING_SPINNER_FRAMES,
28
31
  } from "./constants.js";
29
32
  import { formatPath } from "./format.js";
@@ -130,39 +133,85 @@ export function createHeaderComponent(
130
133
  render(width: number): string[] {
131
134
  const animationStyle =
132
135
  animateAscii && !animationComplete ? "animated" : "static";
136
+ const variant = HEADER_VARIANTS[state.headerVariant];
137
+ const fullHeaderWidth = headerWidth(state.headerVariant);
138
+ const canUseFullHeader =
139
+ width >= fullHeaderWidth &&
140
+ (variant.minRowsForFull === undefined ||
141
+ _tui.terminal.rows >= variant.minRowsForFull);
142
+ const mascotLinesSource = variant.mascot ?? [];
143
+ const mascotToneMap = variant.mascotMap ?? [];
144
+ const fullMascotWidth = mascotWidth(state.headerVariant);
133
145
  const showMascot =
146
+ canUseFullHeader &&
147
+ mascotLinesSource.length > 0 &&
134
148
  _tui.terminal.rows >= MASCOT_MIN_ROWS &&
135
- width >= MASCOT_WIDTH + MASCOT_GAP + HEADER_WIDTH;
136
- const useFullHeader = width >= HEADER_WIDTH;
137
- const headerLines = useFullHeader ? [...HEADER] : [...HEADER_FALLBACK];
149
+ width >= fullMascotWidth + MASCOT_GAP + fullHeaderWidth;
150
+ const headerLines = canUseFullHeader
151
+ ? [...variant.header]
152
+ : [...(variant.fallbackHeader ?? HEADER_FALLBACK)];
153
+ const headerToneMap = canUseFullHeader ? (variant.headerMap ?? []) : [];
138
154
  const metadataRows = renderHeaderMetadata(pi, ctx, state, width);
139
155
  const logoLines = headerLines.map((line, index) => {
140
- const baseColor = useFullHeader && index >= 6 ? "mdLink" : "accent";
156
+ const toneLine = headerToneMap[index];
157
+ if (toneLine && variant.headerTonePalette) {
158
+ return animateAsciiLineWithToneMap(
159
+ line,
160
+ toneLine,
161
+ index,
162
+ frame,
163
+ theme,
164
+ variant.headerTonePalette,
165
+ animationStyle,
166
+ );
167
+ }
168
+ const palette = canUseFullHeader
169
+ ? variant.linePalette(index)
170
+ : (variant.fallbackLinePalette?.(index) ??
171
+ variant.linePalette(index));
141
172
  return animateAsciiLine(
142
173
  line,
143
174
  index,
144
175
  frame,
145
176
  theme,
146
- baseColor,
147
- "mdHeading",
148
- "mdLink",
177
+ palette.baseColor,
178
+ palette.highlightColor,
179
+ palette.trailColor,
149
180
  animationStyle,
150
181
  );
151
182
  });
152
- const mascotLines = MASCOT.map((line, index) =>
153
- animateAsciiLine(
183
+ const mascotLines = mascotLinesSource.map((line, index) => {
184
+ const toneLine = mascotToneMap[index];
185
+ if (toneLine && variant.mascotTonePalette) {
186
+ return animateAsciiLineWithToneMap(
187
+ line,
188
+ toneLine,
189
+ index,
190
+ frame,
191
+ theme,
192
+ variant.mascotTonePalette,
193
+ animationStyle,
194
+ );
195
+ }
196
+ return animateAsciiLine(
154
197
  line,
155
198
  index,
156
199
  frame,
157
200
  theme,
158
- "muted",
159
- "mdCode",
160
- "muted",
201
+ variant.mascotPalette.baseColor,
202
+ variant.mascotPalette.highlightColor,
203
+ variant.mascotPalette.trailColor,
161
204
  animationStyle,
162
- ),
163
- );
205
+ );
206
+ });
164
207
  const headerBlock = showMascot
165
- ? composeSideBySide(mascotLines, logoLines, width)
208
+ ? composeSideBySide(
209
+ mascotLines,
210
+ fullMascotWidth,
211
+ logoLines,
212
+ width,
213
+ fullHeaderWidth,
214
+ )
166
215
  : logoLines.map((line) => fitCenterVisible(line, width));
167
216
  return [
168
217
  "",
@@ -1,6 +1,6 @@
1
1
  import type { TUI } from "@earendil-works/pi-tui";
2
2
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
- import { HEADER_WIDTH, MASCOT_GAP, MASCOT_WIDTH } from "./constants.js";
3
+ import { MASCOT_GAP } from "./constants.js";
4
4
 
5
5
  export function sanitizeStatusText(text: string): string {
6
6
  return text
@@ -26,27 +26,27 @@ export function padVisibleRight(line: string, width: number): string {
26
26
  return `${line}${" ".repeat(Math.max(0, width - visibleWidth(line)))}`;
27
27
  }
28
28
 
29
- function getCombinedHeaderWidth(): number {
30
- return MASCOT_WIDTH + MASCOT_GAP + HEADER_WIDTH;
31
- }
32
-
33
29
  export function composeSideBySide(
34
30
  leftLines: string[],
31
+ leftWidth: number,
35
32
  rightLines: string[],
36
33
  width: number,
34
+ rightWidth: number,
37
35
  ): string[] {
38
- const combinedWidth = getCombinedHeaderWidth();
36
+ const combinedWidth = leftWidth + MASCOT_GAP + rightWidth;
39
37
  if (width < combinedWidth) return rightLines;
40
38
  const leftPad = " ".repeat(
41
39
  Math.max(0, Math.floor((width - combinedWidth) / 2)),
42
40
  );
43
41
  const rows = Math.max(leftLines.length, rightLines.length);
44
42
  const topOffset = Math.max(0, Math.floor((rows - rightLines.length) / 2));
45
- return Array.from({ length: rows }, (_, index) => {
46
- const left = padVisibleRight(leftLines[index] ?? "", MASCOT_WIDTH);
43
+ const composedRows: string[] = [];
44
+ for (let index = 0; index < rows; index += 1) {
45
+ const left = padVisibleRight(leftLines[index] ?? "", leftWidth);
47
46
  const right = rightLines[index - topOffset] ?? "";
48
- return `${leftPad}${left}${" ".repeat(MASCOT_GAP)}${right}`;
49
- });
47
+ composedRows.push(`${leftPad}${left}${" ".repeat(MASCOT_GAP)}${right}`);
48
+ }
49
+ return composedRows;
50
50
  }
51
51
 
52
52
  export function internalLineTarget(tui: TUI): number {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osdy-pi",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Osdy Pi package: themes, header, and custom editor for Pi.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -24,7 +24,8 @@
24
24
  "pi": {
25
25
  "themes": [
26
26
  "./themes/osdy-pi-dark.json",
27
- "./themes/osdy-pi-light.json"
27
+ "./themes/osdy-pi-light.json",
28
+ "./themes/osdy-pi-simple.json"
28
29
  ],
29
30
  "extensions": [
30
31
  "./extensions/osdy-pi.ts"
@@ -62,6 +62,12 @@
62
62
  "syntaxType": "violet",
63
63
  "syntaxOperator": "hotPink",
64
64
  "syntaxPunctuation": "lavenderGray",
65
+ "mascotBg": "#F9F7F2",
66
+ "mascotBright": "#F0E5D7",
67
+ "mascotLight": "#C7B4A1",
68
+ "mascotMid": "#7D6F67",
69
+ "mascotDark": "#2A2321",
70
+ "htmlPink": "#FF2E8C",
65
71
  "thinkingOff": "#3A2852",
66
72
  "thinkingMinimal": "lavenderGray",
67
73
  "thinkingLow": "purple",
@@ -62,6 +62,12 @@
62
62
  "syntaxType": "violet",
63
63
  "syntaxOperator": "hotPink",
64
64
  "syntaxPunctuation": "lavenderGray",
65
+ "mascotBg": "#8F7E73",
66
+ "mascotBright": "#E8DACB",
67
+ "mascotLight": "#B79F8E",
68
+ "mascotMid": "#74665F",
69
+ "mascotDark": "#2E2725",
70
+ "htmlPink": "#FF2E8C",
65
71
  "thinkingOff": "dimPurple",
66
72
  "thinkingMinimal": "lavenderGray",
67
73
  "thinkingLow": "purple",
@@ -0,0 +1,83 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/earendil-works/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
+ "name": "osdy-pi-simple",
4
+ "vars": {
5
+ "slate900": "#0B1220",
6
+ "slate800": "#111827",
7
+ "slate700": "#1F2937",
8
+ "slate600": "#475569",
9
+ "steel": "#94A3B8",
10
+ "mist": "#CBD5E1",
11
+ "ice": "#E2E8F0",
12
+ "deepBlue": "#0A66FF",
13
+ "royalBlue": "#2563EB",
14
+ "cyanBlue": "#38BDF8",
15
+ "ferrari": "#FF2800",
16
+ "ember": "#FF5A36",
17
+ "roseRed": "#E11D48"
18
+ },
19
+ "colors": {
20
+ "accent": "ferrari",
21
+ "border": "royalBlue",
22
+ "borderAccent": "deepBlue",
23
+ "borderMuted": "slate600",
24
+ "success": "cyanBlue",
25
+ "error": "ember",
26
+ "warning": "ferrari",
27
+ "muted": "steel",
28
+ "dim": "slate600",
29
+ "text": "ice",
30
+ "thinkingText": "steel",
31
+ "selectedBg": "slate700",
32
+ "userMessageBg": "slate800",
33
+ "userMessageText": "ice",
34
+ "customMessageBg": "slate700",
35
+ "customMessageText": "ice",
36
+ "customMessageLabel": "deepBlue",
37
+ "toolPendingBg": "slate800",
38
+ "toolSuccessBg": "#10263A",
39
+ "toolErrorBg": "#3B1417",
40
+ "toolTitle": "deepBlue",
41
+ "toolOutput": "ice",
42
+ "mdHeading": "royalBlue",
43
+ "mdLink": "cyanBlue",
44
+ "mdLinkUrl": "mist",
45
+ "mdCode": "deepBlue",
46
+ "mdCodeBlock": "ice",
47
+ "mdCodeBlockBorder": "royalBlue",
48
+ "mdQuote": "steel",
49
+ "mdQuoteBorder": "slate600",
50
+ "mdHr": "slate600",
51
+ "mdListBullet": "ferrari",
52
+ "toolDiffAdded": "cyanBlue",
53
+ "toolDiffRemoved": "ember",
54
+ "toolDiffContext": "steel",
55
+ "syntaxComment": "slate600",
56
+ "syntaxKeyword": "ferrari",
57
+ "syntaxFunction": "cyanBlue",
58
+ "syntaxVariable": "ice",
59
+ "syntaxString": "mist",
60
+ "syntaxNumber": "royalBlue",
61
+ "syntaxType": "royalBlue",
62
+ "syntaxOperator": "ember",
63
+ "syntaxPunctuation": "steel",
64
+ "mascotBg": "#F4EEE5",
65
+ "mascotBright": "#E6D8C8",
66
+ "mascotLight": "#BFA998",
67
+ "mascotMid": "#7A6B63",
68
+ "mascotDark": "#241F1D",
69
+ "htmlPink": "#FF2E8C",
70
+ "thinkingOff": "slate600",
71
+ "thinkingMinimal": "steel",
72
+ "thinkingLow": "royalBlue",
73
+ "thinkingMedium": "deepBlue",
74
+ "thinkingHigh": "cyanBlue",
75
+ "thinkingXhigh": "ember",
76
+ "bashMode": "royalBlue"
77
+ },
78
+ "export": {
79
+ "pageBg": "slate900",
80
+ "cardBg": "slate800",
81
+ "infoBg": "slate700"
82
+ }
83
+ }