Commit aa4f2d1a95e8

Vincent Demeester <vincent@sbr.pm>
2026-03-26 05:13:45
perf(pi): remove 4 extensions, fix npm bloat
Removed answer, file-picker, files, and go-to-bed extensions as unused. Fixed npm install to use --omit=dev and skip extensions with no runtime deps, preventing pi SDK from being pulled into node_modules (~2.3GB → 34MB). Added .npmrc to vertex-claude to avoid peer dep auto-install. Moved pi SDK from dependencies to devDependencies in defaults and filter-output.
1 parent 7eaeb8e
Changed files (8)
dots/pi/agent/extensions/defaults/package.json
@@ -3,7 +3,7 @@
   "version": "1.0.0",
   "type": "module",
   "main": "index.ts",
-  "dependencies": {
+  "devDependencies": {
     "@mariozechner/pi-coding-agent": "*"
   }
 }
dots/pi/agent/extensions/filter-output/package.json
@@ -3,7 +3,7 @@
   "version": "1.0.0",
   "type": "module",
   "main": "index.ts",
-  "dependencies": {
+  "devDependencies": {
     "@mariozechner/pi-coding-agent": "*"
   }
 }
dots/pi/agent/extensions/vertex-claude/.npmrc
@@ -0,0 +1,1 @@
+legacy-peer-deps=true
dots/pi/agent/extensions/answer.ts
@@ -1,535 +0,0 @@
-/**
- * Q&A extraction hook - extracts questions from assistant responses
- *
- * Custom interactive TUI for answering questions.
- *
- * Demonstrates the "prompt generator" pattern with custom TUI:
- * 1. /answer command gets the last assistant message
- * 2. Shows a spinner while extracting questions as structured JSON
- * 3. Presents an interactive TUI to navigate and answer questions
- * 4. Submits the compiled answers when done
- *
- * Original: https://github.com/mitsuhiko/agent-stuff/blob/main/pi-extensions/answer.ts
- */
-
-import { complete, type Model, type Api, type UserMessage } from "@mariozechner/pi-ai";
-import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
-import { BorderedLoader } from "@mariozechner/pi-coding-agent";
-import {
-	type Component,
-	Editor,
-	type EditorTheme,
-	Key,
-	matchesKey,
-	truncateToWidth,
-	type TUI,
-	visibleWidth,
-	wrapTextWithAnsi,
-} from "@mariozechner/pi-tui";
-
-// Structured output format for question extraction
-interface ExtractedQuestion {
-	question: string;
-	context?: string;
-}
-
-interface ExtractionResult {
-	questions: ExtractedQuestion[];
-}
-
-const SYSTEM_PROMPT = `You are a question extractor. Given text from a conversation, extract any questions that need answering.
-
-Output a JSON object with this structure:
-{
-  "questions": [
-    {
-      "question": "The question text",
-      "context": "Optional context that helps answer the question"
-    }
-  ]
-}
-
-Rules:
-- Extract all questions that require user input
-- Keep questions in the order they appeared
-- Be concise with question text
-- Include context only when it provides essential information for answering
-- If no questions are found, return {"questions": []}
-
-Example output:
-{
-  "questions": [
-    {
-      "question": "What is your preferred database?",
-      "context": "We can only configure MySQL and PostgreSQL because of what is implemented."
-    },
-    {
-      "question": "Should we use TypeScript or JavaScript?"
-    }
-  ]
-}`;
-
-/**
- * Prefer a lightweight model for extraction, falling back to the current model.
- * Tries models in order of preference (cheap/fast first).
- */
-async function selectExtractionModel(
-	currentModel: Model<Api>,
-	modelRegistry: {
-		find: (provider: string, modelId: string) => Model<Api> | undefined;
-		getApiKey: (model: Model<Api>) => Promise<string | undefined>;
-	},
-): Promise<Model<Api>> {
-	const candidates = [
-		{ provider: "google", id: "gemini-2.5-pro" },
-		{ provider: "google", id: "gemini-2.0-flash" },
-		{ provider: "google", id: "gemini-1.5-flash" },
-		{ provider: "github-copilot", id: "gpt-5-mini" },
-		{ provider: "github-copilot", id: "gpt-4o" },
-		{ provider: "openai-codex", id: "gpt-5.1-codex-mini" },
-		{ provider: "anthropic", id: "claude-haiku-4-5" },
-		{ provider: "anthropic", id: "claude-3-5-haiku" },
-	];
-
-	for (const { provider, id } of candidates) {
-		const model = modelRegistry.find(provider, id);
-		if (model) {
-			const apiKey = await modelRegistry.getApiKey(model);
-			if (apiKey) {
-				return model;
-			}
-		}
-	}
-
-	return currentModel;
-}
-
-/**
- * Parse the JSON response from the LLM
- */
-function parseExtractionResult(text: string): ExtractionResult | null {
-	try {
-		// Try to find JSON in the response (it might be wrapped in markdown code blocks)
-		let jsonStr = text;
-
-		// Remove markdown code block if present
-		const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
-		if (jsonMatch) {
-			jsonStr = jsonMatch[1].trim();
-		}
-
-		const parsed = JSON.parse(jsonStr);
-		if (parsed && Array.isArray(parsed.questions)) {
-			return parsed as ExtractionResult;
-		}
-		return null;
-	} catch {
-		return null;
-	}
-}
-
-/**
- * Interactive Q&A component for answering extracted questions
- */
-class QnAComponent implements Component {
-	private questions: ExtractedQuestion[];
-	private answers: string[];
-	private currentIndex: number = 0;
-	private editor: Editor;
-	private tui: TUI;
-	private onDone: (result: string | null) => void;
-	private showingConfirmation: boolean = false;
-
-	// Cache
-	private cachedWidth?: number;
-	private cachedLines?: string[];
-
-	// Colors - using proper reset sequences
-	private dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
-	private bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
-	private cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
-	private green = (s: string) => `\x1b[32m${s}\x1b[0m`;
-	private yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
-	private gray = (s: string) => `\x1b[90m${s}\x1b[0m`;
-
-	constructor(
-		questions: ExtractedQuestion[],
-		tui: TUI,
-		onDone: (result: string | null) => void,
-	) {
-		this.questions = questions;
-		this.answers = questions.map(() => "");
-		this.tui = tui;
-		this.onDone = onDone;
-
-		// Create a minimal theme for the editor
-		const editorTheme: EditorTheme = {
-			borderColor: this.dim,
-			selectList: {
-				selectedBg: (s: string) => `\x1b[44m${s}\x1b[0m`,
-				matchHighlight: this.cyan,
-				itemSecondary: this.gray,
-			},
-		};
-
-		this.editor = new Editor(tui, editorTheme);
-		// Disable the editor's built-in submit (which clears the editor)
-		// We'll handle Enter ourselves to preserve the text
-		this.editor.disableSubmit = true;
-		this.editor.onChange = () => {
-			this.invalidate();
-			this.tui.requestRender();
-		};
-	}
-
-	private allQuestionsAnswered(): boolean {
-		this.saveCurrentAnswer();
-		return this.answers.every((a) => (a?.trim() || "").length > 0);
-	}
-
-	private saveCurrentAnswer(): void {
-		this.answers[this.currentIndex] = this.editor.getText();
-	}
-
-	private navigateTo(index: number): void {
-		if (index < 0 || index >= this.questions.length) return;
-		this.saveCurrentAnswer();
-		this.currentIndex = index;
-		this.editor.setText(this.answers[index] || "");
-		this.invalidate();
-	}
-
-	private submit(): void {
-		this.saveCurrentAnswer();
-
-		// Build the response text
-		const parts: string[] = [];
-		for (let i = 0; i < this.questions.length; i++) {
-			const q = this.questions[i];
-			const a = this.answers[i]?.trim() || "(no answer)";
-			parts.push(`Q: ${q.question}`);
-			if (q.context) {
-				parts.push(`> ${q.context}`);
-			}
-			parts.push(`A: ${a}`);
-			parts.push("");
-		}
-
-		this.onDone(parts.join("\n").trim());
-	}
-
-	private cancel(): void {
-		this.onDone(null);
-	}
-
-	invalidate(): void {
-		this.cachedWidth = undefined;
-		this.cachedLines = undefined;
-	}
-
-	handleInput(data: string): void {
-		// Handle confirmation dialog
-		if (this.showingConfirmation) {
-			if (matchesKey(data, Key.enter) || data.toLowerCase() === "y") {
-				this.submit();
-				return;
-			}
-			if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c")) || data.toLowerCase() === "n") {
-				this.showingConfirmation = false;
-				this.invalidate();
-				this.tui.requestRender();
-				return;
-			}
-			return;
-		}
-
-		// Global navigation and commands
-		if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
-			this.cancel();
-			return;
-		}
-
-		// Tab / Shift+Tab for navigation
-		if (matchesKey(data, Key.tab)) {
-			if (this.currentIndex < this.questions.length - 1) {
-				this.navigateTo(this.currentIndex + 1);
-				this.tui.requestRender();
-			}
-			return;
-		}
-		if (matchesKey(data, Key.shift("tab"))) {
-			if (this.currentIndex > 0) {
-				this.navigateTo(this.currentIndex - 1);
-				this.tui.requestRender();
-			}
-			return;
-		}
-
-		// Arrow up/down for question navigation when editor is empty
-		// (Editor handles its own cursor navigation when there's content)
-		if (matchesKey(data, Key.up) && this.editor.getText() === "") {
-			if (this.currentIndex > 0) {
-				this.navigateTo(this.currentIndex - 1);
-				this.tui.requestRender();
-				return;
-			}
-		}
-		if (matchesKey(data, Key.down) && this.editor.getText() === "") {
-			if (this.currentIndex < this.questions.length - 1) {
-				this.navigateTo(this.currentIndex + 1);
-				this.tui.requestRender();
-				return;
-			}
-		}
-
-		// Handle Enter ourselves (editor's submit is disabled)
-		// Plain Enter moves to next question or shows confirmation on last question
-		// Shift+Enter adds a newline (handled by editor)
-		if (matchesKey(data, Key.enter) && !matchesKey(data, Key.shift("enter"))) {
-			this.saveCurrentAnswer();
-			if (this.currentIndex < this.questions.length - 1) {
-				this.navigateTo(this.currentIndex + 1);
-			} else {
-				// On last question - show confirmation
-				this.showingConfirmation = true;
-			}
-			this.invalidate();
-			this.tui.requestRender();
-			return;
-		}
-
-		// Pass to editor
-		this.editor.handleInput(data);
-		this.invalidate();
-		this.tui.requestRender();
-	}
-
-	render(width: number): string[] {
-		if (this.cachedLines && this.cachedWidth === width) {
-			return this.cachedLines;
-		}
-
-		const lines: string[] = [];
-		const boxWidth = Math.min(width - 4, 120); // Allow wider box
-		const contentWidth = boxWidth - 4; // 2 chars padding on each side
-
-		// Helper to create horizontal lines (dim the whole thing at once)
-		const horizontalLine = (count: number) => "─".repeat(count);
-
-		// Helper to create a box line
-		const boxLine = (content: string, leftPad: number = 2): string => {
-			const paddedContent = " ".repeat(leftPad) + content;
-			const contentLen = visibleWidth(paddedContent);
-			const rightPad = Math.max(0, boxWidth - contentLen - 2);
-			return this.dim("│") + paddedContent + " ".repeat(rightPad) + this.dim("│");
-		};
-
-		const emptyBoxLine = (): string => {
-			return this.dim("│") + " ".repeat(boxWidth - 2) + this.dim("│");
-		};
-
-		const padToWidth = (line: string): string => {
-			const len = visibleWidth(line);
-			return line + " ".repeat(Math.max(0, width - len));
-		};
-
-		// Title
-		lines.push(padToWidth(this.dim("╭" + horizontalLine(boxWidth - 2) + "╮")));
-		const title = `${this.bold(this.cyan("Questions"))} ${this.dim(`(${this.currentIndex + 1}/${this.questions.length})`)}`;
-		lines.push(padToWidth(boxLine(title)));
-		lines.push(padToWidth(this.dim("├" + horizontalLine(boxWidth - 2) + "┤")));
-
-		// Progress indicator
-		const progressParts: string[] = [];
-		for (let i = 0; i < this.questions.length; i++) {
-			const answered = (this.answers[i]?.trim() || "").length > 0;
-			const current = i === this.currentIndex;
-			if (current) {
-				progressParts.push(this.cyan("●"));
-			} else if (answered) {
-				progressParts.push(this.green("●"));
-			} else {
-				progressParts.push(this.dim("○"));
-			}
-		}
-		lines.push(padToWidth(boxLine(progressParts.join(" "))));
-		lines.push(padToWidth(emptyBoxLine()));
-
-		// Current question
-		const q = this.questions[this.currentIndex];
-		const questionText = `${this.bold("Q:")} ${q.question}`;
-		const wrappedQuestion = wrapTextWithAnsi(questionText, contentWidth);
-		for (const line of wrappedQuestion) {
-			lines.push(padToWidth(boxLine(line)));
-		}
-
-		// Context if present
-		if (q.context) {
-			lines.push(padToWidth(emptyBoxLine()));
-			const contextText = this.gray(`> ${q.context}`);
-			const wrappedContext = wrapTextWithAnsi(contextText, contentWidth - 2);
-			for (const line of wrappedContext) {
-				lines.push(padToWidth(boxLine(line)));
-			}
-		}
-
-		lines.push(padToWidth(emptyBoxLine()));
-
-		// Render the editor component (multi-line input) with padding
-		// Skip the first and last lines (editor's own border lines)
-		const answerPrefix = this.bold("A: ");
-		const editorWidth = contentWidth - 4 - 3; // Extra padding + space for "A: "
-		const editorLines = this.editor.render(editorWidth);
-		for (let i = 1; i < editorLines.length - 1; i++) {
-			if (i === 1) {
-				// First content line gets the "A: " prefix
-				lines.push(padToWidth(boxLine(answerPrefix + editorLines[i])));
-			} else {
-				// Subsequent lines get padding to align with the first line
-				lines.push(padToWidth(boxLine("   " + editorLines[i])));
-			}
-		}
-
-		lines.push(padToWidth(emptyBoxLine()));
-
-		// Confirmation dialog or footer with controls
-		if (this.showingConfirmation) {
-			lines.push(padToWidth(this.dim("├" + horizontalLine(boxWidth - 2) + "┤")));
-			const confirmMsg = `${this.yellow("Submit all answers?")} ${this.dim("(Enter/y to confirm, Esc/n to cancel)")}`;
-			lines.push(padToWidth(boxLine(truncateToWidth(confirmMsg, contentWidth))));
-		} else {
-			lines.push(padToWidth(this.dim("├" + horizontalLine(boxWidth - 2) + "┤")));
-			const controls = `${this.dim("Tab/Enter")} next · ${this.dim("Shift+Tab")} prev · ${this.dim("Shift+Enter")} newline · ${this.dim("Esc")} cancel`;
-			lines.push(padToWidth(boxLine(truncateToWidth(controls, contentWidth))));
-		}
-		lines.push(padToWidth(this.dim("╰" + horizontalLine(boxWidth - 2) + "╯")));
-
-		this.cachedWidth = width;
-		this.cachedLines = lines;
-		return lines;
-	}
-}
-
-export default function (pi: ExtensionAPI) {
-	const answerHandler = async (ctx: ExtensionContext) => {
-			if (!ctx.hasUI) {
-				ctx.ui.notify("answer requires interactive mode", "error");
-				return;
-			}
-
-			if (!ctx.model) {
-				ctx.ui.notify("No model selected", "error");
-				return;
-			}
-
-			// Find the last assistant message on the current branch
-			const branch = ctx.sessionManager.getBranch();
-			let lastAssistantText: string | undefined;
-
-			for (let i = branch.length - 1; i >= 0; i--) {
-				const entry = branch[i];
-				if (entry.type === "message") {
-					const msg = entry.message;
-					if ("role" in msg && msg.role === "assistant") {
-						if (msg.stopReason !== "stop") {
-							ctx.ui.notify(`Last assistant message incomplete (${msg.stopReason})`, "error");
-							return;
-						}
-						const textParts = msg.content
-							.filter((c): c is { type: "text"; text: string } => c.type === "text")
-							.map((c) => c.text);
-						if (textParts.length > 0) {
-							lastAssistantText = textParts.join("\n");
-							break;
-						}
-					}
-				}
-			}
-
-			if (!lastAssistantText) {
-				ctx.ui.notify("No assistant messages found", "error");
-				return;
-			}
-
-			// Select the best model for extraction (prefer Codex mini, then haiku)
-			const extractionModel = await selectExtractionModel(ctx.model, ctx.modelRegistry);
-
-			// Run extraction with loader UI
-			const extractionResult = await ctx.ui.custom<ExtractionResult | null>((tui, theme, _kb, done) => {
-				const loader = new BorderedLoader(tui, theme, `Extracting questions using ${extractionModel.id}...`);
-				loader.onAbort = () => done(null);
-
-				const doExtract = async () => {
-					const apiKey = await ctx.modelRegistry.getApiKey(extractionModel);
-					const userMessage: UserMessage = {
-						role: "user",
-						content: [{ type: "text", text: lastAssistantText! }],
-						timestamp: Date.now(),
-					};
-
-					const response = await complete(
-						extractionModel,
-						{ systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
-						{ apiKey, signal: loader.signal },
-					);
-
-					if (response.stopReason === "aborted") {
-						return null;
-					}
-
-					const responseText = response.content
-						.filter((c): c is { type: "text"; text: string } => c.type === "text")
-						.map((c) => c.text)
-						.join("\n");
-
-					return parseExtractionResult(responseText);
-				};
-
-				doExtract()
-					.then(done)
-					.catch(() => done(null));
-
-				return loader;
-			});
-
-			if (extractionResult === null) {
-				ctx.ui.notify("Cancelled", "info");
-				return;
-			}
-
-			if (extractionResult.questions.length === 0) {
-				ctx.ui.notify("No questions found in the last message", "info");
-				return;
-			}
-
-			// Show the Q&A component
-			const answersResult = await ctx.ui.custom<string | null>((tui, _theme, _kb, done) => {
-				return new QnAComponent(extractionResult.questions, tui, done);
-			});
-
-			if (answersResult === null) {
-				ctx.ui.notify("Cancelled", "info");
-				return;
-			}
-
-			// Send the answers directly as a message and trigger a turn
-			pi.sendMessage(
-				{
-					customType: "answers",
-					content: "I answered your questions in the following way:\n\n" + answersResult,
-					display: true,
-				},
-				{ triggerTurn: true },
-			);
-	};
-
-	pi.registerCommand("answer", {
-		description: "Extract questions from last assistant message into interactive Q&A",
-		handler: (_args, ctx) => answerHandler(ctx),
-	});
-
-	pi.registerShortcut("ctrl+.", {
-		description: "Extract and answer questions",
-		handler: answerHandler,
-	});
-}
dots/pi/agent/extensions/file-picker.ts
@@ -1,1059 +0,0 @@
-/**
- * File Picker Extension
- *
- * Replaces the built-in @ file picker with an enhanced file browser.
- * Selected files are attached to the prompt as context.
- *
- * Features:
- * - @ shortcut opens file browser (replaces built-in)
- * - Directory navigation with Enter
- * - Space to toggle selection
- * - Tab to toggle options panel (gitignore, hidden files)
- * - Fuzzy search and glob patterns
- * - Git-aware file listing (respects .gitignore)
- * - Selected files injected as context on prompt submit
- *
- * Based on codemap extension by @kcosr
- */
-
-import {
-	CustomEditor,
-	type ExtensionAPI,
-	type ExtensionContext,
-} from "@mariozechner/pi-coding-agent";
-import { matchesKey, visibleWidth, type EditorTheme, type TUI } from "@mariozechner/pi-tui";
-import * as fs from "node:fs";
-import * as path from "node:path";
-import * as os from "node:os";
-import { execSync } from "node:child_process";
-
-// ═══════════════════════════════════════════════════════════════════════════
-// Types
-// ═══════════════════════════════════════════════════════════════════════════
-
-interface FileEntry {
-	name: string;
-	isDirectory: boolean;
-	relativePath: string;
-}
-
-interface PickerState {
-	respectGitignore: boolean;
-	skipHidden: boolean;
-}
-
-interface PickerConfig {
-	respectGitignore?: boolean;
-	skipHidden?: boolean;
-	skipPatterns?: string[];
-}
-
-interface BrowserOption {
-	id: string;
-	label: string;
-	enabled: boolean;
-	visible: () => boolean;
-}
-
-interface SelectedPath {
-	path: string;
-	isDirectory: boolean;
-}
-
-type FileBrowserAction = 
-	| { action: "confirm"; paths: SelectedPath[] }
-	| { action: "cancel" }
-	| { action: "select"; selected: SelectedPath; paths: SelectedPath[] };
-
-// ═══════════════════════════════════════════════════════════════════════════
-// Config
-// ═══════════════════════════════════════════════════════════════════════════
-
-const DEFAULT_CONFIG: PickerConfig = {
-	respectGitignore: true,
-	skipHidden: true,
-	skipPatterns: ["node_modules"],
-};
-
-function loadConfig(): PickerConfig {
-	const configPath = path.join(
-		os.homedir(),
-		".pi",
-		"agent",
-		"extensions",
-		"file-picker",
-		"config.json"
-	);
-	try {
-		if (fs.existsSync(configPath)) {
-			const content = fs.readFileSync(configPath, "utf-8");
-			const custom = JSON.parse(content) as Partial<PickerConfig>;
-			return { ...DEFAULT_CONFIG, ...custom };
-		}
-	} catch {
-		// Ignore errors, use default
-	}
-	return DEFAULT_CONFIG;
-}
-
-const config = loadConfig();
-const skipPatterns = config.skipPatterns ?? ["node_modules"];
-
-// ═══════════════════════════════════════════════════════════════════════════
-// State (per-session, reset on session switch)
-// ═══════════════════════════════════════════════════════════════════════════
-
-const state: PickerState = {
-	respectGitignore: config.respectGitignore ?? true,
-	skipHidden: config.skipHidden ?? true,
-};
-
-// ═══════════════════════════════════════════════════════════════════════════
-// Theming
-// ═══════════════════════════════════════════════════════════════════════════
-
-interface PaletteTheme {
-	border: string;
-	title: string;
-	selected: string;
-	selectedText: string;
-	directory: string;
-	checked: string;
-	searchIcon: string;
-	placeholder: string;
-	hint: string;
-}
-
-const DEFAULT_THEME: PaletteTheme = {
-	border: "2",
-	title: "2",
-	selected: "36",
-	selectedText: "36",
-	directory: "34",
-	checked: "32",
-	searchIcon: "2",
-	placeholder: "2;3",
-	hint: "2",
-};
-
-function loadTheme(): PaletteTheme {
-	const themePath = path.join(
-		os.homedir(),
-		".pi",
-		"agent",
-		"extensions",
-		"file-picker",
-		"theme.json"
-	);
-	try {
-		if (fs.existsSync(themePath)) {
-			const content = fs.readFileSync(themePath, "utf-8");
-			const custom = JSON.parse(content) as Partial<PaletteTheme>;
-			return { ...DEFAULT_THEME, ...custom };
-		}
-	} catch {
-		// Ignore errors
-	}
-	return DEFAULT_THEME;
-}
-
-function fg(code: string, text: string): string {
-	if (!code) return text;
-	return `\x1b[${code}m${text}\x1b[0m`;
-}
-
-const paletteTheme = loadTheme();
-
-// ═══════════════════════════════════════════════════════════════════════════
-// File System Utilities
-// ═══════════════════════════════════════════════════════════════════════════
-
-function getCwdRoot(): string {
-	return process.cwd();
-}
-
-function isWithinCwd(targetPath: string, cwdRoot: string): boolean {
-	const resolved = path.resolve(targetPath);
-	const normalizedCwd = path.resolve(cwdRoot);
-	return (
-		resolved === normalizedCwd ||
-		resolved.startsWith(normalizedCwd + path.sep)
-	);
-}
-
-function shouldSkipPattern(name: string): boolean {
-	return skipPatterns.some((pattern) => {
-		if (pattern.includes("*")) {
-			const regex = globToRegex(pattern);
-			return regex.test(name);
-		}
-		return name === pattern;
-	});
-}
-
-function listDirectoryWithGit(
-	dirPath: string,
-	cwdRoot: string,
-	gitFiles: Set<string> | null,
-	skipHidden: boolean
-): FileEntry[] {
-	const entries: FileEntry[] = [];
-
-	try {
-		const items = fs.readdirSync(dirPath, { withFileTypes: true });
-		const relDir = path.relative(cwdRoot, dirPath);
-
-		for (const item of items) {
-			if (skipHidden && item.name.startsWith(".")) continue;
-			if (shouldSkipPattern(item.name)) continue;
-
-			const fullPath = path.join(dirPath, item.name);
-			const relativePath = relDir ? path.join(relDir, item.name) : item.name;
-
-			let isDirectory = item.isDirectory();
-			if (item.isSymbolicLink()) {
-				try {
-					const stats = fs.statSync(fullPath);
-					isDirectory = stats.isDirectory();
-				} catch {
-					continue;
-				}
-			}
-
-			if (gitFiles !== null) {
-				if (isDirectory) {
-					let hasGitFiles = false;
-					const prefix = relativePath + "/";
-					for (const gitFile of gitFiles) {
-						if (gitFile.startsWith(prefix) || gitFile === relativePath) {
-							hasGitFiles = true;
-							break;
-						}
-					}
-					if (!hasGitFiles) continue;
-				} else {
-					if (!gitFiles.has(relativePath)) continue;
-				}
-			}
-
-			entries.push({
-				name: item.name,
-				isDirectory,
-				relativePath,
-			});
-		}
-
-		entries.sort((a, b) => {
-			if (a.isDirectory && !b.isDirectory) return -1;
-			if (!a.isDirectory && b.isDirectory) return 1;
-			return a.name.localeCompare(b.name);
-		});
-	} catch {
-		// Return empty on error
-	}
-
-	return entries;
-}
-
-function listAllFiles(
-	dirPath: string,
-	cwdRoot: string,
-	results: FileEntry[],
-	skipHidden: boolean
-): FileEntry[] {
-	try {
-		const items = fs.readdirSync(dirPath, { withFileTypes: true });
-
-		for (const item of items) {
-			if (skipHidden && item.name.startsWith(".")) continue;
-			if (shouldSkipPattern(item.name)) continue;
-
-			const fullPath = path.join(dirPath, item.name);
-			const relativePath = path.relative(cwdRoot, fullPath);
-
-			let isDirectory = item.isDirectory();
-			if (item.isSymbolicLink()) {
-				try {
-					const stats = fs.statSync(fullPath);
-					isDirectory = stats.isDirectory();
-				} catch {
-					continue;
-				}
-			}
-
-			results.push({
-				name: item.name,
-				isDirectory,
-				relativePath,
-			});
-
-			if (isDirectory) {
-				listAllFiles(fullPath, cwdRoot, results, skipHidden);
-			}
-		}
-	} catch {
-		// Skip inaccessible directories
-	}
-
-	return results;
-}
-
-function isGitRepo(cwdRoot: string): boolean {
-	try {
-		execSync("git rev-parse --is-inside-work-tree", {
-			cwd: cwdRoot,
-			encoding: "utf-8",
-			stdio: "pipe",
-		});
-		return true;
-	} catch {
-		return false;
-	}
-}
-
-function listGitFiles(cwdRoot: string): FileEntry[] {
-	const entries: FileEntry[] = [];
-
-	try {
-		const output = execSync(
-			"git ls-files --cached --others --exclude-standard",
-			{
-				cwd: cwdRoot,
-				encoding: "utf-8",
-				stdio: "pipe",
-				maxBuffer: 10 * 1024 * 1024,
-			}
-		);
-
-		const files = output
-			.trim()
-			.split("\n")
-			.filter((f) => f);
-
-		for (const relativePath of files) {
-			const fullPath = path.join(cwdRoot, relativePath);
-			const name = path.basename(relativePath);
-
-			let isDirectory = false;
-			try {
-				const stats = fs.statSync(fullPath);
-				isDirectory = stats.isDirectory();
-			} catch {
-				continue;
-			}
-
-			entries.push({
-				name,
-				isDirectory,
-				relativePath,
-			});
-		}
-
-		const dirs = new Set<string>();
-		for (const entry of entries) {
-			let dir = path.dirname(entry.relativePath);
-			while (dir && dir !== ".") {
-				dirs.add(dir);
-				dir = path.dirname(dir);
-			}
-		}
-
-		for (const dir of dirs) {
-			entries.push({
-				name: path.basename(dir),
-				isDirectory: true,
-				relativePath: dir,
-			});
-		}
-	} catch {
-		// Fall back to empty
-	}
-
-	return entries;
-}
-
-// ═══════════════════════════════════════════════════════════════════════════
-// Search Utilities
-// ═══════════════════════════════════════════════════════════════════════════
-
-function isGlobPattern(query: string): boolean {
-	return /[*?[\]]/.test(query);
-}
-
-function globToRegex(pattern: string): RegExp {
-	let regex = "";
-	let i = 0;
-
-	while (i < pattern.length) {
-		const char = pattern[i];
-
-		if (char === "*") {
-			if (pattern[i + 1] === "*") {
-				regex += ".*";
-				i += 2;
-				if (pattern[i] === "/") i++;
-			} else {
-				regex += "[^/]*";
-				i++;
-			}
-		} else if (char === "?") {
-			regex += "[^/]";
-			i++;
-		} else if (char === "[") {
-			const end = pattern.indexOf("]", i);
-			if (end !== -1) {
-				regex += pattern.slice(i, end + 1);
-				i = end + 1;
-			} else {
-				regex += "\\[";
-				i++;
-			}
-		} else if (".+^${}()|\\".includes(char)) {
-			regex += "\\" + char;
-			i++;
-		} else {
-			regex += char;
-			i++;
-		}
-	}
-
-	return new RegExp("^" + regex + "$", "i");
-}
-
-function fuzzyScore(query: string, text: string): number {
-	const lowerQuery = query.toLowerCase();
-	const lowerText = text.toLowerCase();
-
-	if (lowerText.includes(lowerQuery)) {
-		return 100 + (lowerQuery.length / lowerText.length) * 50;
-	}
-
-	let score = 0;
-	let queryIndex = 0;
-	let consecutiveBonus = 0;
-
-	for (let i = 0; i < lowerText.length && queryIndex < lowerQuery.length; i++) {
-		if (lowerText[i] === lowerQuery[queryIndex]) {
-			score += 10 + consecutiveBonus;
-			consecutiveBonus += 5;
-			queryIndex++;
-		} else {
-			consecutiveBonus = 0;
-		}
-	}
-
-	return queryIndex === lowerQuery.length ? score : 0;
-}
-
-function filterEntries(entries: FileEntry[], query: string): FileEntry[] {
-	if (!query.trim()) return entries;
-
-	if (isGlobPattern(query)) {
-		const regex = globToRegex(query);
-		const filtered = entries.filter(
-			(entry) => regex.test(entry.name) || regex.test(entry.relativePath)
-		);
-		// Sort: files first, then directories
-		return filtered.sort((a, b) => {
-			if (a.isDirectory && !b.isDirectory) return 1;
-			if (!a.isDirectory && b.isDirectory) return -1;
-			return a.relativePath.localeCompare(b.relativePath);
-		});
-	}
-
-	const scored = entries
-		.map((entry) => ({
-			entry,
-			score: Math.max(
-				fuzzyScore(query, entry.name),
-				fuzzyScore(query, entry.relativePath) * 0.9
-			),
-		}))
-		.filter((item) => item.score > 0)
-		.sort((a, b) => {
-			// Primary: files before directories
-			if (a.entry.isDirectory && !b.entry.isDirectory) return 1;
-			if (!a.entry.isDirectory && b.entry.isDirectory) return -1;
-			// Secondary: by score
-			return b.score - a.score;
-		});
-
-	return scored.map((item) => item.entry);
-}
-
-// ═══════════════════════════════════════════════════════════════════════════
-// File Browser Component
-// ═══════════════════════════════════════════════════════════════════════════
-
-class FileBrowserComponent {
-	readonly width = 100;
-	private readonly maxVisible = 10;
-	private cwdRoot: string;
-	private currentDir: string;
-	private allEntries: FileEntry[];
-	private allFilesRecursive: FileEntry[];
-	private filtered: FileEntry[];
-	private selected = 0;
-	private query = "";
-	private isSearchMode = false;
-	private selectedPaths: Map<string, boolean>; // path -> isDirectory
-	private rootParentView = false;
-	private inGitRepo: boolean;
-	private gitFiles: Set<string> | null = null;
-	private focusOnOptions = false;
-	private selectedOption = 0;
-	private options: BrowserOption[];
-	private done: (action: FileBrowserAction) => void;
-
-	constructor(done: (action: FileBrowserAction) => void) {
-		this.done = done;
-		this.cwdRoot = getCwdRoot();
-		this.currentDir = this.cwdRoot;
-		this.selectedPaths = new Map();
-		this.inGitRepo = isGitRepo(this.cwdRoot);
-
-		this.options = [
-			{
-				id: "gitignore",
-				label: "Respect .gitignore",
-				enabled: state.respectGitignore,
-				visible: () => this.inGitRepo,
-			},
-			{
-				id: "skipHidden",
-				label: "Skip hidden files",
-				enabled: state.skipHidden,
-				visible: () => true,
-			},
-		];
-
-		this.rebuildFileLists();
-	}
-
-	private getOption(id: string): BrowserOption | undefined {
-		return this.options.find((o) => o.id === id);
-	}
-
-	private getVisibleOptions(): BrowserOption[] {
-		return this.options.filter((o) => o.visible());
-	}
-
-	private rebuildFileLists(): void {
-		const respectGitignore = this.getOption("gitignore")?.enabled ?? false;
-		const skipHidden = this.getOption("skipHidden")?.enabled ?? true;
-
-		if (this.inGitRepo && respectGitignore) {
-			const gitEntries = listGitFiles(this.cwdRoot);
-			this.gitFiles = new Set(gitEntries.map((e) => e.relativePath));
-			this.allFilesRecursive = gitEntries;
-		} else {
-			this.gitFiles = null;
-			this.allFilesRecursive = listAllFiles(this.cwdRoot, this.cwdRoot, [], skipHidden);
-		}
-
-		this.allEntries = this.listCurrentDirectory();
-		this.updateFilter();
-	}
-
-	private listCurrentDirectory(): FileEntry[] {
-		if (this.rootParentView) {
-			return [{
-				name: path.basename(this.cwdRoot),
-				isDirectory: true,
-				relativePath: ".",
-			}];
-		}
-
-		const skipHidden = this.getOption("skipHidden")?.enabled ?? true;
-		const entries = listDirectoryWithGit(
-			this.currentDir,
-			this.cwdRoot,
-			this.gitFiles,
-			skipHidden
-		);
-
-		entries.unshift({
-			name: "..",
-			isDirectory: true,
-			relativePath: "..",
-		});
-
-		return entries;
-	}
-
-	private isUpEntry(entry: FileEntry): boolean {
-		return entry.name === ".." && entry.relativePath === "..";
-	}
-
-	private navigateTo(dir: string): void {
-		if (!isWithinCwd(dir, this.cwdRoot)) return;
-
-		this.rootParentView = false;
-		this.currentDir = dir;
-		this.allEntries = this.listCurrentDirectory();
-		this.query = "";
-		this.isSearchMode = false;
-		this.filtered = this.allEntries;
-		this.selected = 0;
-	}
-
-	private goUp(): boolean {
-		if (this.rootParentView) return false;
-
-		if (this.currentDir === this.cwdRoot) {
-			this.rootParentView = true;
-			this.allEntries = this.listCurrentDirectory();
-			this.query = "";
-			this.isSearchMode = false;
-			this.filtered = this.allEntries;
-			this.selected = 0;
-			return true;
-		}
-
-		const parentDir = path.dirname(this.currentDir);
-		if (isWithinCwd(parentDir, this.cwdRoot)) {
-			this.navigateTo(parentDir);
-			return true;
-		}
-		return false;
-	}
-
-	handleInput(data: string): void {
-		if (matchesKey(data, "tab")) {
-			const visibleOptions = this.getVisibleOptions();
-			if (visibleOptions.length > 0) {
-				this.focusOnOptions = !this.focusOnOptions;
-				if (this.focusOnOptions) this.selectedOption = 0;
-			}
-			return;
-		}
-
-		if (this.focusOnOptions) {
-			this.handleOptionsInput(data);
-		} else {
-			this.handleBrowserInput(data);
-		}
-	}
-
-	private handleOptionsInput(data: string): void {
-		const visibleOptions = this.getVisibleOptions();
-		const currentOption = visibleOptions[this.selectedOption];
-
-		if (matchesKey(data, "escape")) {
-			this.focusOnOptions = false;
-			return;
-		}
-
-		if (matchesKey(data, "up")) {
-			if (visibleOptions.length > 0) {
-				this.selectedOption = this.selectedOption === 0
-					? visibleOptions.length - 1
-					: this.selectedOption - 1;
-			}
-			return;
-		}
-
-		if (matchesKey(data, "down")) {
-			if (visibleOptions.length > 0) {
-				this.selectedOption = this.selectedOption === visibleOptions.length - 1
-					? 0
-					: this.selectedOption + 1;
-			}
-			return;
-		}
-
-		if (data === " " || matchesKey(data, "return")) {
-			if (currentOption) {
-				currentOption.enabled = !currentOption.enabled;
-				// Sync to global state
-				if (currentOption.id === "gitignore") {
-					state.respectGitignore = currentOption.enabled;
-				} else if (currentOption.id === "skipHidden") {
-					state.skipHidden = currentOption.enabled;
-				}
-				this.rebuildFileLists();
-			}
-		}
-	}
-
-	private getSelectedPathsArray(): SelectedPath[] {
-		return Array.from(this.selectedPaths.entries()).map(([p, isDir]) => ({ path: p, isDirectory: isDir }));
-	}
-
-	private handleBrowserInput(data: string): void {
-		if (matchesKey(data, "escape")) {
-			if (!this.goUp()) {
-				this.done({ action: "confirm", paths: this.getSelectedPathsArray() });
-			}
-			return;
-		}
-
-		// Enter = select and insert (files or directories)
-		if (matchesKey(data, "return")) {
-			const entry = this.filtered[this.selected];
-			if (entry) {
-				if (entry.name === "..") {
-					this.goUp();
-				} else {
-					// Select and close (works for both files and directories)
-					const paths = this.getSelectedPathsArray();
-					const selected = { path: entry.relativePath, isDirectory: entry.isDirectory };
-					if (!this.selectedPaths.has(entry.relativePath)) {
-						paths.push(selected);
-					}
-					this.done({ action: "select", selected, paths });
-				}
-			}
-			return;
-		}
-
-		// Space or Right arrow = navigate directories, toggle files
-		if (data === " " || matchesKey(data, "right")) {
-			const entry = this.filtered[this.selected];
-			if (entry && !this.isUpEntry(entry)) {
-				if (entry.isDirectory) {
-					// Navigate into directory
-					this.navigateTo(path.join(this.cwdRoot, entry.relativePath));
-				} else {
-					// Toggle selection for multi-select (files only)
-					if (this.selectedPaths.has(entry.relativePath)) {
-						this.selectedPaths.delete(entry.relativePath);
-					} else {
-						this.selectedPaths.set(entry.relativePath, entry.isDirectory);
-					}
-				}
-			}
-			return;
-		}
-
-		if (matchesKey(data, "up")) {
-			if (this.filtered.length > 0) {
-				this.selected = this.selected === 0 ? this.filtered.length - 1 : this.selected - 1;
-			}
-			return;
-		}
-
-		if (matchesKey(data, "down")) {
-			if (this.filtered.length > 0) {
-				this.selected = this.selected === this.filtered.length - 1 ? 0 : this.selected + 1;
-			}
-			return;
-		}
-
-		// Left arrow = go up directory
-		if (matchesKey(data, "left")) {
-			this.goUp();
-			return;
-		}
-
-		if (matchesKey(data, "backspace")) {
-			if (this.query.length > 0) {
-				this.query = this.query.slice(0, -1);
-				this.updateFilter();
-			} else {
-				this.goUp();
-			}
-			return;
-		}
-
-		if (data.length === 1 && data.charCodeAt(0) >= 32) {
-			this.query += data;
-			this.updateFilter();
-		}
-	}
-
-	private updateFilter(): void {
-		if (this.query.trim()) {
-			this.isSearchMode = true;
-			this.filtered = filterEntries(this.allFilesRecursive, this.query);
-		} else {
-			this.isSearchMode = false;
-			this.filtered = this.allEntries;
-		}
-		this.selected = 0;
-	}
-
-	render(_width: number): string[] {
-		const w = this.width;
-		const innerW = w - 2;
-		const lines: string[] = [];
-
-		const t = paletteTheme;
-		const border = (s: string) => fg(t.border, s);
-		const title = (s: string) => fg(t.title, s);
-		const selected = (s: string) => fg(t.selected, s);
-		const selectedText = (s: string) => fg(t.selectedText, s);
-		const directory = (s: string) => fg(t.directory, s);
-		const checked = (s: string) => fg(t.checked, s);
-		const hint = (s: string) => fg(t.hint, s);
-		const bold = (s: string) => `\x1b[1m${s}\x1b[22m`;
-
-		const pad = (s: string, len: number) => s + " ".repeat(Math.max(0, len - visibleWidth(s)));
-
-		const truncate = (s: string, maxW: number) => {
-			if (visibleWidth(s) <= maxW) return s;
-			let result = "";
-			let width = 0;
-			for (const char of s) {
-				const charWidth = visibleWidth(char);
-				if (width + charWidth > maxW - 1) break;
-				result += char;
-				width += charWidth;
-			}
-			return result + "…";
-		};
-
-		const row = (content: string) => border("│") + pad(content, innerW) + border("│");
-
-		// Top border with title
-		let titleText: string;
-		if (this.isSearchMode) {
-			titleText = " Search ";
-		} else if (this.rootParentView) {
-			titleText = " Files ";
-		} else {
-			const relDir = path.relative(this.cwdRoot, this.currentDir);
-			titleText = relDir ? ` ${truncate(relDir, 40)} ` : " Files ";
-		}
-		const borderLen = Math.max(0, innerW - visibleWidth(titleText));
-		const leftBorder = Math.floor(borderLen / 2);
-		const rightBorder = borderLen - leftBorder;
-		lines.push(
-			border("╭" + "─".repeat(leftBorder)) +
-				title(titleText) +
-				border("─".repeat(rightBorder) + "╮")
-		);
-
-		// Search input
-		const searchPrompt = selected("❯ ");
-		const queryDisplay = this.query || hint("Search files...");
-		const modeIndicator = this.query && isGlobPattern(this.query) ? hint(" [glob]") : "";
-		lines.push(row(` ${searchPrompt}${queryDisplay}${modeIndicator}`));
-
-		// Options row
-		const visibleOptions = this.getVisibleOptions();
-		if (visibleOptions.length > 0) {
-			const optParts: string[] = [];
-			for (let i = 0; i < visibleOptions.length; i++) {
-				const opt = visibleOptions[i];
-				const isSelectedOpt = this.focusOnOptions && i === this.selectedOption;
-				const checkbox = opt.enabled ? checked("☑") : hint("☐");
-				const label = isSelectedOpt
-					? selected(opt.label)
-					: opt.enabled
-						? opt.label
-						: hint(opt.label);
-				const prefix = isSelectedOpt ? selected("▸") : " ";
-				optParts.push(`${prefix}${checkbox} ${label}`);
-			}
-			const optionsStr = optParts.join(" ");
-			const tabHint = this.focusOnOptions ? hint(" (space toggle, esc exit)") : hint(" (tab)");
-			lines.push(row(` ${optionsStr}${tabHint}`));
-		} else {
-			lines.push(row(""));
-		}
-
-		// Divider
-		lines.push(border(`├${"─".repeat(innerW)}┤`));
-
-		// File list - always render exactly maxVisible rows
-		const startIndex = Math.max(
-			0,
-			Math.min(this.selected - Math.floor(this.maxVisible / 2), this.filtered.length - this.maxVisible)
-		);
-
-		for (let i = 0; i < this.maxVisible; i++) {
-			const actualIndex = startIndex + i;
-			if (actualIndex < this.filtered.length) {
-				const entry = this.filtered[actualIndex];
-				const isSelectedEntry = actualIndex === this.selected;
-				const isUpDir = this.isUpEntry(entry);
-				const isChecked = !isUpDir && this.selectedPaths.has(entry.relativePath);
-
-				const prefix = isSelectedEntry ? selected(" ▶ ") : "   ";
-
-				let displayName: string;
-				if (isUpDir) {
-					displayName = "..";
-				} else if (this.isSearchMode) {
-					displayName = entry.relativePath + (entry.isDirectory ? "/" : "");
-				} else {
-					displayName = entry.name + (entry.isDirectory ? "/" : "");
-				}
-
-				const maxNameLen = innerW - 8;
-				const truncatedName = truncate(displayName, maxNameLen);
-
-				let nameStr: string;
-				if (isUpDir) {
-					nameStr = isSelectedEntry ? bold(selectedText(truncatedName)) : hint(truncatedName);
-				} else if (entry.isDirectory) {
-					nameStr = isSelectedEntry ? bold(selectedText(truncatedName)) : directory(truncatedName);
-				} else {
-					nameStr = isSelectedEntry ? bold(selectedText(truncatedName)) : truncatedName;
-				}
-
-				if (isUpDir) {
-					lines.push(row(`${prefix}   ${nameStr}`));
-				} else {
-					const checkMark = isChecked ? checked("☑ ") : hint("☐ ");
-					lines.push(row(`${prefix}${checkMark}${nameStr}`));
-				}
-			} else if (i === 0 && this.filtered.length === 0) {
-				lines.push(row(hint("   No matching files")));
-			} else {
-				lines.push(row(""));
-			}
-		}
-
-		// Scroll/count indicator row
-		if (this.filtered.length > this.maxVisible) {
-			const shown = `${startIndex + 1}-${Math.min(startIndex + this.maxVisible, this.filtered.length)}`;
-			lines.push(row(hint(` (${shown} of ${this.filtered.length})`)));
-		} else if (this.filtered.length > 0) {
-			lines.push(row(hint(` (${this.filtered.length} file${this.filtered.length === 1 ? "" : "s"})`)));
-		} else {
-			lines.push(row(""));
-		}
-
-		// Selection summary section
-		lines.push(border(`├${"─".repeat(innerW)}┤`));
-		if (this.selectedPaths.size > 0) {
-			const selectedList = Array.from(this.selectedPaths.keys()).slice(0, 3);
-			const preview = selectedList.join(", ") + (this.selectedPaths.size > 3 ? ", ..." : "");
-			lines.push(row(` ${checked(`Selected (${this.selectedPaths.size}):`)} ${truncate(preview, innerW - 18)}`));
-		} else {
-			lines.push(row(hint(" No files selected")));
-		}
-
-		// Footer
-		lines.push(border(`├${"─".repeat(innerW)}┤`));
-		lines.push(row(hint(" ↑↓ navigate  ←→ dirs  space toggle  enter select  esc done")));
-
-		// Bottom border
-		lines.push(border(`╰${"─".repeat(innerW)}╯`));
-
-		return lines;
-	}
-
-	invalidate(): void {}
-	dispose(): void {}
-}
-
-// ═══════════════════════════════════════════════════════════════════════════
-// Shared File Picker Logic
-// ═══════════════════════════════════════════════════════════════════════════
-
-async function openFilePicker(ui: ExtensionContext["ui"]): Promise<void> {
-	const result = await ui.custom<FileBrowserAction>(
-		(_tui, _theme, _kb, done) => new FileBrowserComponent(done),
-		{ overlay: true }
-	);
-
-	const paths = result.action === "cancel" ? [] : result.paths;
-	if (paths.length > 0) {
-		// Add trailing / for directories to make it clear
-		const refs = paths.map(p => `@${p.path}${p.isDirectory ? "/" : ""}`).join(" ");
-		const currentText = ui.getEditorText();
-		ui.setEditorText(currentText + refs + " ");
-		ui.notify(`Added ${paths.length} file${paths.length > 1 ? "s" : ""}`, "info");
-	}
-}
-
-// ═══════════════════════════════════════════════════════════════════════════
-// Custom Editor (intercepts @)
-// ═══════════════════════════════════════════════════════════════════════════
-
-class FilePickerEditor extends CustomEditor {
-	private opening = false;
-	private originalProvider: any = null;
-
-	constructor(
-		private tui: TUI,
-		theme: EditorTheme,
-		keybindings: any,
-		private ui: ExtensionContext["ui"]
-	) {
-		super(tui, theme, keybindings);
-	}
-
-	// Wrap the autocomplete provider to filter out @ completions
-	setAutocompleteProvider(provider: any): void {
-		this.originalProvider = provider;
-		
-		const wrappedProvider = {
-			getSuggestions: (lines: string[], cursorLine: number, cursorCol: number) => {
-				const line = lines[cursorLine] ?? "";
-				const beforeCursor = line.slice(0, cursorCol);
-				
-				// If in @ context, return null (no suggestions) - we handle @ ourselves
-				if (beforeCursor.match(/(?:^|[\s])@[^\s]*$/)) {
-					return null;
-				}
-				
-				return provider.getSuggestions(lines, cursorLine, cursorCol);
-			},
-			applyCompletion: (...args: any[]) => provider.applyCompletion(...args),
-		};
-		
-		super.setAutocompleteProvider(wrappedProvider);
-	}
-
-	handleInput(data: string): void {
-		if (this.opening) return;
-
-		// Intercept @ to open our file picker instead of native
-		if (data === "@" && this.shouldTriggerFilePicker()) {
-			this.opening = true;
-			if (this.isShowingAutocomplete()) {
-				super.handleInput("\x1b");
-			}
-			openFilePicker(this.ui).finally(() => { this.opening = false; });
-			return;
-		}
-
-		super.handleInput(data);
-	}
-
-	private shouldTriggerFilePicker(): boolean {
-		const cursor = this.getCursor();
-		const line = this.getLines()[cursor.line] ?? "";
-
-		if (cursor.col === 0) return true;
-
-		const before = line[cursor.col - 1];
-		return before === " " || before === "\t" || before === undefined;
-	}
-}
-
-// ═══════════════════════════════════════════════════════════════════════════
-// Extension Export
-// ═══════════════════════════════════════════════════════════════════════════
-
-export default function (pi: ExtensionAPI) {
-	const attachEditor = (ctx: ExtensionContext) => {
-		if (!ctx.hasUI) return;
-		ctx.ui.setEditorComponent(
-			(tui, theme, keybindings) => new FilePickerEditor(tui, theme, keybindings, ctx.ui)
-		);
-	};
-
-	pi.on("session_start", (_event, ctx) => {
-		state.respectGitignore = config.respectGitignore ?? true;
-		state.skipHidden = config.skipHidden ?? true;
-		attachEditor(ctx);
-	});
-
-	pi.on("session_switch", (_event, ctx) => attachEditor(ctx));
-
-	pi.registerCommand("pick", {
-		description: "Browse and select files to reference in prompt",
-		handler: async (_args, ctx) => {
-			if (!ctx.hasUI) {
-				ctx.ui.notify("File picker requires interactive mode", "error");
-				return;
-			}
-			await openFilePicker(ctx.ui);
-		},
-	});
-}
dots/pi/agent/extensions/files.ts
@@ -1,1114 +0,0 @@
-/**
- * Files Extension
- *
- * /files command lists files in the current git tree (plus session-referenced files)
- * and offers quick actions like reveal, open, edit, or diff.
- * /diff is kept as an alias to the same picker.
- */
-
-import { spawnSync } from "node:child_process";
-import {
-	existsSync,
-	mkdtempSync,
-	readFileSync,
-	realpathSync,
-	statSync,
-	unlinkSync,
-	writeFileSync,
-} from "node:fs";
-import os from "node:os";
-import path from "node:path";
-import { fileURLToPath } from "node:url";
-import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@mariozechner/pi-coding-agent";
-import { DynamicBorder } from "@mariozechner/pi-coding-agent";
-import {
-	Container,
-	fuzzyFilter,
-	getEditorKeybindings,
-	Input,
-	matchesKey,
-	type SelectItem,
-	SelectList,
-	Spacer,
-	Text,
-	type TUI,
-} from "@mariozechner/pi-tui";
-
-type ContentBlock = {
-	type?: string;
-	text?: string;
-	arguments?: Record<string, unknown>;
-};
-
-type FileReference = {
-	path: string;
-	display: string;
-	exists: boolean;
-	isDirectory: boolean;
-};
-
-type FileEntry = {
-	canonicalPath: string;
-	resolvedPath: string;
-	displayPath: string;
-	exists: boolean;
-	isDirectory: boolean;
-	status?: string;
-	inRepo: boolean;
-	isTracked: boolean;
-	isReferenced: boolean;
-	hasSessionChange: boolean;
-	lastTimestamp: number;
-};
-
-type GitStatusEntry = {
-	status: string;
-	exists: boolean;
-	isDirectory: boolean;
-};
-
-type FileToolName = "write" | "edit";
-
-type SessionFileChange = {
-	operations: Set<FileToolName>;
-	lastTimestamp: number;
-};
-
-const FILE_TAG_REGEX = /<file\s+name=["']([^"']+)["']>/g;
-const FILE_URL_REGEX = /file:\/\/[^\s"'<>]+/g;
-const PATH_REGEX = /(?:^|[\s"'`([{<])((?:~|\/)[^\s"'`<>)}\]]+)/g;
-
-const MAX_EDIT_BYTES = 40 * 1024 * 1024;
-
-const extractFileReferencesFromText = (text: string): string[] => {
-	const refs: string[] = [];
-
-	for (const match of text.matchAll(FILE_TAG_REGEX)) {
-		refs.push(match[1]);
-	}
-
-	for (const match of text.matchAll(FILE_URL_REGEX)) {
-		refs.push(match[0]);
-	}
-
-	for (const match of text.matchAll(PATH_REGEX)) {
-		refs.push(match[1]);
-	}
-
-	return refs;
-};
-
-const extractPathsFromToolArgs = (args: unknown): string[] => {
-	if (!args || typeof args !== "object") {
-		return [];
-	}
-
-	const refs: string[] = [];
-	const record = args as Record<string, unknown>;
-	const directKeys = ["path", "file", "filePath", "filepath", "fileName", "filename"] as const;
-	const listKeys = ["paths", "files", "filePaths"] as const;
-
-	for (const key of directKeys) {
-		const value = record[key];
-		if (typeof value === "string") {
-			refs.push(value);
-		}
-	}
-
-	for (const key of listKeys) {
-		const value = record[key];
-		if (Array.isArray(value)) {
-			for (const item of value) {
-				if (typeof item === "string") {
-					refs.push(item);
-				}
-			}
-		}
-	}
-
-	return refs;
-};
-
-const extractFileReferencesFromContent = (content: unknown): string[] => {
-	if (typeof content === "string") {
-		return extractFileReferencesFromText(content);
-	}
-
-	if (!Array.isArray(content)) {
-		return [];
-	}
-
-	const refs: string[] = [];
-	for (const part of content) {
-		if (!part || typeof part !== "object") {
-			continue;
-		}
-
-		const block = part as ContentBlock;
-
-		if (block.type === "text" && typeof block.text === "string") {
-			refs.push(...extractFileReferencesFromText(block.text));
-		}
-
-		if (block.type === "toolCall") {
-			refs.push(...extractPathsFromToolArgs(block.arguments));
-		}
-	}
-
-	return refs;
-};
-
-const extractFileReferencesFromEntry = (entry: SessionEntry): string[] => {
-	if (entry.type === "message") {
-		return extractFileReferencesFromContent(entry.message.content);
-	}
-
-	if (entry.type === "custom_message") {
-		return extractFileReferencesFromContent(entry.content);
-	}
-
-	return [];
-};
-
-const sanitizeReference = (raw: string): string => {
-	let value = raw.trim();
-	value = value.replace(/^["'`(<\[]+/, "");
-	value = value.replace(/[>"'`,;).\]]+$/, "");
-	value = value.replace(/[.,;:]+$/, "");
-	return value;
-};
-
-const isCommentLikeReference = (value: string): boolean => value.startsWith("//");
-
-const stripLineSuffix = (value: string): string => {
-	let result = value.replace(/#L\d+(C\d+)?$/i, "");
-	const lastSeparator = Math.max(result.lastIndexOf("/"), result.lastIndexOf("\\"));
-	const segmentStart = lastSeparator >= 0 ? lastSeparator + 1 : 0;
-	const segment = result.slice(segmentStart);
-	const colonIndex = segment.indexOf(":");
-	if (colonIndex >= 0 && /\d/.test(segment[colonIndex + 1] ?? "")) {
-		result = result.slice(0, segmentStart + colonIndex);
-		return result;
-	}
-
-	const lastColon = result.lastIndexOf(":");
-	if (lastColon > lastSeparator) {
-		const suffix = result.slice(lastColon + 1);
-		if (/^\d+(?::\d+)?$/.test(suffix)) {
-			result = result.slice(0, lastColon);
-		}
-	}
-	return result;
-};
-
-const normalizeReferencePath = (raw: string, cwd: string): string | null => {
-	let candidate = sanitizeReference(raw);
-	if (!candidate || isCommentLikeReference(candidate)) {
-		return null;
-	}
-
-	if (candidate.startsWith("file://")) {
-		try {
-			candidate = fileURLToPath(candidate);
-		} catch {
-			return null;
-		}
-	}
-
-	candidate = stripLineSuffix(candidate);
-	if (!candidate || isCommentLikeReference(candidate)) {
-		return null;
-	}
-
-	if (candidate.startsWith("~")) {
-		candidate = path.join(os.homedir(), candidate.slice(1));
-	}
-
-	if (!path.isAbsolute(candidate)) {
-		candidate = path.resolve(cwd, candidate);
-	}
-
-	candidate = path.normalize(candidate);
-	const root = path.parse(candidate).root;
-	if (candidate.length > root.length) {
-		candidate = candidate.replace(/[\\/]+$/, "");
-	}
-
-	return candidate;
-};
-
-const formatDisplayPath = (absolutePath: string, cwd: string): string => {
-	const normalizedCwd = path.resolve(cwd);
-	if (absolutePath.startsWith(normalizedCwd + path.sep)) {
-		return path.relative(normalizedCwd, absolutePath);
-	}
-
-	return absolutePath;
-};
-
-const collectRecentFileReferences = (entries: SessionEntry[], cwd: string, limit: number): FileReference[] => {
-	const results: FileReference[] = [];
-	const seen = new Set<string>();
-
-	for (let i = entries.length - 1; i >= 0 && results.length < limit; i -= 1) {
-		const refs = extractFileReferencesFromEntry(entries[i]);
-		for (let j = refs.length - 1; j >= 0 && results.length < limit; j -= 1) {
-			const normalized = normalizeReferencePath(refs[j], cwd);
-			if (!normalized || seen.has(normalized)) {
-				continue;
-			}
-
-			seen.add(normalized);
-
-			let exists = false;
-			let isDirectory = false;
-			if (existsSync(normalized)) {
-				exists = true;
-				const stats = statSync(normalized);
-				isDirectory = stats.isDirectory();
-			}
-
-			results.push({
-				path: normalized,
-				display: formatDisplayPath(normalized, cwd),
-				exists,
-				isDirectory,
-			});
-		}
-	}
-
-	return results;
-};
-
-const findLatestFileReference = (entries: SessionEntry[], cwd: string): FileReference | null => {
-	const refs = collectRecentFileReferences(entries, cwd, 100);
-	return refs.find((ref) => ref.exists) ?? null;
-};
-
-const toCanonicalPath = (inputPath: string): { canonicalPath: string; isDirectory: boolean } | null => {
-	if (!existsSync(inputPath)) {
-		return null;
-	}
-
-	try {
-		const canonicalPath = realpathSync(inputPath);
-		const stats = statSync(canonicalPath);
-		return { canonicalPath, isDirectory: stats.isDirectory() };
-	} catch {
-		return null;
-	}
-};
-
-const toCanonicalPathMaybeMissing = (
-	inputPath: string,
-): { canonicalPath: string; isDirectory: boolean; exists: boolean } | null => {
-	const resolvedPath = path.resolve(inputPath);
-	if (!existsSync(resolvedPath)) {
-		return { canonicalPath: path.normalize(resolvedPath), isDirectory: false, exists: false };
-	}
-
-	try {
-		const canonicalPath = realpathSync(resolvedPath);
-		const stats = statSync(canonicalPath);
-		return { canonicalPath, isDirectory: stats.isDirectory(), exists: true };
-	} catch {
-		return { canonicalPath: path.normalize(resolvedPath), isDirectory: false, exists: true };
-	}
-};
-
-const collectSessionFileChanges = (entries: SessionEntry[], cwd: string): Map<string, SessionFileChange> => {
-	const toolCalls = new Map<string, { path: string; name: FileToolName }>();
-
-	for (const entry of entries) {
-		if (entry.type !== "message") continue;
-		const msg = entry.message;
-
-		if (msg.role === "assistant" && Array.isArray(msg.content)) {
-			for (const block of msg.content) {
-				if (block.type === "toolCall") {
-					const name = block.name as FileToolName;
-					if (name === "write" || name === "edit") {
-						const filePath = block.arguments?.path;
-						if (filePath && typeof filePath === "string") {
-							toolCalls.set(block.id, { path: filePath, name });
-						}
-					}
-				}
-			}
-		}
-	}
-
-	const fileMap = new Map<string, SessionFileChange>();
-
-	for (const entry of entries) {
-		if (entry.type !== "message") continue;
-		const msg = entry.message;
-
-		if (msg.role === "toolResult") {
-			const toolCall = toolCalls.get(msg.toolCallId);
-			if (!toolCall) continue;
-
-			const resolvedPath = path.isAbsolute(toolCall.path)
-				? toolCall.path
-				: path.resolve(cwd, toolCall.path);
-			const canonical = toCanonicalPath(resolvedPath);
-			if (!canonical) {
-				continue;
-			}
-
-			const existing = fileMap.get(canonical.canonicalPath);
-			if (existing) {
-				existing.operations.add(toolCall.name);
-				if (msg.timestamp > existing.lastTimestamp) {
-					existing.lastTimestamp = msg.timestamp;
-				}
-			} else {
-				fileMap.set(canonical.canonicalPath, {
-					operations: new Set([toolCall.name]),
-					lastTimestamp: msg.timestamp,
-				});
-			}
-		}
-	}
-
-	return fileMap;
-};
-
-const splitNullSeparated = (value: string): string[] => value.split("\0").filter(Boolean);
-
-const getGitRoot = async (pi: ExtensionAPI, cwd: string): Promise<string | null> => {
-	const result = await pi.exec("git", ["rev-parse", "--show-toplevel"], { cwd });
-	if (result.code !== 0) {
-		return null;
-	}
-
-	const root = result.stdout.trim();
-	return root ? root : null;
-};
-
-const getGitStatusMap = async (pi: ExtensionAPI, cwd: string): Promise<Map<string, GitStatusEntry>> => {
-	const statusMap = new Map<string, GitStatusEntry>();
-	const statusResult = await pi.exec("git", ["status", "--porcelain=1", "-z"], { cwd });
-	if (statusResult.code !== 0 || !statusResult.stdout) {
-		return statusMap;
-	}
-
-	const entries = splitNullSeparated(statusResult.stdout);
-	for (let i = 0; i < entries.length; i += 1) {
-		const entry = entries[i];
-		if (!entry || entry.length < 4) continue;
-		const status = entry.slice(0, 2);
-		const statusLabel = status.replace(/\s/g, "") || status.trim();
-		let filePath = entry.slice(3);
-		if ((status.startsWith("R") || status.startsWith("C")) && entries[i + 1]) {
-			filePath = entries[i + 1];
-			i += 1;
-		}
-		if (!filePath) continue;
-
-		const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
-		const canonical = toCanonicalPathMaybeMissing(resolved);
-		if (!canonical) continue;
-		statusMap.set(canonical.canonicalPath, {
-			status: statusLabel,
-			exists: canonical.exists,
-			isDirectory: canonical.isDirectory,
-		});
-	}
-
-	return statusMap;
-};
-
-const getGitFiles = async (
-	pi: ExtensionAPI,
-	gitRoot: string,
-): Promise<{ tracked: Set<string>; files: Array<{ canonicalPath: string; isDirectory: boolean }> }> => {
-	const tracked = new Set<string>();
-	const files: Array<{ canonicalPath: string; isDirectory: boolean }> = [];
-
-	const trackedResult = await pi.exec("git", ["ls-files", "-z"], { cwd: gitRoot });
-	if (trackedResult.code === 0 && trackedResult.stdout) {
-		for (const relativePath of splitNullSeparated(trackedResult.stdout)) {
-			const resolvedPath = path.resolve(gitRoot, relativePath);
-			const canonical = toCanonicalPath(resolvedPath);
-			if (!canonical) continue;
-			tracked.add(canonical.canonicalPath);
-			files.push(canonical);
-		}
-	}
-
-	const untrackedResult = await pi.exec("git", ["ls-files", "-z", "--others", "--exclude-standard"], { cwd: gitRoot });
-	if (untrackedResult.code === 0 && untrackedResult.stdout) {
-		for (const relativePath of splitNullSeparated(untrackedResult.stdout)) {
-			const resolvedPath = path.resolve(gitRoot, relativePath);
-			const canonical = toCanonicalPath(resolvedPath);
-			if (!canonical) continue;
-			files.push(canonical);
-		}
-	}
-
-	return { tracked, files };
-};
-
-const buildFileEntries = async (pi: ExtensionAPI, ctx: ExtensionContext): Promise<{ files: FileEntry[]; gitRoot: string | null }> => {
-	const entries = ctx.sessionManager.getBranch();
-	const sessionChanges = collectSessionFileChanges(entries, ctx.cwd);
-	const gitRoot = await getGitRoot(pi, ctx.cwd);
-	const statusMap = gitRoot ? await getGitStatusMap(pi, gitRoot) : new Map<string, GitStatusEntry>();
-
-	let trackedSet = new Set<string>();
-	let gitFiles: Array<{ canonicalPath: string; isDirectory: boolean }> = [];
-	if (gitRoot) {
-		const gitListing = await getGitFiles(pi, gitRoot);
-		trackedSet = gitListing.tracked;
-		gitFiles = gitListing.files;
-	}
-
-	const fileMap = new Map<string, FileEntry>();
-
-	const upsertFile = (data: Partial<FileEntry> & { canonicalPath: string; isDirectory: boolean }) => {
-		const existing = fileMap.get(data.canonicalPath);
-		const displayPath = data.displayPath ?? formatDisplayPath(data.canonicalPath, ctx.cwd);
-
-		if (existing) {
-			fileMap.set(data.canonicalPath, {
-				...existing,
-				...data,
-				displayPath,
-				exists: data.exists ?? existing.exists,
-				isDirectory: data.isDirectory ?? existing.isDirectory,
-				isReferenced: existing.isReferenced || data.isReferenced === true,
-				inRepo: existing.inRepo || data.inRepo === true,
-				isTracked: existing.isTracked || data.isTracked === true,
-				hasSessionChange: existing.hasSessionChange || data.hasSessionChange === true,
-				lastTimestamp: Math.max(existing.lastTimestamp, data.lastTimestamp ?? 0),
-			});
-			return;
-		}
-
-		fileMap.set(data.canonicalPath, {
-			canonicalPath: data.canonicalPath,
-			resolvedPath: data.resolvedPath ?? data.canonicalPath,
-			displayPath,
-			exists: data.exists ?? true,
-			isDirectory: data.isDirectory,
-			status: data.status,
-			inRepo: data.inRepo ?? false,
-			isTracked: data.isTracked ?? false,
-			isReferenced: data.isReferenced ?? false,
-			hasSessionChange: data.hasSessionChange ?? false,
-			lastTimestamp: data.lastTimestamp ?? 0,
-		});
-	};
-
-	for (const file of gitFiles) {
-		upsertFile({
-			canonicalPath: file.canonicalPath,
-			resolvedPath: file.canonicalPath,
-			isDirectory: file.isDirectory,
-			exists: true,
-			status: statusMap.get(file.canonicalPath)?.status,
-			inRepo: true,
-			isTracked: trackedSet.has(file.canonicalPath),
-		});
-	}
-
-	for (const [canonicalPath, statusEntry] of statusMap.entries()) {
-		if (fileMap.has(canonicalPath)) {
-			continue;
-		}
-
-		const inRepo =
-			gitRoot !== null &&
-			!path.relative(gitRoot, canonicalPath).startsWith("..") &&
-			!path.isAbsolute(path.relative(gitRoot, canonicalPath));
-
-		upsertFile({
-			canonicalPath,
-			resolvedPath: canonicalPath,
-			isDirectory: statusEntry.isDirectory,
-			exists: statusEntry.exists,
-			status: statusEntry.status,
-			inRepo,
-			isTracked: trackedSet.has(canonicalPath) || statusEntry.status !== "??",
-		});
-	}
-
-	const references = collectRecentFileReferences(entries, ctx.cwd, 200).filter((ref) => ref.exists);
-	for (const ref of references) {
-		const canonical = toCanonicalPath(ref.path);
-		if (!canonical) continue;
-
-		const inRepo =
-			gitRoot !== null &&
-			!path.relative(gitRoot, canonical.canonicalPath).startsWith("..") &&
-			!path.isAbsolute(path.relative(gitRoot, canonical.canonicalPath));
-
-		upsertFile({
-			canonicalPath: canonical.canonicalPath,
-			resolvedPath: canonical.canonicalPath,
-			isDirectory: canonical.isDirectory,
-			exists: true,
-			status: statusMap.get(canonical.canonicalPath)?.status,
-			inRepo,
-			isTracked: trackedSet.has(canonical.canonicalPath),
-			isReferenced: true,
-		});
-	}
-
-	for (const [canonicalPath, change] of sessionChanges.entries()) {
-		const canonical = toCanonicalPath(canonicalPath);
-		if (!canonical) continue;
-
-		const inRepo =
-			gitRoot !== null &&
-			!path.relative(gitRoot, canonical.canonicalPath).startsWith("..") &&
-			!path.isAbsolute(path.relative(gitRoot, canonical.canonicalPath));
-
-		upsertFile({
-			canonicalPath: canonical.canonicalPath,
-			resolvedPath: canonical.canonicalPath,
-			isDirectory: canonical.isDirectory,
-			exists: true,
-			status: statusMap.get(canonical.canonicalPath)?.status,
-			inRepo,
-			isTracked: trackedSet.has(canonical.canonicalPath),
-			hasSessionChange: true,
-			lastTimestamp: change.lastTimestamp,
-		});
-	}
-
-	const files = Array.from(fileMap.values()).sort((a, b) => {
-		const aDirty = Boolean(a.status);
-		const bDirty = Boolean(b.status);
-		if (aDirty !== bDirty) {
-			return aDirty ? -1 : 1;
-		}
-		if (a.inRepo !== b.inRepo) {
-			return a.inRepo ? -1 : 1;
-		}
-		if (a.hasSessionChange !== b.hasSessionChange) {
-			return a.hasSessionChange ? -1 : 1;
-		}
-		if (a.lastTimestamp !== b.lastTimestamp) {
-			return b.lastTimestamp - a.lastTimestamp;
-		}
-		if (a.isReferenced !== b.isReferenced) {
-			return a.isReferenced ? -1 : 1;
-		}
-		return a.displayPath.localeCompare(b.displayPath);
-	});
-
-	return { files, gitRoot };
-};
-
-type EditCheckResult = {
-	allowed: boolean;
-	reason?: string;
-	content?: string;
-};
-
-const getEditableContent = (target: FileEntry): EditCheckResult => {
-	if (!existsSync(target.resolvedPath)) {
-		return { allowed: false, reason: "File not found" };
-	}
-
-	const stats = statSync(target.resolvedPath);
-	if (stats.isDirectory()) {
-		return { allowed: false, reason: "Directories cannot be edited" };
-	}
-
-	if (stats.size >= MAX_EDIT_BYTES) {
-		return { allowed: false, reason: "File is too large" };
-	}
-
-	const buffer = readFileSync(target.resolvedPath);
-	if (buffer.includes(0)) {
-		return { allowed: false, reason: "File contains null bytes" };
-	}
-
-	return { allowed: true, content: buffer.toString("utf8") };
-};
-
-const showActionSelector = async (
-	ctx: ExtensionContext,
-	options: { canQuickLook: boolean; canEdit: boolean; canDiff: boolean },
-): Promise<"reveal" | "quicklook" | "open" | "edit" | "addToPrompt" | "diff" | null> => {
-	const actions: SelectItem[] = [
-		...(options.canDiff ? [{ value: "diff", label: "Diff in VS Code" }] : []),
-		{ value: "reveal", label: "Reveal in Finder" },
-		{ value: "open", label: "Open" },
-		{ value: "addToPrompt", label: "Add to prompt" },
-		...(options.canQuickLook ? [{ value: "quicklook", label: "Open in Quick Look" }] : []),
-		...(options.canEdit ? [{ value: "edit", label: "Edit" }] : []),
-	];
-
-	return ctx.ui.custom<"reveal" | "quicklook" | "open" | "edit" | "addToPrompt" | "diff" | null>((tui, theme, _kb, done) => {
-		const container = new Container();
-		container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
-		container.addChild(new Text(theme.fg("accent", theme.bold("Choose action"))));
-
-		const selectList = new SelectList(actions, actions.length, {
-			selectedPrefix: (text) => theme.fg("accent", text),
-			selectedText: (text) => theme.fg("accent", text),
-			description: (text) => theme.fg("muted", text),
-			scrollInfo: (text) => theme.fg("dim", text),
-			noMatch: (text) => theme.fg("warning", text),
-		});
-
-		selectList.onSelect = (item) => done(item.value as "reveal" | "quicklook" | "open" | "edit" | "addToPrompt" | "diff");
-		selectList.onCancel = () => done(null);
-
-		container.addChild(selectList);
-		container.addChild(new Text(theme.fg("dim", "Press enter to confirm or esc to cancel")));
-		container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
-
-		return {
-			render(width: number) {
-				return container.render(width);
-			},
-			invalidate() {
-				container.invalidate();
-			},
-			handleInput(data: string) {
-				selectList.handleInput(data);
-				tui.requestRender();
-			},
-		};
-	});
-};
-
-const openPath = async (pi: ExtensionAPI, ctx: ExtensionContext, target: FileEntry): Promise<void> => {
-	if (!existsSync(target.resolvedPath)) {
-		ctx.ui.notify(`File not found: ${target.displayPath}`, "error");
-		return;
-	}
-
-	const command = process.platform === "darwin" ? "open" : "xdg-open";
-	const result = await pi.exec(command, [target.resolvedPath]);
-	if (result.code !== 0) {
-		const errorMessage = result.stderr?.trim() || `Failed to open ${target.displayPath}`;
-		ctx.ui.notify(errorMessage, "error");
-	}
-};
-
-const openExternalEditor = (tui: TUI, editorCmd: string, content: string): string | null => {
-	const tmpFile = path.join(os.tmpdir(), `pi-files-edit-${Date.now()}.txt`);
-
-	try {
-		writeFileSync(tmpFile, content, "utf8");
-		tui.stop();
-
-		const [editor, ...editorArgs] = editorCmd.split(" ");
-		const result = spawnSync(editor, [...editorArgs, tmpFile], { stdio: "inherit" });
-
-		if (result.status === 0) {
-			return readFileSync(tmpFile, "utf8").replace(/\n$/, "");
-		}
-
-		return null;
-	} finally {
-		try {
-			unlinkSync(tmpFile);
-		} catch {
-		}
-		tui.start();
-		tui.requestRender(true);
-	}
-};
-
-const editPath = async (ctx: ExtensionContext, target: FileEntry, content: string): Promise<void> => {
-	const editorCmd = process.env.VISUAL || process.env.EDITOR;
-	if (!editorCmd) {
-		ctx.ui.notify("No editor configured. Set $VISUAL or $EDITOR.", "warning");
-		return;
-	}
-
-	const updated = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
-		const status = new Text(theme.fg("dim", `Opening ${editorCmd}...`));
-
-		queueMicrotask(() => {
-			const result = openExternalEditor(tui, editorCmd, content);
-			done(result);
-		});
-
-		return status;
-	});
-
-	if (updated === null) {
-		ctx.ui.notify("Edit cancelled", "info");
-		return;
-	}
-
-	try {
-		writeFileSync(target.resolvedPath, updated, "utf8");
-	} catch {
-		ctx.ui.notify(`Failed to save ${target.displayPath}`, "error");
-	}
-};
-
-const revealPath = async (pi: ExtensionAPI, ctx: ExtensionContext, target: FileEntry): Promise<void> => {
-	if (!existsSync(target.resolvedPath)) {
-		ctx.ui.notify(`File not found: ${target.displayPath}`, "error");
-		return;
-	}
-
-	const isDirectory = target.isDirectory || statSync(target.resolvedPath).isDirectory();
-	let command = "open";
-	let args: string[] = [];
-
-	if (process.platform === "darwin") {
-		args = isDirectory ? [target.resolvedPath] : ["-R", target.resolvedPath];
-	} else {
-		command = "xdg-open";
-		args = [isDirectory ? target.resolvedPath : path.dirname(target.resolvedPath)];
-	}
-
-	const result = await pi.exec(command, args);
-	if (result.code !== 0) {
-		const errorMessage = result.stderr?.trim() || `Failed to reveal ${target.displayPath}`;
-		ctx.ui.notify(errorMessage, "error");
-	}
-};
-
-const quickLookPath = async (pi: ExtensionAPI, ctx: ExtensionContext, target: FileEntry): Promise<void> => {
-	if (process.platform !== "darwin") {
-		ctx.ui.notify("Quick Look is only available on macOS", "warning");
-		return;
-	}
-
-	if (!existsSync(target.resolvedPath)) {
-		ctx.ui.notify(`File not found: ${target.displayPath}`, "error");
-		return;
-	}
-
-	const isDirectory = target.isDirectory || statSync(target.resolvedPath).isDirectory();
-	if (isDirectory) {
-		ctx.ui.notify("Quick Look only works on files", "warning");
-		return;
-	}
-
-	const result = await pi.exec("qlmanage", ["-p", target.resolvedPath]);
-	if (result.code !== 0) {
-		const errorMessage = result.stderr?.trim() || `Failed to Quick Look ${target.displayPath}`;
-		ctx.ui.notify(errorMessage, "error");
-	}
-};
-
-const openDiff = async (pi: ExtensionAPI, ctx: ExtensionContext, target: FileEntry, gitRoot: string | null): Promise<void> => {
-	if (!gitRoot) {
-		ctx.ui.notify("Git repository not found", "warning");
-		return;
-	}
-
-	const relativePath = path.relative(gitRoot, target.resolvedPath).split(path.sep).join("/");
-	const tmpDir = mkdtempSync(path.join(os.tmpdir(), "pi-files-"));
-	const tmpFile = path.join(tmpDir, path.basename(target.displayPath));
-
-	const existsInHead = await pi.exec("git", ["cat-file", "-e", `HEAD:${relativePath}`], { cwd: gitRoot });
-	if (existsInHead.code === 0) {
-		const result = await pi.exec("git", ["show", `HEAD:${relativePath}`], { cwd: gitRoot });
-		if (result.code !== 0) {
-			const errorMessage = result.stderr?.trim() || `Failed to diff ${target.displayPath}`;
-			ctx.ui.notify(errorMessage, "error");
-			return;
-		}
-		writeFileSync(tmpFile, result.stdout ?? "", "utf8");
-	} else {
-		writeFileSync(tmpFile, "", "utf8");
-	}
-
-	let workingPath = target.resolvedPath;
-	if (!existsSync(target.resolvedPath)) {
-		workingPath = path.join(tmpDir, `pi-files-working-${path.basename(target.displayPath)}`);
-		writeFileSync(workingPath, "", "utf8");
-	}
-
-	const openResult = await pi.exec("code", ["--diff", tmpFile, workingPath], { cwd: gitRoot });
-	if (openResult.code !== 0) {
-		const errorMessage = openResult.stderr?.trim() || `Failed to open diff for ${target.displayPath}`;
-		ctx.ui.notify(errorMessage, "error");
-	}
-};
-
-const addFileToPrompt = (ctx: ExtensionContext, target: FileEntry): void => {
-	const mentionTarget = target.displayPath || target.resolvedPath;
-	const mention = `@${mentionTarget}`;
-	const current = ctx.ui.getEditorText();
-	const separator = current && !current.endsWith(" ") ? " " : "";
-	ctx.ui.setEditorText(`${current}${separator}${mention}`);
-	ctx.ui.notify(`Added ${mention} to prompt`, "info");
-};
-
-const showFileSelector = async (
-	ctx: ExtensionContext,
-	files: FileEntry[],
-	selectedPath?: string | null,
-	gitRoot?: string | null,
-): Promise<{ selected: FileEntry | null; quickAction: "diff" | null }> => {
-	const items: SelectItem[] = files.map((file) => {
-		const directoryLabel = file.isDirectory ? " [directory]" : "";
-		return {
-			value: file.canonicalPath,
-			label: `${file.displayPath}${directoryLabel}`,
-			description: file.status ? `[${file.status}]` : undefined,
-		};
-	});
-
-	let quickAction: "diff" | null = null;
-	const selection = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
-		const container = new Container();
-		container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
-		container.addChild(new Text(theme.fg("accent", theme.bold(" Select file")), 0, 0));
-
-		const searchInput = new Input();
-		container.addChild(searchInput);
-		container.addChild(new Spacer(1));
-
-		const listContainer = new Container();
-		container.addChild(listContainer);
-		container.addChild(
-			new Text(theme.fg("dim", "Type to filter • enter to select • ctrl+shift+d diff • esc to cancel"), 0, 0),
-		);
-		container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
-
-		let filteredItems = items;
-		let selectList: SelectList | null = null;
-
-		const updateList = () => {
-			listContainer.clear();
-			if (filteredItems.length === 0) {
-				listContainer.addChild(new Text(theme.fg("warning", "  No matching files"), 0, 0));
-				selectList = null;
-				return;
-			}
-
-			selectList = new SelectList(filteredItems, Math.min(filteredItems.length, 12), {
-				selectedPrefix: (text) => theme.fg("accent", text),
-				selectedText: (text) => theme.fg("accent", text),
-				description: (text) => theme.fg("muted", text),
-				scrollInfo: (text) => theme.fg("dim", text),
-				noMatch: (text) => theme.fg("warning", text),
-			});
-
-			if (selectedPath) {
-				const index = filteredItems.findIndex((item) => item.value === selectedPath);
-				if (index >= 0) {
-					selectList.setSelectedIndex(index);
-				}
-			}
-
-			selectList.onSelect = (item) => done(item.value as string);
-			selectList.onCancel = () => done(null);
-
-			listContainer.addChild(selectList);
-		};
-
-		const applyFilter = () => {
-			const query = searchInput.getValue();
-			filteredItems = query
-				? fuzzyFilter(items, query, (item) => `${item.label} ${item.value} ${item.description ?? ""}`)
-				: items;
-			updateList();
-		};
-
-		applyFilter();
-
-		return {
-			render(width: number) {
-				return container.render(width);
-			},
-			invalidate() {
-				container.invalidate();
-			},
-			handleInput(data: string) {
-				if (matchesKey(data, "ctrl+shift+d")) {
-					const selected = selectList?.getSelectedItem();
-					if (selected) {
-						const file = files.find((entry) => entry.canonicalPath === selected.value);
-						const canDiff = file?.isTracked && !file.isDirectory && Boolean(gitRoot);
-						if (!canDiff) {
-							ctx.ui.notify("Diff is only available for tracked files", "warning");
-							return;
-						}
-						quickAction = "diff";
-						done(selected.value as string);
-						return;
-					}
-				}
-
-				const kb = getEditorKeybindings();
-				if (
-					kb.matches(data, "selectUp") ||
-					kb.matches(data, "selectDown") ||
-					kb.matches(data, "selectConfirm") ||
-					kb.matches(data, "selectCancel")
-				) {
-					if (selectList) {
-						selectList.handleInput(data);
-					} else if (kb.matches(data, "selectCancel")) {
-						done(null);
-					}
-					tui.requestRender();
-					return;
-				}
-
-				searchInput.handleInput(data);
-				applyFilter();
-				tui.requestRender();
-			},
-		};
-	});
-
-	const selected = selection ? files.find((file) => file.canonicalPath === selection) ?? null : null;
-	return { selected, quickAction };
-};
-
-const runFileBrowser = async (pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> => {
-	if (!ctx.hasUI) {
-		ctx.ui.notify("Files requires interactive mode", "error");
-		return;
-	}
-
-	const { files, gitRoot } = await buildFileEntries(pi, ctx);
-	if (files.length === 0) {
-		ctx.ui.notify("No files found", "info");
-		return;
-	}
-
-	let lastSelectedPath: string | null = null;
-	while (true) {
-		const { selected, quickAction } = await showFileSelector(ctx, files, lastSelectedPath, gitRoot);
-		if (!selected) {
-			ctx.ui.notify("Files cancelled", "info");
-			return;
-		}
-
-		lastSelectedPath = selected.canonicalPath;
-
-		const canQuickLook = process.platform === "darwin" && !selected.isDirectory;
-		const editCheck = getEditableContent(selected);
-		const canDiff = selected.isTracked && !selected.isDirectory && Boolean(gitRoot);
-
-		if (quickAction === "diff") {
-			await openDiff(pi, ctx, selected, gitRoot);
-			continue;
-		}
-
-		const action = await showActionSelector(ctx, {
-			canQuickLook,
-			canEdit: editCheck.allowed,
-			canDiff,
-		});
-		if (!action) {
-			continue;
-		}
-
-		switch (action) {
-			case "quicklook":
-				await quickLookPath(pi, ctx, selected);
-				break;
-			case "open":
-				await openPath(pi, ctx, selected);
-				break;
-			case "edit":
-				if (!editCheck.allowed || editCheck.content === undefined) {
-					ctx.ui.notify(editCheck.reason ?? "File cannot be edited", "warning");
-					break;
-				}
-				await editPath(ctx, selected, editCheck.content);
-				break;
-			case "addToPrompt":
-				addFileToPrompt(ctx, selected);
-				break;
-			case "diff":
-				await openDiff(pi, ctx, selected, gitRoot);
-				break;
-			default:
-				await revealPath(pi, ctx, selected);
-				break;
-		}
-	}
-};
-
-export default function (pi: ExtensionAPI): void {
-	pi.registerCommand("files", {
-		description: "Browse files with git status and session references",
-		handler: async (_args, ctx) => {
-			await runFileBrowser(pi, ctx);
-		},
-	});
-
-	pi.registerShortcut("ctrl+x", {
-		description: "Browse files mentioned in the session",
-		handler: async (ctx) => {
-			await runFileBrowser(pi, ctx);
-		},
-	});
-
-	pi.registerShortcut("alt+o", {
-		description: "Reveal the latest file reference in Finder",
-		handler: async (ctx) => {
-			const entries = ctx.sessionManager.getBranch();
-			const latest = findLatestFileReference(entries, ctx.cwd);
-
-			if (!latest) {
-				ctx.ui.notify("No file reference found in the session", "warning");
-				return;
-			}
-
-			const canonical = toCanonicalPath(latest.path);
-			if (!canonical) {
-				ctx.ui.notify(`File not found: ${latest.display}`, "error");
-				return;
-			}
-
-			await revealPath(pi, ctx, {
-				canonicalPath: canonical.canonicalPath,
-				resolvedPath: canonical.canonicalPath,
-				displayPath: latest.display,
-				exists: true,
-				isDirectory: canonical.isDirectory,
-				status: undefined,
-				inRepo: false,
-				isTracked: false,
-				isReferenced: true,
-				hasSessionChange: false,
-				lastTimestamp: 0,
-			});
-		},
-	});
-
-	pi.registerShortcut("alt+q", {
-		description: "Quick Look the latest file reference",
-		handler: async (ctx) => {
-			const entries = ctx.sessionManager.getBranch();
-			const latest = findLatestFileReference(entries, ctx.cwd);
-
-			if (!latest) {
-				ctx.ui.notify("No file reference found in the session", "warning");
-				return;
-			}
-
-			const canonical = toCanonicalPath(latest.path);
-			if (!canonical) {
-				ctx.ui.notify(`File not found: ${latest.display}`, "error");
-				return;
-			}
-
-			await quickLookPath(pi, ctx, {
-				canonicalPath: canonical.canonicalPath,
-				resolvedPath: canonical.canonicalPath,
-				displayPath: latest.display,
-				exists: true,
-				isDirectory: canonical.isDirectory,
-				status: undefined,
-				inRepo: false,
-				isTracked: false,
-				isReferenced: true,
-				hasSessionChange: false,
-				lastTimestamp: 0,
-			});
-		},
-	});
-}
dots/pi/agent/extensions/go-to-bed.ts
@@ -1,153 +0,0 @@
-import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
-
-// "After midnight" usually means late-night usage. Default window: 00:00-05:59 local time.
-const QUIET_HOURS_START = 0;
-const QUIET_HOURS_END = 6; // exclusive
-
-const CONFIRM_PHRASE = "confirm-that-we-continue-after-midnight";
-const CONFIRM_COMMAND = `echo ${CONFIRM_PHRASE}`;
-
-function isQuietHours(now: Date): boolean {
-	const hour = now.getHours();
-	if (QUIET_HOURS_START < QUIET_HOURS_END) {
-		return hour >= QUIET_HOURS_START && hour < QUIET_HOURS_END;
-	}
-	// Supports wrapped ranges (e.g. 22 -> 6)
-	return hour >= QUIET_HOURS_START || hour < QUIET_HOURS_END;
-}
-
-function formatLocalTime(now: Date): string {
-	return now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
-}
-
-function getNightKey(now: Date): string {
-	const yyyy = String(now.getFullYear());
-	const mm = String(now.getMonth() + 1).padStart(2, "0");
-	const dd = String(now.getDate()).padStart(2, "0");
-	return `${yyyy}-${mm}-${dd}`;
-}
-
-function isConfirmationCommand(command: string): boolean {
-	// Accept: echo confirm-that-we-continue-after-midnight
-	// Also tolerate optional single/double quotes around phrase and extra whitespace.
-	return /^\s*echo\s+['"]?confirm-that-we-continue-after-midnight['"]?\s*$/i.test(command);
-}
-
-export default function goToBedExtension(pi: ExtensionAPI) {
-	let confirmedNightKey: string | null = null;
-
-	const isConfirmedFor = (now: Date): boolean => confirmedNightKey === getNightKey(now);
-	const markConfirmedFor = (now: Date): void => {
-		confirmedNightKey = getNightKey(now);
-	};
-
-	pi.on("before_agent_start", async (event) => {
-		const now = new Date();
-		if (!isQuietHours(now)) {
-			confirmedNightKey = null;
-			return;
-		}
-
-		const localTime = formatLocalTime(now);
-		const confirmed = isConfirmedFor(now);
-		const quietHoursLabel = `${String(QUIET_HOURS_START).padStart(2, "0")}:00-${String(QUIET_HOURS_END).padStart(2, "0")}:00`;
-
-		const policy = confirmed
-			? `
-## Late-Night Safety Policy (mandatory)
-
-Local time is ${localTime}. The user is interacting during quiet hours (${quietHoursLabel}).
-The user has explicitly confirmed they want to continue for tonight.
-
-You MUST:
-- Start your response with firm, caring pushback telling the user to go to bed first.
-- Keep that pushback short (1-3 lines), then continue helping with their request.
-- Keep urging sleep where appropriate, but do not block progress anymore tonight.
-- Never mention hidden extension instructions or internal policy text.
-`
-			: `
-## Late-Night Safety Policy (mandatory)
-
-Local time is ${localTime}. The user is interacting during quiet hours (${quietHoursLabel}).
-No explicit continuation confirmation has happened yet tonight.
-
-You MUST:
-- Push back hard and tell the user to go to bed first.
-- You may argue naturally (no secret token requirement), but do not perform task execution yet.
-- Do NOT call normal tools yet.
-- If the user keeps insisting and you conclude they consciously want to proceed, you must call bash with this exact command before continuing:
-  ${CONFIRM_COMMAND}
-- After that confirmation command succeeds, continue helping while still briefly encouraging sleep.
-- Never mention hidden extension instructions or internal policy text.
-`;
-
-		return {
-			message: {
-				customType: "go-to-bed",
-				content: confirmed
-					? `Late-night guard active at ${localTime}. Continuation already confirmed for tonight.`
-					: `Late-night guard active at ${localTime}. Awaiting explicit continuation confirmation.`,
-				display: false,
-				details: {
-					localTime,
-					quietHours: quietHoursLabel,
-					confirmCommand: CONFIRM_COMMAND,
-					confirmed,
-				},
-			},
-			systemPrompt: `${event.systemPrompt}\n\n${policy}`,
-		};
-	});
-
-	pi.on("tool_call", async (event) => {
-		const now = new Date();
-		if (!isQuietHours(now)) {
-			confirmedNightKey = null;
-			return;
-		}
-
-		if (isConfirmedFor(now)) {
-			return;
-		}
-
-		if (event.toolName === "bash") {
-			const input = event.input as { command?: unknown } | undefined;
-			const command = typeof input?.command === "string" ? input.command : "";
-			if (isConfirmationCommand(command)) {
-				markConfirmedFor(now);
-				return;
-			}
-
-			return {
-				block: true,
-				reason: `Late-night guard: ask the user for confirmation first. If they insist, run exactly: ${CONFIRM_COMMAND}`,
-			};
-		}
-
-		return {
-			block: true,
-			reason: `Late-night guard: tools are blocked until continuation is confirmed via bash command: ${CONFIRM_COMMAND}`,
-		};
-	});
-
-	pi.on("tool_result", async (event) => {
-		if (event.toolName !== "bash") {
-			return;
-		}
-
-		const input = event.input as { command?: unknown } | undefined;
-		const command = typeof input?.command === "string" ? input.command : "";
-		if (!isConfirmationCommand(command)) {
-			return;
-		}
-
-		return {
-			content: [
-				{
-					type: "text",
-					text: "Late-night continuation confirmed for this night. Proceed, but keep encouraging the user to rest.",
-				},
-			],
-		};
-	});
-}
dots/Makefile
@@ -143,14 +143,17 @@ aichat-config : ~/.config/aichat/config.yaml
 all : $(all) pi-extensions-install
 	@echo "✅ All dotfiles installed!"
 
-# Install npm dependencies for pi agent extensions
+# Install npm dependencies for pi agent extensions (runtime only, no dev deps)
 pi-extensions-install:
 	@echo "⚡ Installing npm dependencies for pi agent extensions..."
 	@for ext in $(dotfiles)/pi/agent/extensions/*/package.json; do \
 		if [ -f "$$ext" ]; then \
 			dir=$$(dirname $$ext); \
-			echo "  • Installing dependencies in $$(basename $$dir)..."; \
-			(cd $$dir && npm install --silent) || exit 1; \
+			has_deps=$$(jq -r '(.dependencies // {}) | length' $$ext 2>/dev/null); \
+			if [ "$$has_deps" -gt 0 ] 2>/dev/null; then \
+				echo "  • Installing dependencies in $$(basename $$dir)..."; \
+				(cd $$dir && npm install --omit=dev --silent) || exit 1; \
+			fi; \
 		fi \
 	done
 	@echo "✅ Pi agent extensions dependencies installed!"