codex rules

This commit is contained in:
2026-03-12 15:01:19 +00:00
parent 32752b42e0
commit 0f99afae67
3 changed files with 873 additions and 251 deletions
+249 -249
View File
@@ -14,9 +14,9 @@ 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,
BorderedLoader,
convertToLlm,
serializeConversation,
} from "@mariozechner/pi-coding-agent";
import * as fs from "node:fs";
import * as path from "node:path";
@@ -43,306 +43,306 @@ Output ONLY the session name, nothing else.`;
const AUTO_NAME_MODEL = getModel("opencode-go", "minimax-m2.5");
// Number of messages before auto-naming kicks in
const AUTO_NAME_THRESHOLD = 2;
const AUTO_NAME_THRESHOLD = 1;
// 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
}
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) {
// Track if we've already attempted auto-naming for this session
let autoNamedAttempted = false;
export default function(pi: ExtensionAPI) {
// Track if we've already attempted auto-naming for this session
let autoNamedAttempted = false;
// 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}`);
// 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;
}
// 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",
);
// 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}`);
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;
}
// 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");
// 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;
}
// 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);
// 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}`);
log(`Total messages to analyze: ${messages.length}`);
if (messages.length === 0) {
log("No messages found, aborting");
return;
}
if (messages.length === 0) {
log("No messages found, aborting");
return;
}
// Convert to LLM format and serialize
const llmMessages = convertToLlm(messages);
const conversationText = serializeConversation(llmMessages);
// Convert to LLM format and serialize
const llmMessages = convertToLlm(messages);
const conversationText = serializeConversation(llmMessages);
log(`Conversation text length: ${conversationText.length}`);
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;
// 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...");
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"}`);
// 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;
}
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 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 },
);
const response = await complete(
AUTO_NAME_MODEL,
{ systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
{ apiKey },
);
log(`Response received, stopReason: ${response.stopReason}`);
log(`Response received, stopReason: ${response.stopReason}`);
if (response.stopReason === "aborted") {
log("Request was aborted");
return;
}
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
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}"`);
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
// 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}"`);
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");
}
};
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);
});
});
// 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");
});
// 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");
});
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();
// 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;
}
// 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;
}
// 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);
// 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;
}
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);
// 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;
// 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<string | null>(
(tui, theme, _kb, done) => {
const loader = new BorderedLoader(
tui,
theme,
"Generating session name...",
);
loader.onAbort = () => done(null);
// Generate name with loader UI
const result = await ctx.ui.custom<string | null>(
(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 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 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 },
);
const response = await complete(
AUTO_NAME_MODEL,
{ systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
{ apiKey, signal: loader.signal },
);
if (response.stopReason === "aborted") {
return null;
}
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
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;
};
return name;
};
doGenerate()
.then(done)
.catch((err) => {
console.error("Auto-naming failed:", err);
done(null);
});
doGenerate()
.then(done)
.catch((err) => {
console.error("Auto-naming failed:", err);
done(null);
});
return loader;
},
);
return loader;
},
);
if (result === null) {
ctx.ui.notify("Auto-naming cancelled", "info");
return;
}
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
// 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;
}
if (!cleanName) {
ctx.ui.notify("Failed to generate name", "error");
return;
}
pi.setSessionName(cleanName);
ctx.ui.notify(`Session auto-named: ${cleanName}`, "info");
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");
},
});
// Manual naming
pi.setSessionName(trimmedArgs);
ctx.ui.notify(`Session named: ${trimmedArgs}`, "info");
},
});
}