diff --git a/package-lock.json b/package-lock.json index 459b8b8b..106ec648 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3195,11 +3195,11 @@ }, "packages/agent": { "name": "@mariozechner/pi-agent", - "version": "0.7.22", + "version": "0.7.23", "license": "MIT", "dependencies": { - "@mariozechner/pi-ai": "^0.7.21", - "@mariozechner/pi-tui": "^0.7.21" + "@mariozechner/pi-ai": "^0.7.22", + "@mariozechner/pi-tui": "^0.7.22" }, "devDependencies": { "@types/node": "^24.3.0", @@ -3225,7 +3225,7 @@ }, "packages/ai": { "name": "@mariozechner/pi-ai", - "version": "0.7.22", + "version": "0.7.23", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.61.0", @@ -3272,11 +3272,11 @@ }, "packages/coding-agent": { "name": "@mariozechner/pi-coding-agent", - "version": "0.7.22", + "version": "0.7.23", "license": "MIT", "dependencies": { - "@mariozechner/pi-agent": "^0.7.21", - "@mariozechner/pi-ai": "^0.7.21", + "@mariozechner/pi-agent": "^0.7.22", + "@mariozechner/pi-ai": "^0.7.22", "chalk": "^5.5.0", "diff": "^8.0.2", "glob": "^11.0.3" @@ -3319,10 +3319,10 @@ }, "packages/pods": { "name": "@mariozechner/pi", - "version": "0.7.22", + "version": "0.7.23", "license": "MIT", "dependencies": { - "@mariozechner/pi-agent": "^0.7.21", + "@mariozechner/pi-agent": "^0.7.22", "chalk": "^5.5.0" }, "bin": { @@ -3345,7 +3345,7 @@ }, "packages/proxy": { "name": "@mariozechner/pi-proxy", - "version": "0.7.22", + "version": "0.7.23", "dependencies": { "@hono/node-server": "^1.14.0", "hono": "^4.6.16" @@ -3361,7 +3361,7 @@ }, "packages/tui": { "name": "@mariozechner/pi-tui", - "version": "0.7.22", + "version": "0.7.23", "license": "MIT", "dependencies": { "@types/mime-types": "^2.1.4", @@ -3400,12 +3400,12 @@ }, "packages/web-ui": { "name": "@mariozechner/pi-web-ui", - "version": "0.7.22", + "version": "0.7.23", "license": "MIT", "dependencies": { "@lmstudio/sdk": "^1.5.0", - "@mariozechner/pi-ai": "^0.7.21", - "@mariozechner/pi-tui": "^0.7.21", + "@mariozechner/pi-ai": "^0.7.22", + "@mariozechner/pi-tui": "^0.7.22", "docx-preview": "^0.3.7", "jszip": "^3.10.1", "lucide": "^0.544.0", diff --git a/packages/agent/package.json b/packages/agent/package.json index 3f01bb69..b9e7bf23 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-agent", - "version": "0.7.22", + "version": "0.7.23", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -18,8 +18,8 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@mariozechner/pi-ai": "^0.7.22", - "@mariozechner/pi-tui": "^0.7.22" + "@mariozechner/pi-ai": "^0.7.23", + "@mariozechner/pi-tui": "^0.7.23" }, "keywords": [ "ai", diff --git a/packages/ai/package.json b/packages/ai/package.json index fb858d7e..39b7b3bc 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-ai", - "version": "0.7.22", + "version": "0.7.23", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index cc0e51d9..7df32f6a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,21 @@ ## [Unreleased] +## [0.7.23] - 2025-11-20 + +### Added + +- **Update Notifications**: Interactive mode now checks for new versions on startup and displays a notification if an update is available. + +### Changed + +- **System Prompt**: Updated system prompt to instruct agent to output plain text summaries directly instead of using cat or bash commands to display what it did. + +### Fixed + +- **File Path Completion**: Removed 10-file limit in tab completion selector. All matching files and directories now appear in the completion list. +- **Absolute Path Completion**: Fixed tab completion for absolute paths (e.g., `/Applications`). Absolute paths in the middle of text (like "hey /") now complete correctly. Also fixed crashes when trying to stat inaccessible files (like macOS `.VolumeIcon.icns`) during directory traversal. + ## [0.7.22] - 2025-11-19 ### Fixed diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 56921b33..c6abf2ec 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-coding-agent", - "version": "0.7.22", + "version": "0.7.23", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "bin": { @@ -21,8 +21,8 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@mariozechner/pi-agent": "^0.7.22", - "@mariozechner/pi-ai": "^0.7.22", + "@mariozechner/pi-agent": "^0.7.23", + "@mariozechner/pi-ai": "^0.7.23", "chalk": "^5.5.0", "diff": "^8.0.2", "glob": "^11.0.3" diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index 1801c49a..b77eeb89 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -219,6 +219,7 @@ Guidelines: - Use write only for new files or complete rewrites - Be concise in your responses - Show file paths clearly when working with files +- When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did Documentation: - Your own documentation (including custom model setup) is at: ${readmePath} @@ -308,6 +309,25 @@ function loadProjectContextFiles(): Array<{ path: string; content: string }> { return contextFiles; } +async function checkForNewVersion(currentVersion: string): Promise { + try { + const response = await fetch("https://registry.npmjs.org/@mariozechner/pi-coding-agent/latest"); + if (!response.ok) return null; + + const data = await response.json(); + const latestVersion = data.version; + + if (latestVersion && latestVersion !== currentVersion) { + return latestVersion; + } + + return null; + } catch (error) { + // Silently fail - don't disrupt the user experience + return null; + } +} + async function selectSession(sessionManager: SessionManager): Promise { return new Promise((resolve) => { const ui = new TUI(new ProcessTerminal()); @@ -344,8 +364,9 @@ async function runInteractiveMode( version: string, changelogMarkdown: string | null = null, modelFallbackMessage: string | null = null, + newVersion: string | null = null, ): Promise { - const renderer = new TuiRenderer(agent, sessionManager, settingsManager, version, changelogMarkdown); + const renderer = new TuiRenderer(agent, sessionManager, settingsManager, version, changelogMarkdown, newVersion); // Initialize TUI await renderer.init(); @@ -752,6 +773,17 @@ export async function main(args: string[]) { // RPC mode - headless operation await runRpcMode(agent, sessionManager); } else if (isInteractive) { + // Check for new version (don't block startup if it takes too long) + let newVersion: string | null = null; + try { + newVersion = await Promise.race([ + checkForNewVersion(VERSION), + new Promise((resolve) => setTimeout(() => resolve(null), 1000)), // 1 second timeout + ]); + } catch (e) { + // Ignore errors + } + // Check if we should show changelog (only in interactive mode, only for new sessions) let changelogMarkdown: string | null = null; if (!parsed.continue && !parsed.resume) { @@ -789,6 +821,7 @@ export async function main(args: string[]) { VERSION, changelogMarkdown, modelFallbackMessage, + newVersion, ); } else { // CLI mode with messages diff --git a/packages/coding-agent/src/tui/tui-renderer.ts b/packages/coding-agent/src/tui/tui-renderer.ts index 12c826a9..ec5cfe87 100644 --- a/packages/coding-agent/src/tui/tui-renderer.ts +++ b/packages/coding-agent/src/tui/tui-renderer.ts @@ -51,6 +51,7 @@ export class TuiRenderer { private onInterruptCallback?: () => void; private lastSigintTime = 0; private changelogMarkdown: string | null = null; + private newVersion: string | null = null; // Streaming message tracking private streamingComponent: AssistantMessageComponent | null = null; @@ -79,11 +80,13 @@ export class TuiRenderer { settingsManager: SettingsManager, version: string, changelogMarkdown: string | null = null, + newVersion: string | null = null, ) { this.agent = agent; this.sessionManager = sessionManager; this.settingsManager = settingsManager; this.version = version; + this.newVersion = newVersion; this.changelogMarkdown = changelogMarkdown; this.ui = new TUI(new ProcessTerminal()); this.chatContainer = new Container(); @@ -181,6 +184,23 @@ export class TuiRenderer { this.ui.addChild(header); this.ui.addChild(new Spacer(1)); + // Add new version notification if available + if (this.newVersion) { + this.ui.addChild(new DynamicBorder(chalk.yellow)); + this.ui.addChild( + new Text( + chalk.bold.yellow("Update Available") + + "\n" + + chalk.gray(`New version ${this.newVersion} is available. Run: `) + + chalk.cyan("npm install -g @mariozechner/pi-coding-agent"), + 1, + 0, + ), + ); + this.ui.addChild(new Spacer(1)); + this.ui.addChild(new DynamicBorder(chalk.yellow)); + } + // Add changelog if provided if (this.changelogMarkdown) { this.ui.addChild(new DynamicBorder(chalk.cyan)); diff --git a/packages/pods/package.json b/packages/pods/package.json index 77841f13..a1ee944a 100644 --- a/packages/pods/package.json +++ b/packages/pods/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi", - "version": "0.7.22", + "version": "0.7.23", "description": "CLI tool for managing vLLM deployments on GPU pods", "type": "module", "bin": { @@ -34,7 +34,7 @@ "node": ">=20.0.0" }, "dependencies": { - "@mariozechner/pi-agent": "^0.7.22", + "@mariozechner/pi-agent": "^0.7.23", "chalk": "^5.5.0" }, "devDependencies": {} diff --git a/packages/proxy/package.json b/packages/proxy/package.json index 1e88b31c..a4db5623 100644 --- a/packages/proxy/package.json +++ b/packages/proxy/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-proxy", - "version": "0.7.22", + "version": "0.7.23", "type": "module", "description": "CORS and authentication proxy for pi-ai", "main": "dist/index.js", diff --git a/packages/tui/package.json b/packages/tui/package.json index 6d2c6565..4d1e6478 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-tui", - "version": "0.7.22", + "version": "0.7.23", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", diff --git a/packages/tui/src/autocomplete.ts b/packages/tui/src/autocomplete.ts index dfef465c..9d366fff 100644 --- a/packages/tui/src/autocomplete.ts +++ b/packages/tui/src/autocomplete.ts @@ -286,10 +286,12 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { // Match paths - including those ending with /, ~/, or any word at end for forced extraction // This regex captures: // - Paths starting from beginning of line or after space/quote/equals - // - Optional ./ or ../ or ~/ prefix (including the trailing slash for ~/) + // - Absolute paths starting with / + // - Relative paths with ./ or ../ + // - Home directory paths with ~/ // - The path itself (can include / in the middle) // - For forced extraction, capture any word at the end - const matches = text.match(/(?:^|[\s"'=])((?:~\/|\.{0,2}\/?)?(?:[^\s"'=]*\/?)*[^\s"'=]*)$/); + const matches = text.match(/(?:^|[\s"'=])((?:\/|~\/|\.{1,2}\/)?(?:[^\s"'=]*\/?)*[^\s"'=]*)$/); if (!matches) { // If forced extraction and no matches, return empty string to trigger from current dir return forceExtract ? "" : null; @@ -354,10 +356,11 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { expandedPrefix === "../" || expandedPrefix === "~" || expandedPrefix === "~/" || + expandedPrefix === "/" || prefix === "@" ) { // Complete from specified position - if (prefix.startsWith("~")) { + if (prefix.startsWith("~") || expandedPrefix === "/") { searchDir = expandedPrefix; } else { searchDir = join(this.basePath, expandedPrefix); @@ -365,7 +368,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { searchPrefix = ""; } else if (expandedPrefix.endsWith("/")) { // If prefix ends with /, show contents of that directory - if (prefix.startsWith("~") || (isAtPrefix && expandedPrefix.startsWith("/"))) { + if (prefix.startsWith("~") || expandedPrefix.startsWith("/")) { searchDir = expandedPrefix; } else { searchDir = join(this.basePath, expandedPrefix); @@ -375,7 +378,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { // Split into directory and file prefix const dir = dirname(expandedPrefix); const file = basename(expandedPrefix); - if (prefix.startsWith("~") || (isAtPrefix && expandedPrefix.startsWith("/"))) { + if (prefix.startsWith("~") || expandedPrefix.startsWith("/")) { searchDir = dir; } else { searchDir = join(this.basePath, dir); @@ -392,7 +395,13 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { } const fullPath = join(searchDir, entry); - const isDirectory = statSync(fullPath).isDirectory(); + let isDirectory: boolean; + try { + isDirectory = statSync(fullPath).isDirectory(); + } catch (e) { + // Skip files we can't stat (permission issues, broken symlinks, etc.) + continue; + } // For @ prefix, filter to only show directories and attachable files if (isAtPrefix && !isDirectory && !isAttachableFile(fullPath)) { @@ -430,6 +439,14 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { const homeRelativeDir = prefix.slice(2); // Remove ~/ const dir = dirname(homeRelativeDir); relativePath = "~/" + (dir === "." ? entry : join(dir, entry)); + } else if (prefix.startsWith("/")) { + // Absolute path - construct properly + const dir = dirname(prefix); + if (dir === "/") { + relativePath = "/" + entry; + } else { + relativePath = dir + "/" + entry; + } } else { relativePath = join(dirname(prefix), entry); } @@ -458,7 +475,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { return a.label.localeCompare(b.label); }); - return suggestions.slice(0, 10); // Limit to 10 suggestions + return suggestions; } catch (e) { // Directory doesn't exist or not accessible return []; @@ -474,8 +491,8 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { const currentLine = lines[cursorLine] || ""; const textBeforeCursor = currentLine.slice(0, cursorCol); - // Don't trigger if we're in a slash command - if (textBeforeCursor.startsWith("/") && !textBeforeCursor.includes(" ")) { + // Don't trigger if we're typing a slash command at the start of the line + if (textBeforeCursor.trim().startsWith("/") && !textBeforeCursor.trim().includes(" ")) { return null; } @@ -499,8 +516,8 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { const currentLine = lines[cursorLine] || ""; const textBeforeCursor = currentLine.slice(0, cursorCol); - // Don't trigger if we're in a slash command - if (textBeforeCursor.startsWith("/") && !textBeforeCursor.includes(" ")) { + // Don't trigger if we're typing a slash command at the start of the line + if (textBeforeCursor.trim().startsWith("/") && !textBeforeCursor.trim().includes(" ")) { return false; } diff --git a/packages/tui/test/autocomplete.test.ts b/packages/tui/test/autocomplete.test.ts new file mode 100644 index 00000000..234ba6eb --- /dev/null +++ b/packages/tui/test/autocomplete.test.ts @@ -0,0 +1,64 @@ +import assert from "node:assert"; +import { describe, it } from "node:test"; +import { CombinedAutocompleteProvider } from "../src/autocomplete.js"; + +describe("CombinedAutocompleteProvider", () => { + describe("extractPathPrefix", () => { + it("extracts / from 'hey /' when forced", () => { + const provider = new CombinedAutocompleteProvider([], "/tmp"); + const lines = ["hey /"]; + const cursorLine = 0; + const cursorCol = 5; // After the "/" + + const result = provider.getForceFileSuggestions(lines, cursorLine, cursorCol); + + assert.notEqual(result, null, "Should return suggestions for root directory"); + if (result) { + assert.strictEqual(result.prefix, "/", "Prefix should be '/'"); + } + }); + + it("extracts /A from '/A' when forced", () => { + const provider = new CombinedAutocompleteProvider([], "/tmp"); + const lines = ["/A"]; + const cursorLine = 0; + const cursorCol = 2; // After the "A" + + const result = provider.getForceFileSuggestions(lines, cursorLine, cursorCol); + + console.log("Result:", result); + // This might return null if /A doesn't match anything, which is fine + // We're mainly testing that the prefix extraction works + if (result) { + assert.strictEqual(result.prefix, "/A", "Prefix should be '/A'"); + } + }); + + it("does not trigger for slash commands", () => { + const provider = new CombinedAutocompleteProvider([], "/tmp"); + const lines = ["/model"]; + const cursorLine = 0; + const cursorCol = 6; // After "model" + + const result = provider.getForceFileSuggestions(lines, cursorLine, cursorCol); + + console.log("Result:", result); + assert.strictEqual(result, null, "Should not trigger for slash commands"); + }); + + it("triggers for absolute paths after slash command argument", () => { + const provider = new CombinedAutocompleteProvider([], "/tmp"); + const lines = ["/command /"]; + const cursorLine = 0; + const cursorCol = 10; // After the second "/" + + const result = provider.getForceFileSuggestions(lines, cursorLine, cursorCol); + + console.log("Result:", result); + assert.notEqual(result, null, "Should trigger for absolute paths in command arguments"); + if (result) { + assert.strictEqual(result.prefix, "/", "Prefix should be '/'"); + } + }); + }); +}); diff --git a/packages/web-ui/package.json b/packages/web-ui/package.json index f8e75372..86d8b945 100644 --- a/packages/web-ui/package.json +++ b/packages/web-ui/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-web-ui", - "version": "0.7.22", + "version": "0.7.23", "description": "Reusable web UI components for AI chat interfaces powered by @mariozechner/pi-ai", "type": "module", "main": "dist/index.js", @@ -18,8 +18,8 @@ }, "dependencies": { "@lmstudio/sdk": "^1.5.0", - "@mariozechner/pi-ai": "^0.7.22", - "@mariozechner/pi-tui": "^0.7.22", + "@mariozechner/pi-ai": "^0.7.23", + "@mariozechner/pi-tui": "^0.7.23", "docx-preview": "^0.3.7", "jszip": "^3.10.1", "lucide": "^0.544.0",