Files
myfirst-addon/views/demo.html
logikonline e79fd3f281 feat: add .NET host process integration
Implements native host process support with .NET backend:
- Add .NET host project with HTTP endpoints (health check, hello world)
- Configure multi-platform host executables in addon.json
- Add host communication methods in main process
- Create host demo tab in UI
- Add .gitignore for .NET build artifacts

The host process runs on a dynamic port and communicates via HTTP with the main addon process.
2026-01-18 14:48:43 -05:00

870 lines
25 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My First Addon - Demo</title>
<style>
/* ============================================================
STYLES
GitCaddy injects CSS variables for theming. Use them!
============================================================ */
* {
box-sizing: border-box;
margin: 0;
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);
font-size: var(--font-size, 13px);
color: var(--foreground, #cccccc);
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 {
color: var(--foreground, #cccccc);
margin-bottom: 12px;
}
h1 {
font-size: 1.5em;
border-bottom: 1px solid var(--border, #3c3c3c);
padding-bottom: 8px;
}
h2 {
font-size: 1.2em;
margin-top: 24px;
}
h3 {
font-size: 1em;
margin-top: 16px;
}
p {
margin-bottom: 12px;
color: var(--foreground-muted, #999999);
}
.section {
background-color: var(--background-secondary, #252526);
border: 1px solid var(--border, #3c3c3c);
border-radius: 6px;
padding: 16px;
margin-bottom: 16px;
}
.info-grid {
display: grid;
grid-template-columns: 140px 1fr;
gap: 8px 16px;
margin-bottom: 16px;
}
.info-label {
color: var(--foreground-muted, #999999);
font-weight: 500;
}
.info-value {
color: var(--foreground, #cccccc);
font-family: var(--font-family-mono, 'Consolas', 'Monaco', monospace);
}
.feature-list {
list-style: none;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 8px;
}
.feature-list li {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background-color: var(--background, #1e1e1e);
border-radius: 4px;
}
.feature-list li::before {
content: "\2713"; /* Checkmark */
color: var(--success, #4ec9b0);
font-weight: bold;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 16px;
font-size: 13px;
font-weight: 500;
color: var(--button-foreground, #ffffff);
background-color: var(--button-background, #0e639c);
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.15s;
}
.btn:hover {
background-color: var(--button-hover-background, #1177bb);
}
.btn:active {
background-color: var(--button-active-background, #0d5a8c);
}
.btn-secondary {
background-color: var(--button-secondary-background, #3c3c3c);
color: var(--button-secondary-foreground, #cccccc);
}
.btn-secondary:hover {
background-color: var(--button-secondary-hover-background, #4c4c4c);
}
.button-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-top: 12px;
}
.status-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.status-active {
background-color: var(--success-background, rgba(78, 201, 176, 0.2));
color: var(--success, #4ec9b0);
}
.status-inactive {
background-color: var(--warning-background, rgba(252, 186, 3, 0.2));
color: var(--warning, #fcba03);
}
.code-block {
background-color: var(--background, #1e1e1e);
border: 1px solid var(--border, #3c3c3c);
border-radius: 4px;
padding: 12px;
font-family: var(--font-family-mono, 'Consolas', 'Monaco', monospace);
font-size: 12px;
overflow-x: auto;
white-space: pre-wrap;
}
.log-output {
max-height: 200px;
overflow-y: auto;
}
.log-entry {
padding: 4px 0;
border-bottom: 1px solid var(--border, #3c3c3c);
}
.log-entry:last-child {
border-bottom: none;
}
.log-time {
color: var(--foreground-muted, #666666);
margin-right: 8px;
}
.log-info { color: var(--info, #75beff); }
.log-warn { color: var(--warning, #fcba03); }
.log-error { color: var(--error, #f14c4c); }
.log-debug { color: var(--foreground-muted, #666666); }
.tabs {
display: flex;
border-bottom: 1px solid var(--border, #3c3c3c);
margin-bottom: 16px;
}
.tab {
padding: 8px 16px;
border: none;
background: none;
color: var(--foreground-muted, #999999);
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.15s;
}
.tab:hover {
color: var(--foreground, #cccccc);
}
.tab.active {
color: var(--foreground, #cccccc);
border-bottom-color: var(--accent, #0e639c);
}
.tab-content {
display: none;
flex: 1;
min-height: 0;
overflow: hidden;
}
.tab-content.active {
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 {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid var(--foreground-muted, #666666);
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<h1>My First Addon</h1>
<p>This demo showcases the GitCaddy addon API features.</p>
<!-- Tabs Navigation -->
<div class="tabs">
<button class="tab active" data-tab="overview">Overview</button>
<button class="tab" data-tab="actions">Actions</button>
<button class="tab" data-tab="ipc">IPC Demo</button>
<button class="tab" data-tab="host">Host Demo</button>
<button class="tab" data-tab="logs">Logs</button>
</div>
<!-- Overview Tab -->
<div id="overview" class="tab-content active">
<div class="section">
<h2>Addon Information</h2>
<div class="info-grid">
<span class="info-label">Addon ID:</span>
<span class="info-value" id="addon-id">Loading...</span>
<span class="info-label">Addon Path:</span>
<span class="info-value" id="addon-path">Loading...</span>
<span class="info-label">Status:</span>
<span class="info-value" id="addon-status">
<span class="status-badge status-inactive">Inactive</span>
</span>
</div>
</div>
<div class="section">
<h2>Current Settings</h2>
<div class="info-grid" id="settings-display">
<span class="info-label">Loading...</span>
<span class="info-value"></span>
</div>
</div>
<div class="section">
<h2>Features Demonstrated</h2>
<ul class="feature-list" id="features-list">
<li>Loading features...</li>
</ul>
</div>
</div>
<!-- Actions Tab -->
<div id="actions" class="tab-content">
<div class="section">
<h2>Test Actions</h2>
<p>Click the buttons below to test various addon actions.</p>
<h3>Notifications</h3>
<p>Send a notification to the user.</p>
<div class="button-group">
<button class="btn" onclick="performAction('notify')">
Show Notification
</button>
</div>
<h3>Logging</h3>
<p>Write messages to the addon log.</p>
<div class="button-group">
<button class="btn btn-secondary" onclick="performAction('log')">
Write Test Logs
</button>
</div>
<h3>Settings</h3>
<p>Retrieve current settings from the addon.</p>
<div class="button-group">
<button class="btn btn-secondary" onclick="performAction('settings')">
Get Settings
</button>
</div>
</div>
<div class="section">
<h2>Action Result</h2>
<div class="code-block" id="action-result">
Click an action button to see the result here.
</div>
</div>
</div>
<!-- IPC Demo Tab -->
<div id="ipc" class="tab-content">
<div class="section">
<h2>IPC Communication</h2>
<p>
Addons can communicate between renderer and main process using IPC.
This demonstrates sending messages and invoking methods.
</p>
<h3>Send Message</h3>
<p>Send a one-way message to the main process.</p>
<div class="button-group">
<button class="btn btn-secondary" onclick="sendMessage()">
Send Message
</button>
</div>
<h3>Invoke Method</h3>
<p>Call a method on the main process and wait for response.</p>
<div class="button-group">
<button class="btn" onclick="invokeMethod()">
Invoke getDemoData
</button>
</div>
</div>
<div class="section">
<h2>IPC Result</h2>
<div class="code-block" id="ipc-result">
Use the buttons above to test IPC communication.
</div>
</div>
</div>
<!-- Host Demo Tab -->
<div id="host" class="tab-content">
<div class="section">
<h2>.NET Host Communication</h2>
<p>
This addon includes a .NET native host process that provides HTTP endpoints.
GitCaddy automatically starts the host when the addon is activated and
provides the port number via <code>context.hostPort</code>.
</p>
<h3>Hello World</h3>
<p>Call the /hello endpoint on the .NET host.</p>
<div class="button-group">
<button class="btn" onclick="callHelloWorld()">
Call Hello World
</button>
<button class="btn btn-secondary" onclick="callHelloWorldWithName()">
Call with Name
</button>
</div>
<h3>Host Response</h3>
<div class="code-block" id="host-result">
Click a button above to test host communication.
</div>
</div>
<div class="section">
<h2>Host Status</h2>
<div class="info-row">
<span class="info-label">Host Port:</span>
<span class="info-value" id="host-port">Loading...</span>
</div>
<div class="info-row">
<span class="info-label">Host Available:</span>
<span class="info-value" id="host-available">Unknown</span>
</div>
</div>
</div>
<!-- Logs Tab -->
<div id="logs" class="tab-content">
<div class="section">
<h2>Activity Log</h2>
<p>Real-time log of addon activity in this view.</p>
<div class="code-block log-output" id="log-output">
<div class="log-entry">
<span class="log-time">[--:--:--]</span>
<span class="log-info">Waiting for activity...</span>
</div>
</div>
<div class="button-group">
<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 postMessage
============================================================ -->
<script>
// ============================================================
// 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
// ============================================================
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);
});
}
// ============================================================
// 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
// ============================================================
async function initializeView() {
log('info', 'Demo view initialized');
// Update host status display
updateHostStatus();
// Load addon data
try {
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']
});
}
}
// 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
// ============================================================
function updateDisplay(data) {
// Update addon info
document.getElementById('addon-id').textContent = data.addonId || 'Unknown';
document.getElementById('addon-path').textContent = data.addonPath || 'Unknown';
// Update status badge
const statusEl = document.getElementById('addon-status');
if (data.isActive) {
statusEl.innerHTML = '<span class="status-badge status-active">Active</span>';
} else {
statusEl.innerHTML = '<span class="status-badge status-inactive">Inactive</span>';
}
// Update settings
const settingsEl = document.getElementById('settings-display');
if (data.settings) {
const settingsHtml = Object.entries(data.settings)
.filter(([key]) => key !== 'apiKey') // Don't show API key
.map(([key, value]) => `
<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>${escapeHtml(f)}</li>`)
.join('');
}
}
// ============================================================
// TAB NAVIGATION
// ============================================================
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
// Update tab buttons
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// Update tab content
const tabId = tab.dataset.tab;
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
document.getElementById(tabId).classList.add('active');
log('debug', `Switched to ${tabId} tab`);
});
});
// ============================================================
// ACTION HANDLERS
// ============================================================
async function performAction(actionType) {
const resultEl = document.getElementById('action-result');
resultEl.innerHTML = '<span class="loading"></span> Executing...';
log('info', `Performing action: ${actionType}`);
try {
const result = await invokeAddon('performAction', actionType);
resultEl.textContent = JSON.stringify(result, null, 2);
log('info', `Action result: ${JSON.stringify(result)}`);
} catch (err) {
resultEl.textContent = 'Error: ' + err.message;
log('error', `Action failed: ${err.message}`);
}
}
// ============================================================
// IPC HANDLERS
// ============================================================
function sendMessage() {
const resultEl = document.getElementById('ipc-result');
// 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...');
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() {
const resultEl = document.getElementById('ipc-result');
resultEl.innerHTML = '<span class="loading"></span> Invoking...';
log('info', 'Invoking getDemoData method');
try {
const result = await invokeAddon('getDemoData');
resultEl.textContent = JSON.stringify(result, null, 2);
log('info', 'Method invocation successful');
} catch (err) {
resultEl.textContent = 'Error: ' + err.message;
log('error', `Method invocation failed: ${err.message}`);
}
}
// ============================================================
// HOST DEMO FUNCTIONS
// ============================================================
async function callHelloWorld() {
const resultEl = document.getElementById('host-result');
resultEl.innerHTML = '<span class="loading"></span> Calling host...';
log('info', 'Calling host /hello endpoint');
try {
const result = await invokeAddon('callHelloWorld');
resultEl.textContent = JSON.stringify(result, null, 2);
if (result.error) {
log('error', `Host error: ${result.error}`);
document.getElementById('host-available').textContent = 'No (Error)';
} else {
log('info', `Host response: ${result.message}`);
document.getElementById('host-available').textContent = 'Yes';
}
} catch (err) {
resultEl.textContent = 'Error: ' + err.message;
log('error', `Host call failed: ${err.message}`);
document.getElementById('host-available').textContent = 'No (Exception)';
}
}
async function callHelloWorldWithName() {
const name = prompt('Enter your name:', 'World');
if (!name) return;
const resultEl = document.getElementById('host-result');
resultEl.innerHTML = '<span class="loading"></span> Calling host...';
log('info', `Calling host /hello/${name} endpoint`);
try {
const result = await invokeAddon('callHelloWorld', name);
resultEl.textContent = JSON.stringify(result, null, 2);
if (result.error) {
log('error', `Host error: ${result.error}`);
} else {
log('info', `Host response: ${result.message}`);
}
} catch (err) {
resultEl.textContent = 'Error: ' + err.message;
log('error', `Host call failed: ${err.message}`);
}
}
function updateHostStatus() {
// hostPort is received in addon:init-context
const portEl = document.getElementById('host-port');
if (hostPort) {
portEl.textContent = hostPort;
document.getElementById('host-available').textContent = 'Checking...';
} else {
portEl.textContent = 'Not available';
document.getElementById('host-available').textContent = 'No (No port)';
}
}
// ============================================================
// LOGGING
// ============================================================
function log(level, message) {
const logEl = document.getElementById('log-output');
const now = new Date();
const time = now.toTimeString().split(' ')[0];
const entry = document.createElement('div');
entry.className = 'log-entry';
entry.innerHTML = `
<span class="log-time">[${time}]</span>
<span class="log-${level}">${escapeHtml(message)}</span>
`;
logEl.appendChild(entry);
logEl.scrollTop = logEl.scrollHeight;
}
function clearLogs() {
const logEl = document.getElementById('log-output');
logEl.innerHTML = `
<div class="log-entry">
<span class="log-time">[${new Date().toTimeString().split(' ')[0]}]</span>
<span class="log-info">Logs cleared</span>
</div>
`;
}
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>