@mirasim/computer-use 0.1.0-rc.6 → 0.1.0-rc.7
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 +3 -1
- package/dist/platform/browser/bridge/model-commands.js +1 -1
- package/dist/platform/browser/bridge/model-core.d.ts +7 -0
- package/dist/platform/browser/bridge/model-core.js +1 -1
- package/dist/platform/browser/bridge/server.js +1 -1
- package/dist/platform/browser/bridge-tabs.d.ts +3 -0
- package/dist/platform/browser/bridge-tabs.js +1 -1
- package/dist/platform/browser/driver-verbs.js +2 -2
- package/dist/platform/browser/driver.d.ts +2 -0
- package/dist/platform/browser/driver.js +3 -2
- package/dist/platform/browser/messages.d.ts +5 -0
- package/dist/platform/browser/messages.js +2 -2
- package/dist/platform/chromium/actions.js +2 -2
- package/dist/platform/chromium/run-js.d.ts +5 -1
- package/dist/platform/chromium/run-js.js +2 -2
- package/dist/platform/electron/driver.js +1 -1
- package/dist/platform/electron/verbs.js +1 -1
- package/dist/platform/macos/type-verb.js +1 -1
- package/dist/platform/tools/api-contract.d.ts +1 -0
- package/dist/platform/tools/api.js +1 -1
- package/dist/platform/tools/repl-docs.js +1 -1
- package/dist/platform/tools/repl.js +12 -12
- package/extension/_locales/en/messages.json +179 -0
- package/extension/_locales/zh_CN/messages.json +179 -0
- package/extension/access.js +245 -0
- package/extension/ask.html +26 -0
- package/extension/ask.js +20 -0
- package/extension/background.js +79 -3
- package/extension/group.js +335 -79
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-16.png +0 -0
- package/extension/icons/icon-32.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/manifest.json +8 -2
- package/extension/panel.css +86 -0
- package/extension/panel.html +86 -0
- package/extension/panel.js +192 -0
- package/extension/relay.js +264 -5
- package/extension/toolbar.js +172 -0
- package/package.json +5 -5
- package/scripts/extension.mjs +4 -2
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the agent may reach in this browser: the user's own settings, enforced HERE.
|
|
3
|
+
*
|
|
4
|
+
* The extension is the one layer of the connection the agent does not run, so the limits live
|
|
5
|
+
* in it and hold whichever bridge is on the other end (the desktop app's, an npm package's, an
|
|
6
|
+
* older one). The relay asks this module before it lists a tab, attaches one, runs a command or
|
|
7
|
+
* forwards an event (relay.js), and the tab group says which tabs are in scope (group.js).
|
|
8
|
+
*
|
|
9
|
+
* - **scope** — `group` (the default): tabs in the mirasim group, tabs the agent opened and
|
|
10
|
+
* the popups of those; `all`: every tab, as before 0.1.3; `ask`: every tab is listed, and the
|
|
11
|
+
* first attach of one outside the group waits for the user's yes in a small window.
|
|
12
|
+
* - **site rules** — a block list, and an optional allow-only list, both of host names that
|
|
13
|
+
* also cover their subdomains. They hold in every scope: a blocked site is neither listed,
|
|
14
|
+
* opened, navigated to nor read, and its cookies and downloads are filtered out.
|
|
15
|
+
* - **pause** — every tab is let go of, and every command but the keepalive and the tab
|
|
16
|
+
* listing is refused until the user resumes (group.js `reevaluate`, relay.js).
|
|
17
|
+
*
|
|
18
|
+
* Settings live in `chrome.storage.sync` (they follow the user's Chrome profile); the pause,
|
|
19
|
+
* the ask-mode grants and the log of refusals live in `chrome.storage.session`, gone with the
|
|
20
|
+
* browser session they describe.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const SETTINGS_KEY = "mirasim-access";
|
|
24
|
+
export const STATE_KEY = "mirasim-access-state";
|
|
25
|
+
/** Set once the user has opened the panel; until then the toolbar shows "new". */
|
|
26
|
+
export const PANEL_SEEN_KEY = "mirasim-panel-seen";
|
|
27
|
+
export const SCOPES = ["group", "all", "ask"];
|
|
28
|
+
export const DEFAULT_SETTINGS = Object.freeze({ scope: "group", blocked: [], allowOnly: false, allowed: [] });
|
|
29
|
+
/** How many refusals the panel shows. */
|
|
30
|
+
const LOG_LIMIT = 20;
|
|
31
|
+
/** The same refusal again within this window is one entry, not a flood (a paused agent retries). */
|
|
32
|
+
const LOG_DEDUPE_MS = 2_000;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A pattern as the user typed it, reduced to a host name: `https://www.Bank.com/login` and
|
|
36
|
+
* `*.bank.com` both become what they mean (`www.bank.com`, `bank.com`). Empty for nonsense.
|
|
37
|
+
*/
|
|
38
|
+
export function normalizePattern(text) {
|
|
39
|
+
let s = String(text ?? "").trim().toLowerCase();
|
|
40
|
+
if (!s) return "";
|
|
41
|
+
s = s.replace(/^[a-z][a-z0-9+.-]*:\/\//, "");
|
|
42
|
+
s = s.split(/[/?#]/)[0];
|
|
43
|
+
s = s.replace(/^.*@/, "").replace(/:\d+$/, "");
|
|
44
|
+
s = s.replace(/^(\*\.)+/, "").replace(/^\.+/, "").replace(/\.+$/, "");
|
|
45
|
+
return /^[a-z0-9.-]+$/.test(s) || /^\[[0-9a-f:]+\]$/.test(s) ? s : "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** `host` is `pattern` or one of its subdomains. */
|
|
49
|
+
export function hostMatches(host, pattern) {
|
|
50
|
+
return host === pattern || host.endsWith(`.${pattern}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The host a URL loads from, or null for one that loads nothing from the web (about:blank,
|
|
55
|
+
* data:, the browser's own pages). A blob: URL is its origin's.
|
|
56
|
+
*/
|
|
57
|
+
export function hostOf(url) {
|
|
58
|
+
if (!url) return null;
|
|
59
|
+
try {
|
|
60
|
+
let u = new URL(url);
|
|
61
|
+
if (u.protocol === "blob:") u = new URL(u.pathname);
|
|
62
|
+
if (!/^(https?|wss?|ftp):$/.test(u.protocol)) return null;
|
|
63
|
+
return u.hostname.toLowerCase();
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function cleanList(list) {
|
|
70
|
+
return [...new Set((Array.isArray(list) ? list : []).map(normalizePattern).filter(Boolean))];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Settings as stored, made whole: unknown fields dropped, every list normalized. */
|
|
74
|
+
export function cleanSettings(raw) {
|
|
75
|
+
const s = raw && typeof raw === "object" ? raw : {};
|
|
76
|
+
return {
|
|
77
|
+
scope: SCOPES.includes(s.scope) ? s.scope : DEFAULT_SETTINGS.scope,
|
|
78
|
+
blocked: cleanList(s.blocked),
|
|
79
|
+
allowOnly: s.allowOnly === true,
|
|
80
|
+
allowed: cleanList(s.allowed),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class AccessPolicy {
|
|
85
|
+
constructor() {
|
|
86
|
+
this.settings = cleanSettings(DEFAULT_SETTINGS);
|
|
87
|
+
this.paused = false;
|
|
88
|
+
/** Ask mode: tabs the user said yes to, until they close. */
|
|
89
|
+
this.grants = new Set();
|
|
90
|
+
/** @type {Array<{at: number, reason: string, tabId?: number, url?: string, detail?: string}>} */
|
|
91
|
+
this.log = [];
|
|
92
|
+
/** @type {Set<() => void>} */
|
|
93
|
+
this._listeners = new Set();
|
|
94
|
+
chrome.storage.onChanged.addListener((changes, area) => {
|
|
95
|
+
if (area === "sync" && changes[SETTINGS_KEY]) {
|
|
96
|
+
this.settings = cleanSettings(changes[SETTINGS_KEY].newValue);
|
|
97
|
+
this._emit();
|
|
98
|
+
} else if (area === "session" && changes[STATE_KEY]) {
|
|
99
|
+
const before = this.paused;
|
|
100
|
+
this._adoptState(changes[STATE_KEY].newValue);
|
|
101
|
+
if (before !== this.paused) this._emit();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
this.ready = this._load();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async _load() {
|
|
108
|
+
const [sync, session] = await Promise.all([
|
|
109
|
+
chrome.storage.sync.get(SETTINGS_KEY).catch(() => ({})),
|
|
110
|
+
chrome.storage.session.get(STATE_KEY).catch(() => ({})),
|
|
111
|
+
]);
|
|
112
|
+
this.settings = cleanSettings(sync?.[SETTINGS_KEY]);
|
|
113
|
+
this._adoptState(session?.[STATE_KEY]);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
_adoptState(state) {
|
|
117
|
+
this.paused = state?.paused === true;
|
|
118
|
+
this.grants = new Set(Array.isArray(state?.grants) ? state.grants.filter(Number.isInteger) : []);
|
|
119
|
+
this.log = Array.isArray(state?.log) ? state.log.slice(-LOG_LIMIT) : [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_saveState() {
|
|
123
|
+
return chrome.storage.session
|
|
124
|
+
.set({ [STATE_KEY]: { paused: this.paused, grants: [...this.grants], log: this.log } })
|
|
125
|
+
.catch(() => {});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Called whenever what is reachable may have changed: settings, or the pause. */
|
|
129
|
+
onChange(listener) {
|
|
130
|
+
this._listeners.add(listener);
|
|
131
|
+
return () => this._listeners.delete(listener);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_emit() {
|
|
135
|
+
for (const listener of this._listeners) {
|
|
136
|
+
try {
|
|
137
|
+
listener();
|
|
138
|
+
} catch {
|
|
139
|
+
/* one listener's failure is not the others' */
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async saveSettings(next) {
|
|
145
|
+
this.settings = cleanSettings(next);
|
|
146
|
+
await chrome.storage.sync.set({ [SETTINGS_KEY]: this.settings });
|
|
147
|
+
this._emit();
|
|
148
|
+
return this.settings;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async setPaused(paused) {
|
|
152
|
+
this.paused = paused === true;
|
|
153
|
+
await this._saveState();
|
|
154
|
+
this._emit();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
grant(tabId) {
|
|
158
|
+
this.grants.add(tabId);
|
|
159
|
+
void this._saveState();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
forget(tabId) {
|
|
163
|
+
if (this.grants.delete(tabId)) void this._saveState();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Anything stricter than "every tab, every site"? */
|
|
167
|
+
restrictive() {
|
|
168
|
+
const s = this.settings;
|
|
169
|
+
return s.scope !== "all" || s.blocked.length > 0 || s.allowOnly;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Why a URL is off limits, or null when it is not: `site-blocked` for the block list,
|
|
174
|
+
* `site-not-allowed` for a host outside the allow-only list. A URL that loads nothing from
|
|
175
|
+
* the web (about:blank, data:) is never refused here.
|
|
176
|
+
*/
|
|
177
|
+
siteVerdict(url) {
|
|
178
|
+
const host = hostOf(url);
|
|
179
|
+
if (host === null) return null;
|
|
180
|
+
const { blocked, allowOnly, allowed } = this.settings;
|
|
181
|
+
if (blocked.some((p) => hostMatches(host, p))) return "site-blocked";
|
|
182
|
+
if (allowOnly && !allowed.some((p) => hostMatches(host, p))) return "site-not-allowed";
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* May the agent see this cookie? A cookie set for a parent domain is sent to the blocked
|
|
188
|
+
* subdomain too, so `.bank.com` is withheld when `online.bank.com` is blocked; on an allow list,
|
|
189
|
+
* a cookie counts when it is sent to some allowed host.
|
|
190
|
+
*/
|
|
191
|
+
cookieAllowed(domain) {
|
|
192
|
+
const d = String(domain ?? "").replace(/^\./, "").toLowerCase();
|
|
193
|
+
if (!d) return true;
|
|
194
|
+
const related = (p) => hostMatches(d, p) || hostMatches(p, d);
|
|
195
|
+
const { blocked, allowOnly, allowed } = this.settings;
|
|
196
|
+
if (blocked.some(related)) return false;
|
|
197
|
+
if (allowOnly && !allowed.some(related)) return false;
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
record(entry) {
|
|
202
|
+
const last = this.log[this.log.length - 1];
|
|
203
|
+
const now = Date.now();
|
|
204
|
+
if (last && last.reason === entry.reason && last.tabId === entry.tabId && last.url === entry.url && now - last.at < LOG_DEDUPE_MS) return;
|
|
205
|
+
this.log.push({ at: now, ...entry });
|
|
206
|
+
if (this.log.length > LOG_LIMIT) this.log.splice(0, this.log.length - LOG_LIMIT);
|
|
207
|
+
void this._saveState();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* A refusal the bridge passes on to the agent as the command's error: what was refused and
|
|
213
|
+
* what the agent can do about it, in words a model relays to the person.
|
|
214
|
+
*/
|
|
215
|
+
export function refusal(reason, { tabId, url } = {}) {
|
|
216
|
+
const host = hostOf(url) ?? url ?? "this site";
|
|
217
|
+
switch (reason) {
|
|
218
|
+
case "site-blocked":
|
|
219
|
+
return `${host} is blocked by the user's Mirasim site rules; the agent cannot open or read it.`;
|
|
220
|
+
case "site-not-allowed":
|
|
221
|
+
return `${host} is not on the user's Mirasim allow list; the agent can only use the sites on it.`;
|
|
222
|
+
case "out-of-scope":
|
|
223
|
+
return `Tab ${tabId} is not one the user handed to the agent. Ask them to click the Mirasim toolbar icon on that tab to hand it over, or open a tab of your own.`;
|
|
224
|
+
case "ask-pending":
|
|
225
|
+
return `Waiting for the user to allow tab ${tabId} in the Mirasim prompt; try again once they answer.`;
|
|
226
|
+
case "declined":
|
|
227
|
+
return `The user declined to let the agent use tab ${tabId}.`;
|
|
228
|
+
case "paused":
|
|
229
|
+
return "The user paused the agent from the Mirasim extension; wait until they resume it.";
|
|
230
|
+
case "clear-cookies":
|
|
231
|
+
return "Clearing every browser cookie is refused while the user limits what the agent can reach.";
|
|
232
|
+
case "cookie-site":
|
|
233
|
+
return `Cookies of ${host} are off limits under the user's Mirasim site rules.`;
|
|
234
|
+
default:
|
|
235
|
+
return "Refused by the user's Mirasim access settings.";
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export class AccessError extends Error {
|
|
240
|
+
constructor(reason, details = {}) {
|
|
241
|
+
super(refusal(reason, details));
|
|
242
|
+
this.reason = reason;
|
|
243
|
+
this.details = details;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>Mirasim</title>
|
|
6
|
+
<link rel="stylesheet" href="panel.css" />
|
|
7
|
+
</head>
|
|
8
|
+
<body class="ask">
|
|
9
|
+
<main>
|
|
10
|
+
<header>
|
|
11
|
+
<img src="icons/icon-32.png" alt="" width="24" height="24" />
|
|
12
|
+
<div class="brand"><strong data-i18n="askTitle"></strong></div>
|
|
13
|
+
</header>
|
|
14
|
+
<div class="card">
|
|
15
|
+
<div id="tab-title" class="tab-title"></div>
|
|
16
|
+
<div id="tab-host" class="tab-host"></div>
|
|
17
|
+
</div>
|
|
18
|
+
<p class="quiet" data-i18n="askBody"></p>
|
|
19
|
+
<div class="row">
|
|
20
|
+
<button id="allow" type="button" data-i18n="allow"></button>
|
|
21
|
+
<button id="deny" type="button" class="ghost" data-i18n="deny"></button>
|
|
22
|
+
</div>
|
|
23
|
+
</main>
|
|
24
|
+
<script src="ask.js" type="module"></script>
|
|
25
|
+
</body>
|
|
26
|
+
</html>
|
package/extension/ask.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** The ask window (`ask` scope, group.js `_ask`): may the agent use this tab? */
|
|
2
|
+
|
|
3
|
+
const t = (key) => chrome.i18n.getMessage(key) || key;
|
|
4
|
+
for (const el of document.querySelectorAll("[data-i18n]")) el.textContent = t(el.dataset.i18n);
|
|
5
|
+
|
|
6
|
+
const tabId = Number(new URLSearchParams(location.search).get("tab"));
|
|
7
|
+
const tab = await chrome.tabs.get(tabId).catch(() => null);
|
|
8
|
+
if (!tab) {
|
|
9
|
+
document.getElementById("tab-title").textContent = t("askGone");
|
|
10
|
+
document.getElementById("allow").disabled = true;
|
|
11
|
+
} else {
|
|
12
|
+
document.getElementById("tab-title").textContent = tab.title || tab.url;
|
|
13
|
+
document.getElementById("tab-host").textContent = tab.url;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function answer(allow) {
|
|
17
|
+
chrome.runtime.sendMessage({ type: "access.answer", tabId, allow }, () => window.close());
|
|
18
|
+
}
|
|
19
|
+
document.getElementById("allow").addEventListener("click", () => answer(true));
|
|
20
|
+
document.getElementById("deny").addEventListener("click", () => answer(false));
|
package/extension/background.js
CHANGED
|
@@ -19,8 +19,10 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { RelayConnection, debugLog } from "./relay.js";
|
|
22
|
-
import { MirasimTabGroup } from "./group.js";
|
|
22
|
+
import { MirasimTabGroup, addToMirasimGroup } from "./group.js";
|
|
23
23
|
import { nativeTransport, webSocketTransport } from "./transport.js";
|
|
24
|
+
import { AccessPolicy, PANEL_SEEN_KEY } from "./access.js";
|
|
25
|
+
import { Toolbar } from "./toolbar.js";
|
|
24
26
|
|
|
25
27
|
const CONNECT_TIMEOUT_MS = 10_000;
|
|
26
28
|
/** How long the native probe waits for `pong` before calling the route dead. */
|
|
@@ -65,6 +67,9 @@ class BridgeExtension {
|
|
|
65
67
|
this._timer = null;
|
|
66
68
|
this._lastError = null;
|
|
67
69
|
this._updateVersion = null;
|
|
70
|
+
/** The user's access settings, held by every connection (access.js). */
|
|
71
|
+
this._policy = new AccessPolicy();
|
|
72
|
+
this._toolbar = new Toolbar(this._policy, () => this._activeGroup);
|
|
68
73
|
chrome.runtime.onUpdateAvailable.addListener(({ version }) => {
|
|
69
74
|
this._updateVersion = version;
|
|
70
75
|
if (this._activeGroup) this._activeGroup.updateAvailable(version);
|
|
@@ -91,7 +96,14 @@ class BridgeExtension {
|
|
|
91
96
|
});
|
|
92
97
|
return false;
|
|
93
98
|
default:
|
|
94
|
-
|
|
99
|
+
// The panel and the ask window: this extension's own pages, nothing else.
|
|
100
|
+
if (sender?.id !== chrome.runtime.id || !sender.url?.startsWith(chrome.runtime.getURL(""))) return false;
|
|
101
|
+
if (!String(message?.type).startsWith("panel.") && message?.type !== "access.answer") return false;
|
|
102
|
+
this._onPageMessage(message).then(
|
|
103
|
+
(result) => sendResponse({ ok: true, result }),
|
|
104
|
+
(e) => sendResponse({ ok: false, error: e?.message ?? String(e) }),
|
|
105
|
+
);
|
|
106
|
+
return true;
|
|
95
107
|
}
|
|
96
108
|
});
|
|
97
109
|
|
|
@@ -109,6 +121,67 @@ class BridgeExtension {
|
|
|
109
121
|
void this._connectNative();
|
|
110
122
|
}
|
|
111
123
|
|
|
124
|
+
// ─── the panel and the ask window ────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
async _onPageMessage(message) {
|
|
127
|
+
await this._policy.ready;
|
|
128
|
+
const group = this._activeGroup;
|
|
129
|
+
switch (message.type) {
|
|
130
|
+
case "panel.state":
|
|
131
|
+
return await this._panelState(message.tabId);
|
|
132
|
+
case "panel.seen":
|
|
133
|
+
await chrome.storage.local.set({ [PANEL_SEEN_KEY]: true });
|
|
134
|
+
return {};
|
|
135
|
+
case "panel.settings":
|
|
136
|
+
return await this._policy.saveSettings(message.settings);
|
|
137
|
+
case "panel.pause":
|
|
138
|
+
await this._policy.setPaused(message.paused === true);
|
|
139
|
+
return {};
|
|
140
|
+
case "panel.handOver":
|
|
141
|
+
await addToMirasimGroup(message.tabId);
|
|
142
|
+
await this._toolbar.refresh(message.tabId);
|
|
143
|
+
return {};
|
|
144
|
+
case "panel.takeBack":
|
|
145
|
+
if (group) await group.takeBack(message.tabId);
|
|
146
|
+
else await chrome.tabs.ungroup(message.tabId).catch(() => {});
|
|
147
|
+
await this._toolbar.refresh(message.tabId);
|
|
148
|
+
return {};
|
|
149
|
+
case "panel.focusTab": {
|
|
150
|
+
const tab = await chrome.tabs.update(message.tabId, { active: true });
|
|
151
|
+
if (tab?.windowId !== undefined) await chrome.windows.update(tab.windowId, { focused: true });
|
|
152
|
+
return {};
|
|
153
|
+
}
|
|
154
|
+
case "access.answer":
|
|
155
|
+
group?.answerAsk(message.tabId, message.allow === true);
|
|
156
|
+
return {};
|
|
157
|
+
default:
|
|
158
|
+
throw new Error(`Unknown message: ${message.type}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Everything the panel shows, in one answer. */
|
|
163
|
+
async _panelState(tabId) {
|
|
164
|
+
const group = this._activeGroup;
|
|
165
|
+
const tab = Number.isInteger(tabId) ? await chrome.tabs.get(tabId).catch(() => null) : null;
|
|
166
|
+
const using = [];
|
|
167
|
+
for (const id of group?.connectedTabIds() ?? []) {
|
|
168
|
+
const t = await chrome.tabs.get(id).catch(() => null);
|
|
169
|
+
if (t) using.push({ id, title: t.title ?? "", url: t.url ?? "" });
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
connected: !!group,
|
|
173
|
+
transport: this._transport,
|
|
174
|
+
error: group ? null : this._lastError,
|
|
175
|
+
extension: chrome.runtime.getManifest().version,
|
|
176
|
+
bridge: group?.bridgeInfo ?? null,
|
|
177
|
+
paused: this._policy.paused,
|
|
178
|
+
settings: this._policy.settings,
|
|
179
|
+
tab: tab ? { id: tab.id, title: tab.title ?? "", url: tab.url ?? "", ...(await this._toolbar.stateOf(tab)) } : null,
|
|
180
|
+
using,
|
|
181
|
+
log: this._policy.log,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
112
185
|
// ─── native messaging ────────────────────────────────────────────────────
|
|
113
186
|
|
|
114
187
|
/** Try each host in turn; keep the first that answers `pong`. */
|
|
@@ -257,11 +330,13 @@ class BridgeExtension {
|
|
|
257
330
|
async _adopt(connection, transport) {
|
|
258
331
|
this._activeGroup?.close("Replaced by a new bridge connection");
|
|
259
332
|
this._activeGroup = null;
|
|
260
|
-
const group = new MirasimTabGroup(connection);
|
|
333
|
+
const group = new MirasimTabGroup(connection, this._policy);
|
|
334
|
+
group.ontabstate = (tabId) => void this._toolbar.refresh(tabId);
|
|
261
335
|
group.onclose = () => {
|
|
262
336
|
if (this._activeGroup !== group) return;
|
|
263
337
|
this._activeGroup = null;
|
|
264
338
|
this._transport = null;
|
|
339
|
+
void this._toolbar.refreshAll();
|
|
265
340
|
// Native reconnects on its own; the ws path needs a new hand-over.
|
|
266
341
|
if (transport === "native") this._scheduleRetry();
|
|
267
342
|
};
|
|
@@ -273,6 +348,7 @@ class BridgeExtension {
|
|
|
273
348
|
// Armed WHILE connected, deliberately — see HEARTBEAT_MINUTES.
|
|
274
349
|
this._armAlarm(HEARTBEAT_MINUTES);
|
|
275
350
|
await group.ready;
|
|
351
|
+
void this._toolbar.refreshAll();
|
|
276
352
|
if (this._updateVersion) group.updateAvailable(this._updateVersion);
|
|
277
353
|
}
|
|
278
354
|
}
|