mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-21 18:05:11 +00:00
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
This commit is contained in:
parent
8b23c0a45e
commit
b4f833c259
7 changed files with 113 additions and 11 deletions
|
|
@ -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))
|
- 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
|
## [0.49.0] - 2026-01-17
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
* Simple text input component for extensions.
|
* 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 { theme } from "../theme/theme.js";
|
||||||
import { CountdownTimer } from "./countdown-timer.js";
|
import { CountdownTimer } from "./countdown-timer.js";
|
||||||
import { DynamicBorder } from "./dynamic-border.js";
|
import { DynamicBorder } from "./dynamic-border.js";
|
||||||
|
|
@ -13,7 +13,7 @@ export interface ExtensionInputOptions {
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExtensionInputComponent extends Container {
|
export class ExtensionInputComponent extends Container implements Focusable {
|
||||||
private input: Input;
|
private input: Input;
|
||||||
private onSubmitCallback: (value: string) => void;
|
private onSubmitCallback: (value: string) => void;
|
||||||
private onCancelCallback: () => void;
|
private onCancelCallback: () => void;
|
||||||
|
|
@ -21,6 +21,16 @@ export class ExtensionInputComponent extends Container {
|
||||||
private baseTitle: string;
|
private baseTitle: string;
|
||||||
private countdown: CountdownTimer | undefined;
|
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(
|
constructor(
|
||||||
title: string,
|
title: string,
|
||||||
_placeholder: string | undefined,
|
_placeholder: string | undefined,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { getOAuthProviders } from "@mariozechner/pi-ai";
|
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 { exec } from "child_process";
|
||||||
import { theme } from "../theme/theme.js";
|
import { theme } from "../theme/theme.js";
|
||||||
import { DynamicBorder } from "./dynamic-border.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
|
* 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 contentContainer: Container;
|
||||||
private input: Input;
|
private input: Input;
|
||||||
private tui: TUI;
|
private tui: TUI;
|
||||||
|
|
@ -16,6 +16,16 @@ export class LoginDialogComponent extends Container {
|
||||||
private inputResolver?: (value: string) => void;
|
private inputResolver?: (value: string) => void;
|
||||||
private inputRejecter?: (error: Error) => 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(
|
constructor(
|
||||||
tui: TUI,
|
tui: TUI,
|
||||||
providerId: string,
|
providerId: string,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,14 @@
|
||||||
import { type Model, modelsAreEqual } from "@mariozechner/pi-ai";
|
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 { ModelRegistry } from "../../../core/model-registry.js";
|
||||||
import type { SettingsManager } from "../../../core/settings-manager.js";
|
import type { SettingsManager } from "../../../core/settings-manager.js";
|
||||||
import { theme } from "../theme/theme.js";
|
import { theme } from "../theme/theme.js";
|
||||||
|
|
@ -19,8 +28,18 @@ interface ScopedModelItem {
|
||||||
/**
|
/**
|
||||||
* Component that renders a model selector with search
|
* Component that renders a model selector with search
|
||||||
*/
|
*/
|
||||||
export class ModelSelectorComponent extends Container {
|
export class ModelSelectorComponent extends Container implements Focusable {
|
||||||
private searchInput: Input;
|
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 listContainer: Container;
|
||||||
private allModels: ModelItem[] = [];
|
private allModels: ModelItem[] = [];
|
||||||
private filteredModels: ModelItem[] = [];
|
private filteredModels: ModelItem[] = [];
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { Model } from "@mariozechner/pi-ai";
|
import type { Model } from "@mariozechner/pi-ai";
|
||||||
import {
|
import {
|
||||||
Container,
|
Container,
|
||||||
|
type Focusable,
|
||||||
fuzzyFilter,
|
fuzzyFilter,
|
||||||
getEditorKeybindings,
|
getEditorKeybindings,
|
||||||
Input,
|
Input,
|
||||||
|
|
@ -92,13 +93,23 @@ export interface ModelsCallbacks {
|
||||||
* Component for enabling/disabling models for Ctrl+P cycling.
|
* Component for enabling/disabling models for Ctrl+P cycling.
|
||||||
* Changes are session-only until explicitly persisted with Ctrl+S.
|
* 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 modelsById: Map<string, Model<any>> = new Map();
|
||||||
private allIds: string[] = [];
|
private allIds: string[] = [];
|
||||||
private enabledIds: EnabledIds = null;
|
private enabledIds: EnabledIds = null;
|
||||||
private filteredItems: ModelItem[] = [];
|
private filteredItems: ModelItem[] = [];
|
||||||
private selectedIndex = 0;
|
private selectedIndex = 0;
|
||||||
private searchInput: Input;
|
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 listContainer: Container;
|
||||||
private footerText: Text;
|
private footerText: Text;
|
||||||
private callbacks: ModelsCallbacks;
|
private callbacks: ModelsCallbacks;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import * as os from "node:os";
|
||||||
import {
|
import {
|
||||||
type Component,
|
type Component,
|
||||||
Container,
|
Container,
|
||||||
|
type Focusable,
|
||||||
getEditorKeybindings,
|
getEditorKeybindings,
|
||||||
Input,
|
Input,
|
||||||
matchesKey,
|
matchesKey,
|
||||||
|
|
@ -105,7 +106,7 @@ class SessionSelectorHeader implements Component {
|
||||||
/**
|
/**
|
||||||
* Custom session list component with multi-line items and search
|
* Custom session list component with multi-line items and search
|
||||||
*/
|
*/
|
||||||
class SessionList implements Component {
|
class SessionList implements Component, Focusable {
|
||||||
private allSessions: SessionInfo[] = [];
|
private allSessions: SessionInfo[] = [];
|
||||||
private filteredSessions: SessionInfo[] = [];
|
private filteredSessions: SessionInfo[] = [];
|
||||||
private selectedIndex: number = 0;
|
private selectedIndex: number = 0;
|
||||||
|
|
@ -119,6 +120,16 @@ class SessionList implements Component {
|
||||||
public onToggleSort?: () => void;
|
public onToggleSort?: () => void;
|
||||||
private maxVisible: number = 5; // Max sessions visible (each session is 3 lines: msg + metadata + blank)
|
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) {
|
constructor(sessions: SessionInfo[], showCwd: boolean, sortMode: SortMode) {
|
||||||
this.allSessions = sessions;
|
this.allSessions = sessions;
|
||||||
this.filteredSessions = sessions;
|
this.filteredSessions = sessions;
|
||||||
|
|
@ -290,7 +301,7 @@ type SessionsLoader = (onProgress?: SessionListProgress) => Promise<SessionInfo[
|
||||||
/**
|
/**
|
||||||
* Component that renders a session selector
|
* Component that renders a session selector
|
||||||
*/
|
*/
|
||||||
export class SessionSelectorComponent extends Container {
|
export class SessionSelectorComponent extends Container implements Focusable {
|
||||||
private sessionList: SessionList;
|
private sessionList: SessionList;
|
||||||
private header: SessionSelectorHeader;
|
private header: SessionSelectorHeader;
|
||||||
private scope: SessionScope = "current";
|
private scope: SessionScope = "current";
|
||||||
|
|
@ -302,6 +313,16 @@ export class SessionSelectorComponent extends Container {
|
||||||
private onCancel: () => void;
|
private onCancel: () => void;
|
||||||
private requestRender: () => 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(
|
constructor(
|
||||||
currentSessionsLoader: SessionsLoader,
|
currentSessionsLoader: SessionsLoader,
|
||||||
allSessionsLoader: SessionsLoader,
|
allSessionsLoader: SessionsLoader,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import {
|
import {
|
||||||
type Component,
|
type Component,
|
||||||
Container,
|
Container,
|
||||||
|
type Focusable,
|
||||||
getEditorKeybindings,
|
getEditorKeybindings,
|
||||||
Input,
|
Input,
|
||||||
matchesKey,
|
matchesKey,
|
||||||
|
|
@ -889,12 +890,22 @@ class SearchLine implements Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Label input component shown when editing a label */
|
/** Label input component shown when editing a label */
|
||||||
class LabelInput implements Component {
|
class LabelInput implements Component, Focusable {
|
||||||
private input: Input;
|
private input: Input;
|
||||||
private entryId: string;
|
private entryId: string;
|
||||||
public onSubmit?: (entryId: string, label: string | undefined) => void;
|
public onSubmit?: (entryId: string, label: string | undefined) => void;
|
||||||
public onCancel?: () => 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) {
|
constructor(entryId: string, currentLabel: string | undefined) {
|
||||||
this.entryId = entryId;
|
this.entryId = entryId;
|
||||||
this.input = new Input();
|
this.input = new Input();
|
||||||
|
|
@ -933,13 +944,26 @@ class LabelInput implements Component {
|
||||||
/**
|
/**
|
||||||
* Component that renders a session tree selector for navigation
|
* 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 treeList: TreeList;
|
||||||
private labelInput: LabelInput | null = null;
|
private labelInput: LabelInput | null = null;
|
||||||
private labelInputContainer: Container;
|
private labelInputContainer: Container;
|
||||||
private treeContainer: Container;
|
private treeContainer: Container;
|
||||||
private onLabelChangeCallback?: (entryId: string, label: string | undefined) => void;
|
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(
|
constructor(
|
||||||
tree: SessionTreeNode[],
|
tree: SessionTreeNode[],
|
||||||
currentLeafId: string | null,
|
currentLeafId: string | null,
|
||||||
|
|
@ -997,6 +1021,9 @@ export class TreeSelectorComponent extends Container {
|
||||||
};
|
};
|
||||||
this.labelInput.onCancel = () => this.hideLabelInput();
|
this.labelInput.onCancel = () => this.hideLabelInput();
|
||||||
|
|
||||||
|
// Propagate current focused state to the new labelInput
|
||||||
|
this.labelInput.focused = this._focused;
|
||||||
|
|
||||||
this.treeContainer.clear();
|
this.treeContainer.clear();
|
||||||
this.labelInputContainer.clear();
|
this.labelInputContainer.clear();
|
||||||
this.labelInputContainer.addChild(this.labelInput);
|
this.labelInputContainer.addChild(this.labelInput);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue