Release v0.7.23

This commit is contained in:
Mario Zechner 2025-11-20 11:59:17 +01:00
parent ddb5a4497f
commit b3d4478b61
13 changed files with 189 additions and 40 deletions

28
package-lock.json generated
View file

@ -3195,11 +3195,11 @@
}, },
"packages/agent": { "packages/agent": {
"name": "@mariozechner/pi-agent", "name": "@mariozechner/pi-agent",
"version": "0.7.22", "version": "0.7.23",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@mariozechner/pi-ai": "^0.7.21", "@mariozechner/pi-ai": "^0.7.22",
"@mariozechner/pi-tui": "^0.7.21" "@mariozechner/pi-tui": "^0.7.22"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.3.0", "@types/node": "^24.3.0",
@ -3225,7 +3225,7 @@
}, },
"packages/ai": { "packages/ai": {
"name": "@mariozechner/pi-ai", "name": "@mariozechner/pi-ai",
"version": "0.7.22", "version": "0.7.23",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.61.0", "@anthropic-ai/sdk": "^0.61.0",
@ -3272,11 +3272,11 @@
}, },
"packages/coding-agent": { "packages/coding-agent": {
"name": "@mariozechner/pi-coding-agent", "name": "@mariozechner/pi-coding-agent",
"version": "0.7.22", "version": "0.7.23",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@mariozechner/pi-agent": "^0.7.21", "@mariozechner/pi-agent": "^0.7.22",
"@mariozechner/pi-ai": "^0.7.21", "@mariozechner/pi-ai": "^0.7.22",
"chalk": "^5.5.0", "chalk": "^5.5.0",
"diff": "^8.0.2", "diff": "^8.0.2",
"glob": "^11.0.3" "glob": "^11.0.3"
@ -3319,10 +3319,10 @@
}, },
"packages/pods": { "packages/pods": {
"name": "@mariozechner/pi", "name": "@mariozechner/pi",
"version": "0.7.22", "version": "0.7.23",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@mariozechner/pi-agent": "^0.7.21", "@mariozechner/pi-agent": "^0.7.22",
"chalk": "^5.5.0" "chalk": "^5.5.0"
}, },
"bin": { "bin": {
@ -3345,7 +3345,7 @@
}, },
"packages/proxy": { "packages/proxy": {
"name": "@mariozechner/pi-proxy", "name": "@mariozechner/pi-proxy",
"version": "0.7.22", "version": "0.7.23",
"dependencies": { "dependencies": {
"@hono/node-server": "^1.14.0", "@hono/node-server": "^1.14.0",
"hono": "^4.6.16" "hono": "^4.6.16"
@ -3361,7 +3361,7 @@
}, },
"packages/tui": { "packages/tui": {
"name": "@mariozechner/pi-tui", "name": "@mariozechner/pi-tui",
"version": "0.7.22", "version": "0.7.23",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/mime-types": "^2.1.4", "@types/mime-types": "^2.1.4",
@ -3400,12 +3400,12 @@
}, },
"packages/web-ui": { "packages/web-ui": {
"name": "@mariozechner/pi-web-ui", "name": "@mariozechner/pi-web-ui",
"version": "0.7.22", "version": "0.7.23",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@lmstudio/sdk": "^1.5.0", "@lmstudio/sdk": "^1.5.0",
"@mariozechner/pi-ai": "^0.7.21", "@mariozechner/pi-ai": "^0.7.22",
"@mariozechner/pi-tui": "^0.7.21", "@mariozechner/pi-tui": "^0.7.22",
"docx-preview": "^0.3.7", "docx-preview": "^0.3.7",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"lucide": "^0.544.0", "lucide": "^0.544.0",

View file

@ -1,6 +1,6 @@
{ {
"name": "@mariozechner/pi-agent", "name": "@mariozechner/pi-agent",
"version": "0.7.22", "version": "0.7.23",
"description": "General-purpose agent with transport abstraction, state management, and attachment support", "description": "General-purpose agent with transport abstraction, state management, and attachment support",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",
@ -18,8 +18,8 @@
"prepublishOnly": "npm run clean && npm run build" "prepublishOnly": "npm run clean && npm run build"
}, },
"dependencies": { "dependencies": {
"@mariozechner/pi-ai": "^0.7.22", "@mariozechner/pi-ai": "^0.7.23",
"@mariozechner/pi-tui": "^0.7.22" "@mariozechner/pi-tui": "^0.7.23"
}, },
"keywords": [ "keywords": [
"ai", "ai",

View file

@ -1,6 +1,6 @@
{ {
"name": "@mariozechner/pi-ai", "name": "@mariozechner/pi-ai",
"version": "0.7.22", "version": "0.7.23",
"description": "Unified LLM API with automatic model discovery and provider configuration", "description": "Unified LLM API with automatic model discovery and provider configuration",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",

View file

@ -2,6 +2,21 @@
## [Unreleased] ## [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 ## [0.7.22] - 2025-11-19
### Fixed ### Fixed

View file

@ -1,6 +1,6 @@
{ {
"name": "@mariozechner/pi-coding-agent", "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", "description": "Coding agent CLI with read, bash, edit, write tools and session management",
"type": "module", "type": "module",
"bin": { "bin": {
@ -21,8 +21,8 @@
"prepublishOnly": "npm run clean && npm run build" "prepublishOnly": "npm run clean && npm run build"
}, },
"dependencies": { "dependencies": {
"@mariozechner/pi-agent": "^0.7.22", "@mariozechner/pi-agent": "^0.7.23",
"@mariozechner/pi-ai": "^0.7.22", "@mariozechner/pi-ai": "^0.7.23",
"chalk": "^5.5.0", "chalk": "^5.5.0",
"diff": "^8.0.2", "diff": "^8.0.2",
"glob": "^11.0.3" "glob": "^11.0.3"

View file

@ -219,6 +219,7 @@ Guidelines:
- Use write only for new files or complete rewrites - Use write only for new files or complete rewrites
- Be concise in your responses - Be concise in your responses
- Show file paths clearly when working with files - 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: Documentation:
- Your own documentation (including custom model setup) is at: ${readmePath} - Your own documentation (including custom model setup) is at: ${readmePath}
@ -308,6 +309,25 @@ function loadProjectContextFiles(): Array<{ path: string; content: string }> {
return contextFiles; return contextFiles;
} }
async function checkForNewVersion(currentVersion: string): Promise<string | null> {
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<string | null> { async function selectSession(sessionManager: SessionManager): Promise<string | null> {
return new Promise((resolve) => { return new Promise((resolve) => {
const ui = new TUI(new ProcessTerminal()); const ui = new TUI(new ProcessTerminal());
@ -344,8 +364,9 @@ async function runInteractiveMode(
version: string, version: string,
changelogMarkdown: string | null = null, changelogMarkdown: string | null = null,
modelFallbackMessage: string | null = null, modelFallbackMessage: string | null = null,
newVersion: string | null = null,
): Promise<void> { ): Promise<void> {
const renderer = new TuiRenderer(agent, sessionManager, settingsManager, version, changelogMarkdown); const renderer = new TuiRenderer(agent, sessionManager, settingsManager, version, changelogMarkdown, newVersion);
// Initialize TUI // Initialize TUI
await renderer.init(); await renderer.init();
@ -752,6 +773,17 @@ export async function main(args: string[]) {
// RPC mode - headless operation // RPC mode - headless operation
await runRpcMode(agent, sessionManager); await runRpcMode(agent, sessionManager);
} else if (isInteractive) { } 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<null>((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) // Check if we should show changelog (only in interactive mode, only for new sessions)
let changelogMarkdown: string | null = null; let changelogMarkdown: string | null = null;
if (!parsed.continue && !parsed.resume) { if (!parsed.continue && !parsed.resume) {
@ -789,6 +821,7 @@ export async function main(args: string[]) {
VERSION, VERSION,
changelogMarkdown, changelogMarkdown,
modelFallbackMessage, modelFallbackMessage,
newVersion,
); );
} else { } else {
// CLI mode with messages // CLI mode with messages

View file

@ -51,6 +51,7 @@ export class TuiRenderer {
private onInterruptCallback?: () => void; private onInterruptCallback?: () => void;
private lastSigintTime = 0; private lastSigintTime = 0;
private changelogMarkdown: string | null = null; private changelogMarkdown: string | null = null;
private newVersion: string | null = null;
// Streaming message tracking // Streaming message tracking
private streamingComponent: AssistantMessageComponent | null = null; private streamingComponent: AssistantMessageComponent | null = null;
@ -79,11 +80,13 @@ export class TuiRenderer {
settingsManager: SettingsManager, settingsManager: SettingsManager,
version: string, version: string,
changelogMarkdown: string | null = null, changelogMarkdown: string | null = null,
newVersion: string | null = null,
) { ) {
this.agent = agent; this.agent = agent;
this.sessionManager = sessionManager; this.sessionManager = sessionManager;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.version = version; this.version = version;
this.newVersion = newVersion;
this.changelogMarkdown = changelogMarkdown; this.changelogMarkdown = changelogMarkdown;
this.ui = new TUI(new ProcessTerminal()); this.ui = new TUI(new ProcessTerminal());
this.chatContainer = new Container(); this.chatContainer = new Container();
@ -181,6 +184,23 @@ export class TuiRenderer {
this.ui.addChild(header); this.ui.addChild(header);
this.ui.addChild(new Spacer(1)); 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 // Add changelog if provided
if (this.changelogMarkdown) { if (this.changelogMarkdown) {
this.ui.addChild(new DynamicBorder(chalk.cyan)); this.ui.addChild(new DynamicBorder(chalk.cyan));

View file

@ -1,6 +1,6 @@
{ {
"name": "@mariozechner/pi", "name": "@mariozechner/pi",
"version": "0.7.22", "version": "0.7.23",
"description": "CLI tool for managing vLLM deployments on GPU pods", "description": "CLI tool for managing vLLM deployments on GPU pods",
"type": "module", "type": "module",
"bin": { "bin": {
@ -34,7 +34,7 @@
"node": ">=20.0.0" "node": ">=20.0.0"
}, },
"dependencies": { "dependencies": {
"@mariozechner/pi-agent": "^0.7.22", "@mariozechner/pi-agent": "^0.7.23",
"chalk": "^5.5.0" "chalk": "^5.5.0"
}, },
"devDependencies": {} "devDependencies": {}

View file

@ -1,6 +1,6 @@
{ {
"name": "@mariozechner/pi-proxy", "name": "@mariozechner/pi-proxy",
"version": "0.7.22", "version": "0.7.23",
"type": "module", "type": "module",
"description": "CORS and authentication proxy for pi-ai", "description": "CORS and authentication proxy for pi-ai",
"main": "dist/index.js", "main": "dist/index.js",

View file

@ -1,6 +1,6 @@
{ {
"name": "@mariozechner/pi-tui", "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", "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View file

@ -286,10 +286,12 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
// Match paths - including those ending with /, ~/, or any word at end for forced extraction // Match paths - including those ending with /, ~/, or any word at end for forced extraction
// This regex captures: // This regex captures:
// - Paths starting from beginning of line or after space/quote/equals // - 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) // - The path itself (can include / in the middle)
// - For forced extraction, capture any word at the end // - 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 (!matches) {
// If forced extraction and no matches, return empty string to trigger from current dir // If forced extraction and no matches, return empty string to trigger from current dir
return forceExtract ? "" : null; return forceExtract ? "" : null;
@ -354,10 +356,11 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
expandedPrefix === "../" || expandedPrefix === "../" ||
expandedPrefix === "~" || expandedPrefix === "~" ||
expandedPrefix === "~/" || expandedPrefix === "~/" ||
expandedPrefix === "/" ||
prefix === "@" prefix === "@"
) { ) {
// Complete from specified position // Complete from specified position
if (prefix.startsWith("~")) { if (prefix.startsWith("~") || expandedPrefix === "/") {
searchDir = expandedPrefix; searchDir = expandedPrefix;
} else { } else {
searchDir = join(this.basePath, expandedPrefix); searchDir = join(this.basePath, expandedPrefix);
@ -365,7 +368,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
searchPrefix = ""; searchPrefix = "";
} else if (expandedPrefix.endsWith("/")) { } else if (expandedPrefix.endsWith("/")) {
// If prefix ends with /, show contents of that directory // If prefix ends with /, show contents of that directory
if (prefix.startsWith("~") || (isAtPrefix && expandedPrefix.startsWith("/"))) { if (prefix.startsWith("~") || expandedPrefix.startsWith("/")) {
searchDir = expandedPrefix; searchDir = expandedPrefix;
} else { } else {
searchDir = join(this.basePath, expandedPrefix); searchDir = join(this.basePath, expandedPrefix);
@ -375,7 +378,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
// Split into directory and file prefix // Split into directory and file prefix
const dir = dirname(expandedPrefix); const dir = dirname(expandedPrefix);
const file = basename(expandedPrefix); const file = basename(expandedPrefix);
if (prefix.startsWith("~") || (isAtPrefix && expandedPrefix.startsWith("/"))) { if (prefix.startsWith("~") || expandedPrefix.startsWith("/")) {
searchDir = dir; searchDir = dir;
} else { } else {
searchDir = join(this.basePath, dir); searchDir = join(this.basePath, dir);
@ -392,7 +395,13 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
} }
const fullPath = join(searchDir, entry); 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 // For @ prefix, filter to only show directories and attachable files
if (isAtPrefix && !isDirectory && !isAttachableFile(fullPath)) { if (isAtPrefix && !isDirectory && !isAttachableFile(fullPath)) {
@ -430,6 +439,14 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
const homeRelativeDir = prefix.slice(2); // Remove ~/ const homeRelativeDir = prefix.slice(2); // Remove ~/
const dir = dirname(homeRelativeDir); const dir = dirname(homeRelativeDir);
relativePath = "~/" + (dir === "." ? entry : join(dir, entry)); 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 { } else {
relativePath = join(dirname(prefix), entry); relativePath = join(dirname(prefix), entry);
} }
@ -458,7 +475,7 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
return a.label.localeCompare(b.label); return a.label.localeCompare(b.label);
}); });
return suggestions.slice(0, 10); // Limit to 10 suggestions return suggestions;
} catch (e) { } catch (e) {
// Directory doesn't exist or not accessible // Directory doesn't exist or not accessible
return []; return [];
@ -474,8 +491,8 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
const currentLine = lines[cursorLine] || ""; const currentLine = lines[cursorLine] || "";
const textBeforeCursor = currentLine.slice(0, cursorCol); const textBeforeCursor = currentLine.slice(0, cursorCol);
// Don't trigger if we're in a slash command // Don't trigger if we're typing a slash command at the start of the line
if (textBeforeCursor.startsWith("/") && !textBeforeCursor.includes(" ")) { if (textBeforeCursor.trim().startsWith("/") && !textBeforeCursor.trim().includes(" ")) {
return null; return null;
} }
@ -499,8 +516,8 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
const currentLine = lines[cursorLine] || ""; const currentLine = lines[cursorLine] || "";
const textBeforeCursor = currentLine.slice(0, cursorCol); const textBeforeCursor = currentLine.slice(0, cursorCol);
// Don't trigger if we're in a slash command // Don't trigger if we're typing a slash command at the start of the line
if (textBeforeCursor.startsWith("/") && !textBeforeCursor.includes(" ")) { if (textBeforeCursor.trim().startsWith("/") && !textBeforeCursor.trim().includes(" ")) {
return false; return false;
} }

View file

@ -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 '/'");
}
});
});
});

View file

@ -1,6 +1,6 @@
{ {
"name": "@mariozechner/pi-web-ui", "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", "description": "Reusable web UI components for AI chat interfaces powered by @mariozechner/pi-ai",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
@ -18,8 +18,8 @@
}, },
"dependencies": { "dependencies": {
"@lmstudio/sdk": "^1.5.0", "@lmstudio/sdk": "^1.5.0",
"@mariozechner/pi-ai": "^0.7.22", "@mariozechner/pi-ai": "^0.7.23",
"@mariozechner/pi-tui": "^0.7.22", "@mariozechner/pi-tui": "^0.7.23",
"docx-preview": "^0.3.7", "docx-preview": "^0.3.7",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"lucide": "^0.544.0", "lucide": "^0.544.0",