add pi-graphify extension
This commit is contained in:
242
extensions/pi-graphify/src/statusbar.ts
Normal file
242
extensions/pi-graphify/src/statusbar.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
||||
import type { ResolvedConfig } from "./config.js";
|
||||
|
||||
const MODULE_ID = "pi-graphify";
|
||||
|
||||
// ── Config Interface ───────────────────────────────────────────────────────
|
||||
|
||||
export interface StatusbarConfig {
|
||||
enabled?: boolean;
|
||||
placement?: {
|
||||
line?: number; // 1-based line number
|
||||
side?: "left" | "right";
|
||||
index?: number; // Position within side (0 = first)
|
||||
};
|
||||
show_icon?: boolean;
|
||||
show_text?: boolean;
|
||||
icon?: string; // Hex code
|
||||
icon_color?: string; // "accent", "dim", "success", "warning", "error"
|
||||
icon_color_uninitialized?: string;
|
||||
text_font_color?: string;
|
||||
text_font_color_uninitialized?: string;
|
||||
separator_before?: { icon?: string; text?: string; icon_color?: string };
|
||||
separator_after?: { icon?: string; text?: string; icon_color?: string };
|
||||
auto_heal?: {
|
||||
enabled?: boolean;
|
||||
max_retries?: number;
|
||||
retry_interval_ms?: number;
|
||||
};
|
||||
}
|
||||
|
||||
// ── State ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface StatusbarState {
|
||||
consecutiveFailures: number;
|
||||
lastErrorAt: number;
|
||||
healedAt: number;
|
||||
isDegraded: boolean;
|
||||
}
|
||||
|
||||
function makeStatusbarState(): StatusbarState {
|
||||
return { consecutiveFailures: 0, lastErrorAt: 0, healedAt: 0, isDegraded: false };
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
function emitStatusbarEvent(
|
||||
pi: ExtensionAPI,
|
||||
event: string,
|
||||
data: Record<string, unknown>,
|
||||
): boolean {
|
||||
try {
|
||||
pi.events.emit(event, data);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultPlacement(): NonNullable<StatusbarConfig["placement"]> {
|
||||
return { line: 2, side: "left", index: 4 };
|
||||
}
|
||||
|
||||
function resolveStatusbarConfig(config: ResolvedConfig): StatusbarConfig {
|
||||
const sb = config.statusbar ?? {};
|
||||
return {
|
||||
enabled: sb.enabled !== false,
|
||||
placement: { ...getDefaultPlacement(), ...sb.placement },
|
||||
show_icon: sb.show_icon !== false,
|
||||
show_text: sb.show_text !== false,
|
||||
icon: sb.icon ?? "f035b",
|
||||
icon_color: sb.icon_color ?? "accent",
|
||||
icon_color_uninitialized: sb.icon_color_uninitialized ?? "dim",
|
||||
text_font_color: sb.text_font_color ?? "dim",
|
||||
text_font_color_uninitialized: sb.text_font_color_uninitialized ?? "dim",
|
||||
separator_before: sb.separator_before,
|
||||
separator_after: sb.separator_after,
|
||||
auto_heal: {
|
||||
enabled: sb.auto_heal?.enabled !== false,
|
||||
max_retries: sb.auto_heal?.max_retries ?? 3,
|
||||
retry_interval_ms: sb.auto_heal?.retry_interval_ms ?? 5000,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function shouldAttemptUpdate(
|
||||
state: StatusbarState,
|
||||
autoHeal: StatusbarConfig["auto_heal"],
|
||||
): boolean {
|
||||
if (!state.isDegraded) return true;
|
||||
if (!autoHeal?.enabled) return false;
|
||||
|
||||
const now = Date.now();
|
||||
const retryInterval = autoHeal.retry_interval_ms ?? 5000;
|
||||
const maxRetries = autoHeal.max_retries ?? 3;
|
||||
|
||||
if (state.consecutiveFailures >= maxRetries) {
|
||||
return now - state.lastErrorAt > retryInterval * 2;
|
||||
}
|
||||
|
||||
return now - state.lastErrorAt > retryInterval;
|
||||
}
|
||||
|
||||
// ── Public API ─────────────────────────────────────────────────────────────
|
||||
|
||||
export function registerGraphifyStatusbar(pi: ExtensionAPI, config: ResolvedConfig): void {
|
||||
const sbConfig = resolveStatusbarConfig(config);
|
||||
if (!sbConfig.enabled) return;
|
||||
|
||||
const contributePayload: Record<string, unknown> = {
|
||||
id: MODULE_ID,
|
||||
label: "Graphify",
|
||||
description: "Knowledge graph status",
|
||||
default_placement: sbConfig.placement,
|
||||
default_style: {
|
||||
show_icon: sbConfig.show_icon,
|
||||
icon: sbConfig.icon,
|
||||
icon_color: sbConfig.icon_color,
|
||||
show_text: sbConfig.show_text,
|
||||
text_font_color: sbConfig.text_font_color,
|
||||
text_font_caps: "small",
|
||||
text_font_style: "regular",
|
||||
},
|
||||
priority: 0,
|
||||
};
|
||||
if (sbConfig.separator_before) contributePayload.separator_before = sbConfig.separator_before;
|
||||
if (sbConfig.separator_after) contributePayload.separator_after = sbConfig.separator_after;
|
||||
|
||||
emitStatusbarEvent(pi, "statusbar:widget:contribute", contributePayload);
|
||||
|
||||
emitStatusbarEvent(pi, "statusbar:module:register", {
|
||||
id: MODULE_ID,
|
||||
text: "initializing...",
|
||||
visible: true,
|
||||
placement: sbConfig.placement,
|
||||
style: {
|
||||
show_icon: sbConfig.show_icon,
|
||||
icon: sbConfig.icon,
|
||||
icon_color: sbConfig.icon_color,
|
||||
show_text: sbConfig.show_text,
|
||||
text_font_color: sbConfig.text_font_color,
|
||||
text_font_caps: "small",
|
||||
text_font_style: "regular",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function unregisterGraphifyStatusbar(pi: ExtensionAPI): void {
|
||||
emitStatusbarEvent(pi, "statusbar:module:unregister", { id: MODULE_ID });
|
||||
}
|
||||
|
||||
export async function updateGraphifyStatusbar(
|
||||
pi: ExtensionAPI,
|
||||
config: ResolvedConfig,
|
||||
ctx: ExtensionContext,
|
||||
state: StatusbarState,
|
||||
): Promise<void> {
|
||||
const sbConfig = resolveStatusbarConfig(config);
|
||||
if (!sbConfig.enabled) return;
|
||||
if (!shouldAttemptUpdate(state, sbConfig.auto_heal)) return;
|
||||
|
||||
try {
|
||||
const checkResult = await pi.exec(
|
||||
"sh",
|
||||
["-c", "test -f graphify-out/graph.json && echo 'yes' || echo 'no'"],
|
||||
{ cwd: ctx.cwd },
|
||||
);
|
||||
|
||||
let text: string;
|
||||
let iconColor = sbConfig.icon_color ?? "accent";
|
||||
let textColor = sbConfig.text_font_color ?? "dim";
|
||||
|
||||
if (checkResult.stdout.trim() === "yes") {
|
||||
const statsResult = await pi.exec(
|
||||
"sh",
|
||||
[
|
||||
"-c",
|
||||
`python3 -c "
|
||||
import json
|
||||
from pathlib import Path
|
||||
try:
|
||||
data = json.loads(Path('graphify-out/graph.json').read_text())
|
||||
nodes = len(data.get('nodes', []))
|
||||
edges = len(data.get('links', []))
|
||||
print(f'{nodes}n {edges}e')
|
||||
except Exception:
|
||||
print('graph ready')
|
||||
" 2>/dev/null || echo "graph ready"`,
|
||||
],
|
||||
{ cwd: ctx.cwd },
|
||||
);
|
||||
text = statsResult.stdout.trim() || "graph ready";
|
||||
} else {
|
||||
text = "no graph";
|
||||
iconColor = sbConfig.icon_color_uninitialized ?? "dim";
|
||||
textColor = sbConfig.text_font_color_uninitialized ?? "dim";
|
||||
}
|
||||
|
||||
emitStatusbarEvent(pi, "statusbar:module:register", {
|
||||
id: MODULE_ID,
|
||||
text,
|
||||
visible: true,
|
||||
placement: sbConfig.placement,
|
||||
style: {
|
||||
show_icon: sbConfig.show_icon,
|
||||
icon: sbConfig.icon,
|
||||
icon_color: iconColor,
|
||||
show_text: sbConfig.show_text,
|
||||
text_font_color: textColor,
|
||||
text_font_caps: "small",
|
||||
text_font_style: "regular",
|
||||
},
|
||||
});
|
||||
|
||||
if (state.isDegraded) {
|
||||
state.healedAt = Date.now();
|
||||
}
|
||||
state.consecutiveFailures = 0;
|
||||
state.isDegraded = false;
|
||||
} catch (_error) {
|
||||
state.consecutiveFailures += 1;
|
||||
state.lastErrorAt = Date.now();
|
||||
state.isDegraded = true;
|
||||
|
||||
emitStatusbarEvent(pi, "statusbar:module:register", {
|
||||
id: MODULE_ID,
|
||||
text: "error",
|
||||
visible: true,
|
||||
placement: sbConfig.placement,
|
||||
style: {
|
||||
show_icon: sbConfig.show_icon,
|
||||
icon: sbConfig.icon,
|
||||
icon_color: "error",
|
||||
show_text: sbConfig.show_text,
|
||||
text_font_color: "error",
|
||||
text_font_caps: "small",
|
||||
text_font_style: "regular",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { makeStatusbarState as createStatusbarState };
|
||||
Reference in New Issue
Block a user