feat: /agy:status — diagnostica estensione in overlay TUI

- Overlay TUI (ctx.ui.custom) che mostra: binario agy + versione, chiave
  Gemini (valida/mancante, mascherata), modello attivo, backend STT,
  notifiche TTS e ultimo errore significativo dal log di agy.
- Parsing log migliorato: filtra il rumore di glog e cattura errori reali
  (permission denied, quota, 404, timeout).
- Chiusura con Enter o Esc. README aggiornato.
This commit is contained in:
2026-08-10 15:01:34 +02:00
parent f7cc237294
commit 28b2cceefa
2 changed files with 138 additions and 0 deletions
+127
View File
@@ -1781,4 +1781,131 @@ export default function agyExtension(pi: ExtensionAPI) {
}
},
});
// =========================================================================
// Comando: /agy:status — diagnostica estensione in un overlay TUI
// Mostra: binario agy, versione, chiave Gemini, modello attivo, ultimo
// errore dal log, stato config. Chiuso con Enter o Esc.
// =========================================================================
pi.registerCommand("agy:status", {
description: "Mostra lo stato dell'estensione (agi, chiave, modello, ultimo errore)",
handler: async (_args, ctx) => {
const { Container, Text, matchesKey, Key } = await import("@earendil-works/pi-tui");
const { DynamicBorder } = await import("@earendil-works/pi-coding-agent");
// ---- raccolta diagnostica ----
const agyBin = findAgy();
const agyExists =
agyBin === "agy" ? fs.existsSync("/home/enne2/.local/bin/agy") : fs.existsSync(agyBin);
let agyVersion = "(sconosciuta)";
if (agyExists) {
try {
agyVersion =
(
await execFileAsync(agyBin, ["--version"], {
timeout: 8000,
env: { ...process.env, NO_COLOR: "1" },
})
).stdout.trim() || "(sconosciuta)";
} catch {
agyVersion = "(errore esecuzione)";
}
}
const cfgKey = getConfig("geminiApiKey") ?? "";
const fileKey = (() => {
try {
const f = path.join(AGY_CHAT_DIR, "gemini-key");
return fs.existsSync(f) ? fs.readFileSync(f, "utf8").trim() : "";
} catch {
return "";
}
})();
const key = cfgKey || fileKey;
const keyStatus = key ? "✅ valida" : "❌ mancante (usa /agy:key)";
const keyMask = key ? `${key.slice(0, 4)}...${key.slice(-4)}` : "—";
const model = getConfig("agyDefaultModel") || "(default agy)";
const sttBackend = getConfig("sttBackend") ?? "gemini";
const ttsNotify = getConfig("ttsNotify") ?? "true";
// ultimo errore dal log agy (ultima riga con 'ERROR'/'denied'/'quota')
let lastError = "(nessuno)";
try {
const logDir = path.join(os.homedir(), ".gemini", "antigravity-cli", "log");
if (fs.existsSync(logDir)) {
const logs = fs
.readdirSync(logDir)
.map((f) => path.join(logDir, f))
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
const logFile = logs[0];
if (logFile) {
const lines = fs.readFileSync(logFile, "utf8").split("\n");
const meaningful = [...lines]
.reverse()
.filter((l) => /error|denied|quota|failed|timed out/i.test(l))
.filter((l) => !/^ERROR: logging before google\.Init:/i.test(l.trim()));
if (meaningful.length) {
const raw = meaningful[0];
// estrai il messaggio dopo il pattern glog: "go:file.go:NN] msg" oppure dopo ":"
const m = raw.match(/\][\s]*\s*\s*(.*)$/) || raw.match(/:\s*(.*)$/i);
lastError = (m ? m[1] : raw).replace(/^ERROR:|^WARN:/i, "").trim().slice(0, 90) || "(vedi log)";
}
}
}
} catch {
lastError = "(non disponibile)";
}
// ---- overlay TUI ----
await ctx.ui.custom<void>(
(tui, theme, _keybindings, done) => {
const container = new Container();
const render = () => {
container.clear();
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
container.addChild(
new Text(theme.fg("accent", theme.bold("📊 Stato agy-pi")), 1, 1),
);
container.addChild(new Text("", 0, 0));
const line = (label: string, v: string, color: "success" | "error" | "text" | "muted" | "accent" | "warning" | "dim") =>
container.addChild(
new Text(theme.fg("dim", `${label}: `) + theme.fg(color, v), 1, 0),
);
line("Binario agy", agyExists ? `trovato ${agyBin}` : "❌ NON trovato",
agyExists ? "success" : "error");
line("Versione", agyVersion, "text");
line("Chiave Gemini", keyStatus, key ? "success" : "error");
line("Chiave (mask)", keyMask, "muted");
line("Modello attivo", model, "accent");
line("Backend STT", sttBackend, "text");
line("Notifiche TTS", ttsNotify, "text");
container.addChild(new Text("", 0, 0));
container.addChild(
new Text(theme.fg("warning", "Ultimo errore:") + " " + theme.fg("text", lastError), 1, 0),
);
container.addChild(new Text("", 0, 0));
container.addChild(
new Text(theme.fg("dim", "Enter o Esc per chiudere"), 1, 0),
);
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
};
render();
return {
render: (w) => {
render();
return container.render(w);
},
invalidate: () => container.invalidate(),
handleInput: (data) => {
if (matchesKey(data, Key.enter) || matchesKey(data, Key.escape)) done();
},
};
},
{
overlay: true,
overlayOptions: { width: "55%", minWidth: 56, anchor: "center" },
},
);
},
});
}