Reviewed closed GitHub IME positioning bug.
UnderstandIdentified fix and examined code changes.
UnderstandAdded documentation on focus propagation pattern.
EditCommitted and pushed documentation updates.
VerifyAnalyze GitHub issue(s): https://github.com/badlogic/pi-mono/issues/827
For each issue:
Read the issue in full, including all comments and linked issues/PRs.
For bugs:
For feature requests:
Do NOT implement unless explicitly asked. Analyze and propose only.
The user wants me to analyze GitHub issue #827. Let me fetch the issue details first.
>gh issue view 827 --repo badlogic/pi-mono --json title,body,comments,labels,state{"body":"### What happened?\n\nAs the title says. Show/Hide hardware cursor doesn't help, either.\n\n### Steps to reproduce\n\ntype `/` and then use Chinese IME to filter the options \n\n<img width=\"1050\" height=\"252\" alt=\"Image\" src=\"https://github.com/user-attachments/assets/dc691be3-bfd6-4303-a84b-d7e68de0756f\" />\n\nor use Chinese IME to filter the options in `/model` model selector\n\n<img width=\"1052\" height=\"389\" alt=\"Image\" src=\"https://github.com/user-attachments/assets/179c7f13-a60b-4938-abc8-bff1cd4a9b46\" />\n\n### Expected behavior\n\n_No response_\n\n### Version\n\n0.49.0","comments":[],"labels":[{"id":"LA_kwDOPbFNk88AAAACHWTBxw","name":"bug","description":"Something isn't working","color":"d73a4a"}],"state":"CLOSED","title":"When filtering menu options by typing with IME, the candidate window appears in the wrong position."}The issue is about IME (Input Method Editor) candidate window appearing in the wrong position when typing with Chinese IME in pi's menu filtering. The issue is already CLOSED.
This is a bug related to the TUI (Terminal User Interface) component, specifically the menu filtering functionality. The IME candidate window position is typically controlled by the cursor position in the terminal.
Let me check if this issue was fixed by looking at recent commits or related code. First, let me understand the architecture by looking at relevant files.
The issue involves:
/ command menu/model model selectorLet me look at the TUI code to understand how menus work and where cursor positioning happens.
The issue is already CLOSED, but let me analyze it anyway to understand what was involved.
Issue #827: IME candidate window appears in wrong position when filtering menu options
Problem: When using Chinese IME to filter menu options (via / command or /model selector), the IME candidate window appears in the wrong position on screen.
Let me check how this was resolved and understand the relevant code:
>cd /Users/badlogic/workspaces/pi-mono && git log --oneline --all --grep="827" | head -20b4f833c2 fix(coding-agent): fix IME candidate window positioning in menu selectors>cd /Users/badlogic/workspaces/pi-mono && git log --oneline --all --grep="IME" | head -20b4f833c2 fix(coding-agent): fix IME candidate window positioning in menu selectors
673916f6 fix(tui): always position cursor for IME
07fad136 feat(tui): hardware cursor positioning for IME support
d70edf57 fix(coding-agent): detect image MIME via file-type (#205)
af0297cd Add onCompleted callback system for guaranteed console log delivery
4d2ca6ab Add artifact message persistence for session reconstruction>cd /Users/badlogic/workspaces/pi-mono && git log --oneline --all --grep="candidate" | head -20b4f833c2 fix(coding-agent): fix IME candidate window positioning in menu selectorsFound it. Commit b4f833c2 fixed this issue. Let me look at what was changed.
Found the fix. Let me examine what was changed:
>cd /Users/badlogic/workspaces/pi-mono && git show b4f833c2 --statcommit b4f833c2592f2618d546340e22e37e827a0ddaa5
Author: Mario Zechner <[REDACTED]>
Date: Sun Jan 18 17:22:40 2026 +0100
fix(coding-agent): fix IME candidate window positioning in menu selectors
Components with search inputs now implement Focusable interface and
propagate focus state to their child Input components. This allows
the hardware cursor to be positioned correctly for IME candidate
window placement.
Affected components:
- ModelSelectorComponent
- ScopedModelsSelectorComponent
- SessionSelectorComponent (and SessionList)
- ExtensionInputComponent
- LoginDialogComponent
- TreeSelectorComponent (and LabelInput)
fixes #827
packages/coding-agent/CHANGELOG.md | 4 +++
.../interactive/components/extension-input.ts | 14 ++++++++--
.../modes/interactive/components/login-dialog.ts | 14 ++++++++--
.../modes/interactive/components/model-selector.ts | 23 ++++++++++++++--
.../components/scoped-models-selector.ts | 13 ++++++++-
.../interactive/components/session-selector.ts | 25 +++++++++++++++--
.../modes/interactive/components/tree-selector.ts | 31 ++++++++++++++++++++--
7 files changed, 113 insertions(+), 11 deletions(-)>cd /Users/badlogic/workspaces/pi-mono && git show b4f833c2 --no-statfatal: unrecognized argument: --no-stat
Command exited with code 128Let me get the actual diff.
>cd /Users/badlogic/workspaces/pi-mono && git show b4f833c2 -pcommit b4f833c2592f2618d546340e22e37e827a0ddaa5
Author: Mario Zechner <[REDACTED]>
Date: Sun Jan 18 17:22:40 2026 +0100
fix(coding-agent): fix IME candidate window positioning in menu selectors
Components with search inputs now implement Focusable interface and
propagate focus state to their child Input components. This allows
the hardware cursor to be positioned correctly for IME candidate
window placement.
Affected components:
- ModelSelectorComponent
- ScopedModelsSelectorComponent
- SessionSelectorComponent (and SessionList)
- ExtensionInputComponent
- LoginDialogComponent
- TreeSelectorComponent (and LabelInput)
fixes #827
diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md
index 2a7db2d0..dbc60739 100644
--- a/packages/coding-agent/CHANGELOG.md
+++ b/packages/coding-agent/CHANGELOG.md
@@ -6,6 +6,10 @@
- Share URLs now use hash fragments (`#`) instead of query strings (`?`) to prevent session IDs from being sent to buildwithpi.ai ([#828](https://github.com/badlogic/pi-mono/issues/828))
+### Fixed
+
+- Fixed IME candidate window appearing in wrong position when filtering menus with Input Method Editor (e.g., Chinese IME). Components with search inputs now properly propagate focus state for cursor positioning. ([#827](https://github.com/badlogic/pi-mono/issues/827))
+
## [0.49.0] - 2026-01-17
### Added
diff --git a/packages/coding-agent/src/modes/interactive/components/extension-input.ts b/packages/coding-agent/src/modes/interactive/components/extension-input.ts
index 0b7bcd21..16434633 100644
--- a/packages/coding-agent/src/modes/interactive/components/extension-input.ts
+++ b/packages/coding-agent/src/modes/interactive/components/extension-input.ts
@@ -2,7 +2,7 @@
* Simple text input component for extensions.
*/
-import { Container, getEditorKeybindings, Input, Spacer, Text, type TUI } from "@mariozechner/pi-tui";
+import { Container, type Focusable, getEditorKeybindings, Input, Spacer, Text, type TUI } from "@mariozechner/pi-tui";
import { theme } from "../theme/theme.js";
import { CountdownTimer } from "./countdown-timer.js";
import { DynamicBorder } from "./dynamic-border.js";
@@ -13,7 +13,7 @@ export interface ExtensionInputOptions {
timeout?: number;
}
-export class ExtensionInputComponent extends Container {
+export class ExtensionInputComponent extends Container implements Focusable {
private input: Input;
private onSubmitCallback: (value: string) => void;
private onCancelCallback: () => void;
@@ -21,6 +21,16 @@ export class ExtensionInputComponent extends Container {
private baseTitle: string;
private countdown: CountdownTimer | undefined;
+ // Focusable implementation - propagate to input for IME cursor positioning
+ private _focused = false;
+ get focused(): boolean {
+ return this._focused;
+ }
+ set focused(value: boolean) {
+ this._focused = value;
+ this.input.focused = value;
+ }
+
constructor(
title: string,
_placeholder: string | undefined,
diff --git a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts
index f48f1dab..0f875f7b 100644
--- a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts
+++ b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts
@@ -1,5 +1,5 @@
import { getOAuthProviders } from "@mariozechner/pi-ai";
-import { Container, getEditorKeybindings, Input, Spacer, Text, type TUI } from "@mariozechner/pi-tui";
+import { Container, type Focusable, getEditorKeybindings, Input, Spacer, Text, type TUI } from "@mariozechner/pi-tui";
import { exec } from "child_process";
import { theme } from "../theme/theme.js";
import { DynamicBorder } from "./dynamic-border.js";
@@ -8,7 +8,7 @@ import { keyHint } from "./keybinding-hints.js";
/**
* Login dialog component - replaces editor during OAuth login flow
*/
-export class LoginDialogComponent extends Container {
+export class LoginDialogComponent extends Container implements Focusable {
private contentContainer: Container;
private input: Input;
private tui: TUI;
@@ -16,6 +16,16 @@ export class LoginDialogComponent extends Container {
private inputResolver?: (value: string) => void;
private inputRejecter?: (error: Error) => void;
+ // Focusable implementation - propagate to input for IME cursor positioning
+ private _focused = false;
+ get focused(): boolean {
+ return this._focused;
+ }
+ set focused(value: boolean) {
+ this._focused = value;
+ this.input.focused = value;
+ }
+
constructor(
tui: TUI,
providerId: string,
diff --git a/packages/coding-agent/src/modes/interactive/components/model-selector.ts b/packages/coding-agent/src/modes/interactive/components/model-selector.ts
index b0b2d209..ac913d6c 100644
--- a/packages/coding-agent/src/modes/interactive/components/model-selector.ts
+++ b/packages/coding-agent/src/modes/interactive/components/model-selector.ts
@@ -1,5 +1,14 @@
import { type Model, modelsAreEqual } from "@mariozechner/pi-ai";
-import { Container, fuzzyFilter, getEditorKeybindings, Input, Spacer, Text, type TUI } from "@mariozechner/pi-tui";
+import {
+ Container,
+ type Focusable,
+ fuzzyFilter,
+ getEditorKeybindings,
+ Input,
+ Spacer,
+ Text,
+ type TUI,
+} from "@mariozechner/pi-tui";
import type { ModelRegistry } from "../../../core/model-registry.js";
import type { SettingsManager } from "../../../core/settings-manager.js";
import { theme } from "../theme/theme.js";
@@ -19,8 +28,18 @@ interface ScopedModelItem {
/**
* Component that renders a model selector with search
*/
-export class ModelSelectorComponent extends Container {
+export class ModelSelectorComponent extends Container implements Focusable {
private searchInput: Input;
+
+ // Focusable implementation - propagate to searchInput for IME cursor positioning
+ private _focused = false;
+ get focused(): boolean {
+ return this._focused;
+ }
+ set focused(value: boolean) {
+ this._focused = value;
+ this.searchInput.focused = value;
+ }
private listContainer: Container;
private allModels: ModelItem[] = [];
private filteredModels: ModelItem[] = [];
diff --git a/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts b/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts
index 76631731..3895ddee 100644
--- a/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts
+++ b/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts
@@ -1,6 +1,7 @@
import type { Model } from "@mariozechner/pi-ai";
import {
Container,
+ type Focusable,
fuzzyFilter,
getEditorKeybindings,
Input,
@@ -92,13 +93,23 @@ export interface ModelsCallbacks {
* Component for enabling/disabling models for Ctrl+P cycling.
* Changes are session-only until explicitly persisted with Ctrl+S.
*/
-export class ScopedModelsSelectorComponent extends Container {
+export class ScopedModelsSelectorComponent extends Container implements Focusable {
private modelsById: Map<string, Model<any>> = new Map();
private allIds: string[] = [];
private enabledIds: EnabledIds = null;
private filteredItems: ModelItem[] = [];
private selectedIndex = 0;
private searchInput: Input;
+
+ // Focusable implementation - propagate to searchInput for IME cursor positioning
+ private _focused = false;
+ get focused(): boolean {
+ return this._focused;
+ }
+ set focused(value: boolean) {
+ this._focused = value;
+ this.searchInput.focused = value;
+ }
private listContainer: Container;
private footerText: Text;
private callbacks: ModelsCallbacks;
diff --git a/packages/coding-agent/src/modes/interactive/components/session-selector.ts b/packages/coding-agent/src/modes/interactive/components/session-selector.ts
index e06998b9..918e3dad 100644
--- a/packages/coding-agent/src/modes/interactive/components/session-selector.ts
+++ b/packages/coding-agent/src/modes/interactive/components/session-selector.ts
@@ -2,6 +2,7 @@ import * as os from "node:os";
import {
type Component,
Container,
+ type Focusable,
getEditorKeybindings,
Input,
matchesKey,
@@ -105,7 +106,7 @@ class SessionSelectorHeader implements Component {
/**
* Custom session list component with multi-line items and search
*/
-class SessionList implements Component {
+class SessionList implements Component, Focusable {
private allSessions: SessionInfo[] = [];
private filteredSessions: SessionInfo[] = [];
private selectedIndex: number = 0;
@@ -119,6 +120,16 @@ class SessionList implements Component {
public onToggleSort?: () => void;
private maxVisible: number = 5; // Max sessions visible (each session is 3 lines: msg + metadata + blank)
+ // Focusable implementation - propagate to searchInput for IME cursor positioning
+ private _focused = false;
+ get focused(): boolean {
+ return this._focused;
+ }
+ set focused(value: boolean) {
+ this._focused = value;
+ this.searchInput.focused = value;
+ }
+
constructor(sessions: SessionInfo[], showCwd: boolean, sortMode: SortMode) {
this.allSessions = sessions;
this.filteredSessions = sessions;
@@ -290,7 +301,7 @@ type SessionsLoader = (onProgress?: SessionListProgress) => Promise<SessionInfo[
/**
* Component that renders a session selector
*/
-export class SessionSelectorComponent extends Container {
+export class SessionSelectorComponent extends Container implements Focusable {
private sessionList: SessionList;
private header: SessionSelectorHeader;
private scope: SessionScope = "current";
@@ -302,6 +313,16 @@ export class SessionSelectorComponent extends Container {
private onCancel: () => void;
private requestRender: () => void;
+ // Focusable implementation - propagate to sessionList for IME cursor positioning
+ private _focused = false;
+ get focused(): boolean {
+ return this._focused;
+ }
+ set focused(value: boolean) {
+ this._focused = value;
+ this.sessionList.focused = value;
+ }
+
constructor(
currentSessionsLoader: SessionsLoader,
allSessionsLoader: SessionsLoader,
diff --git a/packages/coding-agent/src/modes/interactive/components/tree-selector.ts b/packages/coding-agent/src/modes/interactive/components/tree-selector.ts
index e44ab7a0..b5694677 100644
--- a/packages/coding-agent/src/modes/interactive/components/tree-selector.ts
+++ b/packages/coding-agent/src/modes/interactive/components/tree-selector.ts
@@ -1,6 +1,7 @@
import {
type Component,
Container,
+ type Focusable,
getEditorKeybindings,
Input,
matchesKey,
@@ -889,12 +890,22 @@ class SearchLine implements Component {
}
/** Label input component shown when editing a label */
-class LabelInput implements Component {
+class LabelInput implements Component, Focusable {
private input: Input;
private entryId: string;
public onSubmit?: (entryId: string, label: string | undefined) => void;
public onCancel?: () => void;
+ // Focusable implementation - propagate to input for IME cursor positioning
+ private _focused = false;
+ get focused(): boolean {
+ return this._focused;
+ }
+ set focused(value: boolean) {
+ this._focused = value;
+ this.input.focused = value;
+ }
+
constructor(entryId: string, currentLabel: string | undefined) {
this.entryId = entryId;
this.input = new Input();
@@ -933,13 +944,26 @@ class LabelInput implements Component {
/**
* Component that renders a session tree selector for navigation
*/
-export class TreeSelectorComponent extends Container {
+export class TreeSelectorComponent extends Container implements Focusable {
private treeList: TreeList;
private labelInput: LabelInput | null = null;
private labelInputContainer: Container;
private treeContainer: Container;
private onLabelChangeCallback?: (entryId: string, label: string | undefined) => void;
+ // Focusable implementation - propagate to labelInput when active for IME cursor positioning
+ private _focused = false;
+ get focused(): boolean {
+ return this._focused;
+ }
+ set focused(value: boolean) {
+ this._focused = value;
+ // Propagate to labelInput when it's active
+ if (this.labelInput) {
+ this.labelInput.focused = value;
+ }
+ }
+
constructor(
tree: SessionTreeNode[],
currentLeafId: string | null,
@@ -997,6 +1021,9 @@ export class TreeSelectorComponent extends Container {
};
this.labelInput.onCancel = () => this.hideLabelInput();
+ // Propagate current focused state to the new labelInput
+ this.labelInput.focused = this._focused;
+
this.treeContainer.clear();
this.labelInputContainer.clear();
this.labelInputContainer.addChild(this.labelInput);Now I have a clear picture of the issue and its fix.
Issue #827 Analysis
Problem: When using IME (Input Method Editor) like Chinese IME to filter menu options in pi's TUI, the IME candidate window appeared in the wrong position on screen. This affected:
/ command menu/model model selectorRoot Cause: The Focusable interface in pi-tui controls hardware cursor positioning for IME support. When a component has an embedded Input component (search field), the focus state needs to propagate from the parent container to the child Input so the cursor is positioned at the actual text input location.
The affected components were:
ModelSelectorComponent - model pickerScopedModelsSelectorComponent - Ctrl+P model cycling selectorSessionSelectorComponent (and internal SessionList) - session pickerExtensionInputComponent - extension text inputsLoginDialogComponent - OAuth login flowTreeSelectorComponent (and internal LabelInput) - session tree navigationThese components contained Input children but didn't implement the Focusable interface or propagate focus state to their inputs.
Fix Applied: Each affected component now:
Focusable interface from @mariozechner/pi-tui_focused field with getter/setterfocused state to its child Input component(s)This ensures the hardware cursor is positioned correctly at the actual text input position, allowing the terminal/OS to display the IME candidate window in the correct location.
The issue is already closed and fixed in commit b4f833c2.
Issue #827: IME candidate window appears in wrong position when filtering menus
Status: Already CLOSED and fixed in commit b4f833c2
The Focusable interface in pi-tui controls hardware cursor positioning for IME support. Components with embedded Input children (search fields) weren't propagating the focused state to their child inputs. Without this propagation, the hardware cursor remained at the wrong position, causing IME candidate windows to appear misplaced.
Each affected component now:
Focusable interface from @mariozechner/pi-tui_focused field with getter/setterfocused state to child Input componentsAffected components:
ModelSelectorComponent - model pickerScopedModelsSelectorComponent - Ctrl+P model cycling selectorSessionSelectorComponent (and internal SessionList) - session pickerExtensionInputComponent - extension text inputsLoginDialogComponent - OAuth login flowTreeSelectorComponent (and internal LabelInput) - session tree navigationPattern used (example from ModelSelectorComponent):
export class ModelSelectorComponent extends Container implements Focusable { private searchInput: Input; // Focusable implementation - propagate to searchInput for IME cursor positioning private _focused = false; get focused(): boolean { return this._focused; } set focused(value: boolean) { this._focused = value; this.searchInput.focused = value; } // ...}This issue is resolved. No further action needed.
>git statusOn branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
ok, we should likely also update @packages/coding-agent/docs/tui.md and @packages/tui/README.md to describe this pattern
The user wants me to update the documentation in two files to describe the pattern of implementing Focusable interface for components with embedded Input children. Let me read both files first to understand their current content and structure.
Now I understand both files. I need to add documentation about the pattern for components with embedded Input children that need to propagate focus state for IME cursor positioning.
Looking at the existing content:
packages/coding-agent/docs/tui.md already has a "Focusable Interface (IME Support)" section that explains the basic Focusable interface. I need to add a subsection about Container Components with Embedded Inputs that explains the pattern of propagating focus state.
packages/tui/README.md doesn't mention the Focusable interface at all. I should add a section about it, including the pattern for container components.
Let me add the documentation for both files.
I'll add documentation about the focus propagation pattern to both files.
Now let me add the Focusable interface documentation to packages/tui/README.md. I'll add it after the Component Interface section since it's a related interface.
I've added the documentation to both files. Let me run a check to make sure everything is good.
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1 | head -50> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && npm run check -w @mariozechner/pi-web-ui
Checked 407 files in 241ms. No fixes applied.
> @mariozechner/pi-web-ui@0.49.0 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 37ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.All checks pass. Let me summarize what I did.
Done. Added documentation about the focus propagation pattern to both files:
packages/coding-agent/docs/tui.md - Added a new subsection "Container Components with Embedded Inputs" under the existing "Focusable Interface (IME Support)" section, with a code example showing how to implement the pattern.
packages/tui/README.md - Added a new "Focusable Interface (IME Support)" section after the Component Interface section, explaining both the basic interface and the container propagation pattern.
Both include:
focused statecommit and push
The user wants me to commit and push the changes. I need to commit only the files I modified, which are the two documentation files.
>cd /Users/badlogic/workspaces/pi-mono && git add packages/coding-agent/docs/tui.md packages/tui/README.md && git commit -m "docs: document Focusable interface propagation pattern for IME support
Container components with embedded Input/Editor children must implement
Focusable and propagate focus state to enable correct IME candidate
window positioning.
Related to #827"Running formatting, linting, and type checking...
> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && npm run check -w @mariozechner/pi-web-ui
Checked 407 files in 242ms. No fixes applied.
> @mariozechner/pi-web-ui@0.49.0 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 40ms. No fixes applied.
Checked 3 files in 7ms. No fixes applied.
✅ All pre-commit checks passed!
[main ad337e30] docs: document Focusable interface propagation pattern for IME support
2 files changed, 82 insertions(+)Good, commit succeeded. Now push.
>cd /Users/badlogic/workspaces/pi-mono && git pushTo https://github.com/badlogic/pi-mono.git
b4f833c2..ad337e30 main -> mainDone. Successfully committed and pushed the documentation changes.
Pushed ad337e30 - documentation for the Focusable interface propagation pattern added to both packages/coding-agent/docs/tui.md and packages/tui/README.md.
export class ModelSelectorComponent extends Container implements Focusable { private searchInput: Input; // Focusable implementation - propagate to searchInput for IME cursor positioning private _focused = false; get focused(): boolean { return this._focused; } set focused(value: boolean) { this._focused = value; this.searchInput.focused = value; } // ...}