Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 66 additions & 10 deletions extension/dist/background.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
const DAEMON_PORT = 19825;
const DAEMON_HOST = "localhost";
const DAEMON_WS_URL = `ws://${DAEMON_HOST}:${DAEMON_PORT}/ext`;
const DAEMON_PING_URL = `http://${DAEMON_HOST}:${DAEMON_PORT}/ping`;
const DEFAULT_DAEMON_HOST = "localhost";
const DEFAULT_DAEMON_PORT = 19825;
function buildDaemonEndpoints(host, port) {
const h = (host || DEFAULT_DAEMON_HOST).trim() || DEFAULT_DAEMON_HOST;
const p = Number.isFinite(port) && port >= 1 && port <= 65535 ? port : DEFAULT_DAEMON_PORT;
const hostPart = h.includes(":") && !h.startsWith("[") ? `[${h}]` : h;
return {
ping: `http://${hostPart}:${p}/ping`,
ws: `ws://${hostPart}:${p}/ext`
};
}
const WS_RECONNECT_BASE_DELAY = 2e3;
const WS_RECONNECT_MAX_DELAY = 5e3;

Expand Down Expand Up @@ -210,9 +217,22 @@ function registerListeners() {
});
}

const STORAGE_KEYS = { host: "daemonHost", port: "daemonPort" };
let ws = null;
let activeWsUrl = null;
let pendingWsUrl = null;
let reconnectTimer = null;
let reconnectAttempts = 0;
async function getDaemonSettings() {
const result = await chrome.storage.local.get({
[STORAGE_KEYS.host]: DEFAULT_DAEMON_HOST,
[STORAGE_KEYS.port]: DEFAULT_DAEMON_PORT
});
let host = result[STORAGE_KEYS.host]?.trim() || DEFAULT_DAEMON_HOST;
let port = typeof result[STORAGE_KEYS.port] === "number" ? result[STORAGE_KEYS.port] : DEFAULT_DAEMON_PORT;
if (!Number.isFinite(port) || port < 1 || port > 65535) port = DEFAULT_DAEMON_PORT;
return { host, port };
}
const _origLog = console.log.bind(console);
const _origWarn = console.warn.bind(console);
const _origError = console.error.bind(console);
Expand All @@ -237,21 +257,37 @@ console.error = (...args) => {
forwardLog("error", args);
};
async function connect() {
if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return;
const { host, port } = await getDaemonSettings();
const { ping: pingUrl, ws: wsUrl } = buildDaemonEndpoints(host, port);
if (ws) {
if (ws.readyState === WebSocket.OPEN && activeWsUrl === wsUrl) return;
if (ws.readyState === WebSocket.CONNECTING && pendingWsUrl === wsUrl) return;
try {
ws.close();
} catch {
}
ws = null;
activeWsUrl = null;
pendingWsUrl = null;
}
try {
const res = await fetch(DAEMON_PING_URL, { signal: AbortSignal.timeout(1e3) });
const res = await fetch(pingUrl, { signal: AbortSignal.timeout(1e3) });
if (!res.ok) return;
} catch {
return;
}
try {
ws = new WebSocket(DAEMON_WS_URL);
pendingWsUrl = wsUrl;
ws = new WebSocket(wsUrl);
} catch {
pendingWsUrl = null;
scheduleReconnect();
return;
}
ws.onopen = () => {
console.log("[opencli] Connected to daemon");
pendingWsUrl = null;
activeWsUrl = wsUrl;
reconnectAttempts = 0;
if (reconnectTimer) {
clearTimeout(reconnectTimer);
Expand All @@ -271,6 +307,8 @@ async function connect() {
ws.onclose = () => {
console.log("[opencli] Disconnected from daemon");
ws = null;
activeWsUrl = null;
pendingWsUrl = null;
scheduleReconnect();
};
ws.onerror = () => {
Expand Down Expand Up @@ -364,12 +402,30 @@ chrome.runtime.onStartup.addListener(() => {
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === "keepalive") void connect();
});
chrome.storage.onChanged.addListener((changes, area) => {
if (area !== "local") return;
if (!changes[STORAGE_KEYS.host] && !changes[STORAGE_KEYS.port]) return;
try {
ws?.close();
} catch {
}
ws = null;
activeWsUrl = null;
pendingWsUrl = null;
reconnectAttempts = 0;
void connect();
});
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
if (msg?.type === "getStatus") {
sendResponse({
connected: ws?.readyState === WebSocket.OPEN,
reconnecting: reconnectTimer !== null
void getDaemonSettings().then(({ host, port }) => {
sendResponse({
connected: ws?.readyState === WebSocket.OPEN,
reconnecting: reconnectTimer !== null,
host,
port
});
});
return true;
}
return false;
});
Expand Down
3 changes: 2 additions & 1 deletion extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"tabs",
"cookies",
"activeTab",
"alarms"
"alarms",
"storage"
],
"host_permissions": [
"<all_urls>"
Expand Down
57 changes: 57 additions & 0 deletions extension/popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,55 @@
.dot.connecting { background: #ff9500; }
.status-text { font-size: 13px; color: #555; }
.status-text strong { color: #333; }
.settings {
margin-top: 12px;
padding: 10px 12px;
border-radius: 8px;
background: #f5f5f5;
}
.settings label {
display: block;
font-size: 11px;
color: #666;
margin-bottom: 4px;
margin-top: 8px;
}
.settings label:first-of-type { margin-top: 0; }
.settings input {
width: 100%;
padding: 6px 8px;
border: 1px solid #e0e0e0;
border-radius: 6px;
font-size: 13px;
font-family: inherit;
background: #fff;
color: #333;
}
.settings input:focus {
outline: none;
border-color: #007aff;
}
.settings button {
margin-top: 10px;
width: 100%;
padding: 8px 12px;
border: none;
border-radius: 8px;
background: #007aff;
color: #fff;
font-size: 13px;
font-weight: 500;
font-family: inherit;
cursor: pointer;
}
.settings button:hover { background: #0066d6; }
.settings button:active { opacity: 0.9; }
.settings .save-hint {
margin-top: 6px;
font-size: 11px;
color: #34c759;
min-height: 16px;
}
.hint {
margin-top: 10px;
padding: 8px 10px;
Expand Down Expand Up @@ -73,6 +122,14 @@ <h1>OpenCLI</h1>
<span class="dot disconnected" id="dot"></span>
<span class="status-text" id="status">Checking...</span>
</div>
<div class="settings">
<label for="host">Daemon host</label>
<input type="text" id="host" autocomplete="off" spellcheck="false" placeholder="localhost">
<label for="port">Daemon port</label>
<input type="number" id="port" min="1" max="65535" placeholder="19825">
<button type="button" id="save">Save &amp; reconnect</button>
<div class="save-hint" id="saveHint"></div>
</div>
<div class="hint" id="hint">
This is normal. The extension connects automatically when you run any <code>opencli</code> command.
</div>
Expand Down
45 changes: 43 additions & 2 deletions extension/popup.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Query connection status from background service worker
chrome.runtime.sendMessage({ type: 'getStatus' }, (resp) => {
const DEFAULT_HOST = 'localhost';
const DEFAULT_PORT = 19825;

function renderStatus(resp) {
const dot = document.getElementById('dot');
const status = document.getElementById('status');
const hint = document.getElementById('hint');
Expand All @@ -22,4 +24,43 @@ chrome.runtime.sendMessage({ type: 'getStatus' }, (resp) => {
status.innerHTML = '<strong>No daemon connected</strong>';
hint.style.display = 'block';
}
}

function loadFields() {
chrome.storage.local.get(
{ daemonHost: DEFAULT_HOST, daemonPort: DEFAULT_PORT },
(stored) => {
document.getElementById('host').value = stored.daemonHost || DEFAULT_HOST;
document.getElementById('port').value = String(stored.daemonPort ?? DEFAULT_PORT);
},
);
}

function refreshStatus() {
chrome.runtime.sendMessage({ type: 'getStatus' }, (resp) => {
renderStatus(resp);
});
}

document.getElementById('save').addEventListener('click', () => {
const hostRaw = document.getElementById('host').value;
const host = (hostRaw && hostRaw.trim()) ? hostRaw.trim() : DEFAULT_HOST;
const portNum = parseInt(document.getElementById('port').value, 10);
const hintEl = document.getElementById('saveHint');
if (!Number.isFinite(portNum) || portNum < 1 || portNum > 65535) {
hintEl.textContent = 'Enter a valid port (1–65535).';
hintEl.style.color = '#ff3b30';
return;
}
chrome.storage.local.set({ daemonHost: host, daemonPort: portNum }, () => {
hintEl.textContent = 'Saved. Reconnecting…';
hintEl.style.color = '#34c759';
setTimeout(() => {
hintEl.textContent = '';
refreshStatus();
}, 800);
});
});

loadFields();
refreshStatus();
72 changes: 65 additions & 7 deletions extension/src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,35 @@
*/

import type { Command, Result } from './protocol';
import { DAEMON_WS_URL, DAEMON_PING_URL, WS_RECONNECT_BASE_DELAY, WS_RECONNECT_MAX_DELAY } from './protocol';
import {
DEFAULT_DAEMON_HOST,
DEFAULT_DAEMON_PORT,
WS_RECONNECT_BASE_DELAY,
WS_RECONNECT_MAX_DELAY,
buildDaemonEndpoints,
} from './protocol';
import * as executor from './cdp';

const STORAGE_KEYS = { host: 'daemonHost', port: 'daemonPort' } as const;

let ws: WebSocket | null = null;
let activeWsUrl: string | null = null;
/** URL we are currently connecting to (before onopen). */
let pendingWsUrl: string | null = null;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let reconnectAttempts = 0;

async function getDaemonSettings(): Promise<{ host: string; port: number }> {
const result = await chrome.storage.local.get({
[STORAGE_KEYS.host]: DEFAULT_DAEMON_HOST,
[STORAGE_KEYS.port]: DEFAULT_DAEMON_PORT,
});
let host = result[STORAGE_KEYS.host]?.trim() || DEFAULT_DAEMON_HOST;
let port = typeof result[STORAGE_KEYS.port] === 'number' ? result[STORAGE_KEYS.port] : DEFAULT_DAEMON_PORT;
if (!Number.isFinite(port) || port < 1 || port > 65535) port = DEFAULT_DAEMON_PORT;
return { host, port };
}

// ─── Console log forwarding ──────────────────────────────────────────
// Hook console.log/warn/error to forward logs to daemon via WebSocket.

Expand Down Expand Up @@ -42,24 +64,40 @@ console.error = (...args: unknown[]) => { _origError(...args); forwardLog('error
* call site remains unchanged and the guard can never be accidentally skipped.
*/
async function connect(): Promise<void> {
if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return;
const { host, port } = await getDaemonSettings();
const { ping: pingUrl, ws: wsUrl } = buildDaemonEndpoints(host, port);

if (ws) {
if (ws.readyState === WebSocket.OPEN && activeWsUrl === wsUrl) return;
if (ws.readyState === WebSocket.CONNECTING && pendingWsUrl === wsUrl) return;
try {
ws.close();
} catch { /* ignore */ }
ws = null;
activeWsUrl = null;
pendingWsUrl = null;
}

try {
const res = await fetch(DAEMON_PING_URL, { signal: AbortSignal.timeout(1000) });
const res = await fetch(pingUrl, { signal: AbortSignal.timeout(1000) });
if (!res.ok) return; // unexpected response — not our daemon
} catch {
return; // daemon not running — skip WebSocket to avoid console noise
}

try {
ws = new WebSocket(DAEMON_WS_URL);
pendingWsUrl = wsUrl;
ws = new WebSocket(wsUrl);
} catch {
pendingWsUrl = null;
scheduleReconnect();
return;
}

ws.onopen = () => {
console.log('[opencli] Connected to daemon');
pendingWsUrl = null;
activeWsUrl = wsUrl;
reconnectAttempts = 0; // Reset on successful connection
if (reconnectTimer) {
clearTimeout(reconnectTimer);
Expand All @@ -82,6 +120,8 @@ async function connect(): Promise<void> {
ws.onclose = () => {
console.log('[opencli] Disconnected from daemon');
ws = null;
activeWsUrl = null;
pendingWsUrl = null;
scheduleReconnect();
};

Expand Down Expand Up @@ -218,14 +258,32 @@ chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'keepalive') void connect();
});

chrome.storage.onChanged.addListener((changes, area) => {
if (area !== 'local') return;
if (!changes[STORAGE_KEYS.host] && !changes[STORAGE_KEYS.port]) return;
try {
ws?.close();
} catch { /* ignore */ }
ws = null;
activeWsUrl = null;
pendingWsUrl = null;
reconnectAttempts = 0;
void connect();
});

// ─── Popup status API ───────────────────────────────────────────────

chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
if (msg?.type === 'getStatus') {
sendResponse({
connected: ws?.readyState === WebSocket.OPEN,
reconnecting: reconnectTimer !== null,
void getDaemonSettings().then(({ host, port }) => {
sendResponse({
connected: ws?.readyState === WebSocket.OPEN,
reconnecting: reconnectTimer !== null,
host,
port,
});
});
return true;
}
return false;
});
Expand Down
Loading