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/extensions/osdy-pi.ts
CHANGED
|
@@ -1,652 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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";
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { registerOsdyPi } from "./osdy-pi/runtime.js";
|
|
15
3
|
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
private readonly ctx: ExtensionContext;
|
|
90
|
-
private readonly footerData: any;
|
|
91
|
-
private readonly theme: { fg(name: string, text: string): string };
|
|
92
|
-
|
|
93
|
-
constructor(
|
|
94
|
-
ctx: ExtensionContext,
|
|
95
|
-
footerData: any,
|
|
96
|
-
theme: { fg(name: string, text: string): string },
|
|
97
|
-
) {
|
|
98
|
-
this.ctx = ctx;
|
|
99
|
-
this.footerData = footerData;
|
|
100
|
-
this.theme = theme;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
render(width: number): string[] {
|
|
104
|
-
let location = formatPath(this.ctx.cwd);
|
|
105
|
-
const branch = this.footerData.getGitBranch?.();
|
|
106
|
-
if (branch) location = `${location} (${branch})`;
|
|
107
|
-
const locationLine = truncateToWidth(
|
|
108
|
-
this.theme.fg("dim", location),
|
|
109
|
-
width,
|
|
110
|
-
this.theme.fg("dim", "..."),
|
|
111
|
-
);
|
|
112
|
-
const statuses = this.footerData.getExtensionStatuses?.() ?? new Map();
|
|
113
|
-
const statusEntries = Array.from(statuses.entries()) as Array<
|
|
114
|
-
[string, string]
|
|
115
|
-
>;
|
|
116
|
-
const statusLine = statusEntries
|
|
117
|
-
.sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
|
|
118
|
-
.map(([, text]) => sanitizeStatusText(text))
|
|
119
|
-
.join(" ");
|
|
120
|
-
return statusLine
|
|
121
|
-
? [
|
|
122
|
-
locationLine,
|
|
123
|
-
truncateToWidth(statusLine, width, this.theme.fg("dim", "...")),
|
|
124
|
-
]
|
|
125
|
-
: [locationLine];
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
invalidate(): void {}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function sanitizeStatusText(text: string): string {
|
|
132
|
-
return text
|
|
133
|
-
.replace(/[\r\n\t]/g, " ")
|
|
134
|
-
.replace(/ +/g, " ")
|
|
135
|
-
.trim();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function positiveModulo(value: number, divisor: number): number {
|
|
139
|
-
return ((value % divisor) + divisor) % divisor;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function asciiAnimationMode(): AnimationMode {
|
|
143
|
-
if (!ANIMATION_ENABLED) return "off";
|
|
144
|
-
const override = process.env.OSDY_PI_ANIMATION;
|
|
145
|
-
if (override === "1" || override === "on" || override === "continuous")
|
|
146
|
-
return "continuous";
|
|
147
|
-
if (override === "intro") return "intro";
|
|
148
|
-
if (override === "0" || override === "off") return "off";
|
|
149
|
-
return "intro";
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function animateAsciiLine(
|
|
153
|
-
line: string,
|
|
154
|
-
lineIndex: number,
|
|
155
|
-
frame: number,
|
|
156
|
-
theme: { fg(name: string, text: string): string },
|
|
157
|
-
baseColor: string,
|
|
158
|
-
highlightColor: string,
|
|
159
|
-
trailColor: string,
|
|
160
|
-
animate: boolean,
|
|
161
|
-
): string {
|
|
162
|
-
if (!animate) return theme.fg(baseColor, line);
|
|
163
|
-
return [...line]
|
|
164
|
-
.map((char, charIndex) => {
|
|
165
|
-
if (char === " ") return char;
|
|
166
|
-
const wave = positiveModulo(charIndex + lineIndex * 2 - frame * 5, 44);
|
|
167
|
-
const color =
|
|
168
|
-
wave <= 3 ? highlightColor : wave <= 8 ? trailColor : baseColor;
|
|
169
|
-
return theme.fg(color, char);
|
|
170
|
-
})
|
|
171
|
-
.join("");
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function centerVisible(line: string, width: number): string {
|
|
175
|
-
const leftPad = Math.max(0, Math.floor((width - visibleWidth(line)) / 2));
|
|
176
|
-
return `${" ".repeat(leftPad)}${line}`;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function fitCenterVisible(line: string, width: number): string {
|
|
180
|
-
return centerVisible(truncateToWidth(line, Math.max(1, width)), width);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function padVisibleRight(line: string, width: number): string {
|
|
184
|
-
return `${line}${" ".repeat(Math.max(0, width - visibleWidth(line)))}`;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function composeSideBySide(
|
|
188
|
-
leftLines: string[],
|
|
189
|
-
rightLines: string[],
|
|
190
|
-
width: number,
|
|
191
|
-
): string[] {
|
|
192
|
-
const combinedWidth = MASCOT_WIDTH + MASCOT_GAP + HEADER_WIDTH;
|
|
193
|
-
if (width < combinedWidth) return rightLines;
|
|
194
|
-
const leftPad = " ".repeat(
|
|
195
|
-
Math.max(0, Math.floor((width - combinedWidth) / 2)),
|
|
196
|
-
);
|
|
197
|
-
const rows = Math.max(leftLines.length, rightLines.length);
|
|
198
|
-
const topOffset = Math.max(0, Math.floor((rows - rightLines.length) / 2));
|
|
199
|
-
return Array.from({ length: rows }, (_, index) => {
|
|
200
|
-
const left = padVisibleRight(leftLines[index] ?? "", MASCOT_WIDTH);
|
|
201
|
-
const right = rightLines[index - topOffset] ?? "";
|
|
202
|
-
return `${leftPad}${left}${" ".repeat(MASCOT_GAP)}${right}`;
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function formatPath(cwd: string) {
|
|
207
|
-
const home = process.env.HOME;
|
|
208
|
-
return home && cwd.startsWith(home) ? `~${cwd.slice(home.length)}` : cwd;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function truncateMiddle(value: string, maxWidth: number): string {
|
|
212
|
-
if (maxWidth <= 0) return "";
|
|
213
|
-
if (visibleWidth(value) <= maxWidth) return value;
|
|
214
|
-
if (maxWidth <= 3) return truncateToWidth(value, maxWidth);
|
|
215
|
-
const headWidth = Math.max(1, Math.ceil((maxWidth - 1) / 2));
|
|
216
|
-
const tailWidth = Math.max(1, maxWidth - 1 - headWidth);
|
|
217
|
-
const head = truncateToWidth(value, headWidth, "");
|
|
218
|
-
const tail = truncateToWidth([...value].reverse().join(""), tailWidth, "")
|
|
219
|
-
.split("")
|
|
220
|
-
.reverse()
|
|
221
|
-
.join("");
|
|
222
|
-
return `${head}…${tail}`;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function shortNumber(value: number): string {
|
|
226
|
-
if (!Number.isFinite(value)) return "0";
|
|
227
|
-
if (Math.abs(value) < 1_000) return Math.round(value).toString();
|
|
228
|
-
if (Math.abs(value) < 1_000_000) return `${(value / 1_000).toFixed(1)}k`;
|
|
229
|
-
return `${(value / 1_000_000).toFixed(1)}m`;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function modelLabel(ctx: ExtensionContext): string {
|
|
233
|
-
const model = ctx.model;
|
|
234
|
-
if (!model) return "no model";
|
|
235
|
-
return model.provider ? `${model.provider}/${model.id}` : model.id;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function getMcpServerCount(pi: ExtensionAPI): number {
|
|
239
|
-
const serverKeys = new Set<string>();
|
|
240
|
-
for (const tool of pi.getAllTools()) {
|
|
241
|
-
const source = tool.sourceInfo?.source?.toLowerCase() ?? "";
|
|
242
|
-
const path = tool.sourceInfo?.path?.toLowerCase() ?? "";
|
|
243
|
-
if (!tool.name.toLowerCase().includes("mcp") && !source.includes("mcp"))
|
|
244
|
-
continue;
|
|
245
|
-
serverKeys.add(source || path || tool.name);
|
|
246
|
-
}
|
|
247
|
-
return serverKeys.size;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function countToolsByOrigin(pi: ExtensionAPI, origin: string): number {
|
|
251
|
-
return pi
|
|
252
|
-
.getAllTools()
|
|
253
|
-
.filter((tool) => tool.sourceInfo?.origin?.toLowerCase() === origin).length;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function countToolsBySource(pi: ExtensionAPI, source: string): number {
|
|
257
|
-
return pi
|
|
258
|
-
.getAllTools()
|
|
259
|
-
.filter((tool) => tool.sourceInfo?.source?.toLowerCase().includes(source))
|
|
260
|
-
.length;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function renderHeaderMetadata(
|
|
264
|
-
pi: ExtensionAPI,
|
|
265
|
-
ctx: ExtensionContext,
|
|
266
|
-
state: OsdyState,
|
|
267
|
-
width: number,
|
|
268
|
-
): HeaderMetaRow[] {
|
|
269
|
-
const activeTools = pi.getActiveTools().length;
|
|
270
|
-
const allTools = pi.getAllTools().length;
|
|
271
|
-
const git = state.gitLabel === "no-git" ? "Not a git repo" : state.gitLabel;
|
|
272
|
-
const path = truncateMiddle(formatPath(ctx.cwd), width < 110 ? 34 : 52);
|
|
273
|
-
const mcpServerCount = getMcpServerCount(pi);
|
|
274
|
-
const pluginCount = countToolsByOrigin(pi, "package");
|
|
275
|
-
const extensionCount = countToolsBySource(pi, "extension");
|
|
276
|
-
return [
|
|
277
|
-
{
|
|
278
|
-
leftLabel: "GIT:",
|
|
279
|
-
leftValue: git,
|
|
280
|
-
rightLabel: "PATH:",
|
|
281
|
-
rightValue: path,
|
|
282
|
-
},
|
|
283
|
-
{
|
|
284
|
-
leftLabel: "MCP:",
|
|
285
|
-
leftValue: `${mcpServerCount} servers`,
|
|
286
|
-
rightLabel: "PLUGINS:",
|
|
287
|
-
rightValue: `${pluginCount} package`,
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
leftLabel: "AGENTS:",
|
|
291
|
-
leftValue: `${state.agentsLabel} loaded`,
|
|
292
|
-
rightLabel: "EXTENSIONS:",
|
|
293
|
-
rightValue: `${extensionCount} active`,
|
|
294
|
-
},
|
|
295
|
-
{
|
|
296
|
-
leftLabel: "VER:",
|
|
297
|
-
leftValue: VERSION,
|
|
298
|
-
rightLabel: "TOOLS:",
|
|
299
|
-
rightValue: `${allTools} customs`,
|
|
300
|
-
},
|
|
301
|
-
];
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function renderMetaRows(
|
|
305
|
-
rows: HeaderMetaRow[],
|
|
306
|
-
width: number,
|
|
307
|
-
theme: { fg(name: string, text: string): string },
|
|
308
|
-
): string[] {
|
|
309
|
-
const labelWidth = 11;
|
|
310
|
-
const leftValueWidth = 32;
|
|
311
|
-
const rightValueWidth = 52;
|
|
312
|
-
const gapWidth = 10;
|
|
313
|
-
const blockWidth =
|
|
314
|
-
labelWidth +
|
|
315
|
-
1 +
|
|
316
|
-
leftValueWidth +
|
|
317
|
-
gapWidth +
|
|
318
|
-
labelWidth +
|
|
319
|
-
1 +
|
|
320
|
-
rightValueWidth;
|
|
321
|
-
return rows.map((row) => {
|
|
322
|
-
const leftLabel = theme.fg("mdLink", row.leftLabel.padEnd(labelWidth));
|
|
323
|
-
const leftValue = theme.fg(
|
|
324
|
-
"accent",
|
|
325
|
-
truncateToWidth(row.leftValue, leftValueWidth).padEnd(leftValueWidth),
|
|
326
|
-
);
|
|
327
|
-
const rightLabel = theme.fg("mdLink", row.rightLabel.padEnd(labelWidth));
|
|
328
|
-
const rightValue = theme.fg(
|
|
329
|
-
"accent",
|
|
330
|
-
truncateToWidth(row.rightValue, rightValueWidth).padEnd(rightValueWidth),
|
|
331
|
-
);
|
|
332
|
-
const rowText = `${leftLabel} ${leftValue}${" ".repeat(gapWidth)}${rightLabel} ${rightValue}`;
|
|
333
|
-
return width >= blockWidth
|
|
334
|
-
? centerVisible(rowText, width)
|
|
335
|
-
: fitCenterVisible(rowText, width);
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function usageLabel(ctx: ExtensionContext): string {
|
|
340
|
-
let input = 0;
|
|
341
|
-
let output = 0;
|
|
342
|
-
let cacheRead = 0;
|
|
343
|
-
let cacheWrite = 0;
|
|
344
|
-
let cost = 0;
|
|
345
|
-
for (const entry of ctx.sessionManager.getEntries()) {
|
|
346
|
-
const anyEntry = entry as any;
|
|
347
|
-
if (anyEntry.type !== "message" || anyEntry.message?.role !== "assistant")
|
|
348
|
-
continue;
|
|
349
|
-
const usage = anyEntry.message.usage;
|
|
350
|
-
if (!usage) continue;
|
|
351
|
-
input += usage.input ?? 0;
|
|
352
|
-
output += usage.output ?? 0;
|
|
353
|
-
cacheRead += usage.cacheRead ?? 0;
|
|
354
|
-
cacheWrite += usage.cacheWrite ?? 0;
|
|
355
|
-
cost += usage.cost?.total ?? 0;
|
|
356
|
-
}
|
|
357
|
-
const context = ctx.getContextUsage();
|
|
358
|
-
const contextWindow = context?.contextWindow ?? ctx.model?.contextWindow;
|
|
359
|
-
const percent = context?.percent ?? null;
|
|
360
|
-
const ctxText = contextWindow
|
|
361
|
-
? `ctx ${percent === null ? "?" : Math.round(percent)}%/${shortNumber(contextWindow)}`
|
|
362
|
-
: "ctx ?";
|
|
363
|
-
const cacheText =
|
|
364
|
-
cacheRead || cacheWrite
|
|
365
|
-
? ` R${shortNumber(cacheRead)} W${shortNumber(cacheWrite)}`
|
|
366
|
-
: "";
|
|
367
|
-
return ` tok ↑${shortNumber(input)} ↓${shortNumber(output)}${cacheText} · $${cost.toFixed(4)} · ${ctxText} `;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function fitBorder(
|
|
371
|
-
left: string,
|
|
372
|
-
right: string,
|
|
373
|
-
width: number,
|
|
374
|
-
color: (text: string) => string,
|
|
375
|
-
): string {
|
|
376
|
-
if (width <= 0) return "";
|
|
377
|
-
if (width === 1) return color("─");
|
|
378
|
-
let leftText = left;
|
|
379
|
-
let rightText = right;
|
|
380
|
-
const fixedWidth = 2;
|
|
381
|
-
const minGap = 1;
|
|
382
|
-
while (
|
|
383
|
-
fixedWidth + visibleWidth(leftText) + visibleWidth(rightText) + minGap >
|
|
384
|
-
width &&
|
|
385
|
-
visibleWidth(rightText) > 0
|
|
386
|
-
) {
|
|
387
|
-
rightText = truncateToWidth(
|
|
388
|
-
rightText,
|
|
389
|
-
Math.max(0, visibleWidth(rightText) - 1),
|
|
390
|
-
"",
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
while (
|
|
394
|
-
fixedWidth + visibleWidth(leftText) + visibleWidth(rightText) + minGap >
|
|
395
|
-
width &&
|
|
396
|
-
visibleWidth(leftText) > 0
|
|
397
|
-
) {
|
|
398
|
-
leftText = truncateToWidth(
|
|
399
|
-
leftText,
|
|
400
|
-
Math.max(0, visibleWidth(leftText) - 1),
|
|
401
|
-
"",
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
const fillWidth = Math.max(
|
|
405
|
-
0,
|
|
406
|
-
width - fixedWidth - visibleWidth(leftText) - visibleWidth(rightText),
|
|
407
|
-
);
|
|
408
|
-
return `${color("─")}${leftText}${color("─".repeat(fillWidth))}${rightText}${color("─")}`;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
function internalLineTarget(tui: TUI): number {
|
|
412
|
-
return tui.terminal.rows < 18 ? 3 : 4;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function applyOsdyPi(
|
|
416
|
-
pi: ExtensionAPI,
|
|
417
|
-
ctx: ExtensionContext,
|
|
418
|
-
state: OsdyState,
|
|
419
|
-
notify = false,
|
|
420
|
-
): void {
|
|
421
|
-
if (!ctx.hasUI) return;
|
|
422
|
-
const currentTheme = ctx.ui.theme.name;
|
|
423
|
-
if (!state.previousThemeName && currentTheme && currentTheme !== THEME_NAME)
|
|
424
|
-
state.previousThemeName = currentTheme;
|
|
425
|
-
const osdyTheme = ctx.ui.getTheme(THEME_NAME);
|
|
426
|
-
const themeResult = osdyTheme
|
|
427
|
-
? ctx.ui.setTheme(osdyTheme)
|
|
428
|
-
: ctx.ui.setTheme(THEME_NAME);
|
|
429
|
-
if (!themeResult.success)
|
|
430
|
-
ctx.ui.notify(
|
|
431
|
-
`osdy-pi theme failed: ${themeResult.error ?? "unknown error"}`,
|
|
432
|
-
"warning",
|
|
433
|
-
);
|
|
434
|
-
|
|
435
|
-
ctx.ui.setHeader((_tui, theme) => {
|
|
436
|
-
let frame = 0;
|
|
437
|
-
let animationComplete = false;
|
|
438
|
-
const animationMode = asciiAnimationMode();
|
|
439
|
-
const animateAscii = animationMode !== "off";
|
|
440
|
-
const timer = animateAscii
|
|
441
|
-
? setInterval(() => {
|
|
442
|
-
frame += 1;
|
|
443
|
-
if (animationMode === "intro" && frame >= INTRO_ANIMATION_FRAMES) {
|
|
444
|
-
animationComplete = true;
|
|
445
|
-
clearInterval(timer);
|
|
446
|
-
}
|
|
447
|
-
_tui.requestRender();
|
|
448
|
-
}, ANIMATION_INTERVAL_MS)
|
|
449
|
-
: undefined;
|
|
450
|
-
return {
|
|
451
|
-
render(width: number): string[] {
|
|
452
|
-
const animateFrame = animateAscii && !animationComplete;
|
|
453
|
-
const showMascot =
|
|
454
|
-
_tui.terminal.rows >= MASCOT_MIN_ROWS &&
|
|
455
|
-
width >= MASCOT_WIDTH + MASCOT_GAP + HEADER_WIDTH;
|
|
456
|
-
const useFullHeader = width >= HEADER_WIDTH;
|
|
457
|
-
const headerLines = useFullHeader ? HEADER : HEADER_FALLBACK;
|
|
458
|
-
const metadataRows = renderHeaderMetadata(pi, ctx, state, width);
|
|
459
|
-
const logoLines = headerLines.map((line, index) => {
|
|
460
|
-
const baseColor = useFullHeader && index >= 6 ? "mdLink" : "accent";
|
|
461
|
-
return animateAsciiLine(
|
|
462
|
-
line,
|
|
463
|
-
index,
|
|
464
|
-
frame,
|
|
465
|
-
theme,
|
|
466
|
-
baseColor,
|
|
467
|
-
"mdHeading",
|
|
468
|
-
"mdLink",
|
|
469
|
-
animateFrame,
|
|
470
|
-
);
|
|
471
|
-
});
|
|
472
|
-
const mascotLines = MASCOT.map((line, index) =>
|
|
473
|
-
animateAsciiLine(
|
|
474
|
-
line,
|
|
475
|
-
index,
|
|
476
|
-
frame,
|
|
477
|
-
theme,
|
|
478
|
-
"muted",
|
|
479
|
-
"mdCode",
|
|
480
|
-
"muted",
|
|
481
|
-
animateFrame,
|
|
482
|
-
),
|
|
483
|
-
);
|
|
484
|
-
const headerBlock = showMascot
|
|
485
|
-
? composeSideBySide(mascotLines, logoLines, width)
|
|
486
|
-
: logoLines.map((line) =>
|
|
487
|
-
useFullHeader
|
|
488
|
-
? centerVisible(line, width)
|
|
489
|
-
: fitCenterVisible(line, width),
|
|
490
|
-
);
|
|
491
|
-
return [
|
|
492
|
-
"",
|
|
493
|
-
...headerBlock,
|
|
494
|
-
"",
|
|
495
|
-
...renderMetaRows(metadataRows, width, theme),
|
|
496
|
-
"",
|
|
497
|
-
];
|
|
498
|
-
},
|
|
499
|
-
invalidate() {},
|
|
500
|
-
dispose() {
|
|
501
|
-
if (timer) clearInterval(timer);
|
|
502
|
-
},
|
|
503
|
-
};
|
|
504
|
-
});
|
|
505
|
-
ctx.ui.setFooter(
|
|
506
|
-
(_tui, theme, footerData) => new OsdyFooter(ctx, footerData, theme),
|
|
507
|
-
);
|
|
508
|
-
ctx.ui.setWorkingVisible(false);
|
|
509
|
-
|
|
510
|
-
class OsdyEditor extends CustomEditor {
|
|
511
|
-
private readonly tuiRef: TUI;
|
|
512
|
-
|
|
513
|
-
constructor(
|
|
514
|
-
tuiRef: TUI,
|
|
515
|
-
theme: EditorTheme,
|
|
516
|
-
keybindings: KeybindingsManager,
|
|
517
|
-
) {
|
|
518
|
-
super(tuiRef, theme, keybindings, { paddingX: 1 });
|
|
519
|
-
this.tuiRef = tuiRef;
|
|
520
|
-
}
|
|
521
|
-
render(width: number): string[] {
|
|
522
|
-
if (width < 4 || this.isShowingAutocomplete()) return super.render(width);
|
|
523
|
-
const editorWidth = width;
|
|
524
|
-
const innerWidth = Math.max(1, editorWidth - 2);
|
|
525
|
-
const lines = super.render(innerWidth);
|
|
526
|
-
if (lines.length < 2) return lines;
|
|
527
|
-
let bottomIndex = Math.max(1, lines.length - 1);
|
|
528
|
-
const target = internalLineTarget(this.tuiRef);
|
|
529
|
-
const internalLines = bottomIndex - 1;
|
|
530
|
-
if (internalLines < target) {
|
|
531
|
-
const blank = " ".repeat(innerWidth);
|
|
532
|
-
const added = Array.from(
|
|
533
|
-
{ length: target - internalLines },
|
|
534
|
-
() => blank,
|
|
535
|
-
);
|
|
536
|
-
lines.splice(bottomIndex, 0, ...added);
|
|
537
|
-
bottomIndex += added.length;
|
|
538
|
-
}
|
|
539
|
-
const borderColor = (text: string) => this.borderColor(text);
|
|
540
|
-
const side = borderColor("│");
|
|
541
|
-
for (let i = 1; i < bottomIndex; i += 1) {
|
|
542
|
-
const content = truncateToWidth(lines[i], innerWidth, "");
|
|
543
|
-
const padding = " ".repeat(
|
|
544
|
-
Math.max(0, innerWidth - visibleWidth(content)),
|
|
545
|
-
);
|
|
546
|
-
lines[i] = `${side}${content}${padding}${side}`;
|
|
547
|
-
}
|
|
548
|
-
const topLeft = ctx.ui.theme.fg("mdLink", " Osdy-Pi ");
|
|
549
|
-
const topRight = ctx.ui.theme.fg(
|
|
550
|
-
"muted",
|
|
551
|
-
` ${modelLabel(ctx)} · think ${pi.getThinkingLevel()} `,
|
|
552
|
-
);
|
|
553
|
-
const bottomLeft = ctx.ui.theme.fg("muted", usageLabel(ctx));
|
|
554
|
-
lines[0] = `${borderColor("╭")}${fitBorder(topLeft, topRight, editorWidth - 2, borderColor)}${borderColor("╮")}`;
|
|
555
|
-
lines[bottomIndex] =
|
|
556
|
-
`${borderColor("╰")}${fitBorder(bottomLeft, "", editorWidth - 2, borderColor)}${borderColor("╯")}`;
|
|
557
|
-
return lines;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
ctx.ui.setEditorComponent(
|
|
561
|
-
(tui, theme, keybindings) => new OsdyEditor(tui, theme, keybindings),
|
|
562
|
-
);
|
|
563
|
-
if (notify) ctx.ui.notify("osdy-pi enabled", "info");
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function disableOsdyPi(ctx: ExtensionContext, state: OsdyState): void {
|
|
567
|
-
if (!ctx.hasUI) return;
|
|
568
|
-
ctx.ui.setHeader(undefined);
|
|
569
|
-
ctx.ui.setEditorComponent(undefined);
|
|
570
|
-
ctx.ui.setFooter(undefined);
|
|
571
|
-
ctx.ui.setWorkingVisible(true);
|
|
572
|
-
const targetTheme = state.previousThemeName ?? "dark";
|
|
573
|
-
const result = ctx.ui.setTheme(targetTheme);
|
|
574
|
-
if (!result.success && targetTheme !== "dark") ctx.ui.setTheme("dark");
|
|
575
|
-
ctx.ui.notify("osdy-pi disabled", "info");
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function claimOsdyVisualLayer(
|
|
579
|
-
pi: ExtensionAPI,
|
|
580
|
-
ctx: ExtensionContext,
|
|
581
|
-
state: OsdyState,
|
|
582
|
-
): void {
|
|
583
|
-
if (!state.enabled) return;
|
|
584
|
-
applyOsdyPi(pi, ctx, state);
|
|
585
|
-
|
|
586
|
-
// Some packages install startup headers asynchronously. Re-apply Osdy-Pi
|
|
587
|
-
// after startup so it remains the visual owner without disabling those
|
|
588
|
-
// packages' prompts, agents, MCP tools, commands, or other behavior.
|
|
589
|
-
for (const delayMs of [300, 1000]) {
|
|
590
|
-
setTimeout(() => {
|
|
591
|
-
if (state.enabled) applyOsdyPi(pi, ctx, state);
|
|
592
|
-
}, delayMs);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
export default function (pi: ExtensionAPI) {
|
|
597
|
-
const state: OsdyState = { enabled: true, gitLabel: "-", agentsLabel: "-" };
|
|
598
|
-
pi.on("session_start", (_event, ctx) => {
|
|
599
|
-
void pi
|
|
600
|
-
.exec("git", ["branch", "--show-current"], { cwd: ctx.cwd })
|
|
601
|
-
.then((result) => {
|
|
602
|
-
state.gitLabel = result.stdout.trim() || "detached";
|
|
603
|
-
})
|
|
604
|
-
.catch(() => {
|
|
605
|
-
state.gitLabel = "no-git";
|
|
606
|
-
});
|
|
607
|
-
void pi
|
|
608
|
-
.exec(
|
|
609
|
-
"find",
|
|
610
|
-
[ctx.cwd, "-name", "AGENTS.md", "-o", "-name", "AGENTS.MD"],
|
|
611
|
-
{ cwd: ctx.cwd },
|
|
612
|
-
)
|
|
613
|
-
.then((result) => {
|
|
614
|
-
state.agentsLabel = String(
|
|
615
|
-
result.stdout.split("\n").filter(Boolean).length,
|
|
616
|
-
);
|
|
617
|
-
})
|
|
618
|
-
.catch(() => {
|
|
619
|
-
state.agentsLabel = "0";
|
|
620
|
-
});
|
|
621
|
-
claimOsdyVisualLayer(pi, ctx, state);
|
|
622
|
-
});
|
|
623
|
-
pi.registerCommand("osdy-pi", {
|
|
624
|
-
description: "Manage the Osdy Pi experience: enable, disable, or status.",
|
|
625
|
-
getArgumentCompletions(prefix: string) {
|
|
626
|
-
return ["enable", "disable", "status"]
|
|
627
|
-
.filter((value) => value.startsWith(prefix.trim()))
|
|
628
|
-
.map((value) => ({ value, label: value }));
|
|
629
|
-
},
|
|
630
|
-
handler: async (args, ctx) => {
|
|
631
|
-
const action = args.trim().split(/\s+/, 1)[0] || "status";
|
|
632
|
-
if (action === "enable") {
|
|
633
|
-
state.enabled = true;
|
|
634
|
-
applyOsdyPi(pi, ctx, state, true);
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
if (action === "disable") {
|
|
638
|
-
state.enabled = false;
|
|
639
|
-
disableOsdyPi(ctx, state);
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
if (action !== "status") {
|
|
643
|
-
ctx.ui.notify("Usage: /osdy-pi enable | disable | status", "warning");
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
646
|
-
ctx.ui.notify(
|
|
647
|
-
`osdy-pi ${state.enabled ? "enabled" : "disabled"} · theme ${ctx.ui.theme.name ?? "unknown"} · animation ${asciiAnimationMode()} · ${modelLabel(ctx)} · ${usageLabel(ctx).trim()}`,
|
|
648
|
-
"info",
|
|
649
|
-
);
|
|
650
|
-
},
|
|
651
|
-
});
|
|
4
|
+
export default function (pi: ExtensionAPI): void {
|
|
5
|
+
registerOsdyPi(pi);
|
|
652
6
|
}
|
package/mapche1.png
ADDED
|
Binary file
|