mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 07:04:45 +00:00
Release v0.11.4
This commit is contained in:
parent
285c657b70
commit
e25420a4c8
17 changed files with 154 additions and 102 deletions
14
README.md
14
README.md
|
|
@ -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
36
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue