|
|
|
|
@ -0,0 +1,498 @@
|
|
|
|
|
import { complete, type UserMessage } from "@earendil-works/pi-ai"; |
|
|
|
|
import { |
|
|
|
|
BorderedLoader, |
|
|
|
|
DynamicBorder, |
|
|
|
|
type ExtensionAPI, |
|
|
|
|
type ExtensionContext, |
|
|
|
|
isToolCallEventType, |
|
|
|
|
type WriteToolCallEvent, |
|
|
|
|
type EditToolCallEvent, |
|
|
|
|
} from "@earendil-works/pi-coding-agent"; |
|
|
|
|
import { |
|
|
|
|
Container, |
|
|
|
|
SelectList, |
|
|
|
|
Text, |
|
|
|
|
type SelectItem, |
|
|
|
|
truncateToWidth, |
|
|
|
|
visibleWidth, |
|
|
|
|
wrapTextWithAnsi, |
|
|
|
|
matchesKey, |
|
|
|
|
Key, |
|
|
|
|
type Component, |
|
|
|
|
} from "@earendil-works/pi-tui"; |
|
|
|
|
|
|
|
|
|
type HitlAction = "accept" | "modify" | "discuss" | "reject"; |
|
|
|
|
|
|
|
|
|
const EXPLAIN_SYSTEM_PROMPT = `Sei un revisore di codice. Analizza il codice proposto e rispondi in italiano con una spiegazione BREVISSIMA (massimo 5-6 righe).
|
|
|
|
|
|
|
|
|
|
Indica solo: |
|
|
|
|
- Cosa fa il codice in sintesi. |
|
|
|
|
- Le modifiche principali rispetto al file esistente (se e un edit). |
|
|
|
|
- Un eventuale rischio o miglioramento rilevante. |
|
|
|
|
|
|
|
|
|
Non ripetere il codice. Non aggiungere introduzioni o conclusioni.`;
|
|
|
|
|
|
|
|
|
|
const HITL_SYSTEM_PROMPT_APPENDIX = ` |
|
|
|
|
|
|
|
|
|
## Human-in-the-Loop per scrittura/modifica file |
|
|
|
|
|
|
|
|
|
Quando intendi scrivere o modificare un file, specialmente codice sorgente o file di configurazione, usa SEMPRE gli strumenti
|
|
|
|
|
write |
|
|
|
|
o
|
|
|
|
|
edit |
|
|
|
|
. Non usare
|
|
|
|
|
bash |
|
|
|
|
con redirezioni (>>, >) o comandi come
|
|
|
|
|
echo ... > file |
|
|
|
|
per creare o modificare file, perche queste operazioni aggirerebbero il controllo umano obbligatorio. Se l'utente rifiuta o chiede di discutere una modifica, riporta il motivo e attendi istruzioni.`;
|
|
|
|
|
|
|
|
|
|
// Estensioni di file che rientrano nel focus "programmazione".
|
|
|
|
|
const CODE_EXTENSIONS = new Set([ |
|
|
|
|
".py", ".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", |
|
|
|
|
".java", ".kt", ".scala", ".clj", |
|
|
|
|
".c", ".h", ".cpp", ".hpp", ".cc", ".cxx", |
|
|
|
|
".go", ".rs", ".rb", ".php", ".swift", |
|
|
|
|
".cs", ".fs", ".fsx", ".vb", |
|
|
|
|
".sh", ".bash", ".zsh", ".fish", ".ps1", |
|
|
|
|
".html", ".htm", ".css", ".scss", ".sass", ".less", |
|
|
|
|
".vue", ".svelte", ".astro", |
|
|
|
|
".sql", ".graphql", ".prisma", |
|
|
|
|
".yaml", ".yml", ".json", ".toml", ".ini", ".cfg", ".conf", |
|
|
|
|
".md", ".mdx", ".rst", ".tex", |
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
function isProgrammingFile(path: string): boolean { |
|
|
|
|
const lower = path.toLowerCase(); |
|
|
|
|
for (const ext of CODE_EXTENSIONS) { |
|
|
|
|
if (lower.endsWith(ext)) return true; |
|
|
|
|
} |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function limitCodeSnippet(text: string, maxLines = 80, maxChars = 8000): string { |
|
|
|
|
if (text.length > maxChars) { |
|
|
|
|
text = text.slice(0, maxChars) + "\n\n[... testo troncato per la spiegazione ...]"; |
|
|
|
|
} |
|
|
|
|
const lines = text.split("\n"); |
|
|
|
|
if (lines.length > maxLines) { |
|
|
|
|
return lines.slice(0, maxLines).join("\n") + "\n\n[... altre righe omesse ...]"; |
|
|
|
|
} |
|
|
|
|
return text; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
class ScrollablePanel implements Component { |
|
|
|
|
private lines: string[] = []; |
|
|
|
|
private scroll = 0; |
|
|
|
|
private maxRows: number; |
|
|
|
|
private colorFn: (s: string) => string; |
|
|
|
|
private cachedWidth?: number; |
|
|
|
|
private cachedLines?: string[]; |
|
|
|
|
|
|
|
|
|
constructor(maxRows: number, colorFn: (s: string) => string) { |
|
|
|
|
this.maxRows = maxRows; |
|
|
|
|
this.colorFn = colorFn; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
setLines(lines: string[]) { |
|
|
|
|
this.lines = lines; |
|
|
|
|
this.scroll = 0; |
|
|
|
|
this.invalidate(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
scrollBy(delta: number) { |
|
|
|
|
const maxScroll = Math.max(0, this.lines.length - this.maxRows); |
|
|
|
|
this.scroll = Math.max(0, Math.min(this.scroll + delta, maxScroll)); |
|
|
|
|
this.invalidate(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
goTop() { |
|
|
|
|
this.scroll = 0; |
|
|
|
|
this.invalidate(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
goBottom() { |
|
|
|
|
this.scroll = Math.max(0, this.lines.length - this.maxRows); |
|
|
|
|
this.invalidate(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
handleInput(data: string): boolean { |
|
|
|
|
if (matchesKey(data, Key.up)) { |
|
|
|
|
this.scrollBy(-1); |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
if (matchesKey(data, Key.down)) { |
|
|
|
|
this.scrollBy(1); |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
if (matchesKey(data, Key.ctrl("u"))) { |
|
|
|
|
this.scrollBy(-this.maxRows); |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
if (matchesKey(data, Key.ctrl("d"))) { |
|
|
|
|
this.scrollBy(this.maxRows); |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
if (matchesKey(data, Key.home)) { |
|
|
|
|
this.goTop(); |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
if (matchesKey(data, Key.end)) { |
|
|
|
|
this.goBottom(); |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
render(width: number): string[] { |
|
|
|
|
if (this.cachedLines && this.cachedWidth === width) { |
|
|
|
|
return this.cachedLines; |
|
|
|
|
} |
|
|
|
|
const pad = 2; |
|
|
|
|
const availableWidth = Math.max(1, width - pad); |
|
|
|
|
const wrapped: string[] = []; |
|
|
|
|
for (const line of this.lines) { |
|
|
|
|
const colored = this.colorFn(line); |
|
|
|
|
if (visibleWidth(colored) > availableWidth) { |
|
|
|
|
wrapped.push(...wrapTextWithAnsi(colored, availableWidth)); |
|
|
|
|
} else { |
|
|
|
|
wrapped.push(colored); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
const maxScroll = Math.max(0, wrapped.length - this.maxRows); |
|
|
|
|
const start = Math.min(this.scroll, maxScroll); |
|
|
|
|
const window = wrapped.slice(start, start + this.maxRows); |
|
|
|
|
|
|
|
|
|
const out: string[] = []; |
|
|
|
|
for (let i = 0; i < this.maxRows; i++) { |
|
|
|
|
const raw = window[i] ?? ""; |
|
|
|
|
const line = truncateToWidth(raw, availableWidth, ""); |
|
|
|
|
out.push(line); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
this.cachedWidth = width; |
|
|
|
|
this.cachedLines = out; |
|
|
|
|
return out; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
invalidate(): void { |
|
|
|
|
this.cachedWidth = undefined; |
|
|
|
|
this.cachedLines = undefined; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
getScrollInfo(): { start: number; end: number; total: number } { |
|
|
|
|
const maxScroll = Math.max(0, this.lines.length - this.maxRows); |
|
|
|
|
const start = Math.min(this.scroll, maxScroll); |
|
|
|
|
const visible = Math.min(this.maxRows, this.lines.length); |
|
|
|
|
return { start: start + 1, end: start + visible, total: this.lines.length }; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function buildUserPrompt(event: WriteToolCallEvent | EditToolCallEvent): Promise<string> { |
|
|
|
|
if (isToolCallEventType("write", event)) { |
|
|
|
|
const { path, content } = event.input; |
|
|
|
|
return `File da scrivere: ${path}\n\nCodice proposto:\n\`\`\`\n${limitCodeSnippet(content, 400, 60000)}\n\`\`\`\n\nSpiega in italiano cosa fa questo codice e quali sono le scelte tecniche rilevanti.`; |
|
|
|
|
} |
|
|
|
|
const { path, edits } = event.input; |
|
|
|
|
const editsText = edits |
|
|
|
|
.map( |
|
|
|
|
(e, i) => |
|
|
|
|
`=== Modifica ${i + 1} ===\nTROVA:\n${limitCodeSnippet(e.oldText, 200, 20000)}\nSOSTITUISCI CON:\n${limitCodeSnippet(e.newText, 400, 60000)}`, |
|
|
|
|
) |
|
|
|
|
.join("\n\n"); |
|
|
|
|
return `File da modificare: ${path}\n\nModifiche proposte:\n\n${editsText}\n\nSpiega in italiano cosa cambia, perche, e quali sono le implicazioni tecniche.`; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function generateExplanation( |
|
|
|
|
event: WriteToolCallEvent | EditToolCallEvent, |
|
|
|
|
ctx: ExtensionContext, |
|
|
|
|
): Promise<string | undefined> { |
|
|
|
|
const model = ctx.model; |
|
|
|
|
if (!model) return undefined; |
|
|
|
|
|
|
|
|
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model); |
|
|
|
|
if (!auth.ok || !auth.apiKey) return undefined; |
|
|
|
|
|
|
|
|
|
const userPrompt = await buildUserPrompt(event); |
|
|
|
|
const userMessage: UserMessage = { |
|
|
|
|
role: "user", |
|
|
|
|
content: [{ type: "text", text: userPrompt }], |
|
|
|
|
timestamp: Date.now(), |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
return ctx.ui.custom<string | undefined>((tui, theme, _kb, done) => { |
|
|
|
|
const loader = new BorderedLoader(tui, theme, "Genero spiegazione del codice proposto..."); |
|
|
|
|
loader.onAbort = () => done(undefined); |
|
|
|
|
|
|
|
|
|
const run = async () => { |
|
|
|
|
try { |
|
|
|
|
const response = await complete( |
|
|
|
|
model, |
|
|
|
|
{ systemPrompt: EXPLAIN_SYSTEM_PROMPT, messages: [userMessage] }, |
|
|
|
|
{ apiKey: auth.apiKey, headers: auth.headers, signal: ctx.signal }, |
|
|
|
|
); |
|
|
|
|
const text = response.content |
|
|
|
|
.filter((c): c is { type: "text"; text: string } => c.type === "text") |
|
|
|
|
.map((c) => c.text) |
|
|
|
|
.join("\n"); |
|
|
|
|
done(text || undefined); |
|
|
|
|
} catch { |
|
|
|
|
done(undefined); |
|
|
|
|
} |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
run(); |
|
|
|
|
return loader; |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function buildCodePreview(event: WriteToolCallEvent | EditToolCallEvent): string[] { |
|
|
|
|
if (isToolCallEventType("write", event)) { |
|
|
|
|
return event.input.content.split("\n"); |
|
|
|
|
} |
|
|
|
|
const { edits } = event.input; |
|
|
|
|
const out: string[] = []; |
|
|
|
|
for (let i = 0; i < edits.length; i++) { |
|
|
|
|
out.push(`=== Modifica ${i + 1} ===`); |
|
|
|
|
out.push("--- TROVA ---"); |
|
|
|
|
out.push(...edits[i].oldText.split("\n")); |
|
|
|
|
out.push("+++ SOSTITUISCI CON +++"); |
|
|
|
|
out.push(...edits[i].newText.split("\n")); |
|
|
|
|
if (i < edits.length - 1) out.push(""); |
|
|
|
|
} |
|
|
|
|
return out; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function askHitlAction( |
|
|
|
|
event: WriteToolCallEvent | EditToolCallEvent, |
|
|
|
|
explanation: string | undefined, |
|
|
|
|
ctx: ExtensionContext, |
|
|
|
|
): Promise<HitlAction> { |
|
|
|
|
const path = event.input.path; |
|
|
|
|
const isWrite = event.toolName === "write"; |
|
|
|
|
const title = `${isWrite ? "Scrittura" : "Modifica"} file: ${path}`; |
|
|
|
|
|
|
|
|
|
const items: SelectItem[] = [ |
|
|
|
|
{ value: "accept", label: "Accetta e applica", description: "Applica il codice proposto" }, |
|
|
|
|
{ value: "modify", label: "Modifica prima di applicare", description: "Edita il contenuto prima di scrivere/modificare" }, |
|
|
|
|
{ value: "discuss", label: "Discuti / chiedi", description: "Blocca e spiega cosa vuoi cambiare" }, |
|
|
|
|
{ value: "reject", label: "Rifiuta", description: "Non applicare la modifica" }, |
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
const contentLines: string[] = []; |
|
|
|
|
if (explanation) { |
|
|
|
|
contentLines.push("=== SPIEGAZIONE ==="); |
|
|
|
|
contentLines.push(...explanation.split("\n")); |
|
|
|
|
contentLines.push(""); |
|
|
|
|
} else { |
|
|
|
|
contentLines.push("Spiegazione non generata (modello non disponibile)."); |
|
|
|
|
contentLines.push(""); |
|
|
|
|
} |
|
|
|
|
contentLines.push("=== CODICE PROPOSTO ==="); |
|
|
|
|
contentLines.push(...buildCodePreview(event)); |
|
|
|
|
|
|
|
|
|
return ctx.ui.custom<HitlAction>((tui, theme, _kb, done) => { |
|
|
|
|
const container = new Container(); |
|
|
|
|
let focus: "panel" | "menu" = "panel"; |
|
|
|
|
|
|
|
|
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s))); |
|
|
|
|
container.addChild(new Text(theme.fg("accent", theme.bold(`🛡️ ${title}`)), 1, 0)); |
|
|
|
|
container.addChild(new Text(theme.fg("muted", "Human-in-the-Loop: conferma richiesta"), 1, 0)); |
|
|
|
|
|
|
|
|
|
const panel = new ScrollablePanel(16, (s) => theme.fg("text", s)); |
|
|
|
|
panel.setLines(contentLines); |
|
|
|
|
|
|
|
|
|
const panelLabel = new Text("", 1, 0); |
|
|
|
|
container.addChild(panelLabel); |
|
|
|
|
container.addChild(panel); |
|
|
|
|
|
|
|
|
|
const selectList = new SelectList(items, Math.min(items.length, 10), { |
|
|
|
|
selectedPrefix: (t: string) => theme.fg("accent", t), |
|
|
|
|
selectedText: (t: string) => theme.fg("accent", t), |
|
|
|
|
description: (t: string) => theme.fg("muted", t), |
|
|
|
|
scrollInfo: (t: string) => theme.fg("dim", t), |
|
|
|
|
noMatch: (t: string) => theme.fg("warning", t), |
|
|
|
|
}); |
|
|
|
|
selectList.onSelect = (item) => done(item.value as HitlAction); |
|
|
|
|
selectList.onCancel = () => done("reject"); |
|
|
|
|
container.addChild(selectList); |
|
|
|
|
|
|
|
|
|
const helpText = new Text("", 1, 0); |
|
|
|
|
container.addChild(helpText); |
|
|
|
|
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s))); |
|
|
|
|
|
|
|
|
|
function updateLabels(width: number) { |
|
|
|
|
const info = panel.getScrollInfo(); |
|
|
|
|
const label = focus === "panel" |
|
|
|
|
? theme.fg("accent", theme.bold(`Spiegazione e codice [focus] (${info.start}-${info.end} di ${info.total})`)) |
|
|
|
|
: theme.fg("muted", `Spiegazione e codice (${info.start}-${info.end} di ${info.total})`); |
|
|
|
|
panelLabel.setText(label); |
|
|
|
|
|
|
|
|
|
const hint = focus === "panel" |
|
|
|
|
? "↑↓ scrolla • Ctrl+U/Ctrl+D pagina • Tab menu • Esc rifiuta" |
|
|
|
|
: "↑↓ naviga azione • Enter seleziona • Tab torna al testo • Esc rifiuta"; |
|
|
|
|
helpText.setText(theme.fg("dim", hint)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function refresh() { |
|
|
|
|
panel.invalidate(); |
|
|
|
|
container.invalidate(); |
|
|
|
|
tui.requestRender(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
render: (width: number) => { |
|
|
|
|
updateLabels(width); |
|
|
|
|
return container.render(width); |
|
|
|
|
}, |
|
|
|
|
invalidate: () => { |
|
|
|
|
panel.invalidate(); |
|
|
|
|
container.invalidate(); |
|
|
|
|
}, |
|
|
|
|
handleInput: (data: string) => { |
|
|
|
|
if (matchesKey(data, Key.escape)) { |
|
|
|
|
done("reject"); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
if (matchesKey(data, Key.tab)) { |
|
|
|
|
focus = focus === "panel" ? "menu" : "panel"; |
|
|
|
|
refresh(); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
if (focus === "panel") { |
|
|
|
|
if (panel.handleInput(data)) { |
|
|
|
|
refresh(); |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
selectList.handleInput(data); |
|
|
|
|
refresh(); |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
}; |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function applyModify( |
|
|
|
|
event: WriteToolCallEvent | EditToolCallEvent, |
|
|
|
|
ctx: ExtensionContext, |
|
|
|
|
): Promise<string | undefined> { |
|
|
|
|
if (isToolCallEventType("write", event)) { |
|
|
|
|
const original = event.input.content; |
|
|
|
|
const edited = await ctx.ui.editor(`Modifica il contenuto di ${event.input.path}:`, original); |
|
|
|
|
if (edited === undefined) return "Modifica annullata dall'utente"; |
|
|
|
|
event.input.content = edited; |
|
|
|
|
return undefined; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const { path, edits } = event.input; |
|
|
|
|
if (edits.length === 0) return undefined; |
|
|
|
|
|
|
|
|
|
if (edits.length === 1) { |
|
|
|
|
const edited = await ctx.ui.editor(`Modifica il nuovo testo per ${path}:`, edits[0].newText); |
|
|
|
|
if (edited === undefined) return "Modifica annullata dall'utente"; |
|
|
|
|
edits[0].newText = edited; |
|
|
|
|
return undefined; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const labels = edits.map((e, i) => `Blocco ${i + 1}: ${e.oldText.slice(0, 40).replace(/\n/g, " ")}...`); |
|
|
|
|
const selected = await ctx.ui.select("Quale blocco vuoi modificare?", labels); |
|
|
|
|
if (!selected) return "Modifica annullata dall'utente"; |
|
|
|
|
|
|
|
|
|
const idx = labels.indexOf(selected); |
|
|
|
|
if (idx < 0) return "Blocco non trovato"; |
|
|
|
|
|
|
|
|
|
const edited = await ctx.ui.editor(`Modifica il nuovo testo del blocco ${idx + 1} per ${path}:`, edits[idx].newText); |
|
|
|
|
if (edited === undefined) return "Modifica annullata dall'utente"; |
|
|
|
|
edits[idx].newText = edited; |
|
|
|
|
return undefined; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let hitlEnabled = true; |
|
|
|
|
|
|
|
|
|
export default function hitlProgramming(pi: ExtensionAPI) { |
|
|
|
|
console.log("HITL programming extension loaded"); |
|
|
|
|
|
|
|
|
|
pi.registerCommand("hitl", { |
|
|
|
|
description: "Attiva o disattiva HITL programming", |
|
|
|
|
handler: async (args, ctx) => { |
|
|
|
|
const arg = args.trim().toLowerCase(); |
|
|
|
|
if (arg === "off" || arg === "disattiva" || arg === "disable") { |
|
|
|
|
hitlEnabled = false; |
|
|
|
|
} else if (arg === "on" || arg === "attiva" || arg === "enable") { |
|
|
|
|
hitlEnabled = true; |
|
|
|
|
} else { |
|
|
|
|
hitlEnabled = !hitlEnabled; |
|
|
|
|
} |
|
|
|
|
ctx.ui.notify( |
|
|
|
|
`HITL programming ${hitlEnabled ? "attivato" : "disattivato"} (F10 oppure /hitl ${hitlEnabled ? "off" : "on"})`, |
|
|
|
|
hitlEnabled ? "info" : "warning", |
|
|
|
|
); |
|
|
|
|
}, |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
pi.registerShortcut("f10", { |
|
|
|
|
description: "Attiva o disattiva HITL programming", |
|
|
|
|
handler: async (ctx) => { |
|
|
|
|
hitlEnabled = !hitlEnabled; |
|
|
|
|
ctx.ui.notify( |
|
|
|
|
`HITL programming ${hitlEnabled ? "attivato" : "disattivato"} (F10 oppure /hitl ${hitlEnabled ? "off" : "on"})`, |
|
|
|
|
hitlEnabled ? "info" : "warning", |
|
|
|
|
); |
|
|
|
|
}, |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
pi.on("before_agent_start", async (_event, ctx) => { |
|
|
|
|
return { |
|
|
|
|
systemPrompt: ctx.getSystemPrompt() + HITL_SYSTEM_PROMPT_APPENDIX, |
|
|
|
|
}; |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
pi.on("tool_call", async (event, ctx) => { |
|
|
|
|
if (event.toolName !== "write" && event.toolName !== "edit") return undefined; |
|
|
|
|
|
|
|
|
|
if (!hitlEnabled) return undefined; |
|
|
|
|
|
|
|
|
|
if (!isToolCallEventType("write", event) && !isToolCallEventType("edit", event)) { |
|
|
|
|
return undefined; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const path = event.input.path; |
|
|
|
|
|
|
|
|
|
if (!isProgrammingFile(path)) { |
|
|
|
|
ctx.ui.notify(`HITL: salto file non di programmazione ${path}`, "info"); |
|
|
|
|
return undefined; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (!ctx.hasUI) { |
|
|
|
|
return { |
|
|
|
|
block: true, |
|
|
|
|
reason: "HITL programming extension richiede la UI interattiva di pi. Riprova in modalita TUI.", |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
ctx.ui.notify(`HITL: richiedo conferma per ${event.toolName} ${path}`, "info"); |
|
|
|
|
|
|
|
|
|
const explanation = await generateExplanation(event, ctx); |
|
|
|
|
const action = await askHitlAction(event, explanation, ctx); |
|
|
|
|
|
|
|
|
|
if (action === "reject") { |
|
|
|
|
return { block: true, reason: `L'utente ha rifiutato ${event.toolName} su ${path}` }; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (action === "discuss") { |
|
|
|
|
const topic = await ctx.ui.input("Cosa vuoi discutere o modificare?", ""); |
|
|
|
|
return { |
|
|
|
|
block: true, |
|
|
|
|
reason: `L'utente vuole discutere ${event.toolName} su ${path}: ${topic || "(nessun dettaglio fornito)"}`, |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (action === "modify") { |
|
|
|
|
const reason = await applyModify(event, ctx); |
|
|
|
|
if (reason) { |
|
|
|
|
return { block: true, reason }; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return undefined; |
|
|
|
|
}); |
|
|
|
|
} |