osdy-pi 0.1.3 → 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 +41 -6
- package/extensions/osdy-pi/animation.ts +99 -0
- package/extensions/osdy-pi/border.ts +49 -0
- package/extensions/osdy-pi/constants.ts +338 -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 +194 -0
- package/extensions/osdy-pi/types.ts +53 -0
- package/extensions/osdy-pi/ui.ts +308 -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/package.json +46 -32
- package/themes/osdy-pi-dark.json +6 -0
- package/themes/osdy-pi-light.json +6 -0
- package/themes/osdy-pi-simple.json +83 -0
|
@@ -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"} · style ${state.headerVariant} · animation ${asciiAnimationMode()} · ${modelLabel(ctx)} · ${usageLabel(ctx).trim()}`,
|
|
99
|
+
"info",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { applyOsdyPi, disableOsdyPi, notifyStatus } from "./runtime-helpers.js";
|
|
6
|
+
import type { HeaderVariant, OsdyState, WorkingWidgetState } from "./types.js";
|
|
7
|
+
import {
|
|
8
|
+
createWorkingController,
|
|
9
|
+
type WorkingController,
|
|
10
|
+
} from "./working-controller.js";
|
|
11
|
+
|
|
12
|
+
function scheduleOsdyRefresh(
|
|
13
|
+
delayMs: number,
|
|
14
|
+
pi: ExtensionAPI,
|
|
15
|
+
ctx: ExtensionContext,
|
|
16
|
+
state: OsdyState,
|
|
17
|
+
workingState: WorkingWidgetState,
|
|
18
|
+
): void {
|
|
19
|
+
setTimeout(() => {
|
|
20
|
+
if (state.enabled) applyOsdyPi(pi, ctx, state, workingState);
|
|
21
|
+
}, delayMs);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function claimOsdyVisualLayer(
|
|
25
|
+
pi: ExtensionAPI,
|
|
26
|
+
ctx: ExtensionContext,
|
|
27
|
+
state: OsdyState,
|
|
28
|
+
workingState: WorkingWidgetState,
|
|
29
|
+
): void {
|
|
30
|
+
if (!state.enabled) return;
|
|
31
|
+
applyOsdyPi(pi, ctx, state, workingState);
|
|
32
|
+
for (const delayMs of [300, 1000]) {
|
|
33
|
+
scheduleOsdyRefresh(delayMs, pi, ctx, state, workingState);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function loadSessionMetadata(
|
|
38
|
+
pi: ExtensionAPI,
|
|
39
|
+
ctx: ExtensionContext,
|
|
40
|
+
state: OsdyState,
|
|
41
|
+
): void {
|
|
42
|
+
void pi
|
|
43
|
+
.exec("git", ["branch", "--show-current"], { cwd: ctx.cwd })
|
|
44
|
+
.then((result) => {
|
|
45
|
+
state.gitLabel = result.stdout.trim() || "detached";
|
|
46
|
+
})
|
|
47
|
+
.catch(() => {
|
|
48
|
+
state.gitLabel = "no-git";
|
|
49
|
+
});
|
|
50
|
+
void pi
|
|
51
|
+
.exec("find", [ctx.cwd, "-name", "AGENTS.md", "-o", "-name", "AGENTS.MD"], {
|
|
52
|
+
cwd: ctx.cwd,
|
|
53
|
+
})
|
|
54
|
+
.then((result) => {
|
|
55
|
+
state.agentsLabel = String(
|
|
56
|
+
result.stdout.split("\n").filter(Boolean).length,
|
|
57
|
+
);
|
|
58
|
+
})
|
|
59
|
+
.catch(() => {
|
|
60
|
+
state.agentsLabel = "0";
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
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);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function enableOsdyPi(
|
|
75
|
+
pi: ExtensionAPI,
|
|
76
|
+
ctx: ExtensionContext,
|
|
77
|
+
state: OsdyState,
|
|
78
|
+
workingState: WorkingWidgetState,
|
|
79
|
+
controller: WorkingController,
|
|
80
|
+
): void {
|
|
81
|
+
state.enabled = true;
|
|
82
|
+
applyOsdyPi(pi, ctx, state, workingState, true);
|
|
83
|
+
controller.refreshWorking();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function disableOsdyPiCommand(
|
|
87
|
+
ctx: ExtensionContext,
|
|
88
|
+
state: OsdyState,
|
|
89
|
+
controller: WorkingController,
|
|
90
|
+
): void {
|
|
91
|
+
state.enabled = false;
|
|
92
|
+
controller.stopWorking();
|
|
93
|
+
disableOsdyPi(ctx, state);
|
|
94
|
+
}
|
|
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
|
+
|
|
108
|
+
function registerCommand(
|
|
109
|
+
pi: ExtensionAPI,
|
|
110
|
+
state: OsdyState,
|
|
111
|
+
workingState: WorkingWidgetState,
|
|
112
|
+
controller: WorkingController,
|
|
113
|
+
): void {
|
|
114
|
+
pi.registerCommand("osdy-pi", {
|
|
115
|
+
description:
|
|
116
|
+
"Manage the Osdy Pi experience: enable, disable, status, or style.",
|
|
117
|
+
getArgumentCompletions(prefix: string) {
|
|
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;
|
|
127
|
+
},
|
|
128
|
+
handler: (args, ctx) => {
|
|
129
|
+
const [action = "status"] = parseCommandArgs(args);
|
|
130
|
+
if (action === "enable") {
|
|
131
|
+
enableOsdyPi(pi, ctx, state, workingState, controller);
|
|
132
|
+
return Promise.resolve();
|
|
133
|
+
}
|
|
134
|
+
if (action === "disable") {
|
|
135
|
+
disableOsdyPiCommand(ctx, state, controller);
|
|
136
|
+
return Promise.resolve();
|
|
137
|
+
}
|
|
138
|
+
if (action === "status") {
|
|
139
|
+
notifyStatus(ctx, state);
|
|
140
|
+
return Promise.resolve();
|
|
141
|
+
}
|
|
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
|
+
);
|
|
150
|
+
return Promise.resolve();
|
|
151
|
+
},
|
|
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
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function registerOsdyPi(pi: ExtensionAPI): void {
|
|
166
|
+
const state: OsdyState = {
|
|
167
|
+
enabled: true,
|
|
168
|
+
headerVariant: "osdy-theme",
|
|
169
|
+
gitLabel: "-",
|
|
170
|
+
agentsLabel: "-",
|
|
171
|
+
};
|
|
172
|
+
const workingState: WorkingWidgetState = {
|
|
173
|
+
active: false,
|
|
174
|
+
label: "Working...",
|
|
175
|
+
frame: 0,
|
|
176
|
+
timer: undefined,
|
|
177
|
+
tui: undefined,
|
|
178
|
+
};
|
|
179
|
+
const controller = createWorkingController(state, workingState);
|
|
180
|
+
|
|
181
|
+
pi.on("agent_start", () => controller.onAgentStart());
|
|
182
|
+
pi.on("agent_end", () => controller.onAgentEnd());
|
|
183
|
+
pi.on("tool_execution_start", (event) =>
|
|
184
|
+
controller.onToolStart(event.toolName),
|
|
185
|
+
);
|
|
186
|
+
pi.on("tool_execution_end", () => controller.onToolEnd());
|
|
187
|
+
pi.on("session_shutdown", () => controller.onShutdown());
|
|
188
|
+
pi.on("session_start", (_event, ctx) => {
|
|
189
|
+
loadSessionMetadata(pi, ctx, state);
|
|
190
|
+
claimOsdyVisualLayer(pi, ctx, state, workingState);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
registerCommand(pi, state, workingState, controller);
|
|
194
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { TUI } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
export type AnimationMode = "off" | "intro" | "continuous";
|
|
4
|
+
|
|
5
|
+
export type HeaderVariant = "osdy-theme" | "classic";
|
|
6
|
+
|
|
7
|
+
export type OsdyState = {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
headerVariant: HeaderVariant;
|
|
10
|
+
previousThemeName?: string;
|
|
11
|
+
gitLabel: string;
|
|
12
|
+
agentsLabel: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type WorkingWidgetState = {
|
|
16
|
+
active: boolean;
|
|
17
|
+
label: string;
|
|
18
|
+
frame: number;
|
|
19
|
+
timer: ReturnType<typeof setInterval> | undefined;
|
|
20
|
+
tui: TUI | undefined;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type AssistantUsage = {
|
|
24
|
+
input?: number;
|
|
25
|
+
output?: number;
|
|
26
|
+
cacheRead?: number;
|
|
27
|
+
cacheWrite?: number;
|
|
28
|
+
cost?: {
|
|
29
|
+
total?: number;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type AssistantSessionEntry = {
|
|
34
|
+
type: "message";
|
|
35
|
+
id: string;
|
|
36
|
+
parentId: string | null;
|
|
37
|
+
timestamp: string;
|
|
38
|
+
message?: {
|
|
39
|
+
role?: string;
|
|
40
|
+
usage?: AssistantUsage;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type HeaderMetaRow = {
|
|
45
|
+
leftLabel: string;
|
|
46
|
+
leftValue: string;
|
|
47
|
+
rightLabel: string;
|
|
48
|
+
rightValue: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type SimpleTheme = {
|
|
52
|
+
fg(name: string, text: string): string;
|
|
53
|
+
};
|