browserzap 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/README.md +53 -30
- package/bridge-server/background-runs.js +91 -4
- package/bridge-server/chrome-launcher.js +42 -1
- package/bridge-server/cli-run.js +2 -0
- package/bridge-server/downloads.js +173 -0
- package/bridge-server/executor.js +1 -1
- package/bridge-server/extension-id.js +25 -1
- package/bridge-server/feedback.js +255 -0
- package/bridge-server/index.js +29 -2
- package/bridge-server/limits.js +27 -1
- package/bridge-server/mcp-server.js +219 -18
- package/bridge-server/native-host.js +5 -3
- package/bridge-server/panel.js +21 -3
- package/bridge-server/probe.js +2 -1
- package/bridge-server/resolvers.js +287 -0
- package/bridge-server/server.js +2 -1
- package/bridge-server/setup.js +184 -27
- package/bridge-server/tools.js +153 -4
- package/bridge-server/workflows.js +298 -11
- package/extension/background.js +1 -1
- package/extension/engine.js +1 -1
- package/extension/manifest.json +1 -1
- package/extension/sidepanel.css +1 -1
- package/extension/sidepanel.html +23 -2
- package/extension/sidepanel.js +1 -1
- package/package.json +2 -1
package/extension/sidepanel.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(()=>{"use strict";const e=document.getElementById("status-badge"),t=document.querySelector(".status-dot"),n=document.getElementById("status-message"),a=document.getElementById("status-hint"),s=document.getElementById("port-display"),o=document.getElementById("port-input"),l=document.getElementById("save-port-btn"),r=document.getElementById("token-input"),i=document.getElementById("save-token-btn"),c=document.getElementById("profile-input"),d=document.getElementById("save-profile-btn"),p=document.getElementById("pairing-notice"),u=document.getElementById("pairing-setup"),m=document.getElementById("pairing-fallback"),f=document.getElementById("conflict-banner"),b=document.getElementById("reconnect-btn"),g=document.getElementById("sessions-container"),v=document.getElementById("action-log"),w=document.getElementById("clear-log-btn"),x=document.getElementById("plan-badge"),h=document.getElementById("licence-input"),$=document.getElementById("licence-save-btn"),y=document.getElementById("licence-remove-btn"),k=document.getElementById("licence-status"),E=document.getElementById("device-id"),I="pill pill-muted",C="mt-1 inline-block size-2 shrink-0 rounded-full",L={on:`status-dot connected ${C} bg-ok`,off:`status-dot disconnected ${C} bg-fg-2`},B={success:"bg-ok",error:"bg-danger",pending:"bg-warn"},_=e=>e?"active border-accent bg-accent/15 text-accent":"";let T=null;function S(e,t){return chrome.runtime.sendMessage({source:"sidepanel",type:e,...t})}function j(){if(!T)return;const o=!T.paired||Boolean(T.authorizationError);if(p.hidden=!o,o){const s=!1===T.nativeHost;u.hidden=!1,m.hidden=!s&&!T.authorizationError,e.textContent=T.paired?"token rejected":"not paired",e.className=I,t.className=L.off,n.textContent=T.authorizationError?"The bridge rejected the saved pairing token":"Pair this extension with the bridge to begin",a.textContent=s?"Run: npm install -g browserzap, then browserzap setup":null===T.nativeHost?"Starting the bridge through the native host…":"The bridge only accepts authenticated connections from this machine."}else if(T.versionError){e.textContent="update needed",e.className=I,t.className=L.off,n.textContent=T.versionError;const s=/bridge speaks protocol (\d+), the client sent (\d+)/.exec(T.versionError);a.textContent=s&&Number(s[1])<Number(s[2])?"An older BrowserZap bridge is still running. In the BrowserZap folder run: npm run bridge:restart":'Run "npm run build", reload the extension in chrome://extensions, then run: npm run bridge:restart'}else if(T.connected){const s=A(T.sessions).length;e.textContent=s>0?`${s} agent${s>1?"s":""}`:"online",e.className="pill pill-ok",t.className=L.on,n.textContent=s>0?`${s} agent session${s>1?"s":""} active`:"Bridge connected, waiting for an agent";const o=T.profile;a.textContent=o&&o.connected>1?`This profile: ${o.label}. ${o.connected} Chrome profiles are connected; agents choose one with browser_select_profile.`:o&&o.label&&!/^Profile \d+$/.test(o.label)?`This profile: ${o.label}`:""}else T.conflictReason?(e.textContent="conflict",e.className=I,t.className=L.off,n.textContent=T.conflictReason,a.textContent="Close the other browser, or disable its BrowserZap extension, to use this one."):(e.textContent="offline",e.className=I,t.className=L.off,n.innerHTML=`Waiting for the bridge on port <strong>${s.textContent}</strong>`,a.textContent="The extension retries automatically");const l=T.authorizationError??T.versionError??T.conflictReason??"";var r;f.hidden=0===l.length,f.textContent=l,0!==(r=T.sessions.filter(e=>e.id!==R||(e.approvals??[]).length>0||e.pendingUserAction)).length?g.innerHTML=r.map(e=>{const t=M(e.id),n=e.pendingUserAction?`\n <div class="user-action-alert mt-1 flex flex-col gap-1.5 rounded-md border border-warn/40 bg-warn/10 p-2">\n <h3 class="m-0 text-[12px] font-bold">Action needed</h3>\n <p>${M(e.pendingUserAction)}</p>\n <textarea data-session="${t}" placeholder="Describe what you did (optional)" rows="2"></textarea>\n <button class="btn btn-primary w-full user-done-btn" data-session="${t}">\n Hand back to AI\n </button>\n </div>\n `:"",a=(e.approvals??[]).map(e=>`\n <div class="user-action-alert approval-alert mt-1 flex flex-col gap-1.5 rounded-md border border-accent/40 bg-accent/10 p-2">\n <h3 class="m-0 text-[12px] font-bold">Approve this action?</h3>\n <p>${M(e.summary)}</p>\n <p class="meta m-0">${M(e.risk.replace(/_/g," "))} · ${M(e.origin)}</p>\n <div class="flex gap-1.5">\n <button class="btn btn-primary approval-btn" data-session="${t}" data-approval="${M(e.id)}" data-decision="approve">Approve once</button>\n <button class="btn btn-ghost approval-btn" data-session="${t}" data-approval="${M(e.id)}" data-decision="deny">Deny</button>\n </div>\n </div>\n `).join("");return`\n <section class="session-card panel flex flex-col gap-2">\n <div class="flex items-center justify-between gap-2">\n <span class="font-semibold">${M(e.name)}</span>\n <span class="font-mono text-[10px] text-fg-2">${t}</span>\n </div>\n <div class="flex gap-1">\n <button class="mode-btn btn flex-1 ${_("collaborative"===e.controlMode)}"\n data-session="${t}" data-mode="collaborative" title="Both user and AI">\n <span>Collab</span>\n </button>\n <button class="mode-btn btn flex-1 ${_("ai"===e.controlMode)}"\n data-session="${t}" data-mode="ai" title="AI has full control">\n <span>AI</span>\n </button>\n <button class="mode-btn btn flex-1 ${_("user"===e.controlMode)}"\n data-session="${t}" data-mode="user" title="User has control">\n <span>User</span>\n </button>\n </div>\n ${a}\n ${n}\n </section>\n `}).join(""):g.innerHTML=T?.connected?'<div class="hint mx-3 my-1">Waiting for agent sessions…</div>':"",function(e){if(0===e.length)return void(v.innerHTML='<div class="hint py-2">Waiting for an agent</div>');const t=e.slice(-50);v.innerHTML=t.map(e=>{const t=new Date(e.timestamp).toLocaleTimeString("en-US",{hour12:!1,hour:"2-digit",minute:"2-digit",second:"2-digit"}),n=e.session?`<span class="font-mono text-fg-2">${e.session.slice(0,4)}</span>`:"";return`\n <div class="log-entry flex items-start gap-1.5 border-b border-line py-1 text-[11px]">\n <div class="log-status ${e.status} mt-1 size-1.5 shrink-0 rounded-full ${B[e.status]??"bg-fg-2"}"></div>\n <span class="log-source ${e.source} text-[9px] font-bold uppercase text-fg-2">${e.source}</span>\n ${n}\n <div class="min-w-0 flex-1">\n <div>${M(e.action)}</div>\n ${e.details?`<div class="truncate text-fg-2" title="${M(e.details)}">${M(e.details)}</div>`:""}\n </div>\n <span class="font-mono text-[10px] text-fg-2">${t}</span>\n </div>\n `}).join(""),v.scrollTop=v.scrollHeight}(T.actionLog),X()}!function e(){const t=chrome.runtime.connect({name:"sidepanel-keepalive"});t.onDisconnect.addListener(()=>{setTimeout(e,1e3)}),setTimeout(()=>{try{t.disconnect()}catch{}},2e4)}(),chrome.runtime.onMessage.addListener(e=>{"background"===e.source&&"state_update"===e.type&&(T=e.data,j())}),l.addEventListener("click",async()=>{const e=parseInt(o.value,10);e>=1024&&e<=65535&&(await chrome.storage.local.set({bridgePort:e}),s.textContent=String(e),await S("disconnect"),await S("connect",{port:e}))}),i.addEventListener("click",async()=>{const e=r.value.trim();await S("set_token",{token:e}),r.value="",r.placeholder=e?"paired — paste a new token to replace":"paste token"}),d.addEventListener("click",async()=>{await S("set_profile_label",{label:c.value})}),$.addEventListener("click",async()=>{const e=h.value.trim();if(!e)return;const t=await K("licence_activate",{token:e});h.value="",t.ok?await V():k.textContent=t.error?.message??"That licence was not accepted."}),y.addEventListener("click",async()=>{await K("licence_remove"),await V()}),chrome.storage.local.get(["bz_profile_label"]).then(e=>{"string"==typeof e.bz_profile_label&&(c.value=e.bz_profile_label)}).catch(()=>{}),b.addEventListener("click",async()=>{const e=parseInt(o.value,10);await S("disconnect"),await S("connect",{port:e})}),w.addEventListener("click",()=>{v.innerHTML='<div class="hint py-2">Log cleared</div>'}),g.addEventListener("click",e=>{const t=e.target,n=t.closest(".mode-btn");if(n)return void S("set_control_mode",{sessionId:n.dataset.session,data:n.dataset.mode});const a=t.closest(".approval-btn");if(a)return void S("decide_approval",{sessionId:a.dataset.session,approvalId:a.dataset.approval,approve:"approve"===a.dataset.decision});const s=t.closest(".user-done-btn");if(s){const e=s.dataset.session,t=g.querySelector(`textarea[data-session="${CSS.escape(e)}"]`);S("set_control_mode",{sessionId:e,data:"collaborative",userMessage:t?.value?.trim()||void 0}),t&&(t.value="")}});const R="panel",A=e=>e.filter(e=>e.id!==R);function M(e){return String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}const N=document.getElementById("workflows-panel"),H=document.getElementById("wf-recording"),D=document.getElementById("wf-run"),z=document.getElementById("wf-message"),P=document.getElementById("wf-list");let U=[],q={},O=null,F={},W="";const Z=e=>/is sensitive/.test(e?.description??"");function K(e,t={}){return S("panel",{op:e,args:t})}function Q(e,t=!1){z.textContent=e,z.classList.toggle("text-danger",t)}async function V(){const e=await K("list");e?.ok?(U=e.data?.workflows??[],q=e.data?.lastRuns??{},function(e,t){if(t?.short&&(E.textContent=t.short),!e)return;const n="pro"===e.plan;x.textContent=n?"pro":"free",x.className="pill "+(n?"pill-ok":"pill-muted");const a=n?`Pro${e.expiresAt?` until ${new Date(e.expiresAt).toLocaleDateString()}`:" — no expiry"}${e.licenseId?` · ${e.licenseId}`:""}.`:"Free plan — every browser tool and every safety control. No licence needed. Pro (bulk runs, history, reports) is coming soon.";k.textContent=[a,e.notice,e.reason].filter(Boolean).join(" "),y.hidden=!n}(e.data?.plan,e.data?.device),Q("")):(U=[],Q(e?.error?.message??"Could not load workflows.",!0)),Y()}function Y(){0!==U.length?P.innerHTML=U.map(e=>{const t=M(e.name);if(e.broken)return`<div class="card"><span class="break-words font-semibold">${t}</span><span class="meta">Cannot be used: ${M(e.broken)}</span></div>`;const n=q[e.name],a=n?`<span class="pill ${n.ok?"pill-ok":"USER_ACTION_REQUIRED"===n.code?"pill-warn":"pill-bad"}">${n.ok?"last run ok":M((n.code??"failed").toLowerCase().replace(/_/g," "))}</span>`:'<span class="pill pill-muted">never run</span>',s=O?.name===e.name?function(e){const t=M(e.name),n=Object.entries(e.params??{}).map(([e,t])=>{const n=M(e);return Z(t)?`<label class="flex flex-col gap-0.5 meta">${n}<span class="meta">Sensitive: the run pauses and you type it yourself in the page.</span></label>`:`<label class="flex flex-col gap-0.5 meta">${M(t.description||e)}\n <input type="text" data-param="${n}" value="${M(t.default??"")}" autocomplete="off" spellcheck="false"></label>`}).join(""),a=Object.entries(e.params??{}).filter(([,e])=>!Z(e)).map(([e])=>e);return`\n <div class="wf-form flex flex-col gap-1.5" data-name="${t}">\n ${n||'<span class="meta">No values needed.</span>'}\n <div class="flex flex-wrap gap-1.5">\n <button class="btn btn-primary wf-start" data-name="${t}">Start</button>\n <button class="btn btn-ghost wf-close">Close</button>\n </div>\n ${a.length?`\n <details class="wf-many">\n <summary class="meta cursor-pointer">Run for many (Pro)</summary>\n <div class="mt-1.5 flex flex-col gap-1.5">\n <label class="flex flex-col gap-0.5 meta">One row per run. First row: ${M(a.join(","))}${1===a.length?" (or just one value per line)":""}\n <textarea class="wf-many-text h-24 font-mono" spellcheck="false" placeholder="${M(a.join(","))}"></textarea></label>\n <span class="meta">Runs one after the other. A row that fails is reported and the next one runs.</span>\n <div class="flex flex-wrap gap-1.5"><button class="btn btn-primary wf-start-many" data-name="${t}">Run all</button></div>\n </div>\n </details>`:""}\n </div>`}(O.detail):"";return`\n <div class="card">\n <div class="flex items-center justify-between gap-2"><span class="break-words font-semibold">${t}</span>${a}</div>\n ${e.description?`<span class="meta">${M(e.description)}</span>`:""}\n <span class="meta">${e.steps??0} steps · ${M((e.origins??[]).join(", "))}${n?` · ${M(new Date(n.at).toLocaleString())}`:""}</span>\n ${n&&!n.ok?`<span class="meta">${M(n.summary)}</span>`:""}\n ${s||`<div class="flex flex-wrap gap-1.5"><button class="btn btn-primary wf-open" data-name="${t}">Run…</button></div>`}\n </div>`}).join(""):P.innerHTML='<div class="hint">No workflows yet. Press <b>Record</b>, do a task once in the current tab, then save it.</div>'}let G="",J=!1;function X(){T?.connected&&!J&&V(),J=Boolean(T?.connected);const e=T?.workflowRun??null;!function(e){if(!e)return void(H.innerHTML="");const t=e.steps.map((e,t)=>`\n <li>${M(e.label)}${e.sensitive?'<span class="ml-1 text-[10px] font-bold text-accent" title="You type this yourself on every run">sensitive</span>':""}${e.ambiguous?'<span class="ml-1 text-[10px] font-bold text-warn" title="This target is not unique; the run may stop here">check</span>':""}\n ${t>0?`<button class="btn btn-ghost wf-remove ml-1 px-1 py-0 text-[10px]" data-index="${t}" title="Remove this step">✕</button>`:""}</li>`).join("");e.stopped?H.innerHTML=`\n <div class="card border-accent/40 bg-accent/10">\n <div class="flex items-center justify-between gap-2"><span class="break-words font-semibold">Review and save</span><span class="meta">${e.steps.length} steps</span></div>\n <ol class="m-0 flex list-decimal flex-col gap-0.5 pl-4 text-[12px]">${t}</ol>\n <div class="wf-form flex flex-col gap-1.5">\n <label class="flex flex-col gap-0.5 meta">Name (lowercase letters, digits, - and _)<input type="text" id="wf-save-name" placeholder="e.g. enable-comments" autocomplete="off" spellcheck="false"></label>\n <label class="flex flex-col gap-0.5 meta">Description<input type="text" id="wf-save-desc" autocomplete="off"></label>\n <label class="flex flex-row items-center gap-1.5 meta"><input type="checkbox" id="wf-save-keep"> Keep what I typed as default values</label>\n <label class="flex flex-row items-center gap-1.5 meta"><input type="checkbox" id="wf-save-overwrite"> Replace a workflow with the same name</label>\n <div class="flex flex-wrap gap-1.5">\n <button class="btn btn-primary wf-rec-save">Save workflow</button>\n <button class="btn btn-ghost wf-rec-discard">Discard</button>\n </div>\n </div>\n </div>`:H.innerHTML=`\n <div class="card border-danger/40 bg-danger/10">\n <div class="flex items-center justify-between gap-2"><span class="break-words font-semibold"><span class="mr-1.5 inline-block size-2 animate-pulse rounded-full bg-danger motion-reduce:animate-none"></span>Recording</span><span class="meta">${e.steps.length} steps</span></div>\n <span class="meta">Do the task in the tab as you normally would. Only your own clicks and typing are recorded.</span>\n <ol class="m-0 flex list-decimal flex-col gap-0.5 pl-4 text-[12px]">${t}</ol>\n <div class="flex flex-wrap gap-1.5">\n <button class="btn btn-primary wf-rec-stop">Stop</button>\n <button class="btn btn-ghost wf-rec-discard">Discard</button>\n </div>\n </div>`}(T?.recording??null),function(e){if(!e)return void(D.innerHTML="");const t="running"===e.status||"awaiting_approval"===e.status,n="completed"===e.status?"ok":t||"needs_user"===e.status?"wait":"bad",a=e.batch&&!t?"":e.steps.map(e=>`<li>${e.ok?"✓":"✗"} ${M(e.tool)}${e.ok?"":` — ${M(e.summary)}`}</li>`).join(""),s=e.batch,o=s?`\n <span class="meta">${t?`Set ${s.index+1} of ${s.total} · `:""}${s.done} done${s.failed?` · ${s.failed} need attention`:""}</span>\n ${s.attention.length?`<ul class="m-0 flex list-none flex-col gap-1 p-0 text-[12px]">${s.attention.map(e=>`\n <li class="rounded-md border border-danger/30 bg-danger/5 px-2 py-1">\n <span class="font-semibold">${M(e.label)}</span>: ${e.step?`step ${e.step} (${M(e.tool??"")}${e.target?` ${M(e.target)}`:""})`:"did not start"} — ${M((e.code??"failed").toLowerCase().replace(/_/g," "))}${e.cause?`, ${M(e.cause.replace(/_/g," "))}`:""}\n ${e.screenshot?`<span class="block break-all meta" title="Saved on this computer">Screenshot: ${M(e.screenshot)}</span>`:""}\n </li>`).join("")}</ul>`:""}\n ${s.report?`<span class="meta break-all">Report: ${M(s.report)}</span>`:""}`:"";D.innerHTML=`\n <div class="card border-accent/40 bg-accent/10">\n <div class="flex items-center justify-between gap-2"><span class="break-words font-semibold">${M(e.workflow)}</span><span class="pill pill-${n}">${M(e.status.replace(/_/g," "))}</span></div>\n ${s&&!t?"":`<span class="meta">Step ${Math.min(e.stepIndex+1,e.totalSteps)} of ${e.totalSteps}</span>`}\n ${o}\n <span class="meta">${M(e.summary)}</span>\n ${"awaiting_approval"===e.status?'<span class="text-[12px] font-semibold">Approve or deny the step below, under "BrowserZap workflows".</span>':""}\n ${e.nextAction?`<span class="text-[12px] font-semibold">${M(e.nextAction)}</span>`:""}\n ${a?`<ol class="m-0 flex list-decimal flex-col gap-0.5 pl-4 text-[12px]">${a}</ol>`:""}\n <div class="flex flex-wrap gap-1.5">\n ${t?'<button class="btn btn-danger wf-cancel">Cancel run</button>':""}\n ${s?.canRerun&&!t?'<button class="btn btn-primary wf-rerun-failed">Re-run failed</button>':""}\n ${null===e.canContinueFrom||t?"":`<button class="btn btn-primary wf-continue" data-name="${M(e.workflow)}" data-start="${e.canContinueFrom}">Continue</button>`}\n ${t?"":'<button class="btn btn-ghost wf-dismiss">Dismiss</button>'}\n </div>\n </div>`}(e&&e.runId!==G?e:null);const t=e?`${e.runId}:${e.status}`:"";if(t!==W){const n=e&&!["running","awaiting_approval"].includes(e.status);W=t,n&&V()}}N.addEventListener("click",async e=>{const t=e.target.closest("button");if(t){if("wf-refresh-btn"===t.id)return V();if("wf-record-btn"===t.id){const[e]=await chrome.tabs.query({active:!0,currentWindow:!0}),t=await S("record_start",{tabId:e?.id});return void Q(t?.ok?"":t?.error??"Could not start recording.",!t?.ok)}if(t.classList.contains("wf-rec-stop"))S("record_stop");else if(t.classList.contains("wf-rec-discard"))S("record_discard");else if(t.classList.contains("wf-remove"))S("record_remove_step",{index:Number(t.dataset.index)});else{if(t.classList.contains("wf-rec-save")){const e=document.getElementById("wf-save-name").value.trim(),t=await S("record_save",{name:e,description:document.getElementById("wf-save-desc").value.trim(),keepValues:document.getElementById("wf-save-keep").checked,overwrite:document.getElementById("wf-save-overwrite").checked});return Q(t?.ok?`Saved "${e}".`:t?.error?.message??"Could not save.",!t?.ok),void(t?.ok&&V())}if(t.classList.contains("wf-open")){const e=await K("get",{name:t.dataset.name});return e?.ok?(O={name:t.dataset.name,detail:e.data.workflow},Y()):Q(e?.error?.message??"Could not open the workflow.",!0)}if(t.classList.contains("wf-close"))return O=null,Y();if(t.classList.contains("wf-start")){const e=t.closest(".wf-form"),n={};for(const[e,t]of Object.entries(O?.detail.params??{}))Z(t)&&(n[e]="");e.querySelectorAll("input[data-param]").forEach(e=>{n[e.dataset.param]=e.value}),F=n;const a=await K("run",{name:t.dataset.name,params:n});return a?.ok?(Q(""),G="",O=null,Y()):Q(a?.error?.message??"Could not start the run.",!0)}if(t.classList.contains("wf-start-many")){const e=t.closest(".wf-form").querySelector(".wf-many-text").value,n=await K("run_batch",{name:t.dataset.name,text:e});return n?.ok?(Q(""),G="",O=null,Y()):Q(n?.error?.message??"Could not start the runs.",!0)}if(t.classList.contains("wf-rerun-failed")){const e=await K("rerun_failed");return Q(e?.ok?"":e?.error?.message??"Could not run the failed sets again.",!e?.ok),void(e?.ok&&(G=""))}if(t.classList.contains("wf-continue")){const e=await K("continue",{name:t.dataset.name,params:F,startAt:Number(t.dataset.start)});return void Q(e?.ok?"":e?.error?.message??"Could not continue.",!e?.ok)}if(!t.classList.contains("wf-cancel"))return t.classList.contains("wf-dismiss")?(G=T?.workflowRun?.runId??"",F={},X()):void 0;K("cancel")}}}),async function(){const e=await chrome.storage.local.get(["bridgePort","bridgeToken"]);"number"==typeof e.bridgePort&&(o.value=String(e.bridgePort),s.textContent=String(e.bridgePort)),"string"==typeof e.bridgeToken&&e.bridgeToken&&(r.placeholder="paired — paste a new token to replace"),V();try{const e=await S("get_state");e&&(T=e,j())}catch{}}()})();
|
|
1
|
+
(()=>{"use strict";const e=document.getElementById("status-badge"),t=document.querySelector(".status-dot"),n=document.getElementById("status-message"),a=document.getElementById("status-hint"),s=document.getElementById("port-display"),o=document.getElementById("port-input"),i=document.getElementById("save-port-btn"),l=document.getElementById("token-input"),r=document.getElementById("save-token-btn"),c=document.getElementById("profile-input"),d=document.getElementById("save-profile-btn"),p=document.getElementById("pairing-notice"),m=document.getElementById("pairing-setup"),u=document.getElementById("pairing-fallback"),f=document.getElementById("conflict-banner"),g=document.getElementById("reconnect-btn"),b=document.getElementById("sessions-container"),v=document.getElementById("action-log"),x=document.getElementById("clear-log-btn"),w=document.getElementById("plan-badge"),h=document.getElementById("licence-input"),$=document.getElementById("licence-save-btn"),y=document.getElementById("licence-remove-btn"),k=document.getElementById("licence-status"),E=document.getElementById("device-id"),I="pill pill-muted",C="mt-1 inline-block size-2 shrink-0 rounded-full",L={on:`status-dot connected ${C} bg-ok`,off:`status-dot disconnected ${C} bg-fg-2`},B={success:"bg-ok",error:"bg-danger",pending:"bg-warn"},_=e=>e?"active border-accent bg-accent/15 text-accent":"";let T=null;function S(e,t){return chrome.runtime.sendMessage({source:"sidepanel",type:e,...t})}function j(){if(!T)return;const o=!T.paired||Boolean(T.authorizationError);if(p.hidden=!o,o){const s=!1===T.nativeHost;m.hidden=!1,u.hidden=!s&&!T.authorizationError,e.textContent=T.paired?"token rejected":"not paired",e.className=I,t.className=L.off,n.textContent=T.authorizationError?"The bridge rejected the saved pairing token":"Pair this extension with the bridge to begin",a.textContent=s?"Run: npm install -g browserzap, then browserzap setup":null===T.nativeHost?"Starting the bridge through the native host…":"The bridge only accepts authenticated connections from this machine."}else if(T.versionError){e.textContent="update needed",e.className=I,t.className=L.off,n.textContent=T.versionError;const s=/bridge speaks protocol (\d+), the client sent (\d+)/.exec(T.versionError);a.textContent=s&&Number(s[1])<Number(s[2])?"An older BrowserZap bridge is still running. Run: browserzap restart-bridge":"Update both parts: npm install -g browserzap@latest, browserzap setup, reload the extension, then browserzap restart-bridge"}else if(T.connected){const s=A(T.sessions).length;e.textContent=s>0?`${s} agent${s>1?"s":""}`:"online",e.className="pill pill-ok",t.className=L.on,n.textContent=s>0?`${s} agent session${s>1?"s":""} active`:"Bridge connected, waiting for an agent";const o=T.profile;a.textContent=o&&o.connected>1?`This profile: ${o.label}. ${o.connected} Chrome profiles are connected; agents choose one with browser_select_profile.`:o&&o.label&&!/^Profile \d+$/.test(o.label)?`This profile: ${o.label}`:""}else T.conflictReason?(e.textContent="conflict",e.className=I,t.className=L.off,n.textContent=T.conflictReason,a.textContent="Close the other browser, or disable its BrowserZap extension, to use this one."):(e.textContent="offline",e.className=I,t.className=L.off,"bridge_foreign"===T.nativeError?(n.textContent="Another BrowserZap bridge is using the port and will not pair with this install",a.textContent="Run: browserzap restart-bridge, then press Reconnect under Settings"):(n.innerHTML=`Waiting for the bridge on port <strong>${s.textContent}</strong>`,a.textContent="The extension retries automatically"));const i=T.authorizationError??T.versionError??T.conflictReason??"";var l;f.hidden=0===i.length,f.textContent=i,0!==(l=T.sessions.filter(e=>e.id!==R||(e.approvals??[]).length>0||e.pendingUserAction)).length?b.innerHTML=l.map(e=>{const t=M(e.id),n=e.pendingUserAction?`\n <div class="user-action-alert mt-1 flex flex-col gap-1.5 rounded-md border border-warn/40 bg-warn/10 p-2">\n <h3 class="m-0 text-[12px] font-bold">Action needed</h3>\n <p>${M(e.pendingUserAction)}</p>\n <textarea data-session="${t}" placeholder="Describe what you did (optional)" rows="2"></textarea>\n <button class="btn btn-primary w-full user-done-btn" data-session="${t}">\n Hand back to AI\n </button>\n </div>\n `:"",a=(e.approvals??[]).map(e=>`\n <div class="user-action-alert approval-alert mt-1 flex flex-col gap-1.5 rounded-md border border-accent/40 bg-accent/10 p-2">\n <h3 class="m-0 text-[12px] font-bold">Approve this action?</h3>\n <p>${M(e.summary)}</p>\n <p class="meta m-0">${M(e.risk.replace(/_/g," "))} · ${M(e.origin)}</p>\n <div class="flex gap-1.5">\n <button class="btn btn-primary approval-btn" data-session="${t}" data-approval="${M(e.id)}" data-decision="approve">Approve once</button>\n <button class="btn btn-ghost approval-btn" data-session="${t}" data-approval="${M(e.id)}" data-decision="deny">Deny</button>\n </div>\n </div>\n `).join("");return`\n <section class="session-card panel flex flex-col gap-2">\n <div class="flex items-center justify-between gap-2">\n <span class="font-semibold">${M(e.name)}</span>\n <span class="font-mono text-[10px] text-fg-2">${t}</span>\n </div>\n <div class="flex gap-1">\n <button class="mode-btn btn flex-1 ${_("collaborative"===e.controlMode)}"\n data-session="${t}" data-mode="collaborative" title="Both user and AI">\n <span>Collab</span>\n </button>\n <button class="mode-btn btn flex-1 ${_("ai"===e.controlMode)}"\n data-session="${t}" data-mode="ai" title="AI has full control">\n <span>AI</span>\n </button>\n <button class="mode-btn btn flex-1 ${_("user"===e.controlMode)}"\n data-session="${t}" data-mode="user" title="User has control">\n <span>User</span>\n </button>\n </div>\n ${a}\n ${n}\n </section>\n `}).join(""):b.innerHTML=T?.connected?'<div class="hint mx-3 my-1">Waiting for agent sessions…</div>':"",function(e){if(0===e.length)return void(v.innerHTML='<div class="hint py-2">Waiting for an agent</div>');const t=e.slice(-50);v.innerHTML=t.map(e=>{const t=new Date(e.timestamp).toLocaleTimeString("en-US",{hour12:!1,hour:"2-digit",minute:"2-digit",second:"2-digit"}),n=e.session?`<span class="font-mono text-fg-2">${e.session.slice(0,4)}</span>`:"";return`\n <div class="log-entry flex items-start gap-1.5 border-b border-line py-1 text-[11px]">\n <div class="log-status ${e.status} mt-1 size-1.5 shrink-0 rounded-full ${B[e.status]??"bg-fg-2"}"></div>\n <span class="log-source ${e.source} text-[9px] font-bold uppercase text-fg-2">${e.source}</span>\n ${n}\n <div class="min-w-0 flex-1">\n <div>${M(e.action)}</div>\n ${e.details?`<div class="truncate text-fg-2" title="${M(e.details)}">${M(e.details)}</div>`:""}\n </div>\n <span class="font-mono text-[10px] text-fg-2">${t}</span>\n </div>\n `}).join(""),v.scrollTop=v.scrollHeight}(T.actionLog),X(),function(){const e=T?.comments??{activeTabId:null,comments:[]},t=null!==e.activeTabId;te.textContent=t?"Finish commenting":"Comment on this page",te.classList.toggle("btn-primary",t),se.hidden=0===e.comments.length,oe.textContent=`Send ${e.comments.length} comment${1===e.comments.length?"":"s"} to agent`,oe.disabled=!T?.connected;const n=e.comments.map(e=>`${e.cid}:${e.n}:${e.attachments.length}:${e.screenshot?1:0}`).join("|");n!==re&&(re=n,ae.innerHTML=e.comments.map(e=>{const t=M(e.cid),n=[e.screenshot?`<img src="data:image/png;base64,${e.screenshot}" alt="Screenshot of the element" class="max-h-28 max-w-full rounded border border-line object-contain">`:"",...e.attachments.map((e,n)=>`<span class="relative inline-block"><img src="data:${M(e.mimeType)};base64,${e.data}" alt="Attached image ${n+1}" class="max-h-20 max-w-[8rem] rounded border border-line object-contain"><button class="btn btn-ghost cm-detach absolute right-0 top-0 px-1 py-0 text-[10px]" data-cid="${t}" data-index="${n}" title="Remove image">×</button></span>`)].filter(Boolean).join("");return`<div class="card cm-item" data-cid="${t}">\n <div class="flex items-start justify-between gap-2">\n <span class="flex items-center gap-1.5"><span class="pill pill-warn">${e.n}</span><span class="meta break-words">${M(function(e){const t=e.component,n=t?.source?.file?`${t.source.file.split(/[\\/]/).slice(-2).join("/")}${t.source.line?`:${t.source.line}`:""}`:"";return[e.info.name?`${e.info.role??e.info.tag} "${e.info.name}"`:e.info.role&&"generic"!==e.info.role?e.info.role:e.info.tag??"element",t?.component?`<${t.component}>`:"",n].filter(Boolean).join(" · ")}(e))}</span></span>\n <button class="btn btn-ghost cm-delete px-1.5 py-0" data-cid="${t}" title="Delete comment">Delete</button>\n </div>\n <textarea class="cm-text h-16" data-cid="${t}" maxlength="2000" spellcheck="true">${M(e.text)}</textarea>\n ${n?`<div class="flex flex-wrap items-start gap-1.5">${n}</div>`:""}\n <div class="flex items-center justify-between gap-2">\n <span class="meta truncate" title="${M(e.page.url)}">${M(function(e){try{const t=new URL(e);return t.host+t.pathname}catch{return""}}(e.page.url))}</span>\n ${e.attachments.length<3?`<label class="btn btn-ghost cursor-pointer px-1.5 py-0">Add image<input type="file" accept="image/png,image/jpeg,image/webp,image/gif" class="cm-file hidden" data-cid="${t}"></label>`:""}\n </div>\n </div>`}).join(""))}()}!function e(){const t=chrome.runtime.connect({name:"sidepanel-keepalive"});t.onDisconnect.addListener(()=>{setTimeout(e,1e3)}),setTimeout(()=>{try{t.disconnect()}catch{}},2e4)}(),chrome.runtime.onMessage.addListener(e=>{"background"===e.source&&"state_update"===e.type&&(T=e.data,j())}),i.addEventListener("click",async()=>{const e=parseInt(o.value,10);e>=1024&&e<=65535&&(await chrome.storage.local.set({bridgePort:e}),s.textContent=String(e),await S("disconnect"),await S("connect",{port:e}))}),r.addEventListener("click",async()=>{const e=l.value.trim();await S("set_token",{token:e}),l.value="",l.placeholder=e?"paired — paste a new token to replace":"paste token"}),d.addEventListener("click",async()=>{await S("set_profile_label",{label:c.value})}),$.addEventListener("click",async()=>{const e=h.value.trim();if(!e)return;const t=await K("licence_activate",{token:e});h.value="",t.ok?await V():k.textContent=t.error?.message??"That licence was not accepted."}),y.addEventListener("click",async()=>{await K("licence_remove"),await V()}),chrome.storage.local.get(["bz_profile_label"]).then(e=>{"string"==typeof e.bz_profile_label&&(c.value=e.bz_profile_label)}).catch(()=>{}),g.addEventListener("click",async()=>{const e=parseInt(o.value,10);await S("disconnect"),await S("connect",{port:e})}),x.addEventListener("click",()=>{v.innerHTML='<div class="hint py-2">Log cleared</div>'}),b.addEventListener("click",e=>{const t=e.target,n=t.closest(".mode-btn");if(n)return void S("set_control_mode",{sessionId:n.dataset.session,data:n.dataset.mode});const a=t.closest(".approval-btn");if(a)return void S("decide_approval",{sessionId:a.dataset.session,approvalId:a.dataset.approval,approve:"approve"===a.dataset.decision});const s=t.closest(".user-done-btn");if(s){const e=s.dataset.session,t=b.querySelector(`textarea[data-session="${CSS.escape(e)}"]`);S("set_control_mode",{sessionId:e,data:"collaborative",userMessage:t?.value?.trim()||void 0}),t&&(t.value="")}});const R="panel",A=e=>e.filter(e=>e.id!==R);function M(e){return String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}const N=document.getElementById("workflows-panel"),D=document.getElementById("wf-recording"),H=document.getElementById("wf-run"),z=document.getElementById("wf-message"),P=document.getElementById("wf-list");let U=[],q={},F=null,O={},W="";const Z=e=>/is sensitive/.test(e?.description??"");function K(e,t={}){return S("panel",{op:e,args:t})}function Q(e,t=!1){z.textContent=e,z.classList.toggle("text-danger",t)}async function V(){const e=await K("list");e?.ok?(U=e.data?.workflows??[],q=e.data?.lastRuns??{},function(e,t){if(t?.short&&(E.textContent=t.short),!e)return;const n="pro"===e.plan;w.textContent=n?"pro":"free",w.className="pill "+(n?"pill-ok":"pill-muted");const a=n?`Pro${e.expiresAt?` until ${new Date(e.expiresAt).toLocaleDateString()}`:" — no expiry"}${e.licenseId?` · ${e.licenseId}`:""}.`:"Free plan — every browser tool and every safety control. No licence needed. Pro (bulk runs, history, reports) is coming soon.";k.textContent=[a,e.notice,e.reason].filter(Boolean).join(" "),y.hidden=!n}(e.data?.plan,e.data?.device),Q("")):(U=[],Q(e?.error?.message??"Could not load workflows.",!0)),Y()}function Y(){0!==U.length?P.innerHTML=U.map(e=>{const t=M(e.name);if(e.broken)return`<div class="card"><span class="break-words font-semibold">${t}</span><span class="meta">Cannot be used: ${M(e.broken)}</span></div>`;const n=q[e.name],a=n?`<span class="pill ${n.ok?"pill-ok":"USER_ACTION_REQUIRED"===n.code?"pill-warn":"pill-bad"}">${n.ok?"last run ok":M((n.code??"failed").toLowerCase().replace(/_/g," "))}</span>`:'<span class="pill pill-muted">never run</span>',s=F?.name===e.name?function(e){const t=M(e.name),n=Object.entries(e.params??{}).map(([e,t])=>{const n=M(e);return Z(t)?`<label class="flex flex-col gap-0.5 meta">${n}<span class="meta">Sensitive: the run pauses and you type it yourself in the page.</span></label>`:`<label class="flex flex-col gap-0.5 meta">${M(t.description||e)}\n <input type="text" data-param="${n}" value="${M(t.default??"")}" autocomplete="off" spellcheck="false"></label>`}).join(""),a=Object.entries(e.params??{}).filter(([,e])=>!Z(e)).map(([e])=>e);return`\n <div class="wf-form flex flex-col gap-1.5" data-name="${t}">\n ${n||'<span class="meta">No values needed.</span>'}\n <div class="flex flex-wrap gap-1.5">\n <button class="btn btn-primary wf-start" data-name="${t}">Start</button>\n <button class="btn btn-ghost wf-close">Close</button>\n </div>\n ${a.length?`\n <details class="wf-many">\n <summary class="meta cursor-pointer">Run for many (Pro)</summary>\n <div class="mt-1.5 flex flex-col gap-1.5">\n <label class="flex flex-col gap-0.5 meta">One row per run. First row: ${M(a.join(","))}${1===a.length?" (or just one value per line)":""}\n <textarea class="wf-many-text h-24 font-mono" spellcheck="false" placeholder="${M(a.join(","))}"></textarea></label>\n <span class="meta">Runs one after the other. A row that fails is reported and the next one runs.</span>\n <div class="flex flex-wrap gap-1.5"><button class="btn btn-primary wf-start-many" data-name="${t}">Run all</button></div>\n </div>\n </details>`:""}\n </div>`}(F.detail):"";return`\n <div class="card">\n <div class="flex items-center justify-between gap-2"><span class="break-words font-semibold">${t}</span>${a}</div>\n ${e.description?`<span class="meta">${M(e.description)}</span>`:""}\n <span class="meta">${e.steps??0} steps · ${M((e.origins??[]).join(", "))}${n?` · ${M(new Date(n.at).toLocaleString())}`:""}</span>\n ${n&&!n.ok?`<span class="meta">${M(n.summary)}</span>`:""}\n ${s||`<div class="flex flex-wrap gap-1.5"><button class="btn btn-primary wf-open" data-name="${t}">Run…</button></div>`}\n </div>`}).join(""):P.innerHTML='<div class="hint">No workflows yet. Press <b>Record</b>, do a task once in the current tab, then save it.</div>'}let G="",J=!1;function X(){T?.connected&&!J&&V(),J=Boolean(T?.connected);const e=T?.workflowRun??null;!function(e){if(!e)return void(D.innerHTML="");const t=e.steps.map((e,t)=>`\n <li>${M(e.label)}${e.sensitive?'<span class="ml-1 text-[10px] font-bold text-accent" title="You type this yourself on every run">sensitive</span>':""}${e.ambiguous?'<span class="ml-1 text-[10px] font-bold text-warn" title="This target is not unique; the run may stop here">check</span>':""}\n ${t>0?`<button class="btn btn-ghost wf-remove ml-1 px-1 py-0 text-[10px]" data-index="${t}" title="Remove this step">✕</button>`:""}</li>`).join("");e.stopped?D.innerHTML=`\n <div class="card border-accent/40 bg-accent/10">\n <div class="flex items-center justify-between gap-2"><span class="break-words font-semibold">Review and save</span><span class="meta">${e.steps.length} steps</span></div>\n <ol class="m-0 flex list-decimal flex-col gap-0.5 pl-4 text-[12px]">${t}</ol>\n <div class="wf-form flex flex-col gap-1.5">\n <label class="flex flex-col gap-0.5 meta">Name (lowercase letters, digits, - and _)<input type="text" id="wf-save-name" placeholder="e.g. enable-comments" autocomplete="off" spellcheck="false"></label>\n <label class="flex flex-col gap-0.5 meta">Description<input type="text" id="wf-save-desc" autocomplete="off"></label>\n <label class="flex flex-row items-center gap-1.5 meta"><input type="checkbox" id="wf-save-keep"> Keep what I typed as default values</label>\n <label class="flex flex-row items-center gap-1.5 meta"><input type="checkbox" id="wf-save-overwrite"> Replace a workflow with the same name</label>\n <div class="flex flex-wrap gap-1.5">\n <button class="btn btn-primary wf-rec-save">Save workflow</button>\n <button class="btn btn-ghost wf-rec-discard">Discard</button>\n </div>\n </div>\n </div>`:D.innerHTML=`\n <div class="card border-danger/40 bg-danger/10">\n <div class="flex items-center justify-between gap-2"><span class="break-words font-semibold"><span class="mr-1.5 inline-block size-2 animate-pulse rounded-full bg-danger motion-reduce:animate-none"></span>Recording</span><span class="meta">${e.steps.length} steps</span></div>\n <span class="meta">Do the task in the tab as you normally would. Only your own clicks and typing are recorded.</span>\n <ol class="m-0 flex list-decimal flex-col gap-0.5 pl-4 text-[12px]">${t}</ol>\n <div class="flex flex-wrap gap-1.5">\n <button class="btn btn-primary wf-rec-stop">Stop</button>\n <button class="btn btn-ghost wf-rec-discard">Discard</button>\n </div>\n </div>`}(T?.recording??null),function(e){if(!e)return void(H.innerHTML="");const t="running"===e.status||"awaiting_approval"===e.status,n="completed"===e.status?"ok":t||"needs_user"===e.status?"wait":"bad",a=e.batch&&!t?"":e.steps.map(e=>`<li>${e.ok?"✓":"✗"} ${M(e.tool)}${e.ok?"":` — ${M(e.summary)}`}</li>`).join(""),s=e.batch,o=s?`\n <span class="meta">${t?`Set ${s.index+1} of ${s.total} · `:""}${s.done} done${s.failed?` · ${s.failed} need attention`:""}</span>\n ${s.attention.length?`<ul class="m-0 flex list-none flex-col gap-1 p-0 text-[12px]">${s.attention.map(e=>`\n <li class="rounded-md border border-danger/30 bg-danger/5 px-2 py-1">\n <span class="font-semibold">${M(e.label)}</span>: ${e.step?`step ${e.step} (${M(e.tool??"")}${e.target?` ${M(e.target)}`:""})`:"did not start"} — ${M((e.code??"failed").toLowerCase().replace(/_/g," "))}${e.cause?`, ${M(e.cause.replace(/_/g," "))}`:""}\n ${e.screenshot?`<span class="block break-all meta" title="Saved on this computer">Screenshot: ${M(e.screenshot)}</span>`:""}\n </li>`).join("")}</ul>`:""}\n ${s.report?`<span class="meta break-all">Report: ${M(s.report)}</span>`:""}`:"";H.innerHTML=`\n <div class="card border-accent/40 bg-accent/10">\n <div class="flex items-center justify-between gap-2"><span class="break-words font-semibold">${M(e.workflow)}</span><span class="pill pill-${n}">${M(e.status.replace(/_/g," "))}</span></div>\n ${s&&!t?"":`<span class="meta">Step ${Math.min(e.stepIndex+1,e.totalSteps)} of ${e.totalSteps}</span>`}\n ${o}\n <span class="meta">${M(e.summary)}</span>\n ${"awaiting_approval"===e.status?'<span class="text-[12px] font-semibold">Approve or deny the step below, under "BrowserZap workflows".</span>':""}\n ${e.nextAction?`<span class="text-[12px] font-semibold">${M(e.nextAction)}</span>`:""}\n ${a?`<ol class="m-0 flex list-decimal flex-col gap-0.5 pl-4 text-[12px]">${a}</ol>`:""}\n <div class="flex flex-wrap gap-1.5">\n ${t?'<button class="btn btn-danger wf-cancel">Cancel run</button>':""}\n ${s?.canRerun&&!t?'<button class="btn btn-primary wf-rerun-failed">Re-run failed</button>':""}\n ${null===e.canContinueFrom||t?"":`<button class="btn btn-primary wf-continue" data-name="${M(e.workflow)}" data-start="${e.canContinueFrom}">Continue</button>`}\n ${t?"":'<button class="btn btn-ghost wf-dismiss">Dismiss</button>'}\n </div>\n </div>`}(e&&e.runId!==G?e:null);const t=e?`${e.runId}:${e.status}`:"";if(t!==W){const n=e&&!["running","awaiting_approval"].includes(e.status);W=t,n&&V()}}N.addEventListener("click",async e=>{const t=e.target.closest("button");if(t){if("wf-refresh-btn"===t.id)return V();if("wf-record-btn"===t.id){const[e]=await chrome.tabs.query({active:!0,currentWindow:!0}),t=await S("record_start",{tabId:e?.id});return void Q(t?.ok?"":t?.error??"Could not start recording.",!t?.ok)}if(t.classList.contains("wf-rec-stop"))S("record_stop");else if(t.classList.contains("wf-rec-discard"))S("record_discard");else if(t.classList.contains("wf-remove"))S("record_remove_step",{index:Number(t.dataset.index)});else{if(t.classList.contains("wf-rec-save")){const e=document.getElementById("wf-save-name").value.trim(),t=await S("record_save",{name:e,description:document.getElementById("wf-save-desc").value.trim(),keepValues:document.getElementById("wf-save-keep").checked,overwrite:document.getElementById("wf-save-overwrite").checked});return Q(t?.ok?`Saved "${e}".`:t?.error?.message??"Could not save.",!t?.ok),void(t?.ok&&V())}if(t.classList.contains("wf-open")){const e=await K("get",{name:t.dataset.name});return e?.ok?(F={name:t.dataset.name,detail:e.data.workflow},Y()):Q(e?.error?.message??"Could not open the workflow.",!0)}if(t.classList.contains("wf-close"))return F=null,Y();if(t.classList.contains("wf-start")){const e=t.closest(".wf-form"),n={};for(const[e,t]of Object.entries(F?.detail.params??{}))Z(t)&&(n[e]="");e.querySelectorAll("input[data-param]").forEach(e=>{n[e.dataset.param]=e.value}),O=n;const a=await K("run",{name:t.dataset.name,params:n});return a?.ok?(Q(""),G="",F=null,Y()):Q(a?.error?.message??"Could not start the run.",!0)}if(t.classList.contains("wf-start-many")){const e=t.closest(".wf-form").querySelector(".wf-many-text").value,n=await K("run_batch",{name:t.dataset.name,text:e});return n?.ok?(Q(""),G="",F=null,Y()):Q(n?.error?.message??"Could not start the runs.",!0)}if(t.classList.contains("wf-rerun-failed")){const e=await K("rerun_failed");return Q(e?.ok?"":e?.error?.message??"Could not run the failed sets again.",!e?.ok),void(e?.ok&&(G=""))}if(t.classList.contains("wf-continue")){const e=await K("continue",{name:t.dataset.name,params:O,startAt:Number(t.dataset.start)});return void Q(e?.ok?"":e?.error?.message??"Could not continue.",!e?.ok)}if(!t.classList.contains("wf-cancel"))return t.classList.contains("wf-dismiss")?(G=T?.workflowRun?.runId??"",O={},X()):void 0;K("cancel")}}}),async function(){const e=await chrome.storage.local.get(["bridgePort","bridgeToken"]);"number"==typeof e.bridgePort&&(o.value=String(e.bridgePort),s.textContent=String(e.bridgePort)),"string"==typeof e.bridgeToken&&e.bridgeToken&&(l.placeholder="paired — paste a new token to replace"),V();try{const e=await S("get_state");e&&(T=e,j())}catch{}}();const ee=document.getElementById("comments-panel"),te=document.getElementById("cm-toggle-btn"),ne=document.getElementById("cm-message"),ae=document.getElementById("cm-list"),se=document.getElementById("cm-footer"),oe=document.getElementById("cm-send-btn"),ie=document.getElementById("cm-sent"),le=ne.textContent??"";let re="";function ce(e,t=!1){ne.textContent=e||le,ne.classList.toggle("text-danger",t)}function de(e){return new Promise((t,n)=>{const a=new FileReader;a.onload=()=>t(String(a.result)),a.onerror=()=>n(a.error),a.readAsDataURL(e)})}async function pe(e,t){const n=await S("comment_attach",{cid:e,dataUrl:await de(t)});ce(n?.ok?"":n?.error??"Could not add the image.",!n?.ok)}ee.addEventListener("click",async e=>{const t=e.target.closest("button");if(t){if("cm-toggle-btn"===t.id){ie.hidden=!0;const e=T?.comments?.activeTabId;if(null!=e)return await S("comment_stop"),void ce("");const[t]=await chrome.tabs.query({active:!0,currentWindow:!0}),n=await S("comment_start",{tabId:t?.id});return void ce(n?.ok?"Click an element on the page, write your note, press Save. Esc finishes.":n?.error??"Could not start Comment mode.",!n?.ok)}if("cm-send-btn"===t.id){oe.disabled=!0;const e=await S("comment_send");return oe.disabled=!1,ie.hidden=!e?.ok,void ce(e?.ok?"":e?.error??"Could not send the comments.",!e?.ok)}if("cm-copy-btn"===t.id)return await navigator.clipboard.writeText(document.getElementById("cm-prompt").textContent??"").catch(()=>{}),t.textContent="Copied",void setTimeout(()=>{t.textContent="Copy"},1500);"cm-clear-btn"!==t.id?t.classList.contains("cm-delete")?S("comment_delete",{cid:t.dataset.cid}):t.classList.contains("cm-detach")&&S("comment_detach",{cid:t.dataset.cid,index:Number(t.dataset.index)}):S("comment_clear")}}),ee.addEventListener("change",e=>{const t=e.target;t.classList.contains("cm-file")&&t.files?.[0]&&pe(String(t.dataset.cid),t.files[0])}),ee.addEventListener("focusout",e=>{const t=e.target;t.classList?.contains("cm-text")&&S("comment_update",{cid:t.dataset.cid,text:t.value})}),ee.addEventListener("paste",e=>{const t=e.target;if(!t.classList?.contains("cm-text"))return;const n=Array.from(e.clipboardData?.items??[]).find(e=>e.type.startsWith("image/"))?.getAsFile();n&&(e.preventDefault(),pe(String(t.dataset.cid),n))})})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browserzap",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Let your AI agent (Claude Code, Codex, Cursor or any MCP client) work in your own Chrome, with your logins, your approval and a Stop button.",
|
|
5
5
|
"homepage": "https://browserzap.com",
|
|
6
6
|
"bugs": {
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"dev": "webpack --mode development --watch",
|
|
48
48
|
"clean": "node scripts/clean.mjs",
|
|
49
49
|
"package:extension": "node scripts/package-extension.mjs",
|
|
50
|
+
"package:store": "node scripts/package-store.mjs",
|
|
50
51
|
"start": "node bridge-server/index.js",
|
|
51
52
|
"bridge": "node bridge-server/server.js",
|
|
52
53
|
"health": "node scripts/health.mjs",
|