osdy-pi 0.1.2 → 0.1.4
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 +5 -2
- package/extensions/osdy-pi/animation.ts +59 -0
- package/extensions/osdy-pi/border.ts +49 -0
- package/extensions/osdy-pi/constants.ts +75 -0
- package/extensions/osdy-pi/format.ts +42 -0
- package/extensions/osdy-pi/metrics.ts +210 -0
- package/extensions/osdy-pi/runtime-helpers.ts +101 -0
- package/extensions/osdy-pi/runtime.ts +147 -0
- package/extensions/osdy-pi/types.ts +50 -0
- package/extensions/osdy-pi/ui.ts +259 -0
- package/extensions/osdy-pi/utils.ts +54 -0
- package/extensions/osdy-pi/working-controller.ts +87 -0
- package/extensions/osdy-pi.ts +4 -650
- package/mapche1.png +0 -0
- package/package.json +45 -31
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# Osdy Pi
|
|
2
2
|
|
|
3
|
-
<img width="1857" height="847" alt="image" src="https://github.com/user-attachments/assets/
|
|
3
|
+
<img width="1857" height="847" alt="image" src="https://github.com/user-attachments/assets/028eeb14-3f43-4f1c-9603-0c55a8d2856d" />
|
|
4
4
|
|
|
5
5
|
Theme package for [Pi](https://github.com/earendil-works/pi) with the Osdy terminal style: neon pink/purple colors, a custom ASCII header, and a framed editor experience.
|
|
6
6
|
|
|
7
|
+
Visit the Osdy landing page: [landing-osdy.vercel.app](https://landing-osdy.vercel.app/).
|
|
8
|
+
|
|
7
9
|
## What you get
|
|
8
10
|
|
|
9
11
|
- **Dark theme:** `osdy-pi-dark`, enabled by default when the package starts.
|
|
@@ -14,7 +16,6 @@ Theme package for [Pi](https://github.com/earendil-works/pi) with the Osdy termi
|
|
|
14
16
|
|
|
15
17
|
<img width="1280" height="433" alt="image" src="https://github.com/user-attachments/assets/20c7624d-9ad8-4494-97fb-6b6d81aaf328" />
|
|
16
18
|
|
|
17
|
-
|
|
18
19
|
## Install in Pi
|
|
19
20
|
|
|
20
21
|
Install the package directly from GitHub:
|
|
@@ -84,6 +85,8 @@ To install it from a local path:
|
|
|
84
85
|
pi install /absolute/path/to/Osdy-Pi
|
|
85
86
|
```
|
|
86
87
|
|
|
88
|
+

|
|
89
|
+
|
|
87
90
|
## Package contents
|
|
88
91
|
|
|
89
92
|
```text
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ANIMATION_ENABLED } from "./constants.js";
|
|
2
|
+
import type { AnimationMode, SimpleTheme } from "./types.js";
|
|
3
|
+
import { positiveModulo } from "./utils.js";
|
|
4
|
+
|
|
5
|
+
export type AsciiAnimationStyle = "static" | "animated";
|
|
6
|
+
|
|
7
|
+
const ANIMATION_OVERRIDE_MAP: Record<string, AnimationMode> = {
|
|
8
|
+
"0": "off",
|
|
9
|
+
"1": "continuous",
|
|
10
|
+
off: "off",
|
|
11
|
+
on: "continuous",
|
|
12
|
+
intro: "intro",
|
|
13
|
+
continuous: "continuous",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function getAnimationOverrideMode(
|
|
17
|
+
override: string | undefined,
|
|
18
|
+
): AnimationMode | undefined {
|
|
19
|
+
return override ? ANIMATION_OVERRIDE_MAP[override] : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function asciiAnimationMode(): AnimationMode {
|
|
23
|
+
if (!ANIMATION_ENABLED) return "off";
|
|
24
|
+
return getAnimationOverrideMode(process.env.OSDY_PI_ANIMATION) ?? "intro";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getAnimatedAsciiColor(
|
|
28
|
+
wave: number,
|
|
29
|
+
baseColor: string,
|
|
30
|
+
highlightColor: string,
|
|
31
|
+
trailColor: string,
|
|
32
|
+
): string {
|
|
33
|
+
if (wave <= 3) return highlightColor;
|
|
34
|
+
if (wave <= 8) return trailColor;
|
|
35
|
+
return baseColor;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function animateAsciiLine(
|
|
39
|
+
line: string,
|
|
40
|
+
lineIndex: number,
|
|
41
|
+
frame: number,
|
|
42
|
+
theme: SimpleTheme,
|
|
43
|
+
baseColor: string,
|
|
44
|
+
highlightColor: string,
|
|
45
|
+
trailColor: string,
|
|
46
|
+
style: AsciiAnimationStyle,
|
|
47
|
+
): string {
|
|
48
|
+
if (style === "static") return theme.fg(baseColor, line);
|
|
49
|
+
return Array.from(line)
|
|
50
|
+
.map((char, charIndex) => {
|
|
51
|
+
if (char === " ") return char;
|
|
52
|
+
const wave = positiveModulo(charIndex + lineIndex * 2 - frame * 5, 44);
|
|
53
|
+
return theme.fg(
|
|
54
|
+
getAnimatedAsciiColor(wave, baseColor, highlightColor, trailColor),
|
|
55
|
+
char,
|
|
56
|
+
);
|
|
57
|
+
})
|
|
58
|
+
.join("");
|
|
59
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
function borderOverflows(left: string, right: string, width: number): boolean {
|
|
4
|
+
const fixedWidth = 2;
|
|
5
|
+
const minGap = 1;
|
|
6
|
+
return fixedWidth + visibleWidth(left) + visibleWidth(right) + minGap > width;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function trimBorderText(value: string): string {
|
|
10
|
+
return truncateToWidth(value, Math.max(0, visibleWidth(value) - 1), "");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function shrinkBorderTexts(
|
|
14
|
+
left: string,
|
|
15
|
+
right: string,
|
|
16
|
+
width: number,
|
|
17
|
+
): { leftText: string; rightText: string } {
|
|
18
|
+
let leftText = left;
|
|
19
|
+
let rightText = right;
|
|
20
|
+
while (
|
|
21
|
+
borderOverflows(leftText, rightText, width) &&
|
|
22
|
+
visibleWidth(rightText) > 0
|
|
23
|
+
) {
|
|
24
|
+
rightText = trimBorderText(rightText);
|
|
25
|
+
}
|
|
26
|
+
while (
|
|
27
|
+
borderOverflows(leftText, rightText, width) &&
|
|
28
|
+
visibleWidth(leftText) > 0
|
|
29
|
+
) {
|
|
30
|
+
leftText = trimBorderText(leftText);
|
|
31
|
+
}
|
|
32
|
+
return { leftText, rightText };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function fitBorder(
|
|
36
|
+
left: string,
|
|
37
|
+
right: string,
|
|
38
|
+
width: number,
|
|
39
|
+
color: (text: string) => string,
|
|
40
|
+
): string {
|
|
41
|
+
if (width <= 0) return "";
|
|
42
|
+
if (width === 1) return color("─");
|
|
43
|
+
const { leftText, rightText } = shrinkBorderTexts(left, right, width);
|
|
44
|
+
const fillWidth = Math.max(
|
|
45
|
+
0,
|
|
46
|
+
width - 2 - visibleWidth(leftText) - visibleWidth(rightText),
|
|
47
|
+
);
|
|
48
|
+
return `${color("─")}${leftText}${color("─".repeat(fillWidth))}${rightText}${color("─")}`;
|
|
49
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
export const THEME_NAME = "osdy-pi-dark";
|
|
4
|
+
export const ANIMATION_ENABLED = true;
|
|
5
|
+
export const ANIMATION_INTERVAL_MS = 30;
|
|
6
|
+
export const INTRO_ANIMATION_FRAMES = 28;
|
|
7
|
+
export const WORKING_SPINNER_FRAMES = [
|
|
8
|
+
"⠋",
|
|
9
|
+
"⠙",
|
|
10
|
+
"⠹",
|
|
11
|
+
"⠸",
|
|
12
|
+
"⠼",
|
|
13
|
+
"⠴",
|
|
14
|
+
"⠦",
|
|
15
|
+
"⠧",
|
|
16
|
+
"⠇",
|
|
17
|
+
"⠏",
|
|
18
|
+
];
|
|
19
|
+
export const WORKING_WIDGET_KEY = "osdy-pi-working";
|
|
20
|
+
export const MASCOT_MIN_ROWS = 34;
|
|
21
|
+
export const MASCOT_GAP = 0;
|
|
22
|
+
|
|
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
|
+
" ##****=*****==",
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
export const MASCOT_WIDTH = MASCOT.reduce(
|
|
49
|
+
(maxWidth, line) => Math.max(maxWidth, visibleWidth(line)),
|
|
50
|
+
0,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
export const HEADER = [
|
|
54
|
+
"░█████╗░░██████╗██████╗░██╗░░░██╗░░░░░░██████╗░██╗",
|
|
55
|
+
"██╔══██╗██╔════╝██╔══██╗╚██╗░██╔╝░░░░░░██╔══██╗ ║",
|
|
56
|
+
"██║░░██║╚█████╗░██║░░██║░╚████╔╝░█████╗██████╔╝██║",
|
|
57
|
+
"██║░░██║░╚═══██╗██║░░██║░░╚██╔╝░░╚════╝██╔═══╝░██║",
|
|
58
|
+
"╚█████╔╝██████╔╝██████╔╝░░░██║░░░░░░░░░██║░░░░░██║",
|
|
59
|
+
"░╚════╝░╚═════╝░╚═════╝░░░░╚═╝░░░░░░░░░╚═╝░░░░░╚═╝",
|
|
60
|
+
" ╭━╮╱╱╱╱╱╱╭╮╱╱╱╱╱╭╮╱╱╱╱╱╭╮╱╱╭╮╭╮╱╱╱╱╭━╮",
|
|
61
|
+
" ┃╭╋━┳━━┳━╋╋╮╭━╮╭╯┣━┳┳╮╭╯┣━╮┣╋╯┣━┳━╮┃━┫",
|
|
62
|
+
" ┃╰┫╋┃┃┃┃╋┃┃╰┫╋╰┫╋┃╋┃╭╯┃╋┃┻┫┃┃╋┃┻┫╋╰╋━┃",
|
|
63
|
+
" ╰━┻━┻┻┻┫╭┻┻━┻━━┻━┻━┻╯╱╰━┻━╯╰┻━┻━┻━━┻━╯",
|
|
64
|
+
" ╱╱╱╱╱╱╱╰╯ </>",
|
|
65
|
+
] as const;
|
|
66
|
+
|
|
67
|
+
export const HEADER_WIDTH = HEADER.reduce(
|
|
68
|
+
(maxWidth, line) => Math.max(maxWidth, visibleWidth(line)),
|
|
69
|
+
0,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
export const HEADER_FALLBACK = [
|
|
73
|
+
"OSDY - PI",
|
|
74
|
+
"</> Compilador de ideas",
|
|
75
|
+
] as const;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
export function formatPath(cwd: string): string {
|
|
4
|
+
const home = process.env.HOME;
|
|
5
|
+
return home && cwd.startsWith(home) ? `~${cwd.slice(home.length)}` : cwd;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function getMiddleWidths(maxWidth: number): {
|
|
9
|
+
headWidth: number;
|
|
10
|
+
tailWidth: number;
|
|
11
|
+
} {
|
|
12
|
+
const headWidth = Math.max(1, Math.ceil((maxWidth - 1) / 2));
|
|
13
|
+
return {
|
|
14
|
+
headWidth,
|
|
15
|
+
tailWidth: Math.max(1, maxWidth - 1 - headWidth),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getTrailingText(value: string, tailWidth: number): string {
|
|
20
|
+
const reversedValue = Array.from(value).reverse().join("");
|
|
21
|
+
return truncateToWidth(reversedValue, tailWidth, "")
|
|
22
|
+
.split("")
|
|
23
|
+
.reverse()
|
|
24
|
+
.join("");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function truncateMiddle(value: string, maxWidth: number): string {
|
|
28
|
+
if (maxWidth <= 0) return "";
|
|
29
|
+
if (visibleWidth(value) <= maxWidth) return value;
|
|
30
|
+
if (maxWidth <= 3) return truncateToWidth(value, maxWidth);
|
|
31
|
+
const { headWidth, tailWidth } = getMiddleWidths(maxWidth);
|
|
32
|
+
const head = truncateToWidth(value, headWidth, "");
|
|
33
|
+
const tail = getTrailingText(value, tailWidth);
|
|
34
|
+
return `${head}…${tail}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function shortNumber(value: number): string {
|
|
38
|
+
if (!Number.isFinite(value)) return "0";
|
|
39
|
+
if (Math.abs(value) < 1_000) return Math.round(value).toString();
|
|
40
|
+
if (Math.abs(value) < 1_000_000) return `${(value / 1_000).toFixed(1)}k`;
|
|
41
|
+
return `${(value / 1_000_000).toFixed(1)}m`;
|
|
42
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ExtensionAPI,
|
|
3
|
+
type ExtensionContext,
|
|
4
|
+
VERSION,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
7
|
+
import { formatPath, shortNumber, truncateMiddle } from "./format.js";
|
|
8
|
+
import type {
|
|
9
|
+
AssistantSessionEntry,
|
|
10
|
+
HeaderMetaRow,
|
|
11
|
+
OsdyState,
|
|
12
|
+
SimpleTheme,
|
|
13
|
+
} from "./types.js";
|
|
14
|
+
import { centerVisible, fitCenterVisible } from "./utils.js";
|
|
15
|
+
|
|
16
|
+
function isAssistantSessionEntry(
|
|
17
|
+
entry: unknown,
|
|
18
|
+
): entry is AssistantSessionEntry {
|
|
19
|
+
if (!entry || typeof entry !== "object") return false;
|
|
20
|
+
const candidate = entry as { type?: unknown; message?: unknown };
|
|
21
|
+
if (candidate.type !== "message") return false;
|
|
22
|
+
if (!candidate.message || typeof candidate.message !== "object") return false;
|
|
23
|
+
const message = candidate.message as { role?: unknown };
|
|
24
|
+
return message.role === "assistant";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function modelLabel(ctx: ExtensionContext): string {
|
|
28
|
+
const model = ctx.model;
|
|
29
|
+
if (!model) return "no model";
|
|
30
|
+
return model.provider ? `${model.provider}/${model.id}` : model.id;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getContextLabel(ctx: ExtensionContext): string {
|
|
34
|
+
const context = ctx.getContextUsage();
|
|
35
|
+
const contextWindow = context?.contextWindow ?? ctx.model?.contextWindow;
|
|
36
|
+
const percent = context?.percent ?? null;
|
|
37
|
+
return contextWindow
|
|
38
|
+
? `ctx ${percent === null ? "?" : Math.round(percent)}%/${shortNumber(contextWindow)}`
|
|
39
|
+
: "ctx ?";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getCacheLabel(totals: UsageTotals): string {
|
|
43
|
+
return totals.cacheRead || totals.cacheWrite
|
|
44
|
+
? ` R${shortNumber(totals.cacheRead)} W${shortNumber(totals.cacheWrite)}`
|
|
45
|
+
: "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function usageLabel(ctx: ExtensionContext): string {
|
|
49
|
+
const totals = collectUsageTotals(ctx);
|
|
50
|
+
const cacheText = getCacheLabel(totals);
|
|
51
|
+
const ctxText = getContextLabel(ctx);
|
|
52
|
+
return ` tok ↑${shortNumber(totals.input)} ↓${shortNumber(totals.output)}${cacheText} · $${totals.cost.toFixed(4)} · ${ctxText} `;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type UsageTotals = {
|
|
56
|
+
input: number;
|
|
57
|
+
output: number;
|
|
58
|
+
cacheRead: number;
|
|
59
|
+
cacheWrite: number;
|
|
60
|
+
cost: number;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function createUsageTotals(): UsageTotals {
|
|
64
|
+
return {
|
|
65
|
+
input: 0,
|
|
66
|
+
output: 0,
|
|
67
|
+
cacheRead: 0,
|
|
68
|
+
cacheWrite: 0,
|
|
69
|
+
cost: 0,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function mergeUsageTotals(
|
|
74
|
+
totals: UsageTotals,
|
|
75
|
+
entry: AssistantSessionEntry,
|
|
76
|
+
): UsageTotals {
|
|
77
|
+
const usage = entry.message?.usage;
|
|
78
|
+
if (!usage) return totals;
|
|
79
|
+
return {
|
|
80
|
+
input: totals.input + (usage.input ?? 0),
|
|
81
|
+
output: totals.output + (usage.output ?? 0),
|
|
82
|
+
cacheRead: totals.cacheRead + (usage.cacheRead ?? 0),
|
|
83
|
+
cacheWrite: totals.cacheWrite + (usage.cacheWrite ?? 0),
|
|
84
|
+
cost: totals.cost + (usage.cost?.total ?? 0),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function collectUsageTotals(ctx: ExtensionContext): UsageTotals {
|
|
89
|
+
return ctx.sessionManager
|
|
90
|
+
.getEntries()
|
|
91
|
+
.reduce<UsageTotals>((totals, entry) => {
|
|
92
|
+
return isAssistantSessionEntry(entry)
|
|
93
|
+
? mergeUsageTotals(totals, entry)
|
|
94
|
+
: totals;
|
|
95
|
+
}, createUsageTotals());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getToolSources(pi: ExtensionAPI) {
|
|
99
|
+
return pi.getAllTools().map((tool) => ({
|
|
100
|
+
name: tool.name.toLowerCase(),
|
|
101
|
+
origin: tool.sourceInfo?.origin?.toLowerCase() ?? "",
|
|
102
|
+
source: tool.sourceInfo?.source?.toLowerCase() ?? "",
|
|
103
|
+
path: tool.sourceInfo?.path?.toLowerCase() ?? "",
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isMcpTool(tool: ReturnType<typeof getToolSources>[number]): boolean {
|
|
108
|
+
return tool.name.includes("mcp") || tool.source.includes("mcp");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getToolServerKey(
|
|
112
|
+
tool: ReturnType<typeof getToolSources>[number],
|
|
113
|
+
): string {
|
|
114
|
+
return tool.source || tool.path || tool.name;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getMcpServerCount(pi: ExtensionAPI): number {
|
|
118
|
+
const serverKeys = new Set(
|
|
119
|
+
getToolSources(pi).filter(isMcpTool).map(getToolServerKey),
|
|
120
|
+
);
|
|
121
|
+
return serverKeys.size;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function countToolsByOrigin(pi: ExtensionAPI, origin: string): number {
|
|
125
|
+
return getToolSources(pi).filter((tool) => tool.origin === origin).length;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function countToolsBySource(pi: ExtensionAPI, source: string): number {
|
|
129
|
+
return getToolSources(pi).filter((tool) => tool.source.includes(source))
|
|
130
|
+
.length;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getPathWidth(width: number): number {
|
|
134
|
+
return width < 110 ? 34 : 52;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function renderHeaderMetadata(
|
|
138
|
+
pi: ExtensionAPI,
|
|
139
|
+
ctx: ExtensionContext,
|
|
140
|
+
state: OsdyState,
|
|
141
|
+
width: number,
|
|
142
|
+
): HeaderMetaRow[] {
|
|
143
|
+
const allTools = pi.getAllTools().length;
|
|
144
|
+
const git = state.gitLabel === "no-git" ? "Not a git repo" : state.gitLabel;
|
|
145
|
+
const path = truncateMiddle(formatPath(ctx.cwd), getPathWidth(width));
|
|
146
|
+
const mcpServerCount = getMcpServerCount(pi);
|
|
147
|
+
const pluginCount = countToolsByOrigin(pi, "package");
|
|
148
|
+
const extensionCount = countToolsBySource(pi, "extension");
|
|
149
|
+
return [
|
|
150
|
+
{
|
|
151
|
+
leftLabel: "GIT:",
|
|
152
|
+
leftValue: git,
|
|
153
|
+
rightLabel: "PATH:",
|
|
154
|
+
rightValue: path,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
leftLabel: "MCP:",
|
|
158
|
+
leftValue: `${mcpServerCount} servers`,
|
|
159
|
+
rightLabel: "PLUGINS:",
|
|
160
|
+
rightValue: `${pluginCount} package`,
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
leftLabel: "AGENTS:",
|
|
164
|
+
leftValue: `${state.agentsLabel} loaded`,
|
|
165
|
+
rightLabel: "EXTENSIONS:",
|
|
166
|
+
rightValue: `${extensionCount} active`,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
leftLabel: "VER:",
|
|
170
|
+
leftValue: VERSION,
|
|
171
|
+
rightLabel: "TOOLS:",
|
|
172
|
+
rightValue: `${allTools} customs`,
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function renderMetaRows(
|
|
178
|
+
rows: HeaderMetaRow[],
|
|
179
|
+
width: number,
|
|
180
|
+
theme: SimpleTheme,
|
|
181
|
+
): string[] {
|
|
182
|
+
const labelWidth = 11;
|
|
183
|
+
const leftValueWidth = 32;
|
|
184
|
+
const rightValueWidth = 52;
|
|
185
|
+
const gapWidth = 10;
|
|
186
|
+
const blockWidth =
|
|
187
|
+
labelWidth +
|
|
188
|
+
1 +
|
|
189
|
+
leftValueWidth +
|
|
190
|
+
gapWidth +
|
|
191
|
+
labelWidth +
|
|
192
|
+
1 +
|
|
193
|
+
rightValueWidth;
|
|
194
|
+
return rows.map((row) => {
|
|
195
|
+
const leftLabel = theme.fg("mdLink", row.leftLabel.padEnd(labelWidth));
|
|
196
|
+
const leftValue = theme.fg(
|
|
197
|
+
"accent",
|
|
198
|
+
truncateToWidth(row.leftValue, leftValueWidth).padEnd(leftValueWidth),
|
|
199
|
+
);
|
|
200
|
+
const rightLabel = theme.fg("mdLink", row.rightLabel.padEnd(labelWidth));
|
|
201
|
+
const rightValue = theme.fg(
|
|
202
|
+
"accent",
|
|
203
|
+
truncateToWidth(row.rightValue, rightValueWidth).padEnd(rightValueWidth),
|
|
204
|
+
);
|
|
205
|
+
const rowText = `${leftLabel} ${leftValue}${" ".repeat(gapWidth)}${rightLabel} ${rightValue}`;
|
|
206
|
+
return width >= blockWidth
|
|
207
|
+
? centerVisible(rowText, width)
|
|
208
|
+
: fitCenterVisible(rowText, width);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { asciiAnimationMode } from "./animation.js";
|
|
6
|
+
import { THEME_NAME, WORKING_WIDGET_KEY } from "./constants.js";
|
|
7
|
+
import { modelLabel, usageLabel } from "./metrics.js";
|
|
8
|
+
import type { OsdyState, WorkingWidgetState } from "./types.js";
|
|
9
|
+
import {
|
|
10
|
+
createEditorComponent,
|
|
11
|
+
createFooterComponent,
|
|
12
|
+
createHeaderComponent,
|
|
13
|
+
createWorkingWidgetFactory,
|
|
14
|
+
} from "./ui.js";
|
|
15
|
+
|
|
16
|
+
function shouldRememberTheme(
|
|
17
|
+
previousThemeName: string | undefined,
|
|
18
|
+
currentTheme: string | undefined,
|
|
19
|
+
): currentTheme is string {
|
|
20
|
+
return (
|
|
21
|
+
previousThemeName === undefined &&
|
|
22
|
+
currentTheme !== undefined &&
|
|
23
|
+
currentTheme !== THEME_NAME
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function rememberPreviousTheme(
|
|
28
|
+
ctx: ExtensionContext,
|
|
29
|
+
state: OsdyState,
|
|
30
|
+
): void {
|
|
31
|
+
const currentTheme = ctx.ui.theme.name;
|
|
32
|
+
if (shouldRememberTheme(state.previousThemeName, currentTheme)) {
|
|
33
|
+
state.previousThemeName = currentTheme;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function applyOsdyTheme(ctx: ExtensionContext): void {
|
|
38
|
+
const osdyTheme = ctx.ui.getTheme(THEME_NAME);
|
|
39
|
+
const themeResult = osdyTheme
|
|
40
|
+
? ctx.ui.setTheme(osdyTheme)
|
|
41
|
+
: ctx.ui.setTheme(THEME_NAME);
|
|
42
|
+
if (!themeResult.success) {
|
|
43
|
+
ctx.ui.notify(
|
|
44
|
+
`osdy-pi theme failed: ${themeResult.error ?? "unknown error"}`,
|
|
45
|
+
"warning",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function mountOsdyUi(
|
|
51
|
+
pi: ExtensionAPI,
|
|
52
|
+
ctx: ExtensionContext,
|
|
53
|
+
state: OsdyState,
|
|
54
|
+
workingState: WorkingWidgetState,
|
|
55
|
+
): void {
|
|
56
|
+
ctx.ui.setHeader(createHeaderComponent(pi, ctx, state));
|
|
57
|
+
ctx.ui.setFooter((_tui, theme, footerData) =>
|
|
58
|
+
createFooterComponent(ctx, footerData, theme),
|
|
59
|
+
);
|
|
60
|
+
ctx.ui.setWorkingVisible(false);
|
|
61
|
+
ctx.ui.setWidget(
|
|
62
|
+
WORKING_WIDGET_KEY,
|
|
63
|
+
createWorkingWidgetFactory(workingState),
|
|
64
|
+
{ placement: "aboveEditor" },
|
|
65
|
+
);
|
|
66
|
+
ctx.ui.setEditorComponent(createEditorComponent(pi, ctx));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function applyOsdyPi(
|
|
70
|
+
pi: ExtensionAPI,
|
|
71
|
+
ctx: ExtensionContext,
|
|
72
|
+
state: OsdyState,
|
|
73
|
+
workingState: WorkingWidgetState,
|
|
74
|
+
notify = false,
|
|
75
|
+
): void {
|
|
76
|
+
if (!ctx.hasUI) return;
|
|
77
|
+
rememberPreviousTheme(ctx, state);
|
|
78
|
+
applyOsdyTheme(ctx);
|
|
79
|
+
mountOsdyUi(pi, ctx, state, workingState);
|
|
80
|
+
if (notify) ctx.ui.notify("osdy-pi enabled", "info");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function disableOsdyPi(ctx: ExtensionContext, state: OsdyState): void {
|
|
84
|
+
if (!ctx.hasUI) return;
|
|
85
|
+
ctx.ui.setHeader(undefined);
|
|
86
|
+
ctx.ui.setEditorComponent(undefined);
|
|
87
|
+
ctx.ui.setFooter(undefined);
|
|
88
|
+
ctx.ui.setWidget(WORKING_WIDGET_KEY, undefined);
|
|
89
|
+
ctx.ui.setWorkingVisible(true);
|
|
90
|
+
const targetTheme = state.previousThemeName ?? "dark";
|
|
91
|
+
const result = ctx.ui.setTheme(targetTheme);
|
|
92
|
+
if (!result.success && targetTheme !== "dark") ctx.ui.setTheme("dark");
|
|
93
|
+
ctx.ui.notify("osdy-pi disabled", "info");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function notifyStatus(ctx: ExtensionContext, state: OsdyState): void {
|
|
97
|
+
ctx.ui.notify(
|
|
98
|
+
`osdy-pi ${state.enabled ? "enabled" : "disabled"} · theme ${ctx.ui.theme.name ?? "unknown"} · animation ${asciiAnimationMode()} · ${modelLabel(ctx)} · ${usageLabel(ctx).trim()}`,
|
|
99
|
+
"info",
|
|
100
|
+
);
|
|
101
|
+
}
|