Reviewed GitHub issue and related comments.
UnderstandAnalyzed terminal UI code for cursor handling.
UnderstandImplemented Focusable interface in input components.
BuildConfirmed correct IME candidate window positioning.
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 --json title,body,comments,labels,state --repo badlogic/pi-mono{"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":"OPEN","title":"When filtering menu options by typing with IME, the candidate window appears in the wrong position."}This is a bug report about IME (Input Method Editor) candidate window appearing in the wrong position when filtering menu options in pi. This affects Chinese (and likely other CJK) IME users.
The issue is in the coding-agent package, specifically the TUI components. Let me look at the relevant code files to understand how the menu filtering and cursor positioning works.
First, let me understand the structure of the TUI components and how menus work.
Summary: When using an IME (Input Method Editor, e.g., Chinese) to filter menu options (via / command or /model selector), the IME candidate window appears in the wrong position instead of near the input cursor.
Labels: bug
State: Open
This is a terminal UI issue related to cursor positioning. IME candidate windows position themselves based on the terminal cursor location. Let me examine the relevant TUI code.
>find /Users/badlogic/workspaces/pi-mono/packages -type f -name "*.ts" | xargs grep -l -i "menu\|filter\|autocomplete" | head -20/Users/badlogic/workspaces/pi-mono/packages/coding-agent/test/agent-session-tree-navigation.test.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/test/agent-session-compaction.test.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/test/skills.test.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/test/compaction-extensions.test.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/test/session-manager/labels.test.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/test/compaction.test.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/test/plan-mode-utils.test.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/test/model-registry.test.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/test/rpc.test.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/test/compaction-thinking-model.test.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/test/tools.test.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/test/session-selector-search.test.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/test/block-images.test.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/test/compaction-extensions-example.test.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/dist/core/extensions/types.d.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/dist/core/session-manager.d.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/dist/modes/interactive/components/session-selector-search.d.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/dist/modes/interactive/components/model-selector.d.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/dist/modes/interactive/components/scoped-models-selector.d.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/dist/modes/interactive/components/session-selector.d.ts
xargs: grep: terminated with signal 13; abortingLet me look at the TUI package and coding-agent components related to menus and filtering.
>find /Users/badlogic/workspaces/pi-mono/packages/tui/src -type f -name "*.ts" | head -30/Users/badlogic/workspaces/pi-mono/packages/tui/src/stdin-buffer.ts
/Users/badlogic/workspaces/pi-mono/packages/tui/src/terminal-image.ts
/Users/badlogic/workspaces/pi-mono/packages/tui/src/autocomplete.ts
/Users/badlogic/workspaces/pi-mono/packages/tui/src/tui.ts
/Users/badlogic/workspaces/pi-mono/packages/tui/src/utils.ts
/Users/badlogic/workspaces/pi-mono/packages/tui/src/components/cancellable-loader.ts
/Users/badlogic/workspaces/pi-mono/packages/tui/src/components/image.ts
/Users/badlogic/workspaces/pi-mono/packages/tui/src/components/editor.ts
/Users/badlogic/workspaces/pi-mono/packages/tui/src/components/text.ts
/Users/badlogic/workspaces/pi-mono/packages/tui/src/components/input.ts
/Users/badlogic/workspaces/pi-mono/packages/tui/src/components/markdown.ts
/Users/badlogic/workspaces/pi-mono/packages/tui/src/components/loader.ts
/Users/badlogic/workspaces/pi-mono/packages/tui/src/components/settings-list.ts
/Users/badlogic/workspaces/pi-mono/packages/tui/src/components/spacer.ts
/Users/badlogic/workspaces/pi-mono/packages/tui/src/components/box.ts
/Users/badlogic/workspaces/pi-mono/packages/tui/src/components/truncated-text.ts
/Users/badlogic/workspaces/pi-mono/packages/tui/src/components/select-list.ts
/Users/badlogic/workspaces/pi-mono/packages/tui/src/keys.ts
/Users/badlogic/workspaces/pi-mono/packages/tui/src/editor-component.ts
/Users/badlogic/workspaces/pi-mono/packages/tui/src/keybindings.ts
/Users/badlogic/workspaces/pi-mono/packages/tui/src/terminal.ts
/Users/badlogic/workspaces/pi-mono/packages/tui/src/index.ts
/Users/badlogic/workspaces/pi-mono/packages/tui/src/fuzzy.tsThe TUI package has an autocomplete.ts file. Let me examine that and the select-list.ts component since these are likely involved in the menu filtering.
Now I need to look at the input component and how it handles cursor positioning. The issue is about IME (Input Method Editor) candidate windows appearing at the wrong position. This happens because the terminal cursor isn't positioned correctly when using IME input.
Let me look at the input component and the terminal handling code.
Now I see the issue. I see CURSOR_MARKER being used in the input component. Let me look at the TUI core to understand how this marker is processed and how the cursor positioning works.
Now I understand the architecture. The TUI has a mechanism for cursor positioning using CURSOR_MARKER. When a component is focused and wants IME support, it should:
Focusable interface (which has a focused: boolean property)focused is true, emit CURSOR_MARKER at the cursor position in its render() outputextractCursorPosition() finds this marker, calculates the visual position, and calls positionHardwareCursor() to move the actual terminal cursor thereLooking at the Input component, it already does this correctly. It implements Focusable and emits CURSOR_MARKER when focused.
Now let me look at the model selector and slash command menu components in the coding-agent to see if they also handle this properly.
>find /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src -type f -name "*.ts" | xargs grep -l "SelectList\|model.*selector\|slash.*command\|menu" | head -10/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/interactive-mode.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/thinking-selector.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/theme-selector.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/show-images-selector.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/index.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/model-selector.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/settings-selector.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/theme/theme.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/index.tsI see! The ModelSelectorComponent has an Input component inside it for searching. The Input component already handles the cursor marker correctly, but the issue is that the ModelSelectorComponent doesn't propagate the focused state to its searchInput.
Looking at the code:
ModelSelectorComponent extends ContainersearchInput: Input childhandleInput, it passes it to searchInput.handleInput()Focusable interface, so the searchInput never gets its focused property set to trueThe Input component checks this.focused before emitting the CURSOR_MARKER:
// Hardware cursor marker (zero-width, emitted before fake cursor for IME positioning)const marker = this.focused ? CURSOR_MARKER : "";So if focused is never set to true, the marker is never emitted, and the IME candidate window doesn't know where to appear.
Let me look at how other selectors are implemented and how the focus is managed.
This is a large file. Let me look for the key focus handling to understand how focus is propagated to components with Input fields like ModelSelectorComponent. The issue is that when ModelSelectorComponent is shown and focused, its internal searchInput doesn't have its focused property set to true.
Let me search for where the focus is set when showing the model selector.
>grep -n "setFocus\|ModelSelector\|showModelSelector\|searchInput" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/interactive-mode.ts | head -5080:import { ModelSelectorComponent } from "./components/model-selector.js";
434: this.ui.setFocus(this.editor);
1084: this.ui.setFocus(this.extensionSelector);
1097: this.ui.setFocus(this.editor);
1151: this.ui.setFocus(this.extensionInput);
1164: this.ui.setFocus(this.editor);
1190: this.ui.setFocus(this.extensionEditor);
1202: this.ui.setFocus(this.editor);
1261: this.ui.setFocus(this.editor as Component);
1299: this.ui.setFocus(this.editor);
1345: this.ui.setFocus(component);
1420: this.defaultEditor.onAction("selectModel", () => this.showModelSelector());
2529: this.ui.setFocus(this.editor);
2534: this.ui.setFocus(focus);
2645: this.showModelSelector();
2662: this.showModelSelector(searchTerm);
2705: private showModelSelector(initialSearchInput?: string): void {
2707: const selector = new ModelSelectorComponent(
3127: this.ui.setFocus(dialog);
3142: this.ui.setFocus(this.editor);
3242: this.ui.setFocus(loader);
3249: this.ui.setFocus(this.editor);Now let me see the showModelSelector method to understand how focus is set there:
Now I need to find the showSelector method to understand how it handles focus:
>grep -n "showSelector" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/interactive-mode.ts2525: private showSelector(create: (done: () => void) => { component: Component; focus: Component }): void {
2539: this.showSelector((done) => {
2706: this.showSelector((done) => {
2793: this.showSelector((done) => {
2861: this.showSelector((done) => {
2906: this.showSelector((done) => {
3021: this.showSelector((done) => {
3079: this.showSelector((done) => {Let me look at the showSelector method:
I found the issue! When showSelector is called:
this.ui.setFocus(focus) where focus is the componentFor ModelSelectorComponent, the focus is the selector itself, not its internal searchInput. So when setFocus is called, the TUI sets focused = true on the ModelSelectorComponent, but the ModelSelectorComponent doesn't propagate this to its searchInput.
Looking at the ModelSelectorComponent:
return { component: selector, focus: selector };The selector is returned as the focus target. The TUI then sets selector.focused = true, but ModelSelectorComponent doesn't implement Focusable interface properly (it doesn't have a focused property that propagates to its children).
The Input component inside ModelSelectorComponent (the searchInput) has a focused property that needs to be set to true for the CURSOR_MARKER to be emitted.
The fix needs to:
ModelSelectorComponent implement the Focusable interfacefocused is set on ModelSelectorComponent, propagate it to the searchInputOr alternatively, return selector.getSearchInput() as the focus target instead of selector itself. But this would mean keyboard navigation would go to the input, not the selector. Let me check if this is what we want.
Actually, looking at the code flow:
ModelSelectorComponent.handleInput() handles key inputssearchInput.handleInput() for text inputsearchInput is the component that needs to show the cursor for IMESince ModelSelectorComponent handles all keyboard input via handleInput(), it's the focused component. But for IME cursor positioning, we need searchInput.focused = true so it emits the cursor marker.
The solution is to have ModelSelectorComponent implement Focusable and propagate the focused state to searchInput. I should also check if other selector components have this same pattern.
>grep -l "searchInput\|Input\b" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/*.ts | head -10/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/bordered-loader.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/custom-editor.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/extension-editor.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/extension-input.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/extension-selector.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/footer.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/login-dialog.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/model-selector.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/oauth-selector.ts
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.tsLet me check the other selector components that use Input to see if they have the same pattern and how they handle the focus:
Looking at these components, I can see they all have the same problem. They contain an Input component (or similar) that handles text input, but they don't propagate the focused state to those child components. The Input component checks this.focused before emitting the CURSOR_MARKER.
Let me also look at the BorderedLoader which seems to be used in the issue context (the screenshots show a menu which appears to be rendered using a similar pattern).
Actually, looking at the issue again:
The screenshots show:
Input component for search/filter. The Input component emits CURSOR_MARKER only when focused is true, but none of these parent components propagate the focused state to their child Input components.Root Cause:
ModelSelectorComponent, SessionSelectorComponent, ExtensionInputComponent, etc. contain an Input child for text entryfocused property (if they implement Focusable)focused to their child Input componentsInput component only emits CURSOR_MARKER when focused is trueFix:
All components that contain an Input (or any Focusable component) and delegate text input to it need to propagate the focused state. This can be done by:
Focusable interfacefocused setter that forwards the value to the child InputLet me check which components need to be fixed.
Let me check all components that contain an Input and handle keyboard input:
>grep -rn "searchInput\|private input\|this\.input\|\.handleInput\|Input(" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/*.ts | grep -v "handleInput\(\|extends\|export" | head -40grep: parentheses not balancedLet me fix that command.
>grep -rn "new Input\|searchInput\|private input\b" /Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/*.ts/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/extension-input.ts:17: private input: Input;
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/extension-input.ts:53: this.input = new Input();
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/login-dialog.ts:13: private input: Input;
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/login-dialog.ts:41: this.input = new Input();
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/model-selector.ts:23: private searchInput: Input;
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/model-selector.ts:70: this.searchInput = new Input();
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/model-selector.ts:72: this.searchInput.setValue(initialSearchInput);
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/model-selector.ts:74: this.searchInput.onSubmit = () => {
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/model-selector.ts:80: this.addChild(this.searchInput);
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/model-selector.ts:241: this.searchInput.handleInput(keyData);
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/model-selector.ts:242: this.filterModels(this.searchInput.getValue());
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/model-selector.ts:253: return this.searchInput;
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts:101: private searchInput: Input;
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts:129: this.searchInput = new Input();
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts:130: this.addChild(this.searchInput);
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts:165: const query = this.searchInput.getValue();
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts:258: const targetIds = this.searchInput.getValue() ? this.filteredItems.map((i) => i.fullId) : undefined;
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts:268: const targetIds = this.searchInput.getValue() ? this.filteredItems.map((i) => i.fullId) : undefined;
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts:303: if (this.searchInput.getValue()) {
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts:304: this.searchInput.setValue("");
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts:319: this.searchInput.handleInput(data);
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts:324: return this.searchInput;
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/session-selector.ts:112: private searchInput: Input;
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/session-selector.ts:125: this.searchInput = new Input();
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/session-selector.ts:130: this.searchInput.onSubmit = () => {
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/session-selector.ts:142: this.filterSessions(this.searchInput.getValue());
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/session-selector.ts:148: this.filterSessions(this.searchInput.getValue());
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/session-selector.ts:162: lines.push(...this.searchInput.render(width));
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/session-selector.ts:282: this.searchInput.handleInput(keyData);
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/session-selector.ts:283: this.filterSessions(this.searchInput.getValue());
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/tree-selector.ts:893: private input: Input;
/Users/badlogic/workspaces/pi-mono/packages/coding-agent/src/modes/interactive/components/tree-selector.ts:900: this.input = new Input();Now I have the complete picture. The following components contain an Input child and delegate text input to it:
ModelSelectorComponent - searchInputScopedModelsSelectorComponent - searchInputSessionSelectorComponent - has SessionList which has searchInputExtensionInputComponent - inputLoginDialogComponent - inputTreeSelectorComponent - inputAll of these need to:1. Implement the Focusable interface
2. Propagate the focused state to their child Input component
But wait, let me also check if there's a simpler solution. Let me look at how the TUI.setFocus works and see if we can pass the child component directly.
Actually, looking back at the code flow:1. showSelector calls this.ui.setFocus(focus) where focus is typically the selector component
2. The selector component handles the keyboard input via handleInput()
3. The selector delegates text input to its child Input component
The problem is that the Input component needs focused = true to emit the cursor marker. The current architecture sets focus on the parent, but the child needs to know it's "effectively focused" for IME purposes.
Solution Options:
Option 1 (Component-side fix): Each component with an Input child implements Focusable and propagates the focused state to the child.
Option 2 (TUI-side enhancement): The TUI could automatically find nested Focusable children and set their focused state too. This is more complex and would require traversing the component tree.
Option 3 (Return the Input as focus target): Change showSelector patterns to return the Input as the focus target, not the parent. But this breaks keyboard navigation since the parent handles arrow keys, etc.
Option 1 seems most straightforward—each component that wraps an Input simply needs to implement Focusable and forward the focused state down. I'll add the interface to all six components and have each one pass the focused flag to its child Input component.
Now I have all the information needed for the analysis.
Issue: IME candidate window appears in wrong position when filtering menus with Input Method Editor (e.g., Chinese IME).
Root Cause: Components that contain an Input child for text filtering (like ModelSelectorComponent, SessionSelectorComponent, etc.) don't implement the Focusable interface or propagate the focused state to their child Input component.
The TUI framework uses a cursor marker mechanism for IME positioning:
focused = true, it should emit CURSOR_MARKER at the cursor positionInput component in packages/tui/src/components/input.ts does this correctly:
const marker = this.focused ? CURSOR_MARKER : "";extractCursorPosition() finds this marker and positions the hardware cursor thereThe Problem:
ModelSelectorComponent is focused via ui.setFocus(selector), the TUI sets selector.focused = trueModelSelectorComponent doesn't implement Focusable, so this property is just ignoredsearchInput: Input never gets focused = trueInput never emits CURSOR_MARKERAll components in packages/coding-agent/src/modes/interactive/components/ that contain an Input child:
| Component | Child Input Field |
|---|---|
model-selector.ts | searchInput: Input |
scoped-models-selector.ts | searchInput: Input |
session-selector.ts | SessionList.searchInput: Input |
extension-input.ts | input: Input |
login-dialog.ts | input: Input |
tree-selector.ts | input: Input |
Each affected component needs to:
Focusable interface from @mariozechner/pi-tuifocused property that propagates to the child InputExample fix for ModelSelectorComponent:
import { ..., type Focusable } from "@mariozechner/pi-tui";
export class ModelSelectorComponent extends Container implements Focusable { // ... existing code ... // Add Focusable implementation private _focused = false; get focused(): boolean { return this._focused; } set focused(value: boolean) { this._focused = value; this.searchInput.focused = value; // Propagate to child Input }}The same pattern applies to all other affected components. For SessionSelectorComponent, the focus needs to propagate through to SessionList.searchInput.
packages/coding-agent/src/modes/interactive/components/model-selector.tspackages/coding-agent/src/modes/interactive/components/scoped-models-selector.tspackages/coding-agent/src/modes/interactive/components/session-selector.tspackages/coding-agent/src/modes/interactive/components/extension-input.tspackages/coding-agent/src/modes/interactive/components/login-dialog.tspackages/coding-agent/src/modes/interactive/components/tree-selector.tsimplement
The user wants me to implement the fix for issue #827. I need to modify all the affected components to implement the Focusable interface and propagate the focused state to their child Input components.
Let me start by reading each file in full and then making the necessary edits.
Now I have all the files. Let me also read the session-selector.ts again in full to see the structure better:
Now I have a full picture of all the components. Let me also check the Focusable interface from tui to make sure I import it correctly.The Focusable interface is defined in packages/tui/src/tui.ts:typescript export interface Focusable { focused: boolean; }
Now I need to implement the fix. For each component:
Focusable interface, propagate to searchInput2. ScopedModelsSelectorComponent - Add Focusable interface, propagate to searchInput3. ExtensionInputComponent - Add Focusable interface, propagate to input4. LoginDialogComponent - Add Focusable interface, propagate to input5. SessionSelectorComponent - Add Focusable interface, propagate to sessionList.searchInput through SessionListFocusable interface, propagate to searchInputFocusable interface, propagate to input (via LabelInput when active)For TreeSelectorComponent, the situation is a bit different because:- It has a TreeList for tree navigation (no input field, uses search by typing)- It can show a LabelInput for editing labels (has an Input)Let me check the tree selector again to understand the flow better.
Looking at TreeSelectorComponent:
TreeList and a LabelInput (conditionally)TreeList doesn't have an Input, it uses raw key input for search (stored in searchQuery)LabelInput has an Input componentWhen focus is set on TreeSelectorComponent:- If labelInput is active, it should propagate focus to labelInput.input
TreeList is active, there's no Input component to delegate to. The TreeList handles search directly through key capture and builds searchQuery internally. For typical tree navigation this works fine, but users might still want IME support when searching through the tree.However, TreeList doesn't render an actual Input component—it just displays a SearchLine and accumulates typed characters. IME composition wouldn't integrate well with this direct character-capture approach anyway. So TreeSelectorComponent should propagate focus to labelInput.input when label editing is active, but has nothing to propagate to when treeList is shown. I need to implement Focusable on TreeSelectorComponent and ensure LabelInput's input field receives focus when that mode is active.
Now I'll implement the fix for all affected components. The fix involves implementing the Focusable interface and propagating the focused state to child Input components.
Now I need to add the Focusable implementation to the class.
Now let me update the scoped-models-selector.ts
Now add the Focusable implementation to the class.
Now let me update extension-input.ts
Now add the Focusable implementation to ExtensionInputComponent
Now let me update login-dialog.ts
Now add the Focusable implementation to LoginDialogComponent
Now let me update session-selector.ts. This is more complex because it has an inner class SessionList that has the searchInput, and the outer class SessionSelectorComponent that gets focused. I need to:1. Add Focusable to SessionList (inner class)
2. Add Focusable to SessionSelectorComponent (outer class)
3. Make SessionSelectorComponent propagate focus to sessionList4. Make SessionList propagate focus to searchInput
Now I need to add Focusable to SessionList (inner class)
Now I need to add Focusable to SessionSelectorComponent and propagate to sessionList
Now let me update tree-selector.ts. This one is more complex:
TreeSelectorComponent is the main componentTreeList handles tree navigation (no Input, uses raw key input for search)LabelInput is shown when editing a label (has an Input)When focus is set on TreeSelectorComponent:
labelInput is active, propagate focus to labelInputLabelInput then propagates to its inputActually, looking more carefully at the code, when labelInput is shown, TreeSelectorComponent.handleInput delegates to labelInput.handleInput. But the TUI focus is still on TreeSelectorComponent. So I need to propagate focus through to LabelInput.input.
Let me add Focusable to:
LabelInput (inner class)TreeSelectorComponent (outer class)Now I need to add Focusable to the LabelInput class. Let me find it in the file.
Now I need to add Focusable to TreeSelectorComponent and propagate focus to labelInput when it's active.
Now I also need to ensure that when labelInput is shown, it inherits the current focused state. Let me check the showLabelInput method to add focus propagation there.
Now let me run the check to make sure there are no TypeScript errors.
Now let me run the type checker to verify the changes:
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && npm run check -w @mariozechner/pi-web-ui
Checked 407 files in 247ms. 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 38ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.All checks pass. Now let me update the changelog for the coding-agent package.
All type checks pass. Now let me update the changelog:
I need to add a Fixed entry to the [Unreleased] section.
Now let me commit the changes. I need to be careful to only commit the specific files I modified.
>cd /Users/badlogic/workspaces/pi-mono && git statusOn branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: packages/coding-agent/CHANGELOG.md
modified: packages/coding-agent/src/modes/interactive/components/extension-input.ts
modified: packages/coding-agent/src/modes/interactive/components/login-dialog.ts
modified: packages/coding-agent/src/modes/interactive/components/model-selector.ts
modified: packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts
modified: packages/coding-agent/src/modes/interactive/components/session-selector.ts
modified: packages/coding-agent/src/modes/interactive/components/tree-selector.ts
no changes added to commit (use "git add" and/or "git commit -a")All the files I modified are shown. Let me commit them with a good commit message.
>cd /Users/badlogic/workspaces/pi-mono && git add packages/coding-agent/CHANGELOG.md packages/coding-agent/src/modes/interactive/components/extension-input.ts packages/coding-agent/src/modes/interactive/components/login-dialog.ts packages/coding-agent/src/modes/interactive/components/model-selector.ts packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts packages/coding-agent/src/modes/interactive/components/session-selector.ts packages/coding-agent/src/modes/interactive/components/tree-selector.ts && git commit -m "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"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 239ms. 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 pre-commit checks passed!
[main c16a9833] fix(coding-agent): fix IME candidate window positioning in menu selectors
7 files changed, 113 insertions(+), 11 deletions(-)The commit was successful. Now let me push it.
>cd /Users/badlogic/workspaces/pi-mono && git pushTo https://github.com/badlogic/pi-mono.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'https://github.com/badlogic/pi-mono.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Command exited with code 1There are new changes on the remote. I need to pull and rebase.
>cd /Users/badlogic/workspaces/pi-mono && git pull --rebase && git pushFrom https://github.com/badlogic/pi-mono
964f17b8..8b23c0a4 main -> origin/main
Rebasing (1/1)
Auto-merging packages/coding-agent/CHANGELOG.md
CONFLICT (content): Merge conflict in packages/coding-agent/CHANGELOG.md
error: could not apply c16a9833... fix(coding-agent): fix IME candidate window positioning in menu selectors
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
hint: Disable this message with "git config set advice.mergeConflict false"
Could not apply c16a9833... # fix(coding-agent): fix IME candidate window positioning in menu selectors
Command exited with code 1There's a merge conflict in the CHANGELOG.md file. Let me check what the conflict looks like and resolve it.
I need to resolve the conflict by keeping both entries: the "Changed" entry from HEAD and the "Fixed" entry from my commit.
Now let me add the resolved file and continue the rebase.
>cd /Users/badlogic/workspaces/pi-mono && git add packages/coding-agent/CHANGELOG.md && git rebase --continue[detached HEAD b4f833c2] fix(coding-agent): fix IME candidate window positioning in menu selectors
7 files changed, 113 insertions(+), 11 deletions(-)
Successfully rebased and updated refs/heads/main.Good, the rebase was successful. Now let me push.
>cd /Users/badlogic/workspaces/pi-mono && git pushTo https://github.com/badlogic/pi-mono.git
8b23c0a4..b4f833c2 main -> mainThe push was successful. The fix for issue #827 has been implemented and pushed. Let me summarize what was done.
Done. Here's a summary of the implementation:
Fixed issue #827 by implementing the Focusable interface in all components that contain an Input child for text entry. This ensures the hardware cursor is positioned correctly for IME candidate window placement.
Modified files (7):
model-selector.ts - Added Focusable interface, propagates focus to searchInput
scoped-models-selector.ts - Added Focusable interface, propagates focus to searchInput
session-selector.ts - Added Focusable interface to both SessionList (inner class) and SessionSelectorComponent (outer class). Focus propagates: SessionSelectorComponent → SessionList → searchInput
extension-input.ts - Added Focusable interface, propagates focus to input
login-dialog.ts - Added Focusable interface, propagates focus to input
tree-selector.ts - Added Focusable interface to both LabelInput (inner class) and TreeSelectorComponent (outer class). Focus propagates to LabelInput.input when label editing is active
CHANGELOG.md - Added entry under ## [Unreleased]
The TUI framework uses CURSOR_MARKER for IME cursor positioning. When a component is focused:
component.focused = trueInput component emits CURSOR_MARKER at cursor position only when focused is truePreviously, parent components (like ModelSelectorComponent) were focused but didn't propagate the state to their child Input, so no cursor marker was emitted.
// Hardware cursor marker (zero-width, emitted before fake cursor for IME positioning)const marker = this.focused ? CURSOR_MARKER : "";return { component: selector, focus: selector };const marker = this.focused ? CURSOR_MARKER : "";import { ..., type Focusable } from "@mariozechner/pi-tui";
export class ModelSelectorComponent extends Container implements Focusable { // ... existing code ... // Add Focusable implementation private _focused = false; get focused(): boolean { return this._focused; } set focused(value: boolean) { this._focused = value; this.searchInput.focused = value; // Propagate to child Input }}