// ==UserScript== // @name Xfer Web Picker // @namespace pi.dotfiles // @version 1.14.1 // @description 元素拾取 + 备注批注 + broker 连接/send + 复制 handoff prompt + 页面工具只读采集(v1.14.1:fab 上的 marker 角标常驻显示——无 marker 时为灰色 0,仍可点开标注面板取目标/反查/网络/日志;v1.14.0:SSE 内容捕获——fetch text/event-stream 经 clone 旁路解析、EventSource 记录 message/命名事件,每流保留最近 50 条、逐条 8KB 尾部截断,SSE 专用 1MB 预算;network.sse op 分页读取事件内容;v1.13:Record 模式 ⇧⌥R 记时序事件与现场切片) // @match *://*/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_setClipboard // @grant unsafeWindow // @connect 127.0.0.1 // @run-at document-idle // ==/UserScript== /** * Xfer Web Picker — userscript: pick/note core + broker connection + send flow + page tools. * * Pick/note core (unchanged from v1.0.0): Shadow-DOM overlay UI, fab with drag + * pick entry, frozen pick mode with layer switching (and 1-9 batch picks), IME-safe * note card, note panel, per-item delete, hotkeys ⇧⌥P (pick) / ⇧⌥L (panel), and * framework source extraction (React/Vue dev builds → source file:line). * * Shift group (new in v1.5): ⇧Enter / ⇧click toggles the highlighted element into * a pending group (amber dashed marks, in-memory only — DOM refs can't persist); * Enter with a pending group opens the group note card → one shared note → every * member submits as a regular payloadFor record plus an optional `group` id string * linking members (solo picks never carry `group`, so the wire schema stays * backward compatible). Panel note edits sync across members of the same group. * * Draggable chrome + group reset (new in v1.11): the pick toolbar, note card * and note panel are drag-repositionable anywhere in the viewport (panel via * its header; positions persist per-tab in sessionStorage `pi.wp.barPos` / * `pi.wp.panelPos`). The toolbar now takes pointer events: tap 冻结 toggles * freeze, tap 退出 leaves pick mode, and a ⌫清空组合 chip appears while a * shift-group is pending (the Backspace key works too). Each amber group * mark's number badge is clickable and removes that one element from the * pending group. The panel's 清空 no longer asks for confirmation — * annotations are lightweight by design. * * Broker layer (new in v1.1; protocol v0.1 — NO token, localhost-trust model): * - v1.10: the send button becomes a split button group (发送 → + ▾ more * dropdown). The dropdown's 复制 handoff prompt asks the broker to render * the EXACT handoff doc a submit would deliver (annotation.compose — * broker-only info like the page-tool CLI path and follow-up channel * instructions are baked in) and copies it to the clipboard, so a non-pi * coding agent with bash access can receive the same handoff: paste it, * and it can call `node broker-main.ts page-tool ` itself. * The prompt textarea sends with ⌘/Ctrl+Enter. * - v1.9: picks are never deduped/overwritten (each pick is an independent * record); cssPath forces nth-of-type whenever same-tag siblings exist so * same-class table cells no longer collapse into one selector; every send * appends PAGE_QUERY_RULE telling the agent to do all reverse page.request * BEFORE touching code (HMR full reload breaks the userscript↔broker link). * - manual connect only: ws://127.0.0.1:4719/ws by default; broker URL (incl. * the port — relevant since the broker falls back to an ephemeral port when * 4719 is squatted by another program, see /xfer broker status) editable in * the settings modal (GM `wp.brokerUrl`); hello on open → welcome → green * status dot on the fab. Reconnect is NEVER automatic. Clicking the panel's * conn pill while off dials the saved/default URL first; only a failed * attempt opens the settings modal. * - send box at the bottom of the note panel: prompt textarea (optional — * empty sends DEFAULT_PROMPT: respond to each pick's note, or explain the * element's rendering) + searchable * target combobox fed by targets.list → targets.result (row = name + * `cwd · status`; type-to-filter, ↑↓/Enter 或点击选择), last-used * target persisted in GM `wp.lastTarget`, ⟳ manual refresh. * - annotation.submit (picks reuse the payloadFor schema verbatim) → ack toasts * the handoff_id, closes the note panel and clears the local batch; error * frames toast code + message and keep the panel open. * - page tools (v1.6): inbound page.request{tool:{op, params}} runs ONE of the * fixed read-only ops (page.info / dom.query / dom.html / console.logs / * network.log / framework.inspect) against this page and replies * page.response{ok:true, text:JSON}. No free-form eval, no human modal. * console.* + fetch/XHR captures are always-on ring buffers (200 entries * each, page-realm best-effort patch) so pre-request history is visible * to the agent. The v1.2 ask modal is REMOVED — agents confirm with the * user in their own session, not on the page. * - frames are built only through PROTOCOL constants + frame() builders — the * wire protocol lives in exactly one place, never written inline. * - page tools v1.14: SSE content capture. fetch responses whose content-type * is text/event-stream are cloned (the page keeps the original untouched) * and the clone is pumped by a background SSE frame parser; EventSource * wrappers record `message` plus named events (via a wrapped * addEventListener). Per stream: last 50 events, each tail-capped at 8KB; * a global SSE-only 1MB budget evicts oldest stream buffers first (an * evicted stream's record stays in netRing with sse.captured=false so the * metadata count survives). The new network.sse op returns event slices * (urlFilter/lastN/eventLast/maxCharsPerEvent) under the frame budget. * - fab badge v1.14.1: the marker count badge on the fab is always shown — * `0` in a dim slate style when the batch is empty and a lighter slate when * it has markers — no alarm red. Clicking it opens the note panel either way, * because targets / reverse lookup / network / logs are useful with no marker * collected. * * Storage contract — existing keys stay `pi.wp.*`; the two GM connection keys keep * the round-trial names (no `pi.` prefix) for continuity: * - pi.wp.picks sessionStorage per-tab pick batch (array of payloadFor * records; schema below must stay stable) * - pi.wp.fabPos sessionStorage fab position {x, y} * - pi.wp.barPos sessionStorage pick toolbar position {x, y} (after first drag) * - pi.wp.panelPos sessionStorage note panel position {x, y} (after first drag) * - pi.wp.debug GM storage debug flag (boolean; toggle at runtime via * window.__PI_WP_API__.setDebug(true|false)) * - wp.brokerUrl GM storage broker WS URL override (settings modal) * - wp.lastTarget GM storage last used local target name (persisted choice) * - wp.frameworkProps GM storage framework.inspect props/state opt-in (default off; * settings modal checkbox / __PI_WP_API__) * * Install (Tampermonkey): * 1. Open the Tampermonkey Dashboard → "+" (Create a new script). * 2. Replace the editor content with this entire file and save (Ctrl/Cmd+S). * 3. Reload any page: the round fab appears near the bottom-right corner. * ⇧⌥P = enter/exit pick mode, ⇧⌥L = toggle the note panel (send box inside); * in pick mode ⇧Enter/⇧click add elements to a group, Enter submits its note, * ⌫ (or the toolbar chip / a group mark's number badge) clears pending group * members; toolbar / note card / panel are all draggable. */ (() => { // web-picker.src/constants.js var KEY_PICKS = "pi.wp.picks"; var KEY_POS = "pi.wp.fabPos"; var KEY_BAR_POS = "pi.wp.barPos"; var KEY_PANEL_POS = "pi.wp.panelPos"; var KEY_REC = "pi.wp.rec"; var GM_DEBUG = "pi.wp.debug"; var HOST_FLAG = "data-pi-wp-host"; var MAX_DEPTH = 8; var HOTKEY = { code: "KeyP", alt: true, shift: true }; var HOTKEY_REC = { code: "KeyR", alt: true, shift: true }; var GM_BROKER = "wp.brokerUrl"; var GM_TARGET = "wp.lastTarget"; var GM_FPROPS = "wp.frameworkProps"; var GM_FAB_SIZE = "pi.wp.fabSize"; var DEFAULT_BROKER_URL = "ws://127.0.0.1:4719"; var CAPTURE_MAX = 200; var RESULT_MAX_CHARS = 5e5; var REC_MAX_EVENTS = 50; var REC_SLICE_MAX = 60; var SSE_BUDGET_CHARS = 1024 * 1024; var SSE_MAX_EVENTS = 50; var SSE_MSG_MAX_CHARS = 8e3; var DEFAULT_STYLE_PROPS = [ "display", "position", "color", "background-color", "font-size", "font-weight", "font-family", "line-height", "text-align", "overflow", "z-index", "opacity", "visibility", "width", "height", "margin", "padding", "border", "border-radius", "flex-direction", "gap" ]; // web-picker.src/storage.js var gm = { get(k, d) { try { const v = GM_getValue(k); return v === void 0 ? d : v; } catch (e) { return d; } }, set(k, v) { try { GM_setValue(k, v); } catch (e) { } } }; function debugLog(...args) { if (gm.get(GM_DEBUG, false) !== true) return; console.log("[pi.wp]", ...args); } function loadBatch() { try { const s = sessionStorage.getItem(KEY_PICKS); const b = s ? JSON.parse(s) : []; return Array.isArray(b) ? b : []; } catch (e) { return []; } } function saveBatch(b) { try { sessionStorage.setItem(KEY_PICKS, JSON.stringify(b)); } catch (e) { } } // wire.ts var WIRE_VERSION = 0; var WIRE = { V: WIRE_VERSION, KIND_HELLO: "hello", KIND_WELCOME: "welcome", KIND_ACK: "ack", KIND_ERROR: "error", KIND_SUBMIT: "annotation.submit", KIND_COMPOSE: "annotation.compose", KIND_TARGETS_LIST: "targets.list", KIND_TARGETS_RESULT: "targets.result", KIND_PAGE_REQUEST: "page.request", KIND_PAGE_RESPONSE: "page.response", NS_LOCAL: "local" }; var PAGE_OPS = { INFO: "page.info", DOM_QUERY: "dom.query", DOM_HTML: "dom.html", CONSOLE_LOGS: "console.logs", NETWORK_LOG: "network.log", SSE_LOG: "network.sse", FRAMEWORK_INSPECT: "framework.inspect", DOM_CLICK: "dom.click", DOM_SET_VALUE: "dom.setValue", PAGE_WAIT: "page.wait" }; var PAGE_OP_KINDS = { [PAGE_OPS.INFO]: "read", [PAGE_OPS.DOM_QUERY]: "read", [PAGE_OPS.DOM_HTML]: "read", [PAGE_OPS.CONSOLE_LOGS]: "read", [PAGE_OPS.NETWORK_LOG]: "read", [PAGE_OPS.SSE_LOG]: "read", [PAGE_OPS.FRAMEWORK_INSPECT]: "read", [PAGE_OPS.DOM_CLICK]: "write", [PAGE_OPS.DOM_SET_VALUE]: "write", [PAGE_OPS.PAGE_WAIT]: "read" }; // web-picker.src/protocol.js function frame(type, extra) { return Object.assign({ v: WIRE.V, type }, extra); } function frameHello() { return frame(WIRE.KIND_HELLO, { client: { ua: "tampermonkey", tab: { id: pageTabId(), url: location.href, title: document.title } } }); } function pageTabId() { const KEY = "pi.wp.tabId"; try { let id = sessionStorage.getItem(KEY); if (!id) { id = "tab-" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7); sessionStorage.setItem(KEY, id); } return id; } catch (e) { return "tab-" + Date.now().toString(36); } } function frameSubmit(id, prompt, targetName, record) { return frame(WIRE.KIND_SUBMIT, { id, page: { url: location.href, title: document.title }, picks: loadBatch(), prompt, ...record ? { record } : {}, target: { namespace: WIRE.NS_LOCAL, name: targetName } }); } function frameCompose(id, prompt, targetName, record) { return frame(WIRE.KIND_COMPOSE, { id, page: { url: location.href, title: document.title }, picks: loadBatch(), prompt, ...record ? { record } : {}, ...targetName ? { target: { namespace: WIRE.NS_LOCAL, name: targetName } } : {} }); } function frameTargetsList(id) { return frame(WIRE.KIND_TARGETS_LIST, { id }); } function framePageResponse(id, ok, payload) { return frame(WIRE.KIND_PAGE_RESPONSE, { id, ok, ...ok ? { text: payload } : { error: payload } }); } // web-picker.src/json-safe.js var JSON_SAFE_CAPS = { maxDepth: 5, maxStr: 1e5, maxArray: 500, maxKeys: 200 }; function jsonSafe(value) { let truncated = false; function walk(v, depth) { if (v === null || typeof v === "number" || typeof v === "boolean") return v; if (v === void 0) return null; if (typeof v === "bigint" || typeof v === "symbol") { truncated = true; return String(v); } if (typeof v === "function") { truncated = true; return "ƒ " + (v.name || "anonymous"); } if (typeof v === "string") { if (v.length > JSON_SAFE_CAPS.maxStr) { truncated = true; return v.slice(0, JSON_SAFE_CAPS.maxStr) + "…[truncated]"; } return v; } if (v instanceof Error) { return { name: v.name, message: v.message, stack: walk(v.stack == null ? "" : String(v.stack), depth + 1) }; } if (depth >= JSON_SAFE_CAPS.maxDepth) { truncated = true; return "[maxDepth]"; } if (Array.isArray(v)) { if (v.length > JSON_SAFE_CAPS.maxArray) truncated = true; return v.slice(0, JSON_SAFE_CAPS.maxArray).map((x) => walk(x, depth + 1)); } if (typeof v !== "object") { truncated = true; return String(v); } const out = {}; let keys; try { keys = Object.keys(v); } catch (e) { truncated = true; return "[uninspectable]"; } if (keys.length > JSON_SAFE_CAPS.maxKeys) truncated = true; for (const k of keys.slice(0, JSON_SAFE_CAPS.maxKeys)) { try { out[k] = walk(v[k], depth + 1); } catch (e) { truncated = true; out[k] = "[error]"; } } return out; } let text; try { text = JSON.stringify(walk(value, 0)); } catch (e) { truncated = true; text = '"[unserializable]"'; } return { text: text === void 0 ? "null" : text, truncated }; } // web-picker.src/capture.js var consoleRing = []; var netRing = []; function ringPush(ring, rec) { ring.push(rec); if (ring.length > CAPTURE_MAX) { const removed = ring.splice(0, ring.length - CAPTURE_MAX); for (const r of removed) { if (r && r.sseStream) sseEvict(r.sseStream); } } } var sseStreams = /* @__PURE__ */ new Map(); var sseSeq = 0; var sseBudgetUsed = 0; function sseRecord(rec) { const st = { id: "s" + ++sseSeq, rec, ring: [], chars: 0 }; rec.sse = { id: st.id, msgs: 0, chars: 0, captured: true, events: st.ring }; rec.sseStream = st; sseStreams.set(st.id, st); return st; } function ssePush(st, ev) { try { const full = typeof ev.data === "string" ? ev.data : ""; const text = full.length > SSE_MSG_MAX_CHARS ? full.slice(-SSE_MSG_MAX_CHARS) : full; const entry = { ts: Date.now(), type: ev.type || "message", data: text }; if (ev.id) entry.id = ev.id; st.ring.push(entry); if (st.ring.length > SSE_MAX_EVENTS) st.chars -= (st.ring.shift().data || "").length; st.chars += text.length; st.rec.sse.msgs++; st.rec.sse.chars += full.length; sseBudgetUsed += text.length; while (sseBudgetUsed > SSE_BUDGET_CHARS) { const oldest = sseStreams.values().next().value; if (!oldest || oldest === st) break; sseEvict(oldest); } } catch (e) { } } function sseEvict(st) { try { sseStreams.delete(st.id); sseBudgetUsed -= st.chars; const rec = st.rec; if (rec && rec.sse) { rec.sse.captured = false; rec.sse.events.length = 0; } if (rec && rec.sseStream === st) rec.sseStream = void 0; } catch (e) { } } function sseParseBlock(st, block) { let data = []; let type = "message"; let id; for (const line of block.split("\n")) { if (!line || line.startsWith(":")) continue; const eq = line.indexOf(":"); const field = eq === -1 ? line : line.slice(0, eq); const value = eq === -1 ? "" : line.slice(eq + 1).replace(/^ /, ""); if (field === "data") data.push(value); else if (field === "event") type = value; else if (field === "id") id = value; } if (data.length) ssePush(st, { type, id, data: data.join("\n") }); } async function pumpFetchSse(st, stream) { const decoder = new TextDecoder(); let buf = ""; try { const reader = stream.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); buf = buf.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); let idx; while ((idx = buf.indexOf("\n\n")) !== -1) { sseParseBlock(st, buf.slice(0, idx)); buf = buf.slice(idx + 2); } } const tail = buf.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); if (tail.trim()) sseParseBlock(st, tail); } catch (e) { } } function maybeCaptureSse(res, rec) { try { if (rec.kind !== "sse" || !res || !res.body || typeof res.clone !== "function") return; const clone = res.clone(); if (!clone.body) return; const st = sseRecord(rec); void pumpFetchSse(st, clone.body); } catch (e) { } } function captureRealm() { try { return typeof unsafeWindow !== "undefined" && unsafeWindow ? unsafeWindow : window; } catch (e) { return window; } } function fmtCaptureArg(v) { try { if (typeof v === "string") return v; if (v instanceof Error) return v.stack || String(v); const t2 = jsonSafe(v).text; return t2.length > 400 ? t2.slice(0, 400) + "…" : t2; } catch (e) { return String(v); } } function installCapture() { const realm = captureRealm(); try { for (const level of ["debug", "log", "info", "warn", "error"]) { const original = realm.console && typeof realm.console[level] === "function" ? realm.console[level] : null; if (!original) continue; realm.console[level] = function(...args) { try { const first = typeof args[0] === "string" ? args[0] : ""; if (!first.startsWith("[pi.wp]")) { const rec = { level, text: args.map(fmtCaptureArg).join(" "), ts: Date.now() }; if (level === "error" && args[0] instanceof Error && args[0].stack) rec.stack = String(args[0].stack).slice(0, 2e3); ringPush(consoleRing, rec); } } catch (e) { } return original.apply(this === void 0 ? realm.console : this, args); }; } } catch (e) { } try { realm.addEventListener("error", (ev) => { try { const err = ev && ev.error; ringPush(consoleRing, { level: "error", text: ev && ev.message || "window.onerror", stack: err && err.stack ? String(err.stack).slice(0, 2e3) : void 0, ts: Date.now() }); } catch (e) { } }); realm.addEventListener("unhandledrejection", (ev) => { try { const reason = ev && ev.reason; ringPush(consoleRing, { level: "error", text: "unhandledrejection: " + fmtCaptureArg(reason), stack: reason && reason.stack ? String(reason.stack).slice(0, 2e3) : void 0, ts: Date.now() }); } catch (e) { } }); } catch (e) { } try { const origFetch = realm.fetch; if (typeof origFetch === "function") { realm.fetch = function(...args) { const started = Date.now(); const req = args[0]; const url = typeof req === "string" ? req : req && req.url || ""; const method = args[1] && args[1].method || req && req.method || "GET"; const rec = { method: String(method), url: String(url).slice(0, 500), ts: started, pending: true }; ringPush(netRing, rec); const done = (res, error) => { try { if (error) { rec.status = 0; rec.error = String(error && error.message || error).slice(0, 200); rec.pending = false; } else { rec.status = res && res.status; let ct = ""; try { ct = String(res && res.headers && res.headers.get("content-type") || ""); } catch (e) { } if (ct.includes("text/event-stream")) { rec.kind = "sse"; } else rec.pending = false; } rec.durationMs = Date.now() - started; } catch (e) { } }; return origFetch.apply(this, args).then( (res) => { done(res); maybeCaptureSse(res, rec); return res; }, (err) => { done(null, err); throw err; } ); }; } } catch (e) { } try { const ES = realm.EventSource; if (typeof ES === "function") { realm.EventSource = function(url, cfg) { const es = new ES(url, cfg); try { const rec = { method: "GET", url: String(url || "").slice(0, 500), status: "pending", ts: Date.now(), pending: true, kind: "sse", msgs: 0 }; ringPush(netRing, rec); const st = sseRecord(rec); es.addEventListener("open", () => { rec.status = es.status || 200; }); es.addEventListener("message", (ev) => { rec.msgs++; ssePush(st, { type: "message", id: ev && ev.lastEventId, data: ev && ev.data }); }); es.addEventListener("error", () => { if (es.readyState === 2) rec.pending = false; }); const origAEL = es.addEventListener.bind(es); const seenNamed = /* @__PURE__ */ new Set(); es.addEventListener = (type, cb, opts) => { if (typeof type === "string" && type !== "message" && type !== "open" && type !== "error" && !seenNamed.has(type)) { seenNamed.add(type); try { origAEL(type, (ev) => { rec.msgs++; ssePush(st, { type, id: ev && ev.lastEventId, data: ev && ev.data }); }, opts); } catch (e) { } } return origAEL(type, cb, opts); }; } catch (e) { } return es; }; realm.EventSource.prototype = ES.prototype; } } catch (e) { } try { const XHR = realm.XMLHttpRequest; if (XHR && XHR.prototype) { const origOpen = XHR.prototype.open; const origSend = XHR.prototype.send; if (typeof origOpen === "function" && typeof origSend === "function") { XHR.prototype.open = function(method, url) { try { this.__wpNet = { method: String(method || "GET"), url: String(url || "").slice(0, 500), started: 0 }; } catch (e) { } return origOpen.apply(this, arguments); }; XHR.prototype.send = function() { let meta = null; try { meta = this.__wpNet || null; } catch (e) { } if (!meta) meta = { method: "GET", url: "", started: Date.now() }; meta.started = Date.now(); try { meta.rec = { method: meta.method, url: meta.url, ts: meta.started, pending: true }; ringPush(netRing, meta.rec); this.addEventListener("loadend", () => { try { const rec = meta.rec; if (!rec) return; rec.status = this.status; rec.durationMs = Date.now() - (meta.started || Date.now()); rec.pending = false; } catch (e) { } }); } catch (e) { } return origSend.apply(this, arguments); }; } } } catch (e) { } } // web-picker.src/dom-utils.js var cssEscape = (s) => window.CSS && CSS.escape ? CSS.escape(s) : String(s).replace(/[^a-zA-Z0-9_-]/g, (c) => "\\" + c); function cssPath(el) { if (el.id) return "#" + cssEscape(el.id); const parts = []; let cur = el, depth = 0; while (cur && cur.nodeType === 1 && cur !== document.documentElement && depth < MAX_DEPTH) { let sel = cur.nodeName.toLowerCase(); if (cur.id) { parts.unshift("#" + cssEscape(cur.id)); break; } const sibs = cur.parentElement ? Array.from(cur.parentElement.children).filter((s) => s.nodeName === cur.nodeName) : [cur]; const nth = sibs.length > 1 ? ":nth-of-type(" + (sibs.indexOf(cur) + 1) + ")" : ""; const stable = ["data-testid", "data-test", "data-component", "data-cy", "name"].find((a) => cur.getAttribute && cur.getAttribute(a)); if (stable) { parts.unshift(sel + "[" + stable + '="' + cssEscape(String(cur.getAttribute(stable))) + '"]' + nth); } else { const cls = Array.from(cur.classList || []).filter((c) => c.length > 1).slice(0, 2); if (cls.length) { parts.unshift(sel + "." + cls.map(cssEscape).join(".") + nth); } else { parts.unshift(sel + nth); } } cur = cur.parentElement; depth++; } return parts.join(" > ") || el.nodeName.toLowerCase(); } function xPath(el) { if (el.id) return '//*[@id="' + el.id + '"]'; const parts = []; let cur = el; while (cur && cur.nodeType === 1 && cur !== document.documentElement) { let i = 1, sib = cur; while (sib = sib.previousElementSibling) { if (sib.nodeName === cur.nodeName) i++; } parts.unshift(cur.nodeName.toLowerCase() + "[" + i + "]"); cur = cur.parentElement; } return "/" + parts.join("/"); } function nearestScrollable(el) { let cur = el; while (cur && cur.nodeType === 1) { const cs = getComputedStyle(cur); if (/(auto|scroll|overlay)/.test(cs.overflowY)) return cur; cur = cur.parentElement; } return null; } function selectorPreview(el) { let s = el.tagName.toLowerCase(); if (el.id) s += "#" + el.id; if (el.classList && el.classList.length) s += "." + Array.from(el.classList).slice(0, 2).join("."); return s; } var escapeHtml = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]); // web-picker.src/page-tools.js function writeOpsKey(origin) { return "wp.writeOps." + origin; } function createPageTools(env) { const e = env || {}; const gm2 = e.gm; const doc = e.doc || document; const debugLog2 = e.debugLog || (() => { }); const rings = { consoleRing: e.consoleRing || consoleRing, netRing: e.netRing || netRing }; function writeAllowed() { return !!(gm2 && gm2.get(writeOpsKey(location.origin), false) === true); } function findOutside(selector) { if (typeof selector !== "string" || !selector) throw new Error("params.selector (CSS) is required"); const el = doc.querySelector(selector); if (!el) return null; if (el.closest && el.closest("[" + HOST_FLAG + "]")) { throw new Error("refused: target is the picker overlay itself"); } return el; } function toolPageInfo() { return { url: location.href, title: document.title, readyState: document.readyState, viewport: { w: window.innerWidth, h: window.innerHeight }, dpr: window.devicePixelRatio, scroll: { x: window.scrollX, y: window.scrollY }, ua: navigator.userAgent, language: navigator.language, cookiesEnabled: navigator.cookieEnabled, ts: Date.now() }; } function elToolInfo(el, styleProps) { const r = el.getBoundingClientRect(); const cs = getComputedStyle(el); const style = {}; for (const p of styleProps) style[p] = cs.getPropertyValue(p); const attributes = {}; for (const a of el.attributes) attributes[a.name] = a.value; return { selector: cssPath(el), xpath: xPath(el), tagName: el.tagName.toLowerCase(), id: el.id || void 0, classes: Array.from(el.classList || []), attributes, textPreview: (el.textContent || "").trim().replace(/\s+/g, " ").slice(0, 200), rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) }, visible: !!(r.width || r.height) && cs.visibility !== "hidden" && cs.display !== "none", style }; } function toolDomQuery(params) { const selector = typeof params.selector === "string" ? params.selector : ""; if (!selector) throw new Error("dom.query: params.selector (CSS) is required"); const maxCount = Math.min(50, Math.max(1, Number(params.maxCount) || 10)); const styleProps = Array.isArray(params.styleProps) && params.styleProps.length ? params.styleProps.filter((s) => typeof s === "string").slice(0, 30) : DEFAULT_STYLE_PROPS; const all = doc.querySelectorAll(selector); const nodes = Array.from(all).slice(0, maxCount); return { selector, matched: all.length, returned: nodes.length, elements: nodes.map((el) => elToolInfo(el, styleProps)) }; } function pruneClone(el, depth) { const clone = el.cloneNode(false); if (depth > 1) { for (const child of Array.from(el.childNodes)) { try { if (child.nodeType === 3) { const t2 = (child.textContent || "").trim(); if (t2) clone.appendChild(doc.createTextNode(t2.slice(0, 80) + " ")); } else if (child.nodeType === 1) { clone.appendChild(pruneClone(child, depth - 1)); } } catch (err) { } } } else if (el.childNodes && el.childNodes.length) { clone.appendChild(doc.createTextNode("…")); } return clone; } function toolDomHtml(params) { const selector = typeof params.selector === "string" ? params.selector : "body"; const maxLength = Math.min(2e5, Math.max(200, Number(params.maxLength) || 2e4)); const maxDepth = Math.max(1, Math.min(20, Number(params.maxDepth) || 8)); const target = doc.querySelector(selector); if (!target) throw new Error("dom.html: no element matches " + selector); let html; try { html = pruneClone(target, maxDepth).outerHTML; } catch (err) { throw new Error("dom.html: serialization failed on " + selector + " (" + (err && err.message ? err.message : String(err)) + ")"); } let truncated = false; if (html.length > maxLength) { html = html.slice(0, maxLength); truncated = true; } return { selector, maxDepth, length: html.length, truncated, html }; } function toolConsoleLogs(params) { const lastN = Math.min(500, Math.max(1, Number(params.lastN) || 50)); const sinceTs = typeof params.sinceTs === "number" ? params.sinceTs : 0; const level = typeof params.level === "string" ? params.level : null; const all = rings.consoleRing.filter((en) => en.ts >= sinceTs && (!level || en.level === level)); return { total: all.length, returned: Math.min(lastN, all.length), entries: all.slice(-lastN) }; } function toolNetworkLog(params) { const lastN = Math.min(500, Math.max(1, Number(params.lastN) || 50)); const filter = typeof params.urlFilter === "string" ? params.urlFilter.toLowerCase() : null; const all = rings.netRing.filter((en) => !filter || en.url.toLowerCase().includes(filter)); return { total: all.length, returned: Math.min(lastN, all.length), entries: all.slice(-lastN) }; } function toolSseLog(params) { const urlFilter = typeof params.urlFilter === "string" ? params.urlFilter.toLowerCase() : null; const lastN = Math.min(20, Math.max(1, Number(params.lastN) || 5)); const eventLast = Math.min(SSE_MAX_EVENTS, Math.max(1, Number(params.eventLast) || 10)); const maxChars = Math.min(SSE_MSG_MAX_CHARS, Math.max(64, Number(params.maxCharsPerEvent) || 2e3)); const all = rings.netRing.filter((en) => en && en.sse && (!urlFilter || en.url.toLowerCase().includes(urlFilter))); const streams = all.slice(-lastN).map((en) => { const s = en.sse; return { stream: s.id, url: en.url, method: en.method, status: en.status, pending: en.pending, msgs: s.msgs, chars: s.chars, captured: s.captured, events: s.captured ? s.events.slice(-eventLast).map((ev) => ({ ts: ev.ts, type: ev.type, ...ev.id !== void 0 ? { id: ev.id } : {}, data: ev.data.length > maxChars ? ev.data.slice(-maxChars) : ev.data })) : [] }; }); return { total: all.length, returned: streams.length, streams }; } function chainTargetEl(el) { try { const uw = typeof unsafeWindow !== "undefined" ? unsafeWindow : null; if (!uw || !uw.document) return el; const pageEl = uw.document.querySelector(cssPath(el)); return pageEl || el; } catch (err) { return el; } } function reactChain(el, maxDepth, withProps) { const chain = []; try { for (const k of Object.getOwnPropertyNames(el)) { if (!k.startsWith("__reactFiber$") && !k.startsWith("__reactInternalInstance$")) continue; let f = el[k], guard = 0; while (f && guard++ < 200 && chain.length < maxDepth) { const t2 = f.type; if (t2 && (t2.name || t2.displayName)) { const src = f._debugSource; const level = { component: t2.displayName || t2.name || "" }; if (src && src.fileName) { level.file = src.fileName; level.line = src.lineNumber || 0; } if (withProps && f.memoizedProps !== void 0) level.props = f.memoizedProps; chain.push(level); } f = f.return; } if (chain.length) break; } } catch (err) { } return chain; } function vueChain(el, maxDepth, withProps) { const chain = []; try { let vm = el.__vueParentComponent || el.__vue__ || null, guard = 0; while (vm && guard++ < 200 && chain.length < maxDepth) { const opts = vm.$options || {}; const name = vm.type && (vm.type.name || vm.type.__name) || opts.name || opts.__name || ""; if (opts.__file || name) { const level = { component: name }; if (opts.__file) level.file = opts.__file; if (withProps && vm.$props !== void 0) level.props = vm.$props; chain.push(level); } vm = vm.$parent || null; } } catch (err) { } return chain; } function toolFrameworkInspect(params) { const selector = typeof params.selector === "string" ? params.selector : ""; if (!selector) throw new Error("framework.inspect: params.selector (CSS) is required"); const el = doc.querySelector(selector); if (!el) throw new Error("framework.inspect: no element matches " + selector); const maxDepth = Math.max(1, Math.min(10, Number(params.maxDepth) || 5)); const withProps = params.props !== void 0 ? !!params.props : gm2 ? gm2.get(GM_FPROPS, false) === true : false; const target = chainTargetEl(el); let chain = reactChain(target, maxDepth, withProps); let framework = chain.length ? "react" : null; if (!chain.length) { chain = vueChain(target, maxDepth, withProps); framework = chain.length ? "vue" : null; } return { selector, framework, withProps, depth: chain.length, chain }; } function toolDomClick(params) { const el = findOutside(params.selector); if (!el) throw new Error("dom.click: no element matches " + params.selector); const r = el.getBoundingClientRect(); const opts = { bubbles: true, cancelable: true, view: window, clientX: r.left + r.width / 2, clientY: r.top + r.height / 2, button: 0 }; for (const type of ["pointerdown", "mousedown", "pointerup", "mouseup", "click"]) { const Ctor = type.startsWith("pointer") ? PointerEvent : MouseEvent; try { el.dispatchEvent(new Ctor(type, opts)); } catch (err) { el.dispatchEvent(new MouseEvent(type, opts)); } } el.click(); return { selector: cssPath(el), clicked: true, tagName: el.tagName.toLowerCase(), disabled: !!el.disabled }; } function toolDomSetValue(params) { const el = findOutside(params.selector); if (!el) throw new Error("dom.setValue: no element matches " + params.selector); if (!("value" in el)) throw new Error("dom.setValue: element has no value property (" + el.tagName.toLowerCase() + ")"); const value = params.value === void 0 || params.value === null ? "" : String(params.value); const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : el instanceof HTMLSelectElement ? HTMLSelectElement.prototype : HTMLInputElement.prototype; const setter = Object.getOwnPropertyDescriptor(proto, "value").set; setter.call(el, value); el.dispatchEvent(new Event("input", { bubbles: true })); el.dispatchEvent(new Event("change", { bubbles: true })); return { selector: cssPath(el), value, tagName: el.tagName.toLowerCase() }; } function toolPageWait(params) { const selector = typeof params.selector === "string" ? params.selector : ""; if (!selector) throw new Error("page.wait: params.selector (CSS) is required"); if (doc.querySelector(selector)) return { selector, found: true, waitedMs: 0 }; throw new Error("page.wait: no element matches " + selector + " yet — retry shortly (e.g. every 1-2s, ≤ retry budget)"); } const PAGE_TOOLS = { [PAGE_OPS.INFO]: toolPageInfo, [PAGE_OPS.DOM_QUERY]: toolDomQuery, [PAGE_OPS.DOM_HTML]: toolDomHtml, [PAGE_OPS.CONSOLE_LOGS]: toolConsoleLogs, [PAGE_OPS.NETWORK_LOG]: toolNetworkLog, [PAGE_OPS.SSE_LOG]: toolSseLog, [PAGE_OPS.FRAMEWORK_INSPECT]: toolFrameworkInspect, [PAGE_OPS.DOM_CLICK]: toolDomClick, [PAGE_OPS.DOM_SET_VALUE]: toolDomSetValue, [PAGE_OPS.PAGE_WAIT]: toolPageWait }; function handlePageToolRequest(f, send) { const tool = f.tool && typeof f.tool === "object" && !Array.isArray(f.tool) ? f.tool : null; if (!tool || typeof tool.op !== "string") { send(f.id, false, "invalid_tool: missing tool.op"); return; } const handler = PAGE_TOOLS[tool.op]; if (!handler) { send(f.id, false, "unknown_op: " + tool.op + " (available: " + Object.keys(PAGE_TOOLS).join(", ") + ")"); return; } if (PAGE_OP_KINDS[tool.op] === "write" && !writeAllowed()) { send(f.id, false, "denied_op: " + tool.op + " requires page-write authorization for " + location.origin + " (enable it in the picker settings modal)"); return; } let text; try { const params = tool.params && typeof tool.params === "object" && !Array.isArray(tool.params) ? tool.params : {}; debugLog2("page-tool op:", tool.op, JSON.stringify(params).slice(0, 200)); text = jsonSafe(handler(params)).text; } catch (err) { send(f.id, false, err && err.message ? String(err.message) : String(err)); return; } if (text.length > RESULT_MAX_CHARS) { send(f.id, false, "result_too_large"); return; } send(f.id, true, text); } return { PAGE_TOOLS, handlePageToolRequest, writeAllowed }; } // web-picker.src/i18n.js var GM_LANG = "pi.wp.lang"; var STRINGS = { "menu.connect": { en: "Connect broker", zh: "连接 broker" }, "menu.disconnect": { en: "Disconnect broker", zh: "断开 broker" }, "menu.settings": { en: "Connection settings…", zh: "连接设置…" }, "menu.revokeAuto": { en: "Revoke auto-connect for this page", zh: "撤销本页自动连接" }, "menu.openPanel": { en: "Open pick panel (⇧⌥L)", zh: "打开标注面板 (⇧⌥L)" }, "menu.startPick": { en: "Start picking (⇧⌥P)", zh: "开始拾取 (⇧⌥P)" }, "menu.toggleRecord": { en: "Start/stop recording (⇧⌥R)", zh: "开始/停止录制 (⇧⌥R)" }, "menu.reinject": { en: "Re-inject trigger", zh: "重新注入 trigger" }, "menu.lang": { en: "Language → 中文", zh: "语言 → English" }, "menu.fabSize": { en: "Fab size → large", zh: "悬浮按钮 → 大" }, "toast.autoRevoked": { en: "Auto-connect revoked for {origin}", zh: "已撤销 {origin} 的自动连接授权" }, "toast.reinjected": { en: "Trigger re-injected", zh: "trigger 已重新注入" }, "toast.reinjectStuck": { en: "Trigger still on the page", zh: "trigger 仍在页面上" }, "toast.langSwitched": { en: "Language set to English — reload the page to apply", zh: "语言已设为中文 — 刷新页面生效" }, "toast.fabSize": { en: "Fab size: {size}", zh: "悬浮按钮大小:{size}" }, "bar.cycle": { en: "cycle layer", zh: "切层" }, "bar.jump": { en: "jump", zh: "跳层" }, "bar.select": { en: "select", zh: "选中" }, "bar.group": { en: "add/remove group", zh: "加组/移出" }, "bar.clearGroup": { en: "clear group", zh: "清空组合" }, "gclear.title": { en: "Clear pending group (or press ⌫)", zh: "清空待处理组合(也可按 ⌫)" }, "bar.freeze": { en: "freeze", zh: "冻结" }, "escx.title": { en: "Exit pick mode", zh: "退出拾取模式" }, "bar.exit": { en: "exit", zh: "退出" }, "fab.title": { en: "pi xfer — pick, record, hand off", zh: "pi xfer — 拾取、录制、交接" }, "recbadge.title": { en: "Open pick panel", zh: "打开标注面板" }, "recbadge": { en: "panel", zh: "面板" }, "recstop": { en: "■ Stop", zh: "■ 停止" }, "recdiscard": { en: "Discard", zh: "丢弃" }, "card.placeholder": { en: "Note (optional, Enter to submit)", zh: "备注(可选,留空直接回车提交)" }, "card.cancel": { en: "Cancel Esc", zh: "取消 Esc" }, "card.ok": { en: "✓ OK Enter", zh: "✓ 确认 Enter" }, "card.groupPlaceholder": { en: "Group note (optional, shared by the whole group)", zh: "组备注(可选,整组共用这一条)" }, "card.groupOk": { en: "✓ Submit {n} Enter", zh: "✓ 提交 {n} 项 Enter" }, "panel.title": { en: "Picks", zh: "已选备注" }, "panel.empty": { en: "No elements picked yet", zh: "还没有选中任何元素" }, "panel.pclose.title": { en: "Close Esc", zh: "关闭 Esc" }, "count.items": { en: "{n}", zh: "{n} 条" }, "group.label": { en: "⧉ group {g}", zh: "⧉ 组 {g}" }, "note.placeholder": { en: "Note… (auto-saves on blur)", zh: "备注…(失焦自动保存)" }, "btn.delete": { en: "Delete", zh: "删除" }, "pick.added": { en: "Picked {sel}", zh: "已选中 {sel}" }, "pick.deleted": { en: "Deleted {sel}", zh: "已删除 {sel}" }, "pick.cleared": { en: "Cleared {n} picks", zh: "已清空 {n} 条标注" }, "pick.layers": { en: "{n}", zh: "{n}层" }, "group.added": { en: "Added to group ({n} total)", zh: "已加入组合(共 {n} 项)" }, "group.removed": { en: "Removed from group ({n} left)", zh: "已移出组合(剩 {n} 项)" }, "group.cleared": { en: "Pending group cleared ({n} items)", zh: "已清空待处理组合({n} 项)" }, "group.tagTitle": { en: "Click to remove from group", zh: "点击移出组合" }, "group.selected": { en: "Picked {n} · group {gid}", zh: "已选中 {n} 项 · 组 {gid}" }, "gh.note": { en: " · Entergroup note ({n})", zh: " · Enter组备注({n})" }, "note.saved": { en: "Note saved", zh: "备注已保存" }, "note.synced": { en: " (synced to {n} in group)", zh: "(已同步组内 {n} 项)" }, "record.deleted": { en: "Record deleted", zh: "已删除操作记录" }, "record.summary": { en: "⏺ Record {id} · {n} events", zh: "⏺ 记录 {id} · {n} 事件" }, "record.started": { en: "Recording started · interact with the page, ⇧⌥R to stop", zh: "录制开始 · 操作页面,⇧⌥R 停止" }, "record.discarded": { en: "Recording discarded", zh: "录制已丢弃" }, "record.done": { en: "Recording done · {n} events · open the panel to send", zh: "录制完成 · {n} 事件 · 打开面板发送" }, "send.label": { en: "Send to agent (⌘/Ctrl+Enter to send · prompt optional)", zh: "发送到 agent(⌘/Ctrl+Enter 发送 · prompt 可留空)" }, "prompt.placeholder": { en: "What should the agent do? Leave empty = respond to each pick’s note / explain the element’s rendering", zh: "要让 agent 做什么?留空 = 回应各标注的 note / 解释元素渲染逻辑" }, "target.label": { en: "Target", zh: "目标" }, "tinput.placeholder": { en: "broker not connected", zh: "未连接 broker" }, "trefresh.title": { en: "Refresh target list", zh: "刷新 target 列表" }, "conn.off": { en: "Not connected · click to connect", zh: "未连接 · 点击连接" }, "conn.on": { en: "broker connected", zh: "broker 已连接" }, "conn.connecting": { en: "Connecting…", zh: "连接中…" }, "conn.blocked": { en: "Blocked by site policy", zh: "被站点策略拦截" }, "pill.disconnect.title": { en: "Click to disconnect broker", zh: "点击断开 broker" }, "pill.off.title": { en: "Click to connect broker (opens settings on failure)", zh: "点击连接 broker(失败会打开连接设置)" }, "pill.blocked.title": { en: "This site's Content Security Policy blocks the local broker (connect-src) — retrying cannot succeed here; copy the handoff prompt instead", zh: "本站 CSP(connect-src)拦截了本地 broker — 重试也不会成功;请改用复制 handoff prompt" }, "clearbtn": { en: "Clear", zh: "清空" }, "sendbtn": { en: "Send →", zh: "发送 →" }, "sendmore.title": { en: "More actions", zh: "更多操作" }, "copyprompt": { en: "⧉ Copy handoff prompt", zh: "⧉ 复制 handoff prompt" }, "copyprompt.sub": { en: "broker composes the full handoff doc → clipboard (send to any agent with bash)", zh: "broker 拼好完整 handoff 文档 → 剪贴板(发给任意有 bash 的 agent)" }, // degraded-state button: deliberately short — the status line already says what // gets copied, and the full label overflowed the panel next to a long status. "copyprompt.offline": { en: "⧉ Copy", zh: "⧉ 复制" }, "settings.title": { en: "Connection settings", zh: "连接设置" }, "settings.burl": { en: "BROKER URL (WS)", zh: "BROKER 地址(WS)" }, "settings.sprops": { en: "framework.inspect includes component props/state (default off)", zh: "framework.inspect 附带组件 props/state(默认关)" }, "settings.sauto": { en: "Auto-connect broker on this domain (authorized after one manual connect; silently reconnects on reload)", zh: "本域名自动连接 broker(手动连上过一次即授权,页面刷新后静默重连)" }, "settings.swrite": { en: "Allow agent to operate this page (dom.click / dom.setValue, default off)", zh: "允许 agent 操作本页(dom.click / dom.setValue,默认关)" }, "settings.cancel": { en: "Cancel", zh: "取消" }, "settings.save": { en: "Save & connect", zh: "保存并连接" }, "settings.toast.on": { en: "on", zh: "已开启" }, "settings.toast.off": { en: "off", zh: "已关闭" }, "settings.toast.revoked": { en: "revoked", zh: "已撤销" }, "settings.toast.fprops": { en: "framework.inspect props/state {state}", zh: "framework.inspect props/state {state}" }, "settings.toast.autoLink": { en: "Auto-connect on this page {state}", zh: "本页自动连接 {state}" }, "settings.toast.write": { en: "Agent page-write permission {state}", zh: "agent 页面操作权限 {state}" }, "settings.toast.saved": { en: "Saved, connecting…", zh: "已保存,连接中…" }, "fz.on.title": { en: "Frozen: page interaction blocked, overlays won’t close on click/hover (click to toggle)", zh: "冻结:页面交互已屏蔽,浮层不会因点击/hover 关闭(点击此提示可切换)" }, "fz.off.title": { en: "Live: hover can trigger the page (open submenus etc.) (click to toggle)", zh: "实时:hover 可触发页面(展开子菜单等)(点击此提示可切换)" }, "fz.on": { en: "Frozen: page interaction blocked", zh: "冻结:页面交互已屏蔽" }, "fz.off": { en: "Live: hover can trigger the page", zh: "实时:hover 可触发页面" }, "combo.noSessions": { en: "(no active local sessions)", zh: "(无活跃 local session)" }, "combo.none": { en: "no active local sessions", zh: "无活跃 local session" }, "combo.noMatch": { en: "No matching target", zh: "无匹配目标" }, "combo.hint": { en: "Type to filter · ↑↓ select · Enter confirm · Esc close", zh: "输入过滤 · ↑↓ 选择 · Enter 确认 · Esc 关闭" }, "combo.switched": { en: "Target switched: {name}", zh: "目标已切换:{name}" }, "combo.refreshed": { en: "Target list refreshed ({n})", zh: "目标列表已刷新({n})" }, "combo.noneFound": { en: "No active local sessions found", zh: "没有发现活跃 local session" }, "send.needContent": { en: "Pick elements, record actions, or write a prompt first", zh: "先标注元素、录制操作、或写 prompt" }, "send.composing": { en: "Requesting full handoff prompt from broker…", zh: "正在向 broker 请求完整 handoff prompt…" }, "send.composeFailed": { en: "Failed to compose prompt: {detail}", zh: "拼 prompt 失败: {detail}" }, "send.clipboard": { en: "Clipboard unavailable (needs GM_setClipboard or https/localhost)", zh: "剪贴板不可用(需 GM_setClipboard 或 https/localhost)" }, "send.copied": { en: "Handoff prompt copied ({n} chars · paste to any agent)", zh: "handoff prompt 已复制({n} 字符 · 可粘贴给任意 agent)" }, "send.copiedOffline": { en: "Handoff prompt composed in this tab and copied ({n} chars · no broker link, so no follow-up channel)", zh: "已在本页内拼好 handoff prompt 并复制({n} 字符 · 未连 broker,无回查通道)" }, "send.noTarget": { en: "No target available: run xfer listen in a pi session first", zh: "没有可用目标:先启动 pi session 的 xfer listen" }, "send.noTargetOffline": { en: "No broker link — copy the handoff prompt instead (send button needs a connected broker)", zh: "未连接 broker — 请改用复制 handoff prompt(发送按钮需要已连接的 broker)" }, "send.promptOnly.confirm": { en: "No elements picked — send prompt only?", zh: "没有标注任何元素,只发 prompt?" }, "send.sending": { en: "Sending…", zh: "发送中…" }, "send.delivered": { en: "Delivered to agent (handoff {id})", zh: "已送达 agent(handoff {id})" }, "send.failed": { en: "Send failed: {detail}", zh: "发送失败: {detail}" }, "send.connecting": { en: "Connecting {url} …", zh: "正在连接 {url} …" }, "conn.wsFailed": { en: "WebSocket creation failed: {msg}", zh: "WS 创建失败: {msg}" }, "toast.policyBlocked": { en: "Site policy (CSP connect-src) blocked the local broker connection — retrying cannot succeed on this page; the handoff prompt can still be copied offline", zh: "站点策略(CSP connect-src)拦截了本地 broker 连接 — 本页重试也不会成功;仍可在离线模式复制 handoff prompt" }, "conn.brokerError": { en: "broker error: {code}", zh: "broker 错误: {code}" }, "conn.closed": { en: "broker connection closed", zh: "broker 连接已断开" }, "conn.unreachable": { en: "Cannot reach broker: {url} (edit the address in connection settings)", zh: "broker 连不上: {url}(连接设置里可改地址)" }, "conn.disconnected": { en: "Disconnected", zh: "已断开" }, "conn.notConnected": { en: "broker not connected", zh: "broker 未连接" }, "conn.noResponse": { en: "broker no response", zh: "broker 无响应" } }; var lang = (function detect() { const saved = gm.get(GM_LANG, ""); if (saved === "en" || saved === "zh") return saved; return (typeof navigator !== "undefined" && navigator.language || "").toLowerCase().indexOf("zh") === 0 ? "zh" : "en"; })(); function toggleLang() { lang = lang === "zh" ? "en" : "zh"; gm.set(GM_LANG, lang); return lang; } function t(key, vars) { const entry = STRINGS[key]; if (!entry) { console.warn("[pi.wp.i18n] missing key:", key); return key; } let s = entry[lang] !== void 0 ? entry[lang] : entry.en; if (vars) for (const k in vars) s = s.split("{" + k + "}").join(String(vars[k])); return s; } // web-picker.src/broker-conn.js var BACKOFF_STEPS = [1e3, 4e3, 16e3]; var BACKOFF_CAP = 6e4; var DEFAULT_PROMPT = "请逐条回应本页标注:note 写了要求的按 note 处理;没写 note 的,请解释该元素的渲染逻辑(组件与样式来源)。"; function autoLinkKey(origin) { return "wp.autoLink." + origin; } function policyBlockedKey(origin) { return "wp.policyBlocked." + origin; } function createBrokerConn(deps) { const gm2 = deps.gm; const debugLog2 = deps.debugLog || (() => { }); const toast = deps.toast || (() => { }); const onPageRequest = deps.onPageRequest || (() => { }); let ws = null; let wsState = "off"; let policyWarned = false; let probing = false; let connectSettle = null; let connectingUrl = ""; const pending = /* @__PURE__ */ new Map(); let silent = false; let manualDown = false; let backoffTimer = null; let backoffStep = 0; function autoLinkAllowed() { return gm2.get(autoLinkKey(location.origin), false) === true; } function clearBackoff() { if (backoffTimer) { clearTimeout(backoffTimer); backoffTimer = null; } backoffStep = 0; } function scheduleReconnect() { if (manualDown || wsState === "blocked" || !autoLinkAllowed() || backoffTimer) return; const delay = backoffStep < BACKOFF_STEPS.length ? BACKOFF_STEPS[backoffStep] : BACKOFF_CAP; backoffStep++; debugLog2("auto-link: reconnect in", delay, "ms"); backoffTimer = setTimeout(() => { backoffTimer = null; if (manualDown || wsState !== "off") return; silent = true; connect(); }, delay); } function brokerUrl() { return gm2.get(GM_BROKER, DEFAULT_BROKER_URL); } function sendFrame(obj) { if (!ws || ws.readyState !== 1) return false; try { ws.send(JSON.stringify(obj)); return true; } catch (e) { return false; } } function matchesBroker(ev, url) { if (!ev || ev.violatedDirective !== "connect-src") return false; const blocked = String(ev.blockedURI || ""); if (!blocked) return false; try { const want = new URL(url); const got = new URL(blocked); if (got.host === want.host) return true; } catch (e) { } return blocked.indexOf("127.0.0.1") >= 0 || blocked.indexOf("localhost") >= 0; } function onPolicyViolation(ev) { if (wsState === "on" || !connectingUrl || !matchesBroker(ev, connectingUrl)) return; debugLog2("CSP blocked the broker connection:", ev.violatedDirective, ev.blockedURI); settleConnect(false); clearBackoff(); gm2.set(policyBlockedKey(location.origin), true); setState("blocked"); if (!silent && !policyWarned) { policyWarned = true; toast(t("toast.policyBlocked")); } silent = false; } function settleConnect(ok) { const settled = connectSettle; connectSettle = null; if (settled) settled(ok); } function armPolicyListener() { try { if (typeof document !== "undefined" && typeof document.addEventListener === "function") { document.addEventListener("securitypolicyviolation", onPolicyViolation, true); } } catch (e) { } } function disarmPolicyListener() { try { if (typeof document !== "undefined" && typeof document.removeEventListener === "function") { document.removeEventListener("securitypolicyviolation", onPolicyViolation, true); } } catch (e) { } } function setState(s) { wsState = s; deps.onState(s); } function connect() { clearBackoff(); manualDown = false; if (ws) { try { ws.close(); } catch (e) { } ws = null; } const url = brokerUrl().replace(/\/+$/, "") + "/ws"; connectingUrl = url; policyWarned = false; armPolicyListener(); setState("connecting"); let settle; const attempt = new Promise((resolve) => { settle = resolve; }); connectSettle = settle; let sock; try { sock = new WebSocket(url); } catch (e) { disarmPolicyListener(); settleConnect(false); setState("off"); if (!silent) toast(t("conn.wsFailed", { msg: e.message })); settle(false); if (silent) { silent = false; scheduleReconnect(); } return attempt; } ws = sock; sock.onopen = () => { sendFrame(frameHello()); }; sock.onmessage = (ev) => { let f; try { f = JSON.parse(ev.data); } catch (e) { return; } if (!f || typeof f.type !== "string") return; if (f.type === WIRE.KIND_WELCOME) { setState("on"); if (!silent) gm2.set(autoLinkKey(location.origin), true); gm2.set(policyBlockedKey(location.origin), false); clearBackoff(); const wasSilent = silent; silent = false; if (!wasSilent) toast(t("conn.on")); debugLog2("auto-link: connected", wasSilent ? "(auto)" : "(manual)"); connectingUrl = ""; settleConnect(true); if (deps.onWelcome) deps.onWelcome(); return; } if (f.type === WIRE.KIND_ACK) { const p = pending.get(f.id); if (p && p.kind === "submit") { pending.delete(f.id); p.resolve({ ok: true, result: f.result }); } else if (p && p.kind === "compose") { pending.delete(f.id); p.resolve({ ok: true, text: f.result && typeof f.result.prompt === "string" ? f.result.prompt : "" }); } return; } if (f.type === WIRE.KIND_ERROR) { const p = pending.get(f.id); if (p) { pending.delete(f.id); p.resolve(p.kind === "submit" ? { ok: false, code: f.code, message: f.message } : p.kind === "compose" ? { ok: false, code: f.code, message: f.message } : []); } else { toast(t("conn.brokerError", { code: f.code || "?" })); } return; } if (f.type === WIRE.KIND_TARGETS_RESULT) { const p = pending.get(f.id); if (p && p.kind === "targets") { pending.delete(f.id); p.resolve(Array.isArray(f.targets) ? f.targets : []); } return; } if (f.type === WIRE.KIND_PAGE_REQUEST) { debugLog2("page.request", f.id, f.tool && f.tool.op); onPageRequest(f); return; } }; sock.onclose = () => { if (ws !== sock) return; ws = null; for (const p of pending.values()) p.resolve(p.kind === "submit" ? { ok: false, code: "closed", message: t("conn.closed") } : []); pending.clear(); connectingUrl = ""; disarmPolicyListener(); settleConnect(false); const wasSilent = silent; silent = false; if (wsState !== "off") { setState("off"); } if (wasSilent) { debugLog2("auto-link: attempt failed"); scheduleReconnect(); } else if (wsState === "off") { if (autoLinkAllowed() && !manualDown) { scheduleReconnect(); } else { toast(t("conn.closed")); } } }; sock.onerror = () => { if (ws !== sock || wsState === "off") return; if (wsState === "connecting") { settleConnect(false); setState("off"); } if (!silent) toast(t("conn.unreachable", { url })); }; return attempt; } function disconnect() { manualDown = true; clearBackoff(); silent = false; disarmPolicyListener(); setState("off"); if (ws) { try { ws.close(); } catch (e) { } ws = null; } toast(t("conn.disconnected")); } function maybeAutoConnect() { if (!autoLinkAllowed() || manualDown) return false; silent = true; connect(); return true; } function maybeProbePolicyBlocked() { if (probing || wsState !== "off" || manualDown) return false; if (autoLinkAllowed()) return false; if (gm2.get(policyBlockedKey(location.origin), false) === true) { setState("blocked"); return true; } probing = true; silent = true; connect(); return true; } function revokeAutoLink() { gm2.set(autoLinkKey(location.origin), false); } const PAGE_QUERY_RULE = "\n\n[页面查询规则] 反向查询本页(page.request:dom.query / dom.html / console.logs / framework.inspect / page.wait 等)随时可用:本页已授权 broker 自动重连,修改代码导致页面刷新后,连接会自动恢复(重连期间 page.request 可能短暂返回 no_tabs/timeout,等几秒重试即可,重试上限 3 次)。完成修改后请主动反向查询验证:dom.query 复查目标元素的最终状态,console.logs 检查是否引入新报错。若错误信息表明页面写操作已授权,可用 dom.click / dom.setValue 做交互式验证(如点击按钮、填写表单后复查状态);返回 denied_op 则不要重试写操作。"; function submitToAgent(prompt, targetName, record) { return new Promise((resolve) => { if (wsState !== "on") { resolve({ ok: false, code: "not_connected", message: t("conn.notConnected") }); return; } const id = "a" + Date.now().toString(36) + Math.random().toString(36).slice(2, 5); pending.set(id, { kind: "submit", resolve }); const promptText = (prompt && prompt.trim() ? prompt.trim() : DEFAULT_PROMPT) + PAGE_QUERY_RULE; if (!sendFrame(frameSubmit(id, promptText, targetName, record))) { pending.delete(id); resolve({ ok: false, code: "not_connected", message: t("conn.notConnected") }); return; } setTimeout(() => { if (pending.has(id)) { pending.delete(id); resolve({ ok: false, code: "timeout", message: t("conn.noResponse") }); } }, 1e4); }); } function requestCompose(prompt, targetName, record) { return new Promise((resolve) => { if (wsState !== "on") { resolve({ ok: false, code: "not_connected", message: t("conn.notConnected") }); return; } const id = "c" + Date.now().toString(36) + Math.random().toString(36).slice(2, 5); pending.set(id, { kind: "compose", resolve }); const promptText = (prompt && prompt.trim() ? prompt.trim() : DEFAULT_PROMPT) + PAGE_QUERY_RULE; if (!sendFrame(frameCompose(id, promptText, targetName, record))) { pending.delete(id); resolve({ ok: false, code: "not_connected", message: t("conn.notConnected") }); return; } setTimeout(() => { if (pending.has(id)) { pending.delete(id); resolve({ ok: false, code: "timeout", message: t("conn.noResponse") }); } }, 1e4); }); } function requestTargets() { return new Promise((resolve) => { if (wsState !== "on") { resolve([]); return; } const id = "t" + Date.now().toString(36) + Math.random().toString(36).slice(2, 5); pending.set(id, { kind: "targets", resolve }); if (!sendFrame(frameTargetsList(id))) { pending.delete(id); resolve([]); return; } setTimeout(() => { if (pending.has(id)) { pending.delete(id); resolve([]); } }, 5e3); }); } return { brokerUrl, connect, disconnect, sendFrame, getState: () => wsState, autoLinkAllowed, revokeAutoLink, maybeAutoConnect, maybeProbePolicyBlocked, submitToAgent, requestCompose, requestTargets }; } // web-picker.src/tt.js var POLICY_NAME = "pi-xfer"; var cached; var enforced = false; var detachedBroken = false; function ttPolicy() { if (cached !== void 0) return cached; cached = null; try { const tt = window.trustedTypes; if (tt && typeof tt.createPolicy === "function") { cached = tt.createPolicy(POLICY_NAME, { createHTML: (s) => s }); } } catch (e) { cached = null; } return cached; } function parseDetached(markup) { try { return new DOMParser().parseFromString(markup, "text/html"); } catch (e) { } const doc = document.implementation.createHTMLDocument("pi-xfer"); doc.body.innerHTML = markup; return doc; } var SVG_NS = "http://www.w3.org/2000/svg"; var VOID_TAGS = /* @__PURE__ */ new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]); var RAW_TAGS = /* @__PURE__ */ new Set(["style", "script"]); var NAMED_ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " ", copy: "©", reg: "®", hellip: "…", mdash: "—", ndash: "–", laquo: "«", raquo: "»" }; function decodeEntities(s) { if (s.indexOf("&") === -1) return s; return s.replace(/&(#[xX]?[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g, (m, body) => { if (body[0] === "#") { const code = body[1] === "x" || body[1] === "X" ? parseInt(body.slice(2), 16) : parseInt(body.slice(1), 10); return Number.isFinite(code) ? String.fromCodePoint(code) : m; } return Object.prototype.hasOwnProperty.call(NAMED_ENTITIES, body) ? NAMED_ENTITIES[body] : m; }); } function miniParse(markup) { const frag = document.createDocumentFragment(); const stack = [{ tag: null, el: frag, ns: null }]; const top = () => stack[stack.length - 1]; let i = 0; const len = markup.length; const addText = (raw) => { if (raw) top().el.appendChild(document.createTextNode(decodeEntities(raw))); }; while (i < len) { const lt = markup.indexOf("<", i); if (lt === -1) { addText(markup.slice(i)); break; } addText(markup.slice(i, lt)); if (markup.startsWith("", lt + 4); i = end === -1 ? len : end + 3; continue; } if (markup.startsWith("", lt + 2); const name = markup.slice(lt + 2, gt2 === -1 ? len : gt2).trim().toLowerCase(); for (let k = stack.length - 1; k > 0; k--) { if (stack[k].tag === name) { stack.length = k; break; } } i = gt2 === -1 ? len : gt2 + 1; continue; } const nameMatch = /^([a-zA-Z][a-zA-Z0-9-]*)/.exec(markup.slice(lt + 1)); if (!nameMatch) { addText("<"); i = lt + 1; continue; } const tag = nameMatch[1].toLowerCase(); let j = lt + 1 + nameMatch[1].length; const attrs = []; let gt = -1; let selfClose = false; while (j < len) { while (j < len && /\s/.test(markup[j])) j++; if (markup[j] === ">") { gt = j; break; } if (markup[j] === "/" && markup[j + 1] === ">") { gt = j + 1; selfClose = true; break; } const nameMatch2 = /^[^\s=/>]+/.exec(markup.slice(j)); if (!nameMatch2) { j++; continue; } const attrName = nameMatch2[0]; j += attrName.length; let value = ""; while (j < len && /\s/.test(markup[j])) j++; if (markup[j] === "=") { j++; while (j < len && /\s/.test(markup[j])) j++; const q = markup[j]; if (q === '"' || q === "'") { const end = markup.indexOf(q, j + 1); value = markup.slice(j + 1, end === -1 ? len : end); j = end === -1 ? len : end + 1; } else { const vm = /^[^\s>]+/.exec(markup.slice(j)); value = vm ? vm[0] : ""; j += value.length; } } attrs.push([attrName, value]); } i = gt === -1 ? len : gt + 1; const parent = top(); const ns = parent.ns || (tag === "svg" ? SVG_NS : null); const el = ns ? document.createElementNS(ns, tag) : document.createElement(tag); for (const [n, v] of attrs) el.setAttribute(n, decodeEntities(v)); parent.el.appendChild(el); if (RAW_TAGS.has(tag)) { const lower = markup.toLowerCase(); const close = lower.indexOf("", i + raw.length); i = closeGt === -1 ? len : closeGt + 1; continue; } if (!selfClose && !VOID_TAGS.has(tag)) { stack.push({ tag, el, ns }); } } return frag; } function setHTML(target, html) { if (!target) return; const markup = html == null ? "" : String(html); if (!enforced) { try { target.innerHTML = markup; return; } catch (e) { enforced = true; } } try { const policy = ttPolicy(); if (policy) { target.innerHTML = policy.createHTML(markup); return; } } catch (e) { } if (!detachedBroken && typeof DOMParser === "function") { try { const doc = parseDetached(markup); let node = doc.body.firstChild; while (node) { target.appendChild(node); node = doc.body.firstChild; } return; } catch (e) { detachedBroken = true; } } try { if (typeof document !== "undefined" && document.createElement) { const frag = miniParse(markup); let node = frag.firstChild; while (node) { target.appendChild(node); node = frag.firstChild; } return; } } catch (e) { } try { target.textContent = markup; } catch (e2) { } } function clearHTML(target) { if (!target) return; try { target.innerHTML = ""; } catch (e) { target.textContent = ""; } } // web-picker.src/ui/shadow.js function buildUI() { const host = document.createElement("div"); host.setAttribute(HOST_FLAG, ""); Object.assign(host.style, { position: "fixed", inset: "0", zIndex: "2147483647", pointerEvents: "none" }); function injectHost() { if (host.isConnected || !document.documentElement) return; document.documentElement.appendChild(host); } injectHost(); if (!host.isConnected) { document.addEventListener("DOMContentLoaded", injectHost, { once: true }); } else { new MutationObserver(() => { if (!host.isConnected) injectHost(); }).observe(document.documentElement, { childList: true }); } const root = host.attachShadow({ mode: "open" }); setHTML(root, `
PICK []${t("bar.cycle")} 1-9${t("bar.jump")} Enter${t("bar.select")} ⇧Enter${t("bar.group")} F${t("bar.freeze")} Esc${t("bar.exit")}
REC 0 events ${t("recbadge")}
${t("panel.title")}
${t("send.label")}
${t("target.label")}
${t("conn.off")}
${t("copyprompt")}
${t("copyprompt.sub")}
${t("settings.title")}
${t("settings.burl")}
`); const $ = (id) => root.getElementById(id); const elHL = $("hl"), elGWrap = $("gwrap"), elBadge = $("badge"), elInfo = $("info"), elBar = $("bar"), elGH = $("gh"), elGCLEAR = $("gclear"), elEscX = $("escx"), elFab = $("fab"), elCnt = $("cnt"), elDot = $("dot"), elCard = $("card"), elSel = $("sel"), elRecBar = $("recbar"), elRecCount = $("reccount"), elRecBadge = $("recbadge"), elRecStop = $("recstop"), elRecDiscard = $("recdiscard"), elTxt = $("txt"), elOk = $("ok"), elCancel = $("cancel"), elToast = $("toast"), elPanel = $("panel"), elPH = $("ph"), elPlist = $("plist"), elPcount = $("pcount"), elPclose = $("pclose"), elPrompt = $("prompt"), elTCombo = $("tcombo"), elTInput = $("tinput"), elTDrop = $("tdrop"), elTRefresh = $("trefresh"), elSend = $("sendbtn"), elClear = $("clearbtn"), elSendGroup = $("sendgroup"), elSendMore = $("sendmore"), elSendMenu = $("sendmenu"), elCopyPrompt = $("copyprompt"), elConn = $("connstate"), elConnText = $("conntext"), elSettings = $("settings"), elSendOffline = $("sendoffline"), elSUrl = $("sburl"), elSSave = $("ssave"), elSCancel = $("scancel"), elSProps = $("sprops"), elSAuto = $("sauto"), elSWrite = $("swrite"); if (gm.get(GM_FAB_SIZE, "small") === "large") elFab.classList.add("large"); let pos = { x: window.innerWidth - 68, y: window.innerHeight - 96 }; try { const saved = sessionStorage.getItem(KEY_POS); if (saved) { const p = JSON.parse(saved); if (typeof p.x === "number" && typeof p.y === "number") pos = p; } } catch (e) { } function clampFabPos() { pos.x = Math.max(4, Math.min(window.innerWidth - 50, pos.x)); pos.y = Math.max(4, Math.min(window.innerHeight - 50, pos.y)); } clampFabPos(); elFab.style.left = pos.x + "px"; elFab.style.top = pos.y + "px"; window.addEventListener("resize", () => { clampFabPos(); elFab.style.left = pos.x + "px"; elFab.style.top = pos.y + "px"; try { sessionStorage.setItem(KEY_POS, JSON.stringify(pos)); } catch (err) { } }, true); function toast(msg) { elToast.textContent = msg; elToast.classList.add("show"); clearTimeout(toast._t); toast._t = setTimeout(() => elToast.classList.remove("show"), 2200); } const els = { elHL, elGWrap, elBadge, elInfo, elBar, elGH, elGCLEAR, elEscX, elRecBar, elRecCount, elRecBadge, elRecStop, elRecDiscard, elFab, elCnt, elDot, elCard, elSel, elTxt, elOk, elCancel, elToast, elPanel, elPH, elPlist, elPcount, elPclose, elPrompt, elTCombo, elTInput, elTDrop, elTRefresh, elSend, elClear, elSendGroup, elSendMore, elSendMenu, elCopyPrompt, elConn, elConnText, elSettings, elSUrl, elSSave, elSCancel, elSProps, elSAuto, elSWrite, elSendOffline }; function reinjectTrigger() { if (host.isConnected) return true; document.documentElement.appendChild(host); clampFabPos(); elFab.style.left = pos.x + "px"; elFab.style.top = pos.y + "px"; return host.isConnected; } function observeHostRemoval() { if (!document.documentElement) return; new MutationObserver(() => { if (!host.isConnected) reinjectTrigger(); }).observe(document.documentElement, { childList: true }); } if (document.documentElement) observeHostRemoval(); else document.addEventListener("DOMContentLoaded", observeHostRemoval, { once: true }); return { host, root, $, els, toast, clampFabPos, reinjectTrigger, get pos() { return pos; } }; } // web-picker.src/ui/panel.js function initPanel(ctx) { const { els, toast } = ctx; const { elCnt, elPanel, elPlist, elPcount, elPclose } = els; let panelOpen = false; function refreshCount() { const n = loadBatch().length + (ctx.getRecord && ctx.getRecord() ? 1 : 0); elCnt.textContent = n; elCnt.classList.toggle("zero", n === 0); if (panelOpen) renderPanel(); } ctx.refreshCount = refreshCount; ctx.getPanelOpen = () => panelOpen; function addPick(rec) { const b = loadBatch(); b.push(rec); saveBatch(b); toast(t("pick.added", { sel: rec.selector })); refreshCount(); } ctx.addPick = addPick; function clearBatch() { saveBatch([]); refreshCount(); } ctx.clearBatch = clearBatch; function renderPanel() { const b = loadBatch(); const recHtml = ctx.renderRecordSummary ? ctx.renderRecordSummary() : ""; elPcount.textContent = b.length || recHtml ? t("count.items", { n: b.length + (recHtml ? 1 : 0) }) : ""; if (!b.length && !recHtml) { setHTML(elPlist, `
${t("panel.empty")}
`); return; } setHTML(elPlist, recHtml + b.map( (r, i) => '
' + escapeHtml(r.selector) + "
" + (r.group ? `
${t("group.label", { g: escapeHtml(r.group) })}
` : "") + (r.source && r.source.file ? '
⌘ ' + escapeHtml(r.source.component + " · " + r.source.file + ":" + r.source.line) + "
" : "") + (r.textPreview ? '
' + escapeHtml(r.textPreview) + "
" : "") + `
' + new Date(r.ts).toLocaleTimeString() + `
` ).join("")); } ctx.renderPanel = renderPanel; function openPanel() { panelOpen = true; renderPanel(); elPanel.style.display = "flex"; ctx.panelDrag.clamp(); if (ctx.conn.getState() === "on") ctx.refreshTargets(); else ctx.renderTargetCombo(); } function closePanel() { panelOpen = false; ctx.closeDrop(); ctx.closeSendMenu(); elPanel.style.display = "none"; } ctx.closePanel = closePanel; function togglePanel() { if (panelOpen) closePanel(); else openPanel(); } ctx.togglePanel = togglePanel; elPclose.addEventListener("click", closePanel); elPlist.addEventListener("change", (e) => { const t2 = e.target; if (!t2 || t2.tagName !== "TEXTAREA") return; const item = t2.closest(".item"); if (!item) return; const b = loadBatch(); const i = +item.getAttribute("data-i"); if (!b[i]) return; b[i].note = t2.value.trim(); let synced = 0; if (b[i].group) { for (let j = 0; j < b.length; j++) { if (j === i || b[j].group !== b[i].group) continue; b[j].note = b[i].note; synced++; const ta = elPlist.querySelector('.item[data-i="' + j + '"] textarea'); if (ta) ta.value = b[i].note; } } saveBatch(b); toast(t2("note.saved") + (synced ? t2("note.synced", { n: synced }) : "")); }); elPlist.addEventListener("click", (e) => { const recDel = e.target && e.target.closest ? e.target.closest("[data-rec-del]") : null; if (recDel) { if (ctx.clearRecord) ctx.clearRecord(); renderPanel(); toast(t("record.deleted")); return; } const del = e.target && e.target.closest ? e.target.closest(".del") : null; if (!del) return; const item = del.closest(".item"); if (!item) return; const b = loadBatch(); const i = +item.getAttribute("data-i"); if (!b[i]) return; const sel = b[i].selector; b.splice(i, 1); saveBatch(b); refreshCount(); renderPanel(); toast(t("pick.deleted", { sel })); }); } // web-picker.src/framework.js function reactSource(el) { const keys = Object.getOwnPropertyNames(el); for (const k of keys) { if (!k.startsWith("__reactFiber$") && !k.startsWith("__reactInternalInstance$")) continue; let f = el[k], guard = 0; while (f && guard++ < 200) { const t2 = f.type; const src = f._debugSource; if (t2 && (t2.name || t2.displayName) && src && src.fileName) { return { framework: "react", component: t2.displayName || t2.name || "", file: src.fileName, line: src.lineNumber || 0, column: src.columnNumber || 0 }; } f = f.return; } } return null; } function vueSource(el) { let vm = el.__vueParentComponent || el.__vue__ || null; let guard = 0; while (vm && guard++ < 200) { const opts = vm.$options || {}; if (opts.__file) { const name = vm.type && (vm.type.name || vm.type.__name) || opts.name || opts.__name || ""; return { framework: "vue", component: name, file: opts.__file, line: 0, column: 0 }; } vm = vm.$parent || null; } return null; } function sourceInfo(el) { const direct = reactSource(el) || vueSource(el); if (direct) return direct; try { const uw = typeof unsafeWindow !== "undefined" ? unsafeWindow : null; if (!uw || !uw.document) return null; const sel = cssPath(el); const pageEl = uw.document.querySelector(sel); if (!pageEl || pageEl === el) return null; return reactSource(pageEl) || vueSource(pageEl); } catch (e) { return null; } } // web-picker.src/ui/pick.js function initPick(ctx) { const { host, root, els, toast } = ctx; const { elHL, elGWrap, elBadge, elInfo, elBar, elGH, elGCLEAR, elCard, elSel, elTxt, elOk } = els; let pickMode = false; let stack = []; let idx = 0; let pinned = null; let frozen = true; let groupEls = []; let groupCard = false; ctx.pickState = { get pickMode() { return pickMode; }, get pinned() { return pinned; }, get groupCard() { return groupCard; }, get groupCount() { return groupEls.length; }, get frozen() { return frozen; }, set frozen(v) { frozen = !!v; }, get stack() { return stack; }, set idx(v) { idx = v; }, get idx() { return idx; }, currentEl }; function stackAt(clientX, clientY) { const prev = host.style.pointerEvents; host.style.pointerEvents = "none"; const all = document.elementsFromPoint(clientX, clientY) || []; host.style.pointerEvents = prev; return all.filter((el) => { if (!el || el.nodeType !== 1) return false; if (el.getAttribute && el.getAttribute(HOST_FLAG) !== null) return false; const r = el.getBoundingClientRect(); if (r.width < 2 || r.height < 2) return false; const cs = getComputedStyle(el); if (cs.visibility === "hidden" || cs.display === "none") return false; return true; }); } function currentEl() { return stack[idx] || null; } ctx.currentEl = currentEl; function applyHighlight(el, pin2) { if (!el) { elHL.style.display = "none"; elBadge.style.display = "none"; elInfo.style.display = "none"; return; } const r = el.getBoundingClientRect(); Object.assign(elHL.style, { left: r.left + "px", top: r.top + "px", width: r.width + "px", height: r.height + "px", display: "block" }); elHL.classList.toggle("pin", !!pin2); elBadge.style.display = "block"; elBadge.style.left = r.left + r.width / 2 + "px"; elBadge.style.top = r.top + "px"; setHTML(elBadge, idx + 1 + `/${t("pick.layers", { n: stack.length })} · ` + Math.round(r.width) + "×" + Math.round(r.height) + ""); elInfo.style.display = "block"; elInfo.style.left = r.left + "px"; elInfo.style.top = r.top + "px"; setHTML(elInfo, '' + escapeHtml(selectorPreview(el)) + ' ' + Math.round(r.width) + "×" + Math.round(r.height) + " · " + escapeHtml(cssPath(el)) + ""); } function refresh() { applyHighlight(currentEl(), !!pinned); } ctx.refresh = refresh; function setActive(on) { pickMode = on; host.style.pointerEvents = "none"; elBar.style.display = on ? "flex" : "none"; if (on) ctx.barDrag.clamp(); els.elFab.style.display = on ? "none" : "block"; document.body.style.cursor = on ? "crosshair" : ""; if (on) { frozen = true; ctx.closePanel(); renderGroupMarks(); } else { pinned = null; frozen = false; closeGroupCard(); elCard.style.display = "none"; elHL.style.display = "none"; elBadge.style.display = "none"; elInfo.style.display = "none"; renderGroupMarks(); } ctx.updateFreezeUI(); ctx.refreshCount(); } ctx.setActive = setActive; function isOurUI(e) { const path = e.composedPath ? e.composedPath() : []; return path.indexOf(root) >= 0; } ctx.isOurUI = isOurUI; function isFabTarget(e) { const path = e.composedPath ? e.composedPath() : []; return path.indexOf(els.elFab) >= 0; } ctx.isFabTarget = isFabTarget; function moveCapture(e) { if (!pickMode) return; if (frozen) { e.stopPropagation(); e.preventDefault(); } if (pinned || groupCard || isOurUI(e)) return; stack = stackAt(e.clientX, e.clientY); idx = 0; refresh(); } function hoverCapture(e) { if (!pickMode || !frozen) return; e.stopPropagation(); e.preventDefault(); } function clickCapture(e) { if (isOurUI(e)) return; if (!pickMode) return; const btn0 = e.button === void 0 || e.button === 0; if (e.type === "pointerdown" || e.type === "mousedown") { e.stopPropagation(); e.preventDefault(); if (!btn0 || groupCard) return; if (!pinned) { if (e.shiftKey) groupAt(e.clientX, e.clientY); else pinAt(e.clientX, e.clientY); } return; } e.stopPropagation(); e.preventDefault(); } function wheelCapture(e) { if (!pickMode || pinned || isOurUI(e)) return; const sc = nearestScrollable(e.target); if (sc && sc.scrollHeight > sc.clientHeight) return; e.preventDefault(); window.scrollBy(0, e.deltaY); } window.addEventListener("mousemove", moveCapture, true); window.addEventListener("mouseover", hoverCapture, true); window.addEventListener("mouseout", hoverCapture, true); window.addEventListener("mouseenter", hoverCapture, true); window.addEventListener("mouseleave", hoverCapture, true); window.addEventListener("pointermove", hoverCapture, true); window.addEventListener("pointerover", hoverCapture, true); window.addEventListener("pointerout", hoverCapture, true); window.addEventListener("pointerenter", hoverCapture, true); window.addEventListener("pointerleave", hoverCapture, true); window.addEventListener("pointerdown", clickCapture, true); window.addEventListener("mousedown", clickCapture, true); window.addEventListener("mouseup", clickCapture, true); window.addEventListener("click", clickCapture, true); window.addEventListener("contextmenu", clickCapture, true); window.addEventListener("wheel", wheelCapture, { capture: true, passive: false }); for (const t2 of ["pointerdown", "mousedown", "pointerup", "mouseup", "click", "contextmenu"]) { host.addEventListener(t2, (e) => e.stopPropagation(), false); } function shiftLayer(delta) { if (!stack.length) return; idx = Math.max(0, Math.min(stack.length - 1, idx + delta)); refresh(); } ctx.shiftLayer = shiftLayer; function pin() { const el = currentEl(); if (!el) return; pinned = el; applyHighlight(el, true); const sel = cssPath(el); const r = el.getBoundingClientRect(); let left = r.right + 10; if (left + 300 > window.innerWidth) left = Math.max(8, r.left - 310); let top = r.top; if (top + 240 > window.innerHeight) top = Math.max(8, window.innerHeight - 250); Object.assign(elCard.style, { display: "block", left: left + "px", top: top + "px" }); elSel.textContent = sel; elTxt.value = ""; setTimeout(() => elTxt.focus(), 0); } ctx.pin = pin; function unpin() { pinned = null; elCard.style.display = "none"; refresh(); } ctx.unpin = unpin; function pinAt(x, y) { const st = stackAt(x, y); if (!st.length) return; stack = st; idx = 0; pin(); } function toggleGroup(el) { if (!el) return; const i = groupEls.indexOf(el); if (i >= 0) { groupEls.splice(i, 1); toast(t("group.removed", { n: groupEls.length })); } else { groupEls.push(el); toast(t("group.added", { n: groupEls.length })); } renderGroupMarks(); updateGroupUI(); } function clearPendingGroup() { if (!groupEls.length) return; const n = groupEls.length; groupEls = []; if (groupCard) closeGroupCard(); renderGroupMarks(); updateGroupUI(); toast(t("group.cleared", { n })); } ctx.clearPendingGroup = clearPendingGroup; function groupAt(x, y) { const st = stackAt(x, y); if (!st.length) return; stack = st; idx = 0; toggleGroup(st[0]); } function renderGroupMarks() { clearHTML(elGWrap); if (!pickMode || !groupEls.length) { elGWrap.style.display = "none"; return; } elGWrap.style.display = "block"; groupEls.forEach((el, i) => { const r = el.getBoundingClientRect(); if (!r.width && !r.height) return; const m = document.createElement("div"); m.className = "gmark"; Object.assign(m.style, { left: r.left + "px", top: r.top + "px", width: r.width + "px", height: r.height + "px" }); const tag = document.createElement("span"); tag.className = "gi"; tag.textContent = String(i + 1); tag.setAttribute("data-gi", String(i)); tag.title = t("group.tagTitle"); m.appendChild(tag); elGWrap.appendChild(m); }); } ctx.renderGroupMarks = renderGroupMarks; elGWrap.addEventListener("click", (e) => { const tag = e.target && e.target.closest ? e.target.closest(".gi") : null; if (!tag) return; const i = +tag.getAttribute("data-gi"); if (!(i >= 0) || !groupEls[i]) return; groupEls.splice(i, 1); renderGroupMarks(); updateGroupUI(); toast(t("group.removed", { n: groupEls.length })); }); ctx.toggleGroup = toggleGroup; function updateGroupUI() { const n = groupEls.length; elGH.classList.toggle("on", n > 0); setHTML(elGH, `⇧Enter${t("bar.group")}` + (n ? t("gh.note", { n }) : "")); elGCLEAR.style.display = n ? "inline-flex" : "none"; } ctx.updateGroupUI = updateGroupUI; function openGroupCard() { if (!groupEls.length || pinned) return; groupCard = true; const r = groupEls[0].getBoundingClientRect(); let left = r.right + 10; if (left + 300 > window.innerWidth) left = Math.max(8, r.left - 310); let top = r.top; if (top + 240 > window.innerHeight) top = Math.max(8, window.innerHeight - 250); elSel.textContent = groupEls.map((el) => cssPath(el)).join("\n"); elTxt.value = ""; elTxt.placeholder = t("card.groupPlaceholder"); elOk.textContent = t("card.groupOk", { n: groupEls.length }); Object.assign(elCard.style, { display: "block", left: left + "px", top: top + "px" }); setTimeout(() => elTxt.focus(), 0); } ctx.openGroupCard = openGroupCard; function closeGroupCard() { if (!groupCard) return; groupCard = false; elCard.style.display = "none"; elTxt.placeholder = t("card.placeholder"); elOk.textContent = t("card.ok"); } ctx.closeGroupCard = closeGroupCard; function submitGroup() { if (!groupCard || !groupEls.length) return; const note = elTxt.value.trim(); const gid = "g" + Date.now().toString(36) + Math.random().toString(36).slice(2, 5); const b = loadBatch(); groupEls.forEach((el) => b.push(Object.assign(payloadFor(el, note), { group: gid }))); saveBatch(b); ctx.refreshCount(); const n = groupEls.length; groupEls = []; renderGroupMarks(); updateGroupUI(); closeGroupCard(); toast(t("group.selected", { n, gid })); } ctx.submitGroup = submitGroup; function payloadFor(el, note) { const r = el.getBoundingClientRect(); const attributes = {}; try { for (const a of Array.from(el.attributes).slice(0, 20)) { attributes[a.name] = (a.value || "").slice(0, 120); } } catch (e) { } return { selector: cssPath(el), xpath: xPath(el), tagName: el.tagName.toLowerCase(), id: el.id || void 0, classes: Array.from(el.classList || []), attributes, textPreview: (el.textContent || "").trim().replace(/\s+/g, " ").slice(0, 80), rect: { x: Math.round(r.left), y: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) }, note: note || "", ts: Date.now(), url: location.href, source: sourceInfo(el) }; } ctx.payloadFor = payloadFor; function submit() { if (!pinned) return; ctx.addPick(payloadFor(pinned, elTxt.value.trim())); unpin(); } ctx.submit = submit; elOk.addEventListener("click", () => { if (groupCard) submitGroup(); else submit(); }); els.elCancel.addEventListener("click", () => { if (groupCard) closeGroupCard(); else unpin(); }); window.addEventListener("scroll", () => { if (pickMode) { refresh(); renderGroupMarks(); } }, true); window.addEventListener("resize", () => { if (pickMode) { refresh(); renderGroupMarks(); } }, true); } // web-picker.src/ui/combo.js function initCombo(ctx) { const { els, toast } = ctx; const { elTCombo, elTInput, elTDrop, elTRefresh } = els; const conn = ctx.conn; let targets = []; let comboSel = ""; let comboFilter = ""; let comboHl = 0; let comboOpen = false; ctx.getComboSel = () => comboSel; ctx.getTargets = () => targets.slice(); function comboList() { const q = comboFilter.trim().toLowerCase(); if (!q) return targets.slice(); return targets.filter((t2) => ((t2.name || "") + " " + (t2.cwd || "") + " " + (t2.status || "")).toLowerCase().includes(q)); } function markMatch(text, q) { const s = String(text == null ? "" : text); const i = q ? s.toLowerCase().indexOf(q.toLowerCase()) : -1; if (i < 0) return escapeHtml(s); return escapeHtml(s.slice(0, i)) + "" + escapeHtml(s.slice(i, i + q.length)) + "" + escapeHtml(s.slice(i + q.length)); } function renderDrop() { if (!targets.length) { setHTML(elTDrop, `
${t("combo.noSessions")}
`); return; } const list = comboList(); if (!list.length) { setHTML(elTDrop, `
${t("combo.noMatch")}
`); return; } if (comboHl >= list.length) comboHl = list.length - 1; if (comboHl < 0) comboHl = 0; const q = comboFilter.trim(); setHTML(elTDrop, list.map((t2, i) => { const sub = (t2.cwd || "?") + (t2.status ? " · " + t2.status : ""); return '
' + (q ? markMatch(t2.name, q) : escapeHtml(t2.name)) + (t2.name === comboSel ? ' ✓' : "") + '
' + (q ? markMatch(sub, q) : escapeHtml(sub)) + "
"; }).join("") + `
${t("combo.hint")}
`); const hl = elTDrop.querySelector(".titem.hl"); if (hl) { if (hl.offsetTop < elTDrop.scrollTop) elTDrop.scrollTop = hl.offsetTop; else if (hl.offsetTop + hl.offsetHeight > elTDrop.scrollTop + elTDrop.clientHeight) elTDrop.scrollTop = hl.offsetTop + hl.offsetHeight - elTDrop.clientHeight; } } function openDrop(selectAll) { if (elTInput.disabled || comboOpen) return; comboOpen = true; comboFilter = ""; comboHl = Math.max(0, targets.findIndex((t2) => t2.name === comboSel)); elTDrop.classList.add("open"); renderDrop(); if (selectAll) setTimeout(() => elTInput.select(), 0); } function closeDrop() { if (!comboOpen) return; comboOpen = false; elTDrop.classList.remove("open"); elTInput.value = comboSel; } ctx.closeDrop = closeDrop; ctx.comboOpen = () => comboOpen; function pickTarget(name) { if (!name) return; comboSel = name; gm.set(GM_TARGET, name); closeDrop(); elTInput.blur(); toast(t("combo.switched", { name })); } function renderTargetCombo() { const last = gm.get(GM_TARGET, ""); comboSel = ""; elTInput.disabled = true; elTInput.value = ""; elTInput.placeholder = conn.getState() !== "on" ? t("tinput.placeholder") : t("combo.none"); closeDrop(); if (conn.getState() !== "on" || !targets.length) return; elTInput.disabled = false; comboSel = targets.some((t2) => t2.name === last) ? last : targets[0].name; if (comboOpen) renderDrop(); else elTInput.value = comboSel; } ctx.renderTargetCombo = renderTargetCombo; async function refreshTargets() { targets = await conn.requestTargets(); renderTargetCombo(); } ctx.refreshTargets = refreshTargets; elTInput.addEventListener("focus", () => openDrop(true)); elTInput.addEventListener("input", () => { if (!comboOpen) openDrop(false); comboFilter = elTInput.value; comboHl = 0; renderDrop(); }); elTInput.addEventListener("keydown", (e) => { if (e.isComposing || e.keyCode === 229) return; if (!comboOpen) { if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Enter") { e.preventDefault(); openDrop(true); } return; } const list = comboList(); if (e.key === "ArrowDown") { e.preventDefault(); comboHl = Math.min(list.length - 1, comboHl + 1); renderDrop(); } else if (e.key === "ArrowUp") { e.preventDefault(); comboHl = Math.max(0, comboHl - 1); renderDrop(); } else if (e.key === "Enter") { e.preventDefault(); if (list[comboHl]) pickTarget(list[comboHl].name); } }); elTDrop.addEventListener("mousemove", (e) => { const it = e.target && e.target.closest ? e.target.closest(".titem") : null; if (!it) return; const items = elTDrop.querySelectorAll(".titem"); const i = Array.prototype.indexOf.call(items, it); if (i < 0 || i === comboHl) return; if (items[comboHl]) items[comboHl].classList.remove("hl"); it.classList.add("hl"); comboHl = i; }); elTDrop.addEventListener("click", (e) => { const it = e.target && e.target.closest ? e.target.closest(".titem") : null; if (it) pickTarget(it.getAttribute("data-name") || ""); }); window.addEventListener("pointerdown", (e) => { if (!comboOpen) return; const path = e.composedPath ? e.composedPath() : []; if (path.indexOf(elTCombo) >= 0) return; closeDrop(); }, true); elTRefresh.addEventListener("click", () => { if (conn.getState() !== "on") { ctx.openSettings(); return; } refreshTargets().then(() => toast(targets.length ? t("combo.refreshed", { n: targets.length }) : t("combo.noneFound"))); }); } // web-picker.src/offline-doc.js function pickSection(pick, index) { const lines = [ "### " + (index + 1) + ". " + (pick.selector || "(no selector)"), "", "- xpath: " + (pick.xpath || ""), "- text: " + (pick.textPreview || "") ]; if (pick.attributes && Object.keys(pick.attributes).length) { const attrs = Object.keys(pick.attributes).slice(0, 20).map((k) => pick.attributes[k] ? k + '="' + pick.attributes[k] + '"' : k); if (attrs.length) lines.push("- attrs: " + attrs.join(" ")); } const r = pick.rect || {}; lines.push("- rect: x=" + (r.x || 0) + " y=" + (r.y || 0) + " w=" + (r.w || 0) + " h=" + (r.h || 0)); lines.push("- note: " + (pick.note || "")); if (pick.group) lines.push("- group: " + pick.group); if (pick.source && pick.source.file) lines.push("- source: " + pick.source.file + ":" + pick.source.line); lines.push(""); return lines; } var RECORD_SCENE_MAX = 40; function recordEventLine(ev) { const parts = ["+`" + ev.t + "ms`", "`" + ev.kind + "`"]; if (ev.sel) parts.push("`" + ev.sel + "`"); if (ev.kind === "input" && ev.value !== void 0) parts.push("value=`" + ev.value + "`"); if (ev.kind === "key" && ev.key) parts.push("key=`" + ev.key + "`"); if (ev.kind === "nav" && ev.to) parts.push("→ " + ev.to); if (ev.text) parts.push('— "' + ev.text + '"'); if (ev.ref) parts.push("(pick #" + ev.ref + ")"); return "- " + parts.join(" "); } function recordSection(rec) { const lines = [ "## Operation timeline", "", "- record: " + rec.id, "- span: " + rec.start + (rec.end ? " → " + rec.end : "") + " (" + new Date(rec.start).toISOString() + (rec.end ? " → " + new Date(rec.end).toISOString() : "") + ")", "- url: " + (rec.url1 && rec.url1 !== rec.url0 ? rec.url0 + " → " + rec.url1 : rec.url0 || ""), "", "The user reproduced the problem by hand while the picker recorded. Events are ordered (`seq`) with millisecond offsets (`t`); `input` values are final field values (passwords masked).", "" ]; for (const ev of rec.events || []) lines.push(recordEventLine(ev)); lines.push(""); if (rec.net && rec.net.length) { lines.push("### Network (window slice)", ""); for (const n of rec.net.slice(-RECORD_SCENE_MAX)) { const status = n.pending ? "pending" : String(n.status === void 0 ? "?" : n.status); const sse = n.kind === "sse" ? " sse" + (n.msgs !== void 0 ? " msgs=" + n.msgs : "") : ""; const dur = n.durationMs !== void 0 ? " " + n.durationMs + "ms" : ""; lines.push("- " + (n.method || "?") + " " + (n.url || "?") + " → " + status + sse + dur + (n.error ? ' error="' + n.error + '"' : "")); } lines.push(""); } if (rec.console && rec.console.length) { lines.push("### Console (window slice)", ""); for (const c of rec.console.slice(-RECORD_SCENE_MAX)) { lines.push("- [" + (c.level || "log") + "] " + (c.text || "")); } lines.push(""); } return lines; } function renderOfflineHandoffDoc({ prompt, page, picks, record, target }) { const ts = page && page.ts || Date.now(); const lines = [ "# Web annotation handoff", "", "## Request", "", prompt || "", "", "## Page", "", "- url: " + (page && page.url || ""), "- title: " + (page && page.title || ""), "- ts: " + ts + " (" + new Date(ts).toISOString() + ")", "" ]; if (target) lines.push("- expected receiver: " + target); if (target) lines.push(""); lines.push("## Annotations", ""); const list = Array.isArray(picks) ? picks : []; if (!list.length) lines.push("_No elements were picked on this page._", ""); list.forEach((pick, i) => { lines.push(...pickSection(pick || {}, i)); }); if (record) lines.push(...recordSection(record)); lines.push( "## Offline composition", "", "This document was assembled inside the browser tab by the pi-xfer web-picker userscript: the local broker was not reachable, so **there is no way to query this page later** (no reverse channel, no live browser link). Everything you need is above — the picks, the recorded operation timeline, and its console/network slices. Treat this document as the complete record of what the user saw and did.", "", "---", "", "from: web-picker (browser userscript — composed offline in the tab, no broker link; save this file and open it in an agent session)", "handoff_id: local-" + ts ); return lines.join("\n") + "\n"; } // web-picker.src/ui/send.js function copyText(text) { try { GM_setClipboard(text, "text"); return true; } catch (e) { } try { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).catch(() => { }); return true; } } catch (e) { } return false; } function initSend(ctx) { const { els, toast } = ctx; const { elPrompt, elSend, elClear, elSendGroup, elSendMore, elSendMenu, elCopyPrompt, elConn, elConnText, elSendOffline } = els; const conn = ctx.conn; function composeOffline() { const prompt = elPrompt.value.trim() || DEFAULT_PROMPT; const target = ctx.getComboSel() || gm.get(GM_TARGET, "") || ""; const text = renderOfflineHandoffDoc({ prompt, page: { url: location.href, title: document.title, ts: Date.now() }, picks: loadBatch(), record: ctx.getRecord ? ctx.getRecord() : null, target }); if (!copyText(text)) { toast(t("send.clipboard")); return; } toast(t("send.copiedOffline", { n: text.length })); } async function copyHandoffPrompt() { ctx.closeSendMenu(); const prompt = elPrompt.value.trim(); const target = ctx.getComboSel(); const hasPicks = loadBatch().length > 0; const record = ctx.getRecord ? ctx.getRecord() : null; if (!prompt && !hasPicks && !record) { toast(t("send.needContent")); return; } if (conn.getState() !== "on") { composeOffline(); return; } toast(t("send.composing")); const res = await conn.requestCompose(prompt, target, record); if (!res.ok) { toast(t("send.composeFailed", { detail: res.code + (res.message ? " — " + res.message : "") })); return; } if (!copyText(res.text)) { toast(t("send.clipboard")); return; } toast(t("send.copied", { n: res.text.length })); } async function doSend() { const prompt = elPrompt.value.trim(); const target = ctx.getComboSel(); if (!target) { toast(conn.getState() !== "on" ? t("send.noTargetOffline") : t("send.noTarget")); return; } const hasPicks = loadBatch().length > 0; const record = ctx.getRecord ? ctx.getRecord() : null; if (!prompt && !hasPicks && !record) { toast(t("send.needContent")); return; } if (!hasPicks && !record && !confirm(t("send.promptOnly.confirm"))) return; elSend.disabled = true; elSend.textContent = t("send.sending"); const res = await conn.submitToAgent(prompt, target, record); elSend.disabled = false; elSend.textContent = t("sendbtn"); if (res.ok) { gm.set(GM_TARGET, target); toast(t("send.delivered", { id: res.result && res.result.handoff_id ? res.result.handoff_id : "?" })); elPrompt.value = ""; ctx.clearBatch(); if (record && ctx.clearRecord) ctx.clearRecord(); ctx.closePanel(); } else { toast(t("send.failed", { detail: res.code + (res.message ? " — " + res.message : "") })); } } elSend.addEventListener("click", doSend); elPrompt.addEventListener("keydown", (e) => { if (e.isComposing || e.keyCode === 229) return; if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); doSend(); } }); let sendMenuOpen = false; function closeSendMenu() { if (!sendMenuOpen) return; sendMenuOpen = false; elSendMenu.classList.remove("open"); } ctx.closeSendMenu = closeSendMenu; ctx.sendMenuOpen = () => sendMenuOpen; elSendMore.addEventListener("click", () => { sendMenuOpen = !sendMenuOpen; elSendMenu.classList.toggle("open", sendMenuOpen); }); elCopyPrompt.addEventListener("click", copyHandoffPrompt); if (elSendOffline) elSendOffline.addEventListener("click", copyHandoffPrompt); ctx.copyHandoffPrompt = copyHandoffPrompt; window.addEventListener("pointerdown", (e) => { if (!sendMenuOpen) return; const path = e.composedPath ? e.composedPath() : []; if (path.indexOf(elSendGroup) >= 0) return; closeSendMenu(); }, true); elClear.addEventListener("click", () => { const n = loadBatch().length; if (!n) return; ctx.clearBatch(); ctx.renderPanel(); toast(t("pick.cleared", { n })); }); elConn.addEventListener("click", () => { if (conn.getState() === "on") { conn.disconnect(); return; } if (conn.getState() !== "off") return; toast(t("send.connecting", { url: conn.brokerUrl() })); conn.connect().then((ok) => { if (!ok) ctx.openSettings(); }); }); function renderSendAffordance(s) { const linked = s === "on"; const busy = s === "connecting"; elSendGroup.style.display = linked || busy ? "" : "none"; elSend.disabled = !linked; if (elSendOffline) elSendOffline.hidden = linked || busy; if (!linked) closeSendMenu(); } ctx.renderSendAffordance = renderSendAffordance; ctx.renderConnPill = (s) => { elConn.className = s === "on" ? "on" : s === "connecting" ? "connecting" : s === "blocked" ? "blocked" : ""; elConn.title = s === "on" ? t("pill.disconnect.title") : s === "connecting" ? t("conn.connecting") : s === "blocked" ? t("pill.blocked.title") : t("pill.off.title"); elConnText.textContent = s === "on" ? t("conn.on") : s === "connecting" ? t("conn.connecting") : s === "blocked" ? t("conn.blocked") : t("conn.off"); renderSendAffordance(s); }; } // web-picker.src/ui/settings.js function initSettings(ctx) { const { els, toast } = ctx; const { elSettings, elSUrl, elSSave, elSCancel, elSProps, elSAuto, elSWrite } = els; const conn = ctx.conn; function openSettings() { elSUrl.value = conn.brokerUrl(); elSProps.checked = gm.get(GM_FPROPS, false) === true; elSAuto.checked = conn.autoLinkAllowed(); elSWrite.checked = gm.get(writeOpsKey(location.origin), false) === true; elSettings.style.display = "block"; setTimeout(() => elSUrl.focus(), 0); } function closeSettings() { elSettings.style.display = "none"; } ctx.openSettings = openSettings; ctx.closeSettings = closeSettings; ctx.settingsOpen = () => elSettings.style.display === "block"; elSProps.addEventListener("change", () => { gm.set(GM_FPROPS, elSProps.checked); toast(t("settings.toast.fprops", { state: t(elSProps.checked ? "settings.toast.on" : "settings.toast.off") })); }); elSAuto.addEventListener("change", () => { gm.set(autoLinkKey(location.origin), elSAuto.checked); toast(t("settings.toast.autoLink", { state: t(elSAuto.checked ? "settings.toast.on" : "settings.toast.revoked") })); }); elSWrite.addEventListener("change", () => { gm.set(writeOpsKey(location.origin), elSWrite.checked); toast(t("settings.toast.write", { state: t(elSWrite.checked ? "settings.toast.on" : "settings.toast.off") })); }); elSSave.addEventListener("click", () => { const url = elSUrl.value.trim() || DEFAULT_BROKER_URL; gm.set("wp.brokerUrl", url); closeSettings(); toast(t("settings.toast.saved")); conn.connect(); }); elSCancel.addEventListener("click", closeSettings); } // web-picker.src/ui/draggable.js function makeDraggable(el, handle, opts) { const o = opts || {}; let drag = null; function clampPos(x, y) { const r = el.getBoundingClientRect(); return { x: Math.max(4, Math.min(window.innerWidth - Math.max(48, r.width) - 4, x)), y: Math.max(4, Math.min(window.innerHeight - Math.max(28, r.height) - 4, y)) }; } function apply(x, y) { const p = clampPos(x, y); el.style.left = p.x + "px"; el.style.top = p.y + "px"; el.style.right = "auto"; el.style.transform = "none"; } handle.addEventListener("pointerdown", (e) => { if (e.button !== 0) return; if (e.target.closest && e.target.closest("button, textarea, input, select")) return; const r = el.getBoundingClientRect(); drag = { sx: e.clientX, sy: e.clientY, ox: r.left, oy: r.top, target: e.target, moved: false }; e.preventDefault(); }); window.addEventListener("pointermove", (e) => { if (!drag) return; if (!drag.moved && Math.abs(e.clientX - drag.sx) + Math.abs(e.clientY - drag.sy) <= 4) return; drag.moved = true; apply(drag.ox + e.clientX - drag.sx, drag.oy + e.clientY - drag.sy); }, true); window.addEventListener("pointerup", (e) => { if (!drag) return; const d = drag; drag = null; if (!d.moved) { if (o.onTap) o.onTap(d.target, e); return; } if (!o.key) return; try { const r = el.getBoundingClientRect(); sessionStorage.setItem(o.key, JSON.stringify({ x: r.left, y: r.top })); } catch (err) { } }, true); window.addEventListener("pointercancel", () => { drag = null; }, true); if (o.key) { try { const s = sessionStorage.getItem(o.key); if (s) { const p = JSON.parse(s); if (typeof p.x === "number" && typeof p.y === "number") apply(p.x, p.y); } } catch (err) { } } return { // 视口变小(打开 devtools 等)后把浮层拉回可视区;隐藏中的浮层跳过, // 由 setActive/openPanel 显示时补 clamp clamp() { const r = el.getBoundingClientRect(); if (!r.width && !r.height) return; const p = clampPos(r.left, r.top); if (p.x !== r.left || p.y !== r.top) apply(p.x, p.y); } }; } // web-picker.src/ui/fab.js function initFab(ctx) { const { ui, toast } = ctx; const { host, els } = ui; const { elFab, elCnt, elBar, elCard, elPanel, elPH } = els; let drag = null; function fabPointerDown(e) { if (!ctx.isFabTarget(e)) return; e.stopPropagation(); e.preventDefault(); if (e.button !== 0) return; drag = { sx: e.clientX, sy: e.clientY, ox: ui.pos.x, oy: ui.pos.y, moved: false }; try { elFab.setPointerCapture(e.pointerId); } catch (err) { } } function fabPointerMove(e) { if (!ctx.isFabTarget(e) && !drag) return; e.stopPropagation(); e.preventDefault(); if (!drag) return; const dx = e.clientX - drag.sx, dy = e.clientY - drag.sy; if (Math.abs(dx) + Math.abs(dy) > 4) drag.moved = true; ui.pos.x = Math.max(4, Math.min(window.innerWidth - 50, drag.ox + dx)); ui.pos.y = Math.max(4, Math.min(window.innerHeight - 50, drag.oy + dy)); elFab.style.left = ui.pos.x + "px"; elFab.style.top = ui.pos.y + "px"; } function overBadge(x, y) { const r = elCnt.getBoundingClientRect(); if (r.width <= 0 || r.height <= 0) return false; return x >= r.left - 3 && x <= r.right + 3 && y >= r.top - 3 && y <= r.bottom + 3; } function fabPointerUp(e) { if (!drag) return; e.stopPropagation(); e.preventDefault(); const wasMoved = drag.moved; const hitBadge = overBadge(e.clientX, e.clientY); drag = null; try { sessionStorage.setItem(KEY_POS, JSON.stringify(ui.pos)); } catch (err) { } if (!wasMoved) { if (hitBadge) ctx.togglePanel(); else ctx.setActive(true); } } window.addEventListener("pointerdown", fabPointerDown, true); window.addEventListener("pointermove", fabPointerMove, true); window.addEventListener("pointerup", fabPointerUp, true); const barDrag = makeDraggable(elBar, elBar, { key: KEY_BAR_POS, onTap(target) { const t2 = target && target.closest ? target : null; if (!t2) return; if (t2.closest("#gclear")) ctx.clearPendingGroup(); else if (t2.closest("#fz")) ctx.toggleFreeze(); else if (t2.closest("#escx")) ctx.setActive(false); } }); const cardDrag = makeDraggable(elCard, elCard, {}); const panelDrag = makeDraggable(elPanel, elPH, { key: KEY_PANEL_POS }); ctx.barDrag = barDrag; ctx.cardDrag = cardDrag; ctx.panelDrag = panelDrag; window.addEventListener("resize", () => { barDrag.clamp(); panelDrag.clamp(); cardDrag.clamp(); }, true); void host; void toast; } // web-picker.src/ui/hotkeys.js function initHotkeys(ctx) { const { root, els, toast } = ctx; const { elTxt, elSettings } = els; function updateFreezeUI() { const fz = root.getElementById("fz"); if (!fz) return; fz.classList.toggle("on", ctx.pickState.frozen); fz.title = ctx.pickState.frozen ? t("fz.on.title") : t("fz.off.title"); } ctx.updateFreezeUI = updateFreezeUI; function toggleFreeze() { ctx.pickState.frozen = !ctx.pickState.frozen; updateFreezeUI(); toast(ctx.pickState.frozen ? t("fz.on") : t("fz.off")); } ctx.toggleFreeze = toggleFreeze; window.addEventListener("keydown", (e) => { const isHot = e.code === HOTKEY.code && e.altKey === HOTKEY.alt && e.shiftKey === HOTKEY.shift && !e.ctrlKey && !e.metaKey; const isRecHot = e.code === HOTKEY_REC.code && e.altKey === HOTKEY_REC.alt && e.shiftKey === HOTKEY_REC.shift && !e.ctrlKey && !e.metaKey; const isList = e.code === "KeyL" && e.altKey && e.shiftKey && !e.ctrlKey && !e.metaKey; if (isRecHot) { e.preventDefault(); e.stopPropagation(); ctx.toggleRecord(); return; } if (isList) { e.preventDefault(); e.stopPropagation(); ctx.togglePanel(); return; } if (isHot) { e.preventDefault(); e.stopPropagation(); if (ctx.pickState.pickMode) { if (ctx.pickState.pinned) ctx.unpin(); else ctx.setActive(false); } else ctx.setActive(true); return; } if (ctx.pickState.pickMode && !ctx.pickState.pinned && e.code === "KeyF" && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) { e.preventDefault(); e.stopPropagation(); toggleFreeze(); } }, true); document.addEventListener("keydown", (e) => { const ps = ctx.pickState; if (ctx.sendMenuOpen() && e.key === "Escape" && !e.isComposing && e.keyCode !== 229) { e.preventDefault(); e.stopPropagation(); ctx.closeSendMenu(); return; } if (ctx.comboOpen() && e.key === "Escape" && !e.isComposing && e.keyCode !== 229) { e.preventDefault(); e.stopPropagation(); ctx.closeDrop(); return; } if (ctx.getPanelOpen() && e.key === "Escape" && !e.isComposing && e.keyCode !== 229) { e.preventDefault(); ctx.closePanel(); return; } if (!ps.pickMode) return; if (ps.groupCard) { if (e.isComposing || e.keyCode === 229) return; const firstTarget = e.composedPath && e.composedPath()[0] || e.target; if (e.key === "Enter" && firstTarget === elTxt && !e.shiftKey) { e.preventDefault(); ctx.submitGroup(); } else if (e.key === "Escape") { ctx.closeGroupCard(); } return; } if (ps.pinned) { if (e.isComposing || e.keyCode === 229) return; const firstTarget = e.composedPath && e.composedPath()[0] || e.target; if (e.key === "Enter" && firstTarget === elTxt && !e.shiftKey) { e.preventDefault(); ctx.submit(); } else if (e.key === "Escape") { ctx.unpin(); } return; } if (e.key === "Backspace" && ps.groupCount && !e.isComposing && e.keyCode !== 229) { e.preventDefault(); e.stopPropagation(); ctx.clearPendingGroup(); return; } if (e.key === "Enter" && e.shiftKey && !e.isComposing && e.keyCode !== 229) { e.preventDefault(); e.stopPropagation(); ctx.toggleGroup(ps.currentEl()); return; } switch (e.key) { case "]": case "ArrowDown": e.preventDefault(); ctx.shiftLayer(1); break; case "[": case "ArrowUp": e.preventDefault(); ctx.shiftLayer(-1); break; case "Enter": case " ": e.preventDefault(); if (ps.groupCount) ctx.openGroupCard(); else ctx.pin(); break; case "Escape": e.preventDefault(); ctx.setActive(false); break; default: if (/^[1-9]$/.test(e.key)) { e.preventDefault(); ps.idx = Math.min(ps.stack.length - 1, +e.key - 1); ctx.refresh(); } } }, true); function overlayActive() { return ctx.pickState.pinned != null || ctx.pickState.groupCard || ctx.getPanelOpen() || elSettings.style.display === "block"; } window.addEventListener("focusin", (e) => { if (!overlayActive()) return; const path = e.composedPath ? e.composedPath() : []; if (path.indexOf(root) < 0) return; e.stopPropagation(); const t2 = path[0]; if (!t2 || !t2.dispatchEvent) return; try { t2.dispatchEvent(new FocusEvent("focusin", { bubbles: true, composed: false, cancelable: false })); } catch (err) { } }, true); function isolatePress(e) { if (!overlayActive()) return; const path = e.composedPath ? e.composedPath() : []; if (path.indexOf(root) < 0) return; e.stopPropagation(); const t2 = path[0]; if (!t2 || !t2.dispatchEvent) return; const base = { bubbles: true, composed: false, cancelable: true, clientX: e.clientX, clientY: e.clientY, screenX: e.screenX, screenY: e.screenY, button: e.button, buttons: e.buttons, detail: e.detail }; try { t2.dispatchEvent(new PointerEvent(e.type, Object.assign(base, { pointerId: e.pointerId, pointerType: e.pointerType, isPrimary: e.isPrimary, pressure: e.pressure }))); } catch (err) { try { t2.dispatchEvent(new MouseEvent(e.type, base)); } catch (err2) { } } } ["pointerdown", "mousedown"].forEach((t2) => window.addEventListener(t2, isolatePress, true)); } // web-picker.src/record.js var INPUT_DEBOUNCE_MS = 1e3; function initRecord(ctx) { const { els, toast } = ctx; const { elRecBar, elRecCount, elRecStop, elRecDiscard, elRecBadge } = els; let status = null; let rec = null; let inputTimer = null; let inputEl = null; let urlTimer = null; ctx.recState = { get active() { return status === "rec"; }, get eventCount() { return status === "rec" && rec ? rec.events.length : 0; } }; function save() { try { sessionStorage.setItem(KEY_REC, JSON.stringify({ status, rec })); } catch (e) { } } function load() { try { const s = sessionStorage.getItem(KEY_REC); if (!s) return; const v = JSON.parse(s); if (v && (v.status === "rec" || v.status === "done") && v.rec && Array.isArray(v.rec.events)) { status = v.status; rec = v.rec; } } catch (e) { } } function now() { return Date.now(); } function t0() { return rec ? rec.start : 0; } function isOurUiEl(el) { try { return !!(el && el.closest && el.closest("[" + HOST_FLAG + "]")); } catch (e) { return false; } } function describe(el) { try { return { sel: cssPath(el), text: (el.textContent || "").trim().replace(/\s+/g, " ").slice(0, 60) || void 0 }; } catch (e) { return { sel: el && el.nodeName ? el.nodeName.toLowerCase() : "unknown" }; } } function push(kind, extra) { if (status !== "rec" || !rec) return; if (rec.events.length >= REC_MAX_EVENTS) return; rec.events.push(Object.assign({ seq: rec.events.length + 1, t: now() - t0(), kind }, extra || {})); save(); renderRecBar(); } function swallowByPickMode() { return !!(ctx.pickState && ctx.pickState.pickMode); } function onCaptureClick(e) { if (status !== "rec" || swallowByPickMode()) return; if (isOurUiEl(e.target)) return; const d = describe(e.target); push("click", d); } function rawValue(el) { try { if (el.type === "password") return "***"; const v = String(el.value ?? ""); return v.slice(0, 120); } catch (e) { return ""; } } function onCaptureInput(e) { if (status !== "rec") return; const el = e.target; const tag = el && el.tagName; if (isOurUiEl(el) || tag !== "INPUT" && tag !== "TEXTAREA" && tag !== "SELECT") return; inputEl = el; clearTimeout(inputTimer); inputTimer = setTimeout(() => { commitInput(el); }, INPUT_DEBOUNCE_MS); } function commitInput(el) { clearTimeout(inputTimer); inputTimer = null; inputEl = null; if (status !== "rec" || !el || isOurUiEl(el)) return; const d = describe(el); const prev = rec.events.find((ev) => ev.kind === "input" && ev.sel === d.sel); const value = rawValue(el); if (prev) { prev.value = value; prev.t = now() - t0(); } else push("input", { sel: d.sel, value }); save(); } function onCaptureChange(e) { if (status !== "rec") return; const el = e.target; if (isOurUiEl(el)) return; if (el && (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT")) commitInput(el); } function onCaptureKey(e) { if (status !== "rec" || swallowByPickMode()) return; if (e.key !== "Enter" || e.isComposing || e.keyCode === 229) return; if (isOurUiEl(e.target)) return; const el = e.target; if (el && el.tagName === "TEXTAREA") return; push("key", { sel: describe(el).sel, key: "Enter" }); } function onCaptureSubmit(e) { if (status !== "rec") return; if (isOurUiEl(e.target)) return; push("submit", { sel: describe(e.target).sel }); } function startUrlWatch() { stopUrlWatch(); let last = location.href; urlTimer = setInterval(() => { if (status !== "rec") return; if (location.href !== last) { const from = last; last = location.href; push("nav", { from: from.slice(0, 200), to: location.href.slice(0, 200) }); } }, 500); } function stopUrlWatch() { if (urlTimer) { clearInterval(urlTimer); urlTimer = null; } } function renderRecBar() { if (status !== "rec") { elRecBar.style.display = "none"; return; } elRecBar.style.display = "flex"; elRecCount.textContent = rec.events.length + " events"; } function start() { if (status === "rec") return; if (status === "done") { rec = null; } status = "rec"; rec = { id: "r" + now().toString(36) + Math.random().toString(36).slice(2, 5), start: now(), end: void 0, url0: location.href, url1: void 0, events: [], console: void 0, net: void 0 }; save(); renderRecBar(); startUrlWatch(); toast(t("record.started")); } function stop(discard) { if (status !== "rec" || !rec) return; stopUrlWatch(); clearTimeout(inputTimer); inputTimer = null; inputEl = null; rec.end = now(); rec.url1 = location.href; rec.events = rec.events.slice(0, REC_MAX_EVENTS); try { const sels = (JSON.parse(sessionStorage.getItem("pi.wp.picks") || "[]") || []).map((p) => p && p.selector); for (const ev of rec.events) { if (ev.sel) { const i = sels.indexOf(ev.sel); if (i >= 0) ev.ref = i + 1; } } } catch (e) { } if (discard) { status = null; rec = null; try { sessionStorage.removeItem(KEY_REC); } catch (e) { } toast(t("record.discarded")); } else { status = "done"; rec.console = ctx.consoleRing.filter((c) => c.ts >= rec.start).slice(-REC_SLICE_MAX); rec.net = ctx.netRing.filter((n) => n.ts >= rec.start).slice(-REC_SLICE_MAX); toast(t("record.done", { n: rec.events.length })); } save(); renderRecBar(); if (ctx.refreshCount) ctx.refreshCount(); } ctx.startRecord = start; ctx.stopRecord = stop; ctx.toggleRecord = () => { if (status === "rec") stop(false); else start(); }; ctx.getRecord = () => status === "done" ? rec : null; ctx.clearRecord = () => { if (status !== "done") return; status = null; rec = null; try { sessionStorage.removeItem(KEY_REC); } catch (e) { } if (ctx.refreshCount) ctx.refreshCount(); }; ctx.renderRecordSummary = () => { if (status !== "done" || !rec) return ""; const ev = rec.events.map( (e) => '
+' + e.t + "ms " + escapeHtml(e.kind) + " " + escapeHtml(e.sel || e.to || "") + (e.value !== void 0 ? " = " + escapeHtml(e.value) : "") + "
" ).join(""); return `
${t("record.summary", { id: escapeHtml(rec.id), n: rec.events.length })}
` + ev + '
' + new Date(rec.start).toLocaleTimeString() + " → " + new Date(rec.end).toLocaleTimeString() + `
`; }; elRecStop.addEventListener("click", (e) => { e.stopPropagation(); stop(false); }); elRecDiscard.addEventListener("click", (e) => { e.stopPropagation(); stop(true); }); elRecBadge.addEventListener("click", () => { if (ctx.togglePanel) ctx.togglePanel(); }); load(); renderRecBar(); if (status === "rec") startUrlWatch(); window.addEventListener("click", onCaptureClick, true); window.addEventListener("input", onCaptureInput, true); window.addEventListener("change", onCaptureChange, true); window.addEventListener("keydown", onCaptureKey, true); window.addEventListener("submit", onCaptureSubmit, true); } // web-picker.src/main.js if (!window.__PI_WEBPICKER__) { window.__PI_WEBPICKER__ = true; try { installCapture(); const ui = buildUI(); const ctx = { ui, toast: ui.toast, root: ui.root, host: ui.host, els: ui.els }; const pageTools = createPageTools({ gm, consoleRing, netRing, debugLog }); ctx.pageTools = pageTools; const conn = createBrokerConn({ gm, debugLog, toast: ui.toast, onState: (s) => { ui.els.elDot.className = s === "on" ? "on" : s === "connecting" ? "connecting" : s === "blocked" ? "blocked" : ""; if (ctx.renderConnPill) ctx.renderConnPill(s); if (s !== "on" && ctx.renderTargetCombo) ctx.renderTargetCombo(); }, onWelcome: () => { if (ctx.refreshTargets) void ctx.refreshTargets(); }, onPageRequest: (f) => { try { pageTools.handlePageToolRequest(f, (id, ok, payload) => { conn.sendFrame(framePageResponse(id, ok, payload)); }); } catch (err) { debugLog("page.request dispatch failed:", err && err.message ? err.message : err); conn.sendFrame(framePageResponse(f.id, false, "dispatch_failed: " + (err && err.message ? err.message : String(err)))); } } }); ctx.conn = conn; initPanel(ctx); initPick(ctx); initCombo(ctx); initSend(ctx); initSettings(ctx); initFab(ctx); ctx.consoleRing = consoleRing; ctx.netRing = netRing; initRecord(ctx); initHotkeys(ctx); ctx.refreshCount(); ctx.updateGroupUI(); ctx.renderTargetCombo(); if (ctx.renderSendAffordance) ctx.renderSendAffordance(conn.getState()); conn.maybeAutoConnect(); conn.maybeProbePolicyBlocked(); debugLog("ready — ⇧⌥P pick · ⇧⌥L panel · ⇧Enter group · ⌫ clear group · " + location.host); window.__PI_WP_API__ = { start: () => { ctx.setActive(true); return true; }, stop: () => { ctx.setActive(false); return true; }, panel: ctx.togglePanel, record: () => ctx.toggleRecord(), recordState: () => ctx.recState, getRecord: () => ctx.getRecord(), connect: () => conn.connect(), disconnect: () => conn.disconnect(), settings: ctx.openSettings, send: (prompt, target) => conn.submitToAgent(prompt, target), copyPrompt: ctx.copyHandoffPrompt, targets: ctx.getTargets, refreshTargets: () => ctx.refreshTargets(), snapshot: () => loadBatch(), tools: () => Object.keys(pageTools.PAGE_TOOLS), consoleLog: () => consoleRing.slice(), netLog: () => netRing.slice(), frameworkProps: () => gm.get("wp.frameworkProps", false) === true, setFrameworkProps: (on) => { gm.set("wp.frameworkProps", !!on); ui.els.elSProps.checked = !!on; return !!on; }, setDebug: (on) => { gm.set(GM_DEBUG, !!on); return !!on; }, reinject: ui.reinjectTrigger }; try { GM_registerMenuCommand(t("menu.connect"), () => conn.connect()); GM_registerMenuCommand(t("menu.disconnect"), () => conn.disconnect()); GM_registerMenuCommand(t("menu.settings"), ctx.openSettings); GM_registerMenuCommand(t("menu.revokeAuto"), () => { conn.revokeAutoLink(); ui.toast(t("toast.autoRevoked", { origin: location.origin })); }); GM_registerMenuCommand(t("menu.openPanel"), ctx.togglePanel); GM_registerMenuCommand(t("menu.startPick"), () => ctx.setActive(true)); GM_registerMenuCommand(t("menu.toggleRecord"), () => ctx.toggleRecord()); GM_registerMenuCommand(t("menu.reinject"), () => { ui.toast(ui.reinjectTrigger() ? t("toast.reinjected") : t("toast.reinjectStuck")); }); GM_registerMenuCommand(t("menu.lang"), () => { const lang2 = toggleLang(); ui.toast(t("toast.langSwitched")); debugLog("language set to", lang2); }); GM_registerMenuCommand(t("menu.fabSize"), () => { const next = gm.get(GM_FAB_SIZE, "small") === "small" ? "large" : "small"; gm.set(GM_FAB_SIZE, next); ui.els.elFab.classList.toggle("large", next === "large"); ui.toast(t("toast.fabSize", { size: next })); }); } catch (e) { } } catch (err) { try { failSafeNotice(err); } catch (e) { } debugLog("init failed:", err && err.message ? err.message : err); } } function failSafeNotice(err) { const msg = String(err && err.message || err || "unknown error"); const box = document.createElement("div"); box.setAttribute("data-pi-wp-error", ""); box.textContent = "pi-xfer web picker failed to start: " + msg; Object.assign(box.style, { position: "fixed", right: "12px", bottom: "12px", zIndex: "2147483647", maxWidth: "380px", padding: "10px 12px", borderRadius: "8px", background: "#1e293b", color: "#f8fafc", font: "12px/1.5 system-ui, sans-serif" }); (document.body || document.documentElement).appendChild(box); setTimeout(() => { try { box.remove(); } catch (e) { } }, 12e3); } })();