fix(tui): always position cursor for IME

# Conflicts:
#	packages/coding-agent/CHANGELOG.md
This commit is contained in:
Mario Zechner 2026-01-17 11:38:46 +01:00
parent cd43b8a9ca
commit 673916f63c
7 changed files with 87 additions and 11 deletions

View file

@ -5,6 +5,8 @@
### Added
- Export `keyHint`, `appKeyHint`, `editorKey`, `appKey`, `rawKeyHint` for extensions to format keybinding hints consistently
- Added `showHardwareCursor` setting to control cursor visibility while still positioning it for IME support. ([#800](https://github.com/badlogic/pi-mono/pull/800) by [@ghoulr](https://github.com/ghoulr))
- Added `ctx.compact()` and `ctx.getContextUsage()` to extension contexts for programmatic compaction and context usage checks.
## [0.48.0] - 2026-01-16

View file

@ -789,6 +789,7 @@ Global `~/.pi/agent/settings.json` stores persistent preferences:
"autoResize": true,
"blockImages": false
},
"showHardwareCursor": false,
"extensions": ["/path/to/extension.ts"]
}
```
@ -816,6 +817,7 @@ Global `~/.pi/agent/settings.json` stores persistent preferences:
| `terminal.showImages` | Render images inline (supported terminals) | `true` |
| `images.autoResize` | Auto-resize images to 2000x2000 max for better model compatibility | `true` |
| `images.blockImages` | Prevent images from being sent to LLM providers | `false` |
| `showHardwareCursor` | Show terminal cursor while still positioning it for IME support | `false` |
| `doubleEscapeAction` | Action for double-escape with empty editor: `tree` or `branch` | `tree` |
| `editorPaddingX` | Horizontal padding for input editor (0-3) | `0` |
| `extensions` | Additional extension file paths | `[]` |

View file

@ -71,6 +71,7 @@ export interface Settings {
doubleEscapeAction?: "fork" | "tree"; // Action for double-escape with empty editor (default: "tree")
thinkingBudgets?: ThinkingBudgetsSettings; // Custom token budgets for thinking levels
editorPaddingX?: number; // Horizontal padding for input editor (default: 0)
showHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME
}
/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
@ -482,6 +483,15 @@ export class SettingsManager {
this.save();
}
getShowHardwareCursor(): boolean {
return this.settings.showHardwareCursor ?? process.env.PI_HARDWARE_CURSOR === "1";
}
setShowHardwareCursor(enabled: boolean): void {
this.globalSettings.showHardwareCursor = enabled;
this.save();
}
getEditorPaddingX(): number {
return this.settings.editorPaddingX ?? 0;
}

View file

@ -36,6 +36,7 @@ export interface SettingsConfig {
hideThinkingBlock: boolean;
collapseChangelog: boolean;
doubleEscapeAction: "fork" | "tree";
showHardwareCursor: boolean;
editorPaddingX: number;
}
@ -53,6 +54,7 @@ export interface SettingsCallbacks {
onHideThinkingBlockChange: (hidden: boolean) => void;
onCollapseChangelogChange: (collapsed: boolean) => void;
onDoubleEscapeActionChange: (action: "fork" | "tree") => void;
onShowHardwareCursorChange: (enabled: boolean) => void;
onEditorPaddingXChange: (padding: number) => void;
onCancel: () => void;
}
@ -269,9 +271,19 @@ export class SettingsSelectorComponent extends Container {
values: ["true", "false"],
});
// Editor padding toggle (insert after skill-commands)
// Hardware cursor toggle (insert after skill-commands)
const skillCommandsIndex = items.findIndex((item) => item.id === "skill-commands");
items.splice(skillCommandsIndex + 1, 0, {
id: "show-hardware-cursor",
label: "Show hardware cursor",
description: "Show the terminal cursor while still positioning it for IME support",
currentValue: config.showHardwareCursor ? "true" : "false",
values: ["true", "false"],
});
// Editor padding toggle (insert after show-hardware-cursor)
const hardwareCursorIndex = items.findIndex((item) => item.id === "show-hardware-cursor");
items.splice(hardwareCursorIndex + 1, 0, {
id: "editor-padding",
label: "Editor padding",
description: "Horizontal padding for input editor (0-3)",
@ -318,6 +330,9 @@ export class SettingsSelectorComponent extends Container {
case "double-escape-action":
callbacks.onDoubleEscapeActionChange(newValue as "fork" | "tree");
break;
case "show-hardware-cursor":
callbacks.onShowHardwareCursorChange(newValue === "true");
break;
case "editor-padding":
callbacks.onEditorPaddingXChange(parseInt(newValue, 10));
break;

View file

@ -235,7 +235,7 @@ export class InteractiveMode {
) {
this.session = session;
this.version = VERSION;
this.ui = new TUI(new ProcessTerminal());
this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
this.chatContainer = new Container();
this.pendingMessagesContainer = new Container();
this.statusContainer = new Container();
@ -689,6 +689,18 @@ export class InteractiveMode {
shutdown: () => {
this.shutdownRequested = true;
},
getContextUsage: () => this.session.getContextUsage(),
compact: (options) => {
void (async () => {
try {
const result = await this.session.compact(options?.customInstructions);
options?.onComplete?.(result);
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
options?.onError?.(err);
}
})();
},
},
// ExtensionCommandContextActions - for ctx.* in command handlers
{
@ -813,6 +825,18 @@ export class InteractiveMode {
shutdown: () => {
this.shutdownRequested = true;
},
getContextUsage: () => this.session.getContextUsage(),
compact: (options) => {
void (async () => {
try {
const result = await this.session.compact(options?.customInstructions);
options?.onComplete?.(result);
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
options?.onError?.(err);
}
})();
},
});
// Set up the extension shortcut handler on the default editor
@ -2521,6 +2545,7 @@ export class InteractiveMode {
hideThinkingBlock: this.hideThinkingBlock,
collapseChangelog: this.settingsManager.getCollapseChangelog(),
doubleEscapeAction: this.settingsManager.getDoubleEscapeAction(),
showHardwareCursor: this.settingsManager.getShowHardwareCursor(),
editorPaddingX: this.settingsManager.getEditorPaddingX(),
},
{
@ -2589,6 +2614,10 @@ export class InteractiveMode {
onDoubleEscapeActionChange: (action) => {
this.settingsManager.setDoubleEscapeAction(action);
},
onShowHardwareCursorChange: (enabled) => {
this.settingsManager.setShowHardwareCursor(enabled);
this.ui.setShowHardwareCursor(enabled);
},
onEditorPaddingXChange: (padding) => {
this.settingsManager.setEditorPaddingX(padding);
this.defaultEditor.setPaddingX(padding);

View file

@ -2,6 +2,10 @@
## [Unreleased]
### Added
- Added `showHardwareCursor` getter and setter to control cursor visibility while keeping IME positioning active. ([#800](https://github.com/badlogic/pi-mono/pull/800) by [@ghoulr](https://github.com/ghoulr))
## [0.48.0] - 2026-01-16
### Added

View file

@ -208,7 +208,7 @@ export class TUI extends Container {
private hardwareCursorRow = 0; // Actual terminal cursor row (may differ due to IME positioning)
private inputBuffer = ""; // Buffer for parsing terminal responses
private cellSizeQueryPending = false;
private useHardwareCursor = process.env.PI_HARDWARE_CURSOR === "1";
private showHardwareCursor = process.env.PI_HARDWARE_CURSOR === "1";
// Overlay stack for modal components rendered on top of base content
private overlayStack: {
@ -218,9 +218,25 @@ export class TUI extends Container {
hidden: boolean;
}[] = [];
constructor(terminal: Terminal) {
constructor(terminal: Terminal, showHardwareCursor?: boolean) {
super();
this.terminal = terminal;
if (showHardwareCursor !== undefined) {
this.showHardwareCursor = showHardwareCursor;
}
}
getShowHardwareCursor(): boolean {
return this.showHardwareCursor;
}
setShowHardwareCursor(enabled: boolean): void {
if (this.showHardwareCursor === enabled) return;
this.showHardwareCursor = enabled;
if (!enabled) {
this.terminal.hideCursor();
}
this.requestRender();
}
setFocus(component: Component | null): void {
@ -982,12 +998,6 @@ export class TUI extends Container {
* @param totalLines Total number of rendered lines
*/
private positionHardwareCursor(cursorPos: { row: number; col: number } | null, totalLines: number): void {
// PI_HARDWARE_CURSOR=1 enables hardware cursor (disabled by default due to terminal compatibility issues)
if (!this.useHardwareCursor) {
this.terminal.hideCursor();
return;
}
if (!cursorPos || totalLines <= 0) {
this.terminal.hideCursor();
return;
@ -1013,6 +1023,10 @@ export class TUI extends Container {
}
this.hardwareCursorRow = targetRow;
this.terminal.showCursor();
if (this.showHardwareCursor) {
this.terminal.showCursor();
} else {
this.terminal.hideCursor();
}
}
}