Skip to content

Commit e618be3

Browse files
committed
feat: defer mesh connection on first visit to avoid browser prompt
First-time visitors to the standalone PWA see a connect prompt instead of Chrome's unexpected 'access device' permission. Returning users (with localStorage flag) auto-connect immediately. Local server deployments always auto-connect.
1 parent 1361f26 commit e618be3

4 files changed

Lines changed: 65 additions & 9 deletions

File tree

src/bridges/user/web/frontend/components/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface AppProps {
3131
onJoinRoomInput: (roomName: string) => void;
3232
onRelayConnect: (urlA: string, urlB: string) => void;
3333
onRelayDisconnect: () => void;
34+
onConnectToMesh: () => void;
3435
}
3536

3637
export function App(props: AppProps) {
@@ -53,8 +54,10 @@ export function App(props: AppProps) {
5354
messages={props.messages}
5455
currentRoom={props.currentRoom}
5556
dmTarget={props.dmTarget}
57+
connected={props.connected}
5658
onSendAction={props.onSendAction}
5759
onLeaveRoom={props.onLeaveRoom}
60+
onConnectToMesh={props.onConnectToMesh}
5861
/>
5962
</>
6063
);

src/bridges/user/web/frontend/components/ChatArea.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,20 @@ interface ChatAreaProps {
1212
messages: DisplayMessage[];
1313
currentRoom: string | undefined;
1414
dmTarget: string | undefined;
15+
connected: boolean;
1516
onSendAction: (text: string) => void;
1617
onLeaveRoom: () => void;
18+
onConnectToMesh: () => void;
1719
}
1820

1921
export function ChatArea({
2022
messages,
2123
currentRoom,
2224
dmTarget,
25+
connected,
2326
onSendAction,
2427
onLeaveRoom,
28+
onConnectToMesh,
2529
}: ChatAreaProps) {
2630
const [inputText, setInputText] = useState("");
2731

@@ -61,6 +65,14 @@ export function ChatArea({
6165
)}
6266
</div>
6367
<MessageList messages={messages} />
68+
{!connected && messages.length === 0 && (
69+
<div class="connect-prompt">
70+
<p>Connect to a local mesh to discover agents and rooms.</p>
71+
<button class="connect-btn" onClick={onConnectToMesh}>
72+
Connect to local mesh
73+
</button>
74+
</div>
75+
)}
6476
<div id="input-bar">
6577
<input
6678
id="input"

src/bridges/user/web/frontend/main.tsx

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ import { MeshClient } from "./mesh-client.js";
1616
import { RelayClient } from "./relay-client.js";
1717
import type { Action, DisplayMessage, WsFrame } from "./types.js";
1818
import type { RelayStatus } from "./relay-client.js";
19+
20+
/** Whether the page is served from a local mesh server (vs standalone PWA). */
21+
const isLocalServer = /^(localhost|127\.\d+\.\d+\.\d+)(:\d+)?$/.test(
22+
location.host,
23+
);
1924
import { deliveryEventToMessage, roomMessageToDisplay } from "./messages.js";
2025
import { parseDeepLink, resolveDeepLink, syncUrl } from "./url-sync.js";
2126

@@ -50,7 +55,10 @@ meshClient.subscribe((meshState) => {
5055
state.setConnected(meshState.connected);
5156
});
5257

53-
meshClient.connect();
58+
// Don't auto-connect the MeshClient on first load.
59+
// The user must explicitly connect to avoid Chrome's "access device"
60+
// prompt appearing before the user understands the UI.
61+
// meshClient.connect() is called from onConnectToMesh().
5462

5563
// ---------------------------------------------------------------------------
5664
// Relay client — P2P mesh relay
@@ -265,6 +273,11 @@ function rerender(): void {
265273
onRelayDisconnect={() => {
266274
relayClient.disconnect();
267275
}}
276+
onConnectToMesh={() => {
277+
localStorage.setItem("agent-comms-connected", "true");
278+
meshClient.connect();
279+
if (isLocalServer) ws.connect();
280+
}}
268281
/>,
269282
rootEl,
270283
);
@@ -278,14 +291,14 @@ state.subscribe(rerender);
278291

279292
const deepLink = parseDeepLink(location.search);
280293

281-
// Only connect the direct server WebSocket when served from localhost.
282-
// On GitHub Pages / standalone deployments, only MeshClient is used.
283-
/** Whether the page is served from a local mesh server (vs standalone PWA). */
284-
const isLocalServer = /^(localhost|127\.\d+\.\d+\.\d+)(:\d+)?$/.test(
285-
location.host,
286-
);
287-
if (isLocalServer) {
288-
ws.connect();
294+
// Auto-connect when served from local server, or when the user has connected before.
295+
// First-time visitors to the standalone PWA see a connect prompt instead of
296+
// Chrome's unexpected "access device" permission prompt.
297+
const hasConnectedBefore =
298+
localStorage.getItem("agent-comms-connected") === "true";
299+
if (isLocalServer || hasConnectedBefore) {
300+
meshClient.connect();
301+
if (isLocalServer) ws.connect();
289302
}
290303

291304
// Initial state fetch, then resolve any deep link from the URL

src/bridges/user/web/frontend/styles.css

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,3 +507,31 @@ body {
507507
color: var(--red);
508508
line-height: 1.4;
509509
}
510+
511+
/* Connect prompt */
512+
.connect-prompt {
513+
display: flex;
514+
flex-direction: column;
515+
align-items: center;
516+
justify-content: center;
517+
gap: 12px;
518+
padding: 32px;
519+
text-align: center;
520+
color: var(--dim);
521+
}
522+
.connect-prompt p {
523+
margin: 0;
524+
}
525+
.connect-btn {
526+
padding: 8px 20px;
527+
border: 1px solid var(--accent);
528+
background: transparent;
529+
color: var(--accent);
530+
border-radius: 4px;
531+
cursor: pointer;
532+
font-size: 14px;
533+
}
534+
.connect-btn:hover {
535+
background: var(--accent);
536+
color: var(--bg);
537+
}

0 commit comments

Comments
 (0)