mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 02:04:05 +00:00
Release v0.11.4
This commit is contained in:
parent
285c657b70
commit
e25420a4c8
17 changed files with 154 additions and 102 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue