Skip to content

Commit 32248c0

Browse files
committed
feat: add settings for daemon host and port in popup
- Introduced a settings section in the popup to allow users to configure the daemon host and port. - Implemented storage functionality to save and retrieve these settings. - Updated connection logic to use the configured host and port for WebSocket connections. - Enhanced the background script to handle changes in the stored settings and reconnect accordingly. - Bumped version to 1.5.5 to reflect these changes. Made-with: Cursor
1 parent eead9e0 commit 32248c0

File tree

6 files changed

+248
-25
lines changed

6 files changed

+248
-25
lines changed

extension/dist/background.js

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1-
const DAEMON_PORT = 19825;
2-
const DAEMON_HOST = "localhost";
3-
const DAEMON_WS_URL = `ws://${DAEMON_HOST}:${DAEMON_PORT}/ext`;
4-
const DAEMON_PING_URL = `http://${DAEMON_HOST}:${DAEMON_PORT}/ping`;
1+
const DEFAULT_DAEMON_HOST = "localhost";
2+
const DEFAULT_DAEMON_PORT = 19825;
3+
function buildDaemonEndpoints(host, port) {
4+
const h = (host || DEFAULT_DAEMON_HOST).trim() || DEFAULT_DAEMON_HOST;
5+
const p = Number.isFinite(port) && port >= 1 && port <= 65535 ? port : DEFAULT_DAEMON_PORT;
6+
const hostPart = h.includes(":") && !h.startsWith("[") ? `[${h}]` : h;
7+
return {
8+
ping: `http://${hostPart}:${p}/ping`,
9+
ws: `ws://${hostPart}:${p}/ext`
10+
};
11+
}
512
const WS_RECONNECT_BASE_DELAY = 2e3;
613
const WS_RECONNECT_MAX_DELAY = 5e3;
714

@@ -210,9 +217,22 @@ function registerListeners() {
210217
});
211218
}
212219

220+
const STORAGE_KEYS = { host: "daemonHost", port: "daemonPort" };
213221
let ws = null;
222+
let activeWsUrl = null;
223+
let pendingWsUrl = null;
214224
let reconnectTimer = null;
215225
let reconnectAttempts = 0;
226+
async function getDaemonSettings() {
227+
const result = await chrome.storage.local.get({
228+
[STORAGE_KEYS.host]: DEFAULT_DAEMON_HOST,
229+
[STORAGE_KEYS.port]: DEFAULT_DAEMON_PORT
230+
});
231+
let host = result[STORAGE_KEYS.host]?.trim() || DEFAULT_DAEMON_HOST;
232+
let port = typeof result[STORAGE_KEYS.port] === "number" ? result[STORAGE_KEYS.port] : DEFAULT_DAEMON_PORT;
233+
if (!Number.isFinite(port) || port < 1 || port > 65535) port = DEFAULT_DAEMON_PORT;
234+
return { host, port };
235+
}
216236
const _origLog = console.log.bind(console);
217237
const _origWarn = console.warn.bind(console);
218238
const _origError = console.error.bind(console);
@@ -237,21 +257,37 @@ console.error = (...args) => {
237257
forwardLog("error", args);
238258
};
239259
async function connect() {
240-
if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return;
260+
const { host, port } = await getDaemonSettings();
261+
const { ping: pingUrl, ws: wsUrl } = buildDaemonEndpoints(host, port);
262+
if (ws) {
263+
if (ws.readyState === WebSocket.OPEN && activeWsUrl === wsUrl) return;
264+
if (ws.readyState === WebSocket.CONNECTING && pendingWsUrl === wsUrl) return;
265+
try {
266+
ws.close();
267+
} catch {
268+
}
269+
ws = null;
270+
activeWsUrl = null;
271+
pendingWsUrl = null;
272+
}
241273
try {
242-
const res = await fetch(DAEMON_PING_URL, { signal: AbortSignal.timeout(1e3) });
274+
const res = await fetch(pingUrl, { signal: AbortSignal.timeout(1e3) });
243275
if (!res.ok) return;
244276
} catch {
245277
return;
246278
}
247279
try {
248-
ws = new WebSocket(DAEMON_WS_URL);
280+
pendingWsUrl = wsUrl;
281+
ws = new WebSocket(wsUrl);
249282
} catch {
283+
pendingWsUrl = null;
250284
scheduleReconnect();
251285
return;
252286
}
253287
ws.onopen = () => {
254288
console.log("[opencli] Connected to daemon");
289+
pendingWsUrl = null;
290+
activeWsUrl = wsUrl;
255291
reconnectAttempts = 0;
256292
if (reconnectTimer) {
257293
clearTimeout(reconnectTimer);
@@ -271,6 +307,8 @@ async function connect() {
271307
ws.onclose = () => {
272308
console.log("[opencli] Disconnected from daemon");
273309
ws = null;
310+
activeWsUrl = null;
311+
pendingWsUrl = null;
274312
scheduleReconnect();
275313
};
276314
ws.onerror = () => {
@@ -364,12 +402,30 @@ chrome.runtime.onStartup.addListener(() => {
364402
chrome.alarms.onAlarm.addListener((alarm) => {
365403
if (alarm.name === "keepalive") void connect();
366404
});
405+
chrome.storage.onChanged.addListener((changes, area) => {
406+
if (area !== "local") return;
407+
if (!changes[STORAGE_KEYS.host] && !changes[STORAGE_KEYS.port]) return;
408+
try {
409+
ws?.close();
410+
} catch {
411+
}
412+
ws = null;
413+
activeWsUrl = null;
414+
pendingWsUrl = null;
415+
reconnectAttempts = 0;
416+
void connect();
417+
});
367418
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
368419
if (msg?.type === "getStatus") {
369-
sendResponse({
370-
connected: ws?.readyState === WebSocket.OPEN,
371-
reconnecting: reconnectTimer !== null
420+
void getDaemonSettings().then(({ host, port }) => {
421+
sendResponse({
422+
connected: ws?.readyState === WebSocket.OPEN,
423+
reconnecting: reconnectTimer !== null,
424+
host,
425+
port
426+
});
372427
});
428+
return true;
373429
}
374430
return false;
375431
});

extension/manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"tabs",
1010
"cookies",
1111
"activeTab",
12-
"alarms"
12+
"alarms",
13+
"storage"
1314
],
1415
"host_permissions": [
1516
"<all_urls>"

extension/popup.html

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,55 @@
3838
.dot.connecting { background: #ff9500; }
3939
.status-text { font-size: 13px; color: #555; }
4040
.status-text strong { color: #333; }
41+
.settings {
42+
margin-top: 12px;
43+
padding: 10px 12px;
44+
border-radius: 8px;
45+
background: #f5f5f5;
46+
}
47+
.settings label {
48+
display: block;
49+
font-size: 11px;
50+
color: #666;
51+
margin-bottom: 4px;
52+
margin-top: 8px;
53+
}
54+
.settings label:first-of-type { margin-top: 0; }
55+
.settings input {
56+
width: 100%;
57+
padding: 6px 8px;
58+
border: 1px solid #e0e0e0;
59+
border-radius: 6px;
60+
font-size: 13px;
61+
font-family: inherit;
62+
background: #fff;
63+
color: #333;
64+
}
65+
.settings input:focus {
66+
outline: none;
67+
border-color: #007aff;
68+
}
69+
.settings button {
70+
margin-top: 10px;
71+
width: 100%;
72+
padding: 8px 12px;
73+
border: none;
74+
border-radius: 8px;
75+
background: #007aff;
76+
color: #fff;
77+
font-size: 13px;
78+
font-weight: 500;
79+
font-family: inherit;
80+
cursor: pointer;
81+
}
82+
.settings button:hover { background: #0066d6; }
83+
.settings button:active { opacity: 0.9; }
84+
.settings .save-hint {
85+
margin-top: 6px;
86+
font-size: 11px;
87+
color: #34c759;
88+
min-height: 16px;
89+
}
4190
.hint {
4291
margin-top: 10px;
4392
padding: 8px 10px;
@@ -73,6 +122,14 @@ <h1>OpenCLI</h1>
73122
<span class="dot disconnected" id="dot"></span>
74123
<span class="status-text" id="status">Checking...</span>
75124
</div>
125+
<div class="settings">
126+
<label for="host">Daemon host</label>
127+
<input type="text" id="host" autocomplete="off" spellcheck="false" placeholder="localhost">
128+
<label for="port">Daemon port</label>
129+
<input type="number" id="port" min="1" max="65535" placeholder="19825">
130+
<button type="button" id="save">Save &amp; reconnect</button>
131+
<div class="save-hint" id="saveHint"></div>
132+
</div>
76133
<div class="hint" id="hint">
77134
This is normal. The extension connects automatically when you run any <code>opencli</code> command.
78135
</div>

extension/popup.js

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
// Query connection status from background service worker
2-
chrome.runtime.sendMessage({ type: 'getStatus' }, (resp) => {
1+
const DEFAULT_HOST = 'localhost';
2+
const DEFAULT_PORT = 19825;
3+
4+
function renderStatus(resp) {
35
const dot = document.getElementById('dot');
46
const status = document.getElementById('status');
57
const hint = document.getElementById('hint');
@@ -22,4 +24,43 @@ chrome.runtime.sendMessage({ type: 'getStatus' }, (resp) => {
2224
status.innerHTML = '<strong>No daemon connected</strong>';
2325
hint.style.display = 'block';
2426
}
27+
}
28+
29+
function loadFields() {
30+
chrome.storage.local.get(
31+
{ daemonHost: DEFAULT_HOST, daemonPort: DEFAULT_PORT },
32+
(stored) => {
33+
document.getElementById('host').value = stored.daemonHost || DEFAULT_HOST;
34+
document.getElementById('port').value = String(stored.daemonPort ?? DEFAULT_PORT);
35+
},
36+
);
37+
}
38+
39+
function refreshStatus() {
40+
chrome.runtime.sendMessage({ type: 'getStatus' }, (resp) => {
41+
renderStatus(resp);
42+
});
43+
}
44+
45+
document.getElementById('save').addEventListener('click', () => {
46+
const hostRaw = document.getElementById('host').value;
47+
const host = (hostRaw && hostRaw.trim()) ? hostRaw.trim() : DEFAULT_HOST;
48+
const portNum = parseInt(document.getElementById('port').value, 10);
49+
const hintEl = document.getElementById('saveHint');
50+
if (!Number.isFinite(portNum) || portNum < 1 || portNum > 65535) {
51+
hintEl.textContent = 'Enter a valid port (1–65535).';
52+
hintEl.style.color = '#ff3b30';
53+
return;
54+
}
55+
chrome.storage.local.set({ daemonHost: host, daemonPort: portNum }, () => {
56+
hintEl.textContent = 'Saved. Reconnecting…';
57+
hintEl.style.color = '#34c759';
58+
setTimeout(() => {
59+
hintEl.textContent = '';
60+
refreshStatus();
61+
}, 800);
62+
});
2563
});
64+
65+
loadFields();
66+
refreshStatus();

extension/src/background.ts

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,35 @@
66
*/
77

88
import type { Command, Result } from './protocol';
9-
import { DAEMON_WS_URL, DAEMON_PING_URL, WS_RECONNECT_BASE_DELAY, WS_RECONNECT_MAX_DELAY } from './protocol';
9+
import {
10+
DEFAULT_DAEMON_HOST,
11+
DEFAULT_DAEMON_PORT,
12+
WS_RECONNECT_BASE_DELAY,
13+
WS_RECONNECT_MAX_DELAY,
14+
buildDaemonEndpoints,
15+
} from './protocol';
1016
import * as executor from './cdp';
1117

18+
const STORAGE_KEYS = { host: 'daemonHost', port: 'daemonPort' } as const;
19+
1220
let ws: WebSocket | null = null;
21+
let activeWsUrl: string | null = null;
22+
/** URL we are currently connecting to (before onopen). */
23+
let pendingWsUrl: string | null = null;
1324
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
1425
let reconnectAttempts = 0;
1526

27+
async function getDaemonSettings(): Promise<{ host: string; port: number }> {
28+
const result = await chrome.storage.local.get({
29+
[STORAGE_KEYS.host]: DEFAULT_DAEMON_HOST,
30+
[STORAGE_KEYS.port]: DEFAULT_DAEMON_PORT,
31+
});
32+
let host = result[STORAGE_KEYS.host]?.trim() || DEFAULT_DAEMON_HOST;
33+
let port = typeof result[STORAGE_KEYS.port] === 'number' ? result[STORAGE_KEYS.port] : DEFAULT_DAEMON_PORT;
34+
if (!Number.isFinite(port) || port < 1 || port > 65535) port = DEFAULT_DAEMON_PORT;
35+
return { host, port };
36+
}
37+
1638
// ─── Console log forwarding ──────────────────────────────────────────
1739
// Hook console.log/warn/error to forward logs to daemon via WebSocket.
1840

@@ -42,24 +64,40 @@ console.error = (...args: unknown[]) => { _origError(...args); forwardLog('error
4264
* call site remains unchanged and the guard can never be accidentally skipped.
4365
*/
4466
async function connect(): Promise<void> {
45-
if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return;
67+
const { host, port } = await getDaemonSettings();
68+
const { ping: pingUrl, ws: wsUrl } = buildDaemonEndpoints(host, port);
69+
70+
if (ws) {
71+
if (ws.readyState === WebSocket.OPEN && activeWsUrl === wsUrl) return;
72+
if (ws.readyState === WebSocket.CONNECTING && pendingWsUrl === wsUrl) return;
73+
try {
74+
ws.close();
75+
} catch { /* ignore */ }
76+
ws = null;
77+
activeWsUrl = null;
78+
pendingWsUrl = null;
79+
}
4680

4781
try {
48-
const res = await fetch(DAEMON_PING_URL, { signal: AbortSignal.timeout(1000) });
82+
const res = await fetch(pingUrl, { signal: AbortSignal.timeout(1000) });
4983
if (!res.ok) return; // unexpected response — not our daemon
5084
} catch {
5185
return; // daemon not running — skip WebSocket to avoid console noise
5286
}
5387

5488
try {
55-
ws = new WebSocket(DAEMON_WS_URL);
89+
pendingWsUrl = wsUrl;
90+
ws = new WebSocket(wsUrl);
5691
} catch {
92+
pendingWsUrl = null;
5793
scheduleReconnect();
5894
return;
5995
}
6096

6197
ws.onopen = () => {
6298
console.log('[opencli] Connected to daemon');
99+
pendingWsUrl = null;
100+
activeWsUrl = wsUrl;
63101
reconnectAttempts = 0; // Reset on successful connection
64102
if (reconnectTimer) {
65103
clearTimeout(reconnectTimer);
@@ -82,6 +120,8 @@ async function connect(): Promise<void> {
82120
ws.onclose = () => {
83121
console.log('[opencli] Disconnected from daemon');
84122
ws = null;
123+
activeWsUrl = null;
124+
pendingWsUrl = null;
85125
scheduleReconnect();
86126
};
87127

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

261+
chrome.storage.onChanged.addListener((changes, area) => {
262+
if (area !== 'local') return;
263+
if (!changes[STORAGE_KEYS.host] && !changes[STORAGE_KEYS.port]) return;
264+
try {
265+
ws?.close();
266+
} catch { /* ignore */ }
267+
ws = null;
268+
activeWsUrl = null;
269+
pendingWsUrl = null;
270+
reconnectAttempts = 0;
271+
void connect();
272+
});
273+
221274
// ─── Popup status API ───────────────────────────────────────────────
222275

223276
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
224277
if (msg?.type === 'getStatus') {
225-
sendResponse({
226-
connected: ws?.readyState === WebSocket.OPEN,
227-
reconnecting: reconnectTimer !== null,
278+
void getDaemonSettings().then(({ host, port }) => {
279+
sendResponse({
280+
connected: ws?.readyState === WebSocket.OPEN,
281+
reconnecting: reconnectTimer !== null,
282+
host,
283+
port,
284+
});
228285
});
286+
return true;
229287
}
230288
return false;
231289
});

0 commit comments

Comments
 (0)