From 48ea444bc43945bbd35e5c709602a2d637684916 Mon Sep 17 00:00:00 2001 From: ferologics Date: Fri, 16 Jan 2026 22:39:24 +0100 Subject: [PATCH] fix: align input editor with message content padding Adds paddingX option to Editor component and hardcodes paddingX: 1 in coding-agent editors so the cursor/text aligns with chat message content. --- packages/coding-agent/CHANGELOG.md | 1 + .../interactive/components/custom-editor.ts | 6 ++-- .../components/extension-editor.ts | 13 +++++-- .../src/modes/interactive/interactive-mode.ts | 3 +- packages/tui/CHANGELOG.md | 4 +++ packages/tui/src/components/editor.ts | 35 ++++++++++++++----- packages/tui/src/index.ts | 2 +- 7 files changed, 48 insertions(+), 16 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 2c5bac90..e59bfa5a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -15,6 +15,7 @@ ### Fixed - Fixed crash during auto-compaction when summarization fails (e.g., quota exceeded). Now displays error message instead of crashing ([#792](https://github.com/badlogic/pi-mono/issues/792)) +- Input editor now aligns with message content padding ([#791](https://github.com/badlogic/pi-mono/pull/791) by [@ferologics](https://github.com/ferologics)) - Fixed `--no-extensions` flag not preventing extension discovery ([#776](https://github.com/badlogic/pi-mono/issues/776)) - Fixed extension messages rendering twice on startup when `pi.sendMessage({ display: true })` is called during `session_start` ([#765](https://github.com/badlogic/pi-mono/pull/765) by [@dannote](https://github.com/dannote)) - Fixed `PI_CODING_AGENT_DIR` env var not expanding tilde (`~`) to home directory ([#778](https://github.com/badlogic/pi-mono/pull/778) by [@aliou](https://github.com/aliou)) diff --git a/packages/coding-agent/src/modes/interactive/components/custom-editor.ts b/packages/coding-agent/src/modes/interactive/components/custom-editor.ts index 6587a632..5ec7b11b 100644 --- a/packages/coding-agent/src/modes/interactive/components/custom-editor.ts +++ b/packages/coding-agent/src/modes/interactive/components/custom-editor.ts @@ -1,4 +1,4 @@ -import { Editor, type EditorTheme, type TUI } from "@mariozechner/pi-tui"; +import { Editor, type EditorOptions, type EditorTheme, type TUI } from "@mariozechner/pi-tui"; import type { AppAction, KeybindingsManager } from "../../../core/keybindings.js"; /** @@ -15,8 +15,8 @@ export class CustomEditor extends Editor { /** Handler for extension-registered shortcuts. Returns true if handled. */ public onExtensionShortcut?: (data: string) => boolean; - constructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) { - super(tui, theme); + constructor(tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager, options?: EditorOptions) { + super(tui, theme, options); this.keybindings = keybindings; } diff --git a/packages/coding-agent/src/modes/interactive/components/extension-editor.ts b/packages/coding-agent/src/modes/interactive/components/extension-editor.ts index b8c70b6a..a9912f00 100644 --- a/packages/coding-agent/src/modes/interactive/components/extension-editor.ts +++ b/packages/coding-agent/src/modes/interactive/components/extension-editor.ts @@ -7,7 +7,15 @@ import { spawnSync } from "node:child_process"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; -import { Container, Editor, getEditorKeybindings, Spacer, Text, type TUI } from "@mariozechner/pi-tui"; +import { + Container, + Editor, + type EditorOptions, + getEditorKeybindings, + Spacer, + Text, + type TUI, +} from "@mariozechner/pi-tui"; import type { KeybindingsManager } from "../../../core/keybindings.js"; import { getEditorTheme, theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; @@ -27,6 +35,7 @@ export class ExtensionEditorComponent extends Container { prefill: string | undefined, onSubmit: (value: string) => void, onCancel: () => void, + options?: EditorOptions, ) { super(); @@ -44,7 +53,7 @@ export class ExtensionEditorComponent extends Container { this.addChild(new Spacer(1)); // Create editor - this.editor = new Editor(tui, getEditorTheme()); + this.editor = new Editor(tui, getEditorTheme(), options); if (prefill) { this.editor.setText(prefill); } diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 38f2597d..0c48e5ae 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -241,7 +241,7 @@ export class InteractiveMode { this.statusContainer = new Container(); this.widgetContainer = new Container(); this.keybindings = KeybindingsManager.create(); - this.defaultEditor = new CustomEditor(this.ui, getEditorTheme(), this.keybindings); + this.defaultEditor = new CustomEditor(this.ui, getEditorTheme(), this.keybindings, { paddingX: 1 }); this.editor = this.defaultEditor; this.editorContainer = new Container(); this.editorContainer.addChild(this.editor as Component); @@ -1150,6 +1150,7 @@ export class InteractiveMode { this.hideExtensionEditor(); resolve(undefined); }, + { paddingX: 1 }, ); this.editorContainer.clear(); diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index d363f171..0a4be46a 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- `EditorOptions` with optional `paddingX` for horizontal content padding ([#791](https://github.com/badlogic/pi-mono/pull/791) by [@ferologics](https://github.com/ferologics)) + ### Changed - Hardware cursor is now disabled by default for better terminal compatibility. Set `PI_HARDWARE_CURSOR=1` to enable (replaces `PI_NO_HARDWARE_CURSOR=1` which disabled it). diff --git a/packages/tui/src/components/editor.ts b/packages/tui/src/components/editor.ts index 494fe91a..c06d5da8 100644 --- a/packages/tui/src/components/editor.ts +++ b/packages/tui/src/components/editor.ts @@ -242,6 +242,10 @@ export interface EditorTheme { selectList: SelectListTheme; } +export interface EditorOptions { + paddingX?: number; +} + export class Editor implements Component, Focusable { private state: EditorState = { lines: [""], @@ -254,6 +258,7 @@ export class Editor implements Component, Focusable { protected tui: TUI; private theme: EditorTheme; + private paddingX: number = 0; // Store last render width for cursor navigation private lastWidth: number = 80; @@ -287,10 +292,12 @@ export class Editor implements Component, Focusable { public onChange?: (text: string) => void; public disableSubmit: boolean = false; - constructor(tui: TUI, theme: EditorTheme) { + constructor(tui: TUI, theme: EditorTheme, options: EditorOptions = {}) { this.tui = tui; this.theme = theme; this.borderColor = theme.borderColor; + const paddingX = options.paddingX ?? 0; + this.paddingX = Number.isFinite(paddingX) ? Math.max(0, Math.floor(paddingX)) : 0; } setAutocompleteProvider(provider: AutocompleteProvider): void { @@ -364,13 +371,17 @@ export class Editor implements Component, Focusable { } render(width: number): string[] { + const maxPadding = Math.max(0, Math.floor((width - 1) / 2)); + const paddingX = Math.min(this.paddingX, maxPadding); + const contentWidth = Math.max(1, width - paddingX * 2); + // Store width for cursor navigation - this.lastWidth = width; + this.lastWidth = contentWidth; const horizontal = this.borderColor("─"); - // Layout the text - use full width - const layoutLines = this.layoutText(width); + // Layout the text - use content width + const layoutLines = this.layoutText(contentWidth); // Calculate max visible lines: 30% of terminal height, minimum 5 lines const terminalRows = this.tui.terminal.rows; @@ -395,6 +406,8 @@ export class Editor implements Component, Focusable { const visibleLines = layoutLines.slice(this.scrollOffset, this.scrollOffset + maxVisibleLines); const result: string[] = []; + const leftPadding = " ".repeat(paddingX); + const rightPadding = leftPadding; // Render top border (with scroll indicator if scrolled down) if (this.scrollOffset > 0) { @@ -432,7 +445,7 @@ export class Editor implements Component, Focusable { // lineVisibleWidth stays the same - we're replacing, not adding } else { // Cursor is at the end - check if we have room for the space - if (lineVisibleWidth < width) { + if (lineVisibleWidth < contentWidth) { // We have room - add highlighted space const cursor = "\x1b[7m \x1b[0m"; displayText = before + marker + cursor; @@ -458,10 +471,10 @@ export class Editor implements Component, Focusable { } // Calculate padding based on actual visible width - const padding = " ".repeat(Math.max(0, width - lineVisibleWidth)); + const padding = " ".repeat(Math.max(0, contentWidth - lineVisibleWidth)); // Render the line (no side borders, just horizontal lines above and below) - result.push(displayText + padding); + result.push(`${leftPadding}${displayText}${padding}${rightPadding}`); } // Render bottom border (with scroll indicator if more content below) @@ -476,8 +489,12 @@ export class Editor implements Component, Focusable { // Add autocomplete list if active if (this.isAutocompleting && this.autocompleteList) { - const autocompleteResult = this.autocompleteList.render(width); - result.push(...autocompleteResult); + const autocompleteResult = this.autocompleteList.render(contentWidth); + for (const line of autocompleteResult) { + const lineWidth = visibleWidth(line); + const linePadding = " ".repeat(Math.max(0, contentWidth - lineWidth)); + result.push(`${leftPadding}${line}${linePadding}${rightPadding}`); + } } return result; diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index 3bf1f76b..d0c971d9 100644 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -10,7 +10,7 @@ export { // Components export { Box } from "./components/box.js"; export { CancellableLoader } from "./components/cancellable-loader.js"; -export { Editor, type EditorTheme } from "./components/editor.js"; +export { Editor, type EditorOptions, type EditorTheme } from "./components/editor.js"; export { Image, type ImageOptions, type ImageTheme } from "./components/image.js"; export { Input } from "./components/input.js"; export { Loader } from "./components/loader.js";