chore: bump version to 0.1652

This commit is contained in:
arkon
2026-02-27 00:34:38 +01:00
parent c50ccbf8a8
commit 5f1f66bf60
68 changed files with 9990 additions and 62 deletions
+289
View File
@@ -3466,6 +3466,7 @@ class CodemanApp {
const overlay = document.getElementById('welcomeOverlay');
if (overlay) {
overlay.classList.add('visible');
this.loadTunnelStatus();
}
}
@@ -3474,6 +3475,12 @@ class CodemanApp {
if (overlay) {
overlay.classList.remove('visible');
}
// Collapse expanded QR when leaving welcome screen
const qrWrap = document.getElementById('welcomeQr');
if (qrWrap) {
clearTimeout(this._welcomeQrShrinkTimer);
qrWrap.classList.remove('expanded');
}
}
/**
@@ -4894,6 +4901,57 @@ class CodemanApp {
this.openImagePopup(data);
});
// ========== Tunnel Events ==========
addListener('tunnel:started', (e) => {
const data = JSON.parse(e.data);
console.log('[Tunnel] Started:', data.url);
this._dismissTunnelConnecting();
this._updateTunnelUrlDisplay(data.url);
const welcomeVisible = document.getElementById('welcomeOverlay')?.classList.contains('visible');
if (welcomeVisible) {
// On welcome screen: QR appears inline, expanded first
this._updateWelcomeTunnelBtn(true, data.url, true);
this.showToast(`Tunnel active`, 'success');
} else {
// Not on welcome screen: popup QR overlay
this._updateWelcomeTunnelBtn(true, data.url);
this.showToast(`Tunnel active: ${data.url}`, 'success');
this.showTunnelQR();
}
});
addListener('tunnel:stopped', () => {
console.log('[Tunnel] Stopped');
this._dismissTunnelConnecting();
this._updateTunnelUrlDisplay(null);
this._updateWelcomeTunnelBtn(false);
this.closeTunnelQR();
});
addListener('tunnel:progress', (e) => {
const data = JSON.parse(e.data);
console.log('[Tunnel] Progress:', data.message);
const toast = document.getElementById('tunnelConnectingToast');
if (toast) {
toast.innerHTML = `<span class="tunnel-spinner"></span> ${data.message}`;
}
// Also update button text if on welcome screen
const btn = document.getElementById('welcomeTunnelBtn');
if (btn?.classList.contains('connecting')) {
btn.innerHTML = `<span class="tunnel-spinner"></span> ${data.message}`;
}
});
addListener('tunnel:error', (e) => {
const data = JSON.parse(e.data);
console.warn('[Tunnel] Error:', data.message);
this._dismissTunnelConnecting();
this.showToast(`Tunnel error: ${data.message}`, 'error');
const btn = document.getElementById('welcomeTunnelBtn');
if (btn) { btn.disabled = false; btn.classList.remove('connecting'); }
});
// Plan subagent visibility events (show Opus agents during plan generation)
addListener('plan:subagent', (e) => {
const data = JSON.parse(e.data);
@@ -11389,6 +11447,8 @@ class CodemanApp {
document.getElementById('appSettingsSubagentTracking').checked = settings.subagentTrackingEnabled ?? defaults.subagentTrackingEnabled ?? true;
document.getElementById('appSettingsSubagentActiveTabOnly').checked = settings.subagentActiveTabOnly ?? defaults.subagentActiveTabOnly ?? true;
document.getElementById('appSettingsImageWatcherEnabled').checked = settings.imageWatcherEnabled ?? defaults.imageWatcherEnabled ?? false;
document.getElementById('appSettingsTunnelEnabled').checked = settings.tunnelEnabled ?? false;
this.loadTunnelStatus();
document.getElementById('appSettingsLocalEcho').checked = settings.localEchoEnabled ?? MobileDetection.isTouchDevice();
document.getElementById('appSettingsTabTwoRows').checked = settings.tabTwoRows ?? defaults.tabTwoRows ?? false;
// Claude CLI settings
@@ -11515,6 +11575,229 @@ class CodemanApp {
}
}
async loadTunnelStatus() {
try {
const res = await fetch('/api/tunnel/status');
const status = await res.json();
const active = status.running && status.url;
this._updateTunnelUrlDisplay(active ? status.url : null);
this._updateWelcomeTunnelBtn(!!active, active ? status.url : null);
} catch {
this._updateTunnelUrlDisplay(null);
this._updateWelcomeTunnelBtn(false);
}
}
_updateTunnelUrlDisplay(url) {
const row = document.getElementById('tunnelUrlRow');
const display = document.getElementById('tunnelUrlDisplay');
if (!row || !display) return;
if (url) {
row.style.display = '';
display.textContent = url;
display.onclick = () => {
navigator.clipboard.writeText(url).then(() => {
this.showToast('Tunnel URL copied', 'success');
});
};
} else {
row.style.display = 'none';
display.textContent = '';
display.onclick = null;
}
}
showTunnelQR() {
// Close existing popup if open
this.closeTunnelQR();
const overlay = document.createElement('div');
overlay.id = 'tunnelQrOverlay';
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:5000;display:flex;align-items:center;justify-content:center;cursor:pointer';
overlay.onclick = (e) => { if (e.target === overlay) this.closeTunnelQR(); };
const card = document.createElement('div');
card.style.cssText = 'background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:24px;text-align:center;max-width:340px;width:90vw;box-shadow:var(--shadow-lg);cursor:default';
card.innerHTML = `
<div style="font-size:14px;font-weight:600;color:var(--text-primary);margin-bottom:16px">Scan to connect</div>
<div id="tunnelQrContainer" style="background:#fff;border-radius:8px;padding:16px;display:inline-block">
<div style="color:#666;font-size:12px">Loading...</div>
</div>
<div id="tunnelQrUrl" style="margin-top:12px;font-family:monospace;font-size:11px;color:var(--text-muted);word-break:break-all;cursor:pointer" title="Click to copy"></div>
<button onclick="app.closeTunnelQR()" style="margin-top:16px;padding:6px 20px;background:var(--bg-elevated);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);cursor:pointer;font-size:13px">Close</button>
`;
overlay.appendChild(card);
document.body.appendChild(overlay);
// Fetch QR SVG from server
fetch('/api/tunnel/qr')
.then(res => {
if (!res.ok) throw new Error('Tunnel not running');
return res.json();
})
.then(data => {
const container = document.getElementById('tunnelQrContainer');
if (container && data.svg) container.innerHTML = data.svg;
})
.catch(() => {
const container = document.getElementById('tunnelQrContainer');
if (container) container.innerHTML = '<div style="color:#c00;font-size:12px;padding:20px">Tunnel not active</div>';
});
// Fetch URL for display
fetch('/api/tunnel/status')
.then(r => r.json())
.then(status => {
const urlEl = document.getElementById('tunnelQrUrl');
if (urlEl && status.url) {
urlEl.textContent = status.url;
urlEl.onclick = () => {
navigator.clipboard.writeText(status.url).then(() => {
this.showToast('Tunnel URL copied', 'success');
});
};
}
})
.catch(() => {});
// Close on Escape
this._tunnelQrEscHandler = (e) => { if (e.key === 'Escape') this.closeTunnelQR(); };
document.addEventListener('keydown', this._tunnelQrEscHandler);
}
closeTunnelQR() {
const overlay = document.getElementById('tunnelQrOverlay');
if (overlay) overlay.remove();
if (this._tunnelQrEscHandler) {
document.removeEventListener('keydown', this._tunnelQrEscHandler);
this._tunnelQrEscHandler = null;
}
}
async toggleTunnelFromWelcome() {
const btn = document.getElementById('welcomeTunnelBtn');
if (!btn) return;
const isActive = btn.classList.contains('active');
btn.disabled = true;
try {
const res = await fetch('/api/settings');
const current = res.ok ? await res.json() : {};
const newEnabled = !isActive;
current.tunnelEnabled = newEnabled;
await fetch('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(current),
});
if (newEnabled) {
this._showTunnelConnecting();
} else {
this._dismissTunnelConnecting();
this.showToast('Tunnel stopped', 'info');
this._updateWelcomeTunnelBtn(false);
btn.disabled = false;
}
} catch (err) {
this._dismissTunnelConnecting();
this.showToast('Failed to toggle tunnel', 'error');
btn.disabled = false;
}
}
_showTunnelConnecting() {
const btn = document.getElementById('welcomeTunnelBtn');
if (btn) {
btn.classList.add('connecting');
btn.innerHTML = `
<span class="tunnel-spinner"></span>
Connecting...`;
}
// Persistent toast with spinner
this._dismissTunnelConnecting();
const toast = document.createElement('div');
toast.className = 'toast toast-info show';
toast.id = 'tunnelConnectingToast';
toast.innerHTML = '<span class="tunnel-spinner"></span> Cloudflare Tunnel connecting...';
toast.style.pointerEvents = 'auto';
if (!this._toastContainer) {
this._toastContainer = document.querySelector('.toast-container');
if (!this._toastContainer) {
this._toastContainer = document.createElement('div');
this._toastContainer.className = 'toast-container';
document.body.appendChild(this._toastContainer);
}
}
this._toastContainer.appendChild(toast);
}
_dismissTunnelConnecting() {
const toast = document.getElementById('tunnelConnectingToast');
if (toast) {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 200);
}
const btn = document.getElementById('welcomeTunnelBtn');
if (btn) btn.classList.remove('connecting');
}
_updateWelcomeTunnelBtn(active, url, firstAppear = false) {
const btn = document.getElementById('welcomeTunnelBtn');
if (btn) {
btn.disabled = false;
if (active) {
btn.classList.remove('connecting');
btn.classList.add('active');
btn.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
Tunnel Active`;
} else {
btn.classList.remove('active', 'connecting');
btn.innerHTML = `
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
Cloudflare Tunnel`;
}
}
// Update welcome QR code
const qrWrap = document.getElementById('welcomeQr');
const qrInner = document.getElementById('welcomeQrInner');
const qrUrl = document.getElementById('welcomeQrUrl');
if (!qrWrap || !qrInner) return;
if (active) {
qrWrap.classList.add('visible');
// First appear: start expanded, auto-shrink after 8s
if (firstAppear) {
qrWrap.classList.add('expanded');
clearTimeout(this._welcomeQrShrinkTimer);
this._welcomeQrShrinkTimer = setTimeout(() => {
qrWrap.classList.remove('expanded');
}, 8000);
}
if (url) {
qrUrl.textContent = url;
qrUrl.title = 'Click QR to enlarge';
}
fetch('/api/tunnel/qr')
.then(r => { if (!r.ok) throw new Error(); return r.json(); })
.then(data => { if (data.svg) qrInner.innerHTML = data.svg; })
.catch(() => { qrInner.innerHTML = '<div style="color:#999;font-size:11px;padding:20px">QR unavailable</div>'; });
} else {
clearTimeout(this._welcomeQrShrinkTimer);
qrWrap.classList.remove('visible', 'expanded');
qrInner.innerHTML = '';
if (qrUrl) qrUrl.textContent = '';
}
}
toggleWelcomeQrSize() {
const qrWrap = document.getElementById('welcomeQr');
if (qrWrap) {
clearTimeout(this._welcomeQrShrinkTimer);
qrWrap.classList.toggle('expanded');
}
}
toggleDeepgramKeyVisibility() {
const input = document.getElementById('voiceDeepgramKey');
const btn = document.getElementById('voiceKeyToggleBtn');
@@ -11642,6 +11925,7 @@ class CodemanApp {
subagentTrackingEnabled: document.getElementById('appSettingsSubagentTracking').checked,
subagentActiveTabOnly: document.getElementById('appSettingsSubagentActiveTabOnly').checked,
imageWatcherEnabled: document.getElementById('appSettingsImageWatcherEnabled').checked,
tunnelEnabled: document.getElementById('appSettingsTunnelEnabled').checked,
localEchoEnabled: document.getElementById('appSettingsLocalEcho').checked,
tabTwoRows: document.getElementById('appSettingsTabTwoRows').checked,
// Claude CLI settings
@@ -11760,6 +12044,11 @@ class CodemanApp {
await this.saveModelConfigFromSettings();
this.showToast('Settings saved', 'success');
// Show tunnel-specific feedback if toggled on
if (settings.tunnelEnabled) {
this.showToast('Tunnel starting — QR code will appear when ready...', 'info');
}
} catch (err) {
// Server save failed but localStorage succeeded
this.showToast('Settings saved locally', 'warning');