docs: update addon API documentation and examples

Add comprehensive documentation for:
- Event subscription API with examples
- License status implementation for commercial addons
- Header actions configuration for view panels
- View communication using postMessage instead of bridge
- Clarify activate() must return true for successful activation

Update demo and settings views to reflect new communication patterns.
This commit is contained in:
2026-01-18 14:03:23 -05:00
parent 2b9527cc5f
commit d5be7cc604
5 changed files with 521 additions and 75 deletions

228
README.md
View File

@@ -92,8 +92,11 @@ class MyAddon {
// Called once when addon is loaded
async initialize(context) { }
// Called when addon is enabled
async activate() { }
// Called when addon is enabled - MUST return true for successful activation
async activate() {
// ... setup code ...
return true;
}
// Called when addon is disabled
async deactivate() { }
@@ -103,6 +106,8 @@ class MyAddon {
}
```
**Important**: The `activate()` method must return `true` to indicate successful activation. If it returns `undefined` or `false`, the addon will fail to activate.
### Context Object
The `context` object provides:
@@ -113,12 +118,62 @@ 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 emitter
context.events // Event subscription
context.ipc // IPC communication
context.appState // Access to app state
context.hostPort // Port of native host (if configured)
```
### Event Subscription
Subscribe to app events via `context.events`:
```javascript
// Repository changed
const disposable = 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 ready
context.events.onAppReady(() => {
console.log('App is ready');
});
// App will quit
context.events.onAppWillQuit(() => {
// Clean up resources
});
// Remember to dispose when done
disposable.dispose();
```
### License Status (Optional)
For commercial addons, implement `getLicenseStatus()`:
```javascript
getLicenseStatus() {
// For free addons, always return valid
return { valid: true };
// For commercial addons:
// return {
// valid: this.isLicenseValid,
// message: this.isLicenseValid ? undefined : 'License expired'
// };
}
```
### Invoke Method
GitCaddy calls your addon's `invoke` method for all actions:
@@ -182,12 +237,27 @@ Position options: `start`, `end`, `after-push-pull`, `after-branch`
"type": "iframe",
"source": "views/my-view.html"
},
"context": ["repository"]
"context": ["repository"],
"headerActions": [
{
"id": "refresh",
"label": "Refresh",
"icon": "🔄",
"primary": true
},
{
"id": "settings",
"label": "Settings",
"icon": "⚙️"
}
]
}
```
Context options: `repository`, `diff`, `commit`, `always`
**Header Actions**: Buttons displayed in GitCaddy's view header bar (next to the close button). Use this instead of creating your own header inside the HTML view. The `primary` property highlights the button.
### Context Menu Items
```json
@@ -219,30 +289,144 @@ Types: `boolean`, `string`, `number`, `select`
## View Communication
Views communicate with the addon via the `window.gitcaddy` bridge:
Views (iframes) communicate with GitCaddy via `postMessage`:
### Receiving Messages from GitCaddy
```javascript
// Wait for bridge to be ready
if (window.gitcaddy) {
init();
} else {
window.addEventListener('gitcaddy-ready', init);
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':
// Received initial context (repositoryPath, hostPort)
const { repositoryPath, hostPort } = data;
initializeView();
break;
case 'addon:header-action':
// Header button was clicked
handleHeaderAction(actionId);
break;
case 'addon:invoke-response':
// Response from an invoke request
handleInvokeResponse(requestId, result, error);
break;
}
});
```
### Invoking Addon Methods
```javascript
// Helper for invoke with Promise support
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
}, '*');
// Timeout after 30 seconds
setTimeout(() => {
if (pendingRequests.has(requestId)) {
pendingRequests.delete(requestId);
reject(new Error('Request timed out'));
}
}, 30000);
});
}
// Invoke addon methods
const result = await window.gitcaddy.invoke('myMethod', arg1, arg2);
// Usage
const data = await invokeAddon('getDemoData');
```
// Send one-way messages
window.gitcaddy.send('my-channel', { data: 'value' });
### Other Messages to GitCaddy
// Listen for messages
window.gitcaddy.on('addon-message', (data) => {
console.log('Received:', data);
});
```javascript
// Open addon settings dialog
window.parent.postMessage({
type: 'addon:open-settings',
addonId: ADDON_ID
}, '*');
// Logging
window.gitcaddy.log.info('Something happened');
window.gitcaddy.log.error('Something went wrong');
// Refresh toolbar badge
window.parent.postMessage({
type: 'addon:refresh-badge',
addonId: ADDON_ID,
buttonId: 'my-button',
data: { count: 5 }
}, '*');
```
## Dark Mode Styling
GitCaddy injects CSS variables for theming. Use them to match the app's appearance:
### Theme Colors
```css
/* Background colors */
background-color: var(--background, #1e1e1e);
background-color: var(--background-secondary, #252526);
/* Text colors */
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);
```
### Dark Mode Scrollbars
Always style scrollbars to match dark mode:
```css
/* Webkit browsers (Chrome, Safari, Edge) */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: var(--background-secondary, #252526);
border-radius: 5px;
}
::-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 Hosts

View File

@@ -114,7 +114,23 @@
"source": "views/demo.html"
},
"_comment_context": "When to show: repository, diff, commit, always",
"context": ["repository"]
"context": ["repository"],
"_comment_headerActions": "Buttons shown in GitCaddy's view header bar (next to the X close button)",
"_comment_headerActions_note": "Use this instead of creating your own header inside the HTML view",
"headerActions": [
{
"id": "refresh",
"label": "Refresh",
"icon": "🔄",
"_comment_primary": "Primary buttons are highlighted",
"primary": true
},
{
"id": "settings",
"label": "Settings",
"icon": "⚙️"
}
]
}
],

View File

@@ -76,13 +76,14 @@ class MyFirstAddon {
this.disposables.push(settingsDisposable);
// Register for repository events
const repoDisposable = context.events.on('repository-changed', (repo) => {
// Note: Use the specific event methods, not a generic 'on' method
const repoDisposable = context.events.onRepositorySelected((repo) => {
this.onRepositoryChanged(repo);
});
this.disposables.push(repoDisposable);
// Register for commit events
const commitDisposable = context.events.on('commit-created', (commit) => {
const commitDisposable = context.events.onCommitCreated((commit) => {
this.onCommitCreated(commit);
});
this.disposables.push(commitDisposable);
@@ -104,6 +105,7 @@ class MyFirstAddon {
// this.startPeriodicTask();
this.context?.log.info('MyFirstAddon activated');
return true; // Must return true to indicate successful activation
}
/**
@@ -209,6 +211,30 @@ class MyFirstAddon {
}
}
// ============================================================
// LICENSE METHODS
// Optional: Implement if your addon has licensing
// ============================================================
/**
* Get the license status for this addon.
*
* GitCaddy calls this method to check if the addon is licensed.
* If not implemented, the addon is assumed to be licensed (free addon).
*
* @returns {{ valid: boolean, message?: string }}
*/
getLicenseStatus() {
// For free/open-source addons, always return valid
return { valid: true };
// For commercial addons, you would check license here:
// return {
// valid: this.isLicenseValid,
// message: this.isLicenseValid ? undefined : 'License expired'
// };
}
// ============================================================
// SETTINGS METHODS
// ============================================================
@@ -489,6 +515,16 @@ class MyFirstAddon {
* @property {function} onDidChange - Register for setting changes
*/
/**
* @typedef {Object} IAddonEvents
* @property {function} onAppReady - Called when app is ready
* @property {function} onAppWillQuit - Called before app quits
* @property {function} onRepositorySelected - Called when repository changes
* @property {function} onFilesChanged - Called when files change
* @property {function} onCommitCreated - Called when a commit is created
* @property {function} emit - Emit a custom event
*/
// Export the addon class as default
module.exports = MyFirstAddon;
module.exports.default = MyFirstAddon;

View File

@@ -16,6 +16,10 @@
padding: 0;
}
html, body {
height: 100%;
}
body {
/* Use GitCaddy's theme colors */
font-family: var(--font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
@@ -24,6 +28,43 @@
background-color: var(--background, #1e1e1e);
padding: 16px;
line-height: 1.5;
display: flex;
flex-direction: column;
}
/* ============================================================
DARK MODE SCROLLBAR STYLING
Use theme colors for scrollbars to match dark mode
============================================================ */
/* Webkit browsers (Chrome, Safari, Edge) */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: var(--background-secondary, #252526);
border-radius: 5px;
}
::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb, #5a5a5a);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-thumb-hover, #7a7a7a);
}
::-webkit-scrollbar-corner {
background: var(--background-secondary, #252526);
}
/* Firefox */
* {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb, #5a5a5a) var(--background-secondary, #252526);
}
h1, h2, h3 {
@@ -221,10 +262,28 @@
.tab-content {
display: none;
flex: 1;
min-height: 0;
overflow: hidden;
}
.tab-content.active {
display: block;
display: flex;
flex-direction: column;
}
/* Make the logs section fill available space */
#logs .section {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
#logs .log-output {
flex: 1;
max-height: none;
min-height: 100px;
}
.loading {
@@ -376,60 +435,158 @@
<button class="btn btn-secondary" onclick="clearLogs()">
Clear Logs
</button>
<button class="btn" onclick="copyLogs()">
📋 Copy Logs
</button>
</div>
</div>
</div>
<!-- ============================================================
SCRIPTS
Communication with GitCaddy happens via window.gitcaddy
Communication with GitCaddy happens via postMessage
============================================================ -->
<script>
// ============================================================
// GITCADDY BRIDGE
// GitCaddy injects a bridge object for communication
// ADDON CONFIGURATION
// ============================================================
const ADDON_ID = 'com.example.myfirst-addon';
// Context received from GitCaddy
let repositoryPath = null;
let hostPort = null;
// Pending invoke requests (for async responses)
const pendingRequests = new Map();
let requestIdCounter = 0;
// ============================================================
// INVOKE HELPER
// Invoke addon methods via postMessage
// ============================================================
/**
* The GitCaddy bridge provides:
* - gitcaddy.invoke(method, ...args) - Call addon main process methods
* - gitcaddy.send(channel, data) - Send one-way message to main
* - gitcaddy.on(channel, callback) - Listen for messages from main
* - gitcaddy.log.info/warn/error/debug() - Log messages
*
* The bridge is injected automatically when the view loads.
*/
function invokeAddon(method, ...args) {
return new Promise((resolve, reject) => {
const requestId = ++requestIdCounter;
pendingRequests.set(requestId, { resolve, reject });
// Wait for the bridge to be ready
function whenReady(callback) {
if (window.gitcaddy) {
callback();
} else {
window.addEventListener('gitcaddy-ready', callback);
window.parent.postMessage({
type: 'addon:invoke',
requestId,
addonId: ADDON_ID,
method,
args
}, '*');
// Timeout after 30 seconds
setTimeout(() => {
if (pendingRequests.has(requestId)) {
pendingRequests.delete(requestId);
reject(new Error('Request timed out'));
}
}, 30000);
});
}
// ============================================================
// MESSAGE HANDLER
// Handle messages from GitCaddy
// ============================================================
window.addEventListener('message', (event) => {
const { type, data, actionId, requestId, result, error } = event.data || {};
switch (type) {
case 'addon:init-context':
// Received initial context from GitCaddy
repositoryPath = data?.repositoryPath;
hostPort = data?.hostPort;
log('info', 'Received context from GitCaddy');
initializeView();
break;
case 'addon:header-action':
// Header button was clicked
log('info', `Header action clicked: ${actionId}`);
handleHeaderAction(actionId);
break;
case 'addon:invoke-response':
// Response from an invoke request
if (pendingRequests.has(requestId)) {
const { resolve, reject } = pendingRequests.get(requestId);
pendingRequests.delete(requestId);
if (error) {
reject(new Error(error));
} else {
resolve(result);
}
}
break;
}
});
// ============================================================
// HEADER ACTIONS
// Handle header button clicks from addon.json -> headerActions
// ============================================================
function handleHeaderAction(actionId) {
switch (actionId) {
case 'refresh':
refreshData();
break;
case 'settings':
openSettings();
break;
default:
log('warn', `Unknown header action: ${actionId}`);
}
}
function openSettings() {
window.parent.postMessage({
type: 'addon:open-settings',
addonId: ADDON_ID
}, '*');
}
// ============================================================
// INITIALIZATION
// ============================================================
whenReady(async () => {
async function initializeView() {
log('info', 'Demo view initialized');
// Load addon data
try {
const data = await window.gitcaddy.invoke('getDemoData');
const data = await invokeAddon('getDemoData');
updateDisplay(data);
log('info', 'Loaded addon data');
} catch (err) {
log('error', 'Failed to load addon data: ' + err.message);
// Show fallback data
updateDisplay({
addonId: ADDON_ID,
addonPath: 'Loading...',
isActive: true,
settings: {},
features: ['Toolbar buttons', 'Menu items', 'Context menus', 'Custom views', 'Settings storage', 'IPC communication', 'Event handling']
});
}
}
// Listen for messages from main process
window.gitcaddy.on('addon-message', (data) => {
log('info', 'Received message: ' + JSON.stringify(data));
});
});
// Refresh data when header Refresh button is clicked
async function refreshData() {
log('info', 'Refreshing addon data...');
try {
const data = await invokeAddon('getDemoData');
updateDisplay(data);
log('info', 'Data refreshed successfully');
} catch (err) {
log('error', 'Failed to refresh: ' + err.message);
}
}
// ============================================================
// DISPLAY UPDATES
@@ -451,19 +608,20 @@
// Update settings
const settingsEl = document.getElementById('settings-display');
if (data.settings) {
settingsEl.innerHTML = Object.entries(data.settings)
const settingsHtml = Object.entries(data.settings)
.filter(([key]) => key !== 'apiKey') // Don't show API key
.map(([key, value]) => `
<span class="info-label">${key}:</span>
<span class="info-value">${JSON.stringify(value)}</span>
<span class="info-label">${escapeHtml(key)}:</span>
<span class="info-value">${escapeHtml(JSON.stringify(value))}</span>
`).join('');
settingsEl.innerHTML = settingsHtml || '<span class="info-label">No settings configured</span>';
}
// Update features list
const featuresEl = document.getElementById('features-list');
if (data.features && data.features.length > 0) {
featuresEl.innerHTML = data.features
.map(f => `<li>${f}</li>`)
.map(f => `<li>${escapeHtml(f)}</li>`)
.join('');
}
}
@@ -497,7 +655,7 @@
log('info', `Performing action: ${actionType}`);
try {
const result = await window.gitcaddy.invoke('performAction', actionType);
const result = await invokeAddon('performAction', actionType);
resultEl.textContent = JSON.stringify(result, null, 2);
log('info', `Action result: ${JSON.stringify(result)}`);
} catch (err) {
@@ -513,20 +671,19 @@
function sendMessage() {
const resultEl = document.getElementById('ipc-result');
if (!window.gitcaddy) {
resultEl.textContent = 'Error: GitCaddy bridge not available';
return;
}
// Send a one-way message (just demonstrates the pattern)
// Note: For iframes, use invokeAddon for two-way communication
log('info', 'Sending demo message via invoke...');
// Send a one-way message
window.gitcaddy.send('demo-message', {
type: 'greeting',
message: 'Hello from the demo view!',
timestamp: new Date().toISOString()
});
resultEl.textContent = 'Message sent! Check main process logs.';
log('info', 'Sent message to main process');
invokeAddon('performAction', 'log')
.then(result => {
resultEl.textContent = 'Message sent! Result: ' + JSON.stringify(result, null, 2);
log('info', 'Message sent successfully');
})
.catch(err => {
resultEl.textContent = 'Error: ' + err.message;
log('error', 'Send message failed: ' + err.message);
});
}
async function invokeMethod() {
@@ -535,7 +692,7 @@
log('info', 'Invoking getDemoData method');
try {
const result = await window.gitcaddy.invoke('getDemoData');
const result = await invokeAddon('getDemoData');
resultEl.textContent = JSON.stringify(result, null, 2);
log('info', 'Method invocation successful');
} catch (err) {
@@ -562,11 +719,6 @@
logEl.appendChild(entry);
logEl.scrollTop = logEl.scrollHeight;
// Also log to GitCaddy's logger if available
if (window.gitcaddy?.log) {
window.gitcaddy.log[level]?.(message);
}
}
function clearLogs() {
@@ -579,11 +731,35 @@
`;
}
function copyLogs() {
const logEl = document.getElementById('log-output');
const logEntries = logEl.querySelectorAll('.log-entry');
const logText = Array.from(logEntries).map(entry => {
const time = entry.querySelector('.log-time')?.textContent || '';
const message = entry.querySelector('span:last-child')?.textContent || '';
return `${time} ${message}`;
}).join('\n');
navigator.clipboard.writeText(logText)
.then(() => {
log('info', 'Logs copied to clipboard!');
})
.catch(err => {
log('error', 'Failed to copy logs: ' + err.message);
});
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// ============================================================
// STARTUP
// Initialize immediately in case context was already sent
// ============================================================
log('info', 'Demo view loading...');
</script>
</body>
</html>

View File

@@ -25,6 +25,40 @@
line-height: 1.5;
}
/* ============================================================
DARK MODE SCROLLBAR STYLING
============================================================ */
/* Webkit browsers (Chrome, Safari, Edge) */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: var(--background-secondary, #252526);
border-radius: 5px;
}
::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb, #5a5a5a);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-thumb-hover, #7a7a7a);
}
::-webkit-scrollbar-corner {
background: var(--background-secondary, #252526);
}
/* Firefox */
* {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb, #5a5a5a) var(--background-secondary, #252526);
}
h1 {
font-size: 1.4em;
margin-bottom: 8px;