feat(coding-agent): add composable editor factory access

closes #3935
This commit is contained in:
Mario Zechner
2026-04-29 23:26:25 +02:00
parent c1dd6082ee
commit d698647b12
7 changed files with 36 additions and 7 deletions

View File

@@ -2,6 +2,10 @@
## [Unreleased]
### Added
- Added `ctx.ui.getEditorComponent()` so extensions can wrap the currently configured custom editor factory ([#3935](https://github.com/badlogic/pi-mono/issues/3935)).
### Fixed
- Fixed idle follow-up submission to clear the editor like normal message submission ([#3926](https://github.com/badlogic/pi-mono/issues/3926)).

View File

@@ -2225,6 +2225,10 @@ ctx.ui.setToolsExpanded(wasExpanded);
// Custom editor (vim mode, emacs mode, etc.)
ctx.ui.setEditorComponent((tui, theme, keybindings) => new VimEditor(tui, theme, keybindings));
const currentEditor = ctx.ui.getEditorComponent();
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
new WrappedEditor(tui, theme, keybindings, currentEditor?.(tui, theme, keybindings))
);
ctx.ui.setEditorComponent(undefined); // Restore default editor
// Theme management (see themes.md for creating themes)
@@ -2379,8 +2383,18 @@ export default function (pi: ExtensionAPI) {
- Extend `CustomEditor` (not base `Editor`) to get app keybindings (escape to abort, ctrl+d, model switching)
- Call `super.handleInput(data)` for keys you don't handle
- Factory receives `theme` and `keybindings` from the app
- Use `ctx.ui.getEditorComponent()` before `setEditorComponent()` to wrap the previously configured custom editor
- Pass `undefined` to restore default: `ctx.ui.setEditorComponent(undefined)`
To compose with another extension that already replaced the editor, capture the previous factory before setting yours:
```typescript
const previous = ctx.ui.getEditorComponent();
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
new MyEditor(tui, theme, keybindings, { base: previous?.(tui, theme, keybindings) })
);
```
See [tui.md](tui.md) Pattern 7 for a complete example with mode indicator.
### Message Rendering

View File

@@ -47,6 +47,7 @@ export type {
ContextUsage,
CustomToolCallEvent,
CustomToolResultEvent,
EditorFactory,
EditToolCallEvent,
EditToolResultEvent,
ExecOptions,

View File

@@ -207,6 +207,7 @@ const noOpUIContext: ExtensionUIContext = {
editor: async () => undefined,
addAutocompleteProvider: () => {},
setEditorComponent: () => {},
getEditorComponent: () => undefined,
get theme() {
return theme;
},

View File

@@ -115,6 +115,7 @@ export interface WorkingIndicatorOptions {
/** Wrap the current autocomplete provider with additional behavior. */
export type AutocompleteProviderFactory = (current: AutocompleteProvider) => AutocompleteProvider;
export type EditorFactory = (tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => EditorComponent;
/**
* UI context for extensions to request interactive UI.
@@ -249,9 +250,10 @@ export interface ExtensionUIContext {
* );
* ```
*/
setEditorComponent(
factory: ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => EditorComponent) | undefined,
): void;
setEditorComponent(factory: EditorFactory | undefined): void;
/** Get the currently configured custom editor factory, or undefined when using the default editor. */
getEditorComponent(): EditorFactory | undefined;
/** Get the current theme for styling. */
readonly theme: Theme;

View File

@@ -20,7 +20,6 @@ import type {
AutocompleteItem,
AutocompleteProvider,
EditorComponent,
EditorTheme,
Keybinding,
KeyId,
MarkdownTheme,
@@ -60,6 +59,7 @@ import { type AgentSession, type AgentSessionEvent, parseSkillBlock } from "../.
import { type AgentSessionRuntime, SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
import type {
AutocompleteProviderFactory,
EditorFactory,
ExtensionCommandContext,
ExtensionContext,
ExtensionRunner,
@@ -251,6 +251,7 @@ export class InteractiveMode {
private statusContainer: Container;
private defaultEditor: CustomEditor;
private editor: EditorComponent;
private editorComponentFactory: EditorFactory | undefined;
private autocompleteProvider: AutocompleteProvider | undefined;
private autocompleteProviderWrappers: AutocompleteProviderFactory[] = [];
private fdPath: string | undefined;
@@ -1985,6 +1986,7 @@ export class InteractiveMode {
this.setupAutocompleteProvider();
},
setEditorComponent: (factory) => this.setCustomEditorComponent(factory),
getEditorComponent: () => this.editorComponentFactory,
get theme() {
return theme;
},
@@ -2182,9 +2184,9 @@ export class InteractiveMode {
* Set a custom editor component from an extension.
* Pass undefined to restore the default editor.
*/
private setCustomEditorComponent(
factory: ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => EditorComponent) | undefined,
): void {
private setCustomEditorComponent(factory: EditorFactory | undefined): void {
this.editorComponentFactory = factory;
// Save text from current editor before switching
const currentText = this.editor.getText();

View File

@@ -271,6 +271,11 @@ export async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<neve
// Custom editor components not supported in RPC mode
},
getEditorComponent() {
// Custom editor components not supported in RPC mode
return undefined;
},
get theme() {
return theme;
},