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> = 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 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);