Commit c5f92f8f5607

Vincent Demeester <vincent@sbr.pm>
2026-02-06 09:48:39
feat(pi): enhance terminal-status with branch, model, skills, errors
Added git branch display, model indicator with short names, skill icons with emoji, and error indicator (✗) on tool failures. Title now shows: π project (branch) [model] • context
1 parent a2e2b7a
Changed files (1)
dots
pi
agent
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");
   });