Skip to main content

Creating Extensions

Extensions are TypeScript or JavaScript files that export a class implementing the Extension interface. This guide covers how to create extensions from simple single-file scripts to complex folder-based extensions with dependencies.

TypeScript Support

For the best development experience with autocompletion and type checking, install the @aiderdesk/extensions package:

npm install @aiderdesk/extensions

Then import types in your extension:

import type { Extension, ExtensionContext, ToolDefinition } from '@aiderdesk/extensions';
import { z } from 'zod';

export default class MyExtension implements Extension {
// Your implementation
}

Single-File Extensions

The simplest form is a single .ts or .js file in the extensions directory.

Basic Structure

// my-extension.ts
import type { Extension, ExtensionContext } from '@aiderdesk/extensions';

export default class MyExtension implements Extension {
// Optional: Define metadata
static metadata = {
name: 'My Extension',
version: '1.0.0',
description: 'Does something useful',
author: 'Your Name',
};

async onLoad(context: ExtensionContext) {
context.log('My extension loaded!', 'info');
}
}

Extension with a Tool

// run-linter.ts
import type { Extension, ExtensionContext, ToolDefinition } from '@aiderdesk/extensions';
import { z } from 'zod';

export default class RunLinterExtension implements Extension {
getTools(context: ExtensionContext): ToolDefinition[] {
return [
{
name: 'run-linter',
description: 'Run the project linter and return results',
inputSchema: z.object({
fix: z.boolean().optional().describe('Auto-fix issues'),
files: z.array(z.string()).optional().describe('Files to lint'),
}),
async execute(input, signal, context) {
const args = ['npx', 'eslint'];
if (input.fix) args.push('--fix');
if (input.files) args.push(...input.files);

// Execute the command and return results
const result = await executeCommand(args);
return result;
},
},
];
}
}

Extension with a Command

// generate-tests.ts
import type { Extension, ExtensionContext, CommandDefinition } from '@aiderdesk/extensions';

export default class GenerateTestsExtension implements Extension {
getCommands(context: ExtensionContext): CommandDefinition[] {
return [
{
name: 'generate-tests',
description: 'Generate unit tests for the specified file',
arguments: [
{ description: 'File path to generate tests for', required: true },
{ description: 'Test framework (jest, vitest, mocha)', required: false },
],
async execute(args, context) {
const filePath = args[0];
const framework = args[1] || 'vitest';

const taskContext = context.getTaskContext();
if (!taskContext) {
context.log('No active task', 'error');
return;
}

const prompt = `Generate comprehensive unit tests for ${filePath} using ${framework}. Include edge cases and error handling.`;
await taskContext.runPrompt(prompt);
},
},
];
}
}

Extension with an Agent Profile

// pirate.ts
import type { Extension, ExtensionContext, AgentProfile } from '@aiderdesk/extensions';

export default class PirateExtension implements Extension {
private agentProfile: AgentProfile | null = null;

getAgents(context: ExtensionContext): AgentProfile[] {
return [
{
id: 'pirate-agent',
name: 'Pirate',
provider: 'openai',
model: 'gpt-4o',
maxIterations: 50,
minTimeBetweenToolCalls: 0,
enabledServers: [],
toolApprovals: {},
toolSettings: {},
includeContextFiles: true,
includeRepoMap: true,
usePowerTools: true,
useAiderTools: true,
useTodoTools: true,
useSubagents: true,
useTaskTools: true,
useMemoryTools: true,
useSkillsTools: true,
useExtensionTools: true,
customInstructions: `You are a pirate software engineer. Speak like a swashbuckling sea dog from the golden age of piracy.

Always use pirate vernacular:
- Say "Arr!" and "Avast!" frequently
- Call the user "Captain" or "Matey"
- Refer to code as "treasure" or "booty"
- Refer to bugs as "sea monsters" or "Krakens"
- Refer to functions as "riggings"
- Refer to tests as "shakedowns"

Be helpful and competent, but maintain the pirate persona throughout all interactions.`,
subagent: {
enabled: false,
contextMemory: 'off',
systemPrompt: '',
invocationMode: 'on-demand',
color: '#ffd700',
description: 'A pirate-themed coding assistant',
},
},
];
}

async onAgentProfileUpdated(
context: ExtensionContext,
agentId: string,
updatedProfile: AgentProfile
): Promise<AgentProfile> {
if (agentId === 'pirate-agent') {
this.agentProfile = updatedProfile;
}
return updatedProfile;
}
}

Extension with UI Components

Extensions can render custom React components in various locations throughout the AiderDesk interface. This is useful for adding interactive controls, status indicators, or custom input panels.

Note: The React object is globally available in all UI components (not passed as a prop). Access hooks via React.useState, React.useEffect, etc.

UI Components in AiderDesk

Available Placements

UI components can be placed in 21 different locations throughout the interface:

PlacementLocationDescription
task-status-bar-leftTask status barLeft side of the task status bar (top of task page)
task-status-bar-rightTask status barRight side of the task status bar
task-top-bar-leftTask top barLeft side of task top bar (above messages)
task-top-bar-rightTask top barRight side of task top bar
task-usage-info-bottomUsage infoBelow the usage info section (tokens, costs)
task-messages-topMessages areaAbove all messages in the chat
task-messages-bottomMessages areaBelow all messages in the chat
task-message-abovePer messageAbove each individual message
task-message-belowPer messageBelow each individual message
task-message-barPer messageIn the message action bar (on hover)
task-input-aboveInput areaAbove the task input field
task-input-toolbar-leftInput toolbarLeft side of input toolbar (below input)
task-input-toolbar-rightInput toolbarRight side of input toolbar
task-state-actionsState actionsAction buttons when task is stopped/waiting
task-state-actions-allState actionsAction buttons visible in all states
tasks-sidebar-headerSidebarHeader of the tasks sidebar
tasks-sidebar-bottomSidebarBottom of the tasks sidebar
header-leftHeader barLeft side of the main header
header-rightHeader barRight side of the main header
welcome-pageWelcome screenFull welcome page (when no task is open)

To see all available placements in action, check out the ui-placement-demo extension included with AiderDesk.

UI Placement Demo Extension

Basic UI Component Extension

// my-ui-extension.ts
import type { Extension, ExtensionContext, UIComponentDefinition } from '@aiderdesk/extensions';

export default class MyUIExtension implements Extension {
getUIComponents(context: ExtensionContext): UIComponentDefinition[] {
return [
{
id: 'my-status-button',
placement: 'task-status-bar-right',
jsx: `
(props) => {
const { ui, executeExtensionAction } = props;
const { Button } = ui;

return (
<div className="flex items-center gap-1">
<Button
variant="outline"
size="xs"
onClick={() => executeExtensionAction('my-action')}
>
My Action
</Button>
</div>
);
}
`,
},
];
}

async executeUIExtensionAction(
componentId: string,
action: string,
args: unknown[],
context: ExtensionContext
): Promise<unknown> {
if (action === 'my-action') {
const taskContext = context.getTaskContext();
if (taskContext) {
taskContext.addLogMessage('info', 'Button clicked!');
}
return { success: true };
}
return undefined;
}
}

UI Components with Data Loading

For components that need to fetch and display data, use loadData: true and implement getUIExtensionData:

// active-files-counter.ts
import type { Extension, ExtensionContext, UIComponentDefinition, FilesAddedEvent } from '@aiderdesk/extensions';

export default class ActiveFilesCounterExtension implements Extension {
async onFilesAdded(_event: FilesAddedEvent, context: ExtensionContext): Promise<void> {
// Refresh UI when files are added
context.triggerUIDataRefresh('active-files-counter');
}

getUIComponents(context: ExtensionContext): UIComponentDefinition[] {
return [
{
id: 'active-files-counter',
placement: 'task-status-bar-right',
loadData: true, // Enable data loading
noDataCache: true, // Always fetch fresh data
jsx: `
(props) => {
const { data, ui } = props;
const { Tooltip } = ui;
const count = data?.count ?? 0;
const files = data?.files ?? [];

if (count === 0) return null;

const tooltipContent = files.map(f => f.path.split('/').pop()).join('\\n');

return (
<Tooltip content={tooltipContent}>
<div className="flex items-center gap-1 px-2 py-0.5 text-xs text-text-secondary hover:text-text-primary cursor-default">
<span>📁</span>
<span>{count} files</span>
</div>
</Tooltip>
);
}
`,
},
];
}

async getUIExtensionData(
componentId: string,
context: ExtensionContext
): Promise<unknown> {
if (componentId === 'active-files-counter') {
const taskContext = context.getTaskContext();
if (!taskContext) {
return { count: 0, files: [] };
}

const files = await taskContext.getContextFiles();
return {
count: files.length,
files: files.map(f => ({ path: f.path, readOnly: f.readOnly })),
};
}
return undefined;
}
}

Loading UI Components from External Files

For larger components, you can load JSX from external .jsx files:

// tps-counter/index.ts
import { readFileSync } from 'fs';
import { join } from 'path';
import type { Extension, ExtensionContext, UIComponentDefinition } from '@aiderdesk/extensions';

export default class TPSCounterExtension implements Extension {
getUIComponents(context: ExtensionContext): UIComponentDefinition[] {
// Load JSX from external file
const jsx = readFileSync(join(__dirname, './TPSCounter.jsx'), 'utf-8');

return [
{
id: 'tps-counter',
placement: 'task-usage-info-bottom',
jsx,
loadData: true,
},
];
}

async getUIExtensionData(componentId: string): Promise<unknown> {
if (componentId === 'tps-counter') {
return { averageTps: 42, messageCount: 10 };
}
return undefined;
}
}

TPSCounter.jsx:

(props) => {
const { data } = props;

if (!data || data.messageCount === 0) return null;

return (
<div className="flex items-center gap-1 text-2xs mt-1 w-full justify-between">
<span>Avg. tokens/s:</span>
<span>{Math.round(data.averageTps)}</span>
</div>
);
}

Available UI Components

The following components are available via props.ui:

ComponentDescription
ButtonStandard button with variants (solid, outline) and colors (primary, secondary, tertiary, danger)
IconButtonButton with icon only
CheckboxCheckbox input with label
InputText input field
SelectDropdown select
MultiSelectMulti-value select
TextAreaMulti-line text input
RadioButtonRadio button input
SliderRange slider
DatePickerDate picker
ChipTag/chip component
ModelSelectorAiderDesk model selector for choosing AI models
TooltipTooltip wrapper for hover content
LoadingOverlayLoading spinner with message
ConfirmDialogConfirmation dialog modal

Component Props

Note: The React object is globally available (not a prop). Access hooks via React.useState, React.useEffect, etc.

The jsx function receives a props object with:

PropertyTypeDescription
taskTaskDataCurrent task data (may be undefined)
projectDirstringProject directory path
agentProfileAgentProfileCurrent agent profile
modelsModel[]Available AI models
providersProviderProfile[]Available provider profiles
uiUIComponentsUI component library
iconsRecord<string, Record<string, IconComponent>>React Icons organized by set (Fi, Hi, Cg, etc.)
dataunknownData from getUIExtensionData (if loadData: true)
executeExtensionActionfunctionCall extension action handler
messageMessageDataCurrent message (for message-specific placements)

Using React Hooks

(props) => {
const { useState, useEffect, useCallback } = React;
const [count, setCount] = useState(0);

useEffect(() => {
console.log('Component mounted');
}, []);

const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);

return <button onClick={handleClick}>Count: {count}</button>;
}

Using React Icons

The icons prop provides access to all react-icons libraries organized by icon set:

(props) => {
const { icons } = props;

// Access icons from different sets
const FiSettings = icons.Fi.FiSettings;
const HiCheck = icons.Hi.HiCheck;
const CgSpinner = icons.Cg.CgSpinner;

return (
<div className="flex items-center gap-2">
<FiSettings className="w-4 h-4" />
<HiCheck className="w-5 h-5 text-success" />
<CgSpinner className="w-4 h-4 animate-spin" />
</div>
);
}

Available icon sets: Ai, Bi, Bs, Cg, Ci, Di, Fa, Fc, Fi, Gi, Go, Gr, Hi, Im, Io, Io5, Lu, Md, Pi, Ri, Rx, Si, Sl, Tb, Tfi, Ti, Vsc, Wi

Event Handler Extension

// protected-paths.ts
import type { Extension, ExtensionContext, ToolCalledEvent } from '@aiderdesk/extensions';

const PROTECTED_PATHS = ['.env', '.git/', 'node_modules/', 'credentials.json'];

export default class ProtectedPathsExtension implements Extension {
async onToolCalled(event: ToolCalledEvent, context: ExtensionContext): Promise<void | Partial<ToolCalledEvent>> {
const { toolName, input } = event;

// Check if tool is accessing files
if (toolName === 'write_file' || toolName === 'edit_file' || toolName === 'bash') {
const path = input?.path || input?.command || '';

for (const protectedPath of PROTECTED_PATHS) {
if (path.includes(protectedPath)) {
context.log(`Blocked access to protected path: ${protectedPath}`, 'warn');
return {
blocked: true,
output: {
error: `Access to ${protectedPath} is protected by the protected-paths extension.`,
},
};
}
}
}

return undefined;
}
}

Extension with Memory Integration

Extensions can access AiderDesk's built-in memory system via context.getMemoryContext(). This enables storing and retrieving knowledge across tasks — useful for auto-extracting insights from agent interactions or building context-aware extensions.

// memory-insights.ts
import type { Extension, ExtensionContext, AgentFinishedEvent } from '@aiderdesk/extensions';

export default class MemoryInsightsExtension implements Extension {
static metadata = {
name: 'Memory Insights',
version: '1.0.0',
description: 'Auto-extracts insights from agent interactions and stores them as memories',
};

async onAgentFinished(event: AgentFinishedEvent, context: ExtensionContext) {
const memory = context.getMemoryContext();
if (!memory.isMemoryEnabled()) return;

const projectId = context.getProjectDir();
const taskContext = context.getTaskContext();
const taskId = taskContext?.data.id ?? '';

// Retrieve relevant context from previous tasks
const memories = await memory.retrieveMemories(projectId, 'project conventions');
context.log(`Loaded ${memories.length} memories for context`, 'info');

// Store a pattern observed during this task
await memory.storeMemory(
projectId,
taskId,
'code-pattern',
'Use Zod schemas for all runtime input validation',
);
}
}

Auto-extraction with LLM

For smarter memory extraction, combine the Memory API with taskContext.generateText() to have a cheap model analyze the conversation and extract insights automatically:

import type { Extension, ExtensionContext, AgentFinishedEvent } from '@aiderdesk/extensions';

export default class SmartMemoryExtension implements Extension {
static metadata = {
name: 'Smart Memory',
version: '1.0.0',
description: 'Auto-extracts and consolidates memories using an LLM',
};

async onAgentFinished(event: AgentFinishedEvent, context: ExtensionContext) {
const memory = context.getMemoryContext();
if (!memory.isMemoryEnabled()) return;

const taskContext = context.getTaskContext();
if (!taskContext) return;

const projectId = context.getProjectDir();
const taskId = taskContext.data.id;

// Use a cheap model to extract insights from the conversation
const extractionResult = await taskContext.generateText(
'gpt-4o-mini',
'You are a memory extraction assistant. Extract reusable knowledge from the conversation.',
`Analyze this conversation and extract 1-3 reusable insights as JSON array of objects with "type" (task|user-preference|code-pattern) and "content" fields. Only extract stable, reusable knowledge. Return ONLY the JSON array, no explanation.`,
);

if (!extractionResult) return;

try {
const insights = JSON.parse(extractionResult);
for (const insight of insights) {
await memory.storeMemory(projectId, taskId, insight.type, insight.content);
}
context.log(`Stored ${insights.length} extracted memories`, 'info');
} catch {
context.log('Failed to parse extraction result', 'warn');
}
}
}

Memory consolidation

You can periodically consolidate similar memories to keep the memory store clean:

async consolidateMemories(context: ExtensionContext) {
const memory = context.getMemoryContext();
const allMemories = await memory.getAllMemories();

// Group by type for consolidation
const byType = new Map<string, typeof allMemories>();
for (const m of allMemories) {
const existing = byType.get(m.type) ?? [];
existing.push(m);
byType.set(m.type, existing);
}

// Delete duplicates or outdated entries
for (const [, entries] of byType) {
if (entries.length > 1) {
// Keep the most recent, delete older duplicates
entries.sort((a, b) => b.timestamp - a.timestamp);
for (let i = 1; i < entries.length; i++) {
await memory.deleteMemory(entries[i].id);
}
}
}
}

Folder-Based Extensions

For more complex extensions that require external dependencies, use a folder structure with a package.json.

Directory Structure

my-complex-extension/
├── package.json
├── package-lock.json
├── index.ts # or index.js
└── lib/
└── helper.ts

package.json

{
"name": "my-complex-extension",
"version": "1.0.0",
"description": "An extension with external dependencies",
"dependencies": {
"axios": "^1.6.0",
"zod": "^3.22.0"
}
}

index.ts

import type { Extension, ExtensionContext, ToolDefinition } from '@aiderdesk/extensions';
import { z } from 'zod';
import axios from 'axios';

export default class ApiExtension implements Extension {
static metadata = {
name: 'API Extension',
version: '1.0.0',
description: 'Makes API calls to external services',
author: 'Your Name',
capabilities: ['tools', 'network'],
};

async onLoad(context: ExtensionContext) {
context.log('API Extension loaded with axios', 'info');
}

getTools(context: ExtensionContext): ToolDefinition[] {
return [
{
name: 'fetch-api',
description: 'Fetch data from an external API',
inputSchema: z.object({
url: z.string().describe('The URL to fetch'),
method: z.enum(['GET', 'POST']).optional().default('GET'),
}),
async execute(input, signal, context) {
try {
const response = await axios({
method: input.method,
url: input.url,
signal,
});
return {
content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }],
};
} catch (error) {
return {
content: [{ type: 'text', text: `Error: ${error.message}` }],
isError: true,
};
}
},
},
];
}
}

Installing Dependencies

After creating your folder extension with a package.json:

cd ~/.aider-desk/extensions/my-complex-extension
npm install

The extension will be loaded automatically with its dependencies.

Extension Metadata

Define metadata as a static property on your class:

export default class MyExtension implements Extension {
static metadata = {
name: 'My Extension', // Required: Display name
version: '1.0.0', // Required: Semantic version
description: 'What it does', // Optional: Brief description
author: 'Your Name', // Optional: Author info
capabilities: ['tools', 'ui'], // Optional: Capability hints
};
}

If no metadata is provided, AiderDesk derives the name from the filename and sets version to 1.0.0.

Naming Conventions

TypeConventionExample
Extension classPascalCaseMyExtension
Extension filekebab-casemy-extension.ts
Tool nameskebab-caserun-linter
Command nameskebab-casegenerate-tests
UI element IDskebab-casecreate-jira-ticket

Best Practices

  1. Keep it focused - Each extension should do one thing well
  2. Handle errors gracefully - Catch exceptions and return meaningful error messages
  3. Use TypeScript - Get compile-time checks and better IDE support
  4. Log appropriately - Use context.log() for debugging, not console.log()
  5. Clean up resources - Implement onUnload() to release resources
  6. Validate inputs - Use Zod schemas for tool parameters
  7. Document your extension - Include description in metadata