Add ExtensionAPI methods, preset example, and TUI documentation improvements

- ExtensionAPI: setModel(), getThinkingLevel(), setThinkingLevel() methods
- New preset.ts example with plan/implement presets for model/thinking/tools switching
- Export all UI components from pi-coding-agent for extension use
- docs/tui.md: Common Patterns section with copy-paste code for SelectList, BorderedLoader, SettingsList, setStatus, setWidget, setFooter
- docs/tui.md: Key Rules section for extension UI development
- docs/extensions.md: Exhaustive example links for all ExtensionAPI methods and events
- System prompt now references docs/tui.md for TUI development

Fixes #509, relates to #347
This commit is contained in:
Mario Zechner 2026-01-06 23:24:23 +01:00
parent c35a18b2b3
commit 59d8b7948c
14 changed files with 850 additions and 13 deletions

View file

@ -39,6 +39,7 @@ export type {
FindToolResultEvent,
GetActiveToolsHandler,
GetAllToolsHandler,
GetThinkingLevelHandler,
GrepToolResultEvent,
LoadExtensionsResult,
// Loaded Extension
@ -70,6 +71,8 @@ export type {
SessionSwitchEvent,
SessionTreeEvent,
SetActiveToolsHandler,
SetModelHandler,
SetThinkingLevelHandler,
// Events - Tool
ToolCallEvent,
ToolCallEventResult,

View file

@ -23,6 +23,7 @@ import type {
ExtensionUIContext,
GetActiveToolsHandler,
GetAllToolsHandler,
GetThinkingLevelHandler,
LoadExtensionsResult,
LoadedExtension,
MessageRenderer,
@ -31,6 +32,8 @@ import type {
SendMessageHandler,
SendUserMessageHandler,
SetActiveToolsHandler,
SetModelHandler,
SetThinkingLevelHandler,
ToolDefinition,
} from "./types.js";
@ -124,6 +127,9 @@ function createExtensionAPI(
setGetActiveToolsHandler: (handler: GetActiveToolsHandler) => void;
setGetAllToolsHandler: (handler: GetAllToolsHandler) => void;
setSetActiveToolsHandler: (handler: SetActiveToolsHandler) => void;
setSetModelHandler: (handler: SetModelHandler) => void;
setGetThinkingLevelHandler: (handler: GetThinkingLevelHandler) => void;
setSetThinkingLevelHandler: (handler: SetThinkingLevelHandler) => void;
setFlagValue: (name: string, value: boolean | string) => void;
} {
let sendMessageHandler: SendMessageHandler = () => {};
@ -132,6 +138,9 @@ function createExtensionAPI(
let getActiveToolsHandler: GetActiveToolsHandler = () => [];
let getAllToolsHandler: GetAllToolsHandler = () => [];
let setActiveToolsHandler: SetActiveToolsHandler = () => {};
let setModelHandler: SetModelHandler = async () => false;
let getThinkingLevelHandler: GetThinkingLevelHandler = () => "off";
let setThinkingLevelHandler: SetThinkingLevelHandler = () => {};
const messageRenderers = new Map<string, MessageRenderer>();
const commands = new Map<string, RegisteredCommand>();
@ -213,6 +222,18 @@ function createExtensionAPI(
setActiveToolsHandler(toolNames);
},
setModel(model) {
return setModelHandler(model);
},
getThinkingLevel() {
return getThinkingLevelHandler();
},
setThinkingLevel(level) {
setThinkingLevelHandler(level);
},
events: eventBus,
} as ExtensionAPI;
@ -241,6 +262,15 @@ function createExtensionAPI(
setSetActiveToolsHandler: (handler: SetActiveToolsHandler) => {
setActiveToolsHandler = handler;
},
setSetModelHandler: (handler: SetModelHandler) => {
setModelHandler = handler;
},
setGetThinkingLevelHandler: (handler: GetThinkingLevelHandler) => {
getThinkingLevelHandler = handler;
},
setSetThinkingLevelHandler: (handler: SetThinkingLevelHandler) => {
setThinkingLevelHandler = handler;
},
setFlagValue: (name: string, value: boolean | string) => {
flagValues.set(name, value);
},
@ -277,6 +307,9 @@ async function loadExtensionWithBun(
setGetActiveToolsHandler,
setGetAllToolsHandler,
setSetActiveToolsHandler,
setSetModelHandler,
setGetThinkingLevelHandler,
setSetThinkingLevelHandler,
setFlagValue,
} = createExtensionAPI(handlers, tools, cwd, extensionPath, eventBus, sharedUI);
@ -299,6 +332,9 @@ async function loadExtensionWithBun(
setGetActiveToolsHandler,
setGetAllToolsHandler,
setSetActiveToolsHandler,
setSetModelHandler,
setGetThinkingLevelHandler,
setSetThinkingLevelHandler,
setFlagValue,
},
error: null,
@ -359,6 +395,9 @@ async function loadExtension(
setGetActiveToolsHandler,
setGetAllToolsHandler,
setSetActiveToolsHandler,
setSetModelHandler,
setGetThinkingLevelHandler,
setSetThinkingLevelHandler,
setFlagValue,
} = createExtensionAPI(handlers, tools, cwd, extensionPath, eventBus, sharedUI);
@ -381,6 +420,9 @@ async function loadExtension(
setGetActiveToolsHandler,
setGetAllToolsHandler,
setSetActiveToolsHandler,
setSetModelHandler,
setGetThinkingLevelHandler,
setSetThinkingLevelHandler,
setFlagValue,
},
error: null,
@ -416,6 +458,9 @@ export function loadExtensionFromFactory(
setGetActiveToolsHandler,
setGetAllToolsHandler,
setSetActiveToolsHandler,
setSetModelHandler,
setGetThinkingLevelHandler,
setSetThinkingLevelHandler,
setFlagValue,
} = createExtensionAPI(handlers, tools, cwd, name, eventBus, sharedUI);
@ -437,6 +482,9 @@ export function loadExtensionFromFactory(
setGetActiveToolsHandler,
setGetAllToolsHandler,
setSetActiveToolsHandler,
setSetModelHandler,
setGetThinkingLevelHandler,
setSetThinkingLevelHandler,
setFlagValue,
};
}

View file

@ -23,6 +23,7 @@ import type {
ExtensionUIContext,
GetActiveToolsHandler,
GetAllToolsHandler,
GetThinkingLevelHandler,
LoadedExtension,
MessageRenderer,
RegisteredCommand,
@ -32,6 +33,8 @@ import type {
SessionBeforeCompactResult,
SessionBeforeTreeResult,
SetActiveToolsHandler,
SetModelHandler,
SetThinkingLevelHandler,
ToolCallEvent,
ToolCallEventResult,
ToolResultEventResult,
@ -115,6 +118,9 @@ export class ExtensionRunner {
getActiveToolsHandler: GetActiveToolsHandler;
getAllToolsHandler: GetAllToolsHandler;
setActiveToolsHandler: SetActiveToolsHandler;
setModelHandler: SetModelHandler;
getThinkingLevelHandler: GetThinkingLevelHandler;
setThinkingLevelHandler: SetThinkingLevelHandler;
newSessionHandler?: NewSessionHandler;
branchHandler?: BranchHandler;
navigateTreeHandler?: NavigateTreeHandler;
@ -148,6 +154,9 @@ export class ExtensionRunner {
ext.setGetActiveToolsHandler(options.getActiveToolsHandler);
ext.setGetAllToolsHandler(options.getAllToolsHandler);
ext.setSetActiveToolsHandler(options.setActiveToolsHandler);
ext.setSetModelHandler(options.setModelHandler);
ext.setGetThinkingLevelHandler(options.getThinkingLevelHandler);
ext.setSetThinkingLevelHandler(options.setThinkingLevelHandler);
}
this.uiContext = options.uiContext ?? noOpUIContext;

View file

@ -8,7 +8,12 @@
* - Interact with the user via UI primitives
*/
import type { AgentMessage, AgentToolResult, AgentToolUpdateCallback } from "@mariozechner/pi-agent-core";
import type {
AgentMessage,
AgentToolResult,
AgentToolUpdateCallback,
ThinkingLevel,
} from "@mariozechner/pi-agent-core";
import type { ImageContent, Model, TextContent, ToolResultMessage } from "@mariozechner/pi-ai";
import type { Component, KeyId, TUI } from "@mariozechner/pi-tui";
import type { Static, TSchema } from "@sinclair/typebox";
@ -619,6 +624,19 @@ export interface ExtensionAPI {
/** Set the active tools by name. */
setActiveTools(toolNames: string[]): void;
// =========================================================================
// Model and Thinking Level
// =========================================================================
/** Set the current model. Returns false if no API key available. */
setModel(model: Model<any>): Promise<boolean>;
/** Get current thinking level. */
getThinkingLevel(): ThinkingLevel;
/** Set thinking level (clamped to model capabilities). */
setThinkingLevel(level: ThinkingLevel): void;
/** Shared event bus for extension communication. */
events: EventBus;
}
@ -670,6 +688,12 @@ export type GetAllToolsHandler = () => string[];
export type SetActiveToolsHandler = (toolNames: string[]) => void;
export type SetModelHandler = (model: Model<any>) => Promise<boolean>;
export type GetThinkingLevelHandler = () => ThinkingLevel;
export type SetThinkingLevelHandler = (level: ThinkingLevel) => void;
/** Loaded extension with all registered items. */
export interface LoadedExtension {
path: string;
@ -687,6 +711,9 @@ export interface LoadedExtension {
setGetActiveToolsHandler: (handler: GetActiveToolsHandler) => void;
setGetAllToolsHandler: (handler: GetAllToolsHandler) => void;
setSetActiveToolsHandler: (handler: SetActiveToolsHandler) => void;
setSetModelHandler: (handler: SetModelHandler) => void;
setGetThinkingLevelHandler: (handler: GetThinkingLevelHandler) => void;
setSetThinkingLevelHandler: (handler: SetThinkingLevelHandler) => void;
setFlagValue: (name: string, value: boolean | string) => void;
}

View file

@ -283,7 +283,7 @@ Documentation:
- Main documentation: ${readmePath}
- Additional docs: ${docsPath}
- Examples: ${examplesPath} (extensions, custom tools, SDK)
- When asked to create: custom models/providers (README.md), extensions (docs/extensions.md, examples/extensions/), themes (docs/theme.md), skills (docs/skills.md)
- When asked to create: custom models/providers (README.md), extensions (docs/extensions.md, examples/extensions/), themes (docs/theme.md), skills (docs/skills.md), TUI components (docs/tui.md - has copy-paste patterns)
- Always read the doc, examples, AND follow .md cross-references before implementing`;
if (appendSection) {

View file

@ -525,6 +525,14 @@ export class InteractiveMode {
return { cancelled: false };
},
setModelHandler: async (model) => {
const key = await this.session.modelRegistry.getApiKey(model);
if (!key) return false;
await this.session.setModel(model);
return true;
},
getThinkingLevelHandler: () => this.session.thinkingLevel,
setThinkingLevelHandler: (level) => this.session.setThinkingLevel(level),
isIdle: () => !this.session.isStreaming,
waitForIdle: () => this.session.agent.waitForIdle(),
abort: () => {

View file

@ -48,6 +48,14 @@ export async function runPrintMode(
getActiveToolsHandler: () => session.getActiveToolNames(),
getAllToolsHandler: () => session.getAllToolNames(),
setActiveToolsHandler: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
setModelHandler: async (model) => {
const key = await session.modelRegistry.getApiKey(model);
if (!key) return false;
await session.setModel(model);
return true;
},
getThinkingLevelHandler: () => session.thinkingLevel,
setThinkingLevelHandler: (level) => session.setThinkingLevel(level),
});
extensionRunner.onError((err) => {
console.error(`Extension error (${err.extensionPath}): ${err.error}`);

View file

@ -244,6 +244,14 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
getActiveToolsHandler: () => session.getActiveToolNames(),
getAllToolsHandler: () => session.getAllToolNames(),
setActiveToolsHandler: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
setModelHandler: async (model) => {
const key = await session.modelRegistry.getApiKey(model);
if (!key) return false;
await session.setModel(model);
return true;
},
getThinkingLevelHandler: () => session.thinkingLevel,
setThinkingLevelHandler: (level) => session.setThinkingLevel(level),
uiContext: createExtensionUIContext(),
hasUI: false,
});