This commit is contained in:
2026-07-26 14:02:37 +07:00
parent bc56546b49
commit 367ebc1c7f
171 changed files with 4617 additions and 10402 deletions
+9
View File
@@ -2,6 +2,15 @@
## [Unreleased]
## [0.82.1] - 2026-07-25
## [0.82.0] - 2026-07-24
### Fixed
- Fixed debug and crash logs to use the configured TUI log directory, including `PI_CODING_AGENT_DIR`, instead of always writing under `~/.pi/agent` ([#6958](https://github.com/earendil-works/pi/pull/6958) by [@davidbrai](https://github.com/davidbrai)).
- Fixed narrow terminals crashing when the editor's bottom scroll indicator exceeded the terminal width ([#7015](https://github.com/earendil-works/pi/pull/7015) by [@christianklotz](https://github.com/christianklotz)).
## [0.81.1] - 2026-07-21
## [0.81.0] - 2026-07-21
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@earendil-works/pi-tui",
"version": "0.81.1",
"version": "0.82.1",
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
"type": "module",
"main": "dist/index.js",
+16 -11
View File
@@ -9,7 +9,7 @@ import {
getGraphemeSegmenter,
getWordSegmenter,
isWhitespaceChar,
truncateToWidth,
sliceByColumn,
visibleWidth,
} from "../utils.ts";
import { findWordBackward, findWordForward } from "../word-navigation.ts";
@@ -256,6 +256,17 @@ function buildDebouncePattern(triggerCharacters: string[]): RegExp {
return new RegExp(`(?:^|[ \\t])(?:@(?:"[^"]*|[^\\s]*)|[${escapedWithoutAt.join("")}][^\\s]*)$`);
}
function createScrollBorder(direction: "↑" | "↓", hiddenLineCount: number, width: number): string {
const availableWidth = Math.max(0, width);
const indicator = `─── ${direction} ${hiddenLineCount} more `;
const remaining = availableWidth - visibleWidth(indicator);
if (remaining >= 0) return indicator + "─".repeat(remaining);
const ellipsis = "...".slice(0, availableWidth);
const indicatorWidth = availableWidth - visibleWidth(ellipsis);
return sliceByColumn(indicator, 0, indicatorWidth, true) + ellipsis;
}
export class Editor implements Component, Focusable {
private state: EditorState = {
lines: [""],
@@ -513,13 +524,8 @@ export class Editor implements Component, Focusable {
// Render top border (with scroll indicator if scrolled down)
if (this.scrollOffset > 0) {
const indicator = `─── ↑ ${this.scrollOffset} more `;
const remaining = width - visibleWidth(indicator);
if (remaining >= 0) {
result.push(this.borderColor(indicator + "─".repeat(remaining)));
} else {
result.push(this.borderColor(truncateToWidth(indicator, width)));
}
const border = createScrollBorder("↑", this.scrollOffset, width);
result.push(this.borderColor(border));
} else {
result.push(horizontal.repeat(width));
}
@@ -575,9 +581,8 @@ export class Editor implements Component, Focusable {
// Render bottom border (with scroll indicator if more content below)
const linesBelow = layoutLines.length - (this.scrollOffset + visibleLines.length);
if (linesBelow > 0) {
const indicator = `─── ↓ ${linesBelow} more `;
const remaining = width - visibleWidth(indicator);
result.push(this.borderColor(indicator + "─".repeat(Math.max(0, remaining))));
const border = createScrollBorder("↓", linesBelow, width);
result.push(this.borderColor(border));
} else {
result.push(horizontal.repeat(width));
}
+25
View File
@@ -699,6 +699,31 @@ describe("Editor component", () => {
});
});
describe("Scroll indicators", () => {
it("keeps truncated scroll indicators within width and preserves their color (issue #6962)", () => {
const width = 10;
const borderColor = (text: string) => `\x1b[35m${text}\x1b[39m`;
const editor = new Editor(createTestTUI(width), { ...defaultEditorTheme, borderColor });
editor.setText(Array.from({ length: 20 }, (_, index) => `line ${index}`).join("\n"));
// Render once to initialize wrapping, then move the cursor so content remains above and below the viewport.
editor.render(width);
for (let index = 0; index < 10; index++) editor.handleInput("\x1b[A");
const lines = editor.render(width);
const topBorder = lines[0]!;
const bottomBorder = lines.at(-1)!;
assert.match(stripVTControlCharacters(topBorder), /^─── ↑/);
assert.match(stripVTControlCharacters(bottomBorder), /^─── ↓/);
assert.strictEqual(topBorder, borderColor(stripVTControlCharacters(topBorder)));
assert.strictEqual(bottomBorder, borderColor(stripVTControlCharacters(bottomBorder)));
for (const line of lines) {
assert.strictEqual(visibleWidth(line), width, `line exceeds width ${width}: ${JSON.stringify(line)}`);
}
});
});
describe("Grapheme-aware text wrapping", () => {
it("wraps lines correctly when text contains wide emojis", () => {
const editor = new Editor(createTestTUI(), defaultEditorTheme);