Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
43 changes: 40 additions & 3 deletions extension/entrypoints/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { TabManager } from "../services/TabManager";
import { ConnectionManager } from "../services/ConnectionManager";
import { CDPRouter } from "../services/CDPRouter";
import { StateManager } from "../services/StateManager";
import type { PopupMessage, StateResponse } from "../utils/types";
import type { PopupMessage, StateResponse, RelayUrlResponse } from "../utils/types";

export default defineBackground(() => {
// Create connection manager first (needed for sendMessage)
Expand All @@ -34,11 +34,12 @@ export default defineBackground(() => {
tabManager,
});

// Create connection manager
// Create connection manager with relay URL from state
connectionManager = new ConnectionManager({
logger,
onMessage: (msg) => cdpRouter.handleCommand(msg),
onDisconnect: () => tabManager.detachAll(),
getRelayUrl: () => stateManager.getRelayUrl(),
});

// Keep-alive alarm name for Chrome Alarms API
Expand All @@ -63,6 +64,16 @@ export default defineBackground(() => {
updateBadge(isActive);
}

// Handle relay URL changes
async function handleRelayUrlChange(relayUrl: string): Promise<void> {
await stateManager.setRelayUrl(relayUrl);
// Reconnect with new URL if active
const state = await stateManager.getState();
if (state.isActive) {
await connectionManager.reconnect();
}
}

// Handle debugger events
function onDebuggerEvent(
source: chrome.debugger.DebuggerSession,
Expand All @@ -88,7 +99,7 @@ export default defineBackground(() => {
(
message: PopupMessage,
_sender: chrome.runtime.MessageSender,
sendResponse: (response: StateResponse) => void
sendResponse: (response: StateResponse | RelayUrlResponse) => void
) => {
if (message.type === "getState") {
(async () => {
Expand All @@ -115,6 +126,32 @@ export default defineBackground(() => {
return true; // Async response
}

if (message.type === "getRelayUrl") {
(async () => {
try {
const relayUrl = await stateManager.getRelayUrl();
sendResponse({ relayUrl });
} catch (error) {
logger.debug("Error getting relay URL:", error);
sendResponse({ relayUrl: "" });
}
})();
return true; // Async response
}

if (message.type === "setRelayUrl") {
(async () => {
try {
await handleRelayUrlChange(message.relayUrl);
sendResponse({ relayUrl: message.relayUrl });
} catch (error) {
logger.debug("Error setting relay URL:", error);
sendResponse({ relayUrl: "" });
}
})();
return true; // Async response
}

return false;
}
);
Expand Down
9 changes: 9 additions & 0 deletions extension/entrypoints/popup/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ <h1>Dev Browser</h1>
<span id="status-text">Inactive</span>
</div>
<p id="connection-status" class="connection-status"></p>

<div class="settings-section">
<label for="relay-url" class="settings-label">Relay Server URL</label>
<div class="url-input-row">
<input type="text" id="relay-url" placeholder="ws://localhost:9222/extension" />
<button id="save-url" class="save-btn">Save</button>
</div>
<p id="url-status" class="url-status"></p>
</div>
</div>
<script type="module" src="./main.ts"></script>
</body>
Expand Down
65 changes: 64 additions & 1 deletion extension/entrypoints/popup/main.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import type { GetStateMessage, SetStateMessage, StateResponse } from "../../utils/types";
import type {
GetStateMessage,
SetStateMessage,
GetRelayUrlMessage,
SetRelayUrlMessage,
StateResponse,
RelayUrlResponse,
} from "../../utils/types";
import { validateRelayUrl } from "../../utils/constants";

const toggle = document.getElementById("active-toggle") as HTMLInputElement;
const statusText = document.getElementById("status-text") as HTMLSpanElement;
const connectionStatus = document.getElementById("connection-status") as HTMLParagraphElement;
const relayUrlInput = document.getElementById("relay-url") as HTMLInputElement;
const saveUrlButton = document.getElementById("save-url") as HTMLButtonElement;
const urlStatus = document.getElementById("url-status") as HTMLParagraphElement;

function updateUI(state: StateResponse): void {
toggle.checked = state.isActive;
Expand All @@ -27,8 +38,50 @@ function refreshState(): void {
});
}

function loadRelayUrl(): void {
chrome.runtime.sendMessage<GetRelayUrlMessage, RelayUrlResponse>(
{ type: "getRelayUrl" },
(response) => {
if (response) {
relayUrlInput.value = response.relayUrl;
}
}
);
}

function saveRelayUrl(): void {
const relayUrl = relayUrlInput.value.trim();

const validationError = validateRelayUrl(relayUrl);
if (validationError) {
urlStatus.textContent = validationError;
urlStatus.className = "url-status error";
return;
}

chrome.runtime.sendMessage<SetRelayUrlMessage, RelayUrlResponse>(
{ type: "setRelayUrl", relayUrl },
(response) => {
if (chrome.runtime.lastError) {
urlStatus.textContent = "Failed to save URL";
urlStatus.className = "url-status error";
return;
}
if (response) {
urlStatus.textContent = "Saved! Reconnecting...";
urlStatus.className = "url-status saved";
setTimeout(() => {
urlStatus.textContent = "";
urlStatus.className = "url-status";
}, 3000);
}
}
);
}

// Load initial state
refreshState();
loadRelayUrl();

// Poll for state updates while popup is open
const pollInterval = setInterval(refreshState, 1000);
Expand All @@ -50,3 +103,13 @@ toggle.addEventListener("change", () => {
}
);
});

// Handle save URL button
saveUrlButton.addEventListener("click", saveRelayUrl);

// Handle Enter key in URL input
relayUrlInput.addEventListener("keypress", (event) => {
if (event.key === "Enter") {
saveRelayUrl();
}
});
70 changes: 69 additions & 1 deletion extension/entrypoints/popup/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ body {
}

.popup {
width: 200px;
width: 280px;
padding: 16px;
}

Expand Down Expand Up @@ -94,3 +94,71 @@ input:checked + .slider::before {
.connection-status.connecting {
color: #ff9800;
}

/* Settings section */
.settings-section {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #eee;
}

.settings-label {
display: block;
font-size: 12px;
font-weight: 500;
color: #666;
margin-bottom: 8px;
}

.url-input-row {
display: flex;
gap: 8px;
}

#relay-url {
flex: 1;
padding: 8px;
font-size: 12px;
border: 1px solid #ddd;
border-radius: 4px;
outline: none;
}

#relay-url:focus {
border-color: #4caf50;
}

.save-btn {
padding: 8px 12px;
font-size: 12px;
font-weight: 500;
color: #fff;
background-color: #4caf50;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}

.save-btn:hover {
background-color: #43a047;
}

.save-btn:active {
background-color: #388e3c;
}

.url-status {
margin-top: 8px;
font-size: 11px;
color: #888;
min-height: 14px;
}

.url-status.saved {
color: #4caf50;
}

.url-status.error {
color: #f44336;
}
39 changes: 33 additions & 6 deletions extension/services/ConnectionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

import type { Logger } from "../utils/logger";
import type { ExtensionCommandMessage, ExtensionResponseMessage } from "../utils/types";
import { DEFAULT_RELAY_URL, wsUrlToHttpUrl } from "../utils/constants";

const RELAY_URL = "ws://localhost:9222/extension";
const RECONNECT_INTERVAL = 3000;

export interface ConnectionManagerDeps {
logger: Logger;
onMessage: (message: ExtensionCommandMessage) => Promise<unknown>;
onDisconnect: () => void;
getRelayUrl?: () => Promise<string>;
}

export class ConnectionManager {
Expand All @@ -21,11 +22,13 @@ export class ConnectionManager {
private logger: Logger;
private onMessage: (message: ExtensionCommandMessage) => Promise<unknown>;
private onDisconnect: () => void;
private getRelayUrl: () => Promise<string>;

constructor(deps: ConnectionManagerDeps) {
this.logger = deps.logger;
this.onMessage = deps.onMessage;
this.onDisconnect = deps.onDisconnect;
this.getRelayUrl = deps.getRelayUrl ?? (async () => DEFAULT_RELAY_URL);
}

/**
Expand All @@ -44,9 +47,13 @@ export class ConnectionManager {
return false;
}

// Get the HTTP URL from the WebSocket URL
const relayUrl = await this.getRelayUrl();
const httpUrl = wsUrlToHttpUrl(relayUrl);

// Verify server is actually reachable
try {
const response = await fetch("http://localhost:9222", {
const response = await fetch(httpUrl, {
method: "HEAD",
signal: AbortSignal.timeout(1000),
});
Expand Down Expand Up @@ -130,21 +137,41 @@ export class ConnectionManager {
}
}

/**
* Force reconnect with new relay URL.
*/
async reconnect(): Promise<void> {
if (this.ws) {
// Remove handlers before closing to prevent onDisconnect side effects
this.ws.onclose = null;
this.ws.onerror = null;
this.ws.onmessage = null;
this.ws.close();
this.ws = null;
}
if (this.shouldMaintain) {
await this.tryConnect();
}
}

/**
* Try to connect to relay server once.
*/
private async tryConnect(): Promise<void> {
if (this.isConnected()) return;

const relayUrl = await this.getRelayUrl();
const httpUrl = wsUrlToHttpUrl(relayUrl);

// Check if server is available
try {
await fetch("http://localhost:9222", { method: "HEAD" });
await fetch(httpUrl, { method: "HEAD" });
} catch {
return;
}

this.logger.debug("Connecting to relay server...");
const socket = new WebSocket(RELAY_URL);
this.logger.debug(`Connecting to relay server at ${relayUrl}...`);
const socket = new WebSocket(relayUrl);

await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
Expand All @@ -169,7 +196,7 @@ export class ConnectionManager {

this.ws = socket;
this.setupSocketHandlers(socket);
this.logger.log("Connected to relay server");
this.logger.log(`Connected to relay server at ${relayUrl}`);
}

/**
Expand Down
19 changes: 19 additions & 0 deletions extension/services/StateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
* StateManager - Manages extension active/inactive state with persistence.
*/

import { DEFAULT_RELAY_URL } from "../utils/constants";

const STORAGE_KEY = "devBrowserActiveState";
const RELAY_URL_KEY = "devBrowserRelayUrl";

export interface ExtensionState {
isActive: boolean;
Expand All @@ -25,4 +28,20 @@ export class StateManager {
async setState(state: ExtensionState): Promise<void> {
await chrome.storage.local.set({ [STORAGE_KEY]: state });
}

/**
* Get the relay URL.
* Defaults to localhost:9222 if not configured.
*/
async getRelayUrl(): Promise<string> {
const result = await chrome.storage.local.get(RELAY_URL_KEY);
return (result[RELAY_URL_KEY] as string) ?? DEFAULT_RELAY_URL;
}

/**
* Set the relay URL.
*/
async setRelayUrl(url: string): Promise<void> {
await chrome.storage.local.set({ [RELAY_URL_KEY]: url.trim() });
}
}
Loading