@agent360/browser-mcp 1.25.0 → 1.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -21
- package/bin/cli.js +102 -44
- package/extension/background.js +934 -109
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/manifest.json +2 -2
- package/extension/offscreen.js +142 -10
- package/extension/popup.html +36 -2
- package/extension/popup.js +37 -21
- package/index.js +686 -54
- package/package.json +17 -3
- package/tools.js +95 -8
- package/vagt.js +57 -0
|
Binary file
|
|
Binary file
|
package/extension/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Agent360 Browser MCP",
|
|
4
|
-
"version": "1.
|
|
5
|
-
"description": "
|
|
4
|
+
"version": "1.28.1",
|
|
5
|
+
"description": "Give Claude Code your real logged-in Chrome. Two parts: this extension + one npx command. Multi-session, local, MIT.",
|
|
6
6
|
"permissions": [
|
|
7
7
|
"tabs",
|
|
8
8
|
"tabGroups",
|
package/extension/offscreen.js
CHANGED
|
@@ -1,24 +1,99 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Offscreen Document — Persistent WebSocket bridge (multi-session)
|
|
3
3
|
*
|
|
4
|
-
* Scans port range 9876-
|
|
4
|
+
* Scans port range 9876-9895 and maintains connections to ALL active
|
|
5
5
|
* MCP servers. Each Claude Code session gets its own port automatically.
|
|
6
6
|
* Passes port ID with every command so background.js can track tab ownership.
|
|
7
7
|
*
|
|
8
8
|
* Flow: MCP Server(s) ←(WS)→ this ←(chrome.runtime.sendMessage)→ Service Worker → Chrome APIs
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
// Et offscreen-dokument har IKKE chrome.runtime.getManifest() — kaldet kaster
|
|
12
|
+
// "chrome.runtime.getManifest is not a function". Versionen kommer derfor med i
|
|
13
|
+
// dokumentets egen URL, sat af background.js da dokumentet blev oprettet. Det er
|
|
14
|
+
// samtidig den rigtige semantik: et dokument oprettet af en aeldre udgave baerer
|
|
15
|
+
// den aeldre version, saa `offscreenSvarer()` kan se forskel paa "svarer" og
|
|
16
|
+
// "er den udgave vi koerer nu".
|
|
17
|
+
function minVersion() {
|
|
18
|
+
try { return new URLSearchParams(location.search).get('v') || null; } catch { return null; }
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
const BASE_PORT = 9876;
|
|
12
22
|
const MAX_PORT = 9895;
|
|
13
23
|
const connections = new Map(); // port → WebSocket
|
|
14
24
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
// ── Hvorfor porte foerst PROBES med fetch, og ikke bare aabnes (MAALT 22/8) ──────
|
|
26
|
+
//
|
|
27
|
+
// Chrome bremser nye WebSocket-haandtryk. Formlen staar i Chromiums egen kilde
|
|
28
|
+
// (services/network/websocket_throttler.cc):
|
|
29
|
+
//
|
|
30
|
+
// forsinkelse = rand(1000..5000) ms × 2^min(p + f/(s+1), 16) / 65536
|
|
31
|
+
//
|
|
32
|
+
// p = haandtryk i luften lige nu · f = mislykkede · s = lykkedes. Bremsen er
|
|
33
|
+
// PER RENDERER-PROCES, ikke per adresse — at sprede sig over 20 porte hjaelper
|
|
34
|
+
// altsaa ingenting. Og den her funktion aabnede foer 20 WebSockets i ét smaek
|
|
35
|
+
// hvert 2. sekund, hvoraf de fleste var mod doede porte. Det satte baade p og f
|
|
36
|
+
// i vejret og laaste eksponenten paa loftet, saa HVER forbindelse betalte den
|
|
37
|
+
// maksimale straf paa op til 5 sekunder.
|
|
38
|
+
//
|
|
39
|
+
// Dertil kom vores egen faelde: connectTimeout stod paa 2000 ms — UNDER bremsens
|
|
40
|
+
// maksimum. Vi draebte altsaa systematisk forbindelser der bare stod og ventede,
|
|
41
|
+
// og Chromiums header siger det rent ud: at destruere en PendingConnection uden
|
|
42
|
+
// at haandtrykket er fuldfoert TAELLER SOM EN FEJL. Hvert drab gjorde bremsen
|
|
43
|
+
// haardere, hvilket draebte flere. En spiral vi selv drev.
|
|
44
|
+
//
|
|
45
|
+
// Maalt effekt: den 10. chat var 15,5 sekunder om at komme op, og 5 af 19 kom
|
|
46
|
+
// aldrig inden for 22 sekunder.
|
|
47
|
+
//
|
|
48
|
+
// Rettelsen er to ting:
|
|
49
|
+
// 1. Find levende porte med et almindeligt HTTP-kald. En ws-server svarer
|
|
50
|
+
// "426 Upgrade Required" paa et GET; en doed port afviser. HTTP-kald taeller
|
|
51
|
+
// IKKE med i WebSocket-bremsen — maalt: 100 fejlede HTTP-probes kostede nul,
|
|
52
|
+
// 400 fejlede WS-forsoeg kostede 14 sekunder. Vi aabner nu kun WebSockets
|
|
53
|
+
// mod porte vi VED der sidder en server paa, saa f falder til ~0.
|
|
54
|
+
// 2. connectTimeout haevet over bremsens 5-sekunders loft, saa vi ikke laengere
|
|
55
|
+
// draeber vores egne ventende forbindelser.
|
|
56
|
+
//
|
|
57
|
+
// Sidegevinst: det er sikrere. Foer aabnede udvidelsen blindt en WebSocket mod
|
|
58
|
+
// hvad der nu maatte lytte paa 9876-9895.
|
|
59
|
+
const PROBE_TIMEOUT_MS = 400;
|
|
60
|
+
let skanner = false;
|
|
61
|
+
|
|
62
|
+
async function harServer(port) {
|
|
63
|
+
try {
|
|
64
|
+
const svar = await fetch(`http://127.0.0.1:${port}/`, {
|
|
65
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
66
|
+
cache: 'no-store',
|
|
67
|
+
});
|
|
68
|
+
return svar.status === 426; // ws-serverens svar paa et almindeligt GET
|
|
69
|
+
} catch {
|
|
70
|
+
return false; // afvist, timeout eller ingen der lytter
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function scanPorts() {
|
|
75
|
+
if (skanner) return; // skanningen er nu asynkron; undgaa overlap
|
|
76
|
+
skanner = true;
|
|
77
|
+
try {
|
|
78
|
+
const kandidater = [];
|
|
79
|
+
for (let port = BASE_PORT; port <= MAX_PORT; port++) {
|
|
80
|
+
const existing = connections.get(port);
|
|
81
|
+
if (existing && (existing.readyState === WebSocket.OPEN || existing.readyState === WebSocket.CONNECTING)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
kandidater.push(port);
|
|
20
85
|
}
|
|
21
|
-
|
|
86
|
+
if (!kandidater.length) return;
|
|
87
|
+
|
|
88
|
+
// Probes koeres parallelt — de er gratis i bremsens regnskab.
|
|
89
|
+
const levende = await Promise.all(
|
|
90
|
+
kandidater.map(async (port) => (await harServer(port)) ? port : null),
|
|
91
|
+
);
|
|
92
|
+
for (const port of levende) {
|
|
93
|
+
if (port !== null) tryConnect(port);
|
|
94
|
+
}
|
|
95
|
+
} finally {
|
|
96
|
+
skanner = false;
|
|
22
97
|
}
|
|
23
98
|
}
|
|
24
99
|
|
|
@@ -30,13 +105,54 @@ function tryConnect(port) {
|
|
|
30
105
|
return;
|
|
31
106
|
}
|
|
32
107
|
|
|
108
|
+
// MAALT 21/8: en og samme udvidelse stod med TO aabne forbindelser til den samme
|
|
109
|
+
// server. Aarsagen laa her: forbindelsen blev foerst skrevet i kortet i onopen.
|
|
110
|
+
// scanPorts koerer hvert 2. sekund og springer kun over hvis kortet allerede har
|
|
111
|
+
// en forbindelse der er OPEN eller CONNECTING — men i vinduet mellem `new WebSocket`
|
|
112
|
+
// og onopen stod der intet i kortet. Naeste scan lavede derfor endnu en. Den foerste
|
|
113
|
+
// blev forældreloes: aldrig lukket, aldrig i kortet, men fuldt aaben.
|
|
114
|
+
// Registrering med det samme lukker vinduet.
|
|
115
|
+
connections.set(port, ws);
|
|
116
|
+
|
|
117
|
+
// 8000, ikke 2000: Chromes bremse kan lovligt holde et haandtryk i op til 5000 ms,
|
|
118
|
+
// og at lukke ned foer det taeller som en fejl der goer bremsen haardere. Se blokken
|
|
119
|
+
// over scanPorts. En haengende port koster nu en plads i 8 sekunder — scanPorts
|
|
120
|
+
// springer allerede CONNECTING over, saa det blokerer intet.
|
|
33
121
|
const connectTimeout = setTimeout(() => {
|
|
34
122
|
if (ws.readyState !== WebSocket.OPEN) ws.close();
|
|
35
|
-
},
|
|
123
|
+
}, 8000);
|
|
36
124
|
|
|
37
125
|
ws.onopen = () => {
|
|
38
126
|
clearTimeout(connectTimeout);
|
|
39
|
-
connections.set(port, ws);
|
|
127
|
+
connections.set(port, ws); // stadig vores? scanPorts har ikke lavet en nyere
|
|
128
|
+
|
|
129
|
+
// ── Identitets-haandtryk (MAALT 21/8) ──────────────────────────────────
|
|
130
|
+
// To udgaver af udvidelsen kan vaere indlaest i den samme Chrome samtidig —
|
|
131
|
+
// fx en "load unpacked"-kopi ved siden af en anden. Begge scanner de samme
|
|
132
|
+
// porte, saa BEGGE forbinder til hver eneste MCP-server. Serveren havde kun
|
|
133
|
+
// én socket-variabel, som hver ny forbindelse overskrev, saa kommandoerne
|
|
134
|
+
// landede hos den der forbandt sidst — vilkaarligt hvilken af de to. Den
|
|
135
|
+
// anden fortsatte med sit eget sessions-kort og sine egne fane-grupper.
|
|
136
|
+
// Haandtrykket giver serveren det den mangler for at kunne se at der er to,
|
|
137
|
+
// vaelge den nyeste, og sige det hoejt i stedet for at gaette i tavshed.
|
|
138
|
+
// MAALT 22/8 mod en aegte Chrome: haandtrykket ankom ALDRIG, selv om fetch-probet
|
|
139
|
+
// — som ligger i samme fil og samme dokument — virkede fint. Den tomme `catch {}`
|
|
140
|
+
// slugte aarsagen i tavshed, saa fejlen var usynlig baade for serveren og for os.
|
|
141
|
+
// To rettelser: haandtrykket sendes nu UANSET om manifest-opslaget lykkes (det er
|
|
142
|
+
// selve beskeden serveren har brug for, ikke felterne i den), og en fejl bliver
|
|
143
|
+
// logget i stedet for at forsvinde.
|
|
144
|
+
let hilsen = { type: 'hello', extensionId: null, version: minVersion(), name: null };
|
|
145
|
+
try {
|
|
146
|
+
hilsen = { type: 'hello', extensionId: chrome.runtime.id, version: minVersion(), name: null };
|
|
147
|
+
} catch (e) {
|
|
148
|
+
console.warn('[Offscreen] kunne ikke bygge haandtrykket:', e?.message || e);
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
ws.send(JSON.stringify(hilsen));
|
|
152
|
+
} catch (e) {
|
|
153
|
+
console.warn('[Offscreen] kunne ikke sende haandtrykket:', e?.message || e);
|
|
154
|
+
}
|
|
155
|
+
|
|
40
156
|
console.log(`[Offscreen] Connected to MCP server on port ${port} (${connections.size} total)`);
|
|
41
157
|
updateStatus();
|
|
42
158
|
};
|
|
@@ -44,13 +160,14 @@ function tryConnect(port) {
|
|
|
44
160
|
ws.onmessage = async (event) => {
|
|
45
161
|
let cmd;
|
|
46
162
|
try { cmd = JSON.parse(event.data); } catch { return; }
|
|
47
|
-
const { id, method, params } = cmd;
|
|
163
|
+
const { id, method, params, pid } = cmd;
|
|
48
164
|
|
|
49
165
|
try {
|
|
50
166
|
// Include port so background.js knows which session owns this command
|
|
51
167
|
const result = await chrome.runtime.sendMessage({
|
|
52
168
|
type: 'mcp_command',
|
|
53
169
|
port,
|
|
170
|
+
pid,
|
|
54
171
|
method,
|
|
55
172
|
params: params || {},
|
|
56
173
|
});
|
|
@@ -92,6 +209,21 @@ function updateStatus() {
|
|
|
92
209
|
}).catch(() => {});
|
|
93
210
|
}
|
|
94
211
|
|
|
212
|
+
// Svar paa hjerteslaget. Uden dette svar kan background.js ikke skelne et levende
|
|
213
|
+
// dokument fra et der findes men aldrig fik sit script indlaest — og saa bliver et
|
|
214
|
+
// doedt dokument aldrig erstattet.
|
|
215
|
+
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
|
216
|
+
if (msg?.type !== 'bmcp_ping') return;
|
|
217
|
+
// MAALT 22/8: svaret var bare { ok: true }. Det fortalte om broen var I LIVE, ikke
|
|
218
|
+
// om den var OPDATERET — og det er to forskellige spoergsmaal. En bro fra en aeldre
|
|
219
|
+
// udgave svarer lige saa villigt paa ping, saa ensureOffscreen regnede den for rask
|
|
220
|
+
// og udskiftede den aldrig. Resultatet: kode-aendringer slog aldrig igennem uden en
|
|
221
|
+
// fuld genstart af Chrome, heller ikke efter "Genindlaes" paa chrome://extensions.
|
|
222
|
+
// Versionen med i svaret goer forskellen synlig.
|
|
223
|
+
sendResponse({ ok: true, version: minVersion() });
|
|
224
|
+
return true;
|
|
225
|
+
});
|
|
226
|
+
|
|
95
227
|
// Listen for terminate signals from background.js (sent when last tab in a session closes)
|
|
96
228
|
chrome.runtime.onMessage.addListener((msg) => {
|
|
97
229
|
if (msg.type !== 'terminate_mcp_session' || typeof msg.port !== 'number') return;
|
package/extension/popup.html
CHANGED
|
@@ -37,6 +37,24 @@
|
|
|
37
37
|
.color-purple { border-color: #a855f7; } .color-purple .session-header { color: #a855f7; }
|
|
38
38
|
.color-cyan { border-color: #06b6d4; } .color-cyan .session-header { color: #06b6d4; }
|
|
39
39
|
.color-orange { border-color: #f97316; } .color-orange .session-header { color: #f97316; }
|
|
40
|
+
/* Shown only while the MCP server is missing — the store can't install it for you */
|
|
41
|
+
.setup { display: none; background: #1e293b; border: 1px solid #334155; border-left: 3px solid #f59e0b; border-radius: 8px; padding: 10px; margin-bottom: 8px; }
|
|
42
|
+
.setup.show { display: block; }
|
|
43
|
+
.setup p { font-size: 11px; line-height: 1.5; color: #94a3b8; margin-bottom: 8px; }
|
|
44
|
+
.setup p b { color: #e2e8f0; }
|
|
45
|
+
.setup code { display: block; background: #0f172a; border-radius: 6px; padding: 8px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; color: #22c55e; word-break: break-all; margin-bottom: 6px; }
|
|
46
|
+
.setup .row { display: flex; gap: 6px; }
|
|
47
|
+
.setup .row button, .setup .row a { flex: 1; margin: 0; padding: 6px; border-radius: 6px; background: #334155; color: #e2e8f0; font-size: 11px; text-align: center; text-decoration: none; border: none; cursor: pointer; }
|
|
48
|
+
.setup .row a:hover, .setup .row button:hover { background: #475569; }
|
|
49
|
+
/* Shown once both halves are up but the agent has not touched a tab yet — the
|
|
50
|
+
moment a new user checks "did it work?" and needs to know what to say next. */
|
|
51
|
+
.try { display: none; background: #1e293b; border: 1px solid #334155; border-left: 3px solid #22c55e; border-radius: 8px; padding: 10px; margin-bottom: 8px; }
|
|
52
|
+
.try.show { display: block; }
|
|
53
|
+
.try p { font-size: 11px; line-height: 1.5; color: #94a3b8; margin-bottom: 8px; }
|
|
54
|
+
.try p b { color: #e2e8f0; }
|
|
55
|
+
.try .prompt { display: block; background: #0f172a; border-radius: 6px; padding: 8px; font-size: 11px; color: #e2e8f0; margin-bottom: 6px; font-style: italic; }
|
|
56
|
+
.try a { display: block; text-align: center; padding: 6px; border-radius: 6px; background: #334155; color: #e2e8f0; font-size: 11px; text-decoration: none; }
|
|
57
|
+
.try a:hover { background: #475569; }
|
|
40
58
|
</style>
|
|
41
59
|
</head>
|
|
42
60
|
<body>
|
|
@@ -44,12 +62,28 @@
|
|
|
44
62
|
|
|
45
63
|
<div class="status">
|
|
46
64
|
<div class="dot off" id="dot"></div>
|
|
47
|
-
<span class="label" id="label">Checking
|
|
65
|
+
<span class="label" id="label">Checking…</span>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div class="setup" id="setup">
|
|
69
|
+
<p><b>This extension is only half of Browser MCP.</b> It needs a local MCP server to talk to — the Chrome Web Store cannot install that part. Register it once, then restart your agent. Claude Code:</p>
|
|
70
|
+
<code id="setupCmd">claude mcp add --scope user browser-mcp -- npx @agent360/browser-mcp@latest</code>
|
|
71
|
+
<div class="row">
|
|
72
|
+
<button id="copyCmd">Copy command</button>
|
|
73
|
+
<a href="https://browsermcp.dev/docs/install-claude-code/" target="_blank" rel="noopener">Setup guide</a>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="try" id="try">
|
|
78
|
+
<p><b>Connected — you're ready.</b> Nothing happens until you ask your agent for something. Try saying:</p>
|
|
79
|
+
<span class="prompt">"Take a screenshot of my current Chrome tab."</span>
|
|
80
|
+
<span class="prompt">"Open my Gmail tab and tell me who sent my last 3 emails."</span>
|
|
81
|
+
<a href="https://browsermcp.dev/#try" target="_blank" rel="noopener">More things to try</a>
|
|
48
82
|
</div>
|
|
49
83
|
|
|
50
84
|
<div class="section-title">Sessions</div>
|
|
51
85
|
<div id="sessions" class="sessions">
|
|
52
|
-
<div class="empty">No
|
|
86
|
+
<div class="empty">No tabs claimed yet</div>
|
|
53
87
|
</div>
|
|
54
88
|
|
|
55
89
|
<div class="section-title">Action Log</div>
|
package/extension/popup.js
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
// ── Status ──────────────────────────────────────────────────────────────────
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
// mcpCount = agent clients on the socket. `sessions` = agents that have actually claimed a
|
|
4
|
+
// tab. They differ, and showing "Connected — 1 session" above "No active sessions" read as a
|
|
5
|
+
// contradiction at exactly the moment a new user checks whether the install worked.
|
|
6
|
+
function paintStatus(connected, count, sessionCount) {
|
|
6
7
|
document.getElementById('dot').className = `dot ${connected ? 'on' : 'off'}`;
|
|
7
|
-
document.getElementById('label').textContent = connected
|
|
8
|
-
?
|
|
9
|
-
:
|
|
10
|
-
}
|
|
8
|
+
document.getElementById('label').textContent = !connected
|
|
9
|
+
? 'Not connected — no MCP server found'
|
|
10
|
+
: sessionCount > 0
|
|
11
|
+
? `Connected — ${sessionCount} active session${sessionCount === 1 ? '' : 's'}`
|
|
12
|
+
: `Connected — ready (${count} agent${count === 1 ? '' : 's'} listening)`;
|
|
13
|
+
// The Chrome Web Store can only install the extension. If nothing is listening on
|
|
14
|
+
// ports 9876-9895, the user almost certainly never ran the npx install — say so.
|
|
15
|
+
document.getElementById('setup').classList.toggle('show', !connected);
|
|
16
|
+
// Connected but idle: they got it working and now need to know what to say.
|
|
17
|
+
document.getElementById('try').classList.toggle('show', connected && sessionCount === 0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function refreshStatus() {
|
|
21
|
+
chrome.storage.local.get({ mcpConnected: false, mcpCount: 0, sessions: {} }, (result) => {
|
|
22
|
+
paintStatus(result.mcpConnected === true, result.mcpCount || 0,
|
|
23
|
+
Object.keys(result.sessions || {}).length);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
refreshStatus();
|
|
11
28
|
|
|
12
29
|
// ── Sessions with tabs ─────────────────────────────────────────────────────
|
|
13
30
|
|
|
@@ -17,7 +34,7 @@ function renderSessions() {
|
|
|
17
34
|
const entries = Object.entries(sessions);
|
|
18
35
|
|
|
19
36
|
if (!entries.length) {
|
|
20
|
-
container.innerHTML = '<div class="empty">
|
|
37
|
+
container.innerHTML = '<div class="empty">No tabs claimed yet</div>';
|
|
21
38
|
return;
|
|
22
39
|
}
|
|
23
40
|
|
|
@@ -40,7 +57,7 @@ function renderSessions() {
|
|
|
40
57
|
const color = session.color || 'blue';
|
|
41
58
|
const tabHtml = tabInfos.length
|
|
42
59
|
? tabInfos.map(u => `<div>• ${u}</div>`).join('')
|
|
43
|
-
: '<div>
|
|
60
|
+
: '<div>No tabs</div>';
|
|
44
61
|
return `
|
|
45
62
|
<div class="session-card color-${color}">
|
|
46
63
|
<div class="session-header">${session.label} <span style="font-weight:normal;font-size:10px;color:#64748b">port ${port}</span></div>
|
|
@@ -59,11 +76,11 @@ function renderLog() {
|
|
|
59
76
|
chrome.storage.local.get({ actionLog: [] }, ({ actionLog }) => {
|
|
60
77
|
const container = document.getElementById('log');
|
|
61
78
|
if (!actionLog.length) {
|
|
62
|
-
container.innerHTML = '<div class="empty">
|
|
79
|
+
container.innerHTML = '<div class="empty">No actions yet</div>';
|
|
63
80
|
return;
|
|
64
81
|
}
|
|
65
82
|
container.innerHTML = actionLog.slice(0, 30).map(entry => {
|
|
66
|
-
const time = new Date(entry.time).toLocaleTimeString(
|
|
83
|
+
const time = new Date(entry.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
67
84
|
const cat = entry.category || 'safe';
|
|
68
85
|
const cls = cat === 'sensitive' ? 'log-sensitive' : 'log-safe';
|
|
69
86
|
return `<div class="log-entry"><span class="log-time">${time}</span><span class="log-method ${cls}">${entry.method}</span><span class="log-session">${entry.session || ''}</span></div>`;
|
|
@@ -77,20 +94,19 @@ renderLog();
|
|
|
77
94
|
|
|
78
95
|
document.getElementById('reconnect').addEventListener('click', () => {
|
|
79
96
|
chrome.runtime.sendMessage({ type: 'reconnect' });
|
|
80
|
-
document.getElementById('label').textContent = 'Reconnecting
|
|
97
|
+
document.getElementById('label').textContent = 'Reconnecting…';
|
|
81
98
|
setTimeout(() => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const count = result.mcpCount || 0;
|
|
85
|
-
document.getElementById('dot').className = `dot ${connected ? 'on' : 'off'}`;
|
|
86
|
-
document.getElementById('label').textContent = connected
|
|
87
|
-
? `Forbundet til ${count} session${count > 1 ? 's' : ''}`
|
|
88
|
-
: 'Ikke forbundet';
|
|
89
|
-
renderSessions();
|
|
90
|
-
});
|
|
99
|
+
refreshStatus();
|
|
100
|
+
renderSessions();
|
|
91
101
|
}, 3000);
|
|
92
102
|
});
|
|
93
103
|
|
|
104
|
+
document.getElementById('copyCmd').addEventListener('click', (e) => {
|
|
105
|
+
navigator.clipboard.writeText(document.getElementById('setupCmd').textContent.trim());
|
|
106
|
+
e.target.textContent = 'Copied!';
|
|
107
|
+
setTimeout(() => { e.target.textContent = 'Copy command'; }, 1500);
|
|
108
|
+
});
|
|
109
|
+
|
|
94
110
|
document.getElementById('clearLog').addEventListener('click', () => {
|
|
95
111
|
chrome.storage.local.set({ actionLog: [] }, renderLog);
|
|
96
112
|
});
|