From 42dda2013b995457acbc278c00fc04560f0689af Mon Sep 17 00:00:00 2001 From: ace-pm Date: Wed, 15 Apr 2026 16:18:29 +0200 Subject: [PATCH] Remove old working-vibes and prompt-history extensions. Consolidated into unified sf-tui extension. Removed old git-footer extension in favor of sf-tui footer implementation. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/resources/extensions/git-footer/index.ts | 108 --------- .../extensions/prompt-history/index.ts | 228 ------------------ src/resources/extensions/sf/working-vibes.ts | 85 ------- 3 files changed, 421 deletions(-) delete mode 100644 src/resources/extensions/git-footer/index.ts delete mode 100644 src/resources/extensions/prompt-history/index.ts delete mode 100644 src/resources/extensions/sf/working-vibes.ts diff --git a/src/resources/extensions/git-footer/index.ts b/src/resources/extensions/git-footer/index.ts deleted file mode 100644 index 01bac59cb..000000000 --- a/src/resources/extensions/git-footer/index.ts +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Git Footer Extension — show branch + dirty status in the footer - * - * Renders a minimal git status line when not overridden by auto-mode. - * Updates automatically when file or git operations complete. - */ - -import type { ExtensionAPI, ExtensionContext, Theme, ReadonlyFooterDataProvider } from "@sf-run/pi-coding-agent"; -import { truncateToWidth } from "@sf-run/pi-tui"; -import { execFileSync } from "node:child_process"; - -let cachedBranch: string | null = null; -let cachedDirty = false; -let cachedUntracked = false; -let lastFetchAt = 0; - -function refreshGitStatus(cwd: string): void { - const now = Date.now(); - if (now - lastFetchAt < 500) return; // debounce - lastFetchAt = now; - - try { - cachedBranch = execFileSync("git", ["branch", "--show-current"], { - cwd, - encoding: "utf-8", - stdio: ["pipe", "pipe", "ignore"], - timeout: 2000, - }).trim() || null; - } catch { - cachedBranch = null; - cachedDirty = false; - cachedUntracked = false; - return; - } - - try { - const status = execFileSync("git", ["status", "--porcelain"], { - cwd, - encoding: "utf-8", - stdio: ["pipe", "pipe", "ignore"], - timeout: 2000, - }); - const lines = status.split("\n").filter((l) => l.length > 2); - cachedDirty = lines.some((l) => l[0] === "M" || l[0] === "A" || l[0] === "D" || l[0] === "R" || l[0] === "C" || l[1] === "M" || l[1] === "A" || l[1] === "D" || l[1] === "R" || l[1] === "C"); - cachedUntracked = lines.some((l) => l.startsWith("??")); - } catch { - cachedDirty = false; - cachedUntracked = false; - } -} - -function invalidateGitStatus(): void { - lastFetchAt = 0; -} - -function renderGitFooter(theme: Theme, _footerData: ReadonlyFooterDataProvider, width: number): string[] { - const cwd = process.cwd(); - refreshGitStatus(cwd); - - if (!cachedBranch) { - return []; - } - - const parts: string[] = []; - parts.push(`⎇ ${cachedBranch}`); - if (cachedDirty) parts.push("✗"); - else if (cachedUntracked) parts.push("?"); - else parts.push("✓"); - - const text = theme.fg("dim", parts.join(" ")); - return [truncateToWidth(text, width, theme.fg("dim", "…"))]; -} - -export default function gitFooter(pi: ExtensionAPI) { - let tuiRef: { requestRender: () => void } | null = null; - - pi.on("session_start", async (_event, ctx: ExtensionContext) => { - if (!ctx.hasUI) return; - - ctx.ui.setFooter((tui, theme, footerData) => { - tuiRef = tui; - return { - render: (width: number) => renderGitFooter(theme, footerData, width), - invalidate: () => {}, - dispose: () => { - tuiRef = null; - }, - }; - }); - }); - - pi.on("tool_result", async (event) => { - // Invalidate git status on file changes or git commands - if (event.toolName === "write" || event.toolName === "edit") { - invalidateGitStatus(); - tuiRef?.requestRender(); - return; - } - - if (event.toolName === "bash" && event.input?.command) { - const cmd = String(event.input.command); - if (/\bgit\b/.test(cmd)) { - invalidateGitStatus(); - tuiRef?.requestRender(); - } - } - }); -} diff --git a/src/resources/extensions/prompt-history/index.ts b/src/resources/extensions/prompt-history/index.ts deleted file mode 100644 index 9c1c3f1da..000000000 --- a/src/resources/extensions/prompt-history/index.ts +++ /dev/null @@ -1,228 +0,0 @@ -/** - * Prompt History Extension — stash and recall prompts - * - * Features: - * - Automatically stashes every prompt sent to the agent - * - Ctrl+Alt+H opens the stash overlay - * - Navigate with ↑/↓ or j/k, Enter to insert, Esc to cancel - */ - -import type { ExtensionAPI, ExtensionContext, Theme } from "@sf-run/pi-coding-agent"; -import { truncateToWidth, visibleWidth, matchesKey, Key } from "@sf-run/pi-tui"; -import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; -import { join, dirname } from "node:path"; -import { homedir } from "node:os"; - -const STASH_LIMIT = 20; - -interface StashData { - version: number; - history: string[]; -} - -function getStashPath(): string { - return join(homedir(), ".sf", "agent", "prompt-history.json"); -} - -function readStash(): string[] { - const path = getStashPath(); - try { - if (!existsSync(path)) return []; - const data = JSON.parse(readFileSync(path, "utf-8")) as StashData; - if (!data || !Array.isArray(data.history)) return []; - return data.history.filter((h): h is string => typeof h === "string" && h.trim().length > 0); - } catch { - return []; - } -} - -function writeStash(history: string[]): void { - const path = getStashPath(); - try { - mkdirSync(dirname(path), { recursive: true }); - writeFileSync( - path, - JSON.stringify({ version: 1, history: history.slice(0, STASH_LIMIT) }, null, 2) + "\n", - "utf-8" - ); - } catch { /* non-fatal */ } -} - -function pushStash(history: string[], text: string): void { - const trimmed = text.trim(); - if (!trimmed) return; - if (history[0] === trimmed) return; - history.unshift(trimmed); - if (history.length > STASH_LIMIT) { - history.length = STASH_LIMIT; - } -} - -function buildPreview(text: string, maxWidth: number): string { - const compact = text.replace(/\s+/g, " ").trim(); - if (!compact) return "(empty)"; - return truncateToWidth(compact, maxWidth, "…"); -} - -class PromptHistoryOverlay { - private tui: { requestRender: () => void }; - private theme: Theme; - private onClose: (selected: string | null) => void; - private items: string[]; - private selected = 0; - private cachedWidth = 0; - private cachedLines: string[] = []; - - constructor( - tui: { requestRender: () => void }, - theme: Theme, - items: string[], - onClose: (selected: string | null) => void - ) { - this.tui = tui; - this.theme = theme; - this.items = items; - this.onClose = onClose; - } - - handleInput(data: string): void { - if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) { - this.onClose(null); - return; - } - if (matchesKey(data, Key.return) || matchesKey(data, Key.enter)) { - this.onClose(this.items[this.selected] ?? null); - return; - } - if (matchesKey(data, Key.down) || data === "j") { - this.selected = Math.min(this.items.length - 1, this.selected + 1); - this.invalidate(); - this.tui.requestRender(); - return; - } - if (matchesKey(data, Key.up) || data === "k") { - this.selected = Math.max(0, this.selected - 1); - this.invalidate(); - this.tui.requestRender(); - return; - } - } - - invalidate(): void { - this.cachedWidth = 0; - } - - render(width: number): string[] { - if (this.cachedWidth === width) { - return this.cachedLines; - } - - const th = this.theme; - const boxWidth = Math.min(80, width - 4); - const innerWidth = boxWidth - 4; - - const padLine = (line: string): string => { - const len = visibleWidth(line); - return line + " ".repeat(Math.max(0, width - len)); - }; - - const boxLine = (content: string): string => { - const len = visibleWidth(content); - const padding = Math.max(0, boxWidth - 2 - len); - return th.fg("dim", "│ ") + content + " ".repeat(padding) + th.fg("dim", " │"); - }; - - const lines: string[] = []; - lines.push(""); - lines.push(padLine(th.fg("dim", "╭" + "─".repeat(boxWidth) + "╮"))); - lines.push(padLine(boxLine(th.bold(th.fg("accent", "📜 Prompt History"))))); - lines.push(padLine(th.fg("dim", "├" + "─".repeat(boxWidth) + "┤"))); - lines.push(padLine(boxLine(th.fg("dim", "↑/jk navigate • Enter insert • Esc cancel")))); - lines.push(padLine(boxLine(""))); - - for (let i = 0; i < this.items.length; i++) { - const item = this.items[i]!; - const preview = buildPreview(item, innerWidth - 6); - const pointer = i === this.selected ? th.fg("accent", "❯ ") : " "; - const num = i < 9 ? th.fg("dim", `${i + 1}`) : " "; - const label = i === this.selected ? th.fg("accent", preview) : preview; - lines.push(padLine(boxLine(`${pointer}${num}. ${label}`))); - } - - lines.push(padLine(boxLine(""))); - lines.push(padLine(th.fg("dim", "├" + "─".repeat(boxWidth) + "┤"))); - lines.push(padLine(boxLine(th.fg("dim", `${this.items.length} stashed prompts`)))); - lines.push(padLine(th.fg("dim", "╰" + "─".repeat(boxWidth) + "╯"))); - lines.push(""); - - this.cachedLines = lines; - this.cachedWidth = width; - return lines; - } -} - -async function openPromptHistoryOverlay(ctx: ExtensionContext): Promise { - if (!ctx.hasUI) { - ctx.ui.notify("Prompt history requires interactive mode", "error"); - return; - } - - const items = readStash(); - if (items.length === 0) { - ctx.ui.notify("No stashed prompts yet. Send a message to build history.", "info"); - return; - } - - const selected = await ctx.ui.custom( - (tui, theme, _kb, done) => { - const overlay = new PromptHistoryOverlay(tui, theme, items, (sel) => done(sel)); - return { - render: (w) => overlay.render(w), - invalidate: () => overlay.invalidate(), - handleInput: (data) => overlay.handleInput(data), - }; - }, - { - overlay: true, - overlayOptions: { - width: "90%", - minWidth: 60, - maxHeight: "80%", - anchor: "center", - backdrop: true, - }, - } - ); - - if (selected) { - ctx.ui.setEditorText(selected); - ctx.ui.notify("Inserted prompt from history", "info"); - } -} - -export default function promptHistory(pi: ExtensionAPI) { - const stash = readStash(); - - pi.on("session_start", async (_event, ctx) => { - if (!ctx.hasUI) return; - - pi.registerShortcut(Key.ctrlAlt("h"), { - description: "Open prompt history stash", - handler: openPromptHistoryOverlay, - }); - - // Fallback for terminals where Ctrl+Alt chords are not forwarded - pi.registerShortcut(Key.ctrlShift("h"), { - description: "Open prompt history stash (fallback)", - handler: openPromptHistoryOverlay, - }); - }); - - pi.on("before_agent_start", async (event) => { - const prompt = event.prompt?.trim(); - if (prompt) { - pushStash(stash, prompt); - writeStash(stash); - } - }); -} diff --git a/src/resources/extensions/sf/working-vibes.ts b/src/resources/extensions/sf/working-vibes.ts deleted file mode 100644 index 07005134b..000000000 --- a/src/resources/extensions/sf/working-vibes.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Working Vibes — context-aware working messages - * - * Replaces the generic "Thinking..." with something more fun and informative. - */ - -import type { ExtensionContext } from "@sf-run/pi-coding-agent"; - -const VIBE_EMOJIS = ["✨", "🔥", "🚀", "⚡", "🧠", "🔍", "🛠️", "🎨", "🧪", "📦"]; - -function randomEmoji(): string { - return VIBE_EMOJIS[Math.floor(Math.random() * VIBE_EMOJIS.length)]; -} - -function vibeForPrompt(prompt: string): string { - const p = prompt.toLowerCase(); - if (p.includes("fix") || p.includes("bug") || p.includes("error") || p.includes("crash")) { - return `${randomEmoji()} Hunting bugs...`; - } - if (p.includes("test") || p.includes("spec") || p.includes("jest") || p.includes("vitest")) { - return `${randomEmoji()} Writing tests...`; - } - if (p.includes("refactor") || p.includes("clean") || p.includes("simplify")) { - return `${randomEmoji()} Refactoring...`; - } - if (p.includes("doc") || p.includes("readme") || p.includes("comment")) { - return `${randomEmoji()} Writing docs...`; - } - if (p.includes("deploy") || p.includes("release") || p.includes("publish")) { - return `${randomEmoji()} Shipping it...`; - } - if (p.includes("review") || p.includes("audit") || p.includes("check")) { - return `${randomEmoji()} Reviewing code...`; - } - if (p.includes("plan") || p.includes("design") || p.includes("architect")) { - return `${randomEmoji()} Architecting...`; - } - if (p.includes("search") || p.includes("find") || p.includes("lookup")) { - return `${randomEmoji()} Searching...`; - } - if (p.includes("implement") || p.includes("build") || p.includes("create")) { - return `${randomEmoji()} Building...`; - } - return `${randomEmoji()} Thinking...`; -} - -function vibeForTool(toolName: string, _input: unknown): string { - switch (toolName) { - case "bash": - return `${randomEmoji()} Running commands...`; - case "write": - return `${randomEmoji()} Writing files...`; - case "edit": - return `${randomEmoji()} Editing code...`; - case "read": - return `${randomEmoji()} Reading files...`; - case "search-the-web": - return `${randomEmoji()} Searching the web...`; - case "browser-navigate": - return `${randomEmoji()} Browsing...`; - case "ask_user_questions": - return `${randomEmoji()} Asking questions...`; - case "sf_task_complete": - case "sf_slice_complete": - case "sf_milestone_complete": - return `${randomEmoji()} Updating project state...`; - default: - return `${randomEmoji()} Working...`; - } -} - -export function setVibeForPrompt(ctx: ExtensionContext, prompt: string): void { - if (!ctx.hasUI) return; - ctx.ui.setWorkingMessage(vibeForPrompt(prompt)); -} - -export function setVibeForTool(ctx: ExtensionContext, toolName: string, input: unknown): void { - if (!ctx.hasUI) return; - ctx.ui.setWorkingMessage(vibeForTool(toolName, input)); -} - -export function clearVibe(ctx: ExtensionContext): void { - if (!ctx.hasUI) return; - ctx.ui.setWorkingMessage(); -}