3126992daf627ee3a351d32c382abb40bcf38024dcfa16af40a7cfb32525c8b8beforeToolCallenrichContext1/**
2 * Browser Bridge Core
3 *
4 * Manages WebSocket connections to Chrome extensions.
5 * Request/response correlation. Site scoping. Activity logging.
6 */
7
8import { v4 as uuidv4 } from "uuid";
9import log from "../../seed/log.js";
10import { createNote } from "../../seed/tree/notes.js";
11
12let _metadata = null;
13let _Node = null;
14
15export function configure({ metadata, Node }) {
16 _metadata = metadata;
17 _Node = Node;
18}
19
20// Active browser connections: userId -> socket
21const _connections = new Map();
22
23// Pending requests: requestId -> { resolve, reject, timer }
24const _pending = new Map();
25
26// Last known URL per user
27const _currentUrls = new Map();
28
29const REQUEST_TIMEOUT_MS = 15000;
30
31// ─────────────────────────────────────────────────────────────────────────
32// CONNECTION MANAGEMENT
33// ─────────────────────────────────────────────────────────────────────────
34
35export function registerConnection(userId, socket) {
36 // Disconnect previous browser connection for this user (if any)
37 const existing = _connections.get(userId);
38 if (existing && existing.id !== socket.id) {
39 existing.emit("browserDisconnected", { reason: "new connection" });
40 }
41 _connections.set(userId, socket);
42 log.info("BrowserBridge", `Browser connected for user ${userId}`);
43
44 socket.on("disconnect", () => {
45 if (_connections.get(userId)?.id === socket.id) {
46 _connections.delete(userId);
47 _currentUrls.delete(userId);
48 // Reject all pending requests for this user
49 for (const [id, pending] of _pending) {
50 if (pending.userId === userId) {
51 clearTimeout(pending.timer);
52 pending.reject(new Error("Browser disconnected"));
53 _pending.delete(id);
54 }
55 }
56 log.info("BrowserBridge", `Browser disconnected for user ${userId}`);
57 }
58 });
59}
60
61export function isConnected(userId) {
62 return _connections.has(userId);
63}
64
65export function getCurrentUrl(userId) {
66 return _currentUrls.get(userId) || null;
67}
68
69export function setCurrentUrl(userId, url) {
70 _currentUrls.set(userId, url);
71}
72
73// ─────────────────────────────────────────────────────────────────────────
74// REQUEST / RESPONSE
75// ─────────────────────────────────────────────────────────────────────────
76
77/**
78 * Send a request to the Chrome extension and wait for a response.
79 * Returns a Promise that resolves with the response data.
80 */
81export function sendRequest(userId, event, data = {}, timeoutMs = REQUEST_TIMEOUT_MS) {
82 const socket = _connections.get(userId);
83 if (!socket) {
84 return Promise.reject(new Error("No browser connected. Install the TreeOS Chrome extension and connect it."));
85 }
86
87 const requestId = uuidv4();
88 return new Promise((resolve, reject) => {
89 const timer = setTimeout(() => {
90 _pending.delete(requestId);
91 reject(new Error("Browser request timed out (15s). The page may be unresponsive."));
92 }, timeoutMs);
93
94 _pending.set(requestId, { resolve, reject, timer, userId });
95 socket.emit(event, { ...data, requestId });
96 });
97}
98
99/**
100 * Resolve a pending request when the Chrome extension responds.
101 */
102export function resolveRequest(requestId, data) {
103 const pending = _pending.get(requestId);
104 if (!pending) return false;
105 clearTimeout(pending.timer);
106 _pending.delete(requestId);
107 pending.resolve(data);
108 return true;
109}
110
111// ─────────────────────────────────────────────────────────────────────────
112// SITE SCOPING
113// ─────────────────────────────────────────────────────────────────────────
114
115/**
116 * Check if the AI can access a URL from this node position.
117 * Walks ancestor metadata for browserBridge.autoApprove/alwaysAsk/blocked.
118 */
119export async function checkSiteAccess(nodeId, url) {
120 if (!url) return { allowed: true, needsApproval: true, blocked: false, reason: "no URL" };
121
122 let domain;
123 try {
124 domain = new URL(url).hostname;
125 } catch {
126 return { allowed: false, blocked: true, reason: "invalid URL" };
127 }
128
129 // Walk ancestor chain for site scoping config
130 const config = await collectSiteConfig(nodeId);
131
132 // Check blocked first
133 if (matchesDomain(domain, config.blocked)) {
134 return { allowed: false, blocked: true, reason: `${domain} is blocked at this position` };
135 }
136
137 // Check auto-approve
138 if (matchesDomain(domain, config.autoApprove)) {
139 return { allowed: true, needsApproval: false, blocked: false, reason: "auto-approved" };
140 }
141
142 // Check always-ask
143 if (matchesDomain(domain, config.alwaysAsk)) {
144 return { allowed: true, needsApproval: true, blocked: false, reason: "requires approval" };
145 }
146
147 // Default: allowed but needs approval
148 return { allowed: true, needsApproval: true, blocked: false, reason: "default: requires approval" };
149}
150
151async function collectSiteConfig(nodeId) {
152 const config = { autoApprove: [], alwaysAsk: [], blocked: [] };
153 if (!_Node) return config;
154
155 let current = await _Node.findById(nodeId).select("parent metadata").lean();
156 let depth = 0;
157
158 while (current && depth < 20) {
159 const meta = current.metadata instanceof Map
160 ? current.metadata.get("browser-bridge")
161 : current.metadata?.["browser-bridge"];
162
163 if (meta) {
164 if (Array.isArray(meta.autoApprove)) config.autoApprove.push(...meta.autoApprove);
165 if (Array.isArray(meta.alwaysAsk)) config.alwaysAsk.push(...meta.alwaysAsk);
166 if (Array.isArray(meta.blocked)) config.blocked.push(...meta.blocked);
167 }
168
169 if (!current.parent) break;
170 current = await _Node.findById(current.parent).select("parent metadata").lean();
171 depth++;
172 }
173
174 return config;
175}
176
177function matchesDomain(domain, patterns) {
178 if (!patterns || patterns.length === 0) return false;
179 const lower = domain.toLowerCase();
180 for (const pattern of patterns) {
181 const p = pattern.toLowerCase();
182 if (p === lower) return true;
183 if (p.startsWith("*.") && lower.endsWith(p.slice(1))) return true;
184 // Match with or without www
185 if (lower === "www." + p || p === "www." + lower) return true;
186 }
187 return false;
188}
189
190// ─────────────────────────────────────────────────────────────────────────
191// ACTIVITY LOGGING
192// ─────────────────────────────────────────────────────────────────────────
193
194/**
195 * Log a browser action as a note on the node.
196 */
197export async function logAction(nodeId, userId, action, url, result) {
198 try {
199 const summary = `[browser] ${action.type || action}${url ? " on " + url : ""}`;
200 const detail = typeof result === "string" ? result : (result?.success ? "succeeded" : "failed");
201 await createNote({
202 contentType: "text",
203 content: `${summary}: ${detail}`,
204 userId: userId || "SYSTEM",
205 nodeId,
206 wasAi: true,
207 });
208 } catch (err) {
209 log.debug("BrowserBridge", `Activity log failed: ${err.message}`);
210 }
211}
2121/**
2 * Browser Bridge
3 *
4 * The AI sees and acts through the user's browser.
5 * Chrome extension connects via Socket.IO. Server-side tools bridge AI calls
6 * to browser actions. Confined scope. Site-scoped. Approve-gated. Logged.
7 */
8
9import log from "../../seed/log.js";
10import User from "../../seed/models/user.js";
11import {
12 configure,
13 registerConnection,
14 resolveRequest,
15 setCurrentUrl,
16 isConnected,
17 sendRequest,
18 checkSiteAccess,
19 getCurrentUrl,
20} from "./core.js";
21import getTools from "./tools.js";
22
23const WRITE_TOOLS = new Set([
24 "browser-click",
25 "browser-type",
26 "browser-comment",
27]);
28
29export async function init(core) {
30 configure({
31 metadata: core.metadata,
32 Node: core.models.Node,
33 });
34
35 // ── Socket.IO event handlers for Chrome extension ──────────────────
36
37 // Auth: Chrome extension sends API key after connecting
38 core.websocket.registerSocketHandler("browserAuth", async ({ socket, data }) => {
39 try {
40 const apiKey = data?.apiKey;
41 if (!apiKey && !(data.username && data.password)) {
42 socket.emit("browserAuthResult", { success: false, error: "API key or username + password required" });
43 return;
44 }
45
46 let userId = null;
47 const { username, password } = data;
48
49 // Method 1: API key (if api-keys extension data exists)
50 if (apiKey) {
51 try {
52 // User imported at top of file
53 const { getUserMeta } = await import("../../seed/tree/userMetadata.js");
54 const { default: bcryptMod } = await import("bcrypt");
55
56 const prefix = apiKey.slice(0, 8);
57 log.debug("BrowserBridge", `API key auth attempt, prefix: ${prefix}`);
58 const candidates = await User.find({
59 "metadata.apiKeys": {
60 $elemMatch: { keyPrefix: prefix, revoked: { $ne: true } },
61 },
62 }).select("_id username metadata");
63
64 log.debug("BrowserBridge", `Found ${candidates.length} candidates for prefix ${prefix}`);
65
66 for (const user of candidates) {
67 const keys = getUserMeta(user, "apiKeys");
68 if (!Array.isArray(keys)) continue;
69 for (const key of keys) {
70 if (key.revoked || key.keyPrefix !== prefix) continue;
71 const match = await bcryptMod.compare(apiKey, key.keyHash);
72 if (match) {
73 userId = String(user._id);
74 socket.username = user.username;
75 break;
76 }
77 }
78 if (userId) break;
79 }
80 } catch (err) {
81 log.warn("BrowserBridge", `API key auth error: ${err.message}`);
82 }
83 }
84
85 // Method 2: username + password (always available, no extension needed)
86 if (!userId && username && password) {
87 try {
88 // User imported at top of file
89 const { default: bcryptMod } = await import("bcrypt");
90 log.debug("BrowserBridge", `Password auth attempt for user: ${username}`);
91 const user = await User.findOne({ username }).select("_id username password");
92 if (user) {
93 const match = await bcryptMod.compare(password, user.password);
94 log.debug("BrowserBridge", `Password match for ${username}: ${match}`);
95 if (match) {
96 userId = String(user._id);
97 socket.username = user.username;
98 }
99 } else {
100 log.debug("BrowserBridge", `User not found: ${username}`);
101 }
102 } catch (err) {
103 log.warn("BrowserBridge", `Password auth error: ${err.message}`);
104 }
105 }
106
107 if (!userId) {
108 socket.emit("browserAuthResult", { success: false, error: "Invalid API key" });
109 return;
110 }
111
112 // Set userId on socket (doesn't affect authSessions since it was null on connect)
113 socket.userId = userId;
114 socket._browserBridge = true;
115
116 registerConnection(userId, socket);
117 socket.emit("browserAuthResult", { success: true });
118 } catch (err) {
119 socket.emit("browserAuthResult", { success: false, error: err.message });
120 }
121 });
122
123 // Response handlers: resolve pending requests from AI tool calls
124 const responseEvents = ["pageState", "actionResult", "screenshot", "networkLog", "tabsList"];
125 for (const event of responseEvents) {
126 core.websocket.registerSocketHandler(event, ({ data }) => {
127 if (data?.requestId) {
128 resolveRequest(data.requestId, data.data || data);
129 }
130 });
131 }
132
133 // Unprompted: user navigated to a new page
134 core.websocket.registerSocketHandler("pageNavigated", ({ socket, data }) => {
135 const userId = socket.userId;
136 if (!userId || !socket._browserBridge) return;
137 if (data?.url) {
138 setCurrentUrl(userId, data.url);
139 log.verbose("BrowserBridge", `${userId} navigated to ${data.url}`);
140 }
141 });
142
143 // ── beforeToolCall: site scoping guard ─────────────────────────────
144
145 core.hooks.register("beforeToolCall", async (hookData) => {
146 const { toolName, args, userId } = hookData;
147 if (!WRITE_TOOLS.has(toolName)) return;
148
149 const nodeId = args?.nodeId;
150 if (!nodeId) return;
151
152 const url = toolName === "browser-navigate" ? args?.url : getCurrentUrl(userId);
153 if (!url) return;
154
155 const access = await checkSiteAccess(nodeId, url);
156 if (access.blocked) {
157 hookData.cancelled = true;
158 hookData.reason = `Browser action blocked: ${access.reason}`;
159 }
160 }, "browser-bridge");
161
162 // ── afterScopeChange: auto-set browser-agent mode when allowed ──────
163 core.hooks.register("afterScopeChange", async ({ nodeId, allowed }) => {
164 if (!allowed || !Array.isArray(allowed)) return;
165 if (!allowed.includes("browser-bridge")) return;
166 try {
167 const { setExtMeta } = await import("../../seed/tree/extensionMetadata.js");
168 const node = await core.models.Node.findById(nodeId);
169 if (!node) return;
170 const modes = node.metadata instanceof Map ? node.metadata.get("modes") : node.metadata?.modes;
171 // Only set if no respond mode already set
172 if (!modes?.respond) {
173 await setExtMeta(node, "modes", { ...(modes || {}), respond: "tree:browser-agent" });
174 log.info("BrowserBridge", `Auto-set browser-agent mode on ${nodeId}`);
175 }
176 } catch {}
177 }, "browser-bridge");
178
179 // ── enrichContext: tell AI browser is available ─────────────────────
180
181 core.hooks.register("enrichContext", async ({ context, node, meta }) => {
182 const bbMeta = meta?.["browser-bridge"];
183 // Only inject if the extension is active at this position
184 // (spatial scoping already filters hooks, so if we're here, it's allowed)
185 context.browserBridge = {
186 available: true,
187 note: "BROWSER BRIDGE IS ACTIVE. You control the user's REAL browser. " +
188 "To interact with websites use BROWSER tools, NOT tree tools. " +
189 "browser-read = see the actual webpage content and elements. " +
190 "browser-click = click a real element on the website. " +
191 "browser-type = type into a real input on the website. " +
192 "browser-navigate = go to a real URL. " +
193 "browser-comment = post a comment/reply on social sites (handles the full flow automatically). " +
194 "Tree tools (create-node, create-note) only affect the tree, NOT websites. " +
195 "To post a comment or reply, use browser-comment with the text. " +
196 "Always call browser-read first to understand the page.",
197 };
198 if (bbMeta?.autoApprove?.length) {
199 context.browserBridge.autoApprovedSites = bbMeta.autoApprove;
200 }
201 if (bbMeta?.blocked?.length) {
202 context.browserBridge.blockedSites = bbMeta.blocked;
203 }
204 }, "browser-bridge");
205
206 // Register browser agent mode
207 const { default: agentMode } = await import("./modes/agent.js");
208 core.modes.registerMode("tree:browser-agent", agentMode, "browser-bridge");
209
210 log.info("BrowserBridge", "Loaded. Confined. The AI can see and act through the browser.");
211
212 return {
213 tools: getTools(),
214 modeTools: [
215 { modeKey: "tree:browser-agent", toolNames: ["browser-read", "browser-click", "browser-type", "browser-navigate", "browser-comment"] },
216 { modeKey: "tree:respond", toolNames: ["browser-read", "browser-click", "browser-type", "browser-navigate", "browser-comment"] },
217 { modeKey: "tree:librarian", toolNames: ["browser-read", "browser-click", "browser-type", "browser-navigate", "browser-comment"] },
218 ],
219 exports: {
220 isConnected,
221 sendRequest,
222 checkSiteAccess,
223 getCurrentUrl,
224 },
225 };
226}
2271export default {
2 name: "browser-bridge",
3 version: "1.0.3",
4 scope: "confined",
5 builtFor: "treeos-connect",
6 description:
7 "The AI sees and acts through your browser. A Chrome extension connects via Socket.IO. " +
8 "The AI gets accessibility trees, clicks elements, types text, navigates pages, takes screenshots. " +
9 "Confined scope: inactive everywhere until explicitly allowed per branch. " +
10 "Write actions require operator approval unless auto-approved by site scoping. " +
11 "Read actions (page state, extract, screenshot) are always allowed. " +
12 "Every browser action is logged as a note. The most powerful and most dangerous extension " +
13 "in the ecosystem. All safety layers active by default.",
14
15 territory: "reading and interacting with web pages through the browser",
16 classifierHints: [
17 /\b(click|type|navigate|browse|open|visit|go to|read.*page|what.*page|this site|this page|webpage|website)\b/i,
18 /\b(post|comment|reply|submit|login|sign in|search.*web|fill.*form|enter.*field)\b/i,
19 /\b(browser|tab|screen|what do you see|what's on)\b/i,
20 ],
21
22 guidedMode: "tree:browser-agent",
23
24 needs: {
25 services: ["hooks", "websocket", "metadata", "modes"],
26 models: ["Node"],
27 },
28
29 optional: {
30 extensions: ["approve", "kb", "study", "api-keys"],
31 },
32
33 provides: {
34 models: {},
35 routes: false,
36 tools: true,
37 jobs: false,
38 orchestrator: false,
39 energyActions: {},
40 sessionTypes: {},
41 env: [],
42 cli: [],
43
44 hooks: {
45 fires: [],
46 listens: ["beforeToolCall", "enrichContext"],
47 },
48 },
49};
501export default {
2 name: "tree:browser-agent",
3 emoji: "🌐",
4 label: "Browser Agent",
5 bigMode: "tree",
6 hidden: true,
7
8 toolNames: [
9 "browser-read",
10 "browser-click",
11 "browser-type",
12 "browser-navigate",
13 "browser-comment",
14 "browser-fetch",
15 ],
16
17 maxMessagesBeforeLoop: 20,
18 preserveContextOnLoop: true,
19
20 buildSystemPrompt({ username }) {
21 return `You are a browser agent for ${username}. You control their real Chrome browser.
22
23YOUR TOOLS:
24- browser-read: See the current page. Returns URL, title, text content, and interactive elements with IDs (e1, e2, etc.)
25- browser-click: Click an element by its ID from browser-read
26- browser-type: Type text into an input field by its ID from browser-read
27- browser-navigate: Go to a URL
28
29WORKFLOW:
301. ALWAYS call browser-read first to see what's on the page
312. Find the element ID you need from the results
323. Use browser-click or browser-type with that ID
334. Call browser-read again to verify the result
34
35RULES:
36- You act, you don't just describe. When the user says "click X", you click it.
37- When the user says "reply with hi", use browser-comment with the text "hi". It handles the full flow.
38- Never say you can't interact with websites. You can. Use your tools.
39- If an element ID doesn't work, call browser-read again to get fresh IDs.
40- Report what you did briefly after acting.`.trim();
41 },
42};
431/**
2 * Browser Bridge Tools
3 *
4 * 7 MCP tools the AI can call to interact with the user's browser.
5 * Read tools: always allowed. Write tools: site-scoped, approve-gated.
6 */
7
8import { z } from "zod";
9import { sendRequest, isConnected, getCurrentUrl, checkSiteAccess, logAction } from "./core.js";
10
11function text(str) {
12 return { content: [{ type: "text", text: String(str) }] };
13}
14
15function json(data) {
16 const str = typeof data === "string" ? data : JSON.stringify(data, null, 2);
17 return text(truncate(str));
18}
19
20// Cap tool results to prevent token overload
21const MAX_RESULT_CHARS = 6000;
22
23function truncate(str) {
24 if (typeof str !== "string") str = JSON.stringify(str, null, 2);
25 if (str.length > MAX_RESULT_CHARS) {
26 return str.slice(0, MAX_RESULT_CHARS) + "\n\n[truncated, " + str.length + " total chars. Page is large. Use browser-extract for text content or ask about specific elements.]";
27 }
28 return str;
29}
30
31function requireBrowser(userId) {
32 if (!isConnected(userId)) {
33 throw new Error("No browser connected. The user needs to install the TreeOS Chrome extension and connect it.");
34 }
35}
36
37export default function getTools() {
38 return [
39 // ── READ TOOLS ──────────────────────────────────────────────────
40
41 {
42 name: "browser-read",
43 description:
44 "Read the current page in the user's browser. Returns the page URL, title, text content, " +
45 "and interactive elements with IDs (e1, e2, etc.) for clicking and typing. " +
46 "Call this first to understand what's on the page before taking any action.",
47 annotations: { readOnlyHint: true },
48 schema: {
49 userId: z.string().describe("Injected by server. Ignore."),
50 },
51 handler: async ({ userId }) => {
52 requireBrowser(userId);
53
54 // Get page state (accessibility tree + metadata)
55 const stateResult = await sendRequest(userId, "getPageState", {});
56 const state = stateResult?.data || stateResult;
57
58 // Get text content
59 let pageText = "";
60 try {
61 const extractResult = await sendRequest(userId, "executeAction", { action: { type: "extract" } });
62 const content = extractResult?.data?.text || extractResult?.text || extractResult?.data || extractResult;
63 pageText = typeof content === "string" ? content : JSON.stringify(content);
64 } catch {}
65
66 // Extract only useful interactive elements
67 // Skip: unnamed spans, unnamed links, sidebar/footer noise
68 const SKIP_ROLES = new Set(["presentation", "img", "complementary", "contentinfo", "navigation"]);
69 const interactiveElements = [];
70 function walkTree(nodes) {
71 if (!nodes) return;
72 const list = Array.isArray(nodes) ? nodes : [nodes];
73 for (const node of list) {
74 if (node.id && !SKIP_ROLES.has(node.role)) {
75 // Only include elements with names, or key roles (button, textbox, link with name)
76 const hasName = node.name && node.name.trim().length > 0;
77 const isAction = node.role === "button" || node.role === "textbox" || node.role === "searchbox" || node.role === "combobox";
78 if (hasName || isAction) {
79 interactiveElements.push({
80 id: node.id,
81 role: node.role,
82 name: node.name ? node.name.slice(0, 60) : undefined,
83 });
84 }
85 }
86 if (node.children) walkTree(node.children);
87 }
88 }
89 walkTree(state?.tree);
90
91 // Cap at 40 elements max
92 const elements = interactiveElements.slice(0, 40);
93
94 // Put interactive elements FIRST (AI needs these to act), text SECOND (gets truncated first)
95 const combined = {
96 url: state?.url || "unknown",
97 title: state?.title || "unknown",
98 elements,
99 pageText: pageText.slice(0, 2000),
100 };
101
102 return json(combined);
103 },
104 },
105
106
107 // ── WRITE TOOLS ─────────────────────────────────────────────────
108
109 {
110 name: "browser-click",
111 description:
112 "Click an element in the user's browser. Use the element ID from browser-get-state (e.g. 'e5'). " +
113 "Always call browser-get-state first to see available elements and their IDs.",
114 annotations: { readOnlyHint: false, destructiveHint: true },
115 schema: {
116 elementId: z.string().describe("Element ID from the accessibility tree (e.g. 'e5')"),
117 userId: z.string().describe("Injected by server. Ignore."),
118 },
119 handler: async ({ elementId, nodeId, userId }) => {
120 requireBrowser(userId);
121 const result = await sendRequest(userId, "executeAction", {
122 action: { type: "click", elementId },
123 });
124 logAction(nodeId, userId, { type: "click", elementId }, getCurrentUrl(userId), result).catch(() => {});
125 return json(result);
126 },
127 },
128
129 {
130 name: "browser-type",
131 description:
132 "Type text into an input field in the user's browser. Use the element ID from browser-get-state. " +
133 "The element must be a text input, textarea, or contenteditable element.",
134 annotations: { readOnlyHint: false, destructiveHint: true },
135 schema: {
136 elementId: z.string().describe("Element ID of the input field (e.g. 'e12')"),
137 text: z.string().describe("Text to type into the field"),
138 userId: z.string().describe("Injected by server. Ignore."),
139 },
140 handler: async ({ elementId, text, nodeId, userId }) => {
141 requireBrowser(userId);
142 const result = await sendRequest(userId, "executeAction", {
143 action: { type: "type", elementId, text },
144 });
145 logAction(nodeId, userId, { type: "type", elementId }, getCurrentUrl(userId), result).catch(() => {});
146 return json(result);
147 },
148 },
149
150 {
151 name: "browser-navigate",
152 description:
153 "Navigate the user's browser to a URL. Use for opening documentation, websites, or web apps. " +
154 "The URL must be allowed by the site scoping configuration at this tree position.",
155 annotations: { readOnlyHint: false, destructiveHint: true },
156 schema: {
157 url: z.string().describe("The URL to navigate to"),
158 userId: z.string().describe("Injected by server. Ignore."),
159 },
160 handler: async ({ url, nodeId, userId }) => {
161 requireBrowser(userId);
162 // Check site access against the TARGET url specifically
163 const access = await checkSiteAccess(nodeId, url);
164 if (access.blocked) {
165 return text(`Blocked: ${access.reason}. This site is not allowed at this tree position.`);
166 }
167 const result = await sendRequest(userId, "executeAction", {
168 action: { type: "navigate", url },
169 });
170 logAction(nodeId, userId, { type: "navigate", url }, url, result).catch(() => {});
171 return json(result);
172 },
173 },
174
175 {
176 name: "browser-comment",
177 description:
178 "Post a comment or reply on the current page. Works on Reddit, forums, and social sites. " +
179 "Automatically finds the reply button, clicks it, types your text, and submits. " +
180 "One tool call does the full flow. No need to manually find buttons.",
181 annotations: { readOnlyHint: false, destructiveHint: true },
182 schema: {
183 commentText: z.string().describe("The text to post as a comment or reply"),
184 replyTo: z.string().optional().describe("Username to reply to (e.g. 'u/someone'). If omitted, posts to the main post."),
185 userId: z.string().describe("Injected by server. Ignore."),
186 },
187 handler: async ({ commentText, replyTo, userId }) => {
188 requireBrowser(userId);
189 const result = await sendRequest(userId, "executeAction", {
190 action: { type: "comment", text: commentText, replyTo: replyTo || null },
191 });
192 const res = result?.data || result;
193 if (res?.success && res?.submitted) {
194 return text(`Comment posted: "${commentText}"`);
195 } else if (res?.typed && !res?.submitted) {
196 return text(`Typed "${commentText}" but could not find submit button. Click it manually.`);
197 }
198 return text(`Failed to post comment: ${res?.error || "unknown error"}`);
199 },
200 },
201
202 {
203 name: "browser-fetch",
204 description:
205 "Fetch a URL and return the raw response. Works with JSON APIs, RSS feeds, or any URL. " +
206 "Server-side fetch, no browser needed. Use this for structured data instead of browser-read.",
207 annotations: { readOnlyHint: true },
208 schema: {
209 url: z.string().describe("URL to fetch"),
210 userId: z.string().describe("Injected by server. Ignore."),
211 },
212 handler: async ({ url, userId }) => {
213 if (!url) return text("URL required.");
214
215 try {
216 const controller = new AbortController();
217 const timeout = setTimeout(() => controller.abort(), 15000);
218
219 const response = await fetch(url, {
220 signal: controller.signal,
221 headers: {
222 "User-Agent": "TreeOS/1.0",
223 "Accept": "application/json, text/html, */*",
224 },
225 });
226 clearTimeout(timeout);
227
228 const contentType = response.headers.get("content-type") || "";
229 const body = await response.text();
230
231 // Cap at 16KB
232 const capped = body.length > 16384
233 ? body.slice(0, 16384) + "\n... (truncated)"
234 : body;
235
236 return text(`[${response.status}] ${contentType}\n\n${capped}`);
237 } catch (err) {
238 return text(`Fetch failed: ${err.message}`);
239 }
240 },
241 },
242
243 ];
244}
2451# TreeOS Browser Bridge
2
3A Chrome extension that gives your TreeOS AI eyes and hands in the browser. The AI can read pages, click elements, type text, navigate URLs, and post comments. All through the same chat interface.
4
5## Setup
6
7### 1. Install the Chrome Extension
8
91. Open `chrome://extensions` in Chrome
102. Enable **Developer mode** (top right toggle)
113. Click **Load unpacked**
124. Select this folder (`treeos-browser-bridge/`)
135. The TreeOS Bridge icon appears in your toolbar
14
15### 2. Connect to Your Land
16
171. Click the TreeOS Bridge icon in Chrome
182. Enter your **Server URL** (e.g. `http://localhost:3000` or `https://your-land.com`)
193. Enter your **Username** and **Password** (or API key if you have one)
204. Click **Connect**
215. The status dot turns green when connected
22
23### 3. Enable Browser Bridge on a Tree Branch
24
25Browser bridge is **confined**. It's inactive everywhere by default. You must explicitly allow it at the tree positions where you want the AI to use the browser.
26
27**CLI:**
28```
29treeos cd MyTree
30treeos ext-allow browser-bridge
31```
32
33**Or at a specific branch:**
34```
35treeos cd MyTree/Web
36treeos ext-allow browser-bridge
37```
38
39The AI can only use browser tools at positions where you've allowed it. Everywhere else, the browser tools don't exist.
40
41### 4. Set the Browser Agent Mode (Optional)
42
43For best results, set the browser-agent mode on the node where you allowed browser-bridge:
44
45```
46treeos mode-set respond tree:browser-agent
47```
48
49This makes the AI act on browser requests instead of just describing them.
50
51## Usage
52
53Once connected and allowed, chat at that branch:
54
55```
56what's on this page
57```
58The AI reads the current page via the accessibility tree.
59
60```
61click the login button
62```
63The AI finds the element and clicks it.
64
65```
66reply to this post saying "interesting, thanks for sharing"
67```
68The AI uses the comment tool to find the reply button, type text, and submit.
69
70```
71navigate to docs.react.dev
72```
73The AI opens the URL in your browser.
74
75## Tools Available to the AI
76
77| Tool | Type | Description |
78|------|------|-------------|
79| `browser-read` | Read | See page URL, title, text content, and interactive elements |
80| `browser-click` | Write | Click an element by ID from browser-read |
81| `browser-type` | Write | Type text into an input field |
82| `browser-navigate` | Write | Go to a URL |
83| `browser-comment` | Write | Post a comment/reply (handles full flow automatically) |
84
85## Site Scoping (Optional)
86
87Control which websites the AI can interact with per tree position. Set in node metadata:
88
89```json
90{
91 "browserBridge": {
92 "autoApprove": ["docs.react.dev", "developer.mozilla.org"],
93 "alwaysAsk": ["*.bank.com"],
94 "blocked": ["facebook.com"]
95 }
96}
97```
98
99- **autoApprove**: Write actions skip approval on these domains
100- **alwaysAsk**: Always require approval (even if confirm is off)
101- **blocked**: AI cannot see or act on these domains
102
103## Safety Layers
104
105All active by default. Cannot be turned off.
106
1071. **Confined scope**: Off everywhere until `ext-allow`
1082. **Site scoping**: Per-node domain allow/block lists
1093. **Confirm actions**: Chrome extension toggle. When on, every write action shows a notification asking permission
1104. **Read-only in query mode**: `query` mode strips write tools automatically
1115. **Activity logging**: Every browser action is logged as a note on the node
112
113## Chrome Extension UI
114
115- **Popup**: Settings, connect/disconnect, status
116- **Side Panel**: Activity log (real-time), Page Tree viewer (accessibility tree visualization)
117- **Notifications**: Action confirmations when "Confirm actions" is enabled
118
119## Troubleshooting
120
121**"No browser connected"**: The Chrome extension isn't connected. Click the icon and check the status dot. Make sure server URL is correct.
122
123**AI says it can't interact with websites**: Make sure you ran `ext-allow browser-bridge` at the current node AND set `mode-set respond tree:browser-agent`.
124
125**Actions succeed but nothing happens visually**: The AI clicks via JavaScript DOM events, not mouse simulation. The page reacts but you won't see a cursor move.
126
127**Page Tree tab is blank**: Refresh the web page you're viewing, then click Page Tree again. The content script only injects on page load.
1281// TreeOS Browser Bridge — Background Service Worker
2// Manages Socket.IO connection to TreeOS and bridges content script <-> server
3
4try {
5 importScripts('lib/socket.io.min.js');
6} catch (e) {
7 console.error('[TreeOS Bridge] Failed to load Socket.IO:', e);
8}
9
10let socket = null;
11let config = { serverUrl: '', apiKey: '', username: '', password: '', autoCapture: false, confirmActions: true };
12let connectionState = 'disconnected'; // disconnected | connecting | connected
13let reconnectTimer = null;
14let pendingConfirmations = new Map();
15
16// ── Config ────────────────────────────────────────────────────────
17
18async function loadConfig() {
19 const stored = await chrome.storage.local.get(['treeos_config']);
20 if (stored.treeos_config) {
21 config = { ...config, ...stored.treeos_config };
22 }
23 return config;
24}
25
26async function saveConfig(newConfig) {
27 config = { ...config, ...newConfig };
28 await chrome.storage.local.set({ treeos_config: config });
29 broadcastState();
30 return config;
31}
32
33// ── Socket.IO Connection ─────────────────────────────────────────
34
35function connect() {
36 if (!config.serverUrl) {
37 updateState('disconnected');
38 return;
39 }
40
41 if (socket?.connected) return;
42
43 updateState('connecting');
44
45 try {
46 // Ensure full URL with protocol for service worker context
47 let serverUrl = config.serverUrl;
48 if (!serverUrl.startsWith('http://') && !serverUrl.startsWith('https://')) {
49 serverUrl = 'http://' + serverUrl;
50 }
51
52 socket = io(serverUrl, {
53 path: '/socket.io',
54 transports: ['websocket'],
55 reconnection: true,
56 reconnectionDelay: 5000,
57 reconnectionAttempts: Infinity,
58 forceNew: true,
59 });
60
61 socket.on('connect', () => {
62 updateState('connected');
63 clearReconnectTimer();
64
65 // Authenticate with API key or username/password
66 socket.emit('browserAuth', {
67 apiKey: config.apiKey || null,
68 username: config.username || null,
69 password: config.password || null,
70 capabilities: ['page_state', 'execute_action', 'screenshot', 'network_log'],
71 });
72 });
73
74 socket.on('browserAuthResult', (data) => {
75 if (data.success) {
76 console.log('[TreeOS Bridge] Authenticated');
77 } else {
78 console.error('[TreeOS Bridge] Auth failed:', data.error);
79 disconnect();
80 }
81 });
82
83 // ── Server requests ──────────────────────────────────────────
84
85 socket.on('getPageState', async (msg) => {
86 const state = await getPageStateFromTab(msg.tabId);
87 socket.emit('pageState', { requestId: msg.requestId, data: state });
88 broadcastActivity('getPageState', { url: state?.url || 'current page' });
89 });
90
91 socket.on('executeAction', async (msg) => {
92 // If confirmation required, hold and ask user
93 if (config.confirmActions && !msg.confirmed) {
94 const confirmId = crypto.randomUUID();
95 pendingConfirmations.set(confirmId, msg);
96
97 chrome.runtime.sendMessage({
98 type: 'confirmAction',
99 confirmId,
100 action: msg.action,
101 description: describeAction(msg.action),
102 }).catch(() => {});
103
104 chrome.notifications?.create(confirmId, {
105 type: 'basic',
106 iconUrl: 'icons/icon128.png',
107 title: 'TreeOS Agent Action',
108 message: describeAction(msg.action),
109 buttons: [{ title: 'Allow' }, { title: 'Deny' }],
110 requireInteraction: true,
111 }).catch(() => {});
112
113 return;
114 }
115
116 const result = await executeActionInTab(msg.action, msg.tabId);
117 socket.emit('actionResult', { requestId: msg.requestId, data: result });
118 broadcastActivity('action', { type: msg.action.type, target: msg.action.elementId || msg.action.url || '', success: result.success });
119
120 // Auto-capture new state after action
121 if (result.success && config.autoCapture) {
122 await new Promise(r => setTimeout(r, 500));
123 const newState = await getPageStateFromTab(msg.tabId);
124 socket.emit('pageState', { requestId: msg.requestId + '_post', data: newState });
125 }
126 });
127
128 socket.on('screenshot', async (msg) => {
129 const dataUrl = await captureScreenshot(msg.tabId);
130 socket.emit('screenshot', { requestId: msg.requestId, data: dataUrl });
131 broadcastActivity('screenshot', {});
132 });
133
134 socket.on('getNetworkLog', async (msg) => {
135 const log = await getNetworkLogFromTab(msg.tabId);
136 socket.emit('networkLog', { requestId: msg.requestId, data: log });
137 });
138
139 socket.on('getTabs', async (msg) => {
140 const tabs = await chrome.tabs.query({ currentWindow: true });
141 const tabData = tabs.map(t => ({
142 id: t.id, url: t.url, title: t.title, active: t.active,
143 }));
144 socket.emit('tabsList', { requestId: msg.requestId, data: tabData });
145 });
146
147 socket.on('activateTab', async (msg) => {
148 await chrome.tabs.update(msg.tabId, { active: true });
149 socket.emit('tabActivated', { requestId: msg.requestId, tabId: msg.tabId });
150 });
151
152 socket.on('newTab', async (msg) => {
153 const tab = await chrome.tabs.create({ url: msg.url || 'about:blank' });
154 socket.emit('tabCreated', { requestId: msg.requestId, tabId: tab.id });
155 });
156
157 socket.on('ping', (msg) => {
158 socket.emit('pong', { requestId: msg.requestId });
159 });
160
161 // ── Connection lifecycle ─────────────────────────────────────
162
163 socket.on('disconnect', (reason) => {
164 updateState('disconnected');
165 if (reason !== 'io client disconnect') {
166 // Auto-reconnect handled by Socket.IO
167 }
168 });
169
170 socket.on('connect_error', (err) => {
171 console.error('[TreeOS Bridge] Connection error:', err.message);
172 updateState('disconnected');
173 });
174
175 socket.on('browserDisconnected', (data) => {
176 console.log('[TreeOS Bridge] Disconnected by server:', data?.reason);
177 });
178
179 } catch (err) {
180 console.error('[TreeOS Bridge] Connection failed:', err);
181 updateState('disconnected');
182 scheduleReconnect();
183 }
184}
185
186function disconnect() {
187 clearReconnectTimer();
188 if (socket) {
189 socket.disconnect();
190 socket = null;
191 }
192 updateState('disconnected');
193}
194
195function scheduleReconnect() {
196 clearReconnectTimer();
197 reconnectTimer = setTimeout(() => connect(), 5000);
198}
199
200function clearReconnectTimer() {
201 if (reconnectTimer) {
202 clearTimeout(reconnectTimer);
203 reconnectTimer = null;
204 }
205}
206
207// ── Content Script Communication ──────────────────────────────────
208
209async function getActiveTabId(preferredTabId) {
210 if (preferredTabId) return preferredTabId;
211 const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
212 return tab?.id;
213}
214
215/**
216 * Ensure content script is injected in the tab.
217 * SPAs (x.com, reddit) destroy the content script context on navigation.
218 * Re-inject before every message to handle this.
219 */
220async function ensureContentScript(tabId) {
221 try {
222 await chrome.tabs.sendMessage(tabId, { type: 'ping' });
223 } catch {
224 // Content script not responding. Re-inject.
225 try {
226 await chrome.scripting.executeScript({
227 target: { tabId },
228 files: ['scripts/content.js'],
229 });
230 // Give it a moment to initialize
231 await new Promise(r => setTimeout(r, 200));
232 } catch (err) {
233 console.warn('[TreeOS Bridge] Cannot inject content script:', err.message);
234 }
235 }
236}
237
238async function getPageStateFromTab(tabId) {
239 const id = await getActiveTabId(tabId);
240 if (!id) return { error: 'No active tab' };
241
242 await ensureContentScript(id);
243
244 try {
245 const response = await chrome.tabs.sendMessage(id, {
246 type: 'getPageState',
247 includeNetwork: true,
248 });
249 return response;
250 } catch (err) {
251 // Content script failed (hostile site like x.com). Fall back to
252 // chrome.scripting.executeScript which runs in the isolated world.
253 try {
254 const [result] = await chrome.scripting.executeScript({
255 target: { tabId: id },
256 world: 'ISOLATED',
257 func: () => {
258 // Minimal page state capture without full tree builder
259 const getText = (el, max) => (el?.textContent || '').trim().slice(0, max);
260 const links = [...document.querySelectorAll('a[href]')].slice(0, 50).map((a, i) => ({
261 role: 'link', name: getText(a, 100), id: 'e' + (i + 1), href: a.href,
262 }));
263 const buttons = [...document.querySelectorAll('button')].slice(0, 30).map((b, i) => ({
264 role: 'button', name: getText(b, 100), id: 'b' + (i + 1),
265 }));
266 const inputs = [...document.querySelectorAll('input, textarea, [contenteditable="true"]')].slice(0, 20).map((el, i) => ({
267 role: el.tagName === 'TEXTAREA' ? 'textbox' : (el.type || 'input'),
268 name: el.placeholder || el.name || '',
269 id: 'i' + (i + 1),
270 }));
271 const bodyText = document.body?.innerText?.slice(0, 8000) || '';
272 return {
273 url: location.href,
274 title: document.title,
275 tree: [...links, ...buttons, ...inputs],
276 text: bodyText,
277 viewport: {
278 width: window.innerWidth,
279 height: window.innerHeight,
280 scrollY: window.scrollY,
281 scrollHeight: document.documentElement.scrollHeight,
282 },
283 timestamp: Date.now(),
284 fallback: true,
285 };
286 },
287 });
288 return result?.result || { error: 'Fallback capture failed' };
289 } catch (err2) {
290 return { error: `Page state failed: ${err2.message}` };
291 }
292 }
293}
294
295async function executeActionInTab(action, tabId) {
296 const id = await getActiveTabId(tabId);
297 if (!id) return { success: false, error: 'No active tab' };
298
299 // Navigate uses Chrome tab API directly. Bypasses hostile SPA routers (x.com, etc.)
300 // that intercept window.location changes inside the content script.
301 if (action.type === 'navigate' && action.url) {
302 try {
303 await chrome.tabs.update(id, { url: action.url });
304 // Wait for page to load
305 await new Promise(resolve => {
306 const listener = (tabId2, changeInfo) => {
307 if (tabId2 === id && changeInfo.status === 'complete') {
308 chrome.tabs.onUpdated.removeListener(listener);
309 resolve();
310 }
311 };
312 chrome.tabs.onUpdated.addListener(listener);
313 // Timeout after 10s
314 setTimeout(() => {
315 chrome.tabs.onUpdated.removeListener(listener);
316 resolve();
317 }, 10000);
318 });
319 return { success: true, action: 'navigated', url: action.url };
320 } catch (err) {
321 return { success: false, error: `Navigate failed: ${err.message}` };
322 }
323 }
324
325 await ensureContentScript(id);
326
327 try {
328 const response = await chrome.tabs.sendMessage(id, {
329 type: 'executeAction',
330 action,
331 recapture: true,
332 });
333 // If content script returned failure, try the fallback path
334 if (response && response.success === false) throw new Error(response.error || 'action failed');
335 return response;
336 } catch (err) {
337 // Content script failed or returned error. Fall back to chrome.scripting for hostile sites.
338 try {
339 const [result] = await chrome.scripting.executeScript({
340 target: { tabId: id },
341 world: 'ISOLATED',
342 args: [action],
343 func: async (action) => {
344 function findEl(id) {
345 if (!id) return null;
346 // Try by our assigned IDs from the fallback page state
347 const prefix = id.charAt(0);
348 const idx = parseInt(id.slice(1)) - 1;
349 if (prefix === 'e') return document.querySelectorAll('a[href]')[idx];
350 if (prefix === 'b') return document.querySelectorAll('button')[idx];
351 if (prefix === 'i') return document.querySelectorAll('input, textarea, [contenteditable="true"]')[idx];
352 return null;
353 }
354
355 switch (action.type) {
356 case 'click': {
357 const el = findEl(action.elementId);
358 if (!el) return { success: false, error: 'Element not found: ' + action.elementId };
359 el.scrollIntoView({ behavior: 'instant', block: 'center' });
360 el.focus();
361 el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
362 return { success: true, action: 'clicked', elementId: action.elementId };
363 }
364
365 case 'type': {
366 // Find contenteditable or textarea
367 let el = findEl(action.elementId);
368 if (!el) {
369 // Try to find any visible contenteditable or textarea
370 const all = document.querySelectorAll('[contenteditable="true"], textarea, [role="textbox"]');
371 for (const candidate of all) {
372 const r = candidate.getBoundingClientRect();
373 if (r.height > 20 && r.width > 50) { el = candidate; break; }
374 }
375 }
376 if (!el) return { success: false, error: 'No text input found' };
377
378 el.scrollIntoView({ behavior: 'instant', block: 'center' });
379 el.focus();
380
381 // Clear and type
382 if (el.contentEditable === 'true' || el.getAttribute('role') === 'textbox') {
383 el.textContent = '';
384 // Use DataTransfer for paste simulation (bypasses React interception)
385 const dt = new DataTransfer();
386 dt.setData('text/plain', action.text);
387 const pasteEvent = new ClipboardEvent('paste', {
388 bubbles: true, cancelable: true, clipboardData: dt,
389 });
390 el.dispatchEvent(pasteEvent);
391 // Fallback: direct insert
392 if (!el.textContent.includes(action.text)) {
393 el.textContent = action.text;
394 el.dispatchEvent(new Event('input', { bubbles: true }));
395 }
396 } else {
397 el.value = action.text;
398 el.dispatchEvent(new Event('input', { bubbles: true }));
399 el.dispatchEvent(new Event('change', { bubbles: true }));
400 }
401 return { success: true, action: 'typed', text: action.text };
402 }
403
404 case 'comment': {
405 const text = action.text;
406 if (!text) return { success: false, error: 'text required' };
407
408 // Step 1: Click a reply button to open the compose box
409 // Reddit, forums, etc. hide the textbox until you click reply
410 let textbox = null;
411
412 // Look for existing open compose box first
413 let all = document.querySelectorAll('[contenteditable="true"], textarea, [role="textbox"]');
414 for (const el of all) {
415 const r = el.getBoundingClientRect();
416 if (r.height > 20 && r.width > 50) { textbox = el; break; }
417 }
418
419 // No open box? Click a reply button
420 if (!textbox) {
421 // New Reddit: shreddit-comment-action-row buttons
422 const actionRow = document.querySelector('shreddit-comment-action-row');
423 if (actionRow) {
424 const btns = actionRow.querySelectorAll('button');
425 if (btns.length >= 1) btns[0].click();
426 }
427
428 // Old Reddit / generic: find a "reply" link or button
429 if (!actionRow) {
430 const replyLinks = document.querySelectorAll('a, button');
431 for (const el of replyLinks) {
432 const t = (el.textContent || '').trim().toLowerCase();
433 if (t === 'reply' || t === 'comment') {
434 el.click();
435 break;
436 }
437 }
438 }
439
440 // Wait for compose box to appear
441 await new Promise(r => setTimeout(r, 1500));
442
443 // Try again
444 all = document.querySelectorAll('[contenteditable="true"], textarea, [role="textbox"]');
445 for (const el of all) {
446 const r = el.getBoundingClientRect();
447 if (r.height > 20 && r.width > 50) { textbox = el; break; }
448 }
449 // Last resort: any visible input
450 if (!textbox && all.length) textbox = all[all.length - 1];
451 }
452
453 if (!textbox) return { success: false, error: 'No compose box found after clicking reply' };
454
455 textbox.focus();
456 await new Promise(r => setTimeout(r, 300));
457
458 // Method 1: execCommand insertText (works on many sites)
459 let typed = false;
460 try {
461 document.execCommand('selectAll', false, null);
462 typed = document.execCommand('insertText', false, text);
463 } catch {}
464
465 // Method 2: Paste simulation via ClipboardEvent
466 if (!typed || !textbox.textContent?.includes(text)) {
467 const dt = new DataTransfer();
468 dt.setData('text/plain', text);
469 textbox.dispatchEvent(new ClipboardEvent('paste', {
470 bubbles: true, cancelable: true, clipboardData: dt,
471 }));
472 }
473
474 // Method 3: Direct content set + input event
475 if (!textbox.textContent?.includes(text)) {
476 if (textbox.contentEditable === 'true') {
477 textbox.innerHTML = '<p>' + text + '</p>';
478 } else {
479 textbox.value = text;
480 }
481 textbox.dispatchEvent(new Event('input', { bubbles: true }));
482 textbox.dispatchEvent(new Event('change', { bubbles: true }));
483 }
484
485 // Wait for React/framework to process
486 await new Promise(r => setTimeout(r, 1000));
487
488 // Verify text landed
489 const hasText = textbox.textContent?.includes(text) || textbox.value?.includes?.(text);
490 if (!hasText) {
491 return { success: false, error: 'Text did not register in compose box', typed: false };
492 }
493
494 // Find and click post/submit button
495 const buttons = document.querySelectorAll('button');
496 const submitWords = ['post', 'tweet', 'reply', 'send', 'submit', 'comment', 'save'];
497 let submitBtn = null;
498
499 // Try data-testid first (X.com uses tweetButton, tweetButtonInline)
500 submitBtn = document.querySelector('[data-testid="tweetButton"], [data-testid="tweetButtonInline"]');
501
502 // Try finding a button containing a span with submit text
503 if (!submitBtn) {
504 const spans = document.querySelectorAll('button span');
505 for (const span of spans) {
506 const t = span.textContent.trim().toLowerCase();
507 if (submitWords.some(w => t === w)) {
508 const btn = span.closest('button');
509 if (btn && btn.offsetParent !== null) { submitBtn = btn; break; }
510 }
511 }
512 }
513
514 // Fall back to button text/aria-label search
515 if (!submitBtn) {
516 for (const btn of buttons) {
517 const t = (btn.textContent || btn.getAttribute('aria-label') || '').trim().toLowerCase();
518 if (submitWords.some(w => t.includes(w))) {
519 if (btn.offsetParent !== null) { submitBtn = btn; break; }
520 }
521 }
522 }
523
524 if (submitBtn) {
525 // Wait for React to process the input
526 return new Promise(resolve => {
527 setTimeout(() => {
528 submitBtn.click();
529 resolve({ success: true, action: 'commented', submitted: true, text });
530 }, 500);
531 });
532 }
533
534 return { success: true, action: 'typed', submitted: false, text, error: 'Typed but could not find submit button' };
535 }
536
537 case 'extract': {
538 const el = action.elementId ? findEl(action.elementId) : document.body;
539 return { success: true, text: (el?.innerText || '').slice(0, 8000) };
540 }
541
542 default:
543 return { success: false, error: 'Unsupported fallback action: ' + action.type };
544 }
545 },
546 });
547 return result?.result || { success: false, error: 'Fallback action failed' };
548 } catch (err2) {
549 return { success: false, error: `Action failed: ${err2.message}` };
550 }
551 }
552}
553
554async function captureScreenshot(tabId) {
555 const id = await getActiveTabId(tabId);
556 if (id) await chrome.tabs.update(id, { active: true });
557
558 try {
559 const dataUrl = await chrome.tabs.captureVisibleTab(null, {
560 format: 'png',
561 quality: 80,
562 });
563 return dataUrl;
564 } catch (err) {
565 return { error: err.message };
566 }
567}
568
569async function getNetworkLogFromTab(tabId) {
570 const id = await getActiveTabId(tabId);
571 if (!id) return { error: 'No active tab' };
572
573 try {
574 return await chrome.tabs.sendMessage(id, { type: 'getNetworkLog' });
575 } catch (err) {
576 return { error: err.message };
577 }
578}
579
580// ── Action Descriptions (for confirmation UI) ─────────────────────
581
582function describeAction(action) {
583 switch (action.type) {
584 case 'click': return `Click element ${action.elementId}`;
585 case 'type': return `Type "${action.text}" into ${action.elementId}`;
586 case 'navigate': return `Navigate to ${action.url}`;
587 case 'select': return `Select "${action.value}" in ${action.elementId}`;
588 case 'scroll': return `Scroll ${action.direction || 'down'}`;
589 case 'keypress': return `Press ${action.key}`;
590 case 'back': return 'Go back';
591 case 'forward': return 'Go forward';
592 case 'extract': return 'Extract page text';
593 default: return `${action.type} action`;
594 }
595}
596
597// ── Activity Log ──────────────────────────────────────────────
598
599let activityLog = [];
600const MAX_ACTIVITY = 50;
601
602function broadcastActivity(action, details) {
603 const entry = { action, details, time: new Date().toISOString() };
604 activityLog.push(entry);
605 if (activityLog.length > MAX_ACTIVITY) activityLog.shift();
606 chrome.runtime.sendMessage({ type: 'activity', entry, log: activityLog }).catch(() => {});
607}
608
609// ── State Broadcasting ────────────────────────────────────────────
610
611function updateState(state) {
612 connectionState = state;
613 broadcastState();
614}
615
616function broadcastState() {
617 chrome.runtime.sendMessage({
618 type: 'stateUpdate',
619 connectionState,
620 config: { ...config, apiKey: config.apiKey ? '••••' : '' },
621 }).catch(() => {});
622}
623
624// ── Internal Message Handler (from popup / side panel) ────────────
625
626chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
627 (async () => {
628 switch (msg.type) {
629 case 'getState': {
630 sendResponse({
631 connectionState,
632 config: { ...config, apiKey: config.apiKey ? '••••' : '' },
633 });
634 break;
635 }
636
637 case 'getActivityLog': {
638 sendResponse({ log: activityLog });
639 break;
640 }
641
642 case 'saveConfig': {
643 const updated = await saveConfig(msg.config);
644 sendResponse({ success: true, config: updated });
645 break;
646 }
647
648 case 'connect': {
649 await loadConfig();
650 connect();
651 sendResponse({ success: true });
652 break;
653 }
654
655 case 'disconnect': {
656 disconnect();
657 sendResponse({ success: true });
658 break;
659 }
660
661 case 'confirmActionResponse': {
662 const pending = pendingConfirmations.get(msg.confirmId);
663 if (pending) {
664 pendingConfirmations.delete(msg.confirmId);
665 if (msg.allowed) {
666 // Re-dispatch with confirmed flag
667 const result = await executeActionInTab(pending.action, pending.tabId);
668 socket?.emit('actionResult', { requestId: pending.requestId, data: result });
669 } else {
670 socket?.emit('actionResult', {
671 requestId: pending.requestId,
672 data: { success: false, error: 'User denied action' },
673 });
674 }
675 }
676 sendResponse({ success: true });
677 break;
678 }
679
680 case 'manualCapture': {
681 console.log('[TreeOS Bridge] manualCapture requested');
682 try {
683 const state = await getPageStateFromTab();
684 console.log('[TreeOS Bridge] manualCapture result:', state?.error || `tree: ${!!state?.tree}, url: ${state?.url}`);
685 if (socket?.connected) {
686 socket.emit('pageState', { requestId: 'manual', data: state });
687 }
688 sendResponse({ success: true, sent: !!socket?.connected, state });
689 } catch (err) {
690 console.error('[TreeOS Bridge] manualCapture error:', err);
691 sendResponse({ success: false, state: { error: err.message } });
692 }
693 break;
694 }
695
696 case 'manualScreenshot': {
697 const dataUrl = await captureScreenshot();
698 if (socket?.connected) {
699 socket.emit('screenshot', { requestId: 'manual', data: dataUrl });
700 sendResponse({ success: true, sent: true });
701 } else {
702 sendResponse({ success: true, sent: false });
703 }
704 break;
705 }
706
707 default:
708 sendResponse({ error: 'Unknown message type' });
709 }
710 })();
711 return true;
712});
713
714// ── Notification Button Handler ───────────────────────────────────
715
716chrome.notifications?.onButtonClicked?.addListener((notifId, buttonIndex) => {
717 chrome.runtime.sendMessage({
718 type: 'confirmActionResponse',
719 confirmId: notifId,
720 allowed: buttonIndex === 0,
721 }).catch(() => {});
722 chrome.notifications.clear(notifId);
723});
724
725// ── Tab Navigation Listener (auto-report URL changes) ─────────────
726
727chrome.tabs.onUpdated.addListener((tabId, changeInfo) => {
728 if (changeInfo.status === 'complete' && connectionState === 'connected') {
729 chrome.tabs.get(tabId).then(tab => {
730 if (tab.active) {
731 socket?.emit('pageNavigated', {
732 tabId,
733 url: tab.url,
734 title: tab.title,
735 });
736 }
737 }).catch(() => {});
738 }
739});
740
741// ── Side Panel Behavior ───────────────────────────────────────────
742
743chrome.sidePanel?.setPanelBehavior?.({ openPanelOnActionClick: false }).catch(() => {});
744
745// ── Boot ──────────────────────────────────────────────────────────
746
747loadConfig().then(() => {
748 if (config.serverUrl && config.apiKey) {
749 connect();
750 }
751});
7521/*!
2 * Socket.IO v4.8.1
3 * (c) 2014-2024 Guillermo Rauch
4 * Released under the MIT License.
5 */
6!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t="undefined"!=typeof globalThis?globalThis:t||self).io=n()}(this,(function(){"use strict";function t(t,n){(null==n||n>t.length)&&(n=t.length);for(var i=0,r=Array(n);i<n;i++)r[i]=t[i];return r}function n(t,n){for(var i=0;i<n.length;i++){var r=n[i];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,f(r.key),r)}}function i(t,i,r){return i&&n(t.prototype,i),r&&n(t,r),Object.defineProperty(t,"prototype",{writable:!1}),t}function r(n,i){var r="undefined"!=typeof Symbol&&n[Symbol.iterator]||n["@@iterator"];if(!r){if(Array.isArray(n)||(r=function(n,i){if(n){if("string"==typeof n)return t(n,i);var r={}.toString.call(n).slice(8,-1);return"Object"===r&&n.constructor&&(r=n.constructor.name),"Map"===r||"Set"===r?Array.from(n):"Arguments"===r||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?t(n,i):void 0}}(n))||i&&n&&"number"==typeof n.length){r&&(n=r);var e=0,o=function(){};return{s:o,n:function(){return e>=n.length?{done:!0}:{done:!1,value:n[e++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var s,u=!0,h=!1;return{s:function(){r=r.call(n)},n:function(){var t=r.next();return u=t.done,t},e:function(t){h=!0,s=t},f:function(){try{u||null==r.return||r.return()}finally{if(h)throw s}}}}function e(){return e=Object.assign?Object.assign.bind():function(t){for(var n=1;n<arguments.length;n++){var i=arguments[n];for(var r in i)({}).hasOwnProperty.call(i,r)&&(t[r]=i[r])}return t},e.apply(null,arguments)}function o(t){return o=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(t){return t.__proto__||Object.getPrototypeOf(t)},o(t)}function s(t,n){t.prototype=Object.create(n.prototype),t.prototype.constructor=t,h(t,n)}function u(){try{var t=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){})))}catch(t){}return(u=function(){return!!t})()}function h(t,n){return h=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,n){return t.__proto__=n,t},h(t,n)}function f(t){var n=function(t,n){if("object"!=typeof t||!t)return t;var i=t[Symbol.toPrimitive];if(void 0!==i){var r=i.call(t,n||"default");if("object"!=typeof r)return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===n?String:Number)(t)}(t,"string");return"symbol"==typeof n?n:n+""}function c(t){return c="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},c(t)}function a(t){var n="function"==typeof Map?new Map:void 0;return a=function(t){if(null===t||!function(t){try{return-1!==Function.toString.call(t).indexOf("[native code]")}catch(n){return"function"==typeof t}}(t))return t;if("function"!=typeof t)throw new TypeError("Super expression must either be null or a function");if(void 0!==n){if(n.has(t))return n.get(t);n.set(t,i)}function i(){return function(t,n,i){if(u())return Reflect.construct.apply(null,arguments);var r=[null];r.push.apply(r,n);var e=new(t.bind.apply(t,r));return i&&h(e,i.prototype),e}(t,arguments,o(this).constructor)}return i.prototype=Object.create(t.prototype,{constructor:{value:i,enumerable:!1,writable:!0,configurable:!0}}),h(i,t)},a(t)}var v=Object.create(null);v.open="0",v.close="1",v.ping="2",v.pong="3",v.message="4",v.upgrade="5",v.noop="6";var l=Object.create(null);Object.keys(v).forEach((function(t){l[v[t]]=t}));var p,d={type:"error",data:"parser error"},y="function"==typeof Blob||"undefined"!=typeof Blob&&"[object BlobConstructor]"===Object.prototype.toString.call(Blob),b="function"==typeof ArrayBuffer,w=function(t){return"function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(t):t&&t.buffer instanceof ArrayBuffer},g=function(t,n,i){var r=t.type,e=t.data;return y&&e instanceof Blob?n?i(e):m(e,i):b&&(e instanceof ArrayBuffer||w(e))?n?i(e):m(new Blob([e]),i):i(v[r]+(e||""))},m=function(t,n){var i=new FileReader;return i.onload=function(){var t=i.result.split(",")[1];n("b"+(t||""))},i.readAsDataURL(t)};function k(t){return t instanceof Uint8Array?t:t instanceof ArrayBuffer?new Uint8Array(t):new Uint8Array(t.buffer,t.byteOffset,t.byteLength)}for(var A="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",j="undefined"==typeof Uint8Array?[]:new Uint8Array(256),E=0;E<64;E++)j[A.charCodeAt(E)]=E;var O,B="function"==typeof ArrayBuffer,S=function(t,n){if("string"!=typeof t)return{type:"message",data:C(t,n)};var i=t.charAt(0);return"b"===i?{type:"message",data:N(t.substring(1),n)}:l[i]?t.length>1?{type:l[i],data:t.substring(1)}:{type:l[i]}:d},N=function(t,n){if(B){var i=function(t){var n,i,r,e,o,s=.75*t.length,u=t.length,h=0;"="===t[t.length-1]&&(s--,"="===t[t.length-2]&&s--);var f=new ArrayBuffer(s),c=new Uint8Array(f);for(n=0;n<u;n+=4)i=j[t.charCodeAt(n)],r=j[t.charCodeAt(n+1)],e=j[t.charCodeAt(n+2)],o=j[t.charCodeAt(n+3)],c[h++]=i<<2|r>>4,c[h++]=(15&r)<<4|e>>2,c[h++]=(3&e)<<6|63&o;return f}(t);return C(i,n)}return{base64:!0,data:t}},C=function(t,n){return"blob"===n?t instanceof Blob?t:new Blob([t]):t instanceof ArrayBuffer?t:t.buffer},T=String.fromCharCode(30);function U(){return new TransformStream({transform:function(t,n){!function(t,n){y&&t.data instanceof Blob?t.data.arrayBuffer().then(k).then(n):b&&(t.data instanceof ArrayBuffer||w(t.data))?n(k(t.data)):g(t,!1,(function(t){p||(p=new TextEncoder),n(p.encode(t))}))}(t,(function(i){var r,e=i.length;if(e<126)r=new Uint8Array(1),new DataView(r.buffer).setUint8(0,e);else if(e<65536){r=new Uint8Array(3);var o=new DataView(r.buffer);o.setUint8(0,126),o.setUint16(1,e)}else{r=new Uint8Array(9);var s=new DataView(r.buffer);s.setUint8(0,127),s.setBigUint64(1,BigInt(e))}t.data&&"string"!=typeof t.data&&(r[0]|=128),n.enqueue(r),n.enqueue(i)}))}})}function M(t){return t.reduce((function(t,n){return t+n.length}),0)}function x(t,n){if(t[0].length===n)return t.shift();for(var i=new Uint8Array(n),r=0,e=0;e<n;e++)i[e]=t[0][r++],r===t[0].length&&(t.shift(),r=0);return t.length&&r<t[0].length&&(t[0]=t[0].slice(r)),i}function I(t){if(t)return function(t){for(var n in I.prototype)t[n]=I.prototype[n];return t}(t)}I.prototype.on=I.prototype.addEventListener=function(t,n){return this.t=this.t||{},(this.t["$"+t]=this.t["$"+t]||[]).push(n),this},I.prototype.once=function(t,n){function i(){this.off(t,i),n.apply(this,arguments)}return i.fn=n,this.on(t,i),this},I.prototype.off=I.prototype.removeListener=I.prototype.removeAllListeners=I.prototype.removeEventListener=function(t,n){if(this.t=this.t||{},0==arguments.length)return this.t={},this;var i,r=this.t["$"+t];if(!r)return this;if(1==arguments.length)return delete this.t["$"+t],this;for(var e=0;e<r.length;e++)if((i=r[e])===n||i.fn===n){r.splice(e,1);break}return 0===r.length&&delete this.t["$"+t],this},I.prototype.emit=function(t){this.t=this.t||{};for(var n=new Array(arguments.length-1),i=this.t["$"+t],r=1;r<arguments.length;r++)n[r-1]=arguments[r];if(i){r=0;for(var e=(i=i.slice(0)).length;r<e;++r)i[r].apply(this,n)}return this},I.prototype.emitReserved=I.prototype.emit,I.prototype.listeners=function(t){return this.t=this.t||{},this.t["$"+t]||[]},I.prototype.hasListeners=function(t){return!!this.listeners(t).length};var R="function"==typeof Promise&&"function"==typeof Promise.resolve?function(t){return Promise.resolve().then(t)}:function(t,n){return n(t,0)},L="undefined"!=typeof self?self:"undefined"!=typeof window?window:Function("return this")();function _(t){for(var n=arguments.length,i=new Array(n>1?n-1:0),r=1;r<n;r++)i[r-1]=arguments[r];return i.reduce((function(n,i){return t.hasOwnProperty(i)&&(n[i]=t[i]),n}),{})}var D=L.setTimeout,P=L.clearTimeout;function $(t,n){n.useNativeTimers?(t.setTimeoutFn=D.bind(L),t.clearTimeoutFn=P.bind(L)):(t.setTimeoutFn=L.setTimeout.bind(L),t.clearTimeoutFn=L.clearTimeout.bind(L))}function F(){return Date.now().toString(36).substring(3)+Math.random().toString(36).substring(2,5)}var V=function(t){function n(n,i,r){var e;return(e=t.call(this,n)||this).description=i,e.context=r,e.type="TransportError",e}return s(n,t),n}(a(Error)),q=function(t){function n(n){var i;return(i=t.call(this)||this).writable=!1,$(i,n),i.opts=n,i.query=n.query,i.socket=n.socket,i.supportsBinary=!n.forceBase64,i}s(n,t);var i=n.prototype;return i.onError=function(n,i,r){return t.prototype.emitReserved.call(this,"error",new V(n,i,r)),this},i.open=function(){return this.readyState="opening",this.doOpen(),this},i.close=function(){return"opening"!==this.readyState&&"open"!==this.readyState||(this.doClose(),this.onClose()),this},i.send=function(t){"open"===this.readyState&&this.write(t)},i.onOpen=function(){this.readyState="open",this.writable=!0,t.prototype.emitReserved.call(this,"open")},i.onData=function(t){var n=S(t,this.socket.binaryType);this.onPacket(n)},i.onPacket=function(n){t.prototype.emitReserved.call(this,"packet",n)},i.onClose=function(n){this.readyState="closed",t.prototype.emitReserved.call(this,"close",n)},i.pause=function(t){},i.createUri=function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return t+"://"+this.i()+this.o()+this.opts.path+this.u(n)},i.i=function(){var t=this.opts.hostname;return-1===t.indexOf(":")?t:"["+t+"]"},i.o=function(){return this.opts.port&&(this.opts.secure&&Number(443!==this.opts.port)||!this.opts.secure&&80!==Number(this.opts.port))?":"+this.opts.port:""},i.u=function(t){var n=function(t){var n="";for(var i in t)t.hasOwnProperty(i)&&(n.length&&(n+="&"),n+=encodeURIComponent(i)+"="+encodeURIComponent(t[i]));return n}(t);return n.length?"?"+n:""},n}(I),X=function(t){function n(){var n;return(n=t.apply(this,arguments)||this).h=!1,n}s(n,t);var r=n.prototype;return r.doOpen=function(){this.v()},r.pause=function(t){var n=this;this.readyState="pausing";var i=function(){n.readyState="paused",t()};if(this.h||!this.writable){var r=0;this.h&&(r++,this.once("pollComplete",(function(){--r||i()}))),this.writable||(r++,this.once("drain",(function(){--r||i()})))}else i()},r.v=function(){this.h=!0,this.doPoll(),this.emitReserved("poll")},r.onData=function(t){var n=this;(function(t,n){for(var i=t.split(T),r=[],e=0;e<i.length;e++){var o=S(i[e],n);if(r.push(o),"error"===o.type)break}return r})(t,this.socket.binaryType).forEach((function(t){if("opening"===n.readyState&&"open"===t.type&&n.onOpen(),"close"===t.type)return n.onClose({description:"transport closed by the server"}),!1;n.onPacket(t)})),"closed"!==this.readyState&&(this.h=!1,this.emitReserved("pollComplete"),"open"===this.readyState&&this.v())},r.doClose=function(){var t=this,n=function(){t.write([{type:"close"}])};"open"===this.readyState?n():this.once("open",n)},r.write=function(t){var n=this;this.writable=!1,function(t,n){var i=t.length,r=new Array(i),e=0;t.forEach((function(t,o){g(t,!1,(function(t){r[o]=t,++e===i&&n(r.join(T))}))}))}(t,(function(t){n.doWrite(t,(function(){n.writable=!0,n.emitReserved("drain")}))}))},r.uri=function(){var t=this.opts.secure?"https":"http",n=this.query||{};return!1!==this.opts.timestampRequests&&(n[this.opts.timestampParam]=F()),this.supportsBinary||n.sid||(n.b64=1),this.createUri(t,n)},i(n,[{key:"name",get:function(){return"polling"}}])}(q),H=!1;try{H="undefined"!=typeof XMLHttpRequest&&"withCredentials"in new XMLHttpRequest}catch(t){}var z=H;function J(){}var K=function(t){function n(n){var i;if(i=t.call(this,n)||this,"undefined"!=typeof location){var r="https:"===location.protocol,e=location.port;e||(e=r?"443":"80"),i.xd="undefined"!=typeof location&&n.hostname!==location.hostname||e!==n.port}return i}s(n,t);var i=n.prototype;return i.doWrite=function(t,n){var i=this,r=this.request({method:"POST",data:t});r.on("success",n),r.on("error",(function(t,n){i.onError("xhr post error",t,n)}))},i.doPoll=function(){var t=this,n=this.request();n.on("data",this.onData.bind(this)),n.on("error",(function(n,i){t.onError("xhr poll error",n,i)})),this.pollXhr=n},n}(X),Y=function(t){function n(n,i,r){var e;return(e=t.call(this)||this).createRequest=n,$(e,r),e.l=r,e.p=r.method||"GET",e.m=i,e.k=void 0!==r.data?r.data:null,e.A(),e}s(n,t);var i=n.prototype;return i.A=function(){var t,i=this,r=_(this.l,"agent","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","autoUnref");r.xdomain=!!this.l.xd;var e=this.j=this.createRequest(r);try{e.open(this.p,this.m,!0);try{if(this.l.extraHeaders)for(var o in e.setDisableHeaderCheck&&e.setDisableHeaderCheck(!0),this.l.extraHeaders)this.l.extraHeaders.hasOwnProperty(o)&&e.setRequestHeader(o,this.l.extraHeaders[o])}catch(t){}if("POST"===this.p)try{e.setRequestHeader("Content-type","text/plain;charset=UTF-8")}catch(t){}try{e.setRequestHeader("Accept","*/*")}catch(t){}null===(t=this.l.cookieJar)||void 0===t||t.addCookies(e),"withCredentials"in e&&(e.withCredentials=this.l.withCredentials),this.l.requestTimeout&&(e.timeout=this.l.requestTimeout),e.onreadystatechange=function(){var t;3===e.readyState&&(null===(t=i.l.cookieJar)||void 0===t||t.parseCookies(e.getResponseHeader("set-cookie"))),4===e.readyState&&(200===e.status||1223===e.status?i.O():i.setTimeoutFn((function(){i.B("number"==typeof e.status?e.status:0)}),0))},e.send(this.k)}catch(t){return void this.setTimeoutFn((function(){i.B(t)}),0)}"undefined"!=typeof document&&(this.S=n.requestsCount++,n.requests[this.S]=this)},i.B=function(t){this.emitReserved("error",t,this.j),this.N(!0)},i.N=function(t){if(void 0!==this.j&&null!==this.j){if(this.j.onreadystatechange=J,t)try{this.j.abort()}catch(t){}"undefined"!=typeof document&&delete n.requests[this.S],this.j=null}},i.O=function(){var t=this.j.responseText;null!==t&&(this.emitReserved("data",t),this.emitReserved("success"),this.N())},i.abort=function(){this.N()},n}(I);if(Y.requestsCount=0,Y.requests={},"undefined"!=typeof document)if("function"==typeof attachEvent)attachEvent("onunload",G);else if("function"==typeof addEventListener){addEventListener("onpagehide"in L?"pagehide":"unload",G,!1)}function G(){for(var t in Y.requests)Y.requests.hasOwnProperty(t)&&Y.requests[t].abort()}var Q,W=(Q=tt({xdomain:!1}))&&null!==Q.responseType,Z=function(t){function n(n){var i;i=t.call(this,n)||this;var r=n&&n.forceBase64;return i.supportsBinary=W&&!r,i}return s(n,t),n.prototype.request=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return e(t,{xd:this.xd},this.opts),new Y(tt,this.uri(),t)},n}(K);function tt(t){var n=t.xdomain;try{if("undefined"!=typeof XMLHttpRequest&&(!n||z))return new XMLHttpRequest}catch(t){}if(!n)try{return new(L[["Active"].concat("Object").join("X")])("Microsoft.XMLHTTP")}catch(t){}}var nt="undefined"!=typeof navigator&&"string"==typeof navigator.product&&"reactnative"===navigator.product.toLowerCase(),it=function(t){function n(){return t.apply(this,arguments)||this}s(n,t);var r=n.prototype;return r.doOpen=function(){var t=this.uri(),n=this.opts.protocols,i=nt?{}:_(this.opts,"agent","perMessageDeflate","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","localAddress","protocolVersion","origin","maxPayload","family","checkServerIdentity");this.opts.extraHeaders&&(i.headers=this.opts.extraHeaders);try{this.ws=this.createSocket(t,n,i)}catch(t){return this.emitReserved("error",t)}this.ws.binaryType=this.socket.binaryType,this.addEventListeners()},r.addEventListeners=function(){var t=this;this.ws.onopen=function(){t.opts.autoUnref&&t.ws.C.unref(),t.onOpen()},this.ws.onclose=function(n){return t.onClose({description:"websocket connection closed",context:n})},this.ws.onmessage=function(n){return t.onData(n.data)},this.ws.onerror=function(n){return t.onError("websocket error",n)}},r.write=function(t){var n=this;this.writable=!1;for(var i=function(){var i=t[r],e=r===t.length-1;g(i,n.supportsBinary,(function(t){try{n.doWrite(i,t)}catch(t){}e&&R((function(){n.writable=!0,n.emitReserved("drain")}),n.setTimeoutFn)}))},r=0;r<t.length;r++)i()},r.doClose=function(){void 0!==this.ws&&(this.ws.onerror=function(){},this.ws.close(),this.ws=null)},r.uri=function(){var t=this.opts.secure?"wss":"ws",n=this.query||{};return this.opts.timestampRequests&&(n[this.opts.timestampParam]=F()),this.supportsBinary||(n.b64=1),this.createUri(t,n)},i(n,[{key:"name",get:function(){return"websocket"}}])}(q),rt=L.WebSocket||L.MozWebSocket,et=function(t){function n(){return t.apply(this,arguments)||this}s(n,t);var i=n.prototype;return i.createSocket=function(t,n,i){return nt?new rt(t,n,i):n?new rt(t,n):new rt(t)},i.doWrite=function(t,n){this.ws.send(n)},n}(it),ot=function(t){function n(){return t.apply(this,arguments)||this}s(n,t);var r=n.prototype;return r.doOpen=function(){var t=this;try{this.T=new WebTransport(this.createUri("https"),this.opts.transportOptions[this.name])}catch(t){return this.emitReserved("error",t)}this.T.closed.then((function(){t.onClose()})).catch((function(n){t.onError("webtransport error",n)})),this.T.ready.then((function(){t.T.createBidirectionalStream().then((function(n){var i=function(t,n){O||(O=new TextDecoder);var i=[],r=0,e=-1,o=!1;return new TransformStream({transform:function(s,u){for(i.push(s);;){if(0===r){if(M(i)<1)break;var h=x(i,1);o=!(128&~h[0]),e=127&h[0],r=e<126?3:126===e?1:2}else if(1===r){if(M(i)<2)break;var f=x(i,2);e=new DataView(f.buffer,f.byteOffset,f.length).getUint16(0),r=3}else if(2===r){if(M(i)<8)break;var c=x(i,8),a=new DataView(c.buffer,c.byteOffset,c.length),v=a.getUint32(0);if(v>Math.pow(2,21)-1){u.enqueue(d);break}e=v*Math.pow(2,32)+a.getUint32(4),r=3}else{if(M(i)<e)break;var l=x(i,e);u.enqueue(S(o?l:O.decode(l),n)),r=0}if(0===e||e>t){u.enqueue(d);break}}}})}(Number.MAX_SAFE_INTEGER,t.socket.binaryType),r=n.readable.pipeThrough(i).getReader(),e=U();e.readable.pipeTo(n.writable),t.U=e.writable.getWriter();!function n(){r.read().then((function(i){var r=i.done,e=i.value;r||(t.onPacket(e),n())})).catch((function(t){}))}();var o={type:"open"};t.query.sid&&(o.data='{"sid":"'.concat(t.query.sid,'"}')),t.U.write(o).then((function(){return t.onOpen()}))}))}))},r.write=function(t){var n=this;this.writable=!1;for(var i=function(){var i=t[r],e=r===t.length-1;n.U.write(i).then((function(){e&&R((function(){n.writable=!0,n.emitReserved("drain")}),n.setTimeoutFn)}))},r=0;r<t.length;r++)i()},r.doClose=function(){var t;null===(t=this.T)||void 0===t||t.close()},i(n,[{key:"name",get:function(){return"webtransport"}}])}(q),st={websocket:et,webtransport:ot,polling:Z},ut=/^(?:(?![^:@\/?#]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@\/?#]*)(?::([^:@\/?#]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/,ht=["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"];function ft(t){if(t.length>8e3)throw"URI too long";var n=t,i=t.indexOf("["),r=t.indexOf("]");-1!=i&&-1!=r&&(t=t.substring(0,i)+t.substring(i,r).replace(/:/g,";")+t.substring(r,t.length));for(var e,o,s=ut.exec(t||""),u={},h=14;h--;)u[ht[h]]=s[h]||"";return-1!=i&&-1!=r&&(u.source=n,u.host=u.host.substring(1,u.host.length-1).replace(/;/g,":"),u.authority=u.authority.replace("[","").replace("]","").replace(/;/g,":"),u.ipv6uri=!0),u.pathNames=function(t,n){var i=/\/{2,9}/g,r=n.replace(i,"/").split("/");"/"!=n.slice(0,1)&&0!==n.length||r.splice(0,1);"/"==n.slice(-1)&&r.splice(r.length-1,1);return r}(0,u.path),u.queryKey=(e=u.query,o={},e.replace(/(?:^|&)([^&=]*)=?([^&]*)/g,(function(t,n,i){n&&(o[n]=i)})),o),u}var ct="function"==typeof addEventListener&&"function"==typeof removeEventListener,at=[];ct&&addEventListener("offline",(function(){at.forEach((function(t){return t()}))}),!1);var vt=function(t){function n(n,i){var r;if((r=t.call(this)||this).binaryType="arraybuffer",r.writeBuffer=[],r.M=0,r.I=-1,r.R=-1,r.L=-1,r._=1/0,n&&"object"===c(n)&&(i=n,n=null),n){var o=ft(n);i.hostname=o.host,i.secure="https"===o.protocol||"wss"===o.protocol,i.port=o.port,o.query&&(i.query=o.query)}else i.host&&(i.hostname=ft(i.host).host);return $(r,i),r.secure=null!=i.secure?i.secure:"undefined"!=typeof location&&"https:"===location.protocol,i.hostname&&!i.port&&(i.port=r.secure?"443":"80"),r.hostname=i.hostname||("undefined"!=typeof location?location.hostname:"localhost"),r.port=i.port||("undefined"!=typeof location&&location.port?location.port:r.secure?"443":"80"),r.transports=[],r.D={},i.transports.forEach((function(t){var n=t.prototype.name;r.transports.push(n),r.D[n]=t})),r.opts=e({path:"/engine.io",agent:!1,withCredentials:!1,upgrade:!0,timestampParam:"t",rememberUpgrade:!1,addTrailingSlash:!0,rejectUnauthorized:!0,perMessageDeflate:{threshold:1024},transportOptions:{},closeOnBeforeunload:!1},i),r.opts.path=r.opts.path.replace(/\/$/,"")+(r.opts.addTrailingSlash?"/":""),"string"==typeof r.opts.query&&(r.opts.query=function(t){for(var n={},i=t.split("&"),r=0,e=i.length;r<e;r++){var o=i[r].split("=");n[decodeURIComponent(o[0])]=decodeURIComponent(o[1])}return n}(r.opts.query)),ct&&(r.opts.closeOnBeforeunload&&(r.P=function(){r.transport&&(r.transport.removeAllListeners(),r.transport.close())},addEventListener("beforeunload",r.P,!1)),"localhost"!==r.hostname&&(r.$=function(){r.F("transport close",{description:"network connection lost"})},at.push(r.$))),r.opts.withCredentials&&(r.V=void 0),r.q(),r}s(n,t);var i=n.prototype;return i.createTransport=function(t){var n=e({},this.opts.query);n.EIO=4,n.transport=t,this.id&&(n.sid=this.id);var i=e({},this.opts,{query:n,socket:this,hostname:this.hostname,secure:this.secure,port:this.port},this.opts.transportOptions[t]);return new this.D[t](i)},i.q=function(){var t=this;if(0!==this.transports.length){var i=this.opts.rememberUpgrade&&n.priorWebsocketSuccess&&-1!==this.transports.indexOf("websocket")?"websocket":this.transports[0];this.readyState="opening";var r=this.createTransport(i);r.open(),this.setTransport(r)}else this.setTimeoutFn((function(){t.emitReserved("error","No transports available")}),0)},i.setTransport=function(t){var n=this;this.transport&&this.transport.removeAllListeners(),this.transport=t,t.on("drain",this.X.bind(this)).on("packet",this.H.bind(this)).on("error",this.B.bind(this)).on("close",(function(t){return n.F("transport close",t)}))},i.onOpen=function(){this.readyState="open",n.priorWebsocketSuccess="websocket"===this.transport.name,this.emitReserved("open"),this.flush()},i.H=function(t){if("opening"===this.readyState||"open"===this.readyState||"closing"===this.readyState)switch(this.emitReserved("packet",t),this.emitReserved("heartbeat"),t.type){case"open":this.onHandshake(JSON.parse(t.data));break;case"ping":this.J("pong"),this.emitReserved("ping"),this.emitReserved("pong"),this.K();break;case"error":var n=new Error("server error");n.code=t.data,this.B(n);break;case"message":this.emitReserved("data",t.data),this.emitReserved("message",t.data)}},i.onHandshake=function(t){this.emitReserved("handshake",t),this.id=t.sid,this.transport.query.sid=t.sid,this.I=t.pingInterval,this.R=t.pingTimeout,this.L=t.maxPayload,this.onOpen(),"closed"!==this.readyState&&this.K()},i.K=function(){var t=this;this.clearTimeoutFn(this.Y);var n=this.I+this.R;this._=Date.now()+n,this.Y=this.setTimeoutFn((function(){t.F("ping timeout")}),n),this.opts.autoUnref&&this.Y.unref()},i.X=function(){this.writeBuffer.splice(0,this.M),this.M=0,0===this.writeBuffer.length?this.emitReserved("drain"):this.flush()},i.flush=function(){if("closed"!==this.readyState&&this.transport.writable&&!this.upgrading&&this.writeBuffer.length){var t=this.G();this.transport.send(t),this.M=t.length,this.emitReserved("flush")}},i.G=function(){if(!(this.L&&"polling"===this.transport.name&&this.writeBuffer.length>1))return this.writeBuffer;for(var t,n=1,i=0;i<this.writeBuffer.length;i++){var r=this.writeBuffer[i].data;if(r&&(n+="string"==typeof(t=r)?function(t){for(var n=0,i=0,r=0,e=t.length;r<e;r++)(n=t.charCodeAt(r))<128?i+=1:n<2048?i+=2:n<55296||n>=57344?i+=3:(r++,i+=4);return i}(t):Math.ceil(1.33*(t.byteLength||t.size))),i>0&&n>this.L)return this.writeBuffer.slice(0,i);n+=2}return this.writeBuffer},i.W=function(){var t=this;if(!this._)return!0;var n=Date.now()>this._;return n&&(this._=0,R((function(){t.F("ping timeout")}),this.setTimeoutFn)),n},i.write=function(t,n,i){return this.J("message",t,n,i),this},i.send=function(t,n,i){return this.J("message",t,n,i),this},i.J=function(t,n,i,r){if("function"==typeof n&&(r=n,n=void 0),"function"==typeof i&&(r=i,i=null),"closing"!==this.readyState&&"closed"!==this.readyState){(i=i||{}).compress=!1!==i.compress;var e={type:t,data:n,options:i};this.emitReserved("packetCreate",e),this.writeBuffer.push(e),r&&this.once("flush",r),this.flush()}},i.close=function(){var t=this,n=function(){t.F("forced close"),t.transport.close()},i=function i(){t.off("upgrade",i),t.off("upgradeError",i),n()},r=function(){t.once("upgrade",i),t.once("upgradeError",i)};return"opening"!==this.readyState&&"open"!==this.readyState||(this.readyState="closing",this.writeBuffer.length?this.once("drain",(function(){t.upgrading?r():n()})):this.upgrading?r():n()),this},i.B=function(t){if(n.priorWebsocketSuccess=!1,this.opts.tryAllTransports&&this.transports.length>1&&"opening"===this.readyState)return this.transports.shift(),this.q();this.emitReserved("error",t),this.F("transport error",t)},i.F=function(t,n){if("opening"===this.readyState||"open"===this.readyState||"closing"===this.readyState){if(this.clearTimeoutFn(this.Y),this.transport.removeAllListeners("close"),this.transport.close(),this.transport.removeAllListeners(),ct&&(this.P&&removeEventListener("beforeunload",this.P,!1),this.$)){var i=at.indexOf(this.$);-1!==i&&at.splice(i,1)}this.readyState="closed",this.id=null,this.emitReserved("close",t,n),this.writeBuffer=[],this.M=0}},n}(I);vt.protocol=4;var lt=function(t){function n(){var n;return(n=t.apply(this,arguments)||this).Z=[],n}s(n,t);var i=n.prototype;return i.onOpen=function(){if(t.prototype.onOpen.call(this),"open"===this.readyState&&this.opts.upgrade)for(var n=0;n<this.Z.length;n++)this.tt(this.Z[n])},i.tt=function(t){var n=this,i=this.createTransport(t),r=!1;vt.priorWebsocketSuccess=!1;var e=function(){r||(i.send([{type:"ping",data:"probe"}]),i.once("packet",(function(t){if(!r)if("pong"===t.type&&"probe"===t.data){if(n.upgrading=!0,n.emitReserved("upgrading",i),!i)return;vt.priorWebsocketSuccess="websocket"===i.name,n.transport.pause((function(){r||"closed"!==n.readyState&&(c(),n.setTransport(i),i.send([{type:"upgrade"}]),n.emitReserved("upgrade",i),i=null,n.upgrading=!1,n.flush())}))}else{var e=new Error("probe error");e.transport=i.name,n.emitReserved("upgradeError",e)}})))};function o(){r||(r=!0,c(),i.close(),i=null)}var s=function(t){var r=new Error("probe error: "+t);r.transport=i.name,o(),n.emitReserved("upgradeError",r)};function u(){s("transport closed")}function h(){s("socket closed")}function f(t){i&&t.name!==i.name&&o()}var c=function(){i.removeListener("open",e),i.removeListener("error",s),i.removeListener("close",u),n.off("close",h),n.off("upgrading",f)};i.once("open",e),i.once("error",s),i.once("close",u),this.once("close",h),this.once("upgrading",f),-1!==this.Z.indexOf("webtransport")&&"webtransport"!==t?this.setTimeoutFn((function(){r||i.open()}),200):i.open()},i.onHandshake=function(n){this.Z=this.nt(n.upgrades),t.prototype.onHandshake.call(this,n)},i.nt=function(t){for(var n=[],i=0;i<t.length;i++)~this.transports.indexOf(t[i])&&n.push(t[i]);return n},n}(vt),pt=function(t){function n(n){var i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},r="object"===c(n)?n:i;return(!r.transports||r.transports&&"string"==typeof r.transports[0])&&(r.transports=(r.transports||["polling","websocket","webtransport"]).map((function(t){return st[t]})).filter((function(t){return!!t}))),t.call(this,n,r)||this}return s(n,t),n}(lt);pt.protocol;var dt="function"==typeof ArrayBuffer,yt=function(t){return"function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(t):t.buffer instanceof ArrayBuffer},bt=Object.prototype.toString,wt="function"==typeof Blob||"undefined"!=typeof Blob&&"[object BlobConstructor]"===bt.call(Blob),gt="function"==typeof File||"undefined"!=typeof File&&"[object FileConstructor]"===bt.call(File);function mt(t){return dt&&(t instanceof ArrayBuffer||yt(t))||wt&&t instanceof Blob||gt&&t instanceof File}function kt(t,n){if(!t||"object"!==c(t))return!1;if(Array.isArray(t)){for(var i=0,r=t.length;i<r;i++)if(kt(t[i]))return!0;return!1}if(mt(t))return!0;if(t.toJSON&&"function"==typeof t.toJSON&&1===arguments.length)return kt(t.toJSON(),!0);for(var e in t)if(Object.prototype.hasOwnProperty.call(t,e)&&kt(t[e]))return!0;return!1}function At(t){var n=[],i=t.data,r=t;return r.data=jt(i,n),r.attachments=n.length,{packet:r,buffers:n}}function jt(t,n){if(!t)return t;if(mt(t)){var i={_placeholder:!0,num:n.length};return n.push(t),i}if(Array.isArray(t)){for(var r=new Array(t.length),e=0;e<t.length;e++)r[e]=jt(t[e],n);return r}if("object"===c(t)&&!(t instanceof Date)){var o={};for(var s in t)Object.prototype.hasOwnProperty.call(t,s)&&(o[s]=jt(t[s],n));return o}return t}function Et(t,n){return t.data=Ot(t.data,n),delete t.attachments,t}function Ot(t,n){if(!t)return t;if(t&&!0===t._placeholder){if("number"==typeof t.num&&t.num>=0&&t.num<n.length)return n[t.num];throw new Error("illegal attachments")}if(Array.isArray(t))for(var i=0;i<t.length;i++)t[i]=Ot(t[i],n);else if("object"===c(t))for(var r in t)Object.prototype.hasOwnProperty.call(t,r)&&(t[r]=Ot(t[r],n));return t}var Bt,St=["connect","connect_error","disconnect","disconnecting","newListener","removeListener"];!function(t){t[t.CONNECT=0]="CONNECT",t[t.DISCONNECT=1]="DISCONNECT",t[t.EVENT=2]="EVENT",t[t.ACK=3]="ACK",t[t.CONNECT_ERROR=4]="CONNECT_ERROR",t[t.BINARY_EVENT=5]="BINARY_EVENT",t[t.BINARY_ACK=6]="BINARY_ACK"}(Bt||(Bt={}));var Nt=function(){function t(t){this.replacer=t}var n=t.prototype;return n.encode=function(t){return t.type!==Bt.EVENT&&t.type!==Bt.ACK||!kt(t)?[this.encodeAsString(t)]:this.encodeAsBinary({type:t.type===Bt.EVENT?Bt.BINARY_EVENT:Bt.BINARY_ACK,nsp:t.nsp,data:t.data,id:t.id})},n.encodeAsString=function(t){var n=""+t.type;return t.type!==Bt.BINARY_EVENT&&t.type!==Bt.BINARY_ACK||(n+=t.attachments+"-"),t.nsp&&"/"!==t.nsp&&(n+=t.nsp+","),null!=t.id&&(n+=t.id),null!=t.data&&(n+=JSON.stringify(t.data,this.replacer)),n},n.encodeAsBinary=function(t){var n=At(t),i=this.encodeAsString(n.packet),r=n.buffers;return r.unshift(i),r},t}(),Ct=function(t){function n(n){var i;return(i=t.call(this)||this).reviver=n,i}s(n,t);var i=n.prototype;return i.add=function(n){var i;if("string"==typeof n){if(this.reconstructor)throw new Error("got plaintext data when reconstructing a packet");var r=(i=this.decodeString(n)).type===Bt.BINARY_EVENT;r||i.type===Bt.BINARY_ACK?(i.type=r?Bt.EVENT:Bt.ACK,this.reconstructor=new Tt(i),0===i.attachments&&t.prototype.emitReserved.call(this,"decoded",i)):t.prototype.emitReserved.call(this,"decoded",i)}else{if(!mt(n)&&!n.base64)throw new Error("Unknown type: "+n);if(!this.reconstructor)throw new Error("got binary data when not reconstructing a packet");(i=this.reconstructor.takeBinaryData(n))&&(this.reconstructor=null,t.prototype.emitReserved.call(this,"decoded",i))}},i.decodeString=function(t){var i=0,r={type:Number(t.charAt(0))};if(void 0===Bt[r.type])throw new Error("unknown packet type "+r.type);if(r.type===Bt.BINARY_EVENT||r.type===Bt.BINARY_ACK){for(var e=i+1;"-"!==t.charAt(++i)&&i!=t.length;);var o=t.substring(e,i);if(o!=Number(o)||"-"!==t.charAt(i))throw new Error("Illegal attachments");r.attachments=Number(o)}if("/"===t.charAt(i+1)){for(var s=i+1;++i;){if(","===t.charAt(i))break;if(i===t.length)break}r.nsp=t.substring(s,i)}else r.nsp="/";var u=t.charAt(i+1);if(""!==u&&Number(u)==u){for(var h=i+1;++i;){var f=t.charAt(i);if(null==f||Number(f)!=f){--i;break}if(i===t.length)break}r.id=Number(t.substring(h,i+1))}if(t.charAt(++i)){var c=this.tryParse(t.substr(i));if(!n.isPayloadValid(r.type,c))throw new Error("invalid payload");r.data=c}return r},i.tryParse=function(t){try{return JSON.parse(t,this.reviver)}catch(t){return!1}},n.isPayloadValid=function(t,n){switch(t){case Bt.CONNECT:return Mt(n);case Bt.DISCONNECT:return void 0===n;case Bt.CONNECT_ERROR:return"string"==typeof n||Mt(n);case Bt.EVENT:case Bt.BINARY_EVENT:return Array.isArray(n)&&("number"==typeof n[0]||"string"==typeof n[0]&&-1===St.indexOf(n[0]));case Bt.ACK:case Bt.BINARY_ACK:return Array.isArray(n)}},i.destroy=function(){this.reconstructor&&(this.reconstructor.finishedReconstruction(),this.reconstructor=null)},n}(I),Tt=function(){function t(t){this.packet=t,this.buffers=[],this.reconPack=t}var n=t.prototype;return n.takeBinaryData=function(t){if(this.buffers.push(t),this.buffers.length===this.reconPack.attachments){var n=Et(this.reconPack,this.buffers);return this.finishedReconstruction(),n}return null},n.finishedReconstruction=function(){this.reconPack=null,this.buffers=[]},t}();var Ut=Number.isInteger||function(t){return"number"==typeof t&&isFinite(t)&&Math.floor(t)===t};function Mt(t){return"[object Object]"===Object.prototype.toString.call(t)}var xt=Object.freeze({__proto__:null,protocol:5,get PacketType(){return Bt},Encoder:Nt,Decoder:Ct,isPacketValid:function(t){return"string"==typeof t.nsp&&(void 0===(n=t.id)||Ut(n))&&function(t,n){switch(t){case Bt.CONNECT:return void 0===n||Mt(n);case Bt.DISCONNECT:return void 0===n;case Bt.EVENT:return Array.isArray(n)&&("number"==typeof n[0]||"string"==typeof n[0]&&-1===St.indexOf(n[0]));case Bt.ACK:return Array.isArray(n);case Bt.CONNECT_ERROR:return"string"==typeof n||Mt(n);default:return!1}}(t.type,t.data);var n}});function It(t,n,i){return t.on(n,i),function(){t.off(n,i)}}var Rt=Object.freeze({connect:1,connect_error:1,disconnect:1,disconnecting:1,newListener:1,removeListener:1}),Lt=function(t){function n(n,i,r){var o;return(o=t.call(this)||this).connected=!1,o.recovered=!1,o.receiveBuffer=[],o.sendBuffer=[],o.it=[],o.rt=0,o.ids=0,o.acks={},o.flags={},o.io=n,o.nsp=i,r&&r.auth&&(o.auth=r.auth),o.l=e({},r),o.io.et&&o.open(),o}s(n,t);var o=n.prototype;return o.subEvents=function(){if(!this.subs){var t=this.io;this.subs=[It(t,"open",this.onopen.bind(this)),It(t,"packet",this.onpacket.bind(this)),It(t,"error",this.onerror.bind(this)),It(t,"close",this.onclose.bind(this))]}},o.connect=function(){return this.connected||(this.subEvents(),this.io.ot||this.io.open(),"open"===this.io.st&&this.onopen()),this},o.open=function(){return this.connect()},o.send=function(){for(var t=arguments.length,n=new Array(t),i=0;i<t;i++)n[i]=arguments[i];return n.unshift("message"),this.emit.apply(this,n),this},o.emit=function(t){var n,i,r;if(Rt.hasOwnProperty(t))throw new Error('"'+t.toString()+'" is a reserved event name');for(var e=arguments.length,o=new Array(e>1?e-1:0),s=1;s<e;s++)o[s-1]=arguments[s];if(o.unshift(t),this.l.retries&&!this.flags.fromQueue&&!this.flags.volatile)return this.ut(o),this;var u={type:Bt.EVENT,data:o,options:{}};if(u.options.compress=!1!==this.flags.compress,"function"==typeof o[o.length-1]){var h=this.ids++,f=o.pop();this.ht(h,f),u.id=h}var c=null===(i=null===(n=this.io.engine)||void 0===n?void 0:n.transport)||void 0===i?void 0:i.writable,a=this.connected&&!(null===(r=this.io.engine)||void 0===r?void 0:r.W());return this.flags.volatile&&!c||(a?(this.notifyOutgoingListeners(u),this.packet(u)):this.sendBuffer.push(u)),this.flags={},this},o.ht=function(t,n){var i,r=this,e=null!==(i=this.flags.timeout)&&void 0!==i?i:this.l.ackTimeout;if(void 0!==e){var o=this.io.setTimeoutFn((function(){delete r.acks[t];for(var i=0;i<r.sendBuffer.length;i++)r.sendBuffer[i].id===t&&r.sendBuffer.splice(i,1);n.call(r,new Error("operation has timed out"))}),e),s=function(){r.io.clearTimeoutFn(o);for(var t=arguments.length,i=new Array(t),e=0;e<t;e++)i[e]=arguments[e];n.apply(r,i)};s.withError=!0,this.acks[t]=s}else this.acks[t]=n},o.emitWithAck=function(t){for(var n=this,i=arguments.length,r=new Array(i>1?i-1:0),e=1;e<i;e++)r[e-1]=arguments[e];return new Promise((function(i,e){var o=function(t,n){return t?e(t):i(n)};o.withError=!0,r.push(o),n.emit.apply(n,[t].concat(r))}))},o.ut=function(t){var n,i=this;"function"==typeof t[t.length-1]&&(n=t.pop());var r={id:this.rt++,tryCount:0,pending:!1,args:t,flags:e({fromQueue:!0},this.flags)};t.push((function(t){if(r===i.it[0]){if(null!==t)r.tryCount>i.l.retries&&(i.it.shift(),n&&n(t));else if(i.it.shift(),n){for(var e=arguments.length,o=new Array(e>1?e-1:0),s=1;s<e;s++)o[s-1]=arguments[s];n.apply(void 0,[null].concat(o))}return r.pending=!1,i.ft()}})),this.it.push(r),this.ft()},o.ft=function(){var t=arguments.length>0&&void 0!==arguments[0]&&arguments[0];if(this.connected&&0!==this.it.length){var n=this.it[0];n.pending&&!t||(n.pending=!0,n.tryCount++,this.flags=n.flags,this.emit.apply(this,n.args))}},o.packet=function(t){t.nsp=this.nsp,this.io.ct(t)},o.onopen=function(){var t=this;"function"==typeof this.auth?this.auth((function(n){t.vt(n)})):this.vt(this.auth)},o.vt=function(t){this.packet({type:Bt.CONNECT,data:this.lt?e({pid:this.lt,offset:this.dt},t):t})},o.onerror=function(t){this.connected||this.emitReserved("connect_error",t)},o.onclose=function(t,n){this.connected=!1,delete this.id,this.emitReserved("disconnect",t,n),this.yt()},o.yt=function(){var t=this;Object.keys(this.acks).forEach((function(n){if(!t.sendBuffer.some((function(t){return String(t.id)===n}))){var i=t.acks[n];delete t.acks[n],i.withError&&i.call(t,new Error("socket has been disconnected"))}}))},o.onpacket=function(t){if(t.nsp===this.nsp)switch(t.type){case Bt.CONNECT:t.data&&t.data.sid?this.onconnect(t.data.sid,t.data.pid):this.emitReserved("connect_error",new Error("It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"));break;case Bt.EVENT:case Bt.BINARY_EVENT:this.onevent(t);break;case Bt.ACK:case Bt.BINARY_ACK:this.onack(t);break;case Bt.DISCONNECT:this.ondisconnect();break;case Bt.CONNECT_ERROR:this.destroy();var n=new Error(t.data.message);n.data=t.data.data,this.emitReserved("connect_error",n)}},o.onevent=function(t){var n=t.data||[];null!=t.id&&n.push(this.ack(t.id)),this.connected?this.emitEvent(n):this.receiveBuffer.push(Object.freeze(n))},o.emitEvent=function(n){if(this.bt&&this.bt.length){var i,e=r(this.bt.slice());try{for(e.s();!(i=e.n()).done;){i.value.apply(this,n)}}catch(t){e.e(t)}finally{e.f()}}t.prototype.emit.apply(this,n),this.lt&&n.length&&"string"==typeof n[n.length-1]&&(this.dt=n[n.length-1])},o.ack=function(t){var n=this,i=!1;return function(){if(!i){i=!0;for(var r=arguments.length,e=new Array(r),o=0;o<r;o++)e[o]=arguments[o];n.packet({type:Bt.ACK,id:t,data:e})}}},o.onack=function(t){var n=this.acks[t.id];"function"==typeof n&&(delete this.acks[t.id],n.withError&&t.data.unshift(null),n.apply(this,t.data))},o.onconnect=function(t,n){this.id=t,this.recovered=n&&this.lt===n,this.lt=n,this.connected=!0,this.emitBuffered(),this.emitReserved("connect"),this.ft(!0)},o.emitBuffered=function(){var t=this;this.receiveBuffer.forEach((function(n){return t.emitEvent(n)})),this.receiveBuffer=[],this.sendBuffer.forEach((function(n){t.notifyOutgoingListeners(n),t.packet(n)})),this.sendBuffer=[]},o.ondisconnect=function(){this.destroy(),this.onclose("io server disconnect")},o.destroy=function(){this.subs&&(this.subs.forEach((function(t){return t()})),this.subs=void 0),this.io.wt(this)},o.disconnect=function(){return this.connected&&this.packet({type:Bt.DISCONNECT}),this.destroy(),this.connected&&this.onclose("io client disconnect"),this},o.close=function(){return this.disconnect()},o.compress=function(t){return this.flags.compress=t,this},o.timeout=function(t){return this.flags.timeout=t,this},o.onAny=function(t){return this.bt=this.bt||[],this.bt.push(t),this},o.prependAny=function(t){return this.bt=this.bt||[],this.bt.unshift(t),this},o.offAny=function(t){if(!this.bt)return this;if(t){for(var n=this.bt,i=0;i<n.length;i++)if(t===n[i])return n.splice(i,1),this}else this.bt=[];return this},o.listenersAny=function(){return this.bt||[]},o.onAnyOutgoing=function(t){return this.gt=this.gt||[],this.gt.push(t),this},o.prependAnyOutgoing=function(t){return this.gt=this.gt||[],this.gt.unshift(t),this},o.offAnyOutgoing=function(t){if(!this.gt)return this;if(t){for(var n=this.gt,i=0;i<n.length;i++)if(t===n[i])return n.splice(i,1),this}else this.gt=[];return this},o.listenersAnyOutgoing=function(){return this.gt||[]},o.notifyOutgoingListeners=function(t){if(this.gt&&this.gt.length){var n,i=r(this.gt.slice());try{for(i.s();!(n=i.n()).done;){n.value.apply(this,t.data)}}catch(t){i.e(t)}finally{i.f()}}},i(n,[{key:"disconnected",get:function(){return!this.connected}},{key:"active",get:function(){return!!this.subs}},{key:"volatile",get:function(){return this.flags.volatile=!0,this}}])}(I);function _t(t){t=t||{},this.ms=t.min||100,this.max=t.max||1e4,this.factor=t.factor||2,this.jitter=t.jitter>0&&t.jitter<=1?t.jitter:0,this.attempts=0}_t.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var n=Math.random(),i=Math.floor(n*this.jitter*t);t=1&Math.floor(10*n)?t+i:t-i}return 0|Math.min(t,this.max)},_t.prototype.reset=function(){this.attempts=0},_t.prototype.setMin=function(t){this.ms=t},_t.prototype.setMax=function(t){this.max=t},_t.prototype.setJitter=function(t){this.jitter=t};var Dt=function(t){function n(n,i){var r,e;(r=t.call(this)||this).nsps={},r.subs=[],n&&"object"===c(n)&&(i=n,n=void 0),(i=i||{}).path=i.path||"/socket.io",r.opts=i,$(r,i),r.reconnection(!1!==i.reconnection),r.reconnectionAttempts(i.reconnectionAttempts||1/0),r.reconnectionDelay(i.reconnectionDelay||1e3),r.reconnectionDelayMax(i.reconnectionDelayMax||5e3),r.randomizationFactor(null!==(e=i.randomizationFactor)&&void 0!==e?e:.5),r.backoff=new _t({min:r.reconnectionDelay(),max:r.reconnectionDelayMax(),jitter:r.randomizationFactor()}),r.timeout(null==i.timeout?2e4:i.timeout),r.st="closed",r.uri=n;var o=i.parser||xt;return r.encoder=new o.Encoder,r.decoder=new o.Decoder,r.et=!1!==i.autoConnect,r.et&&r.open(),r}s(n,t);var i=n.prototype;return i.reconnection=function(t){return arguments.length?(this.kt=!!t,t||(this.skipReconnect=!0),this):this.kt},i.reconnectionAttempts=function(t){return void 0===t?this.At:(this.At=t,this)},i.reconnectionDelay=function(t){var n;return void 0===t?this.jt:(this.jt=t,null===(n=this.backoff)||void 0===n||n.setMin(t),this)},i.randomizationFactor=function(t){var n;return void 0===t?this.Et:(this.Et=t,null===(n=this.backoff)||void 0===n||n.setJitter(t),this)},i.reconnectionDelayMax=function(t){var n;return void 0===t?this.Ot:(this.Ot=t,null===(n=this.backoff)||void 0===n||n.setMax(t),this)},i.timeout=function(t){return arguments.length?(this.Bt=t,this):this.Bt},i.maybeReconnectOnOpen=function(){!this.ot&&this.kt&&0===this.backoff.attempts&&this.reconnect()},i.open=function(t){var n=this;if(~this.st.indexOf("open"))return this;this.engine=new pt(this.uri,this.opts);var i=this.engine,r=this;this.st="opening",this.skipReconnect=!1;var e=It(i,"open",(function(){r.onopen(),t&&t()})),o=function(i){n.cleanup(),n.st="closed",n.emitReserved("error",i),t?t(i):n.maybeReconnectOnOpen()},s=It(i,"error",o);if(!1!==this.Bt){var u=this.Bt,h=this.setTimeoutFn((function(){e(),o(new Error("timeout")),i.close()}),u);this.opts.autoUnref&&h.unref(),this.subs.push((function(){n.clearTimeoutFn(h)}))}return this.subs.push(e),this.subs.push(s),this},i.connect=function(t){return this.open(t)},i.onopen=function(){this.cleanup(),this.st="open",this.emitReserved("open");var t=this.engine;this.subs.push(It(t,"ping",this.onping.bind(this)),It(t,"data",this.ondata.bind(this)),It(t,"error",this.onerror.bind(this)),It(t,"close",this.onclose.bind(this)),It(this.decoder,"decoded",this.ondecoded.bind(this)))},i.onping=function(){this.emitReserved("ping")},i.ondata=function(t){try{this.decoder.add(t)}catch(t){this.onclose("parse error",t)}},i.ondecoded=function(t){var n=this;R((function(){n.emitReserved("packet",t)}),this.setTimeoutFn)},i.onerror=function(t){this.emitReserved("error",t)},i.socket=function(t,n){var i=this.nsps[t];return i?this.et&&!i.active&&i.connect():(i=new Lt(this,t,n),this.nsps[t]=i),i},i.wt=function(t){for(var n=0,i=Object.keys(this.nsps);n<i.length;n++){var r=i[n];if(this.nsps[r].active)return}this.St()},i.ct=function(t){for(var n=this.encoder.encode(t),i=0;i<n.length;i++)this.engine.write(n[i],t.options)},i.cleanup=function(){this.subs.forEach((function(t){return t()})),this.subs.length=0,this.decoder.destroy()},i.St=function(){this.skipReconnect=!0,this.ot=!1,this.onclose("forced close")},i.disconnect=function(){return this.St()},i.onclose=function(t,n){var i;this.cleanup(),null===(i=this.engine)||void 0===i||i.close(),this.backoff.reset(),this.st="closed",this.emitReserved("close",t,n),this.kt&&!this.skipReconnect&&this.reconnect()},i.reconnect=function(){var t=this;if(this.ot||this.skipReconnect)return this;var n=this;if(this.backoff.attempts>=this.At)this.backoff.reset(),this.emitReserved("reconnect_failed"),this.ot=!1;else{var i=this.backoff.duration();this.ot=!0;var r=this.setTimeoutFn((function(){n.skipReconnect||(t.emitReserved("reconnect_attempt",n.backoff.attempts),n.skipReconnect||n.open((function(i){i?(n.ot=!1,n.reconnect(),t.emitReserved("reconnect_error",i)):n.onreconnect()})))}),i);this.opts.autoUnref&&r.unref(),this.subs.push((function(){t.clearTimeoutFn(r)}))}},i.onreconnect=function(){var t=this.backoff.attempts;this.ot=!1,this.backoff.reset(),this.emitReserved("reconnect",t)},n}(I),Pt={};function $t(t,n){"object"===c(t)&&(n=t,t=void 0);var i,r=function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",i=arguments.length>2?arguments[2]:void 0,r=t;i=i||"undefined"!=typeof location&&location,null==t&&(t=i.protocol+"//"+i.host),"string"==typeof t&&("/"===t.charAt(0)&&(t="/"===t.charAt(1)?i.protocol+t:i.host+t),/^(https?|wss?):\/\//.test(t)||(t=void 0!==i?i.protocol+"//"+t:"https://"+t),r=ft(t)),r.port||(/^(http|ws)$/.test(r.protocol)?r.port="80":/^(http|ws)s$/.test(r.protocol)&&(r.port="443")),r.path=r.path||"/";var e=-1!==r.host.indexOf(":")?"["+r.host+"]":r.host;return r.id=r.protocol+"://"+e+":"+r.port+n,r.href=r.protocol+"://"+e+(i&&i.port===r.port?"":":"+r.port),r}(t,(n=n||{}).path||"/socket.io"),e=r.source,o=r.id,s=r.path,u=Pt[o]&&s in Pt[o].nsps;return n.forceNew||n["force new connection"]||!1===n.multiplex||u?i=new Dt(e,n):(Pt[o]||(Pt[o]=new Dt(e,n)),i=Pt[o]),r.query&&!n.query&&(n.query=r.queryKey),i.socket(r.path,n)}return e($t,{Manager:Dt,Socket:Lt,io:$t,connect:$t}),$t}));
7//# sourceMappingURL=socket.io.min.js.map
81{
2 "manifest_version": 3,
3 "name": "TreeOS Browser Bridge",
4 "version": "0.1.0",
5 "description": "Gives your TreeOS AI eyes and hands in the browser",
6 "permissions": [
7 "activeTab",
8 "tabs",
9 "scripting",
10 "storage",
11 "sidePanel"
12 ],
13 "host_permissions": ["<all_urls>"],
14 "background": {
15 "service_worker": "background.js"
16 },
17 "content_scripts": [
18 {
19 "matches": ["<all_urls>"],
20 "js": ["scripts/content.js"],
21 "run_at": "document_idle"
22 }
23 ],
24 "side_panel": {
25 "default_path": "sidepanel.html"
26 },
27 "action": {
28 "default_popup": "popup.html",
29 "default_icon": {
30 "16": "icons/icon16.png",
31 "48": "icons/icon48.png",
32 "128": "icons/icon128.png"
33 }
34 },
35 "icons": {
36 "16": "icons/icon16.png",
37 "48": "icons/icon48.png",
38 "128": "icons/icon128.png"
39 }
40}
411const $ = id => document.getElementById(id);
2
3const statusDot = $('statusDot');
4const statusBar = $('statusBar');
5const serverUrl = $('serverUrl');
6const apiKey = $('apiKey');
7const username = $('username');
8const loginPassword = $('loginPassword');
9const confirmActions = $('confirmActions');
10const autoCapture = $('autoCapture');
11const connectBtn = $('connectBtn');
12const disconnectBtn = $('disconnectBtn');
13const connectedActions = $('connectedActions');
14const confirmations = $('confirmations');
15
16function updateUI(state) {
17 const { connectionState, config } = state;
18
19 statusDot.className = `status-dot ${connectionState}`;
20 statusBar.textContent = connectionState === 'connected'
21 ? `Connected to ${config.serverUrl}`
22 : connectionState === 'connecting'
23 ? 'Connecting...'
24 : 'Not connected';
25
26 connectBtn.style.display = connectionState === 'connected' ? 'none' : '';
27 disconnectBtn.style.display = connectionState === 'connected' ? '' : 'none';
28 connectedActions.style.display = connectionState === 'connected' ? '' : 'none';
29
30 if (config.serverUrl && !serverUrl.value) serverUrl.value = config.serverUrl;
31 confirmActions.checked = config.confirmActions !== false;
32 autoCapture.checked = !!config.autoCapture;
33}
34
35// Load initial state
36chrome.runtime.sendMessage({ type: 'getState' }, (response) => {
37 if (response) updateUI(response);
38});
39
40// Load saved config
41chrome.storage.local.get(['treeos_config'], (result) => {
42 if (result.treeos_config) {
43 serverUrl.value = result.treeos_config.serverUrl || '';
44 apiKey.value = result.treeos_config.apiKey || '';
45 confirmActions.checked = result.treeos_config.confirmActions !== false;
46 autoCapture.checked = !!result.treeos_config.autoCapture;
47 }
48});
49
50// Save config
51$('saveBtn').addEventListener('click', () => {
52 chrome.runtime.sendMessage({
53 type: 'saveConfig',
54 config: {
55 serverUrl: serverUrl.value.replace(/\/+$/, ''),
56 apiKey: apiKey.value,
57 username: username.value,
58 password: loginPassword.value,
59 confirmActions: confirmActions.checked,
60 autoCapture: autoCapture.checked,
61 },
62 }, (resp) => {
63 statusBar.textContent = 'Settings saved';
64 setTimeout(() => chrome.runtime.sendMessage({ type: 'getState' }, updateUI), 1000);
65 });
66});
67
68// Connect
69connectBtn.addEventListener('click', () => {
70 // Save first, then connect
71 chrome.runtime.sendMessage({
72 type: 'saveConfig',
73 config: {
74 serverUrl: serverUrl.value.replace(/\/+$/, ''),
75 apiKey: apiKey.value,
76 username: username.value,
77 password: loginPassword.value,
78 confirmActions: confirmActions.checked,
79 autoCapture: autoCapture.checked,
80 },
81 }, () => {
82 chrome.runtime.sendMessage({ type: 'connect' });
83 });
84});
85
86// Disconnect
87disconnectBtn.addEventListener('click', () => {
88 chrome.runtime.sendMessage({ type: 'disconnect' });
89});
90
91// Manual capture
92$('captureBtn').addEventListener('click', () => {
93 chrome.runtime.sendMessage({ type: 'manualCapture' }, (resp) => {
94 statusBar.textContent = resp?.sent ? 'Page state sent' : 'Captured (not connected)';
95 });
96});
97
98// Screenshot
99$('screenshotBtn').addEventListener('click', () => {
100 chrome.runtime.sendMessage({ type: 'manualScreenshot' }, (resp) => {
101 statusBar.textContent = resp?.sent ? 'Screenshot sent' : 'Captured (not connected)';
102 });
103});
104
105// Open side panel
106$('openPanelBtn').addEventListener('click', async () => {
107 const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
108 if (tab) {
109 chrome.sidePanel.open({ tabId: tab.id });
110 window.close();
111 }
112});
113
114// Listen for state updates
115chrome.runtime.onMessage.addListener((msg) => {
116 if (msg.type === 'stateUpdate') {
117 updateUI(msg);
118 }
119
120 if (msg.type === 'activity') {
121 const e = msg.entry;
122 if (e.action === 'getPageState') statusBar.textContent = 'AI read page state';
123 else if (e.action === 'screenshot') statusBar.textContent = 'AI took screenshot';
124 else if (e.action === 'action') statusBar.textContent = `AI: ${e.details.type} ${e.details.target || ''} ${e.details.success ? '✓' : '✗'}`;
125 }
126
127 if (msg.type === 'confirmAction') {
128 const card = document.createElement('div');
129 card.className = 'confirm-card';
130 card.innerHTML = `
131 <p>${msg.description}</p>
132 <div class="confirm-buttons">
133 <button class="btn-primary allow-btn">Allow</button>
134 <button class="btn-danger deny-btn">Deny</button>
135 </div>
136 `;
137 card.querySelector('.allow-btn').onclick = () => {
138 chrome.runtime.sendMessage({ type: 'confirmActionResponse', confirmId: msg.confirmId, allowed: true });
139 card.remove();
140 };
141 card.querySelector('.deny-btn').onclick = () => {
142 chrome.runtime.sendMessage({ type: 'confirmActionResponse', confirmId: msg.confirmId, allowed: false });
143 card.remove();
144 };
145 confirmations.appendChild(card);
146 }
147});
1481// TreeOS Browser Bridge — Content Script
2// Runs in every page. Captures accessibility tree, executes agent actions.
3
4(() => {
5 const INTERACTIVE_ROLES = new Set([
6 'button', 'link', 'textbox', 'searchbox', 'checkbox', 'radio',
7 'combobox', 'listbox', 'option', 'menuitem', 'tab', 'switch',
8 'slider', 'spinbutton', 'scrollbar', 'menu', 'menubar',
9 'tree', 'treeitem', 'gridcell', 'row', 'columnheader', 'rowheader'
10 ]);
11
12 const INTERACTIVE_TAGS = new Set([
13 'A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'DETAILS', 'SUMMARY'
14 ]);
15
16 const SKIP_TAGS = new Set([
17 'SCRIPT', 'STYLE', 'NOSCRIPT', 'SVG', 'PATH', 'META', 'LINK', 'BR', 'HR'
18 ]);
19
20 // Element registry — maps short IDs to DOM elements for action execution
21 let elementRegistry = new Map();
22 let nextId = 1;
23
24 function resetRegistry() {
25 elementRegistry.clear();
26 nextId = 1;
27 }
28
29 function registerId(el) {
30 const id = `e${nextId++}`;
31 elementRegistry.set(id, el);
32 return id;
33 }
34
35 // ── Accessibility Tree Builder ──────────────────────────────────
36
37 function getRole(el) {
38 // Explicit ARIA role
39 const ariaRole = el.getAttribute('role');
40 if (ariaRole) return ariaRole;
41
42 // Implicit role from tag
43 const tag = el.tagName;
44 const type = el.getAttribute('type');
45
46 const implicitRoles = {
47 'A': el.hasAttribute('href') ? 'link' : null,
48 'BUTTON': 'button',
49 'INPUT': {
50 'checkbox': 'checkbox', 'radio': 'radio', 'range': 'slider',
51 'search': 'searchbox', 'submit': 'button', 'reset': 'button',
52 'text': 'textbox', 'email': 'textbox', 'password': 'textbox',
53 'tel': 'textbox', 'url': 'textbox', 'number': 'spinbutton',
54 }[type] || 'textbox',
55 'SELECT': 'combobox',
56 'TEXTAREA': 'textbox',
57 'H1': 'heading', 'H2': 'heading', 'H3': 'heading',
58 'H4': 'heading', 'H5': 'heading', 'H6': 'heading',
59 'NAV': 'navigation',
60 'MAIN': 'main',
61 'HEADER': 'banner',
62 'FOOTER': 'contentinfo',
63 'ASIDE': 'complementary',
64 'FORM': 'form',
65 'TABLE': 'table',
66 'UL': 'list', 'OL': 'list',
67 'LI': 'listitem',
68 'IMG': 'img',
69 'DETAILS': 'group',
70 'SUMMARY': 'button',
71 'DIALOG': 'dialog',
72 };
73
74 const role = implicitRoles[tag];
75 if (typeof role === 'string') return role;
76 return null;
77 }
78
79 function getAccessibleName(el) {
80 // aria-label takes priority
81 const ariaLabel = el.getAttribute('aria-label');
82 if (ariaLabel) return ariaLabel.trim();
83
84 // aria-labelledby
85 const labelledBy = el.getAttribute('aria-labelledby');
86 if (labelledBy) {
87 const names = labelledBy.split(/\s+/).map(id => {
88 const ref = document.getElementById(id);
89 return ref ? ref.textContent.trim() : '';
90 }).filter(Boolean);
91 if (names.length) return names.join(' ');
92 }
93
94 // Associated label (for inputs)
95 if (el.id) {
96 const label = document.querySelector(`label[for="${el.id}"]`);
97 if (label) return label.textContent.trim();
98 }
99
100 // Closest wrapping label
101 const parentLabel = el.closest('label');
102 if (parentLabel && parentLabel !== el) {
103 // Get label text excluding the input's own text
104 const clone = parentLabel.cloneNode(true);
105 clone.querySelectorAll('input,select,textarea').forEach(c => c.remove());
106 const text = clone.textContent.trim();
107 if (text) return text;
108 }
109
110 // Title attribute
111 const title = el.getAttribute('title');
112 if (title) return title.trim();
113
114 // Alt text for images
115 if (el.tagName === 'IMG') return el.getAttribute('alt')?.trim() || '';
116
117 // Placeholder for inputs
118 const placeholder = el.getAttribute('placeholder');
119 if (placeholder) return placeholder.trim();
120
121 // Value for inputs
122 if (el.tagName === 'INPUT' && el.value) return el.value.trim();
123
124 // Direct text content (only for leaf-ish elements)
125 const directText = getDirectText(el);
126 if (directText) return directText;
127
128 return '';
129 }
130
131 function getDirectText(el) {
132 // Get just this element's direct text, not deeply nested text
133 let text = '';
134 for (const child of el.childNodes) {
135 if (child.nodeType === Node.TEXT_NODE) {
136 text += child.textContent;
137 }
138 }
139 return text.trim().slice(0, 200);
140 }
141
142 function isVisible(el) {
143 if (!el.offsetParent && el.tagName !== 'BODY' && el.tagName !== 'HTML') {
144 // Could be position:fixed or visibility trick
145 const style = getComputedStyle(el);
146 if (style.display === 'none') return false;
147 if (style.visibility === 'hidden') return false;
148 if (style.position !== 'fixed' && style.position !== 'sticky') return false;
149 }
150 const rect = el.getBoundingClientRect();
151 if (rect.width === 0 && rect.height === 0) return false;
152 return true;
153 }
154
155 function isInteractive(el) {
156 if (INTERACTIVE_TAGS.has(el.tagName)) return true;
157 if (el.getAttribute('role') && INTERACTIVE_ROLES.has(el.getAttribute('role'))) return true;
158 if (el.getAttribute('onclick') || el.getAttribute('tabindex')) return true;
159 if (el.contentEditable === 'true') return true;
160
161 // Check for click listeners heuristic — elements with cursor:pointer
162 const style = getComputedStyle(el);
163 if (style.cursor === 'pointer') return true;
164
165 return false;
166 }
167
168 function getElementState(el) {
169 const state = {};
170 if (el.disabled) state.disabled = true;
171 if (el.checked) state.checked = true;
172 if (el.getAttribute('aria-expanded')) state.expanded = el.getAttribute('aria-expanded') === 'true';
173 if (el.getAttribute('aria-selected')) state.selected = el.getAttribute('aria-selected') === 'true';
174 if (el.getAttribute('aria-pressed')) state.pressed = el.getAttribute('aria-pressed') === 'true';
175 if (el.getAttribute('aria-hidden') === 'true') state.hidden = true;
176 if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
177 state.value = el.value || '';
178 }
179 if (el.tagName === 'SELECT') {
180 state.value = el.value || '';
181 state.options = Array.from(el.options).map(o => ({ value: o.value, text: o.text, selected: o.selected }));
182 }
183 return Object.keys(state).length ? state : null;
184 }
185
186 function buildTree(root, depth = 0, maxDepth = 25) {
187 if (depth > maxDepth) return null;
188
189 const nodes = [];
190
191 for (const child of root.children || []) {
192 if (!child.tagName) continue;
193 if (SKIP_TAGS.has(child.tagName)) continue;
194 if (!isVisible(child)) continue;
195
196 const role = getRole(child);
197 const interactive = isInteractive(child);
198 const name = getAccessibleName(child);
199
200 let node = null;
201
202 if (interactive || role) {
203 node = { role: role || child.tagName.toLowerCase() };
204 if (name) node.name = name;
205 if (interactive) node.id = registerId(child);
206
207 const state = getElementState(child);
208 if (state) node.state = state;
209
210 // Get link href
211 if (child.tagName === 'A' && child.href) {
212 node.href = child.href;
213 }
214 }
215
216 // Recurse into children
217 const childNodes = buildTree(child, depth + 1, maxDepth);
218
219 if (node) {
220 if (childNodes.length) node.children = childNodes;
221 nodes.push(node);
222 } else if (childNodes.length) {
223 // Flatten — skip this container, promote children
224 nodes.push(...childNodes);
225 } else if (name && (!child.children || child.children.length === 0)) {
226 // Leaf text node worth including
227 const textContent = child.textContent.trim();
228 if (textContent.length > 1 && textContent.length < 500) {
229 nodes.push({ role: 'text', name: textContent.slice(0, 200) });
230 }
231 }
232 }
233
234 return nodes;
235 }
236
237 function capturePageState() {
238 resetRegistry();
239
240 const tree = buildTree(document.body);
241
242 // Gather some metadata
243 const state = {
244 url: location.href,
245 title: document.title,
246 tree: tree,
247 viewport: {
248 width: window.innerWidth,
249 height: window.innerHeight,
250 scrollY: window.scrollY,
251 scrollHeight: document.documentElement.scrollHeight,
252 },
253 timestamp: Date.now(),
254 };
255
256 return state;
257 }
258
259 // ── Action Executor ─────────────────────────────────────────────
260
261 function simulateClick(el) {
262 el.scrollIntoView({ behavior: 'instant', block: 'center' });
263 el.focus();
264 el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
265 }
266
267 function simulateType(el, text, clear = true) {
268 el.scrollIntoView({ behavior: 'instant', block: 'center' });
269 el.focus();
270
271 // Contenteditable elements (rich text editors like Reddit, Slack, etc.)
272 if (el.contentEditable === 'true' || el.getAttribute('role') === 'textbox') {
273 if (clear) el.textContent = '';
274 el.textContent = text;
275 el.dispatchEvent(new Event('input', { bubbles: true }));
276 // Also try execCommand for React-based editors
277 try {
278 document.execCommand('selectAll', false, null);
279 document.execCommand('insertText', false, text);
280 } catch {}
281 return;
282 }
283
284 // Regular inputs and textareas
285 if (clear) {
286 el.value = '';
287 el.dispatchEvent(new Event('input', { bubbles: true }));
288 }
289
290 el.value = text;
291 el.dispatchEvent(new Event('input', { bubbles: true }));
292 el.dispatchEvent(new Event('change', { bubbles: true }));
293 }
294
295 function simulateKeyPress(el, key) {
296 el.focus();
297 const opts = { key, bubbles: true, cancelable: true };
298 el.dispatchEvent(new KeyboardEvent('keydown', opts));
299 el.dispatchEvent(new KeyboardEvent('keypress', opts));
300 el.dispatchEvent(new KeyboardEvent('keyup', opts));
301 }
302
303 async function executeAction(action) {
304 try {
305 switch (action.type) {
306 case 'click': {
307 const el = elementRegistry.get(action.elementId);
308 if (!el) return { success: false, error: `Element ${action.elementId} not found` };
309 simulateClick(el);
310 return { success: true, action: 'clicked', elementId: action.elementId };
311 }
312
313 case 'type': {
314 const el = elementRegistry.get(action.elementId);
315 if (!el) return { success: false, error: `Element ${action.elementId} not found` };
316 simulateType(el, action.text, action.clear !== false);
317 return { success: true, action: 'typed', elementId: action.elementId };
318 }
319
320 case 'keypress': {
321 const el = action.elementId
322 ? elementRegistry.get(action.elementId)
323 : document.activeElement;
324 if (!el) return { success: false, error: 'No target element' };
325 simulateKeyPress(el, action.key);
326 return { success: true, action: 'keypress', key: action.key };
327 }
328
329 case 'select': {
330 const el = elementRegistry.get(action.elementId);
331 if (!el) return { success: false, error: `Element ${action.elementId} not found` };
332 el.value = action.value;
333 el.dispatchEvent(new Event('change', { bubbles: true }));
334 return { success: true, action: 'selected', value: action.value };
335 }
336
337 case 'scroll': {
338 const amount = action.amount || 500;
339 const direction = action.direction || 'down';
340 const y = direction === 'up' ? -amount : amount;
341 window.scrollBy({ top: y, behavior: 'smooth' });
342 return { success: true, action: 'scrolled', direction, amount };
343 }
344
345 case 'navigate': {
346 window.location.href = action.url;
347 return { success: true, action: 'navigated', url: action.url };
348 }
349
350 case 'back': {
351 history.back();
352 return { success: true, action: 'back' };
353 }
354
355 case 'forward': {
356 history.forward();
357 return { success: true, action: 'forward' };
358 }
359
360 case 'wait': {
361 await new Promise(r => setTimeout(r, action.ms || 1000));
362 return { success: true, action: 'waited', ms: action.ms || 1000 };
363 }
364
365 case 'extract': {
366 // Extract text content from the page or a specific element
367 if (action.elementId) {
368 const el = elementRegistry.get(action.elementId);
369 if (!el) return { success: false, error: `Element ${action.elementId} not found` };
370 return { success: true, text: el.textContent.trim() };
371 }
372 // Extract all visible text
373 const text = document.body.innerText.slice(0, 10000);
374 return { success: true, text };
375 }
376
377 case 'screenshot': {
378 // Content scripts can't take screenshots — delegate to background
379 return { success: false, error: 'screenshot must be handled by background script' };
380 }
381
382 case 'comment': {
383 const text = action.text;
384 const replyTo = action.replyTo;
385 if (!text) return { success: false, error: 'text is required for comment action' };
386
387 let textbox = null;
388
389 // ── FIND OR OPEN A COMMENT BOX ──
390
391 if (replyTo) {
392 // Reply to specific user: find their comment, click its reply button
393 const cleanName = replyTo.replace(/^u\//, '').toLowerCase();
394 const links = document.querySelectorAll('a');
395 let scope = null;
396 for (const link of links) {
397 if ((link.href || '').toLowerCase().includes('/user/' + cleanName)) {
398 scope = link.closest('shreddit-comment, article, .comment');
399 if (scope) break;
400 }
401 }
402 if (scope) {
403 const row = scope.querySelector('shreddit-comment-action-row');
404 if (row) {
405 const btns = row.querySelectorAll('button');
406 if (btns.length >= 1) simulateClick(btns[0]);
407 await new Promise(r => setTimeout(r, 1500));
408 }
409 textbox = scope.querySelector('[contenteditable="true"], textarea');
410 }
411 }
412
413 if (!textbox) {
414 // Check for already open textbox (user clicked reply manually, or previous attempt)
415 const all = document.querySelectorAll('[contenteditable="true"], textarea');
416 for (const el of all) {
417 const r = el.getBoundingClientRect();
418 if (r.height > 30 && r.width > 100) { textbox = el; break; }
419 }
420 }
421
422 if (!textbox) {
423 // Nothing open. Click the first action row's first button (reply on first comment or post)
424 const row = document.querySelector('shreddit-comment-action-row');
425 if (row) {
426 const btns = row.querySelectorAll('button');
427 if (btns.length >= 1) {
428 simulateClick(btns[0]);
429 await new Promise(r => setTimeout(r, 1500));
430 }
431 }
432 // Find what appeared
433 const all = document.querySelectorAll('[contenteditable="true"], textarea');
434 for (const el of all) {
435 const r = el.getBoundingClientRect();
436 if (r.height > 30 && r.width > 100) { textbox = el; break; }
437 }
438 if (!textbox && all.length) textbox = all[all.length - 1];
439 }
440
441 if (!textbox) return { success: false, error: 'Could not find or open a comment box.' };
442
443 // ── TYPE TEXT ──
444
445 textbox.scrollIntoView({ behavior: 'instant', block: 'center' });
446 textbox.focus();
447 await new Promise(r => setTimeout(r, 300));
448
449 let typed = false;
450 try {
451 const sel = window.getSelection();
452 const range = document.createRange();
453 range.selectNodeContents(textbox);
454 sel.removeAllRanges();
455 sel.addRange(range);
456 typed = document.execCommand('insertText', false, text);
457 } catch {}
458
459 if (!typed) {
460 if (textbox.contentEditable === 'true') {
461 textbox.innerHTML = '<p>' + text + '</p>';
462 } else {
463 textbox.value = text;
464 }
465 textbox.dispatchEvent(new Event('input', { bubbles: true }));
466 }
467
468 await new Promise(r => setTimeout(r, 1000));
469 textbox.dispatchEvent(new Event('input', { bubbles: true }));
470 await new Promise(r => setTimeout(r, 500));
471
472 // ── VERIFY TEXT LANDED ──
473 const hasText = (textbox.textContent || '').includes(text) || (textbox.value || '').includes(text);
474 if (!hasText) {
475 // Text didn't register. Try direct content set as last resort.
476 if (textbox.contentEditable === 'true') {
477 textbox.innerHTML = '<p>' + text + '</p>';
478 } else {
479 textbox.value = text;
480 }
481 textbox.dispatchEvent(new Event('input', { bubbles: true }));
482 await new Promise(r => setTimeout(r, 1000));
483
484 // Check again
485 const hasTextRetry = (textbox.textContent || '').includes(text) || (textbox.value || '').includes(text);
486 if (!hasTextRetry) {
487 return { success: false, typed: false, submitted: false, error: 'Text did not register in comment box.' };
488 }
489 }
490
491 // ── CLICK SUBMIT ──
492 // Find the submit button near the textbox (not just any button on the page)
493
494 let submitBtn = null;
495 const submitWords = ['comment', 'reply', 'post', 'submit', 'send', 'save'];
496
497 // Search near the textbox first (parent, siblings, nearby containers)
498 let searchScope = textbox.parentElement;
499 for (let i = 0; i < 5 && searchScope && !submitBtn; i++) {
500 const spans = searchScope.querySelectorAll('button span[slot="content"], button span');
501 for (const span of spans) {
502 const t = span.textContent.trim().toLowerCase();
503 if (submitWords.includes(t)) {
504 submitBtn = span.closest('button');
505 break;
506 }
507 }
508 searchScope = searchScope.parentElement;
509 }
510
511 // Fallback: any button with type=submit
512 if (!submitBtn) submitBtn = document.querySelector('button[type="submit"]');
513
514 if (!submitBtn) return { success: true, typed: true, submitted: false, error: 'Typed but could not find submit button.' };
515
516 simulateClick(submitBtn);
517 return { success: true, typed: true, submitted: true, text };
518 }
519
520 default:
521 return { success: false, error: `Unknown action type: ${action.type}` };
522 }
523 } catch (err) {
524 return { success: false, error: err.message };
525 }
526 }
527
528 // ── Network Interception (optional) ─────────────────────────────
529
530 const interceptedRequests = [];
531 const MAX_INTERCEPTED = 50;
532
533 function installNetworkInterceptor() {
534 // Intercept fetch
535 const originalFetch = window.fetch;
536 window.fetch = async function (...args) {
537 const req = args[0];
538 const url = typeof req === 'string' ? req : req?.url;
539 const method = args[1]?.method || (typeof req === 'object' ? req.method : 'GET');
540
541 const entry = {
542 type: 'fetch',
543 url, method,
544 timestamp: Date.now(),
545 };
546
547 try {
548 const response = await originalFetch.apply(this, args);
549 entry.status = response.status;
550 interceptedRequests.push(entry);
551 if (interceptedRequests.length > MAX_INTERCEPTED) interceptedRequests.shift();
552 return response;
553 } catch (err) {
554 entry.error = err.message;
555 interceptedRequests.push(entry);
556 if (interceptedRequests.length > MAX_INTERCEPTED) interceptedRequests.shift();
557 throw err;
558 }
559 };
560
561 // Intercept XMLHttpRequest
562 const originalOpen = XMLHttpRequest.prototype.open;
563 const originalSend = XMLHttpRequest.prototype.send;
564
565 XMLHttpRequest.prototype.open = function (method, url) {
566 this._intercepted = { method, url, timestamp: Date.now(), type: 'xhr' };
567 return originalOpen.apply(this, arguments);
568 };
569
570 XMLHttpRequest.prototype.send = function () {
571 if (this._intercepted) {
572 this.addEventListener('load', () => {
573 this._intercepted.status = this.status;
574 interceptedRequests.push(this._intercepted);
575 if (interceptedRequests.length > MAX_INTERCEPTED) interceptedRequests.shift();
576 });
577 }
578 return originalSend.apply(this, arguments);
579 };
580 }
581
582 // ── Message Handler ─────────────────────────────────────────────
583
584 chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
585 (async () => {
586 switch (msg.type) {
587 case 'ping': {
588 sendResponse({ alive: true });
589 break;
590 }
591 case 'getPageState': {
592 const state = capturePageState();
593 // Include intercepted network calls if requested
594 if (msg.includeNetwork) {
595 state.networkRequests = [...interceptedRequests];
596 }
597 sendResponse(state);
598 break;
599 }
600
601 case 'executeAction': {
602 // If page state is stale, recapture before executing
603 if (msg.recapture) capturePageState();
604 const result = await executeAction(msg.action);
605 sendResponse(result);
606 break;
607 }
608
609 case 'getNetworkLog': {
610 sendResponse({ requests: [...interceptedRequests] });
611 break;
612 }
613
614 case 'installInterceptor': {
615 installNetworkInterceptor();
616 sendResponse({ success: true });
617 break;
618 }
619
620 case 'ping': {
621 sendResponse({ alive: true, url: location.href });
622 break;
623 }
624
625 default:
626 sendResponse({ error: 'Unknown message type' });
627 }
628 })();
629 return true; // Keep channel open for async response
630 });
631
632 // Auto-install network interceptor
633 installNetworkInterceptor();
634})();
6351const statusDot = document.getElementById('statusDot');
2const logView = document.getElementById('logView');
3const treeView = document.getElementById('treeView');
4const tabLog = document.getElementById('tabLog');
5const tabTree = document.getElementById('tabTree');
6let currentView = 'log';
7let logStarted = false;
8
9// Tab switching
10tabLog.addEventListener('click', () => {
11 currentView = 'log';
12 logView.style.display = 'block';
13 treeView.style.display = 'none';
14 tabLog.classList.add('active');
15 tabTree.classList.remove('active');
16});
17
18tabTree.addEventListener('click', async () => {
19 currentView = 'tree';
20 logView.style.display = 'none';
21 treeView.style.display = 'block';
22 tabLog.classList.remove('active');
23 tabTree.classList.add('active');
24 treeView.innerHTML = '<div class="empty-state"><div>Loading...</div></div>';
25 chrome.runtime.sendMessage({ type: 'manualCapture' }, (resp) => {
26 const state = resp?.state;
27 if (state?.error) {
28 treeView.innerHTML = `<div class="empty-state"><div>Error</div><div style="font-size:11px">${escHtml(state.error)}</div></div>`;
29 return;
30 }
31 const tree = state?.tree;
32 if (tree) {
33 treeView.innerHTML = '';
34 if (state.url) {
35 const header = document.createElement('div');
36 header.style.cssText = 'padding:4px 0 8px;font-size:11px;color:#888;border-bottom:1px solid #ffffff10;margin-bottom:8px;';
37 header.textContent = state.url;
38 treeView.appendChild(header);
39 }
40 renderTree(Array.isArray(tree) ? tree : [tree]);
41 } else {
42 treeView.innerHTML = '<div class="empty-state"><div>Could not load page tree</div><div style="font-size:11px">Try refreshing the tab, then click Page Tree again</div></div>';
43 }
44 });
45});
46
47// Manual actions
48document.getElementById('sendState').addEventListener('click', () => {
49 chrome.runtime.sendMessage({ type: 'manualCapture' }, () => {
50 addLog('state', 'Page state sent');
51 });
52});
53
54document.getElementById('sendScreenshot').addEventListener('click', () => {
55 chrome.runtime.sendMessage({ type: 'manualScreenshot' }, () => {
56 addLog('state', 'Screenshot sent');
57 });
58});
59
60// Logging
61function addLog(type, content) {
62 if (!logStarted) {
63 logView.innerHTML = '';
64 logStarted = true;
65 }
66 const entry = document.createElement('div');
67 entry.className = `log-entry ${type}`;
68 const now = new Date().toLocaleTimeString();
69 entry.innerHTML = `<div class="log-time">${now}</div><div class="log-content">${content}</div>`;
70 logView.appendChild(entry);
71 logView.scrollTop = logView.scrollHeight;
72}
73
74function addConfirmation(confirmId, description) {
75 if (!logStarted) { logView.innerHTML = ''; logStarted = true; }
76 const entry = document.createElement('div');
77 entry.className = 'log-entry confirm';
78 entry.id = `confirm-${confirmId}`;
79 const now = new Date().toLocaleTimeString();
80 entry.innerHTML = `
81 <div class="log-time">${now}</div>
82 <div class="log-content">Agent wants to: <code>${description}</code></div>
83 <div class="confirm-inline">
84 <button class="allow">Allow</button>
85 <button class="deny">Deny</button>
86 </div>
87 `;
88 entry.querySelector('.allow').onclick = () => {
89 chrome.runtime.sendMessage({ type: 'confirmActionResponse', confirmId, allowed: true });
90 entry.querySelector('.confirm-inline').innerHTML = '<span style="color:#40c060">Allowed</span>';
91 };
92 entry.querySelector('.deny').onclick = () => {
93 chrome.runtime.sendMessage({ type: 'confirmActionResponse', confirmId, allowed: false });
94 entry.querySelector('.confirm-inline').innerHTML = '<span style="color:#c04040">Denied</span>';
95 };
96 logView.appendChild(entry);
97 logView.scrollTop = logView.scrollHeight;
98}
99
100// Tree rendering
101function renderTree(nodes, depth) {
102 if (depth === undefined) { depth = 0; treeView.innerHTML = ''; }
103 for (const node of nodes) {
104 const div = document.createElement('div');
105 div.className = `tree-node${node.id ? ' interactive' : ''}`;
106 div.style.paddingLeft = `${depth * 16}px`;
107 let html = `<span class="role">${node.role}</span>`;
108 if (node.name) html += ` <span class="name">"${escHtml(node.name.slice(0, 60))}"</span>`;
109 if (node.id) html += ` <span class="id">[${node.id}]</span>`;
110 div.innerHTML = html;
111 treeView.appendChild(div);
112 if (node.children) renderTree(node.children, depth + 1);
113 }
114}
115
116function escHtml(s) {
117 return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
118}
119
120// Clear activity
121document.getElementById('clearLog').addEventListener('click', () => {
122 logView.innerHTML = '';
123 logStarted = false;
124});
125
126// Listen for updates
127chrome.runtime.onMessage.addListener((msg) => {
128 if (msg.type === 'stateUpdate') {
129 statusDot.className = `status-dot ${msg.connectionState}`;
130 if (msg.connectionState === 'connected') {
131 addLog('state', `Connected to <code>${msg.config.serverUrl}</code>`);
132 } else if (msg.connectionState === 'disconnected') {
133 addLog('error', 'Disconnected');
134 }
135 }
136
137 if (msg.type === 'confirmAction') {
138 addConfirmation(msg.confirmId, msg.description);
139 }
140
141 if (msg.type === 'activity') {
142 const e = msg.entry;
143 const type = e.action === 'action' ? 'action' : 'state';
144 let desc = '';
145 if (e.action === 'getPageState') desc = 'Read page state';
146 else if (e.action === 'screenshot') desc = 'Took screenshot';
147 else if (e.action === 'action') desc = `<code>${e.details.type}</code> ${e.details.target ? 'on <code>' + escHtml(String(e.details.target)) + '</code>' : ''} ${e.details.success ? '\u2713' : '\u2717'}`;
148 else desc = e.action;
149 addLog(type, desc);
150 }
151});
152
153// Initial state + replay activity log
154chrome.runtime.sendMessage({ type: 'getState' }, (resp) => {
155 if (resp) {
156 statusDot.className = `status-dot ${resp.connectionState}`;
157 if (resp.connectionState === 'connected') {
158 addLog('state', `Connected to <code>${resp.config?.serverUrl || 'server'}</code>`);
159 }
160 }
161});
162
163chrome.runtime.sendMessage({ type: 'getActivityLog' }, (resp) => {
164 if (resp?.log?.length) {
165 resp.log.forEach(e => {
166 const type = e.action === 'action' ? 'action' : 'state';
167 let desc = '';
168 if (e.action === 'getPageState') desc = 'Read page state';
169 else if (e.action === 'screenshot') desc = 'Took screenshot';
170 else if (e.action === 'action') desc = `<code>${e.details.type}</code> ${e.details.target ? 'on <code>' + escHtml(String(e.details.target)) + '</code>' : ''} ${e.details.success ? '\u2713' : '\u2717'}`;
171 else desc = e.action;
172 addLog(type, desc);
173 });
174 }
175});
176
| Version | Published | Downloads |
|---|---|---|
| 1.0.3 | 38d ago | 0 |
| 1.0.2 | 46d ago | 0 |
| 1.0.1 | 47d ago | 0 |
| 1.0.0 | 47d ago | 0 |
treeos ext star browser-bridge
Post comments from the CLI: treeos ext comment browser-bridge "your comment"
Max 3 comments per extension. One star and one flag per user.
Loading comments...