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

@ -7,8 +7,13 @@
- ExtensionAPI: `setModel()`, `getThinkingLevel()`, `setThinkingLevel()` methods for extensions to change model and thinking level at runtime ([#509](https://github.com/badlogic/pi-mono/issues/509))
- Exported truncation utilities for custom tools: `truncateHead`, `truncateTail`, `truncateLine`, `formatSize`, `DEFAULT_MAX_BYTES`, `DEFAULT_MAX_LINES`, `TruncationOptions`, `TruncationResult`
- New example `truncated-tool.ts` demonstrating proper output truncation with custom rendering for extensions
- New example `preset.ts` demonstrating preset configurations with model/thinking/tools switching ([#347](https://github.com/badlogic/pi-mono/issues/347))
- Documentation for output truncation best practices in `docs/extensions.md`
- Exported all UI components for extensions: `ArminComponent`, `AssistantMessageComponent`, `BashExecutionComponent`, `BorderedLoader`, `BranchSummaryMessageComponent`, `CompactionSummaryMessageComponent`, `CustomEditor`, `CustomMessageComponent`, `DynamicBorder`, `ExtensionEditorComponent`, `ExtensionInputComponent`, `ExtensionSelectorComponent`, `FooterComponent`, `LoginDialogComponent`, `ModelSelectorComponent`, `OAuthSelectorComponent`, `SessionSelectorComponent`, `SettingsSelectorComponent`, `ShowImagesSelectorComponent`, `ThemeSelectorComponent`, `ThinkingSelectorComponent`, `ToolExecutionComponent`, `TreeSelectorComponent`, `UserMessageComponent`, `UserMessageSelectorComponent`, plus utilities `renderDiff`, `truncateToVisualLines`
- `docs/tui.md`: Common Patterns section with copy-paste code for SelectList, BorderedLoader, SettingsList, setStatus, setWidget, setFooter
- `docs/tui.md`: Key Rules section documenting critical patterns 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 component development
## [0.37.4] - 2026-01-06

View file

@ -306,6 +306,8 @@ pi.on("session_start", async (_event, ctx) => {
});
```
**Examples:** [claude-rules.ts](../examples/extensions/claude-rules.ts), [custom-header.ts](../examples/extensions/custom-header.ts), [file-trigger.ts](../examples/extensions/file-trigger.ts), [status-line.ts](../examples/extensions/status-line.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts)
#### session_before_switch / session_switch
Fired when starting a new session (`/new`) or switching sessions (`/resume`).
@ -327,6 +329,8 @@ pi.on("session_switch", async (event, ctx) => {
});
```
**Examples:** [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts), [dirty-repo-guard.ts](../examples/extensions/dirty-repo-guard.ts), [status-line.ts](../examples/extensions/status-line.ts), [todo.ts](../examples/extensions/todo.ts)
#### session_before_branch / session_branch
Fired when branching via `/branch`.
@ -344,6 +348,8 @@ pi.on("session_branch", async (event, ctx) => {
});
```
**Examples:** [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts), [dirty-repo-guard.ts](../examples/extensions/dirty-repo-guard.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts)
#### session_before_compact / session_compact
Fired on compaction. See [compaction.md](compaction.md) for details.
@ -371,6 +377,8 @@ pi.on("session_compact", async (event, ctx) => {
});
```
**Examples:** [custom-compaction.ts](../examples/extensions/custom-compaction.ts)
#### session_before_tree / session_tree
Fired on `/tree` navigation.
@ -388,6 +396,8 @@ pi.on("session_tree", async (event, ctx) => {
});
```
**Examples:** [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts)
#### session_shutdown
Fired on exit (Ctrl+C, Ctrl+D, SIGTERM).
@ -398,6 +408,8 @@ pi.on("session_shutdown", async (_event, ctx) => {
});
```
**Examples:** [auto-commit-on-exit.ts](../examples/extensions/auto-commit-on-exit.ts)
### Agent Events
#### before_agent_start
@ -422,6 +434,8 @@ pi.on("before_agent_start", async (event, ctx) => {
});
```
**Examples:** [claude-rules.ts](../examples/extensions/claude-rules.ts), [pirate.ts](../examples/extensions/pirate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts)
#### agent_start / agent_end
Fired once per user prompt.
@ -434,6 +448,8 @@ pi.on("agent_end", async (event, ctx) => {
});
```
**Examples:** [chalk-logger.ts](../examples/extensions/chalk-logger.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts)
#### turn_start / turn_end
Fired for each turn (one LLM response + tool calls).
@ -448,6 +464,8 @@ pi.on("turn_end", async (event, ctx) => {
});
```
**Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [status-line.ts](../examples/extensions/status-line.ts)
#### context
Fired before each LLM call. Modify messages non-destructively.
@ -460,6 +478,8 @@ pi.on("context", async (event, ctx) => {
});
```
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts)
### Tool Events
#### tool_call
@ -478,6 +498,8 @@ pi.on("tool_call", async (event, ctx) => {
});
```
**Examples:** [chalk-logger.ts](../examples/extensions/chalk-logger.ts), [permission-gate.ts](../examples/extensions/permission-gate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [protected-paths.ts](../examples/extensions/protected-paths.ts)
#### tool_result
Fired after tool executes. **Can modify result.**
@ -498,6 +520,8 @@ pi.on("tool_result", async (event, ctx) => {
});
```
**Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts)
## ExtensionContext
Every handler receives `ctx: ExtensionContext`:
@ -595,7 +619,7 @@ const result = await ctx.navigateTree("entry-id-456", {
### pi.on(event, handler)
Subscribe to events. See [Events](#events).
Subscribe to events. See [Events](#events) for event types and return values.
### pi.registerTool(definition)
@ -630,9 +654,11 @@ pi.registerTool({
});
```
**Examples:** [hello.ts](../examples/extensions/hello.ts), [question.ts](../examples/extensions/question.ts), [todo.ts](../examples/extensions/todo.ts), [truncated-tool.ts](../examples/extensions/truncated-tool.ts)
### pi.sendMessage(message, options?)
Inject a custom message into the session:
Inject a custom message into the session.
```typescript
pi.sendMessage({
@ -653,6 +679,8 @@ pi.sendMessage({
- `"nextTurn"` - Queued for next user prompt. Does not interrupt or trigger anything.
- `triggerTurn: true` - If agent is idle, trigger an LLM response immediately. Only applies to `"steer"` and `"followUp"` modes (ignored for `"nextTurn"`).
**Examples:** [file-trigger.ts](../examples/extensions/file-trigger.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts)
### pi.sendUserMessage(content, options?)
Send a user message to the agent. Unlike `sendMessage()` which sends custom messages, this sends an actual user message that appears as if typed by the user. Always triggers a turn.
@ -683,7 +711,7 @@ See [send-user-message.ts](../examples/extensions/send-user-message.ts) for a co
### pi.appendEntry(customType, data?)
Persist extension state (does NOT participate in LLM context):
Persist extension state (does NOT participate in LLM context).
```typescript
pi.appendEntry("my-state", { count: 42 });
@ -698,9 +726,11 @@ pi.on("session_start", async (_event, ctx) => {
});
```
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [snake.ts](../examples/extensions/snake.ts), [tools.ts](../examples/extensions/tools.ts)
### pi.registerCommand(name, options)
Register a command:
Register a command.
```typescript
pi.registerCommand("stats", {
@ -712,13 +742,15 @@ pi.registerCommand("stats", {
});
```
**Examples:** [custom-footer.ts](../examples/extensions/custom-footer.ts), [custom-header.ts](../examples/extensions/custom-header.ts), [handoff.ts](../examples/extensions/handoff.ts), [pirate.ts](../examples/extensions/pirate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [qna.ts](../examples/extensions/qna.ts), [send-user-message.ts](../examples/extensions/send-user-message.ts), [snake.ts](../examples/extensions/snake.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts)
### pi.registerMessageRenderer(customType, renderer)
Register a custom TUI renderer for messages with your `customType`. See [Custom UI](#custom-ui).
### pi.registerShortcut(shortcut, options)
Register a keyboard shortcut:
Register a keyboard shortcut.
```typescript
pi.registerShortcut("ctrl+shift+p", {
@ -729,9 +761,11 @@ pi.registerShortcut("ctrl+shift+p", {
});
```
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts)
### pi.registerFlag(name, options)
Register a CLI flag:
Register a CLI flag.
```typescript
pi.registerFlag("--plan", {
@ -746,24 +780,57 @@ if (pi.getFlag("--plan")) {
}
```
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts)
### pi.exec(command, args, options?)
Execute a shell command:
Execute a shell command.
```typescript
const result = await pi.exec("git", ["status"], { signal, timeout: 5000 });
// result.stdout, result.stderr, result.code, result.killed
```
**Examples:** [auto-commit-on-exit.ts](../examples/extensions/auto-commit-on-exit.ts), [dirty-repo-guard.ts](../examples/extensions/dirty-repo-guard.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts)
### pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(names)
Manage active tools:
Manage active tools.
```typescript
const active = pi.getActiveTools(); // ["read", "bash", "edit", "write"]
pi.setActiveTools(["read", "bash"]); // Switch to read-only
```
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [tools.ts](../examples/extensions/tools.ts)
### pi.setModel(model)
Set the current model. Returns `false` if no API key is available for the model.
```typescript
const model = ctx.modelRegistry.find("anthropic", "claude-sonnet-4-5");
if (model) {
const success = await pi.setModel(model);
if (!success) {
ctx.ui.notify("No API key for this model", "error");
}
}
```
**Examples:** [preset.ts](../examples/extensions/preset.ts)
### pi.getThinkingLevel() / pi.setThinkingLevel(level)
Get or set the thinking level. Level is clamped to model capabilities (non-reasoning models always use "off").
```typescript
const current = pi.getThinkingLevel(); // "off" | "minimal" | "low" | "medium" | "high" | "xhigh"
pi.setThinkingLevel("high");
```
**Examples:** [preset.ts](../examples/extensions/preset.ts)
### pi.events
Shared event bus for communication between extensions:
@ -994,6 +1061,14 @@ If `renderCall`/`renderResult` is not defined or throws:
Extensions can interact with users via `ctx.ui` methods and customize how messages/tools render.
**For custom components, see [tui.md](tui.md)** which has copy-paste patterns for:
- Selection dialogs (SelectList)
- Async operations with cancel (BorderedLoader)
- Settings toggles (SettingsList)
- Status indicators (setStatus)
- Widgets above editor (setWidget)
- Custom footers (setFooter)
### Dialogs
```typescript
@ -1013,6 +1088,12 @@ const text = await ctx.ui.editor("Edit:", "prefilled text");
ctx.ui.notify("Done!", "info"); // "info" | "warning" | "error"
```
**Examples:**
- `ctx.ui.select()`: [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts), [dirty-repo-guard.ts](../examples/extensions/dirty-repo-guard.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [permission-gate.ts](../examples/extensions/permission-gate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [question.ts](../examples/extensions/question.ts)
- `ctx.ui.confirm()`: [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts)
- `ctx.ui.editor()`: [handoff.ts](../examples/extensions/handoff.ts)
- `ctx.ui.setEditorText()`: [handoff.ts](../examples/extensions/handoff.ts), [qna.ts](../examples/extensions/qna.ts)
### Widgets, Status, and Footer
```typescript
@ -1040,6 +1121,12 @@ ctx.ui.setEditorText("Prefill text");
const current = ctx.ui.getEditorText();
```
**Examples:**
- `ctx.ui.setStatus()`: [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [status-line.ts](../examples/extensions/status-line.ts)
- `ctx.ui.setWidget()`: [plan-mode.ts](../examples/extensions/plan-mode.ts)
- `ctx.ui.setFooter()`: [custom-footer.ts](../examples/extensions/custom-footer.ts)
- `ctx.ui.setHeader()`: [custom-header.ts](../examples/extensions/custom-header.ts)
### Custom Components
For complex UI, use `ctx.ui.custom()`. This temporarily replaces the editor with your component until `done()` is called:
@ -1069,7 +1156,9 @@ The callback receives:
- `theme` - Current theme for styling
- `done(value)` - Call to close component and return value
See [tui.md](tui.md) for the full component API and [examples/extensions/](../examples/extensions/) for working examples (snake.ts, todo.ts, qna.ts).
See [tui.md](tui.md) for the full component API.
**Examples:** [handoff.ts](../examples/extensions/handoff.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [qna.ts](../examples/extensions/qna.ts), [snake.ts](../examples/extensions/snake.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts)
### Message Rendering

View file

@ -340,7 +340,225 @@ class CachedComponent {
Call `invalidate()` when state changes, then `handle.requestRender()` to trigger re-render.
## Common Patterns
These patterns cover the most common UI needs in extensions. **Copy these patterns instead of building from scratch.**
### Pattern 1: Selection Dialog (SelectList)
For letting users pick from a list of options. Use `SelectList` from `@mariozechner/pi-tui` with `DynamicBorder` for framing.
```typescript
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
import { Container, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
pi.registerCommand("pick", {
handler: async (_args, ctx) => {
const items: SelectItem[] = [
{ value: "opt1", label: "Option 1", description: "First option" },
{ value: "opt2", label: "Option 2", description: "Second option" },
{ value: "opt3", label: "Option 3" }, // description is optional
];
const result = await ctx.ui.custom<string | null>((tui, theme, done) => {
const container = new Container();
// Top border
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
// Title
container.addChild(new Text(theme.fg("accent", theme.bold("Pick an Option")), 1, 0));
// SelectList with theme
const selectList = new SelectList(items, Math.min(items.length, 10), {
selectedPrefix: (t) => theme.fg("accent", t),
selectedText: (t) => theme.fg("accent", t),
description: (t) => theme.fg("muted", t),
scrollInfo: (t) => theme.fg("dim", t),
noMatch: (t) => theme.fg("warning", t),
});
selectList.onSelect = (item) => done(item.value);
selectList.onCancel = () => done(null);
container.addChild(selectList);
// Help text
container.addChild(new Text(theme.fg("dim", "↑↓ navigate • enter select • esc cancel"), 1, 0));
// Bottom border
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
return {
render: (w) => container.render(w),
invalidate: () => container.invalidate(),
handleInput: (data) => { selectList.handleInput(data); tui.requestRender(); },
};
});
if (result) {
ctx.ui.notify(`Selected: ${result}`, "info");
}
},
});
```
**Examples:** [preset.ts](../examples/extensions/preset.ts), [tools.ts](../examples/extensions/tools.ts)
### Pattern 2: Async Operation with Cancel (BorderedLoader)
For operations that take time and should be cancellable. `BorderedLoader` shows a spinner and handles escape to cancel.
```typescript
import { BorderedLoader } from "@mariozechner/pi-coding-agent";
pi.registerCommand("fetch", {
handler: async (_args, ctx) => {
const result = await ctx.ui.custom<string | null>((tui, theme, done) => {
const loader = new BorderedLoader(tui, theme, "Fetching data...");
loader.onAbort = () => done(null);
// Do async work
fetchData(loader.signal)
.then((data) => done(data))
.catch(() => done(null));
return loader;
});
if (result === null) {
ctx.ui.notify("Cancelled", "info");
} else {
ctx.ui.setEditorText(result);
}
},
});
```
**Examples:** [qna.ts](../examples/extensions/qna.ts), [handoff.ts](../examples/extensions/handoff.ts)
### Pattern 3: Settings/Toggles (SettingsList)
For toggling multiple settings. Use `SettingsList` from `@mariozechner/pi-tui` with `getSettingsListTheme()`.
```typescript
import { getSettingsListTheme } from "@mariozechner/pi-coding-agent";
import { Container, type SettingItem, SettingsList, Text } from "@mariozechner/pi-tui";
pi.registerCommand("settings", {
handler: async (_args, ctx) => {
const items: SettingItem[] = [
{ id: "verbose", label: "Verbose mode", currentValue: "off", values: ["on", "off"] },
{ id: "color", label: "Color output", currentValue: "on", values: ["on", "off"] },
];
await ctx.ui.custom((_tui, theme, done) => {
const container = new Container();
container.addChild(new Text(theme.fg("accent", theme.bold("Settings")), 1, 1));
const settingsList = new SettingsList(
items,
Math.min(items.length + 2, 15),
getSettingsListTheme(),
(id, newValue) => {
// Handle value change
ctx.ui.notify(`${id} = ${newValue}`, "info");
},
() => done(undefined), // On close
);
container.addChild(settingsList);
return {
render: (w) => container.render(w),
invalidate: () => container.invalidate(),
handleInput: (data) => settingsList.handleInput?.(data),
};
});
},
});
```
**Examples:** [tools.ts](../examples/extensions/tools.ts)
### Pattern 4: Persistent Status Indicator
Show status in the footer that persists across renders. Good for mode indicators.
```typescript
// Set status (shown in footer)
ctx.ui.setStatus("my-ext", ctx.ui.theme.fg("accent", "● active"));
// Clear status
ctx.ui.setStatus("my-ext", undefined);
```
**Examples:** [status-line.ts](../examples/extensions/status-line.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts)
### Pattern 5: Widget Above Editor
Show persistent content above the input editor. Good for todo lists, progress.
```typescript
// Simple string array
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
// Or with theme
ctx.ui.setWidget("my-widget", (_tui, theme) => {
const lines = items.map((item, i) =>
item.done
? theme.fg("success", "✓ ") + theme.fg("muted", item.text)
: theme.fg("dim", "○ ") + item.text
);
return {
render: () => lines,
invalidate: () => {},
};
});
// Clear
ctx.ui.setWidget("my-widget", undefined);
```
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts)
### Pattern 6: Custom Footer
Replace the entire footer with custom content.
```typescript
ctx.ui.setFooter((_tui, theme) => ({
render(width: number): string[] {
const left = theme.fg("dim", "custom footer");
const right = theme.fg("accent", "status");
const padding = " ".repeat(Math.max(1, width - visibleWidth(left) - visibleWidth(right)));
return [truncateToWidth(left + padding + right, width)];
},
invalidate() {},
}));
// Restore default
ctx.ui.setFooter(undefined);
```
**Examples:** [custom-footer.ts](../examples/extensions/custom-footer.ts)
## Key Rules
1. **Always use theme from callback** - Don't import theme directly. Use `theme` from the `ctx.ui.custom((tui, theme, done) => ...)` callback.
2. **Always type DynamicBorder color param** - Write `(s: string) => theme.fg("accent", s)`, not `(s) => theme.fg("accent", s)`.
3. **Call tui.requestRender() after state changes** - In `handleInput`, call `tui.requestRender()` after updating state.
4. **Return the three-method object** - Custom components need `{ render, invalidate, handleInput }`.
5. **Use existing components** - `SelectList`, `SettingsList`, `BorderedLoader` cover 90% of cases. Don't rebuild them.
## Examples
- **Snake game**: [examples/hooks/snake.ts](../examples/hooks/snake.ts) - Full game with keyboard input, game loop, state persistence
- **Custom tool rendering**: [examples/extensions/todo.ts](../examples/extensions/todo.ts) - Custom `renderCall` and `renderResult`
- **Selection UI**: [examples/extensions/preset.ts](../examples/extensions/preset.ts) - SelectList with DynamicBorder framing
- **Async with cancel**: [examples/extensions/qna.ts](../examples/extensions/qna.ts) - BorderedLoader for LLM calls
- **Settings toggles**: [examples/extensions/tools.ts](../examples/extensions/tools.ts) - SettingsList for tool enable/disable
- **Status indicators**: [examples/extensions/plan-mode.ts](../examples/extensions/plan-mode.ts) - setStatus and setWidget
- **Custom footer**: [examples/extensions/custom-footer.ts](../examples/extensions/custom-footer.ts) - setFooter with stats
- **Snake game**: [examples/extensions/snake.ts](../examples/extensions/snake.ts) - Full game with keyboard input, game loop
- **Custom tool rendering**: [examples/extensions/todo.ts](../examples/extensions/todo.ts) - renderCall and renderResult

View file

@ -36,6 +36,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/
| Extension | Description |
|-----------|-------------|
| `preset.ts` | Named presets for model, thinking level, tools, and instructions via `--preset` flag and `/preset` command |
| `plan-mode.ts` | Claude Code-style plan mode for read-only exploration with `/plan` command |
| `tools.ts` | Interactive `/tools` command to enable/disable tools with session persistence |
| `handoff.ts` | Transfer context to a new focused session via `/handoff <goal>` |

View file

@ -0,0 +1,398 @@
/**
* Preset Extension
*
* Allows defining named presets that configure model, thinking level, tools,
* and system prompt instructions. Presets are defined in JSON config files
* and can be activated via CLI flag, /preset command, or Ctrl+Shift+U to cycle.
*
* Config files (merged, project takes precedence):
* - ~/.pi/agent/presets.json (global)
* - <cwd>/.pi/presets.json (project-local)
*
* Example presets.json:
* ```json
* {
* "plan": {
* "provider": "anthropic",
* "model": "claude-sonnet-4-5",
* "thinkingLevel": "high",
* "tools": ["read", "grep", "find", "ls"],
* "instructions": "You are in PLANNING MODE. Your job is to deeply understand the problem and create a detailed implementation plan.\n\nRules:\n- DO NOT make any changes. You cannot edit or write files.\n- Read files IN FULL (no offset/limit) to get complete context. Partial reads miss critical details.\n- Explore thoroughly: grep for related code, find similar patterns, understand the architecture.\n- Ask clarifying questions if requirements are ambiguous. Do not assume.\n- Identify risks, edge cases, and dependencies before proposing solutions.\n\nOutput:\n- Create a structured plan with numbered steps.\n- For each step: what to change, why, and potential risks.\n- List files that will be modified.\n- Note any tests that should be added or updated.\n\nWhen done, ask the user if they want you to:\n1. Write the plan to a markdown file (e.g., PLAN.md)\n2. Create a GitHub issue with the plan\n3. Proceed to implementation (they should switch to 'implement' preset)"
* },
* "implement": {
* "provider": "anthropic",
* "model": "claude-sonnet-4-5",
* "thinkingLevel": "high",
* "tools": ["read", "bash", "edit", "write"],
* "instructions": "You are in IMPLEMENTATION MODE. Your job is to make focused, correct changes.\n\nRules:\n- Keep scope tight. Do exactly what was asked, no more.\n- Read files before editing to understand current state.\n- Make surgical edits. Prefer edit over write for existing files.\n- Explain your reasoning briefly before each change.\n- Run tests or type checks after changes if the project has them (npm test, npm run check, etc.).\n- If you encounter unexpected complexity, STOP and explain the issue rather than hacking around it.\n\nIf no plan exists:\n- Ask clarifying questions before starting.\n- Propose what you'll do and get confirmation for non-trivial changes.\n\nAfter completing changes:\n- Summarize what was done.\n- Note any follow-up work or tests that should be added."
* }
* }
* ```
*
* Usage:
* - `pi --preset plan` - start with plan preset
* - `/preset` - show selector to switch presets mid-session
* - `/preset implement` - switch to implement preset directly
* - `Ctrl+Shift+U` - cycle through presets
*
* CLI flags always override preset values.
*/
import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
import { Container, Key, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
// Preset configuration
interface Preset {
/** Provider name (e.g., "anthropic", "openai") */
provider?: string;
/** Model ID (e.g., "claude-sonnet-4-5") */
model?: string;
/** Thinking level */
thinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
/** Tools to enable (replaces default set) */
tools?: string[];
/** Instructions to append to system prompt */
instructions?: string;
}
interface PresetsConfig {
[name: string]: Preset;
}
/**
* Load presets from config files.
* Project-local presets override global presets with the same name.
*/
function loadPresets(cwd: string): PresetsConfig {
const globalPath = join(homedir(), ".pi", "agent", "presets.json");
const projectPath = join(cwd, ".pi", "presets.json");
let globalPresets: PresetsConfig = {};
let projectPresets: PresetsConfig = {};
// Load global presets
if (existsSync(globalPath)) {
try {
const content = readFileSync(globalPath, "utf-8");
globalPresets = JSON.parse(content);
} catch (err) {
console.error(`Failed to load global presets from ${globalPath}: ${err}`);
}
}
// Load project presets
if (existsSync(projectPath)) {
try {
const content = readFileSync(projectPath, "utf-8");
projectPresets = JSON.parse(content);
} catch (err) {
console.error(`Failed to load project presets from ${projectPath}: ${err}`);
}
}
// Merge (project overrides global)
return { ...globalPresets, ...projectPresets };
}
export default function presetExtension(pi: ExtensionAPI) {
let presets: PresetsConfig = {};
let activePresetName: string | undefined;
let activePreset: Preset | undefined;
// Register --preset CLI flag
pi.registerFlag("preset", {
description: "Preset configuration to use",
type: "string",
});
/**
* Apply a preset configuration.
*/
async function applyPreset(name: string, preset: Preset, ctx: ExtensionContext): Promise<boolean> {
// Apply model if specified
if (preset.provider && preset.model) {
const model = ctx.modelRegistry.find(preset.provider, preset.model);
if (model) {
const success = await pi.setModel(model);
if (!success) {
ctx.ui.notify(`Preset "${name}": No API key for ${preset.provider}/${preset.model}`, "warning");
}
} else {
ctx.ui.notify(`Preset "${name}": Model ${preset.provider}/${preset.model} not found`, "warning");
}
}
// Apply thinking level if specified
if (preset.thinkingLevel) {
pi.setThinkingLevel(preset.thinkingLevel);
}
// Apply tools if specified
if (preset.tools && preset.tools.length > 0) {
const allTools = pi.getAllTools();
const validTools = preset.tools.filter((t) => allTools.includes(t));
const invalidTools = preset.tools.filter((t) => !allTools.includes(t));
if (invalidTools.length > 0) {
ctx.ui.notify(`Preset "${name}": Unknown tools: ${invalidTools.join(", ")}`, "warning");
}
if (validTools.length > 0) {
pi.setActiveTools(validTools);
}
}
// Store active preset for system prompt injection
activePresetName = name;
activePreset = preset;
return true;
}
/**
* Build description string for a preset.
*/
function buildPresetDescription(preset: Preset): string {
const parts: string[] = [];
if (preset.provider && preset.model) {
parts.push(`${preset.provider}/${preset.model}`);
}
if (preset.thinkingLevel) {
parts.push(`thinking:${preset.thinkingLevel}`);
}
if (preset.tools) {
parts.push(`tools:${preset.tools.join(",")}`);
}
if (preset.instructions) {
const truncated =
preset.instructions.length > 30 ? `${preset.instructions.slice(0, 27)}...` : preset.instructions;
parts.push(`"${truncated}"`);
}
return parts.join(" | ");
}
/**
* Show preset selector UI using custom SelectList component.
*/
async function showPresetSelector(ctx: ExtensionContext): Promise<void> {
const presetNames = Object.keys(presets);
if (presetNames.length === 0) {
ctx.ui.notify("No presets defined. Add presets to ~/.pi/agent/presets.json or .pi/presets.json", "warning");
return;
}
// Build select items with descriptions
const items: SelectItem[] = presetNames.map((name) => {
const preset = presets[name];
const isActive = name === activePresetName;
return {
value: name,
label: isActive ? `${name} (active)` : name,
description: buildPresetDescription(preset),
};
});
// Add "None" option to clear preset
items.push({
value: "(none)",
label: "(none)",
description: "Clear active preset, restore defaults",
});
const result = await ctx.ui.custom<string | null>((tui, theme, done) => {
const container = new Container();
container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
// Header
container.addChild(new Text(theme.fg("accent", theme.bold("Select Preset"))));
// SelectList with themed styling
const selectList = new SelectList(items, Math.min(items.length, 10), {
selectedPrefix: (text) => theme.fg("accent", text),
selectedText: (text) => theme.fg("accent", text),
description: (text) => theme.fg("muted", text),
scrollInfo: (text) => theme.fg("dim", text),
noMatch: (text) => theme.fg("warning", text),
});
selectList.onSelect = (item) => done(item.value);
selectList.onCancel = () => done(null);
container.addChild(selectList);
// Footer hint
container.addChild(new Text(theme.fg("dim", "↑↓ navigate • enter select • esc cancel")));
container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
return {
render(width: number) {
return container.render(width);
},
invalidate() {
container.invalidate();
},
handleInput(data: string) {
selectList.handleInput(data);
tui.requestRender();
},
};
});
if (!result) return;
if (result === "(none)") {
// Clear preset and restore defaults
activePresetName = undefined;
activePreset = undefined;
pi.setActiveTools(["read", "bash", "edit", "write"]);
ctx.ui.notify("Preset cleared, defaults restored", "info");
updateStatus(ctx);
return;
}
const preset = presets[result];
if (preset) {
await applyPreset(result, preset, ctx);
ctx.ui.notify(`Preset "${result}" activated`, "info");
updateStatus(ctx);
}
}
/**
* Update status indicator.
*/
function updateStatus(ctx: ExtensionContext) {
if (activePresetName) {
ctx.ui.setStatus("preset", ctx.ui.theme.fg("accent", `preset:${activePresetName}`));
} else {
ctx.ui.setStatus("preset", undefined);
}
}
function getPresetOrder(): string[] {
return Object.keys(presets).sort();
}
async function cyclePreset(ctx: ExtensionContext): Promise<void> {
const presetNames = getPresetOrder();
if (presetNames.length === 0) {
ctx.ui.notify("No presets defined. Add presets to ~/.pi/agent/presets.json or .pi/presets.json", "warning");
return;
}
const cycleList = ["(none)", ...presetNames];
const currentName = activePresetName ?? "(none)";
const currentIndex = cycleList.indexOf(currentName);
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % cycleList.length;
const nextName = cycleList[nextIndex];
if (nextName === "(none)") {
activePresetName = undefined;
activePreset = undefined;
pi.setActiveTools(["read", "bash", "edit", "write"]);
ctx.ui.notify("Preset cleared, defaults restored", "info");
updateStatus(ctx);
return;
}
const preset = presets[nextName];
if (!preset) return;
await applyPreset(nextName, preset, ctx);
ctx.ui.notify(`Preset "${nextName}" activated`, "info");
updateStatus(ctx);
}
pi.registerShortcut(Key.ctrlShift("u"), {
description: "Cycle presets",
handler: async (ctx) => {
await cyclePreset(ctx);
},
});
// Register /preset command
pi.registerCommand("preset", {
description: "Switch preset configuration",
handler: async (args, ctx) => {
// If preset name provided, apply directly
if (args?.trim()) {
const name = args.trim();
const preset = presets[name];
if (!preset) {
const available = Object.keys(presets).join(", ") || "(none defined)";
ctx.ui.notify(`Unknown preset "${name}". Available: ${available}`, "error");
return;
}
await applyPreset(name, preset, ctx);
ctx.ui.notify(`Preset "${name}" activated`, "info");
updateStatus(ctx);
return;
}
// Otherwise show selector
await showPresetSelector(ctx);
},
});
// Inject preset instructions into system prompt
pi.on("before_agent_start", async () => {
if (activePreset?.instructions) {
return {
systemPromptAppend: activePreset.instructions,
};
}
});
// Initialize on session start
pi.on("session_start", async (_event, ctx) => {
// Load presets from config files
presets = loadPresets(ctx.cwd);
// Check for --preset flag
const presetFlag = pi.getFlag("preset");
if (typeof presetFlag === "string" && presetFlag) {
const preset = presets[presetFlag];
if (preset) {
await applyPreset(presetFlag, preset, ctx);
ctx.ui.notify(`Preset "${presetFlag}" activated`, "info");
} else {
const available = Object.keys(presets).join(", ") || "(none defined)";
ctx.ui.notify(`Unknown preset "${presetFlag}". Available: ${available}`, "warning");
}
}
// Restore preset from session state
const entries = ctx.sessionManager.getEntries();
const presetEntry = entries
.filter((e: { type: string; customType?: string }) => e.type === "custom" && e.customType === "preset-state")
.pop() as { data?: { name: string } } | undefined;
if (presetEntry?.data?.name && !presetFlag) {
const preset = presets[presetEntry.data.name];
if (preset) {
activePresetName = presetEntry.data.name;
activePreset = preset;
// Don't re-apply model/tools on restore, just keep the name for instructions
}
}
updateStatus(ctx);
});
// Persist preset state
pi.on("turn_start", async () => {
if (activePresetName) {
pi.appendEntry("preset-state", { name: activePresetName });
}
});
}

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,
});

View file

@ -88,6 +88,9 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => {
setGetActiveToolsHandler: () => {},
setGetAllToolsHandler: () => {},
setSetActiveToolsHandler: () => {},
setSetModelHandler: () => {},
setGetThinkingLevelHandler: () => {},
setSetThinkingLevelHandler: () => {},
setFlagValue: () => {},
};
}
@ -117,6 +120,9 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => {
getActiveToolsHandler: () => [],
getAllToolsHandler: () => [],
setActiveToolsHandler: () => {},
setModelHandler: async () => false,
getThinkingLevelHandler: () => "off",
setThinkingLevelHandler: () => {},
uiContext: {
select: async () => undefined,
confirm: async () => false,
@ -292,6 +298,9 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => {
setGetActiveToolsHandler: () => {},
setGetAllToolsHandler: () => {},
setSetActiveToolsHandler: () => {},
setSetModelHandler: () => {},
setGetThinkingLevelHandler: () => {},
setSetThinkingLevelHandler: () => {},
setFlagValue: () => {},
};
@ -348,6 +357,9 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => {
setGetActiveToolsHandler: () => {},
setGetAllToolsHandler: () => {},
setSetActiveToolsHandler: () => {},
setSetModelHandler: () => {},
setGetThinkingLevelHandler: () => {},
setSetThinkingLevelHandler: () => {},
setFlagValue: () => {},
};
@ -386,6 +398,9 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => {
setGetActiveToolsHandler: () => {},
setGetAllToolsHandler: () => {},
setSetActiveToolsHandler: () => {},
setSetModelHandler: () => {},
setGetThinkingLevelHandler: () => {},
setSetThinkingLevelHandler: () => {},
setFlagValue: () => {},
};