Creating Plugins

This guide walks you through creating a TeamIDE plugin from scratch.

Prerequisites

  • Node.js 18+ installed
  • Basic knowledge of Vue 3 and TypeScript
  • A GitHub or GitLab account (for publishing)

Project Structure

A minimal plugin has this structure:

my-plugin/
  teamide-plugin.json    # Plugin manifest (required)
  features.json          # Toggleable features (optional)
  dist/
    index.js             # Compiled bundle (required)
  src/
    index.ts             # Entry point
    components/
      Navigation.vue     # Left panel component
      Main.vue           # Center panel component
      Context.vue        # Right panel component (optional)
  package.json
  vite.config.ts         # Build configuration

Step 1: Create the Plugin Manifest

Create teamide-plugin.json at the repository root:

{
  "id": "my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "entry": "./dist/index.js",
  "description": "A custom TeamIDE plugin",
  "icon": "widgets"
}

Important: The id must be unique across all plugins. Use lowercase with hyphens.

Step 2: Set Up the Project

Initialize your project:

mkdir my-plugin && cd my-plugin
npm init -y
npm install -D vite vue typescript @vitejs/plugin-vue

Create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "jsx": "preserve",
    "declaration": true,
    "declarationDir": "./dist/types",
    "sourceMap": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "lib": ["ES2020", "DOM"],
    "skipLibCheck": true,
    "paths": {
      "vue": ["./node_modules/vue"],
      "pinia": ["./node_modules/pinia"],
      "quasar": ["./node_modules/quasar"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Step 3: Configure the Build

Create vite.config.ts:

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  plugins: [vue()],
  build: {
    lib: {
      entry: 'src/index.ts',
      name: 'MyPlugin',
      formats: ['iife'],
      fileName: () => 'index.js'
    },
    outDir: 'dist',
    rollupOptions: {
      external: ['vue', 'pinia', 'quasar'],
      output: {
        globals: {
          vue: 'Vue',
          pinia: 'Pinia',
          quasar: 'Quasar'
        },
        // Ensure the module definition is exported correctly
        footer: `
          if (typeof exports !== 'undefined' && MyPlugin && MyPlugin.default) {
            exports.default = MyPlugin.default;
          }
        `
      }
    }
  }
});

Key points: - Build as IIFE format (not ESM) - Mark vue, pinia, quasar as external (provided by TeamIDE) - Export the module definition as default

Step 4: Create Components

Create src/components/Navigation.vue:

<template>
  <div class="my-plugin-nav q-pa-md">
    <div class="text-h6 q-mb-md">My Plugin</div>
    <q-list>
      <q-item clickable v-ripple @click="doSomething">
        <q-item-section avatar>
          <q-icon name="star" />
        </q-item-section>
        <q-item-section>Action Item</q-item-section>
      </q-item>
    </q-list>
  </div>
</template>

<script setup lang="ts">
function doSomething() {
  console.log('Action clicked!');
}
</script>

<style scoped>
.my-plugin-nav {
  height: 100%;
}
</style>

Main Component (Center Panel)

Create src/components/Main.vue:

<template>
  <div class="my-plugin-main q-pa-lg">
    <h1>Welcome to My Plugin</h1>
    <p>This is the main content area.</p>

    <q-btn
      color="primary"
      label="Open a File"
      @click="openFile"
    />
  </div>
</template>

<script setup lang="ts">
async function openFile() {
  // Use the TeamIDE API to open a file
  if (window.__teamide) {
    await window.__teamide.openFile('/README.md');
  }
}
</script>

<style scoped>
.my-plugin-main {
  height: 100%;
  overflow: auto;
}
</style>

Context Component (Right Panel - Optional)

Create src/components/Context.vue:

<template>
  <div class="my-plugin-context q-pa-md">
    <div class="text-subtitle1 q-mb-md">Details</div>
    <p>Additional context information goes here.</p>
  </div>
</template>

<script setup lang="ts">
// Optional: Add logic for the context panel
</script>

Step 5: Create the Entry Point

Create src/index.ts:

import { defineComponent } from 'vue';
import Navigation from './components/Navigation.vue';
import Main from './components/Main.vue';
import Context from './components/Context.vue';

// Define the module for TeamIDE
const moduleDefinition = {
  id: 'my-plugin',
  name: 'My Plugin',
  icon: 'widgets',
  version: '1.0.0',

  // Required components
  navigationComponent: Navigation,
  mainComponent: Main,

  // Optional components
  contextComponent: Context,

  // Optional lifecycle hooks
  onRegister() {
    console.log('[My Plugin] Registered');
  },

  onActivate() {
    console.log('[My Plugin] Activated');
  },

  onDeactivate() {
    console.log('[My Plugin] Deactivated');
  }
};

export default moduleDefinition;

Step 6: Add TypeScript Declarations

Create src/types.d.ts for the TeamIDE API:

interface PluginAPI {
  openFile: (path: string, projectId?: string) => Promise<void>;
  getProjectId: () => string | null;
  switchModule: (moduleId: string) => void;
  getGitHubToken: () => string | null;
  getGitHubUsername: () => string | null;
  getPluginSettings: (pluginId: string) => Promise<Record<string, unknown> | null>;
  savePluginSettings: (pluginId: string, settings: Record<string, unknown>) => Promise<void>;
}

declare global {
  interface Window {
    __teamide?: PluginAPI;
  }
}

export {};

Step 7: Build the Plugin

Add build scripts to package.json:

{
  "scripts": {
    "build": "vite build",
    "dev": "vite build --watch"
  }
}

Build your plugin:

npm run build

This creates dist/index.js - your plugin bundle.

Step 8: Test Locally

Option A: Add from Local Path

  1. Build your plugin: npm run build
  2. In TeamIDE, go to Settings > Plugins
  3. Click “Add Local Plugin”
  4. Enter the full path to your plugin folder
  5. Click Add

Option B: Add from Workspace

  1. Clone your plugin repository in TeamIDE
  2. Build the plugin (use the terminal module)
  3. In Settings > Plugins, select your repo from the workspace list

Step 9: Publish to GitHub

  1. Ensure dist/index.js is committed (don’t gitignore it)
  2. Push to GitHub
  3. In TeamIDE Settings > Plugins, add the GitHub URL

Example URL: https://github.com/yourusername/my-plugin

Adding Toggleable Features

Create features.json to let users toggle plugin behaviors:

{
  "module": {
    "id": "my-plugin",
    "name": "My Plugin",
    "version": "1.0.0"
  },
  "features": [
    {
      "id": "my-plugin-auto-refresh",
      "name": "Auto Refresh",
      "description": "Automatically refresh data every 30 seconds",
      "default": true,
      "category": "general"
    },
    {
      "id": "my-plugin-dark-mode",
      "name": "Dark Mode",
      "description": "Use dark theme for plugin UI",
      "default": false,
      "category": "advanced"
    }
  ]
}

Check feature status in your code:

import { isFeatureEnabled } from '@teamide/features'; // Pseudo-code

// In your component
const autoRefresh = isFeatureEnabled('my-plugin-auto-refresh');

Using Plugin Settings

Persist user preferences with the settings API:

// Save settings
await window.__teamide?.savePluginSettings('my-plugin', {
  theme: 'dark',
  refreshInterval: 30
});

// Load settings
const settings = await window.__teamide?.getPluginSettings('my-plugin');
if (settings) {
  console.log('Theme:', settings.theme);
}

Module Lifecycle

Plugins can hook into the module lifecycle:

Hook When Called Use Case
onRegister Plugin loads at app startup Initialize services, register actions
onActivate User clicks the plugin tab Load data, start timers
onDeactivate User switches to another tab Cleanup, stop timers
const moduleDefinition = {
  // ... other fields

  onRegister() {
    // Called once when plugin loads
    // Good for: registering global actions, setting up services
  },

  onActivate() {
    // Called when user switches TO this plugin
    // Good for: fetching fresh data, starting intervals
  },

  onDeactivate() {
    // Called when user switches AWAY from this plugin
    // Good for: cleanup, stopping intervals
  }
};

Render Modes

Plugins can render in two modes:

Default Mode (Standard Tab)

The plugin shows as a full tab with navigation, main, and context panels:

const moduleDefinition = {
  id: 'my-plugin',
  renderMode: 'default', // or omit (default is 'default')
  // ...
};

Overlay Mode (Bottom Panel)

The plugin renders as a bottom overlay, like the Terminal module. The tab acts as a toggle:

const moduleDefinition = {
  id: 'my-overlay-plugin',
  renderMode: 'overlay',
  mainComponent: OverlayContent, // Renders in bottom panel
  // navigationComponent not used in overlay mode
};

Best Practices

Do

  • Use Quasar components for consistent UI
  • Handle errors gracefully
  • Clean up resources in onDeactivate
  • Use the Plugin API instead of direct DOM manipulation
  • Keep bundle size small

Don’t

  • Bundle Vue, Pinia, or Quasar (they’re provided)
  • Access internal TeamIDE components or stores
  • Modify global state outside your plugin
  • Make blocking network calls in lifecycle hooks

Example Plugins

Minimal Plugin

A plugin with just a main component:

import { defineComponent, h } from 'vue';

export default {
  id: 'minimal-plugin',
  name: 'Minimal',
  icon: 'star',
  version: '1.0.0',
  navigationComponent: defineComponent({
    setup() {
      return () => h('div', { class: 'q-pa-md' }, 'Navigation');
    }
  }),
  mainComponent: defineComponent({
    setup() {
      return () => h('div', { class: 'q-pa-lg' }, 'Hello from Minimal Plugin!');
    }
  })
};

Plugin with State

Using Pinia for state management:

import { defineStore } from 'pinia';
import { defineComponent, h, computed } from 'vue';

// Define a store for your plugin
const useMyStore = defineStore('my-plugin-store', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment() {
      this.count++;
    }
  }
});

// Use it in your component
const Main = defineComponent({
  setup() {
    const store = useMyStore();
    return () => h('div', { class: 'q-pa-lg' }, [
      h('p', `Count: ${store.count}`),
      h('button', { onClick: () => store.increment() }, 'Increment')
    ]);
  }
});

Troubleshooting

“Plugin does not export a default ModuleDefinition”

Ensure your entry file exports the module definition as default:

export default moduleDefinition;

And your Vite config includes the footer to set exports.default.

Components not rendering

  1. Check that components are valid Vue components
  2. Verify the bundle built successfully
  3. Look for JavaScript errors in the console

Styles not applying

Use scoped styles or prefix your class names to avoid conflicts:

<style scoped>
.my-plugin-container { /* styles */ }
</style>

API methods return null

  • getProjectId() returns null if no repository is selected
  • getGitHubToken() returns null if no GitHub account is connected

Changelog

Date Change
2026-02-02 Initial documentation