Editors, themes and composer shapes
Luis likes Review Desk’s separation between data and presentation. His next goal is a more comfortable authoring environment: a phrase shortcut, a theme picker and a quieter composer frame.
He chooses independent UI conveniences rather than changing the review tool’s meaning.
The examples in this chapter are new complete exercises, not additional files in the observed downloadable suite.
Milestone: insert text without submitting it
Complete TypeScript exercise—save as phrase-key.ts:
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
export default function phraseKey(pi: ExtensionAPI): void {
pi.registerShortcut("ctrl+shift+y", {
description: "Paste a fictional-observation prefix",
handler(ctx) {
if (ctx.mode !== "tui") return;
ctx.ui.pasteToEditor("Fictional observation: ");
},
});
}
Terminal shell—from the directory containing that exercise:
omp --no-extensions -e ./phrase-key.ts
Expected checkpoint: the key chord pastes text through the editor’s paste handling. It does not submit a prompt or call a provider.
The custom host already uses ctrl+shift+x for its built-in autoresearch overlay. This exercise uses ctrl+shift+y instead: a valid chord can still lose to a later registration. Verify the final handler in your host, not merely that your factory registered it.
setEditorText() replaces the editor text. pasteToEditor() follows the TUI paste path, including large-paste handling. getEditorText() reads the current TUI text.
RPC’s pasteToEditor() falls back to a set_editor_text request. Its synchronous getEditorText() returns an empty string; the remote host must track its own composer state.
Exercise: should a timer call pasteToEditor() to force an agent turn?
Answer: no. Editor mutation is not prompt submission. Use an explicit message API if an agent turn is intended, with the appropriate ownership and cost policy.
Milestone: select a theme that actually exists
Luis does not hard-code a theme name or invent a theme-file schema. He asks the host for its available themes.
Complete TypeScript exercise—save as theme-picker.ts:
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
export default function themePicker(pi: ExtensionAPI): void {
pi.registerCommand("workbook-theme", {
description: "Choose an available TUI theme",
async handler(_args, ctx) {
if (ctx.mode !== "tui") {
pi.sendMessage(
{ customType: "workbook.theme", content: "Theme selection requires the TUI.", display: true },
{ triggerTurn: false },
);
return;
}
const themes = await ctx.ui.getAllThemes();
const choice = await ctx.ui.select("Workbook theme", themes.map(theme => theme.name));
if (choice === undefined) return;
const result = await ctx.ui.setTheme(choice);
ctx.ui.notify(
result.success ? `Theme selected: ${choice}` : result.error ?? "Theme selection failed.",
result.success ? "info" : "warning",
);
},
});
}
Human OMP slash command, after loading the file:
/workbook-theme
getTheme(name) loads a theme without selecting it. ui.theme is the current styling object.
Although the type permits setTheme(Theme), the supplied TUI implementation accepts names and returns failure for a direct theme object. RPC and ACP return theme-switching failures and no available theme list.
For authoring a new declarative theme file, start from the matching host’s actual theme schema and a real theme file. That schema is not included in the supplied extension evidence, so this workbook does not invent theme JSON keys.
Exercise: cancel the picker.
Checkpoint: no theme change. A present method or a successful headless no-op would not prove theme switching.
Milestone: register a composer shape
A composer shape owns the editor’s frame, not its domain state.
Complete TypeScript exercise—save as workbook-dock.ts:
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
import type { ComposerStyle } from "@oh-my-pi/pi-tui";
const style: ComposerStyle = {
id: "workbook-field-dock",
sideBorders: false,
verticalChrome: 1,
statusAttachment: "none",
bottomBar: "full",
bottomBarGap: true,
defaultPromptGutter: "> ",
defaultPaddingX: () => 0,
sideChromeWidth: () => 0,
renderTop: ({ box, width, borderColor }) =>
borderColor(box.horizontal.repeat(width)),
renderRow: ({ gutter, text, pad }) => [gutter + text + pad],
renderBottom: () => undefined,
};
export default function workbookDock(pi: ExtensionAPI): void {
pi.registerComposerShape({
label: "Workbook Field Dock",
description: "A single rule above the prompt",
style,
});
}
Load it explicitly, then choose Workbook Field Dock in Appearance → Composer Shape.
Registration adds a choice; it does not automatically select it.
The shape contract includes:
id, persisted ascomposer.shape;sideBorders, which affects cursor reserve, IME behavior and scrollbar layout;verticalChrome, the exact fixed chrome-row count;statusAttachment:top-border,top-rule-chipornone;bottomBar:none,leftorfull;bottomBarGap;defaultPromptGutter;defaultPaddingX()andsideChromeWidth();renderTop(),renderRow()andrenderBottom().
Renderer contexts supply width, padding, box glyphs and border/accent/surface styling functions. Row contexts also supply the already-rendered gutter, text, pad, last-row information, cursor overflow, IME-safe-tail state and scrollbar-thumb state.
Preserve those prepared content pieces. Do not reflow or arbitrarily truncate them inside frame code. Normal rows must occupy the expected visible width; ANSI bytes are not visible columns.
Built-in IDs cannot be replaced:
box, claude, pi, borderless, rule, field, rail.
If the configured extension shape is unavailable, the editor falls back to box.
Exercise: test the shape at a narrow width and with a long line ending at the cursor.
Checkpoint: no unexpected wrapping caused by frame-width miscalculation. A pure renderRow() unit test alone does not establish live editor/IME behavior.
Milestone: replace the editor only when a shortcut is insufficient
A custom editor must be a CustomEditor subclass, not merely a plain TUI Editor or an arbitrary component. The host expects application action keys and callbacks.
This source-checkout exercise uses the supplied class’s exact repository location. Save it at the matching repository root and use a host built from that same checkout; do not mix a copied source class with an unrelated binary.
Complete TypeScript source-checkout exercise—save as workbook-editor.ts:
import type { ExtensionAPI } from "./packages/coding-agent/src/extensibility/extensions/types";
import { CustomEditor } from "./packages/coding-agent/src/modes/components/custom-editor";
import { matchesKey } from "@oh-my-pi/pi-tui";
class FictionalEditor extends CustomEditor {
override handleInput(data: string): void {
if (matchesKey(data, "ctrl+shift+y")) {
this.insertText("[fictional] ");
this.tui?.requestRender();
return;
}
super.handleInput(data);
}
}
export default function workbookEditor(pi: ExtensionAPI): void {
pi.registerCommand("workbook-editor", {
description: "Use the fictional-prefix editor: on | off",
async handler(args, ctx) {
if (ctx.mode !== "tui") {
ctx.ui.notify("Custom editors require the TUI.", "warning");
return;
}
if (args.trim() === "on") {
ctx.ui.setEditorComponent(
(tui, theme, keybindings) => new FictionalEditor(tui, theme, keybindings),
);
} else if (args.trim() === "off") {
ctx.ui.setEditorComponent(undefined);
} else {
ctx.ui.notify("Use /workbook-editor on or /workbook-editor off.", "warning");
}
},
});
}
Terminal shell—from the matching source repository root:
omp --no-extensions -e ./workbook-editor.ts
Do not load this and the phrase-key exercise together: they intentionally demonstrate two ways to own the same chord.
Expected checkpoint: ordinary typing and application shortcuts still pass to super.handleInput(). Turning the editor off restores the default editor.
Custom input behavior needs real composer tests, including paste, Escape, autocomplete and submission. Avoid adding a global raw-input listener merely to implement an editor-local key.
Autocomplete, terminal listeners and renderers
addAutocompleteProvider(factory) wraps the current provider. A good wrapper:
- handles only its own prefix;
- delegates other suggestions and completion application;
- preserves built-in slash/file completion;
- tolerates being reapplied when command metadata refreshes.
The supplied controller stacks factories in registration order. Contract tests cover healthy wrappers surviving a broken sibling factory. Headless adapters accept and ignore these factories.
onTerminalInput(handler) is lower-level. A handler may return { consume: true } or replacement data; it returns an unsubscribe function. Use it only in TUI mode, clean it up, and do not confuse terminal input with desktop-wide keystroke control.
getToolsExpanded() and setToolsExpanded() control TUI tool-output expansion. RPC/ACP return false and ignore the setter.
A supplemental thinking renderer can add presentation after already-visible thinking text.
Complete TypeScript exercise—save as thinking-length.ts:
import type { ExtensionFactory } from "@oh-my-pi/pi-coding-agent";
import { Container, Text } from "@oh-my-pi/pi-tui";
const extension: ExtensionFactory = pi => {
pi.registerAssistantThinkingRenderer((context, theme) => {
const container = new Container();
container.addChild(new Text(theme.fg("dim", `thinking chars: ${context.text.length}`), 0, 0));
return container;
});
};
export default extension;
Its context includes contentIndex, thinkingIndex, visible text and requestRender(). It must not mutate the message. It does not reveal otherwise unavailable reasoning, and it does not change provider context.
Custom message renderers, thinking renderers and tool renderers are different extension points. Review Desk uses the first; Seed Desk stage 3 uses a tool-result renderer.
Source, snapshot 2026-08-29: packages/tui/src/components/composer/types.ts, ComposerStyle; packages/coding-agent/src/modes/components/custom-editor.ts, CustomEditor; packages/coding-agent/src/modes/controllers/extension-ui-controller.ts; packages/coding-agent/test/issue-4919-extension-autocomplete-provider.test.ts; packages/coding-agent/examples/extensions/thinking-note.ts.
Extensions inside those boundaries · Source chapter: extensions/editors-themes-and-composer-shapes. Original evidence remains scoped to its recorded snapshot.