- Add quick start section for development and distribution - Expand project structure to include host and CI/CD files - Add detailed code examples for lifecycle methods and context API - Document invoke method pattern for handling addon actions - Improve descriptions of manifest fields and permissions - Add native host build instructions
My First Addon
A comprehensive demo addon for GitCaddy that showcases all available addon features and APIs. Use this as a reference implementation and template for building your own addons.
Overview
This addon demonstrates:
- Addon Manifest (
addon.json) - How to define your addon's metadata, permissions, and contributions - Main Process Module (
main/index.js) - Lifecycle methods, IPC, settings, and method invocations - Renderer Process Module (
renderer/index.js) - UI components and renderer-side communication - Custom Views - HTML-based views for repository panels and settings
- Native Host (
host/) - .NET backend for computationally intensive or platform-specific operations - Context Menus - Right-click menu items for files and commits
- Notifications - System notifications from addon actions
- CI/CD - Automated build and release workflow
Project Structure
myfirst-addon/
├── addon.json # Addon manifest (required)
├── main/
│ └── index.js # Main process module (required)
├── renderer/
│ └── index.js # Renderer process module (optional)
├── shared/
│ └── types.js # Shared constants and types
├── views/
│ ├── demo.html # Demo repository view
│ └── settings.html # Settings panel view
├── host/ # Native .NET host (optional)
│ ├── Program.cs # Host entry point
│ └── MyFirstAddon.Host.csproj
├── .gitea/
│ └── workflows/
│ └── release.yml # CI/CD workflow
├── .gitignore
└── README.md # This file
Quick Start
For Development
-
Clone/copy this folder to GitCaddy's sideload directory:
Windows: %APPDATA%/GitCaddy/sideload-addons/ macOS: ~/Library/Application Support/GitCaddy/sideload-addons/ Linux: ~/.config/GitCaddy/sideload-addons/ -
If using the .NET host, build it first:
cd host dotnet publish -c Release -r win-x64 --self-contained -p:PublishSingleFile=true -o ../host/win-x64 -
Restart GitCaddy or reload addons from the Addon Manager
For Distribution
Package as a .gcaddon file (zip with different extension) and distribute via:
- Addon Manager's "Install from file" option
- Your addon repository/marketplace
Addon Manifest (addon.json)
The manifest defines everything about your addon.
Required Fields
| Field | Description |
|---|---|
id |
Unique identifier (reverse domain notation: com.yourcompany.addon-name) |
name |
Display name shown in Addon Manager |
version |
Semantic version (major.minor.patch) |
minAppVersion |
Minimum GitCaddy version required |
description |
Short description for the addon list |
main |
Path to main process JavaScript module |
Optional Fields
| Field | Description |
|---|---|
renderer |
Path to renderer process module for UI components |
author |
Author information (name, email, url) |
host |
Native host configuration for .NET/Go/Rust backends |
permissions |
Required permissions (shown to user before install) |
capabilities |
What your addon can do |
contributes |
UI elements your addon adds |
Permissions
"permissions": [
"repository:read", // Read repository data
"repository:write", // Modify repository
"diff:read", // Read diff/changes
"commit:read", // Read commit history
"branch:read", // Read branch information
"network:localhost", // Make local network requests
"network:external", // Make external network requests
"settings:store", // Store addon settings
"notifications:show" // Show system notifications
]
Capabilities
"capabilities": [
"commit-message-generation", // Can generate commit messages
"repository-analysis", // Can analyze repositories
"custom-view" // Has custom UI views
]
Main Process Module
The main module (main/index.js) runs in Electron's main process with full Node.js access.
Lifecycle Methods
class MyAddon {
constructor() {
this.context = null;
this.isActive = false;
}
// Called once when addon is loaded
async initialize(context) {
this.context = context;
context.log.info('Addon initializing...');
// Register event listeners
const disposable = context.events.onRepositorySelected((repo) => {
this.onRepositoryChanged(repo);
});
this.disposables.push(disposable);
}
// Called when addon is enabled - MUST return true
async activate() {
this.context?.log.info('Addon activating...');
this.isActive = true;
return true; // Required for successful activation
}
// Called when addon is disabled
async deactivate() {
this.context?.log.info('Addon deactivating...');
this.isActive = false;
}
// Called when addon is unloaded - clean up resources
dispose() {
for (const disposable of this.disposables) {
disposable.dispose();
}
this.disposables = [];
this.context = null;
}
}
module.exports = MyAddon;
module.exports.default = MyAddon;
Important: The activate() method must return true to indicate successful activation.
Context Object
The context object provides access to GitCaddy APIs:
context.addonId // Your addon's ID
context.addonPath // Path to addon directory
context.manifest // Parsed addon.json
context.log // Logger (.debug, .info, .warn, .error)
context.settings // Settings storage
context.events // Event subscription
context.ipc // IPC communication
context.hostPort // Port of native host (if configured)
Invoke Method
GitCaddy calls your addon's invoke method for all actions (toolbar clicks, menu items, context menus, etc.):
async invoke(method, args) {
switch (method) {
case 'getSettings':
return this.getSettings();
case 'processFiles':
return this.processFiles(args[0]);
case 'analyzeCommit':
return this.analyzeCommit(args[0]);
default:
throw new Error(`Unknown method: ${method}`);
}
}
Showing Notifications
Display system notifications from your addon:
this.context?.ipc.send('show-notification', {
title: 'My Addon',
body: 'Operation completed successfully!',
});
Event Subscription
Subscribe to app events via context.events:
// Repository changed
context.events.onRepositorySelected((repo) => {
console.log('Repository:', repo.path);
});
// Commit created
context.events.onCommitCreated((commit) => {
console.log('New commit:', commit.sha);
});
// Files changed
context.events.onFilesChanged((files) => {
console.log('Files changed:', files.length);
});
// App lifecycle
context.events.onAppReady(() => { });
context.events.onAppWillQuit(() => { });
Settings Storage
// Load settings
const stored = context.settings.getAll();
const value = context.settings.get('myKey');
// Save settings
await context.settings.set('myKey', 'myValue');
// Listen for changes
context.settings.onDidChange((key, value) => {
console.log(`Setting ${key} changed to:`, value);
});
License Status (Optional)
For commercial addons:
getLicenseStatus() {
return { valid: true }; // Free addon
// Commercial addon:
// return {
// valid: this.isLicenseValid,
// message: this.isLicenseValid ? undefined : 'License expired'
// };
}
Contributions
UI elements your addon adds to GitCaddy.
Toolbar Buttons
{
"id": "my-button",
"tooltip": "Button Tooltip",
"icon": { "type": "octicon", "value": "rocket" },
"position": "after-push-pull",
"onClick": {
"action": "show-view",
"target": "my-view"
},
"badge": {
"method": "getBadgeContent"
}
}
Position options: start, end, after-push-pull, after-branch
onClick actions:
show-view- Show a repository viewinvoke-method- Call an addon methodopen-url- Open a URL in browser
Menu Items
{
"id": "my-menu-item",
"menu": "repository",
"label": "My Action",
"accelerator": "CmdOrCtrl+Shift+M",
"onClick": {
"action": "invoke-method",
"method": "menuAction"
}
}
Context Menu Items
Add items to right-click menus:
"contextMenuItems": [
{
"id": "file-action",
"context": "file-list",
"label": "Process with My Addon",
"onClick": {
"action": "invoke-method",
"method": "processFiles"
}
},
{
"id": "commit-action",
"context": "commit-list",
"label": "Analyze with My Addon",
"onClick": {
"action": "invoke-method",
"method": "analyzeCommit"
}
}
]
Context options: file-list (Changes tab), commit-list (History tab)
The invoke method receives context data:
async processFiles(contextData) {
const files = contextData?.files || [];
// files: [{ path: 'src/file.js', status: 'modified' }, ...]
}
async analyzeCommit(contextData) {
const commit = contextData?.commit;
// commit: { sha, summary, body, author: { name, email, date } }
}
Repository Views
{
"id": "my-view",
"title": "My View",
"icon": "rocket",
"renderer": {
"type": "iframe",
"source": "views/my-view.html"
},
"context": ["repository"],
"headerActions": [
{
"id": "refresh",
"label": "Refresh",
"icon": "sync",
"primary": true
},
{
"id": "settings",
"label": "Settings",
"icon": "gear"
}
]
}
Context options: repository, diff, commit, always
Header Actions: Buttons displayed in GitCaddy's view header (next to close button). Use primary: true to highlight.
Settings Definitions
"settingsDefinitions": [
{
"key": "enableFeature",
"type": "boolean",
"default": true,
"description": "Enable the main feature"
},
{
"key": "apiKey",
"type": "string",
"default": "",
"description": "API key for external service"
},
{
"key": "mode",
"type": "select",
"default": "normal",
"options": [
{ "label": "Normal", "value": "normal" },
{ "label": "Advanced", "value": "advanced" }
]
},
{
"key": "maxItems",
"type": "number",
"default": 10
}
]
View Communication
Views (iframes) communicate with GitCaddy via postMessage.
Receiving Messages
const ADDON_ID = 'com.example.myfirst-addon';
window.addEventListener('message', (event) => {
const { type, data, actionId, requestId, result, error } = event.data || {};
switch (type) {
case 'addon:init-context':
// Initial context from GitCaddy
const { repositoryPath, hostPort } = data;
initializeView();
break;
case 'addon:header-action':
// Header button clicked
handleHeaderAction(actionId);
break;
case 'addon:invoke-response':
// Response from invoke request
handleInvokeResponse(requestId, result, error);
break;
}
});
Invoking Addon Methods
const pendingRequests = new Map();
let requestIdCounter = 0;
function invokeAddon(method, ...args) {
return new Promise((resolve, reject) => {
const requestId = ++requestIdCounter;
pendingRequests.set(requestId, { resolve, reject });
window.parent.postMessage({
type: 'addon:invoke',
requestId,
addonId: ADDON_ID,
method,
args
}, '*');
setTimeout(() => {
if (pendingRequests.has(requestId)) {
pendingRequests.delete(requestId);
reject(new Error('Request timed out'));
}
}, 30000);
});
}
// Handle responses
window.addEventListener('message', (event) => {
if (event.data?.type === 'addon:invoke-response') {
const { requestId, result, error } = event.data;
const pending = pendingRequests.get(requestId);
if (pending) {
pendingRequests.delete(requestId);
error ? pending.reject(new Error(error)) : pending.resolve(result);
}
}
});
// Usage
const data = await invokeAddon('getDemoData');
Other Messages
// Open addon settings
window.parent.postMessage({
type: 'addon:open-settings',
addonId: ADDON_ID
}, '*');
Dark Mode Styling
GitCaddy injects CSS variables for theming. Always use these to match the app's appearance:
/* Background */
background-color: var(--background, #1e1e1e);
background-color: var(--background-secondary, #252526);
/* Text */
color: var(--foreground, #cccccc);
color: var(--foreground-muted, #999999);
/* Border */
border-color: var(--border, #3c3c3c);
/* Accent colors */
color: var(--accent, #0e639c);
color: var(--success, #4ec9b0);
color: var(--warning, #fcba03);
color: var(--error, #f14c4c);
color: var(--info, #75beff);
/* Buttons */
background-color: var(--button-background, #0e639c);
background-color: var(--button-secondary-background, #3c3c3c);
Dark Mode Scrollbars
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: var(--background-secondary, #252526);
}
::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb, #5a5a5a);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-thumb-hover, #7a7a7a);
}
/* Firefox */
* {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb, #5a5a5a) var(--background-secondary, #252526);
}
Native Host (.NET)
For addons requiring native code, performance-critical operations, or platform-specific features.
Host Configuration
"host": {
"directory": "host",
"executable": "MyFirstAddon.Host",
"platforms": {
"win-x64": "host/win-x64/MyFirstAddon.Host.exe",
"linux-x64": "host/linux-x64/MyFirstAddon.Host",
"darwin-x64": "host/darwin-x64/MyFirstAddon.Host",
"darwin-arm64": "host/darwin-arm64/MyFirstAddon.Host"
},
"healthCheck": "/health",
"startupTimeout": 10000
}
Host Implementation (Program.cs)
var port = Environment.GetEnvironmentVariable("ADDON_HOST_PORT") ?? "5000";
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls($"http://127.0.0.1:{port}");
builder.Logging.SetMinimumLevel(LogLevel.Warning);
var app = builder.Build();
// Health check endpoint (required)
app.MapGet("/health", () => new {
status = "healthy",
timestamp = DateTime.UtcNow
});
// Your endpoints
app.MapGet("/hello", () => new {
message = "Hello from .NET host!"
});
app.MapGet("/hello/{name}", (string name) => new {
message = $"Hello, {name}!"
});
Console.WriteLine($"MyFirstAddon.Host starting on http://127.0.0.1:{port}");
app.Run();
Building the Host
# Windows
dotnet publish -c Release -r win-x64 --self-contained -p:PublishSingleFile=true -o host/win-x64
# Linux
dotnet publish -c Release -r linux-x64 --self-contained -p:PublishSingleFile=true -o host/linux-x64
# macOS (Intel)
dotnet publish -c Release -r osx-x64 --self-contained -p:PublishSingleFile=true -o host/darwin-x64
# macOS (Apple Silicon)
dotnet publish -c Release -r osx-arm64 --self-contained -p:PublishSingleFile=true -o host/darwin-arm64
Calling the Host
async callHost(endpoint, name) {
if (!this.context?.hostPort) {
return { error: 'Host not available' };
}
const url = name
? `http://127.0.0.1:${this.context.hostPort}${endpoint}/${encodeURIComponent(name)}`
: `http://127.0.0.1:${this.context.hostPort}${endpoint}`;
const response = await fetch(url);
return await response.json();
}
CI/CD Workflow
The .gitea/workflows/release.yml automates building and releasing:
- Create Release - Creates a GitHub/Gitea release
- Build Host - Builds .NET host for all platforms (Windows, Linux, macOS x64/ARM64)
- Package Addon - Creates distribution packages:
myfirst-addon-full-{version}.gcaddon- Complete package with all platform hostsmyfirst-addon-base-{version}.gcaddon- JavaScript only (no hosts)- Individual host binaries for each platform
Triggering a Release
git tag v1.0.0
git push origin v1.0.0
Best Practices
- Use the logger - Always use
context.loginstead ofconsole.log - Handle errors - Wrap async operations in try/catch
- Clean up resources - Dispose event listeners in
dispose() - Secure secrets - Never log API keys; use
hasApiKeypattern in settings - Test all platforms - If using native hosts, test on Windows, macOS, and Linux
- Follow themes - Use CSS variables for dark mode compatibility
- Return true from activate() - Required for successful activation
Resources
- Octicons - Icon library for toolbar buttons
- GitCaddy Documentation - Full addon API documentation
License
MIT - Use this as a template for your own addons!