Release v0.11.4

This commit is contained in:
Mario Zechner 2025-12-01 13:05:12 +01:00
parent 285c657b70
commit e25420a4c8
17 changed files with 154 additions and 102 deletions

View file

@ -59,9 +59,10 @@ These commands:
Complete release process:
1. **Update CHANGELOG.md** (if changes affect coding-agent):
1. **Add changes to CHANGELOG.md** (if changes affect coding-agent):
```bash
# Add your changes to the [Unreleased] section in packages/coding-agent/CHANGELOG.md
# Always add new entries under [Unreleased], never under already-released versions
```
2. **Bump version** (all packages):
@ -71,10 +72,11 @@ Complete release process:
npm run version:major # For breaking changes
```
3. **Update CHANGELOG.md version** (if changes affect coding-agent):
3. **Finalize CHANGELOG.md for release** (if changes affect coding-agent):
```bash
# Move the [Unreleased] section to the new version number with today's date
# Change [Unreleased] to the new version number with today's date
# e.g., ## [0.7.16] - 2025-11-17
# Then add a new empty [Unreleased] section at the top
```
4. **Commit and tag**:
@ -91,6 +93,12 @@ Complete release process:
npm run publish # Publish all packages to npm
```
6. **Add new [Unreleased] section** (for next development cycle):
```bash
# Add a new [Unreleased] section at the top of CHANGELOG.md
# Commit: git commit -am "Add [Unreleased] section"
```
## License
MIT

36
package-lock.json generated
View file

@ -6074,11 +6074,11 @@
},
"packages/agent": {
"name": "@mariozechner/pi-agent-core",
"version": "0.11.3",
"version": "0.11.4",
"license": "MIT",
"dependencies": {
"@mariozechner/pi-ai": "^0.11.2",
"@mariozechner/pi-tui": "^0.11.2"
"@mariozechner/pi-ai": "^0.11.3",
"@mariozechner/pi-tui": "^0.11.3"
},
"devDependencies": {
"@types/node": "^24.3.0",
@ -6108,7 +6108,7 @@
},
"packages/ai": {
"name": "@mariozechner/pi-ai",
"version": "0.11.3",
"version": "0.11.4",
"license": "MIT",
"dependencies": {
"@anthropic-ai/sdk": "^0.61.0",
@ -6149,12 +6149,12 @@
},
"packages/coding-agent": {
"name": "@mariozechner/pi-coding-agent",
"version": "0.11.3",
"version": "0.11.4",
"license": "MIT",
"dependencies": {
"@mariozechner/pi-agent-core": "^0.11.2",
"@mariozechner/pi-ai": "^0.11.2",
"@mariozechner/pi-tui": "^0.11.2",
"@mariozechner/pi-agent-core": "^0.11.3",
"@mariozechner/pi-ai": "^0.11.3",
"@mariozechner/pi-tui": "^0.11.3",
"chalk": "^5.5.0",
"diff": "^8.0.2",
"glob": "^11.0.3"
@ -6191,12 +6191,12 @@
},
"packages/mom": {
"name": "@mariozechner/pi-mom",
"version": "0.11.3",
"version": "0.11.4",
"license": "MIT",
"dependencies": {
"@anthropic-ai/sandbox-runtime": "^0.0.16",
"@mariozechner/pi-agent-core": "^0.11.2",
"@mariozechner/pi-ai": "^0.11.2",
"@mariozechner/pi-agent-core": "^0.11.3",
"@mariozechner/pi-ai": "^0.11.3",
"@sinclair/typebox": "^0.34.0",
"@slack/socket-mode": "^2.0.0",
"@slack/web-api": "^7.0.0",
@ -6234,10 +6234,10 @@
},
"packages/pods": {
"name": "@mariozechner/pi",
"version": "0.11.3",
"version": "0.11.4",
"license": "MIT",
"dependencies": {
"@mariozechner/pi-agent-core": "^0.11.2",
"@mariozechner/pi-agent-core": "^0.11.3",
"chalk": "^5.5.0"
},
"bin": {
@ -6250,7 +6250,7 @@
},
"packages/proxy": {
"name": "@mariozechner/pi-proxy",
"version": "0.11.3",
"version": "0.11.4",
"dependencies": {
"@hono/node-server": "^1.14.0",
"hono": "^4.6.16"
@ -6266,7 +6266,7 @@
},
"packages/tui": {
"name": "@mariozechner/pi-tui",
"version": "0.11.3",
"version": "0.11.4",
"license": "MIT",
"dependencies": {
"@types/mime-types": "^2.1.4",
@ -6310,12 +6310,12 @@
},
"packages/web-ui": {
"name": "@mariozechner/pi-web-ui",
"version": "0.11.3",
"version": "0.11.4",
"license": "MIT",
"dependencies": {
"@lmstudio/sdk": "^1.5.0",
"@mariozechner/pi-ai": "^0.11.2",
"@mariozechner/pi-tui": "^0.11.2",
"@mariozechner/pi-ai": "^0.11.3",
"@mariozechner/pi-tui": "^0.11.3",
"docx-preview": "^0.3.7",
"jszip": "^3.10.1",
"lucide": "^0.544.0",

View file

@ -1,6 +1,6 @@
{
"name": "@mariozechner/pi-agent-core",
"version": "0.11.3",
"version": "0.11.4",
"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.11.3",
"@mariozechner/pi-tui": "^0.11.3"
"@mariozechner/pi-ai": "^0.11.4",
"@mariozechner/pi-tui": "^0.11.4"
},
"keywords": [
"ai",

View file

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

View file

@ -1,5 +1,15 @@
# Changelog
## [0.11.4] - 2025-12-01
### Improved
- **TUI Crash Diagnostics**: When a render error occurs (line exceeds terminal width), all rendered lines are now written to `~/.pi/agent/pi-crash.log` with their indices and visible widths for easier debugging.
### Fixed
- **Session Selector Crash with Wide Characters**: Fixed crash when running `pi -r` to resume sessions containing emojis, CJK characters, or other wide Unicode characters. The session list was using character count instead of visible terminal width for truncation, causing lines to exceed terminal width. Added `truncateToWidth()` utility that properly handles ANSI codes and wide characters. ([#85](https://github.com/badlogic/pi-mono/issues/85))
## [0.11.3] - 2025-12-01
### Added

View file

@ -1,6 +1,6 @@
{
"name": "@mariozechner/pi-coding-agent",
"version": "0.11.3",
"version": "0.11.4",
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
"type": "module",
"bin": {
@ -22,9 +22,9 @@
"prepublishOnly": "npm run clean && npm run build"
},
"dependencies": {
"@mariozechner/pi-agent-core": "^0.11.3",
"@mariozechner/pi-ai": "^0.11.3",
"@mariozechner/pi-tui": "^0.11.3",
"@mariozechner/pi-agent-core": "^0.11.4",
"@mariozechner/pi-ai": "^0.11.4",
"@mariozechner/pi-tui": "^0.11.4",
"chalk": "^5.5.0",
"diff": "^8.0.2",
"glob": "^11.0.3"

View file

@ -1,4 +1,4 @@
import { type Component, Container, Input, Spacer, Text } from "@mariozechner/pi-tui";
import { type Component, Container, Input, Spacer, Text, truncateToWidth } from "@mariozechner/pi-tui";
import type { SessionManager } from "../session-manager.js";
import { theme } from "../theme/theme.js";
import { DynamicBorder } from "./dynamic-border.js";
@ -107,17 +107,17 @@ class SessionList implements Component {
// Normalize first message to single line
const normalizedMessage = session.firstMessage.replace(/\n/g, " ").trim();
// First line: cursor + message
// First line: cursor + message (truncate to visible width)
const cursor = isSelected ? theme.fg("accent", " ") : " ";
const maxMsgWidth = width - 2; // Account for cursor
const truncatedMsg = normalizedMessage.substring(0, maxMsgWidth);
const maxMsgWidth = width - 2; // Account for cursor (2 visible chars)
const truncatedMsg = truncateToWidth(normalizedMessage, maxMsgWidth, "...");
const messageLine = cursor + (isSelected ? theme.bold(truncatedMsg) : truncatedMsg);
// Second line: metadata (dimmed)
// Second line: metadata (dimmed) - also truncate for safety
const modified = formatDate(session.modified);
const msgCount = `${session.messageCount} message${session.messageCount !== 1 ? "s" : ""}`;
const metadata = ` ${modified} · ${msgCount}`;
const metadataLine = theme.fg("dim", metadata);
const metadataLine = theme.fg("dim", truncateToWidth(metadata, width, ""));
lines.push(messageLine);
lines.push(metadataLine);
@ -126,7 +126,8 @@ class SessionList implements Component {
// Add scroll indicator if needed
if (startIndex > 0 || endIndex < this.filteredSessions.length) {
const scrollInfo = theme.fg("muted", ` (${this.selectedIndex + 1}/${this.filteredSessions.length})`);
const scrollText = ` (${this.selectedIndex + 1}/${this.filteredSessions.length})`;
const scrollInfo = theme.fg("muted", truncateToWidth(scrollText, width, ""));
lines.push(scrollInfo);
}

View file

@ -1,6 +1,6 @@
{
"name": "@mariozechner/pi-mom",
"version": "0.11.3",
"version": "0.11.4",
"description": "Slack bot that delegates messages to the pi coding agent",
"type": "module",
"bin": {
@ -21,8 +21,8 @@
},
"dependencies": {
"@anthropic-ai/sandbox-runtime": "^0.0.16",
"@mariozechner/pi-agent-core": "^0.11.3",
"@mariozechner/pi-ai": "^0.11.3",
"@mariozechner/pi-agent-core": "^0.11.4",
"@mariozechner/pi-ai": "^0.11.4",
"@sinclair/typebox": "^0.34.0",
"@slack/socket-mode": "^2.0.0",
"@slack/web-api": "^7.0.0",

View file

@ -1,6 +1,6 @@
{
"name": "@mariozechner/pi",
"version": "0.11.3",
"version": "0.11.4",
"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-core": "^0.11.3",
"@mariozechner/pi-agent-core": "^0.11.4",
"chalk": "^5.5.0"
},
"devDependencies": {}

View file

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

View file

@ -1,6 +1,6 @@
{
"name": "@mariozechner/pi-tui",
"version": "0.11.3",
"version": "0.11.4",
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
"type": "module",
"main": "dist/index.js",

View file

@ -1,4 +1,5 @@
import type { Component } from "../tui.js";
import { truncateToWidth } from "../utils.js";
export interface SelectItem {
value: string;
@ -77,8 +78,8 @@ export class SelectList implements Component {
if (item.description && width > 40) {
// Calculate how much space we have for value + description
const maxValueLength = Math.min(displayValue.length, 30);
const truncatedValue = displayValue.substring(0, maxValueLength);
const maxValueWidth = Math.min(30, width - prefixWidth - 4);
const truncatedValue = truncateToWidth(displayValue, maxValueWidth, "");
const spacing = " ".repeat(Math.max(1, 32 - truncatedValue.length));
// Calculate remaining space for description using visible widths
@ -86,18 +87,18 @@ export class SelectList implements Component {
const remainingWidth = width - descriptionStart - 2; // -2 for safety
if (remainingWidth > 10) {
const truncatedDesc = item.description.substring(0, remainingWidth);
const truncatedDesc = truncateToWidth(item.description, remainingWidth, "");
// Apply selectedText to entire line content
line = this.theme.selectedText("→ " + truncatedValue + spacing + truncatedDesc);
} else {
// Not enough space for description
const maxWidth = width - prefixWidth - 2;
line = this.theme.selectedText("→ " + displayValue.substring(0, maxWidth));
line = this.theme.selectedText("→ " + truncateToWidth(displayValue, maxWidth, ""));
}
} else {
// No description or not enough width
const maxWidth = width - prefixWidth - 2;
line = this.theme.selectedText("→ " + displayValue.substring(0, maxWidth));
line = this.theme.selectedText("→ " + truncateToWidth(displayValue, maxWidth, ""));
}
} else {
const displayValue = item.label || item.value;
@ -105,8 +106,8 @@ export class SelectList implements Component {
if (item.description && width > 40) {
// Calculate how much space we have for value + description
const maxValueLength = Math.min(displayValue.length, 30);
const truncatedValue = displayValue.substring(0, maxValueLength);
const maxValueWidth = Math.min(30, width - prefix.length - 4);
const truncatedValue = truncateToWidth(displayValue, maxValueWidth, "");
const spacing = " ".repeat(Math.max(1, 32 - truncatedValue.length));
// Calculate remaining space for description
@ -114,18 +115,18 @@ export class SelectList implements Component {
const remainingWidth = width - descriptionStart - 2; // -2 for safety
if (remainingWidth > 10) {
const truncatedDesc = item.description.substring(0, remainingWidth);
const truncatedDesc = truncateToWidth(item.description, remainingWidth, "");
const descText = this.theme.description(spacing + truncatedDesc);
line = prefix + truncatedValue + descText;
} else {
// Not enough space for description
const maxWidth = width - prefix.length - 2;
line = prefix + displayValue.substring(0, maxWidth);
line = prefix + truncateToWidth(displayValue, maxWidth, "");
}
} else {
// No description or not enough width
const maxWidth = width - prefix.length - 2;
line = prefix + displayValue.substring(0, maxWidth);
line = prefix + truncateToWidth(displayValue, maxWidth, "");
}
}
@ -136,9 +137,7 @@ export class SelectList implements Component {
if (startIndex > 0 || endIndex < this.filteredItems.length) {
const scrollText = ` (${this.selectedIndex + 1}/${this.filteredItems.length})`;
// Truncate if too long for terminal
const maxWidth = width - 2;
const truncated = scrollText.substring(0, maxWidth);
lines.push(this.theme.scrollInfo(truncated));
lines.push(this.theme.scrollInfo(truncateToWidth(scrollText, width - 2, "")));
}
return lines;

View file

@ -1,5 +1,5 @@
import type { Component } from "../tui.js";
import { visibleWidth } from "../utils.js";
import { truncateToWidth, visibleWidth } from "../utils.js";
/**
* Text component that truncates to fit viewport width
@ -41,46 +41,7 @@ export class TruncatedText implements Component {
}
// Truncate text if needed (accounting for ANSI codes)
let displayText = singleLineText;
const textVisibleWidth = visibleWidth(singleLineText);
if (textVisibleWidth > availableWidth) {
// Need to truncate - walk through the string character by character
let currentWidth = 0;
let truncateAt = 0;
let i = 0;
const ellipsisWidth = 3;
const targetWidth = availableWidth - ellipsisWidth;
while (i < singleLineText.length && currentWidth < targetWidth) {
// Skip ANSI escape sequences (include them in output but don't count width)
if (singleLineText[i] === "\x1b" && singleLineText[i + 1] === "[") {
let j = i + 2;
while (j < singleLineText.length && !/[a-zA-Z]/.test(singleLineText[j])) {
j++;
}
// Include the final letter of the escape sequence
j++;
truncateAt = j;
i = j;
continue;
}
const char = singleLineText[i];
const charWidth = visibleWidth(char);
if (currentWidth + charWidth > targetWidth) {
break;
}
currentWidth += charWidth;
truncateAt = i + 1;
i++;
}
// Add reset code before ellipsis to prevent styling leaking into it
displayText = singleLineText.substring(0, truncateAt) + "\x1b[0m...";
}
const displayText = truncateToWidth(singleLineText, availableWidth);
// Add horizontal padding
const leftPadding = " ".repeat(this.paddingX);

View file

@ -20,4 +20,4 @@ export { TruncatedText } from "./components/truncated-text.js";
export { ProcessTerminal, type Terminal } from "./terminal.js";
export { type Component, Container, TUI } from "./tui.js";
// Utilities
export { visibleWidth } from "./utils.js";
export { truncateToWidth, visibleWidth } from "./utils.js";

View file

@ -2,6 +2,9 @@
* Minimal TUI implementation with differential rendering
*/
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import type { Terminal } from "./terminal.js";
import { visibleWidth } from "./utils.js";
@ -220,7 +223,20 @@ export class TUI extends Container {
if (i > firstChanged) buffer += "\r\n";
buffer += "\x1b[2K"; // Clear current line
if (visibleWidth(newLines[i]) > width) {
throw new Error(`Rendered line ${i} exceeds terminal width\n\n${newLines[i]}`);
// Log all lines to crash file for debugging
const crashLogPath = path.join(os.homedir(), ".pi", "agent", "pi-crash.log");
const crashData = [
`Crash at ${new Date().toISOString()}`,
`Terminal width: ${width}`,
`Line ${i} visible width: ${visibleWidth(newLines[i])}`,
"",
"=== All rendered lines ===",
...newLines.map((line, idx) => `[${idx}] (w=${visibleWidth(line)}) ${line}`),
"",
].join("\n");
fs.mkdirSync(path.dirname(crashLogPath), { recursive: true });
fs.writeFileSync(crashLogPath, crashData);
throw new Error(`Rendered line ${i} exceeds terminal width. Debug log written to ${crashLogPath}`);
}
buffer += newLines[i];
}

View file

@ -285,3 +285,60 @@ export function applyBackgroundToLine(line: string, width: number, bgFn: (text:
const withPadding = line + padding;
return bgFn(withPadding);
}
/**
* Truncate text to fit within a maximum visible width, adding ellipsis if needed.
* Properly handles ANSI escape codes (they don't count toward width).
*
* @param text - Text to truncate (may contain ANSI codes)
* @param maxWidth - Maximum visible width
* @param ellipsis - Ellipsis string to append when truncating (default: "...")
* @returns Truncated text with ellipsis if it exceeded maxWidth
*/
export function truncateToWidth(text: string, maxWidth: number, ellipsis: string = "..."): string {
const textVisibleWidth = visibleWidth(text);
if (textVisibleWidth <= maxWidth) {
return text;
}
const ellipsisWidth = visibleWidth(ellipsis);
const targetWidth = maxWidth - ellipsisWidth;
if (targetWidth <= 0) {
return ellipsis.substring(0, maxWidth);
}
let currentWidth = 0;
let truncateAt = 0;
let i = 0;
while (i < text.length && currentWidth < targetWidth) {
// Skip ANSI escape sequences (include them in output but don't count width)
if (text[i] === "\x1b" && text[i + 1] === "[") {
let j = i + 2;
while (j < text.length && !/[a-zA-Z]/.test(text[j]!)) {
j++;
}
// Include the final letter of the escape sequence
j++;
truncateAt = j;
i = j;
continue;
}
const char = text[i]!;
const charWidth = visibleWidth(char);
if (currentWidth + charWidth > targetWidth) {
break;
}
currentWidth += charWidth;
truncateAt = i + 1;
i++;
}
// Add reset code before ellipsis to prevent styling leaking into it
return text.substring(0, truncateAt) + "\x1b[0m" + ellipsis;
}

View file

@ -1,6 +1,6 @@
{
"name": "@mariozechner/pi-web-ui",
"version": "0.11.3",
"version": "0.11.4",
"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.11.3",
"@mariozechner/pi-tui": "^0.11.3",
"@mariozechner/pi-ai": "^0.11.4",
"@mariozechner/pi-tui": "^0.11.4",
"docx-preview": "^0.3.7",
"jszip": "^3.10.1",
"lucide": "^0.544.0",