# Onda Plugin Development Guide

Onda has a powerful plugin system that lets you extend the terminal with custom commands, panels, themes, keyboard shortcuts, and more. Plugins run in isolated Web Workers for security, communicating with the host application through a message-passing bridge.

This is the full aggregated single-file version of the plugin docs, kept for backwards compatibility and LLM ingestion. The interactive HTML version at `/docs/plugins` is split into accordion sections.

## Read individual files

Each section is also available as a standalone markdown file:

- [quickstart.md](./plugins/quickstart.md)
- [structure.md](./plugins/structure.md)
- [manifest.md](./plugins/manifest.md)
- [capabilities.md](./plugins/capabilities.md)
- [api-reference.md](./plugins/api-reference.md)
- [examples.md](./plugins/examples.md)
- [security.md](./plugins/security.md)
- [best-practices.md](./plugins/best-practices.md)
- [debugging.md](./plugins/debugging.md)
- [faq.md](./plugins/faq.md)

## Table of Contents

1. [Quick Start](#quick-start)
2. [Plugin Structure](#plugin-structure)
3. [Manifest Reference](#manifest-reference)
4. [Capabilities](#capabilities)
5. [API Reference](#api-reference)
6. [Examples](#examples)
7. [Security Model](#security-model)
8. [Best Practices](#best-practices)
9. [Debugging](#debugging)

---

## Quick Start

### 1. Create Plugin Directory

```bash
mkdir -p ~/.config/onda/plugins/my-plugin
cd ~/.config/onda/plugins/my-plugin
```

### 2. Create manifest.json

```json
{
  "id": "my-plugin",
  "name": "My First Plugin",
  "version": "1.0.0",
  "description": "A simple Onda plugin",
  "main": "main.js",
  "capabilities": ["commands", "notifications"],
  "activationEvents": ["onStartup"]
}
```

### 3. Create main.js

```javascript
self.__ondaPlugin = {
  onActivate: async (api) => {
    // Register a command that appears in Command Palette (Cmd+K)
    await api.commands.register('my-plugin.hello', {
      title: 'Say Hello',
      category: 'My Plugin',
      handler: () => {
        api.notifications.show({
          type: 'success',
          message: 'Hello from My Plugin!'
        });
      }
    });

    console.log('[MyPlugin] Activated!');
  }
};
```

### 4. Enable the Plugin

Open Onda, go to Settings (`Cmd+,`), navigate to the Plugins tab, and toggle your plugin ON.

---

## Plugin Structure

```
~/.config/onda/plugins/
└── my-plugin/
    ├── manifest.json    # Plugin metadata and capabilities
    ├── main.js          # Entry point (runs in Web Worker)
    └── assets/          # Optional: icons, images
```

---

## Manifest Reference

### Required Fields

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique identifier (lowercase, hyphens allowed) |
| `name` | string | Display name |
| `version` | string | Semantic version (e.g., "1.0.0") |
| `main` | string | Entry point file |
| `capabilities` | string[] | Required capabilities (see below) |

### Optional Fields

| Field | Type | Description |
|-------|------|-------------|
| `description` | string | Short description |
| `author` | object | `{ name, email?, url? }` |
| `activationEvents` | string[] | When to activate (default: `["onStartup"]`) |
| `httpDomains` | string[] | Allowed HTTP domains (requires `http` capability) |
| `execPatterns` | string[] | Allowed shell commands (requires `exec` capability) |
| `envVars` | object[] | Environment variables for configuration |
| `contributes` | object | Static contributions (panels, keybindings, themes) |

### Environment Variables

Plugins can request environment variables for user configuration:

```json
{
  "envVars": [
    {
      "name": "API_KEY",
      "description": "Your API key for the service",
      "type": "secret",
      "required": true
    },
    {
      "name": "DEBUG_MODE",
      "description": "Enable debug logging",
      "type": "string",
      "default": "false",
      "required": false
    }
  ]
}
```

Types:
- `string` - Regular text input
- `secret` - Masked input (for API keys, tokens)

---

## Capabilities

Plugins must declare required capabilities in their manifest. Only capabilities listed in the manifest will be available at runtime.

| Capability | Description |
|------------|-------------|
| `commands` | Register commands in Command Palette |
| `notifications` | Show toast notifications |
| `terminal:write` | Write text to active terminal |
| `terminal:read` | Read terminal buffer |
| `terminal:subscribe` | Subscribe to real-time terminal output |
| `exec` | Execute shell commands (requires `execPatterns`) |
| `filesystem:read` | Read files |
| `filesystem:write` | Write files |
| `statusbar` | Add items to status bar |
| `panel` | Create custom side panels |
| `http` | Make HTTP requests (requires `httpDomains`) |
| `storage` | Persistent key-value storage |
| `contextmenu` | Add context menu items |
| `keybindings` | Register custom keyboard shortcuts |
| `themes` | Contribute custom color themes |
| `dialog` | Show modal dialogs with input fields |
| `clipboard` | Read/write system clipboard |
| `appRail` | Add icons to left navigation bar |

---

## API Reference

The `api` object is passed to `onActivate()`:

### api.commands

```javascript
// Register a command
await api.commands.register('plugin-id.command-name', {
  title: 'Command Title',       // Shown in Command Palette
  category: 'Category Name',    // Optional grouping
  keybinding: 'Cmd+Shift+P',    // Optional keyboard shortcut
  icon: 'terminal',             // Optional icon (Lucide name or emoji)
  handler: async (args) => {    // Executed when command runs
    // Your code here
  }
});

// Execute another command
await api.commands.execute('other-plugin.command', [arg1, arg2]);
```

**Available Icons:**

Use Lucide icon names (lowercase, kebab-case) or emoji:

| Icon Name | Description |
|-----------|-------------|
| `terminal` | Terminal icon |
| `git-branch` | Git branch |
| `folder` | Folder |
| `file-text` | Text file |
| `zap` | Lightning bolt |
| `search` | Search |
| `settings` | Settings gear |
| `play`, `pause`, `stop` | Media controls |
| `copy`, `clipboard` | Clipboard actions |
| `download`, `upload` | Transfer |

### api.terminal

Requires `terminal:write` and/or `terminal:read` capabilities.

```javascript
// Write to active terminal
await api.terminal.write('echo "Hello World"\n');

// Read full terminal content
const result = await api.terminal.read();
console.log(result.content); // Full buffer content

// Read with options
const result = await api.terminal.read({ scrollback: 100 }); // Last 100 lines

// Get current line (where cursor is)
const current = await api.terminal.getCurrentLine();
console.log(current.content);

// Get last N lines
const lines = await api.terminal.getLastLines(10);
console.log(lines.content); // Last 10 lines joined with \n
console.log(lines.lines);   // Array of 10 lines

// Subscribe to real-time terminal output (requires terminal:subscribe)
await api.terminal.subscribe({ terminalId: 'active' });

// Listen for terminal output events
api.on('terminal:output', (data) => {
  console.log(data); // { terminalId, data, timestamp }
});

// Unsubscribe
await api.terminal.unsubscribe({ terminalId: 'active' });
```

### api.notifications

Show toast notifications in the bottom-right corner.

```javascript
// Basic notification
api.notifications.show({
  type: 'success',  // 'success' | 'error' | 'info' | 'warning'
  message: 'File saved successfully'
});

// With title
api.notifications.show({
  type: 'error',
  title: 'Connection Failed',
  message: 'Unable to reach the server.'
});

// Custom duration (ms) - default is 4000ms
api.notifications.show({
  type: 'info',
  title: 'Tip',
  message: 'Use Cmd+K to open Command Palette',
  duration: 6000
});

// Persistent (duration: 0)
api.notifications.show({
  type: 'warning',
  title: 'Unsaved Changes',
  message: 'You have unsaved changes.',
  duration: 0
});
```

| Type | Icon | Use Case |
|------|------|----------|
| `success` | Green checkmark | Operation completed |
| `error` | Red circle | Something went wrong |
| `info` | Blue info | Tips, information |
| `warning` | Yellow triangle | Caution, attention needed |

### api.exec

Requires `exec` capability and `execPatterns` whitelist in manifest.

```javascript
const result = await api.exec.run('git status', '/path/to/repo');
// result: { stdout, stderr, exitCode }
```

**Manifest requirement:**
```json
{
  "capabilities": ["exec"],
  "execPatterns": ["git *", "npm *", "ls *"]
}
```

### api.filesystem

Requires `filesystem:read` and/or `filesystem:write` capabilities.

```javascript
// Read file
const { content, error } = await api.filesystem.readFile('/path/to/file');

// Write file
await api.filesystem.writeFile('/path/to/file', 'content');

// Read directory
const entries = await api.filesystem.readDir('/path/to/dir');
// entries: [{ name, is_dir, is_file, size, modified }]
```

### api.statusBar

Requires `statusbar` capability.

```javascript
// Add item to status bar
await api.statusBar.addItem({
  id: 'my-item',
  text: 'Status Text',
  icon: 'zap',
  tooltip: 'Hover text',
  position: 'right',       // 'left' or 'right'
  color: '#a78bfa',
  onClick: 'my-plugin.cmd' // Command ID to execute on click
});

// Update existing item
await api.statusBar.updateItem('my-item', { text: 'New Text', icon: 'check' });

// Remove item
await api.statusBar.removeItem('my-item');
```

### api.panel

Requires `panel` capability.

```javascript
// Register a panel
await api.panel.register({
  id: 'my-panel',
  title: 'My Panel',
  icon: 'layout-dashboard',
  position: 'right'  // 'left' or 'right'
});

// Set panel HTML content
await api.panel.setContent('my-panel', '<div>Hello World</div>');

// Show/hide/toggle panel
await api.panel.show('my-panel');
await api.panel.hide('my-panel');
await api.panel.toggle('my-panel');
```

Panel content is rendered as HTML. Style with inline styles:

```javascript
const content = `
  <div style="color: #e4e4e7; padding: 12px;">
    <h3 style="color: #a78bfa;">Title</h3>
    <p>Content here</p>
  </div>
`;
await api.panel.setContent('my-panel', content);
```

### api.http

Requires `http` capability. Uses Electron's `net` module to bypass CORS.

```javascript
const response = await api.http.fetch('https://api.example.com/data', {
  method: 'GET',
  headers: { 'Authorization': 'Bearer token' }
});

// response: { ok, status, statusText, headers, data }
if (response.ok) {
  console.log(response.data);
}
```

**Manifest requirement:**
```json
{
  "capabilities": ["http"],
  "httpDomains": ["api.example.com"]
}
```

### api.storage

Requires `storage` capability. Persistent key-value storage, namespaced per plugin.

```javascript
await api.storage.set('myKey', { foo: 'bar' });
const value = await api.storage.get('myKey'); // { foo: 'bar' }
await api.storage.remove('myKey');
await api.storage.clear();
```

### api.contextMenu

Requires `contextmenu` capability.

```javascript
// Register a context menu item
await api.contextMenu.register('file-panel', {
  id: 'my-action',
  label: 'My Custom Action',
  icon: 'wrench'
});

// Available contexts:
// - 'file-panel'    - Right-click on file/folder
// - 'file-panel-bg' - Right-click on file panel background
// - 'terminal'      - Right-click on terminal
// - 'tab'           - Right-click on tab
// - 'global'        - Available in all contexts

// Update
await api.contextMenu.update('file-panel', {
  id: 'my-action',
  label: 'Updated Label',
  disabled: true
});

// Remove
await api.contextMenu.unregister('my-action');
```

### api.keybindings

Requires `keybindings` capability.

**Manifest-based registration (recommended):**

```json
{
  "contributes": {
    "keybindings": [
      {
        "command": "my-plugin.search",
        "key": "Cmd+Shift+S",
        "label": "Quick Search"
      },
      {
        "command": "my-plugin.run",
        "key": "Cmd+Alt+R",
        "when": "terminalFocus"
      }
    ]
  }
}
```

**Dynamic registration:**

```javascript
await api.keybindings.register({
  command: 'my-plugin.action',
  key: 'Cmd+Shift+X',
  label: 'My Action',
  when: 'always'  // 'always' | 'terminalFocus' | 'editorFocus'
});

await api.keybindings.unregister('my-plugin.action');
const binding = await api.keybindings.get('my-plugin.action');
```

**Key string format:**
- Modifiers: `Cmd`, `Ctrl`, `Alt`, `Shift`
- Combine with `+`: `Cmd+Shift+P`, `Ctrl+Alt+X`
- Single keys: `F1`, `Escape`, `Enter`, `A`

**Context conditions:**
- `always` (default): Works everywhere except when terminal is focused
- `terminalFocus`: Only when terminal is focused
- `editorFocus`: Only when editor is focused

Keybindings are automatically disabled when the terminal is focused (except `terminalFocus` bindings) to avoid conflicts with CLI tools like vim or Claude Code.

### api.themes

Requires `themes` capability.

**Manifest-based registration (recommended):**

```json
{
  "contributes": {
    "themes": [
      {
        "id": "ocean",
        "name": "Ocean Blue",
        "isDark": true,
        "colors": {
          "bgPrimary": "#0a192f",
          "bgSecondary": "#112240",
          "bgTertiary": "#1d3557",
          "bgHover": "#233554",
          "bgActive": "#303c55",
          "textPrimary": "#ccd6f6",
          "textSecondary": "#8892b0",
          "textMuted": "#495670",
          "textInverse": "#0a192f",
          "borderPrimary": "#233554",
          "borderSecondary": "#1d3557",
          "borderFocus": "#64ffda",
          "accent": "#64ffda",
          "accentHover": "#4fd1c5",
          "accentMuted": "#319795",
          "success": "#64ffda",
          "warning": "#ffd166",
          "error": "#ff6b6b",
          "info": "#4299e1",
          "terminal": "#020c1b",
          "selection": "rgba(100, 255, 218, 0.2)"
        }
      }
    ]
  }
}
```

**Dynamic registration:**

```javascript
await api.themes.register({
  id: 'my-theme',
  name: 'My Theme',
  isDark: true,
  colors: { /* all 22 color tokens */ }
});

await api.themes.activate('my-plugin.my-theme');
const themes = await api.themes.list();
const current = await api.themes.getCurrent();
```

**Required color tokens (22 total):**

| Category | Tokens |
|----------|--------|
| Backgrounds | `bgPrimary`, `bgSecondary`, `bgTertiary`, `bgHover`, `bgActive` |
| Text | `textPrimary`, `textSecondary`, `textMuted`, `textInverse` |
| Borders | `borderPrimary`, `borderSecondary`, `borderFocus` |
| Accent | `accent`, `accentHover`, `accentMuted` |
| Semantic | `success`, `warning`, `error`, `info` |
| Special | `terminal`, `selection` |

**Built-in themes:** `dark`, `midnight`, `dracula`, `light`

### api.dialog

Requires `dialog` capability.

```javascript
// Custom dialog with input fields
const result = await api.dialog.show({
  title: 'Enter Info',
  message: 'Please provide the following:',
  fields: [
    { id: 'name', label: 'Name', type: 'text', placeholder: 'Enter name...', required: true },
    { id: 'email', label: 'Email', type: 'text', placeholder: 'your@email.com' }
  ],
  buttons: [
    { id: 'cancel', label: 'Cancel', variant: 'secondary' },
    { id: 'confirm', label: 'OK', variant: 'primary' }
  ]
});
// result: { buttonId: 'confirm', fields: { name: '...', email: '...' } }

// Alert dialog
await api.dialog.alert({ title: 'Alert', message: 'Something happened!' });

// Confirm dialog
const result = await api.dialog.confirm({
  title: 'Confirm',
  message: 'Are you sure?'
});
// result: { buttonId: 'confirm' } or { buttonId: 'cancel' }
```

**Button variants:** `primary`, `secondary`, `danger`

### api.clipboard

Requires `clipboard` capability.

```javascript
await api.clipboard.write('Text to copy');
const { text } = await api.clipboard.read();
```

### api.appRail

Requires `appRail` capability. Add icons to the left navigation bar.

```javascript
await api.appRail.addItem({
  id: 'my-icon',
  icon: 'settings',       // Emoji or Lucide icon name
  tooltip: 'My Panel',
  priority: 50,           // Lower = higher in list
  panelId: 'my-panel'     // Optional: panel to toggle on click
});

await api.appRail.updateItem('my-icon', {
  icon: 'check',
  badge: '5',
  tooltip: 'Updated'
});

await api.appRail.removeItem('my-icon');
```

### api.env

Environment variables for plugin configuration, defined in manifest.

```javascript
const apiKey = await api.env.get('API_KEY');
const allEnv = await api.env.getAll();
// Returns: { API_KEY: '...', DEBUG_MODE: 'true', ... }
```

Users set values in Settings > Plugins > [Your Plugin] > Environment Variables.

### api.on / api.off

Event subscription for plugin-related events.

```javascript
api.on('terminal:output', (data) => {
  // { terminalId, data, timestamp }
});

api.on('panel:action', (data) => {
  // { panelId, actionId }
});

// Unsubscribe
api.off('terminal:output', handler);
```

| Event | Description | Data |
|-------|-------------|------|
| `terminal:output` | Real-time terminal data | `{ terminalId, data, timestamp }` |
| `panel:action` | Panel button clicks | `{ panelId, actionId }` |

---

## Examples

### Git Status Plugin

```javascript
// manifest.json
{
  "id": "git-status",
  "name": "Git Status",
  "version": "1.0.0",
  "main": "main.js",
  "capabilities": ["commands", "exec", "notifications"],
  "execPatterns": ["git status", "git branch"]
}

// main.js
self.__ondaPlugin = {
  onActivate: async (api) => {
    await api.commands.register('git-status.show', {
      title: 'Show Git Status',
      category: 'Git',
      handler: async () => {
        try {
          const result = await api.exec.run('git status --short');
          api.notifications.show({
            type: 'info',
            message: result.stdout || 'Working tree clean'
          });
        } catch (err) {
          api.notifications.show({
            type: 'error',
            message: err.message
          });
        }
      }
    });
  }
};
```

### File Template Plugin

```javascript
// manifest.json
{
  "id": "file-templates",
  "name": "File Templates",
  "version": "1.0.0",
  "main": "main.js",
  "capabilities": ["commands", "filesystem:write", "notifications"]
}

// main.js
self.__ondaPlugin = {
  onActivate: async (api) => {
    await api.commands.register('file-templates.create-readme', {
      title: 'Create README.md',
      category: 'Templates',
      handler: async () => {
        const template = `# Project Name\n\n## Description\n\n## Installation\n\n## Usage\n\n## License\n`;
        await api.filesystem.writeFile('./README.md', template);
        api.notifications.show({
          type: 'success',
          message: 'README.md created!'
        });
      }
    });
  }
};
```

### Full Capability Demo

```javascript
// manifest.json
{
  "id": "capability-demo",
  "name": "Capability Demo",
  "version": "1.0.0",
  "main": "main.js",
  "capabilities": ["commands", "statusbar", "http", "storage", "panel", "notifications"]
}

// main.js
self.__ondaPlugin = {
  onActivate: async (api) => {
    // Status bar item
    await api.statusBar.addItem({
      id: 'demo-status',
      text: 'Demo',
      icon: 'zap',
      tooltip: 'Click to fetch data',
      position: 'right',
      onClick: 'capability-demo.fetch'
    });

    // Register panel
    await api.panel.register({
      id: 'demo-panel',
      title: 'Demo Panel',
      position: 'right'
    });

    // Fetch command
    await api.commands.register('capability-demo.fetch', {
      title: 'Fetch & Display Data',
      category: 'Demo',
      handler: async () => {
        await api.statusBar.updateItem('demo-status', { text: 'Loading...', icon: 'loader' });

        try {
          const response = await api.http.fetch('https://jsonplaceholder.typicode.com/todos/1');
          await api.storage.set('lastData', response.data);
          await api.panel.setContent('demo-panel', `
            <div style="color: #e4e4e7; padding: 12px;">
              <pre>${JSON.stringify(response.data, null, 2)}</pre>
            </div>
          `);
          await api.panel.show('demo-panel');
          await api.statusBar.updateItem('demo-status', { text: 'Done!', icon: 'check' });
          api.notifications.show({ type: 'success', message: 'Data fetched and stored!' });
        } catch (err) {
          api.notifications.show({ type: 'error', message: err.message });
        }
      }
    });
  }
};
```

---

## Security Model

### Sandboxing

- Plugins run in isolated **Web Workers** (no DOM access)
- All host communication goes through a message-passing bridge
- Capabilities must be declared upfront in the manifest

### Permission Levels

1. **Safe** - No user prompt required:
   - `commands`, `notifications`, `statusbar`

2. **Moderate** - Currently allowed, future: user prompt:
   - `terminal:write`, `filesystem:read`, `filesystem:write`

3. **Restricted** - Requires whitelist in manifest:
   - `exec` - Must specify `execPatterns`
   - `http` - Must specify `httpDomains`

---

## Best Practices

### 1. Minimal Capabilities

Only request capabilities you actually need:

```json
// Too broad
"capabilities": ["commands", "exec", "filesystem:read", "filesystem:write", "http"]

// Minimal
"capabilities": ["commands", "notifications"]
```

### 2. Specific exec Patterns

```json
// Too permissive
"execPatterns": ["*"]

// Specific
"execPatterns": ["git status", "git branch", "git log --oneline -10"]
```

### 3. Error Handling

Always wrap API calls in try/catch:

```javascript
handler: async () => {
  try {
    const result = await api.exec.run('some-command');
    // Handle success
  } catch (err) {
    api.notifications.show({
      type: 'error',
      message: `Failed: ${err.message}`
    });
  }
}
```

### 4. Fast Activation

Keep `onActivate` fast - defer heavy work to command handlers:

```javascript
onActivate: async (api) => {
  // Fast: just register commands
  await api.commands.register('my-plugin.heavy-task', {
    title: 'Run Heavy Task',
    handler: async () => {
      // Heavy work happens on-demand
      await doHeavyWork();
    }
  });
}
```

---

## Debugging

### Console Logs

Plugin logs appear in DevTools Console (`Cmd+Option+I`):

```javascript
console.log('[MyPlugin] Debug message');
```

### Common Issues

**Plugin not loading:**
- Check `~/.config/onda/plugins/your-plugin/manifest.json` exists
- Verify JSON is valid
- Check DevTools Console for errors

**Commands not appearing:**
- Ensure `capabilities` includes `"commands"`
- Check command ID is unique
- Reload app after changes

**Permission denied:**
- Add required capability to manifest
- For `exec`, check `execPatterns`
- For `http`, check `httpDomains`

---

## Questions?

Open an issue at: [github.com/mindfullabai/onda/issues](https://github.com/mindfullabai/onda/issues)
