From 961676025bb346476ce7ceff40276bdf21c98c3b Mon Sep 17 00:00:00 2001 From: "Thomas G. Lopes" Date: Fri, 6 Mar 2026 18:09:14 +0000 Subject: [PATCH] auto-name pi sessions with kimi k2.5 after 3 messages --- pi/files/agent/extensions/package.json | 6 +- pi/files/agent/extensions/pnpm-lock.yaml | 90 +++--- pi/files/agent/extensions/session-name.ts | 357 ++++++++++++++++++++-- pi/files/agent/settings.json | 6 +- 4 files changed, 392 insertions(+), 67 deletions(-) diff --git a/pi/files/agent/extensions/package.json b/pi/files/agent/extensions/package.json index 6a92635..002602b 100644 --- a/pi/files/agent/extensions/package.json +++ b/pi/files/agent/extensions/package.json @@ -13,9 +13,9 @@ "vscode-languageserver-protocol": "^3.17.5" }, "devDependencies": { - "@mariozechner/pi-ai": "^0.56.0", - "@mariozechner/pi-coding-agent": "^0.56.0", - "@mariozechner/pi-tui": "^0.56.0", + "@mariozechner/pi-ai": "^0.56.3", + "@mariozechner/pi-coding-agent": "^0.56.3", + "@mariozechner/pi-tui": "^0.56.3", "@types/node": "^25.3.3", "typescript": "^5.7.0" }, diff --git a/pi/files/agent/extensions/pnpm-lock.yaml b/pi/files/agent/extensions/pnpm-lock.yaml index 7200e64..752b136 100644 --- a/pi/files/agent/extensions/pnpm-lock.yaml +++ b/pi/files/agent/extensions/pnpm-lock.yaml @@ -34,14 +34,14 @@ importers: version: 3.17.5 devDependencies: '@mariozechner/pi-ai': - specifier: ^0.56.0 - version: 0.56.0(ws@8.19.0)(zod@3.25.76) + specifier: ^0.56.3 + version: 0.56.3(ws@8.19.0)(zod@4.3.6) '@mariozechner/pi-coding-agent': - specifier: ^0.56.0 - version: 0.56.0(ws@8.19.0)(zod@3.25.76) + specifier: ^0.56.3 + version: 0.56.3(ws@8.19.0)(zod@4.3.6) '@mariozechner/pi-tui': - specifier: ^0.56.0 - version: 0.56.0 + specifier: ^0.56.3 + version: 0.56.3 '@types/node': specifier: ^25.3.3 version: 25.3.3 @@ -289,26 +289,26 @@ packages: resolution: {integrity: sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw==} hasBin: true - '@mariozechner/pi-agent-core@0.56.0': - resolution: {integrity: sha512-p8lEhONkQJgnALbkgpYd9haXcWEB32lXExvB32Y6b7JUTIdU/HIGwe8+NFVmrLrnhORbAE1ORY+3AtwtiogD4g==} + '@mariozechner/pi-agent-core@0.56.3': + resolution: {integrity: sha512-TsI1zENf3wqqKPaERnj486Q4i6Y/y6lAZipLNcfDYUDxDrLwNfQ9EW9xukkbJfTZ8zjG3VZ2pBZe3C7wM51dVQ==} engines: {node: '>=20.0.0'} - '@mariozechner/pi-ai@0.56.0': - resolution: {integrity: sha512-4YvTpPodywMFBMsKJfxjWJN5KcQYYc3WVvfa7mofk9Xnb6HZdFKez8wxznGWX5B6vMizvTnD4cyt/XuMcBLRFw==} + '@mariozechner/pi-ai@0.56.3': + resolution: {integrity: sha512-l4J+cVyVeBLAlGOY/osGDvsbTz0DySCQmR171G6SdbPvIeLGhIi6siZ+zHwq91GJYjv/wtu/08M08ag2mGZKeA==} engines: {node: '>=20.0.0'} hasBin: true - '@mariozechner/pi-coding-agent@0.56.0': - resolution: {integrity: sha512-jnBLaA5z0IhUgohfIrfeGQFhFwpKtbrc9xr4qD573afzjC9xoa7lZX9+Z1Uuh54BVVnVOW9kn3C65ITU0+6SuQ==} - engines: {node: '>=20.0.0'} + '@mariozechner/pi-coding-agent@0.56.3': + resolution: {integrity: sha512-yHgnadye+TT/4NWKBirZUjw/LWdNWTa7M4HJdX2RxRbwuj4q7RZ0Aqy+lQbOHEPDQYhxK3kZb9hjiAbbGficZQ==} + engines: {node: '>=20.6.0'} hasBin: true - '@mariozechner/pi-tui@0.56.0': - resolution: {integrity: sha512-FZnvYvyvKJenFQqIs3iW0MGzrbOrTcAV6jFN9SFFqrjS7RDn8PkZ0iS3wZcey+2sT+YAf8AKu4f4BOXUJNB+IQ==} + '@mariozechner/pi-tui@0.56.3': + resolution: {integrity: sha512-eZ1P9QRKHp78hwx+lITr/mujZqe+eCwL/bOS9vXXkFP070RW4VYum0j7TJ4BrFEH/nNkXRS1tYCXYU05une1bA==} engines: {node: '>=20.0.0'} - '@mistralai/mistralai@1.10.0': - resolution: {integrity: sha512-tdIgWs4Le8vpvPiUEWne6tK0qbVc+jMenujnvTqOjogrJUsCSQhus0tHTU1avDDh5//Rq2dFgP9mWRAdIEoBqg==} + '@mistralai/mistralai@1.14.1': + resolution: {integrity: sha512-IiLmmZFCCTReQgPAT33r7KQ1nYo5JPdvGkrkZqA8qQ2qB1GHgs5LoP5K2ICyrjnpw2n8oSxMM/VP+liiKcGNlQ==} '@mixmark-io/domino@2.2.0': resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} @@ -981,8 +981,8 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - openai@6.10.0: - resolution: {integrity: sha512-ITxOGo7rO3XRMiKA5l7tQ43iNNu+iXGFAcf2t+aWVzzqRaS0i7m1K2BhxNdaveB+5eENhO0VY1FkiZzhBk4v3A==} + openai@6.26.0: + resolution: {integrity: sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==} hasBin: true peerDependencies: ws: ^8.18.0 @@ -1262,18 +1262,18 @@ packages: peerDependencies: zod: ^3.25 || ^4 - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} snapshots: '@anthropic-ai/sdk@0.52.0': {} - '@anthropic-ai/sdk@0.73.0(zod@3.25.76)': + '@anthropic-ai/sdk@0.73.0(zod@4.3.6)': dependencies: json-schema-to-ts: 3.1.1 optionalDependencies: - zod: 3.25.76 + zod: 4.3.6 '@aws-crypto/crc32@5.2.0': dependencies: @@ -1722,9 +1722,9 @@ snapshots: std-env: 3.10.0 yoctocolors: 2.1.2 - '@mariozechner/pi-agent-core@0.56.0(ws@8.19.0)(zod@3.25.76)': + '@mariozechner/pi-agent-core@0.56.3(ws@8.19.0)(zod@4.3.6)': dependencies: - '@mariozechner/pi-ai': 0.56.0(ws@8.19.0)(zod@3.25.76) + '@mariozechner/pi-ai': 0.56.3(ws@8.19.0)(zod@4.3.6) transitivePeerDependencies: - '@modelcontextprotocol/sdk' - aws-crt @@ -1734,21 +1734,21 @@ snapshots: - ws - zod - '@mariozechner/pi-ai@0.56.0(ws@8.19.0)(zod@3.25.76)': + '@mariozechner/pi-ai@0.56.3(ws@8.19.0)(zod@4.3.6)': dependencies: - '@anthropic-ai/sdk': 0.73.0(zod@3.25.76) + '@anthropic-ai/sdk': 0.73.0(zod@4.3.6) '@aws-sdk/client-bedrock-runtime': 3.1002.0 '@google/genai': 1.43.0 - '@mistralai/mistralai': 1.10.0 + '@mistralai/mistralai': 1.14.1 '@sinclair/typebox': 0.34.48 ajv: 8.18.0 ajv-formats: 3.0.1(ajv@8.18.0) chalk: 5.6.2 - openai: 6.10.0(ws@8.19.0)(zod@3.25.76) + openai: 6.26.0(ws@8.19.0)(zod@4.3.6) partial-json: 0.1.7 proxy-agent: 6.5.0 undici: 7.22.0 - zod-to-json-schema: 3.25.1(zod@3.25.76) + zod-to-json-schema: 3.25.1(zod@4.3.6) transitivePeerDependencies: - '@modelcontextprotocol/sdk' - aws-crt @@ -1758,12 +1758,12 @@ snapshots: - ws - zod - '@mariozechner/pi-coding-agent@0.56.0(ws@8.19.0)(zod@3.25.76)': + '@mariozechner/pi-coding-agent@0.56.3(ws@8.19.0)(zod@4.3.6)': dependencies: '@mariozechner/jiti': 2.6.5 - '@mariozechner/pi-agent-core': 0.56.0(ws@8.19.0)(zod@3.25.76) - '@mariozechner/pi-ai': 0.56.0(ws@8.19.0)(zod@3.25.76) - '@mariozechner/pi-tui': 0.56.0 + '@mariozechner/pi-agent-core': 0.56.3(ws@8.19.0)(zod@4.3.6) + '@mariozechner/pi-ai': 0.56.3(ws@8.19.0)(zod@4.3.6) + '@mariozechner/pi-tui': 0.56.3 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 cli-highlight: 2.1.11 @@ -1790,7 +1790,7 @@ snapshots: - ws - zod - '@mariozechner/pi-tui@0.56.0': + '@mariozechner/pi-tui@0.56.3': dependencies: '@types/mime-types': 2.1.4 chalk: 5.6.2 @@ -1800,10 +1800,14 @@ snapshots: optionalDependencies: koffi: 2.15.1 - '@mistralai/mistralai@1.10.0': + '@mistralai/mistralai@1.14.1': dependencies: - zod: 3.25.76 - zod-to-json-schema: 3.25.1(zod@3.25.76) + ws: 8.19.0 + zod: 4.3.6 + zod-to-json-schema: 3.25.1(zod@4.3.6) + transitivePeerDependencies: + - bufferutil + - utf-8-validate '@mixmark-io/domino@2.2.0': {} @@ -2581,10 +2585,10 @@ snapshots: dependencies: wrappy: 1.0.2 - openai@6.10.0(ws@8.19.0)(zod@3.25.76): + openai@6.26.0(ws@8.19.0)(zod@4.3.6): optionalDependencies: ws: 8.19.0 - zod: 3.25.76 + zod: 4.3.6 p-limit@6.2.0: dependencies: @@ -2844,8 +2848,8 @@ snapshots: yoctocolors@2.1.2: {} - zod-to-json-schema@3.25.1(zod@3.25.76): + zod-to-json-schema@3.25.1(zod@4.3.6): dependencies: - zod: 3.25.76 + zod: 4.3.6 - zod@3.25.76: {} + zod@4.3.6: {} diff --git a/pi/files/agent/extensions/session-name.ts b/pi/files/agent/extensions/session-name.ts index 8ff1c37..0dafad5 100644 --- a/pi/files/agent/extensions/session-name.ts +++ b/pi/files/agent/extensions/session-name.ts @@ -1,27 +1,348 @@ /** - * Session naming example. + * Enhanced session naming with AI-powered auto-naming. * - * Shows setSessionName/getSessionName to give sessions friendly names - * that appear in the session selector instead of the first message. + * Features: + * - Manual naming: /session-name [name] + * - Auto-naming command: /session-name --auto + * - Automatic naming: triggers after 3 messages if no name set * - * Usage: /session-name [name] - set or show session name + * Auto-naming analyzes the conversation history and generates a concise, + * descriptive name for the session using a cheap model (MiniMax). */ -import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; +import { complete, type Message } from "@mariozechner/pi-ai"; +import { getModel } from "@mariozechner/pi-ai"; +import type { ExtensionAPI, SessionEntry } from "@mariozechner/pi-coding-agent"; +import { + BorderedLoader, + convertToLlm, + serializeConversation, +} from "@mariozechner/pi-coding-agent"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import * as os from "node:os"; + +const SYSTEM_PROMPT = `You are a session naming assistant. Given a conversation history, generate a short, descriptive session name (2-5 words) that captures the main topic or task. + +Guidelines: +- Be concise but specific +- Use kebab-case or natural language +- Focus on the core task/question +- Avoid generic names like "discussion" or "conversation" +- No quotes, no punctuation at the end + +Examples: +- "fix auth bug" -> "fix-auth-bug" or "authentication fix" +- "how do I deploy to vercel" -> "vercel deployment" +- "explain react hooks" -> "react hooks explanation" +- "optimize database queries" -> "db query optimization" + +Output ONLY the session name, nothing else.`; + +// Cheap model for auto-naming: Minimax from OpenCode Go +const AUTO_NAME_MODEL = getModel("opencode-go", "minimax-m2.5"); + +// Number of messages before auto-naming kicks in +const AUTO_NAME_THRESHOLD = 3; + +// Debug log file +const LOG_FILE = path.join(os.homedir(), ".pi", "session-name-debug.log"); + +function log(message: string) { + const timestamp = new Date().toISOString(); + const entry = `[${timestamp}] ${message}\n`; + try { + fs.appendFileSync(LOG_FILE, entry); + } catch (e) { + // ignore write errors + } +} export default function (pi: ExtensionAPI) { - pi.registerCommand("session-name", { - description: "Set or show session name (usage: /session-name [new name])", - handler: async (args, ctx) => { - const name = args.trim(); + // Track if we've already attempted auto-naming for this session + let autoNamedAttempted = false; - if (name) { - pi.setSessionName(name); - ctx.ui.notify(`Session named: ${name}`, "info"); - } else { - const current = pi.getSessionName(); - ctx.ui.notify(current ? `Session: ${current}` : "No session name set", "info"); - } - }, - }); + // Listen for agent_end to auto-name sessions (non-blocking) + pi.on("agent_end", (_event, ctx) => { + log("=== agent_end triggered ==="); + log(`hasUI: ${ctx.hasUI}`); + log(`current session name: ${pi.getSessionName()}`); + log(`autoNamedAttempted: ${autoNamedAttempted}`); + + // Skip if already has a name or already attempted + if (pi.getSessionName() || autoNamedAttempted) { + log("Skipping: already has name or already attempted"); + return; + } + + // Count user messages in the branch + const branch = ctx.sessionManager.getBranch(); + const userMessages = branch.filter( + (entry): entry is SessionEntry & { type: "message" } => + entry.type === "message" && entry.message.role === "user", + ); + + log(`Total entries in branch: ${branch.length}`); + log(`User messages: ${userMessages.length}`); + log(`Threshold: ${AUTO_NAME_THRESHOLD}`); + + // Only auto-name after threshold is reached + if (userMessages.length < AUTO_NAME_THRESHOLD) { + log("Skipping: below threshold"); + return; + } + + // Mark as attempted so we don't try again + autoNamedAttempted = true; + log("Threshold reached, attempting auto-name"); + + // Only auto-name in interactive mode + if (!ctx.hasUI) { + log("Skipping: no UI (non-interactive mode)"); + return; + } + + // Gather conversation context + const messages = branch + .filter( + (entry): entry is SessionEntry & { type: "message" } => + entry.type === "message", + ) + .map((entry) => entry.message); + + log(`Total messages to analyze: ${messages.length}`); + + if (messages.length === 0) { + log("No messages found, aborting"); + return; + } + + // Convert to LLM format and serialize + const llmMessages = convertToLlm(messages); + const conversationText = serializeConversation(llmMessages); + + log(`Conversation text length: ${conversationText.length}`); + + // Truncate if too long (keep costs low) + const maxChars = 4000; + const truncatedText = + conversationText.length > maxChars + ? conversationText.slice(0, maxChars) + "\n..." + : conversationText; + + log(`Truncated text length: ${truncatedText.length}`); + log("Starting background auto-name..."); + + // Fire-and-forget: run auto-naming in background without blocking + const doAutoName = async () => { + const apiKey = await ctx.modelRegistry.getApiKey(AUTO_NAME_MODEL); + log(`Got API key: ${apiKey ? "yes" : "no"}`); + + if (!apiKey) { + log("No API key available, aborting"); + return; + } + + const userMessage: Message = { + role: "user", + content: [ + { + type: "text", + text: `## Conversation History\n\n${truncatedText}\n\nGenerate a concise session name for this conversation.`, + }, + ], + timestamp: Date.now(), + }; + + const response = await complete( + AUTO_NAME_MODEL, + { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] }, + { apiKey }, + ); + + log(`Response received, stopReason: ${response.stopReason}`); + + if (response.stopReason === "aborted") { + log("Request was aborted"); + return; + } + + const name = response.content + .filter((c): c is { type: "text"; text: string } => c.type === "text") + .map((c) => c.text.trim()) + .join(" ") + .replace(/^[\"']|[\"']$/g, ""); // Remove surrounding quotes + + log(`Generated name: "${name}"`); + + // Clean up the generated name + const cleanName = name + .replace(/\n/g, " ") + .replace(/\s+/g, " ") + .trim() + .slice(0, 50); // Max 50 chars + + log(`Cleaned name: "${cleanName}"`); + + if (cleanName) { + pi.setSessionName(cleanName); + ctx.ui.notify(`Auto-named: ${cleanName}`, "info"); + log(`Successfully set session name to: ${cleanName}`); + } else { + log("Cleaned name was empty, not setting"); + } + }; + + // Run in background without awaiting - don't block the agent + doAutoName().catch((err) => { + log(`ERROR: ${err}`); + console.error("Auto-naming failed:", err); + }); + }); + + // Reset flag on new session + pi.on("session_start", () => { + log("=== session_start ==="); + autoNamedAttempted = false; + log("Reset autoNamedAttempted to false"); + }); + + pi.on("session_switch", () => { + log("=== session_switch ==="); + autoNamedAttempted = false; + log("Reset autoNamedAttempted to false"); + }); + + // Manual command for setting/getting session name + pi.registerCommand("session-name", { + description: + "Set, show, or auto-generate session name (usage: /session-name [name] or /session-name --auto)", + handler: async (args, ctx) => { + const trimmedArgs = args.trim(); + + // Show current name if no args + if (!trimmedArgs) { + const current = pi.getSessionName(); + ctx.ui.notify( + current ? `Session: ${current}` : "No session name set", + "info", + ); + return; + } + + // Auto-generate name using AI + if (trimmedArgs === "--auto" || trimmedArgs === "-a") { + if (!ctx.hasUI) { + ctx.ui.notify("Auto-naming requires interactive mode", "error"); + return; + } + + // Gather conversation context + const branch = ctx.sessionManager.getBranch(); + const messages = branch + .filter( + (entry): entry is SessionEntry & { type: "message" } => + entry.type === "message", + ) + .map((entry) => entry.message); + + if (messages.length === 0) { + ctx.ui.notify("No conversation to analyze", "error"); + return; + } + + // Convert to LLM format and serialize + const llmMessages = convertToLlm(messages); + const conversationText = serializeConversation(llmMessages); + + // Truncate if too long (keep costs low) + const maxChars = 4000; + const truncatedText = + conversationText.length > maxChars + ? conversationText.slice(0, maxChars) + "\n..." + : conversationText; + + // Generate name with loader UI + const result = await ctx.ui.custom( + (tui, theme, _kb, done) => { + const loader = new BorderedLoader( + tui, + theme, + "Generating session name...", + ); + loader.onAbort = () => done(null); + + const doGenerate = async () => { + const apiKey = await ctx.modelRegistry.getApiKey(AUTO_NAME_MODEL); + + const userMessage: Message = { + role: "user", + content: [ + { + type: "text", + text: `## Conversation History\n\n${truncatedText}\n\nGenerate a concise session name for this conversation.`, + }, + ], + timestamp: Date.now(), + }; + + const response = await complete( + AUTO_NAME_MODEL, + { systemPrompt: SYSTEM_PROMPT, messages: [userMessage] }, + { apiKey, signal: loader.signal }, + ); + + if (response.stopReason === "aborted") { + return null; + } + + const name = response.content + .filter( + (c): c is { type: "text"; text: string } => c.type === "text", + ) + .map((c) => c.text.trim()) + .join(" ") + .replace(/^[\"']|[\"']$/g, ""); // Remove surrounding quotes + + return name; + }; + + doGenerate() + .then(done) + .catch((err) => { + console.error("Auto-naming failed:", err); + done(null); + }); + + return loader; + }, + ); + + if (result === null) { + ctx.ui.notify("Auto-naming cancelled", "info"); + return; + } + + // Clean up the generated name + const cleanName = result + .replace(/\n/g, " ") + .replace(/\s+/g, " ") + .trim() + .slice(0, 50); // Max 50 chars + + if (!cleanName) { + ctx.ui.notify("Failed to generate name", "error"); + return; + } + + pi.setSessionName(cleanName); + ctx.ui.notify(`Session auto-named: ${cleanName}`, "info"); + return; + } + + // Manual naming + pi.setSessionName(trimmedArgs); + ctx.ui.notify(`Session named: ${trimmedArgs}`, "info"); + }, + }); } diff --git a/pi/files/agent/settings.json b/pi/files/agent/settings.json index 1960cca..fa5abc9 100644 --- a/pi/files/agent/settings.json +++ b/pi/files/agent/settings.json @@ -1,7 +1,7 @@ { - "lastChangelogVersion": "0.56.1", - "defaultProvider": "openrouter", - "defaultModel": "openai/gpt-5.3-codex", + "lastChangelogVersion": "0.56.3", + "defaultProvider": "opencode-go", + "defaultModel": "kimi-k2.5", "defaultThinkingLevel": "high", "theme": "matugen", "lsp": {