/** * Session Header Extension * * Zeigt den erkannten Sitzungszweck (Intention) als gelbe Überschrift * oberhalb des Editors an. Nutzt das LLM zur Zusammenfassung der ersten * Benutzereingabe, statt sie 1:1 zu transkribieren. * * Farbe: Gelb (warning), damit es sich von anderen UI-Elementen abhebt. * Fallback: Gekürzter Originaltext, wenn die LLM-Zusammenfassung fehlschlägt. */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { complete, getModel } from "@earendil-works/pi-ai"; const MAX_TITLE_LENGTH = 160; const MAX_SUMMARY_TOKENS = 60; function truncate(text: string): string { const trimmed = text.trim(); if (trimmed.length <= MAX_TITLE_LENGTH) return trimmed; return trimmed.slice(0, MAX_TITLE_LENGTH - 3) + "\u2026"; } /** * Ruft das aktuelle LLM auf, um aus der Benutzereingabe eine kurze * Intention-Zusammenfassung (5–12 Wörter) zu extrahieren. */ async function generateIntentionSummary( input: string, ctx: any, ): Promise { try { const provider = ctx.model?.provider; const modelId = ctx.model?.id; if (!provider || !modelId) return null; const model = getModel(provider, modelId); if (!model) return null; const registry = ctx.modelRegistry; const auth = registry ? await registry.getApiKeyAndHeaders(model) : null; if (!auth?.ok || !auth.apiKey) return null; const prompt = [ "Du extrahierst aus der folgenden Benutzereingabe maximal 5–10 Wörter", "als präzisen Session-Titel. Fasse die KERN-Intention zusammen,", "nicht die wörtliche Formulierung. Antworte NUR mit dem Titel,", "keinem weiteren Text.", "", "Eingabe:", input, ].join("\n"); const response = await complete( model, { messages: [ { role: "user", content: [{ type: "text" as const, text: prompt }], timestamp: Date.now(), }, ], }, { apiKey: auth.apiKey, headers: auth.headers, maxTokens: MAX_SUMMARY_TOKENS, }, ); const textParts = response.content .filter((c: any): c is { type: "text"; text: string } => c.type === "text") .map((c: any) => c.text) .join(" ") .trim(); if (!textParts) return null; // Entferne mögliche Anführungszeichen, die das LLM setzt return textParts.replace(/^["']|["']$/g, "").substring(0, MAX_TITLE_LENGTH); } catch (err) { // Bei Fehler (z.B. API nicht erreichbar, Modell nicht gefunden): Fallback auf truncate return null; } } function makeWidget(title: string | null, theme: any) { return { render(_width: number): string[] { if (!title) { return [theme.fg("warning", "\u25C6 Sitzung: (warte auf erste Eingabe\u2026)")]; } const label = theme.fg("warning", theme.bold("\u25C6")); const text = theme.fg("warning", ` ${title}`); return [`${label}${text}`]; }, invalidate() {}, }; } export default function (pi: ExtensionAPI) { let sessionTitle: string | null = null; pi.on("session_start", async (_event, ctx) => { if (!ctx.hasUI) return; // Bestehende Session: ersten User-Eintrag finden und zusammenfassen const entries = ctx.sessionManager.getEntries(); for (const entry of entries) { if (entry.type === "message" && (entry as any).message?.role === "user") { const text = (entry as any).message.content .filter((c: any) => c.type === "text") .map((c: any) => c.text) .join(" "); if (text.trim()) { // Bei Wiederaufnahme: versuche Zusammenfassung asynchron generateIntentionSummary(text, ctx).then((summary) => { if (summary) { sessionTitle = summary; pi.setSessionName(sessionTitle); ctx.ui.setWidget( "session-header", (_ui: any, theme: any) => makeWidget(sessionTitle, theme), ); } }).catch(() => { // Silent fail — truncate reicht als Fallback }); // Interim: gekürzten Originaltext anzeigen, bis Summary da ist sessionTitle = truncate(text); pi.setSessionName(sessionTitle); break; } } } ctx.ui.setWidget("session-header", (_ui: any, theme: any) => makeWidget(sessionTitle, theme)); }); // Erste Benutzereingabe: Intention erkennen statt 1:1 übernehmen pi.on("input", async (event, ctx) => { if (!ctx.hasUI) return; if (sessionTitle) return { action: "continue" }; // Interim: sofort gekürzten Originaltext anzeigen sessionTitle = truncate(event.text); pi.setSessionName(sessionTitle); ctx.ui.setWidget("session-header", (_ui: any, theme: any) => makeWidget(sessionTitle, theme)); // Async: LLM-Zusammenfassung nachladen generateIntentionSummary(event.text, ctx).then((summary) => { if (summary) { sessionTitle = summary; pi.setSessionName(sessionTitle); ctx.ui.setWidget("session-header", (_ui: any, theme: any) => makeWidget(sessionTitle, theme)); } }).catch(() => { // Silent fail — truncate reicht als Fallback }); return { action: "continue" }; }); pi.on("session_before_switch", async () => { sessionTitle = null; }); }