osdy-pi 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Osdy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # osdy-pi
2
+
3
+ Pi package for the Osdy terminal experience.
4
+
5
+ ## Features
6
+
7
+ - `osdy-pi-dark` neon pink/purple theme enabled by default on `session_start`.
8
+ - `osdy-pi-light` neon pink/purple companion theme included.
9
+ - Centered custom Osdy-Pi ASCII header with responsive Git, path, MCP, plugins, AGENTS, extensions, Pi version, and tools status.
10
+ - Custom editor with four internal lines by default and smaller responsive fallback.
11
+ - Full-width Osdy editor frame with complete top, bottom, and side borders in normal terminal widths.
12
+ - Editor border status:
13
+ - top-right: current model and thinking level;
14
+ - bottom-left: input/output/cache tokens, accumulated cost, and context percentage/window.
15
+ - Animation disabled.
16
+
17
+ ## Install
18
+
19
+ Pi packages are not uploaded to a separate Pi registry. They are shared through npm, git, or a local path.
20
+
21
+ ### From npm
22
+
23
+ After publishing this package to npm:
24
+
25
+ ```bash
26
+ pi install npm:osdy-pi
27
+ ```
28
+
29
+ ### From GitHub
30
+
31
+ After pushing this package to a GitHub repository:
32
+
33
+ ```bash
34
+ pi install git:github.com/<owner>/osdy-pi
35
+ ```
36
+
37
+ You can also pin a tag or branch:
38
+
39
+ ```bash
40
+ pi install git:github.com/<owner>/osdy-pi@v0.1.0
41
+ ```
42
+
43
+ ### Local development
44
+
45
+ To test this package from this repository without installing it permanently:
46
+
47
+ ```bash
48
+ pi -e .
49
+ ```
50
+
51
+ To install it from a local path:
52
+
53
+ ```bash
54
+ pi install /absolute/path/to/osdy-pi
55
+ ```
56
+
57
+ ## Publishing
58
+
59
+ ### npm
60
+
61
+ ```bash
62
+ npm login
63
+ npm pack --dry-run
64
+ npm publish
65
+ ```
66
+
67
+ Then verify with:
68
+
69
+ ```bash
70
+ pi install npm:osdy-pi
71
+ ```
72
+
73
+ ### GitHub
74
+
75
+ Initialize the repository, commit the package, push it to GitHub, then install it with the `git:` source shown above.
76
+
77
+ ## Commands
78
+
79
+ ```text
80
+ /osdy-pi enable
81
+ /osdy-pi disable
82
+ /osdy-pi status
83
+ ```
84
+
85
+ `enable` applies the `osdy-pi-dark` theme, custom header, custom editor, and hides the built-in footer/working row to avoid duplicate status UI.
86
+
87
+ `disable` restores the built-in header, editor, footer, and working visibility, then switches back to the previously captured theme or `dark`.
88
+
89
+ ## Package resources
90
+
91
+ The Pi manifest is declared in `package.json`:
92
+
93
+ - themes: `themes/osdy-pi-dark.json`, `themes/osdy-pi-light.json`
94
+ - extension: `extensions/osdy-pi.ts`
95
+
96
+ ## License
97
+
98
+ MIT
@@ -0,0 +1,623 @@
1
+ import {
2
+ CustomEditor,
3
+ VERSION,
4
+ type ExtensionAPI,
5
+ type ExtensionContext,
6
+ type KeybindingsManager,
7
+ } from "@earendil-works/pi-coding-agent";
8
+ import {
9
+ truncateToWidth,
10
+ visibleWidth,
11
+ type Component,
12
+ type EditorTheme,
13
+ type TUI,
14
+ } from "@earendil-works/pi-tui";
15
+
16
+ const THEME_NAME = "osdy-pi-dark";
17
+ const ANIMATION_ENABLED = true;
18
+ const ANIMATION_INTERVAL_MS = 30;
19
+ const INTRO_ANIMATION_FRAMES = 28;
20
+
21
+ type AnimationMode = "off" | "intro" | "continuous";
22
+
23
+ type OsdyState = {
24
+ enabled: boolean;
25
+ previousThemeName?: string;
26
+ gitLabel: string;
27
+ agentsLabel: string;
28
+ };
29
+
30
+ const MASCOT_MIN_ROWS = 34;
31
+ const MASCOT_GAP = 0;
32
+
33
+ const MASCOT = [
34
+ " %% %%%@@ @@%%% @%%",
35
+ " %= --- -=#% @@@@@@@@@%%%-- ----#%",
36
+ " %- -*#@ =+**-------:-**- %%#*--#%",
37
+ " %- -*#%*-----------------*%#*--#%",
38
+ " %+-#*---------------------**-+%",
39
+ " %#=--=- ==-------= ----=*%",
40
+ " %+-- ***** ------ **** -=*%",
41
+ " @%% ****%% %*=-----**% %****# #%%@",
42
+ " @%# ******%%%%*=--==-**%%%%*****#--%",
43
+ " %%#*******##*# %%%% **%#*******%%@",
44
+ " %%%#****** %@%%@% ******#%@%",
45
+ " %%%:: ::#%@ %%%%%",
46
+ " %%%**::::::::::%%% %%#-****%%",
47
+ " %#---: : :: ::-=+%%%%%=-::::+*#%",
48
+ " %%----- : ::----*%%%*****-:::#%",
49
+ " #*------ : ::-----**#+-:=+***=-*%",
50
+ " %%%+*=+------ ::------+*=+%-:::-*##+%%",
51
+ " %*---**-----* **----+*--=+%=---=*#%",
52
+ " %**+==%*----*==%=----##==+*%#+--*%%",
53
+ " %%#%#%@+=+++%%++++==%##%#%#%%%%",
54
+ " ----=+%%%#%%#%*##%###*#%%##%%==---------",
55
+ " ##****=*****==",
56
+ ];
57
+ const MASCOT_WIDTH = MASCOT.reduce(
58
+ (maxWidth, line) => Math.max(maxWidth, visibleWidth(line)),
59
+ 0,
60
+ );
61
+
62
+ type HeaderMetaRow = {
63
+ leftLabel: string;
64
+ leftValue: string;
65
+ rightLabel: string;
66
+ rightValue: string;
67
+ };
68
+
69
+ const HEADER = [
70
+ "░█████╗░░██████╗██████╗░██╗░░░██╗░░░░░░██████╗░██╗",
71
+ "██╔══██╗██╔════╝██╔══██╗╚██╗░██╔╝░░░░░░██╔══██╗ ║",
72
+ "██║░░██║╚█████╗░██║░░██║░╚████╔╝░█████╗██████╔╝██║",
73
+ "██║░░██║░╚═══██╗██║░░██║░░╚██╔╝░░╚════╝██╔═══╝░██║",
74
+ "╚█████╔╝██████╔╝██████╔╝░░░██║░░░░░░░░░██║░░░░░██║",
75
+ "░╚════╝░╚═════╝░╚═════╝░░░░╚═╝░░░░░░░░░╚═╝░░░░░╚═╝",
76
+ " ╭━╮╱╱╱╱╱╱╭╮╱╱╱╱╱╭╮╱╱╱╱╱╭╮╱╱╭╮╭╮╱╱╱╱╭━╮",
77
+ " ┃╭╋━┳━━┳━╋╋╮╭━╮╭╯┣━┳┳╮╭╯┣━╮┣╋╯┣━┳━╮┃━┫",
78
+ " ┃╰┫╋┃┃┃┃╋┃┃╰┫╋╰┫╋┃╋┃╭╯┃╋┃┻┫┃┃╋┃┻┫╋╰╋━┃",
79
+ " ╰━┻━┻┻┻┫╭┻┻━┻━━┻━┻━┻╯╱╰━┻━╯╰┻━┻━┻━━┻━╯",
80
+ " ╱╱╱╱╱╱╱╰╯ </>",
81
+ ];
82
+ const HEADER_WIDTH = HEADER.reduce(
83
+ (maxWidth, line) => Math.max(maxWidth, visibleWidth(line)),
84
+ 0,
85
+ );
86
+ const HEADER_FALLBACK = ["OSDY - PI", "</> Compilador de ideas"];
87
+
88
+ class OsdyFooter implements Component {
89
+ constructor(
90
+ private readonly ctx: ExtensionContext,
91
+ private readonly footerData: any,
92
+ private readonly theme: { fg(name: string, text: string): string },
93
+ ) {}
94
+
95
+ render(width: number): string[] {
96
+ let location = formatPath(this.ctx.cwd);
97
+ const branch = this.footerData.getGitBranch?.();
98
+ if (branch) location = `${location} (${branch})`;
99
+ const locationLine = truncateToWidth(
100
+ this.theme.fg("dim", location),
101
+ width,
102
+ this.theme.fg("dim", "..."),
103
+ );
104
+ const statuses = this.footerData.getExtensionStatuses?.() ?? new Map();
105
+ const statusEntries = Array.from(statuses.entries()) as Array<
106
+ [string, string]
107
+ >;
108
+ const statusLine = statusEntries
109
+ .sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
110
+ .map(([, text]) => sanitizeStatusText(text))
111
+ .join(" ");
112
+ return statusLine
113
+ ? [
114
+ locationLine,
115
+ truncateToWidth(statusLine, width, this.theme.fg("dim", "...")),
116
+ ]
117
+ : [locationLine];
118
+ }
119
+
120
+ invalidate(): void {}
121
+ }
122
+
123
+ function sanitizeStatusText(text: string): string {
124
+ return text
125
+ .replace(/[\r\n\t]/g, " ")
126
+ .replace(/ +/g, " ")
127
+ .trim();
128
+ }
129
+
130
+ function positiveModulo(value: number, divisor: number): number {
131
+ return ((value % divisor) + divisor) % divisor;
132
+ }
133
+
134
+ function asciiAnimationMode(): AnimationMode {
135
+ if (!ANIMATION_ENABLED) return "off";
136
+ const override = process.env.OSDY_PI_ANIMATION;
137
+ if (override === "1" || override === "on" || override === "continuous")
138
+ return "continuous";
139
+ if (override === "intro") return "intro";
140
+ if (override === "0" || override === "off") return "off";
141
+ return "intro";
142
+ }
143
+
144
+ function animateAsciiLine(
145
+ line: string,
146
+ lineIndex: number,
147
+ frame: number,
148
+ theme: { fg(name: string, text: string): string },
149
+ baseColor: string,
150
+ highlightColor: string,
151
+ trailColor: string,
152
+ animate: boolean,
153
+ ): string {
154
+ if (!animate) return theme.fg(baseColor, line);
155
+ return [...line]
156
+ .map((char, charIndex) => {
157
+ if (char === " ") return char;
158
+ const wave = positiveModulo(charIndex + lineIndex * 2 - frame * 5, 44);
159
+ const color =
160
+ wave <= 3 ? highlightColor : wave <= 8 ? trailColor : baseColor;
161
+ return theme.fg(color, char);
162
+ })
163
+ .join("");
164
+ }
165
+
166
+ function centerVisible(line: string, width: number): string {
167
+ const leftPad = Math.max(0, Math.floor((width - visibleWidth(line)) / 2));
168
+ return `${" ".repeat(leftPad)}${line}`;
169
+ }
170
+
171
+ function fitCenterVisible(line: string, width: number): string {
172
+ return centerVisible(truncateToWidth(line, Math.max(1, width)), width);
173
+ }
174
+
175
+ function padVisibleRight(line: string, width: number): string {
176
+ return `${line}${" ".repeat(Math.max(0, width - visibleWidth(line)))}`;
177
+ }
178
+
179
+ function composeSideBySide(
180
+ leftLines: string[],
181
+ rightLines: string[],
182
+ width: number,
183
+ ): string[] {
184
+ const combinedWidth = MASCOT_WIDTH + MASCOT_GAP + HEADER_WIDTH;
185
+ if (width < combinedWidth) return rightLines;
186
+ const leftPad = " ".repeat(
187
+ Math.max(0, Math.floor((width - combinedWidth) / 2)),
188
+ );
189
+ const rows = Math.max(leftLines.length, rightLines.length);
190
+ const topOffset = Math.max(0, Math.floor((rows - rightLines.length) / 2));
191
+ return Array.from({ length: rows }, (_, index) => {
192
+ const left = padVisibleRight(leftLines[index] ?? "", MASCOT_WIDTH);
193
+ const right = rightLines[index - topOffset] ?? "";
194
+ return `${leftPad}${left}${" ".repeat(MASCOT_GAP)}${right}`;
195
+ });
196
+ }
197
+
198
+ function formatPath(cwd: string) {
199
+ const home = process.env.HOME;
200
+ return home && cwd.startsWith(home) ? `~${cwd.slice(home.length)}` : cwd;
201
+ }
202
+
203
+ function truncateMiddle(value: string, maxWidth: number): string {
204
+ if (maxWidth <= 0) return "";
205
+ if (visibleWidth(value) <= maxWidth) return value;
206
+ if (maxWidth <= 3) return truncateToWidth(value, maxWidth);
207
+ const headWidth = Math.max(1, Math.ceil((maxWidth - 1) / 2));
208
+ const tailWidth = Math.max(1, maxWidth - 1 - headWidth);
209
+ const head = truncateToWidth(value, headWidth, "");
210
+ const tail = truncateToWidth([...value].reverse().join(""), tailWidth, "")
211
+ .split("")
212
+ .reverse()
213
+ .join("");
214
+ return `${head}…${tail}`;
215
+ }
216
+
217
+ function shortNumber(value: number): string {
218
+ if (!Number.isFinite(value)) return "0";
219
+ if (Math.abs(value) < 1_000) return Math.round(value).toString();
220
+ if (Math.abs(value) < 1_000_000) return `${(value / 1_000).toFixed(1)}k`;
221
+ return `${(value / 1_000_000).toFixed(1)}m`;
222
+ }
223
+
224
+ function modelLabel(ctx: ExtensionContext): string {
225
+ const model = ctx.model;
226
+ if (!model) return "no model";
227
+ return model.provider ? `${model.provider}/${model.id}` : model.id;
228
+ }
229
+
230
+ function getMcpServerCount(pi: ExtensionAPI): number {
231
+ const serverKeys = new Set<string>();
232
+ for (const tool of pi.getAllTools()) {
233
+ const source = tool.sourceInfo?.source?.toLowerCase() ?? "";
234
+ const path = tool.sourceInfo?.path?.toLowerCase() ?? "";
235
+ if (!tool.name.toLowerCase().includes("mcp") && !source.includes("mcp"))
236
+ continue;
237
+ serverKeys.add(source || path || tool.name);
238
+ }
239
+ return serverKeys.size;
240
+ }
241
+
242
+ function countToolsByOrigin(pi: ExtensionAPI, origin: string): number {
243
+ return pi
244
+ .getAllTools()
245
+ .filter((tool) => tool.sourceInfo?.origin?.toLowerCase() === origin).length;
246
+ }
247
+
248
+ function countToolsBySource(pi: ExtensionAPI, source: string): number {
249
+ return pi
250
+ .getAllTools()
251
+ .filter((tool) => tool.sourceInfo?.source?.toLowerCase().includes(source))
252
+ .length;
253
+ }
254
+
255
+ function renderHeaderMetadata(
256
+ pi: ExtensionAPI,
257
+ ctx: ExtensionContext,
258
+ state: OsdyState,
259
+ width: number,
260
+ ): HeaderMetaRow[] {
261
+ const activeTools = pi.getActiveTools().length;
262
+ const allTools = pi.getAllTools().length;
263
+ const git = state.gitLabel === "no-git" ? "Not a git repo" : state.gitLabel;
264
+ const path = truncateMiddle(formatPath(ctx.cwd), width < 110 ? 34 : 52);
265
+ const mcpServerCount = getMcpServerCount(pi);
266
+ const pluginCount = countToolsByOrigin(pi, "package");
267
+ const extensionCount = countToolsBySource(pi, "extension");
268
+ return [
269
+ {
270
+ leftLabel: "GIT:",
271
+ leftValue: git,
272
+ rightLabel: "PATH:",
273
+ rightValue: path,
274
+ },
275
+ {
276
+ leftLabel: "MCP:",
277
+ leftValue: `${mcpServerCount} servers`,
278
+ rightLabel: "PLUGINS:",
279
+ rightValue: `${pluginCount} package`,
280
+ },
281
+ {
282
+ leftLabel: "AGENTS:",
283
+ leftValue: `${state.agentsLabel} loaded`,
284
+ rightLabel: "EXTENSIONS:",
285
+ rightValue: `${extensionCount} active`,
286
+ },
287
+ {
288
+ leftLabel: "VER:",
289
+ leftValue: VERSION,
290
+ rightLabel: "TOOLS:",
291
+ rightValue: `${allTools} customs`,
292
+ },
293
+ ];
294
+ }
295
+
296
+ function renderMetaRows(
297
+ rows: HeaderMetaRow[],
298
+ width: number,
299
+ theme: { fg(name: string, text: string): string },
300
+ ): string[] {
301
+ const labelWidth = 11;
302
+ const leftValueWidth = 32;
303
+ const rightValueWidth = 52;
304
+ const gapWidth = 10;
305
+ const blockWidth =
306
+ labelWidth +
307
+ 1 +
308
+ leftValueWidth +
309
+ gapWidth +
310
+ labelWidth +
311
+ 1 +
312
+ rightValueWidth;
313
+ return rows.map((row) => {
314
+ const leftLabel = theme.fg("mdLink", row.leftLabel.padEnd(labelWidth));
315
+ const leftValue = theme.fg(
316
+ "accent",
317
+ truncateToWidth(row.leftValue, leftValueWidth).padEnd(leftValueWidth),
318
+ );
319
+ const rightLabel = theme.fg("mdLink", row.rightLabel.padEnd(labelWidth));
320
+ const rightValue = theme.fg(
321
+ "accent",
322
+ truncateToWidth(row.rightValue, rightValueWidth).padEnd(rightValueWidth),
323
+ );
324
+ const rowText = `${leftLabel} ${leftValue}${" ".repeat(gapWidth)}${rightLabel} ${rightValue}`;
325
+ return width >= blockWidth
326
+ ? centerVisible(rowText, width)
327
+ : fitCenterVisible(rowText, width);
328
+ });
329
+ }
330
+
331
+ function usageLabel(ctx: ExtensionContext): string {
332
+ let input = 0;
333
+ let output = 0;
334
+ let cacheRead = 0;
335
+ let cacheWrite = 0;
336
+ let cost = 0;
337
+ for (const entry of ctx.sessionManager.getEntries()) {
338
+ const anyEntry = entry as any;
339
+ if (anyEntry.type !== "message" || anyEntry.message?.role !== "assistant")
340
+ continue;
341
+ const usage = anyEntry.message.usage;
342
+ if (!usage) continue;
343
+ input += usage.input ?? 0;
344
+ output += usage.output ?? 0;
345
+ cacheRead += usage.cacheRead ?? 0;
346
+ cacheWrite += usage.cacheWrite ?? 0;
347
+ cost += usage.cost?.total ?? 0;
348
+ }
349
+ const context = ctx.getContextUsage();
350
+ const contextWindow = context?.contextWindow ?? ctx.model?.contextWindow;
351
+ const percent = context?.percent ?? null;
352
+ const ctxText = contextWindow
353
+ ? `ctx ${percent === null ? "?" : Math.round(percent)}%/${shortNumber(contextWindow)}`
354
+ : "ctx ?";
355
+ const cacheText =
356
+ cacheRead || cacheWrite
357
+ ? ` R${shortNumber(cacheRead)} W${shortNumber(cacheWrite)}`
358
+ : "";
359
+ return ` tok ↑${shortNumber(input)} ↓${shortNumber(output)}${cacheText} · $${cost.toFixed(4)} · ${ctxText} `;
360
+ }
361
+
362
+ function fitBorder(
363
+ left: string,
364
+ right: string,
365
+ width: number,
366
+ color: (text: string) => string,
367
+ ): string {
368
+ if (width <= 0) return "";
369
+ if (width === 1) return color("─");
370
+ let leftText = left;
371
+ let rightText = right;
372
+ const fixedWidth = 2;
373
+ const minGap = 1;
374
+ while (
375
+ fixedWidth + visibleWidth(leftText) + visibleWidth(rightText) + minGap >
376
+ width &&
377
+ visibleWidth(rightText) > 0
378
+ ) {
379
+ rightText = truncateToWidth(
380
+ rightText,
381
+ Math.max(0, visibleWidth(rightText) - 1),
382
+ "",
383
+ );
384
+ }
385
+ while (
386
+ fixedWidth + visibleWidth(leftText) + visibleWidth(rightText) + minGap >
387
+ width &&
388
+ visibleWidth(leftText) > 0
389
+ ) {
390
+ leftText = truncateToWidth(
391
+ leftText,
392
+ Math.max(0, visibleWidth(leftText) - 1),
393
+ "",
394
+ );
395
+ }
396
+ const fillWidth = Math.max(
397
+ 0,
398
+ width - fixedWidth - visibleWidth(leftText) - visibleWidth(rightText),
399
+ );
400
+ return `${color("─")}${leftText}${color("─".repeat(fillWidth))}${rightText}${color("─")}`;
401
+ }
402
+
403
+ function internalLineTarget(tui: TUI): number {
404
+ return tui.terminal.rows < 18 ? 3 : 4;
405
+ }
406
+
407
+ function applyOsdyPi(
408
+ pi: ExtensionAPI,
409
+ ctx: ExtensionContext,
410
+ state: OsdyState,
411
+ notify = false,
412
+ ): void {
413
+ if (!ctx.hasUI) return;
414
+ const currentTheme = ctx.ui.theme.name;
415
+ if (!state.previousThemeName && currentTheme && currentTheme !== THEME_NAME)
416
+ state.previousThemeName = currentTheme;
417
+ const osdyTheme = ctx.ui.getTheme(THEME_NAME);
418
+ const themeResult = osdyTheme
419
+ ? ctx.ui.setTheme(osdyTheme)
420
+ : ctx.ui.setTheme(THEME_NAME);
421
+ if (!themeResult.success)
422
+ ctx.ui.notify(
423
+ `osdy-pi theme failed: ${themeResult.error ?? "unknown error"}`,
424
+ "warning",
425
+ );
426
+
427
+ ctx.ui.setHeader((_tui, theme) => {
428
+ let frame = 0;
429
+ let animationComplete = false;
430
+ const animationMode = asciiAnimationMode();
431
+ const animateAscii = animationMode !== "off";
432
+ const timer = animateAscii
433
+ ? setInterval(() => {
434
+ frame += 1;
435
+ if (animationMode === "intro" && frame >= INTRO_ANIMATION_FRAMES) {
436
+ animationComplete = true;
437
+ clearInterval(timer);
438
+ }
439
+ _tui.requestRender();
440
+ }, ANIMATION_INTERVAL_MS)
441
+ : undefined;
442
+ return {
443
+ render(width: number): string[] {
444
+ const animateFrame = animateAscii && !animationComplete;
445
+ const showMascot =
446
+ _tui.terminal.rows >= MASCOT_MIN_ROWS &&
447
+ width >= MASCOT_WIDTH + MASCOT_GAP + HEADER_WIDTH;
448
+ const useFullHeader = width >= HEADER_WIDTH;
449
+ const headerLines = useFullHeader ? HEADER : HEADER_FALLBACK;
450
+ const metadataRows = renderHeaderMetadata(pi, ctx, state, width);
451
+ const logoLines = headerLines.map((line, index) => {
452
+ const baseColor = useFullHeader && index >= 6 ? "mdLink" : "accent";
453
+ return animateAsciiLine(
454
+ line,
455
+ index,
456
+ frame,
457
+ theme,
458
+ baseColor,
459
+ "mdHeading",
460
+ "mdLink",
461
+ animateFrame,
462
+ );
463
+ });
464
+ const mascotLines = MASCOT.map((line, index) =>
465
+ animateAsciiLine(
466
+ line,
467
+ index,
468
+ frame,
469
+ theme,
470
+ "muted",
471
+ "mdCode",
472
+ "muted",
473
+ animateFrame,
474
+ ),
475
+ );
476
+ const headerBlock = showMascot
477
+ ? composeSideBySide(mascotLines, logoLines, width)
478
+ : logoLines.map((line) =>
479
+ useFullHeader
480
+ ? centerVisible(line, width)
481
+ : fitCenterVisible(line, width),
482
+ );
483
+ return [
484
+ "",
485
+ ...headerBlock,
486
+ "",
487
+ ...renderMetaRows(metadataRows, width, theme),
488
+ "",
489
+ ];
490
+ },
491
+ invalidate() {},
492
+ dispose() {
493
+ if (timer) clearInterval(timer);
494
+ },
495
+ };
496
+ });
497
+ ctx.ui.setFooter(
498
+ (_tui, theme, footerData) => new OsdyFooter(ctx, footerData, theme),
499
+ );
500
+ ctx.ui.setWorkingVisible(false);
501
+
502
+ class OsdyEditor extends CustomEditor {
503
+ constructor(
504
+ private readonly tuiRef: TUI,
505
+ theme: EditorTheme,
506
+ keybindings: KeybindingsManager,
507
+ ) {
508
+ super(tuiRef, theme, keybindings, { paddingX: 1 });
509
+ }
510
+ render(width: number): string[] {
511
+ if (width < 4 || this.isShowingAutocomplete()) return super.render(width);
512
+ const editorWidth = width;
513
+ const innerWidth = Math.max(1, editorWidth - 2);
514
+ const lines = super.render(innerWidth);
515
+ if (lines.length < 2) return lines;
516
+ let bottomIndex = Math.max(1, lines.length - 1);
517
+ const target = internalLineTarget(this.tuiRef);
518
+ const internalLines = bottomIndex - 1;
519
+ if (internalLines < target) {
520
+ const blank = " ".repeat(innerWidth);
521
+ const added = Array.from(
522
+ { length: target - internalLines },
523
+ () => blank,
524
+ );
525
+ lines.splice(bottomIndex, 0, ...added);
526
+ bottomIndex += added.length;
527
+ }
528
+ const borderColor = (text: string) => this.borderColor(text);
529
+ const side = borderColor("│");
530
+ for (let i = 1; i < bottomIndex; i += 1) {
531
+ const content = truncateToWidth(lines[i], innerWidth, "");
532
+ const padding = " ".repeat(
533
+ Math.max(0, innerWidth - visibleWidth(content)),
534
+ );
535
+ lines[i] = `${side}${content}${padding}${side}`;
536
+ }
537
+ const topLeft = ctx.ui.theme.fg("mdLink", " Osdy-Pi ");
538
+ const topRight = ctx.ui.theme.fg(
539
+ "muted",
540
+ ` ${modelLabel(ctx)} · think ${pi.getThinkingLevel()} `,
541
+ );
542
+ const bottomLeft = ctx.ui.theme.fg("muted", usageLabel(ctx));
543
+ lines[0] = `${borderColor("╭")}${fitBorder(topLeft, topRight, editorWidth - 2, borderColor)}${borderColor("╮")}`;
544
+ lines[bottomIndex] =
545
+ `${borderColor("╰")}${fitBorder(bottomLeft, "", editorWidth - 2, borderColor)}${borderColor("╯")}`;
546
+ return lines;
547
+ }
548
+ }
549
+ ctx.ui.setEditorComponent(
550
+ (tui, theme, keybindings) => new OsdyEditor(tui, theme, keybindings),
551
+ );
552
+ if (notify) ctx.ui.notify("osdy-pi enabled", "info");
553
+ }
554
+
555
+ function disableOsdyPi(ctx: ExtensionContext, state: OsdyState): void {
556
+ if (!ctx.hasUI) return;
557
+ ctx.ui.setHeader(undefined);
558
+ ctx.ui.setEditorComponent(undefined);
559
+ ctx.ui.setFooter(undefined);
560
+ ctx.ui.setWorkingVisible(true);
561
+ const targetTheme = state.previousThemeName ?? "dark";
562
+ const result = ctx.ui.setTheme(targetTheme);
563
+ if (!result.success && targetTheme !== "dark") ctx.ui.setTheme("dark");
564
+ ctx.ui.notify("osdy-pi disabled", "info");
565
+ }
566
+
567
+ export default function (pi: ExtensionAPI) {
568
+ const state: OsdyState = { enabled: true, gitLabel: "-", agentsLabel: "-" };
569
+ pi.on("session_start", (_event, ctx) => {
570
+ void pi
571
+ .exec("git", ["branch", "--show-current"], { cwd: ctx.cwd })
572
+ .then((result) => {
573
+ state.gitLabel = result.stdout.trim() || "detached";
574
+ })
575
+ .catch(() => {
576
+ state.gitLabel = "no-git";
577
+ });
578
+ void pi
579
+ .exec(
580
+ "find",
581
+ [ctx.cwd, "-name", "AGENTS.md", "-o", "-name", "AGENTS.MD"],
582
+ { cwd: ctx.cwd },
583
+ )
584
+ .then((result) => {
585
+ state.agentsLabel = String(
586
+ result.stdout.split("\n").filter(Boolean).length,
587
+ );
588
+ })
589
+ .catch(() => {
590
+ state.agentsLabel = "0";
591
+ });
592
+ if (state.enabled) applyOsdyPi(pi, ctx, state);
593
+ });
594
+ pi.registerCommand("osdy-pi", {
595
+ description: "Manage the Osdy Pi experience: enable, disable, or status.",
596
+ getArgumentCompletions(prefix: string) {
597
+ return ["enable", "disable", "status"]
598
+ .filter((value) => value.startsWith(prefix.trim()))
599
+ .map((value) => ({ value, label: value }));
600
+ },
601
+ handler: async (args, ctx) => {
602
+ const action = args.trim().split(/\s+/, 1)[0] || "status";
603
+ if (action === "enable") {
604
+ state.enabled = true;
605
+ applyOsdyPi(pi, ctx, state, true);
606
+ return;
607
+ }
608
+ if (action === "disable") {
609
+ state.enabled = false;
610
+ disableOsdyPi(ctx, state);
611
+ return;
612
+ }
613
+ if (action !== "status") {
614
+ ctx.ui.notify("Usage: /osdy-pi enable | disable | status", "warning");
615
+ return;
616
+ }
617
+ ctx.ui.notify(
618
+ `osdy-pi ${state.enabled ? "enabled" : "disabled"} · theme ${ctx.ui.theme.name ?? "unknown"} · animation ${asciiAnimationMode()} · ${modelLabel(ctx)} · ${usageLabel(ctx).trim()}`,
619
+ "info",
620
+ );
621
+ },
622
+ });
623
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "osdy-pi",
3
+ "version": "0.1.0",
4
+ "description": "Osdy Pi package: themes, header, and custom editor for Pi.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "keywords": [
8
+ "pi-package",
9
+ "pi",
10
+ "theme",
11
+ "extension"
12
+ ],
13
+ "files": [
14
+ "README.md",
15
+ "LICENSE",
16
+ "themes",
17
+ "extensions"
18
+ ],
19
+ "pi": {
20
+ "themes": [
21
+ "./themes/osdy-pi-dark.json",
22
+ "./themes/osdy-pi-light.json"
23
+ ],
24
+ "extensions": [
25
+ "./extensions/osdy-pi.ts"
26
+ ]
27
+ },
28
+ "peerDependencies": {
29
+ "@earendil-works/pi-coding-agent": "*",
30
+ "@earendil-works/pi-tui": "*"
31
+ }
32
+ }
@@ -0,0 +1,78 @@
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-dark",
4
+ "vars": {
5
+ "void": "#080611",
6
+ "surface": "#151022",
7
+ "surface2": "#211433",
8
+ "pink": "#FF4FD8",
9
+ "hotPink": "#FF2EA6",
10
+ "purple": "#9B5CFF",
11
+ "violet": "#C084FC",
12
+ "cyan": "#38D5FF",
13
+ "textSoft": "#F8F4FF",
14
+ "lavenderGray": "#B9A7D8",
15
+ "dimPurple": "#76628F",
16
+ "mint": "#2FE6A6",
17
+ "gold": "#F5C542",
18
+ "coral": "#FF6B8B"
19
+ },
20
+ "colors": {
21
+ "accent": "pink",
22
+ "border": "purple",
23
+ "borderAccent": "hotPink",
24
+ "borderMuted": "#3A2852",
25
+ "success": "mint",
26
+ "error": "coral",
27
+ "warning": "purple",
28
+ "muted": "lavenderGray",
29
+ "dim": "dimPurple",
30
+ "text": "textSoft",
31
+ "thinkingText": "lavenderGray",
32
+ "selectedBg": "surface2",
33
+ "userMessageBg": "surface",
34
+ "userMessageText": "textSoft",
35
+ "customMessageBg": "#1C1230",
36
+ "customMessageText": "textSoft",
37
+ "customMessageLabel": "pink",
38
+ "toolPendingBg": "#17112B",
39
+ "toolSuccessBg": "#10261F",
40
+ "toolErrorBg": "#351326",
41
+ "toolTitle": "pink",
42
+ "toolOutput": "textSoft",
43
+ "mdHeading": "violet",
44
+ "mdLink": "cyan",
45
+ "mdLinkUrl": "lavenderGray",
46
+ "mdCode": "violet",
47
+ "mdCodeBlock": "textSoft",
48
+ "mdCodeBlockBorder": "purple",
49
+ "mdQuote": "lavenderGray",
50
+ "mdQuoteBorder": "purple",
51
+ "mdHr": "#3A2852",
52
+ "mdListBullet": "pink",
53
+ "toolDiffAdded": "mint",
54
+ "toolDiffRemoved": "coral",
55
+ "toolDiffContext": "lavenderGray",
56
+ "syntaxComment": "dimPurple",
57
+ "syntaxKeyword": "pink",
58
+ "syntaxFunction": "cyan",
59
+ "syntaxVariable": "textSoft",
60
+ "syntaxString": "mint",
61
+ "syntaxNumber": "violet",
62
+ "syntaxType": "violet",
63
+ "syntaxOperator": "hotPink",
64
+ "syntaxPunctuation": "lavenderGray",
65
+ "thinkingOff": "#3A2852",
66
+ "thinkingMinimal": "lavenderGray",
67
+ "thinkingLow": "purple",
68
+ "thinkingMedium": "pink",
69
+ "thinkingHigh": "violet",
70
+ "thinkingXhigh": "coral",
71
+ "bashMode": "purple"
72
+ },
73
+ "export": {
74
+ "pageBg": "void",
75
+ "cardBg": "surface",
76
+ "infoBg": "surface2"
77
+ }
78
+ }
@@ -0,0 +1,78 @@
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-light",
4
+ "vars": {
5
+ "page": "#FFF7FD",
6
+ "surface": "#F6EAFE",
7
+ "surface2": "#EBD8FF",
8
+ "ink": "#1B102A",
9
+ "pink": "#C2188F",
10
+ "hotPink": "#E91E9D",
11
+ "purple": "#6D28D9",
12
+ "violet": "#8B5CF6",
13
+ "cyan": "#007EA7",
14
+ "lavenderGray": "#645170",
15
+ "dimPurple": "#9B86AA",
16
+ "mint": "#0F8F68",
17
+ "gold": "#9E7414",
18
+ "coral": "#D63D68"
19
+ },
20
+ "colors": {
21
+ "accent": "pink",
22
+ "border": "purple",
23
+ "borderAccent": "hotPink",
24
+ "borderMuted": "dimPurple",
25
+ "success": "mint",
26
+ "error": "coral",
27
+ "warning": "purple",
28
+ "muted": "lavenderGray",
29
+ "dim": "dimPurple",
30
+ "text": "ink",
31
+ "thinkingText": "lavenderGray",
32
+ "selectedBg": "surface2",
33
+ "userMessageBg": "surface",
34
+ "userMessageText": "ink",
35
+ "customMessageBg": "surface",
36
+ "customMessageText": "ink",
37
+ "customMessageLabel": "pink",
38
+ "toolPendingBg": "#F2E7FF",
39
+ "toolSuccessBg": "#DDF7ED",
40
+ "toolErrorBg": "#FFE3EF",
41
+ "toolTitle": "pink",
42
+ "toolOutput": "ink",
43
+ "mdHeading": "violet",
44
+ "mdLink": "purple",
45
+ "mdLinkUrl": "lavenderGray",
46
+ "mdCode": "violet",
47
+ "mdCodeBlock": "ink",
48
+ "mdCodeBlockBorder": "purple",
49
+ "mdQuote": "lavenderGray",
50
+ "mdQuoteBorder": "purple",
51
+ "mdHr": "dimPurple",
52
+ "mdListBullet": "pink",
53
+ "toolDiffAdded": "mint",
54
+ "toolDiffRemoved": "coral",
55
+ "toolDiffContext": "lavenderGray",
56
+ "syntaxComment": "#806D91",
57
+ "syntaxKeyword": "pink",
58
+ "syntaxFunction": "purple",
59
+ "syntaxVariable": "ink",
60
+ "syntaxString": "mint",
61
+ "syntaxNumber": "violet",
62
+ "syntaxType": "violet",
63
+ "syntaxOperator": "hotPink",
64
+ "syntaxPunctuation": "lavenderGray",
65
+ "thinkingOff": "dimPurple",
66
+ "thinkingMinimal": "lavenderGray",
67
+ "thinkingLow": "purple",
68
+ "thinkingMedium": "pink",
69
+ "thinkingHigh": "violet",
70
+ "thinkingXhigh": "coral",
71
+ "bashMode": "purple"
72
+ },
73
+ "export": {
74
+ "pageBg": "page",
75
+ "cardBg": "#FFFFFF",
76
+ "infoBg": "surface"
77
+ }
78
+ }