Commit c5f92f8f5607
Changed files (1)
dots
pi
agent
extensions
dots/pi/agent/extensions/terminal-status.ts
@@ -2,14 +2,16 @@
* Pi Terminal Status Extension
*
* Manages terminal status updates:
- * 1. Terminal tab title - shows project name, current tool, and status
+ * 1. Terminal tab title - shows project, branch, model, current tool, and status
* 2. Desktop notifications - alerts when agent is ready for input
*
- * Terminal title format: "π <project> • <context>"
+ * Terminal title format: "π <project> (<branch>) [<model>] • <context>"
* Examples:
- * - "π home" (idle)
- * - "π home • bash" (running bash)
- * - "π home • Ready" (waiting for input)
+ * - "π home (main) [sonnet]" (idle)
+ * - "π home (main) [sonnet] • bash" (running bash)
+ * - "π home (main) [sonnet] • ✗ bash" (tool error)
+ * - "π home (main) [sonnet] • 📓 Journal" (using skill)
+ * - "π home (main) [sonnet] • Ready" (waiting for input)
*
* Notification protocols supported:
* - OSC 777: Ghostty, iTerm2, WezTerm, rxvt-unicode
@@ -21,8 +23,16 @@
*/
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
+import { execSync } from "node:child_process";
import path from "node:path";
+// =============================================================================
+// State
+// =============================================================================
+
+let currentModel = "";
+let currentBranch = "";
+
// =============================================================================
// Terminal Title
// =============================================================================
@@ -48,17 +58,121 @@ function getProjectName(): string {
}
}
+/**
+ * Get current git branch
+ */
+function getGitBranch(): string {
+ try {
+ const branch = execSync("git rev-parse --abbrev-ref HEAD 2>/dev/null", {
+ encoding: "utf-8",
+ timeout: 1000,
+ }).trim();
+ return branch || "";
+ } catch {
+ return "";
+ }
+}
+
+/**
+ * Get short model name for display
+ */
+function getShortModelName(modelId: string): string {
+ // Map common model names to short versions
+ const shortNames: Record<string, string> = {
+ "claude-sonnet-4-5-20250514": "sonnet",
+ "claude-sonnet-4-20250514": "sonnet",
+ "claude-3-5-sonnet-20241022": "sonnet",
+ "claude-3-5-sonnet": "sonnet",
+ "claude-sonnet": "sonnet",
+ "claude-opus-4-20250514": "opus",
+ "claude-3-opus": "opus",
+ "claude-opus": "opus",
+ "claude-haiku": "haiku",
+ "claude-3-haiku": "haiku",
+ "gpt-4o": "4o",
+ "gpt-4o-mini": "4o-mini",
+ "gpt-4-turbo": "4-turbo",
+ "gpt-4": "gpt4",
+ "gpt-3.5-turbo": "3.5",
+ "gemini-2.0-flash": "flash",
+ "gemini-2.0-pro": "pro",
+ "gemini-1.5-pro": "1.5-pro",
+ "gemini-1.5-flash": "1.5-flash",
+ "deepseek-chat": "deepseek",
+ "deepseek-reasoner": "r1",
+ };
+
+ // Check exact match first
+ if (shortNames[modelId]) {
+ return shortNames[modelId];
+ }
+
+ // Check partial match
+ for (const [pattern, short] of Object.entries(shortNames)) {
+ if (modelId.includes(pattern)) {
+ return short;
+ }
+ }
+
+ // Fallback: take last part after slash or dash, limit to 10 chars
+ const parts = modelId.split(/[/-]/);
+ const lastPart = parts[parts.length - 1];
+ return lastPart.slice(0, 10);
+}
+
+/**
+ * Get emoji icon for known skills
+ */
+function getSkillIcon(skillName: string): string {
+ const icons: Record<string, string> = {
+ Journal: "📓",
+ Notes: "📝",
+ TODOs: "✅",
+ Org: "📋",
+ Git: "🔀",
+ GitHub: "🐙",
+ Email: "📧",
+ Python: "🐍",
+ golang: "🐹",
+ Rust: "🦀",
+ Nix: "❄️",
+ Kubernetes: "☸️",
+ Tekton: "🔧",
+ Docker: "🐳",
+ Homelab: "🏠",
+ };
+
+ return icons[skillName] || "📚";
+}
+
/**
* Build terminal title with optional context
*/
-function buildTitle(context?: string): string {
+function buildTitle(context?: string, isError?: boolean): string {
const project = getProjectName();
const prefix = "π";
- if (context) {
- return `${prefix} ${project} • ${context}`;
+ let parts = [prefix, project];
+
+ // Add git branch if available
+ if (currentBranch) {
+ parts.push(`(${currentBranch})`);
}
- return `${prefix} ${project}`;
+
+ // Add model if available
+ if (currentModel) {
+ parts.push(`[${currentModel}]`);
+ }
+
+ let title = parts.join(" ");
+
+ // Add context (tool name, status, etc.)
+ if (context) {
+ const errorPrefix = isError ? "✗ " : "";
+ title += ` • ${errorPrefix}${context}`;
+ }
+
+ return title;
}
// =============================================================================
@@ -170,23 +284,49 @@ function notify(title: string, body: string): void {
export default function (pi: ExtensionAPI) {
// Set initial title on session start
- pi.on("session_start", async () => {
+ pi.on("session_start", async (_event, ctx) => {
+ currentBranch = getGitBranch();
+ // Get initial model if available
+ if (ctx.model) {
+ currentModel = getShortModelName(ctx.model.id);
+ }
+ setTerminalTitle(buildTitle());
+ });
+
+ // Update model indicator when model changes
+ pi.on("model_select", async (event) => {
+ currentModel = getShortModelName(event.model.id);
setTerminalTitle(buildTitle());
});
// Update title when tool starts executing
pi.on("tool_call", async (event) => {
const toolName = event.toolName.toLowerCase();
- setTerminalTitle(buildTitle(toolName));
+
+ // Check if this is a skill invocation
+ if (toolName === "skill" && event.input?.skill) {
+ const skillName = event.input.skill;
+ const icon = getSkillIcon(skillName);
+ setTerminalTitle(buildTitle(`${icon} ${skillName}`));
+ } else {
+ setTerminalTitle(buildTitle(toolName));
+ }
});
- // Reset title when tool finishes
- pi.on("tool_result", async () => {
- setTerminalTitle(buildTitle());
+ // Update title when tool finishes (show error if failed)
+ pi.on("tool_result", async (event) => {
+ if (event.isError) {
+ const toolName = event.toolName.toLowerCase();
+ setTerminalTitle(buildTitle(toolName, true));
+ } else {
+ setTerminalTitle(buildTitle());
+ }
});
// Show "Ready" and send notification when agent is done
pi.on("agent_end", async () => {
+ // Refresh branch in case it changed during session
+ currentBranch = getGitBranch();
setTerminalTitle(buildTitle("Ready"));
notify("Pi", "Ready for input");
});