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.
870 lines
25 KiB
HTML
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>
|