@mirasim/computer-use 0.1.0-rc.5 → 0.1.0-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -46,6 +46,23 @@ claude mcp add mirasim-cu -- npx -y @mirasim/computer-use mcp
46
46
 
47
47
  or install the `mirasim-cu` command once with `npm install -g @mirasim/computer-use`.
48
48
 
49
+ In Codex, add it to `~/.codex/config.toml`, and approve both tools: Codex asks before every
50
+ MCP call by default, and when it is set never to ask (`approval_policy = "never"`) a tool
51
+ without `approval_mode = "approve"` is refused outright — `computer_repl_reset` included,
52
+ which is the call a stuck session reaches for:
53
+
54
+ ```toml
55
+ [mcp_servers.mirasim-cu]
56
+ command = "npx"
57
+ args = ["-y", "@mirasim/computer-use", "mcp"]
58
+
59
+ [mcp_servers.mirasim-cu.tools.computer_repl]
60
+ approval_mode = "approve"
61
+
62
+ [mcp_servers.mirasim-cu.tools.computer_repl_reset]
63
+ approval_mode = "approve"
64
+ ```
65
+
49
66
  **Browser** works out of the box: computer-use drives a Chrome of its own (installed Google
50
67
  Chrome is required), keeping its profile under `~/.mirasim-computer-use` so logins carry over
51
68
  between sessions. To drive the browser you are signed into instead, install the
@@ -45,6 +45,8 @@ export declare class BridgeClient {
45
45
  private readonly sendMessage;
46
46
  readonly id: number;
47
47
  autoAttach: boolean;
48
+ readonly heldTabs: Set<number>;
49
+ readonly keptTabs: Set<number>;
48
50
  readonly sessionByTabId: Map<number, string>;
49
51
  readonly tabBySessionId: Map<string, number>;
50
52
  readonly virtualSessions: Map<string, number>;
@@ -1 +1 @@
1
- import{RuntimeContexts as d}from"./runtime-contexts.js";function h(n){return n.discarded===!0||n.status==="unloaded"}const l="mirasim-tab-",u={windowId:1,bounds:{left:0,top:0,width:1440,height:900,windowState:"normal"}};class m{sendMessage;id;autoAttach=!1;sessionByTabId=new Map;tabBySessionId=new Map;virtualSessions=new Map;browserSessions=new Set;nextSession=0;runtimes=new Map;initializedFrames=new Set;frameReplies=new Map;pendingFrames=new Map;constructor(e,i){this.sendMessage=i,this.id=e}trackFrameTree(e,i){this.initializedFrames.has(i)||this.runtimes.has(i)||(this.frameReplies.set(e,i),this.pendingFrames.has(i)||this.pendingFrames.set(i,new d))}send(e){if(this.sendMessage(e),e.id===void 0)return;const i=this.frameReplies.get(e.id);if(i===void 0)return;this.frameReplies.delete(e.id);const t=this.pendingFrames.get(i);this.pendingFrames.delete(i),e.error||this.initializedFrames.add(i);for(const s of t?.values()??[])this.sendMessage({sessionId:i,method:"Runtime.executionContextCreated",params:s})}enableRuntime(e){return this.runtimes.has(e)?!1:(this.runtimes.set(e,new d),!0)}disableRuntime(e){this.runtimes.delete(e),this.pendingFrames.get(e)?.clear()}runtimeEvent(e,i,t,s=!1){const r=this.runtimes.get(e);if(!r||s&&r.has(t))return;r.update(i,t);const a=this.pendingFrames.get(e);a?.update(i,t),!(a&&i==="Runtime.executionContextCreated")&&this.send({sessionId:e,method:i,params:t})}forgetSession(e){this.disableRuntime(e),this.initializedFrames.delete(e),this.pendingFrames.delete(e);for(const[i,t]of this.frameReplies)t===e&&this.frameReplies.delete(i)}dispose(){this.runtimes.clear(),this.initializedFrames.clear(),this.pendingFrames.clear(),this.frameReplies.clear()}tabAlias(e){let i=this.sessionByTabId.get(e);return i||(i=`mir-c${this.id}-t${++this.nextSession}`,this.sessionByTabId.set(e,i),this.tabBySessionId.set(i,e)),i}virtualAlias(e){const i=`mir-c${this.id}-v${++this.nextSession}`;return this.virtualSessions.set(i,e),i}browserAlias(){const e=`mir-c${this.id}-b${++this.nextSession}`;return this.browserSessions.add(e),e}dropTab(e){const i=this.sessionByTabId.get(e)??null;i&&(this.forgetSession(i),this.sessionByTabId.delete(e),this.tabBySessionId.delete(i));for(const[t,s]of this.virtualSessions)s===e&&(this.forgetSession(t),this.virtualSessions.delete(t));return i}}export{m as BridgeClient,u as FAKE_WINDOW,l as STAND_IN_PREFIX,h as tabIsUnloaded};
1
+ import{RuntimeContexts as d}from"./runtime-contexts.js";function h(n){return n.discarded===!0||n.status==="unloaded"}const l="mirasim-tab-",u={windowId:1,bounds:{left:0,top:0,width:1440,height:900,windowState:"normal"}};class m{sendMessage;id;autoAttach=!1;heldTabs=new Set;keptTabs=new Set;sessionByTabId=new Map;tabBySessionId=new Map;virtualSessions=new Map;browserSessions=new Set;nextSession=0;runtimes=new Map;initializedFrames=new Set;frameReplies=new Map;pendingFrames=new Map;constructor(e,t){this.sendMessage=t,this.id=e}trackFrameTree(e,t){this.initializedFrames.has(t)||this.runtimes.has(t)||(this.frameReplies.set(e,t),this.pendingFrames.has(t)||this.pendingFrames.set(t,new d))}send(e){if(this.sendMessage(e),e.id===void 0)return;const t=this.frameReplies.get(e.id);if(t===void 0)return;this.frameReplies.delete(e.id);const i=this.pendingFrames.get(t);this.pendingFrames.delete(t),e.error||this.initializedFrames.add(t);for(const s of i?.values()??[])this.sendMessage({sessionId:t,method:"Runtime.executionContextCreated",params:s})}enableRuntime(e){return this.runtimes.has(e)?!1:(this.runtimes.set(e,new d),!0)}disableRuntime(e){this.runtimes.delete(e),this.pendingFrames.get(e)?.clear()}runtimeEvent(e,t,i,s=!1){const r=this.runtimes.get(e);if(!r||s&&r.has(i))return;r.update(t,i);const a=this.pendingFrames.get(e);a?.update(t,i),!(a&&t==="Runtime.executionContextCreated")&&this.send({sessionId:e,method:t,params:i})}forgetSession(e){this.disableRuntime(e),this.initializedFrames.delete(e),this.pendingFrames.delete(e);for(const[t,i]of this.frameReplies)i===e&&this.frameReplies.delete(t)}dispose(){this.runtimes.clear(),this.initializedFrames.clear(),this.pendingFrames.clear(),this.frameReplies.clear()}tabAlias(e){let t=this.sessionByTabId.get(e);return t||(t=`mir-c${this.id}-t${++this.nextSession}`,this.sessionByTabId.set(e,t),this.tabBySessionId.set(t,e)),t}virtualAlias(e){const t=`mir-c${this.id}-v${++this.nextSession}`;return this.virtualSessions.set(t,e),t}browserAlias(){const e=`mir-c${this.id}-b${++this.nextSession}`;return this.browserSessions.add(e),e}dropTab(e){const t=this.sessionByTabId.get(e)??null;t&&(this.forgetSession(t),this.sessionByTabId.delete(e),this.tabBySessionId.delete(t));for(const[i,s]of this.virtualSessions)s===e&&(this.forgetSession(i),this.virtualSessions.delete(i));return t}}export{m as BridgeClient,u as FAKE_WINDOW,l as STAND_IN_PREFIX,h as tabIsUnloaded};
@@ -5,7 +5,7 @@ export declare abstract class BridgeModelCommands extends BridgeModelCore {
5
5
  handleCommand(client: BridgeClient, method: string, params: unknown, sessionId: string | undefined): Promise<unknown>;
6
6
  private enableAutoAttach;
7
7
  protected reconcileKnownTabs(): Promise<void>;
8
- private assertBrowserSession;
8
+ protected assertBrowserSession(expected: string): void;
9
9
  private tabTarget;
10
10
  private restoreTab;
11
11
  createTarget(url: string | undefined): Promise<{
@@ -1 +1 @@
1
- import{FAKE_WINDOW as b,tabIsUnloaded as w}from"./client.js";import{BridgeModelCore as g}from"./model-core.js";import{hostname as f}from"node:os";class p extends g{restoringTabs=new Map;async handleCommand(e,s,t,r){if(r&&e.browserSessions.has(r))return await this.handleCommand(e,s,t,void 0);const n=t??{};switch(s){case"Mirasim.tabs":return{tabs:await this.listTargets(),browserSession:this.browserSession};case"Mirasim.tabTarget":return{targetId:await this.tabTarget(Number(n.tabId),String(n.browserSession??""))};case"Mirasim.restoreTab":return await this.restoreTab(Number(n.tabId),String(n.targetId??""),String(n.browserSession??""));case"Browser.getVersion":return{protocolVersion:"1.3",product:"Chrome/mirasim-bridge",userAgent:"mirasim-bridge/1.0"};case"Browser.setDownloadBehavior":return{};case"Mirasim.downloads":{if(typeof n.since!="string"||!Number.isFinite(Date.parse(n.since)))throw new Error("Downloads require an ISO start time");if(n.until!==void 0&&(typeof n.until!="string"||!Number.isFinite(Date.parse(n.until))))throw new Error("Downloads require an ISO end time");const a=await this.sendToExtension("chrome.downloads.search",[{startedAfter:n.since,...n.until===void 0?{}:{startedBefore:n.until}}]);return{host:f(),items:a}}case"Browser.getWindowForTarget":return structuredClone(b);case"Browser.setWindowBounds":return{};case"Mirasim.attachRefusals":{const a=Date.now()-6e4;return{refusals:[...this.attachRefusals.values()].filter(o=>o.at>=a).map(({url:o,reason:h})=>({url:o,reason:h}))}}case"Browser.close":throw new Error("Browser.close is not available on the mirasim bridge \u2014 the browser is the user's own");case"Target.setAutoAttach":if(!r)return await this.enableAutoAttach(e),{};break;case"Target.createTarget":return await this.createTarget(typeof n.url=="string"?n.url:void 0);case"Target.closeTarget":return await this.closeTarget(typeof n.targetId=="string"?n.targetId:void 0);case"Target.attachToTarget":if(!r)return await this.attachToTarget(e,typeof n.targetId=="string"?n.targetId:void 0);break;case"Target.attachToBrowserTarget":if(!r)return{sessionId:e.browserAlias()};break;case"Runtime.enable":{if(!r)throw new Error("Runtime.enable requires a target session");const a=this.resolveSession(e,r),o=this.runtimeForSession(a.tabId,a.cdpSessionId),h=e.enableRuntime(r);try{const c=await o.enable(()=>this.sendToExtension("chrome.debugger.sendCommand",[{tabId:a.tabId,...a.cdpSessionId?{sessionId:a.cdpSessionId}:{}},s,...t===void 0?[]:[t]]));if(h)for(const i of o.values())e.runtimeEvent(r,"Runtime.executionContextCreated",i,!0);return c}catch(c){throw h&&e.disableRuntime(r),c}}case"Runtime.disable":if(!r)throw new Error("Runtime.disable requires a target session");return this.resolveSession(e,r),e.disableRuntime(r),{};case"Target.detachFromTarget":{const a=typeof n.sessionId=="string"?n.sessionId:void 0,o=a?e.virtualSessions.get(a):void 0;if(a&&o!==void 0)return e.virtualSessions.delete(a),e.forgetSession(a),this.releaseUnusedTab(o),{};if(a&&e.browserSessions.delete(a))return{};const h=a?this.childSessionOwner.get(a):void 0;if(a&&h===void 0)return{};if(a&&h!==void 0)return await this.sendToExtension("chrome.debugger.sendCommand",[{tabId:h},s,{sessionId:a}]);break}}if(!r)return await this.sendBrowserCommand(s,t);const d=this.resolveSession(e,r);return await this.sendToExtension("chrome.debugger.sendCommand",[{tabId:d.tabId,...d.cdpSessionId?{sessionId:d.cdpSessionId}:{}},s,...t===void 0?[]:[t]])}async enableAutoAttach(e){e.autoAttach=!0,await this.reconcileKnownTabs();const s=[...this.knownTabs].filter(([,t])=>!w(t)).map(([t])=>t);await Promise.all(s.map(t=>this.attachAndAnnounce(e,t).catch(r=>this.log("attach failed:",r))))}async reconcileKnownTabs(){let e;try{e=await this.sendToExtension("chrome.tabs.query",[{}])??[]}catch(t){this.log("tab reconcile failed:",t);return}const s=new Map(e.filter(t=>t.id!==void 0).map(t=>[t.id,t]));for(const t of[...this.knownTabs.keys()]){const r=s.get(t);if(!r)this.knownTabs.delete(t),this.agentTabs.delete(t),this.dropTab(t);else{const n=w(this.knownTabs.get(t));if(this.knownTabs.set(t,r),w(r)&&this.attachments.has(t))this.dropTab(t),this.sendToExtension("bridge.releaseTab",[t]).catch(d=>this.log("release unloaded tab failed:",d));else if(n&&!w(r))for(const d of this.clients)d.autoAttach&&this.attachAndAnnounce(d,t).catch(a=>this.log("restored tab attach failed:",a))}}}assertBrowserSession(e){if(!e||!this.browserSession)throw new Error("Reload the Mirasim extension before restoring tabs: this connection has no browser-session identity.");if(e!==this.browserSession)throw new Error("The browser restarted; list tabs and bind the intended target again.")}async tabTarget(e,s){if(this.assertBrowserSession(s),await this.reconcileKnownTabs(),!this.knownTabs.has(e))throw new Error(`Tab ${e} was closed; list tabs again.`);let t;try{t=await this.sendToExtension("chrome.debugger.getTargets",[])}catch(n){throw String(n).includes("Unknown method")?new Error("Reload the Mirasim extension to enable restoring unloaded tabs."):n}const r=t.find(n=>n.tabId===e);if(this.assertBrowserSession(s),!r?.id)throw new Error(`Chrome did not identify tab ${e}; refusing to restore an unknown target.`);return r.id}restoreTab(e,s,t){this.assertBrowserSession(t);const r=`${t}:${e}`,n=this.restoringTabs.get(r);if(n)return n.then(a=>{if(a.targetId!==s)throw new Error("Tab identity changed; list and bind it again before acting.");return a});const d=(async()=>{if(!s)throw new Error("Restoring a tab requires its claimed target identity.");if(await this.tabTarget(e,t)!==s)throw new Error(`Tab ${e} changed identity before restoration; no reload was sent.`);const a=this.knownTabs.get(e);if(!a)throw new Error(`Tab ${e} was closed; list tabs again.`);if(w(a)){try{await this.sendToExtension("chrome.tabs.reload",[e])}catch(u){const l=u instanceof Error?u.message:String(u);throw new Error(l.includes("Unknown method")?"Reload the Mirasim extension to enable restoring unloaded tabs.":`Could not restore tab ${e}: ${l}`)}const i=Date.now()+1e4;for(;w(this.knownTabs.get(e)??{discarded:!0});){if(Date.now()>=i)throw new Error(`Tab ${e} remained unloaded after restoration; no action was sent to its page.`);if(await new Promise(u=>setTimeout(u,50)),await this.reconcileKnownTabs(),!this.knownTabs.has(e))throw new Error(`Tab ${e} closed while restoring; no replacement tab was opened.`)}}const o=await this.ensureAttached(e);if(o.targetInfo.targetId!==s)throw new Error(`Tab ${e} changed identity while restoring; list and bind it again before acting.`);for(const i of this.clients)i.autoAttach&&this.announce(i,o);const h=[...this.knownTabs.values()].filter(i=>i.windowId===a.windowId&&i.splitViewId===a.splitViewId&&(i.width??0)>0&&(i.height??0)>0),c=h.find(i=>i.id===e)??h.find(i=>i.active)??h[0];return{targetId:o.targetInfo.targetId,...c?{viewport:{width:c.width,height:c.height}}:{}}})();return this.restoringTabs.set(r,d),d.finally(()=>this.restoringTabs.delete(r)).catch(()=>{}),d}async createTarget(e){const s=await this.sendToExtension("chrome.tabs.create",[{url:e||"about:blank",active:!1}]);if(s?.id===void 0)throw new Error("Failed to create tab");this.knownTabs.set(s.id,s),this.agentTabs.add(s.id);const t=await this.ensureAttached(s.id);for(const r of this.clients)r.autoAttach&&this.announce(r,t);return{targetId:t.targetInfo.targetId}}async recycleAgentTabs(){await this.reconcileKnownTabs();const e=[...this.agentTabs].filter(t=>this.knownTabs.has(t));let s=0;for(const t of e){try{await this.sendToExtension("chrome.tabs.remove",[t]),s+=1}catch(r){this.log("recycle failed for tab",t,r)}this.knownTabs.delete(t),this.agentTabs.delete(t),this.dropTab(t)}return{closed:s}}async closeTarget(e){const s=e?this.tabIdForTarget(e):void 0;return s===void 0?{success:!1}:(await this.sendToExtension("chrome.tabs.remove",[s]),{success:!0})}async attachToTarget(e,s){const t=s?this.tabIdForTarget(s):void 0;if(t===void 0)throw new Error(`No target with id: ${s}`);return await this.ensureAttached(t),{sessionId:e.virtualAlias(t)}}async sendBrowserCommand(e,s){const t=this.attachments.values().next().value,r=t?.tabId??[...this.knownTabs].find(([,n])=>!w(n))?.[0];if(r===void 0&&e==="Target.getTargetInfo")return{targetInfo:{targetId:"mirasim-browser",type:"browser",title:"",url:"",attached:!0}};if(r===void 0)throw new Error(`No tab to forward browser-level command: ${e}`);return t||await this.ensureAttached(r),await this.sendToExtension("chrome.debugger.sendCommand",[{tabId:r},e,...s===void 0?[]:[s]])}}export{p as BridgeModelCommands};
1
+ import{FAKE_WINDOW as g,tabIsUnloaded as w}from"./client.js";import{BridgeModelCore as f}from"./model-core.js";import{hostname as T}from"node:os";class p extends f{restoringTabs=new Map;async handleCommand(e,n,t,s){if(s&&e.browserSessions.has(s))return await this.handleCommand(e,n,t,void 0);const a=t??{};switch(n){case"Mirasim.tabs":return{tabs:await this.listTargets(e),browserSession:this.browserSession};case"Mirasim.tabTarget":return{targetId:await this.tabTarget(Number(a.tabId),String(a.browserSession??""))};case"Mirasim.restoreTab":{const{attachment:r,result:i}=await this.restoreTab(Number(a.tabId),String(a.targetId??""),String(a.browserSession??""));return e.heldTabs.add(r.tabId),this.announce(e,r),i}case"Mirasim.holdOnly":{const r=typeof a.targetId=="string"&&a.targetId?this.tabIdForTarget(a.targetId):void 0;return this.holdOnly(e,r),r===void 0||!e.heldTabs.has(r)?{held:!1}:{held:!0,tabId:r,browserSession:this.browserSession}}case"Mirasim.closeTab":{this.assertBrowserSession(String(a.browserSession??""));const r=Number(a.tabId);if(!this.knownTabs.has(r))throw new Error(`Tab ${r} was closed; list tabs again.`);return await this.sendToExtension("chrome.tabs.remove",[r]),{}}case"Browser.getVersion":return{protocolVersion:"1.3",product:"Chrome/mirasim-bridge",userAgent:"mirasim-bridge/1.0"};case"Browser.setDownloadBehavior":return{};case"Mirasim.downloads":{if(typeof a.since!="string"||!Number.isFinite(Date.parse(a.since)))throw new Error("Downloads require an ISO start time");if(a.until!==void 0&&(typeof a.until!="string"||!Number.isFinite(Date.parse(a.until))))throw new Error("Downloads require an ISO end time");const r=await this.sendToExtension("chrome.downloads.search",[{startedAfter:a.since,...a.until===void 0?{}:{startedBefore:a.until}}]);return{host:T(),items:r}}case"Browser.getWindowForTarget":return structuredClone(g);case"Browser.setWindowBounds":return{};case"Mirasim.attachRefusals":{const r=Date.now()-6e4;return{refusals:[...this.attachRefusals.values()].filter(i=>i.at>=r).map(({url:i,reason:d})=>({url:i,reason:d}))}}case"Browser.close":throw new Error("Browser.close is not available on the mirasim bridge \u2014 the browser is the user's own");case"Target.setAutoAttach":if(!s)return await this.enableAutoAttach(e),{};break;case"Target.createTarget":return await this.createTarget(typeof a.url=="string"?a.url:void 0);case"Target.closeTarget":return await this.closeTarget(typeof a.targetId=="string"?a.targetId:void 0);case"Target.attachToTarget":if(!s)return await this.attachToTarget(e,typeof a.targetId=="string"?a.targetId:void 0);break;case"Target.attachToBrowserTarget":if(!s)return{sessionId:e.browserAlias()};break;case"Runtime.enable":{if(!s)throw new Error("Runtime.enable requires a target session");const r=this.resolveSession(e,s),i=this.runtimeForSession(r.tabId,r.cdpSessionId),d=e.enableRuntime(s);try{const c=await i.enable(()=>this.sendToExtension("chrome.debugger.sendCommand",[{tabId:r.tabId,...r.cdpSessionId?{sessionId:r.cdpSessionId}:{}},n,...t===void 0?[]:[t]]));if(d)for(const u of i.values())e.runtimeEvent(s,"Runtime.executionContextCreated",u,!0);return c}catch(c){throw d&&e.disableRuntime(s),c}}case"Runtime.disable":if(!s)throw new Error("Runtime.disable requires a target session");return this.resolveSession(e,s),e.disableRuntime(s),{};case"Target.detachFromTarget":{const r=typeof a.sessionId=="string"?a.sessionId:void 0,i=r?e.virtualSessions.get(r):void 0;if(r&&i!==void 0)return e.virtualSessions.delete(r),e.forgetSession(r),this.releaseUnusedTab(i),{};if(r&&e.browserSessions.delete(r))return{};const d=r?this.childSessionOwner.get(r):void 0;if(r&&d===void 0)return{};if(r&&d!==void 0)return await this.sendToExtension("chrome.debugger.sendCommand",[{tabId:d},n,{sessionId:r}]);break}}if(!s)return await this.sendBrowserCommand(n,t);const o=this.resolveSession(e,s);return await this.sendToExtension("chrome.debugger.sendCommand",[{tabId:o.tabId,...o.cdpSessionId?{sessionId:o.cdpSessionId}:{}},n,...t===void 0?[]:[t]])}async enableAutoAttach(e){e.autoAttach=!0,await this.reconcileKnownTabs();const n=[...this.knownTabs].filter(([t,s])=>!w(s)&&this.wants(e,t)).map(([t])=>t);await Promise.all(n.map(t=>this.attachAndAnnounce(e,t).catch(s=>this.log("attach failed:",s))))}async reconcileKnownTabs(){let e;try{e=await this.sendToExtension("chrome.tabs.query",[{}])??[]}catch(t){this.log("tab reconcile failed:",t);return}const n=new Map(e.filter(t=>t.id!==void 0).map(t=>[t.id,t]));for(const t of[...this.knownTabs.keys()]){const s=n.get(t);if(!s)this.knownTabs.delete(t),this.agentTabs.delete(t),this.dropTab(t);else{const a=w(this.knownTabs.get(t));if(this.knownTabs.set(t,s),w(s)&&this.attachments.has(t))this.dropTab(t),this.sendToExtension("bridge.releaseTab",[t]).catch(o=>this.log("release unloaded tab failed:",o));else if(a&&!w(s))for(const o of this.clients)this.wants(o,t)&&this.attachAndAnnounce(o,t).catch(r=>this.log("restored tab attach failed:",r))}}}assertBrowserSession(e){if(!e||!this.browserSession)throw new Error("Reload the Mirasim extension before restoring tabs: this connection has no browser-session identity.");if(e!==this.browserSession)throw new Error("The browser restarted; list tabs and bind the intended target again.")}async tabTarget(e,n){if(this.assertBrowserSession(n),await this.reconcileKnownTabs(),!this.knownTabs.has(e))throw new Error(`Tab ${e} was closed; list tabs again.`);let t;try{t=await this.sendToExtension("chrome.debugger.getTargets",[])}catch(a){throw String(a).includes("Unknown method")?new Error("Reload the Mirasim extension to enable restoring unloaded tabs."):a}const s=t.find(a=>a.tabId===e);if(this.assertBrowserSession(n),!s?.id)throw new Error(`Chrome did not identify tab ${e}; refusing to restore an unknown target.`);return s.id}restoreTab(e,n,t){this.assertBrowserSession(t);const s=`${t}:${e}`,a=this.restoringTabs.get(s);if(a)return a.then(r=>{if(r.result.targetId!==n)throw new Error("Tab identity changed; list and bind it again before acting.");return r});const o=(async()=>{if(!n)throw new Error("Restoring a tab requires its claimed target identity.");if(await this.tabTarget(e,t)!==n)throw new Error(`Tab ${e} changed identity before restoration; no reload was sent.`);const r=this.knownTabs.get(e);if(!r)throw new Error(`Tab ${e} was closed; list tabs again.`);const i=w(r);if(i){try{await this.sendToExtension("chrome.tabs.reload",[e])}catch(b){const l=b instanceof Error?b.message:String(b);throw new Error(l.includes("Unknown method")?"Reload the Mirasim extension to enable restoring unloaded tabs.":`Could not restore tab ${e}: ${l}`)}const h=Date.now()+1e4;for(;w(this.knownTabs.get(e)??{discarded:!0});){if(Date.now()>=h)throw new Error(`Tab ${e} remained unloaded after restoration; no action was sent to its page.`);if(await new Promise(b=>setTimeout(b,50)),await this.reconcileKnownTabs(),!this.knownTabs.has(e))throw new Error(`Tab ${e} closed while restoring; no replacement tab was opened.`)}}const d=await this.ensureAttached(e);if(d.targetInfo.targetId!==n)throw new Error(`Tab ${e} changed identity while restoring; list and bind it again before acting.`);const c=[...this.knownTabs.values()].filter(h=>h.windowId===r.windowId&&h.splitViewId===r.splitViewId&&(h.width??0)>0&&(h.height??0)>0),u=c.find(h=>h.id===e)??c.find(h=>h.active)??c[0];return{attachment:d,result:{targetId:d.targetInfo.targetId,reloaded:i,...u?{viewport:{width:u.width,height:u.height}}:{}}}})();return this.restoringTabs.set(s,o),o.finally(()=>this.restoringTabs.delete(s)).catch(()=>{}),o}async createTarget(e){const n=await this.sendToExtension("chrome.tabs.create",[{url:e||"about:blank",active:!1}]);if(n?.id===void 0)throw new Error("Failed to create tab");this.knownTabs.set(n.id,n),this.agentTabs.add(n.id);const t=await this.ensureAttached(n.id);for(const s of this.clients)s.autoAttach&&this.announce(s,t);return{targetId:t.targetInfo.targetId}}async recycleAgentTabs(){await this.reconcileKnownTabs();const e=[...this.agentTabs].filter(t=>this.knownTabs.has(t));let n=0;for(const t of e){try{await this.sendToExtension("chrome.tabs.remove",[t]),n+=1}catch(s){this.log("recycle failed for tab",t,s)}this.knownTabs.delete(t),this.agentTabs.delete(t),this.dropTab(t)}return{closed:n}}async closeTarget(e){const n=e?this.tabIdForTarget(e):void 0;return n===void 0?{success:!1}:(await this.sendToExtension("chrome.tabs.remove",[n]),{success:!0})}async attachToTarget(e,n){const t=n?this.tabIdForTarget(n):void 0;if(t===void 0)throw new Error(`No target with id: ${n}`);return await this.ensureAttached(t),{sessionId:e.virtualAlias(t)}}async sendBrowserCommand(e,n){if(e==="Target.getTargetInfo"&&!n?.targetId)return{targetInfo:{targetId:"mirasim-browser",type:"browser",title:"",url:"",attached:!0}};const t=this.attachments.values().next().value,s=t?.tabId??[...this.knownTabs].find(([,a])=>!w(a))?.[0];if(s===void 0)throw new Error(`No tab to forward browser-level command: ${e}`);t||await this.ensureAttached(s);try{return await this.sendToExtension("chrome.debugger.sendCommand",[{tabId:s},e,...n===void 0?[]:[n]])}finally{t||this.releaseUnusedTab(s)}}}export{p as BridgeModelCommands};
@@ -17,6 +17,7 @@ export declare abstract class BridgeModelCore {
17
17
  reason: string;
18
18
  at: number;
19
19
  }>;
20
+ private windowOpens;
20
21
  protected clients: Set<BridgeClient>;
21
22
  private nextClientId;
22
23
  constructor(sendToExtension: SendToExtension, log?: (...args: unknown[]) => void);
@@ -26,6 +27,8 @@ export declare abstract class BridgeModelCore {
26
27
  onTabCreated(tab: BridgeTab, opener?: {
27
28
  agent?: boolean;
28
29
  }): void;
30
+ private claimPopup;
31
+ private onWindowOpen;
29
32
  onTabRemoved(tabId: number): void;
30
33
  onTabReplaced(addedTabId: number, removedTabId: number): void;
31
34
  onDebuggerEvent(source: {
@@ -37,14 +40,18 @@ export declare abstract class BridgeModelCore {
37
40
  }): void;
38
41
  onExtensionDisconnected(): void;
39
42
  onExtensionConnected(browserSession?: string): void;
40
- listTargets(): Promise<Array<{
43
+ listTargets(client?: BridgeClient): Promise<Array<{
41
44
  id: string;
42
45
  tabId: number;
43
46
  type: string;
44
47
  url: string;
45
48
  title: string;
46
49
  unloaded: boolean;
50
+ held?: boolean;
47
51
  }>>;
52
+ holdOnly(client: BridgeClient, keep: number | undefined): void;
53
+ private releaseClientTab;
54
+ protected wants(client: BridgeClient, tabId: number): boolean;
48
55
  protected releaseUnusedTab(tabId: number): void;
49
56
  protected runtimeForSession(tabId: number, sessionId?: string): RuntimeDomain;
50
57
  protected resolveSession(client: BridgeClient, sessionId: string): {
@@ -1 +1 @@
1
- import{BridgeClient as f,STAND_IN_PREFIX as h,tabIsUnloaded as c}from"./client.js";import{RuntimeDomain as u,isRuntimeContextEvent as g}from"./runtime-contexts.js";import{BridgeClient as S}from"./client.js";class m{sendToExtension;log;knownTabs=new Map;agentTabs=new Set;browserSession=null;attachments=new Map;attaching=new Map;childSessionOwner=new Map;attachRefusals=new Map;clients=new Set;nextClientId=0;constructor(t,e=()=>{}){this.sendToExtension=t,this.log=e}addClient(t){const e=new f(++this.nextClientId,t);return this.clients.add(e),e}removeClient(t){this.clients.delete(t),t.dispose();const e=new Set([...t.sessionByTabId.keys(),...t.virtualSessions.values()]);for(const s of e)this.releaseUnusedTab(s)}clientCount(){return this.clients.size}onTabCreated(t,e={}){if(t.id!==void 0){if(this.knownTabs.set(t.id,t),e.agent&&this.agentTabs.add(t.id),c(t)){this.attachments.has(t.id)&&(this.dropTab(t.id),this.sendToExtension("bridge.releaseTab",[t.id]).catch(s=>this.log("release unloaded tab failed:",s)));return}for(const s of this.clients)s.autoAttach&&this.attachAndAnnounce(s,t.id).catch(n=>this.log("auto-attach failed:",n))}}onTabRemoved(t){this.knownTabs.delete(t),this.agentTabs.delete(t),this.dropTab(t)}onTabReplaced(t,e){const s=this.agentTabs.has(e);this.onTabRemoved(e),s&&this.agentTabs.add(t)}onDebuggerEvent(t,e,s){const n=t.tabId;if(n===void 0||!this.attachments.has(n)||t.sessionId&&this.childSessionOwner.get(t.sessionId)!==n)return;const i=s?.sessionId;if(e==="Target.attachedToTarget"&&i)this.childSessionOwner.set(i,n);else if(e==="Target.detachedFromTarget"&&i){this.childSessionOwner.delete(i),this.attachments.get(n)?.runtimes.delete(i);for(const a of this.clients)a.forgetSession(i)}if(g(e)&&this.runtimeForSession(n,t.sessionId).update(e,s),t.sessionId){for(const a of this.clients)e.startsWith("Runtime.")?a.runtimeEvent(t.sessionId,e,s):a.send({sessionId:t.sessionId,method:e,params:s});return}for(const a of this.clients){const r=a.sessionByTabId.get(n);r&&(e.startsWith("Runtime.")?a.runtimeEvent(r,e,s):a.send({sessionId:r,method:e,params:s}));for(const[d,l]of a.virtualSessions)l===n&&(e.startsWith("Runtime.")?a.runtimeEvent(d,e,s):a.send({sessionId:d,method:e,params:s}))}}onDebuggerDetach(t){t.tabId!==void 0&&this.dropTab(t.tabId)}onExtensionDisconnected(){for(const t of[...this.attachments.keys()])this.dropTab(t);this.knownTabs.clear(),this.childSessionOwner.clear()}onExtensionConnected(t){const e=!!t&&t===this.browserSession;e||this.agentTabs.clear(),this.log(`extension connected: ${e?"same":"new"} browser session, kept ${this.agentTabs.size} agent tab mark(s)`),this.browserSession=t??null}async listTargets(){await this.reconcileKnownTabs();let t=[];try{t=await this.sendToExtension("chrome.tabs.query",[{}])??[]}catch(n){this.log("live tab query failed:",n)}const e=new Map(t.filter(n=>n.id!==void 0).map(n=>[n.id,n])),s=[];for(const n of this.knownTabs.keys()){const i=e.get(n),o=this.attachments.get(n)?.targetInfo;s.push({tabId:n,id:typeof o?.targetId=="string"?o.targetId:`${h}${n}`,type:"page",url:i?.url??(typeof o?.url=="string"?o.url:""),title:i?.title??(typeof o?.title=="string"?o.title:""),unloaded:c(i??this.knownTabs.get(n))})}return s}releaseUnusedTab(t){!this.attachments.has(t)||[...this.clients].some(e=>e.sessionByTabId.has(t)||[...e.virtualSessions.values()].includes(t))||(this.dropTab(t),this.sendToExtension("bridge.releaseTab",[t]).catch(e=>this.log("release failed:",e)))}runtimeForSession(t,e){const s=this.attachments.get(t);if(!s)throw new Error(`No debugger attachment for tab: ${t}`);const n=e??"";let i=s.runtimes.get(n);return i||(i=new u,s.runtimes.set(n,i)),i}resolveSession(t,e){const s=t.tabBySessionId.get(e);if(s!==void 0)return{tabId:s};const n=t.virtualSessions.get(e);if(n!==void 0)return{tabId:n};const i=this.childSessionOwner.get(e);if(i!==void 0)return{tabId:i,cdpSessionId:e};throw new Error(`No tab for sessionId: ${e}`)}tabIdForTarget(t){for(const s of this.attachments.values())if(s.targetInfo.targetId===t)return s.tabId;if(!t.startsWith(h))return;const e=Number(t.slice(h.length));return Number.isInteger(e)&&this.knownTabs.has(e)?e:void 0}async attachAndAnnounce(t,e){let s;try{s=await this.ensureAttached(e)}catch(n){this.attachRefusals.set(e,{url:this.knownTabs.get(e)?.url??"",reason:n instanceof Error?n.message:String(n),at:Date.now()});for(const i of this.attachRefusals.keys()){if(this.attachRefusals.size<=20)break;this.attachRefusals.delete(i)}throw n}this.attachRefusals.delete(e),this.announce(t,s)}announce(t,e){if(t.sessionByTabId.has(e.tabId))return;const s=t.tabAlias(e.tabId);t.send({method:"Target.attachedToTarget",params:{sessionId:s,targetInfo:{...e.targetInfo,attached:!0},waitingForDebugger:!1}})}ensureAttached(t){const e=this.knownTabs.get(t);if(!e)return Promise.reject(new Error(`Tab ${t} was closed; list tabs again.`));if(c(e))return Promise.reject(new Error(`Tab ${t} is unloaded by Chrome. Restore this tab before attaching its renderer.`));const s=this.attachments.get(t);if(s)return Promise.resolve(s);const n=this.attaching.get(t);if(n)return n;const i=(async()=>{await this.sendToExtension("chrome.debugger.attach",[{tabId:t},"1.3"]);const o=await this.sendToExtension("chrome.debugger.sendCommand",[{tabId:t},"Target.getTargetInfo"]);if(!o?.targetInfo?.targetId)throw new Error(`Tab ${t} reported no targetInfo`);const a={tabId:t,targetInfo:o.targetInfo,runtimes:new Map};return this.attachments.set(t,a),a})();return this.attaching.set(t,i),i.catch(()=>{}).finally(()=>this.attaching.delete(t)),i}dropTab(t){const e=this.attachments.get(t);this.attachments.delete(t);for(const[s,n]of this.childSessionOwner)if(n===t){this.childSessionOwner.delete(s);for(const i of this.clients)i.forgetSession(s)}for(const s of this.clients){const n=s.dropTab(t);n&&e&&s.send({method:"Target.detachedFromTarget",params:{sessionId:n,targetId:e.targetInfo.targetId}})}}}export{S as BridgeClient,m as BridgeModelCore};
1
+ import{BridgeClient as u,STAND_IN_PREFIX as h,tabIsUnloaded as c}from"./client.js";import{RuntimeDomain as g,isRuntimeContextEvent as T}from"./runtime-contexts.js";import{BridgeClient as k}from"./client.js";const w=1e3;function b(d,t){const e=s=>{try{return new URL(s).href.replace(/\?$/,"")}catch{return s}};return e(d)===e(t)}class y{sendToExtension;log;knownTabs=new Map;agentTabs=new Set;browserSession=null;attachments=new Map;attaching=new Map;childSessionOwner=new Map;attachRefusals=new Map;windowOpens=[];clients=new Set;nextClientId=0;constructor(t,e=()=>{}){this.sendToExtension=t,this.log=e}addClient(t){const e=new u(++this.nextClientId,t);return this.clients.add(e),e}removeClient(t){this.clients.delete(t),t.dispose();const e=new Set([...t.sessionByTabId.keys(),...t.virtualSessions.values()]);for(const s of e)this.releaseUnusedTab(s)}clientCount(){return this.clients.size}onTabCreated(t,e={}){if(t.id===void 0)return;const s=!this.knownTabs.has(t.id);if(this.knownTabs.set(t.id,t),e.agent&&this.agentTabs.add(t.id),c(t)){this.attachments.has(t.id)&&(this.dropTab(t.id),this.sendToExtension("bridge.releaseTab",[t.id]).catch(n=>this.log("release unloaded tab failed:",n)));return}s&&this.claimPopup(t);for(const n of this.clients)this.wants(n,t.id)&&this.attachAndAnnounce(n,t.id).catch(i=>this.log("auto-attach failed:",i))}claimPopup(t){if(this.agentTabs.has(t.id))return;const e=Date.now();this.windowOpens=this.windowOpens.filter(a=>e-a.at<=w);const s=t.url??"",n=this.windowOpens.findIndex(a=>!s||s==="about:blank"||a.url==="about:blank"||b(a.url,s));if(n<0)return;const[i]=this.windowOpens.splice(n,1);for(const a of this.clients)!a.autoAttach||!a.sessionByTabId.has(i.from)||(this.agentTabs.has(i.from)||a.keptTabs.has(i.from)?a.keptTabs.add(t.id):a.heldTabs.add(t.id))}onWindowOpen(t,e){const s=e?.url;[...this.clients].some(n=>n.autoAttach&&n.sessionByTabId.has(t))&&this.windowOpens.push({from:t,url:typeof s=="string"?s:"",at:Date.now()})}onTabRemoved(t){this.knownTabs.delete(t),this.agentTabs.delete(t);for(const e of this.clients)e.heldTabs.delete(t),e.keptTabs.delete(t);this.dropTab(t)}onTabReplaced(t,e){const s=this.agentTabs.has(e),n=[...this.clients].filter(a=>a.heldTabs.has(e)),i=[...this.clients].filter(a=>a.keptTabs.has(e));this.onTabRemoved(e),s&&this.agentTabs.add(t);for(const a of n)a.heldTabs.add(t);for(const a of i)a.keptTabs.add(t)}onDebuggerEvent(t,e,s){const n=t.tabId;if(n===void 0||!this.attachments.has(n)||t.sessionId&&this.childSessionOwner.get(t.sessionId)!==n)return;e==="Page.windowOpen"&&this.onWindowOpen(n,s);const i=s?.sessionId;if(e==="Target.attachedToTarget"&&i)this.childSessionOwner.set(i,n);else if(e==="Target.detachedFromTarget"&&i){this.childSessionOwner.delete(i),this.attachments.get(n)?.runtimes.delete(i);for(const o of this.clients)o.forgetSession(i)}if(T(e)&&this.runtimeForSession(n,t.sessionId).update(e,s),t.sessionId){for(const o of this.clients)e.startsWith("Runtime.")?o.runtimeEvent(t.sessionId,e,s):o.send({sessionId:t.sessionId,method:e,params:s});return}for(const o of this.clients){const r=o.sessionByTabId.get(n);r&&(e.startsWith("Runtime.")?o.runtimeEvent(r,e,s):o.send({sessionId:r,method:e,params:s}));for(const[l,f]of o.virtualSessions)f===n&&(e.startsWith("Runtime.")?o.runtimeEvent(l,e,s):o.send({sessionId:l,method:e,params:s}))}}onDebuggerDetach(t){t.tabId!==void 0&&this.dropTab(t.tabId)}onExtensionDisconnected(){for(const t of[...this.attachments.keys()])this.dropTab(t);this.knownTabs.clear(),this.childSessionOwner.clear()}onExtensionConnected(t){const e=!!t&&t===this.browserSession;e||this.agentTabs.clear(),this.log(`extension connected: ${e?"same":"new"} browser session, kept ${this.agentTabs.size} agent tab mark(s)`),this.browserSession=t??null}async listTargets(t){await this.reconcileKnownTabs();let e=[];try{e=await this.sendToExtension("chrome.tabs.query",[{}])??[]}catch(i){this.log("live tab query failed:",i)}const s=new Map(e.filter(i=>i.id!==void 0).map(i=>[i.id,i])),n=[];for(const i of this.knownTabs.keys()){const a=s.get(i),o=this.attachments.get(i)?.targetInfo;n.push({tabId:i,id:typeof o?.targetId=="string"?o.targetId:`${h}${i}`,type:"page",url:a?.url??(typeof o?.url=="string"?o.url:""),title:a?.title??(typeof o?.title=="string"?o.title:""),unloaded:c(a??this.knownTabs.get(i)),...t?{held:t.sessionByTabId.has(i)}:{}})}return n}holdOnly(t,e){for(const s of[...t.heldTabs])s!==e&&!this.agentTabs.has(s)&&this.releaseClientTab(t,s)}releaseClientTab(t,e){t.heldTabs.delete(e);const s=this.attachments.get(e),n=t.dropTab(e);n&&s&&t.send({method:"Target.detachedFromTarget",params:{sessionId:n,targetId:s.targetInfo.targetId}}),this.releaseUnusedTab(e)}wants(t,e){return t.autoAttach&&(this.agentTabs.has(e)||t.keptTabs.has(e)||t.heldTabs.has(e))}releaseUnusedTab(t){!this.attachments.has(t)||[...this.clients].some(e=>e.sessionByTabId.has(t)||[...e.virtualSessions.values()].includes(t))||(this.dropTab(t),this.sendToExtension("bridge.releaseTab",[t]).catch(e=>this.log("release failed:",e)))}runtimeForSession(t,e){const s=this.attachments.get(t);if(!s)throw new Error(`No debugger attachment for tab: ${t}`);const n=e??"";let i=s.runtimes.get(n);return i||(i=new g,s.runtimes.set(n,i)),i}resolveSession(t,e){const s=t.tabBySessionId.get(e);if(s!==void 0)return{tabId:s};const n=t.virtualSessions.get(e);if(n!==void 0)return{tabId:n};const i=this.childSessionOwner.get(e);if(i!==void 0)return{tabId:i,cdpSessionId:e};throw new Error(`No tab for sessionId: ${e}`)}tabIdForTarget(t){for(const s of this.attachments.values())if(s.targetInfo.targetId===t)return s.tabId;if(!t.startsWith(h))return;const e=Number(t.slice(h.length));return Number.isInteger(e)&&this.knownTabs.has(e)?e:void 0}async attachAndAnnounce(t,e){let s;try{s=await this.ensureAttached(e)}catch(n){this.attachRefusals.set(e,{url:this.knownTabs.get(e)?.url??"",reason:n instanceof Error?n.message:String(n),at:Date.now()});for(const i of this.attachRefusals.keys()){if(this.attachRefusals.size<=20)break;this.attachRefusals.delete(i)}throw n}this.attachRefusals.delete(e),this.announce(t,s)}announce(t,e){if(t.sessionByTabId.has(e.tabId))return;const s=t.tabAlias(e.tabId);t.send({method:"Target.attachedToTarget",params:{sessionId:s,targetInfo:{...e.targetInfo,attached:!0},waitingForDebugger:!1}})}ensureAttached(t){const e=this.knownTabs.get(t);if(!e)return Promise.reject(new Error(`Tab ${t} was closed; list tabs again.`));if(c(e))return Promise.reject(new Error(`Tab ${t} is unloaded by Chrome. Restore this tab before attaching its renderer.`));const s=this.attachments.get(t);if(s)return Promise.resolve(s);const n=this.attaching.get(t);if(n)return n;const i=(async()=>{await this.sendToExtension("chrome.debugger.attach",[{tabId:t},"1.3"]);const a=await this.sendToExtension("chrome.debugger.sendCommand",[{tabId:t},"Target.getTargetInfo"]);if(!a?.targetInfo?.targetId)throw new Error(`Tab ${t} reported no targetInfo`);const o={tabId:t,targetInfo:a.targetInfo,runtimes:new Map};return this.attachments.set(t,o),o})();return this.attaching.set(t,i),i.catch(()=>{}).finally(()=>this.attaching.delete(t)),i}dropTab(t){const e=this.attachments.get(t);this.attachments.delete(t);for(const[s,n]of this.childSessionOwner)if(n===t){this.childSessionOwner.delete(s);for(const i of this.clients)i.forgetSession(s)}for(const s of this.clients){const n=s.dropTab(t);n&&e&&s.send({method:"Target.detachedFromTarget",params:{sessionId:n,targetId:e.targetInfo.targetId}})}}}export{k as BridgeClient,y as BridgeModelCore};
@@ -4,19 +4,27 @@ interface BridgeTabInfo {
4
4
  title: string;
5
5
  url: string;
6
6
  unloaded: boolean;
7
+ held?: boolean;
7
8
  browserSession: string;
8
9
  }
9
10
  export declare function bridgeRequest<T>(browser: Browser, method: string, params?: Record<string, unknown>): Promise<T>;
10
11
  export declare function bridgeTabs(browser: Browser): Promise<{
11
- unloaded: BridgeTabInfo[];
12
+ dormant: BridgeTabInfo[];
12
13
  browserSession: string | null;
13
14
  }>;
14
15
  export declare function bridgeBrowserSession(browser: Browser): Promise<string | null>;
15
16
  export declare function restoreBridgeTab(browser: Browser, tabId: number, targetId: string, browserSession: string): Promise<{
16
17
  page: Page;
18
+ reloaded: boolean;
17
19
  viewport?: {
18
20
  width: number;
19
21
  height: number;
20
22
  };
21
23
  }>;
24
+ export declare function holdOnlyBridgeTab(browser: Browser, targetId: string | undefined): Promise<{
25
+ held: boolean;
26
+ tabId?: number;
27
+ browserSession?: string;
28
+ }>;
29
+ export declare function closeBridgeTab(browser: Browser, tabId: number, browserSession: string): Promise<void>;
22
30
  export {};
@@ -1 +1 @@
1
- import{pageTargetInfo as w}from"./tabs.js";async function s(n,e,t={}){const o=await n.newBrowserCDPSession();try{return await o.send(e,t)}finally{await o.detach().catch(()=>{})}}async function c(n){if(n.version()!=="mirasim-bridge")return{unloaded:[],browserSession:null};try{const e=await s(n,"Mirasim.tabs");return{unloaded:e.tabs.filter(o=>o.unloaded).map(o=>({...o,browserSession:e.browserSession??""})),browserSession:e.browserSession||null}}catch(e){if(/wasn't found|not found|not allowed|unknown method/i.test(String(e)))return{unloaded:[],browserSession:null};throw e}}async function u(n){return(await c(n).catch(()=>null))?.browserSession??null}async function f(n,e,t,o){const a=await s(n,"Mirasim.restoreTab",{tabId:e,targetId:t,browserSession:o}),i=Date.now()+1e4;for(;Date.now()<i;){for(const r of n.contexts().flatMap(l=>l.pages()))if(!r.isClosed()&&(await w(r).catch(()=>null))?.targetId===t)return{page:r,viewport:a.viewport};await new Promise(r=>setTimeout(r,50))}throw new Error(`Restored tab ${e} did not become readable; no action was sent to a replacement page.`)}export{u as bridgeBrowserSession,s as bridgeRequest,c as bridgeTabs,f as restoreBridgeTab};
1
+ import{pageTargetInfo as c}from"./tabs.js";async function a(n,e,r={}){const o=await n.newBrowserCDPSession();try{return await o.send(e,r)}finally{await o.detach().catch(()=>{})}}async function d(n){if(n.version()!=="mirasim-bridge")return{dormant:[],browserSession:null};try{const e=await a(n,"Mirasim.tabs");return{dormant:e.tabs.filter(o=>o.unloaded||o.held===!1).map(o=>({...o,browserSession:e.browserSession??""})),browserSession:e.browserSession||null}}catch(e){if(/wasn't found|not found|not allowed|unknown method/i.test(String(e)))return{dormant:[],browserSession:null};throw e}}async function u(n){return(await d(n).catch(()=>null))?.browserSession??null}async function f(n,e,r,o){const s=await a(n,"Mirasim.restoreTab",{tabId:e,targetId:r,browserSession:o}),i=Date.now()+1e4;for(;Date.now()<i;){for(const t of n.contexts().flatMap(l=>l.pages()))if(!t.isClosed()&&(await c(t).catch(()=>null))?.targetId===r)return{page:t,reloaded:s.reloaded??!0,viewport:s.viewport};await new Promise(t=>setTimeout(t,50))}throw new Error(`Restored tab ${e} did not become readable; no action was sent to a replacement page.`)}async function b(n,e){return await a(n,"Mirasim.holdOnly",e?{targetId:e}:{})}async function m(n,e,r){await a(n,"Mirasim.closeTab",{tabId:e,browserSession:r})}export{u as bridgeBrowserSession,a as bridgeRequest,d as bridgeTabs,m as closeBridgeTab,b as holdOnlyBridgeTab,f as restoreBridgeTab};
@@ -1 +1 @@
1
- import{mkdir as n}from"node:fs/promises";import{join as e}from"node:path";import{chromium as i}from"playwright-core";import{sharedHome as c}from"../home.js";function m(){return e(c(),"downloads",".staging")}async function p(t,r){const o=m();return await n(o,{recursive:!0,mode:448}),i.connectOverCDP(t,{artifactsDir:o,...r===void 0?{}:{timeout:r}})}export{m as browserArtifactsDir,p as connectBrowserCdp};
1
+ import{mkdir as n}from"node:fs/promises";import{join as c}from"node:path";import{chromium as i}from"playwright-core";import{sharedHome as a}from"../home.js";function s(){return c(a(),"downloads",".staging")}const d={colorScheme:null,reducedMotion:null,forcedColors:null,contrast:null};function m(t){const r=o=>{o.emulateMedia(d).catch(()=>{})},e=o=>{o.on("page",r),o.pages().forEach(r)};t.on("context",e),t.contexts().forEach(e)}async function h(t,r){const e=s();await n(e,{recursive:!0,mode:448});const o=await i.connectOverCDP(t,{artifactsDir:e,...r===void 0?{}:{timeout:r}});return m(o),o}export{s as browserArtifactsDir,h as connectBrowserCdp};
@@ -2,6 +2,7 @@ import { type Page } from "playwright-core";
2
2
  import { type ActResult } from "../types.ts";
3
3
  import { BrowserDriverConnect } from "./driver-connect.ts";
4
4
  export declare abstract class BrowserDriverVerbs extends BrowserDriverConnect {
5
+ protected closeDormantTab(_id: string, _confirmed: boolean): Promise<ActResult | null>;
5
6
  private pid;
6
7
  private cdpSessions;
7
8
  protected readonly overridden: Set<Page>;
@@ -1,2 +1,2 @@
1
- import{readFileSync as g}from"node:fs";import"playwright-core";import"../types.js";import{NO_TAB_OPEN_NOTE as m,NO_TAB_YET_NOTE as b}from"./messages.js";import{endpointFileFor as v,keepInBackground as y}from"./acquire.js";import{isFrontmost as k,returnFocusIfTaken as $}from"./focus-guard.js";import{BrowserDriverConnect as _}from"./driver-connect.js";import{landedNote as f,NAV_TIMEOUT_MS as c,navVerdict as d}from"./navigate.js";import{newBackgroundPage as w}from"./tabs.js";import{detectInterference as x,overlaySensitive as P}from"./overlay-guard.js";import{performUploadToRef as T,refuseMissingFiles as I}from"../chromium/actions.js";import{performRunJs as S}from"../chromium/run-js.js";import{withPageCapture as A}from"../chromium/shot.js";class z extends _{pid;cdpSessions=new WeakMap;overridden=new Set;async perform(i,t){if(i==="download"&&typeof t.id=="string")return await this.downloads.query(t.id,t.timeoutMs,t.stop,this.browser??void 0);if(i==="open_url"||i==="new_tab")this.wokeAfterSelfClose=this.closedItsLastTab,this.closedItsLastTab=!1;else if(this.closedItsLastTab)return{ok:!1,note:m};await this.ensure();let r=null;if(!this.active){if(i!=="open_url"&&i!=="new_tab")return{ok:!1,note:b};if(this.active=await w(this.context),this.claim(this.active),i==="new_tab"){if(r=this.tabId(this.active),!t.url)return{ok:!0,note:`new tab ${r} is now the target`};i="open_url"}}this.hookNavigation(),await this.ensureComposited();const s=await k(this.browserPid());if(this.lastPoint=null,this.cfg.extension&&P(i)){const o=await x(this.page());if(o){const h=`another extension or overlay is over the page right now \u2014 ${o}. Close it (dismiss the popup, click away from the autofill suggestion, switch off the interfering extension for this site), then re-observe and act. Acting now would land on the overlay, not the target.`;return this.journal?.publishAction(i,t,this.lastPoint,{ok:!1,note:h}),{ok:!1,note:h}}}let e;try{const o=this.page();e=await A(o,()=>this.page()!==o||o.isClosed()?Promise.resolve({ok:!1,note:"the target changed while waiting for capture \u2014 re-observe the intended tab before acting"}):this.dispatch(i,t))}catch(o){const h=o instanceof Error?o.message.split(`
2
- `)[0]??"dispatch failed":String(o);throw this.journal?.publishAction(i,t,this.lastPoint,{ok:!1,note:h}),o}r&&(e={...e,note:`new tab ${r} is now the target; ${e.note??""}`}),this.journal?.publishAction(i,t,this.lastPoint,e),this.managedProfileDir&&!this.closedItsLastTab&&i!=="activate"&&await y(this.context,this.page(),this.managedProfileDir,this.cfg),this.windowAppeared&&(this.windowAppeared=!1,!s&&i!=="activate"&&await $(this.browserPid()));const n=this.takePendingNote();return n&&(e.note=e.note?`${n}. ${e.note}`:n),e}browserPid(){if(this.pid===void 0){this.pid=null;try{const i=JSON.parse(g(v(this.managedProfileDir),"utf8"));typeof i.pid=="number"&&(this.pid=i.pid)}catch{}}return this.pid}async cdpFor(i){let t=this.cdpSessions.get(i);return t||(t=await this.context.newCDPSession(i),this.cdpSessions.set(i,t)),t}async ensureComposited(){if(this.managedProfileDir)try{const i=await this.cdpFor(this.page()),{windowId:t,bounds:r}=await i.send("Browser.getWindowForTarget");if(r.windowState!=="minimized")return;await i.send("Browser.setWindowBounds",{windowId:t,bounds:{windowState:"normal"}}),this.cfg.window==="offscreen"&&await i.send("Browser.setWindowBounds",{windowId:t,bounds:{left:-4e3,top:200}})}catch{}}async dispatch(i,t){const r=this.page();switch(i){case"download":return t.stop||t.timeoutMs!==void 0?{ok:!1,note:"download stop/timeoutMs require an existing id; omit them to arm a watch"}:await this.downloads.arm(this.browser,r);case"upload":{const s=Array.isArray(t.paths)?t.paths:[],e=I(s);if(e)return e;if(t.ref){const o=await T(s,t.ref,()=>this.locate(t.ref,t.name,!1),()=>this.missing(t.ref,t.name));return!o.ok&&o.note?.includes("did not take the files")?{...o,note:`${o.note}; for a button that opens a chooser, drop ref and click the button next`}:o}this.armedChooser&&r.off("filechooser",this.armedChooser);const n=o=>{o.setFiles(s)};return this.armedChooser=n,r.once("filechooser",n),{ok:!0,note:`the next file chooser will receive ${s.length} file${s.length===1?"":"s"} \u2014 nothing has changed yet; click the control that opens it`}}case"dialog":return this.armedDialog={accept:t.answer==="accept",...typeof t.text=="string"?{text:t.text}:{}},{ok:!0,note:`the next JS dialog will be ${t.answer}ed \u2014 nothing on the page has changed yet; the answer is used when the action that raises the dialog runs`};case"open_url":try{return await r.goto(t.url,{waitUntil:"domcontentloaded",timeout:c}),{ok:!0,note:f("opened",t.url,r.url())}}catch(s){return d(s)}case"activate":return await r.bringToFront(),{ok:!0,note:"activated this browser tab and its window \u2014 foreground focus was requested explicitly"};case"run_js":return await S(()=>r,t.code);case"back":{const s=r.url();try{return await r.goBack({waitUntil:"commit",timeout:c})===null&&r.url()===s?{ok:!0,note:"no earlier page in this tab's history"}:{ok:!0,note:`went back to ${r.url()}`}}catch(e){return d(e)}}case"reload":{const s=r.url();try{return await r.reload({waitUntil:"domcontentloaded",timeout:c}),{ok:!0,note:f("reloaded",s,r.url())}}catch(e){return d(e)}}case"new_tab":{const s=await w(this.context);this.claim(s),this.active=s,this.resetObservationState();const e=this.tabId(s);if(typeof t.url=="string"&&t.url){try{await s.goto(t.url,{waitUntil:"domcontentloaded",timeout:c})}catch(n){const o=d(n);return{...o,note:`new tab ${e} is now the target; ${o.note??"its navigation did not finish"}`}}return{ok:!0,note:`new tab ${e} is now the target; ${f("opened",t.url,s.url())}`}}return{ok:!0,note:`new tab ${e} is now the target`}}case"viewport":{const s=await this.cdpFor(r);if(t.reset)return await s.send("Emulation.clearDeviceMetricsOverride"),this.overridden.delete(r),{ok:!0,note:"viewport override cleared \u2014 this tab is back to the real window size"};const e=Math.round(Number(t.width)||1200),n=Math.round(Number(t.height)||1700);await s.send("Emulation.setDeviceMetricsOverride",{width:e,height:n,deviceScaleFactor:0,mobile:!1}),this.overridden.add(r);const o=await r.evaluate("({ width: innerWidth, height: innerHeight })");return o.width!==e||o.height!==n?{ok:!1,note:`viewport ${e}x${n} did not take effect \u2014 the page reports ${o.width}x${o.height}`}:{ok:!0,note:`viewport of this tab set to ${e}x${n} \u2014 navigate AFTER this, not before, or a list already mounted keeps the rows it has; the size is the user's own again on viewport({reset:true}) or when this session ends`}}case"close_tab":{const s=(await this.ownPages()).filter(a=>!a.isClosed());if(t.all){if(typeof t.id=="string"&&t.id)return{ok:!1,note:"close_tab takes id OR all, not both \u2014 all closes every tab this session owns"};for(const a of s)await a.close().catch(()=>{});return this.closedItsLastTab=!0,{ok:!0,note:`closed ${s.length} tab${s.length===1?"":"s"} \u2014 every one this session opened or claimed; the user's own tabs were not touched. open_url or new_tab starts a fresh one`}}const e=typeof t.id=="string"&&t.id?t.id.replace(`${this.platform}/`,""):this.tabId(r);let n=s.find(a=>this.tabId(a)===e),o=!1;if(!n){const a=(await this.visiblePages()).find(p=>this.tabId(p)===e);if(!a)return{ok:!1,note:`no tab with id ${e} \u2014 call computer.surfaces() for current ids`};if(!t.confirmed)return{ok:!1,note:`${e} is the user's own tab, not this session's. Ask them in the conversation whether to close it \u2014 say which page \u2014 and call close_tab again with confirmed: true if they agree. Reading it and acting in it needs no permission; only closing does`};n=a,o=!0}const h=n===this.active;await n.close().catch(()=>{});const l=o?" \u2014 the user's own, closed on the agreement you reported":"";if(!h)return{ok:!0,note:`closed tab ${e}${l}; the target is unchanged`};const u=s.find(a=>a!==n&&!a.isClosed());return u?(this.active=u,this.resetObservationState(),{ok:!0,note:`closed tab ${e}${l}; the target moved to ${this.tabId(u)} \u2014 re-observe before acting on any earlier ref`}):(this.closedItsLastTab=!0,{ok:!0,note:`closed tab ${e}${l} \u2014 this session has no tab left; open_url or new_tab starts a fresh one`})}}return await this.dispatchPage(i,t)??{ok:!1,note:`unknown action kind ${i}`}}}export{z as BrowserDriverVerbs};
1
+ import{readFileSync as g}from"node:fs";import"playwright-core";import"../types.js";import{NO_TAB_OPEN_NOTE as m,NO_TAB_YET_NOTE as b,userTabCloseRefusal as v}from"./messages.js";import{endpointFileFor as k,keepInBackground as y}from"./acquire.js";import{isFrontmost as $,returnFocusIfTaken as _}from"./focus-guard.js";import{BrowserDriverConnect as T}from"./driver-connect.js";import{landedNote as f,NAV_TIMEOUT_MS as l,navVerdict as d}from"./navigate.js";import{newBackgroundPage as p}from"./tabs.js";import{detectInterference as x,overlaySensitive as P}from"./overlay-guard.js";import{performUploadToRef as S,refuseMissingFiles as I}from"../chromium/actions.js";import{performRunJs as A}from"../chromium/run-js.js";import{withPageCapture as C}from"../chromium/shot.js";class J extends T{async closeDormantTab(o,t){return null}pid;cdpSessions=new WeakMap;overridden=new Set;async perform(o,t){if(o==="download"&&typeof t.id=="string")return await this.downloads.query(t.id,t.timeoutMs,t.stop,this.browser??void 0);if(o==="open_url"||o==="new_tab")this.wokeAfterSelfClose=this.closedItsLastTab,this.closedItsLastTab=!1;else if(this.closedItsLastTab)return{ok:!1,note:m};const r=this.wokeAfterSelfClose;await this.ensure();let e=null;if(o==="new_tab"&&r&&!this.wokeAfterSelfClose&&this.active&&(!this.active.url()||this.active.url()==="about:blank")){if(e=this.tabId(this.active),!t.url)return{ok:!0,note:`new tab ${e} is now the target`};o="open_url"}if(!this.active){if(o!=="open_url"&&o!=="new_tab")return{ok:!1,note:b};if(this.active=await p(this.context),this.claim(this.active),o==="new_tab"){if(e=this.tabId(this.active),!t.url)return{ok:!0,note:`new tab ${e} is now the target`};o="open_url"}}this.hookNavigation(),await this.ensureComposited();const i=await $(this.browserPid());if(this.lastPoint=null,this.cfg.extension&&P(o)){const a=await x(this.page());if(a){const c=`another extension or overlay is over the page right now \u2014 ${a}. Close it (dismiss the popup, click away from the autofill suggestion, switch off the interfering extension for this site), then re-observe and act. Acting now would land on the overlay, not the target.`;return this.journal?.publishAction(o,t,this.lastPoint,{ok:!1,note:c}),{ok:!1,note:c}}}let s;try{const a=this.page();s=await C(a,()=>this.page()!==a||a.isClosed()?Promise.resolve({ok:!1,note:"the target changed while waiting for capture \u2014 re-observe the intended tab before acting"}):this.dispatch(o,t))}catch(a){const c=a instanceof Error?a.message.split(`
2
+ `)[0]??"dispatch failed":String(a);throw this.journal?.publishAction(o,t,this.lastPoint,{ok:!1,note:c}),a}e&&(s={...s,note:`new tab ${e} is now the target; ${s.note??""}`}),this.journal?.publishAction(o,t,this.lastPoint,s),this.managedProfileDir&&!this.closedItsLastTab&&o!=="activate"&&await y(this.context,this.page(),this.managedProfileDir,this.cfg),this.windowAppeared&&(this.windowAppeared=!1,!i&&o!=="activate"&&await _(this.browserPid()));const n=this.takePendingNote();return n&&(s.note=s.note?`${n}. ${s.note}`:n),s}browserPid(){if(this.pid===void 0){this.pid=null;try{const o=JSON.parse(g(k(this.managedProfileDir),"utf8"));typeof o.pid=="number"&&(this.pid=o.pid)}catch{}}return this.pid}async cdpFor(o){let t=this.cdpSessions.get(o);return t||(t=await this.context.newCDPSession(o),this.cdpSessions.set(o,t)),t}async ensureComposited(){if(this.managedProfileDir)try{const o=await this.cdpFor(this.page()),{windowId:t,bounds:r}=await o.send("Browser.getWindowForTarget");if(r.windowState!=="minimized")return;await o.send("Browser.setWindowBounds",{windowId:t,bounds:{windowState:"normal"}}),this.cfg.window==="offscreen"&&await o.send("Browser.setWindowBounds",{windowId:t,bounds:{left:-4e3,top:200}})}catch{}}async dispatch(o,t){const r=this.page();switch(o){case"download":return t.stop||t.timeoutMs!==void 0?{ok:!1,note:"download stop/timeoutMs require an existing id; omit them to arm a watch"}:await this.downloads.arm(this.browser,r);case"upload":{const e=Array.isArray(t.paths)?t.paths:[],i=I(e);if(i)return i;if(t.ref){const n=await S(e,t.ref,()=>this.locate(t.ref,t.name,!1),()=>this.missing(t.ref,t.name));return!n.ok&&n.note?.includes("did not take the files")?{...n,note:`${n.note}; for a button that opens a chooser, drop ref and click the button next`}:n}this.armedChooser&&r.off("filechooser",this.armedChooser);const s=n=>{n.setFiles(e)};return this.armedChooser=s,r.once("filechooser",s),{ok:!0,note:`the next file chooser will receive ${e.length} file${e.length===1?"":"s"} \u2014 nothing has changed yet; click the control that opens it`}}case"dialog":return this.armedDialog={accept:t.answer==="accept",...typeof t.text=="string"?{text:t.text}:{}},{ok:!0,note:`the next JS dialog will be ${t.answer}ed \u2014 nothing on the page has changed yet; the answer is used when the action that raises the dialog runs`};case"open_url":try{return await r.goto(t.url,{waitUntil:"domcontentloaded",timeout:l}),{ok:!0,note:f("opened",t.url,r.url())}}catch(e){return d(e)}case"activate":return await r.bringToFront(),{ok:!0,note:"activated this browser tab and its window \u2014 foreground focus was requested explicitly"};case"run_js":return await A(()=>r,t.code);case"back":{const e=r.url();try{return await r.goBack({waitUntil:"commit",timeout:l})===null&&r.url()===e?{ok:!0,note:"no earlier page in this tab's history"}:{ok:!0,note:`went back to ${r.url()}`}}catch(i){return d(i)}}case"reload":{const e=r.url();try{return await r.reload({waitUntil:"domcontentloaded",timeout:l}),{ok:!0,note:f("reloaded",e,r.url())}}catch(i){return d(i)}}case"new_tab":{const e=await p(this.context);this.claim(e),this.active=e,this.resetObservationState();const i=this.tabId(e);if(typeof t.url=="string"&&t.url){try{await e.goto(t.url,{waitUntil:"domcontentloaded",timeout:l})}catch(s){const n=d(s);return{...n,note:`new tab ${i} is now the target; ${n.note??"its navigation did not finish"}`}}return{ok:!0,note:`new tab ${i} is now the target; ${f("opened",t.url,e.url())}`}}return{ok:!0,note:`new tab ${i} is now the target`}}case"viewport":{const e=await this.cdpFor(r);if(t.reset)return await e.send("Emulation.clearDeviceMetricsOverride"),this.overridden.delete(r),{ok:!0,note:"viewport override cleared \u2014 this tab is back to the real window size"};const i=Math.round(Number(t.width)||1200),s=Math.round(Number(t.height)||1700);await e.send("Emulation.setDeviceMetricsOverride",{width:i,height:s,deviceScaleFactor:0,mobile:!1}),this.overridden.add(r);const n=await r.evaluate("({ width: innerWidth, height: innerHeight })");return n.width!==i||n.height!==s?{ok:!1,note:`viewport ${i}x${s} did not take effect \u2014 the page reports ${n.width}x${n.height}`}:{ok:!0,note:`viewport of this tab set to ${i}x${s} \u2014 navigate AFTER this, not before, or a list already mounted keeps the rows it has; the size is the user's own again on viewport({reset:true}) or when this session ends`}}case"close_tab":{const e=(await this.ownPages()).filter(h=>!h.isClosed());if(t.all){if(typeof t.id=="string"&&t.id)return{ok:!1,note:"close_tab takes id OR all, not both \u2014 all closes every tab this session owns"};for(const h of e)await h.close().catch(()=>{});return this.closedItsLastTab=!0,{ok:!0,note:`closed ${e.length} tab${e.length===1?"":"s"} \u2014 every one this session opened or claimed; the user's own tabs were not touched. open_url or new_tab starts a fresh one`}}const i=typeof t.id=="string"&&t.id?t.id.replace(`${this.platform}/`,""):this.tabId(r);let s=e.find(h=>this.tabId(h)===i),n=!1;if(!s){const h=(await this.visiblePages()).find(w=>this.tabId(w)===i);if(!h)return await this.closeDormantTab(i,t.confirmed===!0)??{ok:!1,note:`no tab with id ${i} \u2014 call computer.surfaces() for current ids`};if(!t.confirmed)return{ok:!1,note:v(i)};s=h,n=!0}const a=s===this.active;await s.close().catch(()=>{});const c=n?" \u2014 the user's own, closed on the agreement you reported":"";if(!a)return{ok:!0,note:`closed tab ${i}${c}; the target is unchanged`};const u=e.find(h=>h!==s&&!h.isClosed());return u?(this.active=u,this.resetObservationState(),{ok:!0,note:`closed tab ${i}${c}; the target moved to ${this.tabId(u)} \u2014 re-observe before acting on any earlier ref`}):(this.closedItsLastTab=!0,{ok:!0,note:`closed tab ${i}${c} \u2014 this session has no tab left; open_url or new_tab starts a fresh one`})}}return await this.dispatchPage(o,t)??{ok:!1,note:`unknown action kind ${o}`}}}export{J as BrowserDriverVerbs};
@@ -2,10 +2,16 @@ import type { Page } from "playwright-core";
2
2
  import { type ActResult, type HeaderChanges, type OwnershipResource, type Snapshot, type TargetInfo, type ToolResult, type VerbSpec, type Verdict } from "../types.ts";
3
3
  import { BrowserDriverVerbs } from "./driver-verbs.ts";
4
4
  export declare class BrowserDriver extends BrowserDriverVerbs {
5
- private unloadedSelection;
5
+ private dormantSelection;
6
+ private borrowed;
7
+ private reported;
8
+ private holdUnsupported;
6
9
  activeTarget(): Promise<string | null>;
7
10
  ownershipResources(): Promise<OwnershipResource[]>;
8
11
  prepareTarget(): Promise<void>;
12
+ private onBridge;
13
+ private syncHeld;
14
+ releaseIdle(): Promise<void>;
9
15
  protected page(): Page;
10
16
  protected adoptWhenReal(page: Page): void;
11
17
  protected previewTarget(): Promise<import("../types.ts").PreviewTarget | null>;
@@ -24,6 +30,7 @@ export declare class BrowserDriver extends BrowserDriverVerbs {
24
30
  }): Promise<ToolResult>;
25
31
  observeImage(opts?: Parameters<BrowserDriverVerbs["observeImage"]>[0]): Promise<ToolResult>;
26
32
  private noTabNote;
33
+ heldNothing(): Promise<"closed" | "not-yet" | null>;
27
34
  private noTabObservation;
28
35
  protected staleRefCause(): string;
29
36
  explainMiss(query: string): string | null;
@@ -32,6 +39,9 @@ export declare class BrowserDriver extends BrowserDriverVerbs {
32
39
  protected noteBridgeSession(session: string | null): boolean;
33
40
  actionResources(kind: string, args: Record<string, unknown>): Promise<OwnershipResource[]>;
34
41
  focusTarget(id: string): Promise<ActResult>;
42
+ perform(kind: string, a: Record<string, any>): Promise<ActResult>;
43
+ private selectTarget;
44
+ protected closeDormantTab(id: string, confirmed: boolean): Promise<ActResult | null>;
35
45
  close(): Promise<void>;
36
46
  protected journalVerdict(verdict: Verdict): void;
37
47
  protected renderHeadLines(s: Snapshot, targets: TargetInfo[], targetsUnchanged?: boolean, changes?: HeaderChanges): string[];
@@ -1,3 +1,3 @@
1
- import{Type as i}from"typebox";import{SurfaceUnreadableError as d,text as h}from"../types.js";import{NO_TAB_OPEN_NOTE as c,NO_TAB_YET_NOTE as u}from"./messages.js";import{PASTE_VERB as b,SELECT_OPTION_VERB as p}from"../chromium/actions-value.js";import{SELECT_TEXT_VERB as w}from"../chromium/select-text.js";import{DIALOG_VERB as f,DRAG_VERB as g,UPLOAD_VERB as m}from"../chromium/actions.js";import{RUN_JS_CODE_DESC as v}from"../chromium/run-js.js";import{BrowserDriverVerbs as y}from"./driver-verbs.js";import{previewPage as k}from"../chromium/preview.js";import{withPageCapture as S}from"../chromium/shot.js";import{DOWNLOAD_VERB as T}from"./downloads.js";import{bridgeRequest as O,bridgeTabs as E,restoreBridgeTab as _}from"./bridge-tabs.js";const R=5e3,N=1500,P=/^(?:[a-z][a-z\d+.-]*:\/\/|(?:localhost|\d{1,3}(?:\.\d{1,3}){3}|\[[\da-f:]+\])(?::\d+)?(?:[/?#]|$)|[\w.-]+:\d+(?:[/?#]|$))/i;async function L(l){if(!l.url())return"";let e;try{return await Promise.race([l.title().catch(()=>""),new Promise(t=>{e=setTimeout(()=>t(""),N),e.unref?.()})])}finally{e&&clearTimeout(e)}}class H extends y{unloadedSelection=null;async activeTarget(){return this.unloadedSelection?.id??super.activeTarget()}async ownershipResources(){return this.unloadedSelection?[{key:`chromium:${this.unloadedSelection.targetId}`,mode:"exclusive"}]:super.ownershipResources()}async prepareTarget(){const e=this.unloadedSelection;if(!e)return;const{page:t,viewport:s}=await _(this.browser,e.tabId,e.targetId,e.browserSession);this.tabIds.set(t,e.id),this.active=t,this.unloadedSelection=null,this.resetObservationState();const o=await t.evaluate("({ width: innerWidth, height: innerHeight })");if(o.width<=0||o.height<=0){const a=await this.perform("viewport",s??{});if(!a.ok)throw new d(`The restored tab has no usable viewport: ${a.note??"resize failed"}`);this.appendNote("Chrome restored this background tab with a zero-size viewport; applied a temporary viewport, cleared when this driver disconnects.")}this.appendNote("Chrome had unloaded this tab; restored the same tab in the background. Read its current state before continuing.")}page(){if(this.unloadedSelection)throw new d(`Tab ${this.unloadedSelection.id} has not been restored; no other page was observed or operated.`);return super.page()}adoptWhenReal(e){this.unloadedSelection||super.adoptWhenReal(e)}previewTarget(){return this.unloadedSelection||!this.browser?.isConnected()||!this.active||this.active.isClosed()?Promise.resolve(null):k(this.active,this.cdpUrl,"browser")}platform="browser";async turnEnded(e){if(this.attached||e.keepBrowserTabs===!0)return;const t=await this.ownPages().catch(()=>[]);for(const s of t)await s.close().catch(()=>{});t.length&&(this.closedItsLastTab=!0,this.resetObservationState())}extraVerbs(){return[{kind:"open_url",schema:i.Object({kind:i.Literal("open_url"),url:i.String({description:"Full URL to open. Only URLs that appeared on a page or were given in the task \u2014 never guess domains."})})},{kind:"activate",schema:i.Object({kind:i.Literal("activate")},{description:"Bring this browser tab and its window to the foreground. This changes the user's foreground window: explain the effect and obtain permission unless it is already authorized. focus() only selects the target. Embedded writes normally wait for the person to yield the window; activate() is an explicit foreground handoff, revoked by subsequent human input. Do not use it to bypass an input wait without the user handing over control."})},{kind:"back",schema:i.Object({kind:i.Literal("back")})},{kind:"reload",schema:i.Object({kind:i.Literal("reload")})},{kind:"new_tab",schema:i.Object({kind:i.Literal("new_tab"),url:i.Optional(i.String({description:"URL to open in it; omit for a blank tab"}))},{description:"Open a NEW tab of this session's own and make it the target \u2014 the current tab keeps its state, and both are listed as targets afterwards."})},{kind:"close_tab",schema:i.Object({kind:i.Literal("close_tab"),id:i.Optional(i.String({description:"Tab id from computer.surfaces(); omit to close the one you are on"})),all:i.Optional(i.Boolean({description:"Close every tab this session opened or claimed \u2014 the tidy-up at the end of a job. Never the user's own, whatever else is set. Refused together with id."})),confirmed:i.Optional(i.Boolean({description:"Only for a tab marked (user's): you ASKED the user in the conversation and they said yes. Never set it because closing would be convenient \u2014 the first refusal tells you to ask, and asking is the point."}))},{description:"Close a tab: the tabs you open outlive your turn and nothing closes them for you, so follow the tab policy the host states (if it states none, close your own once the job is done), and leave open a page that is waiting for the person (a sign-in, a security-key or 2FA prompt, a form for them to confirm). This session's own always; one of the user's (marked (user's) in the listing) only after you asked them in the conversation and they agreed \u2014 then pass confirmed: true."})},{kind:"viewport",schema:i.Object({kind:i.Literal("viewport"),width:i.Optional(i.Number({description:"Viewport width in CSS pixels (default 1200)"})),height:i.Optional(i.Number({description:"Viewport height in CSS pixels \u2014 this is the one that decides how much of a long list exists (default 1700)"})),reset:i.Optional(i.Boolean({description:"Put the tab back to the real window size"}))},{description:"Give THIS tab a viewport of your own size, so a virtualized list (a feed, a search page, a long transcript) mounts more of itself per read. Set it BEFORE opening the page: rows mount against the viewport in effect at load."})},{kind:"run_js",readOnly:!0,schema:i.Object({kind:i.Literal("run_js"),code:i.String({description:v})},{description:"Ask the PAGE something no observation carries \u2014 read its data in bulk, its stored state, or an API it exposes. This is a stranger's site holding the user's logged-in session: read and compute here, act through the interaction verbs."})},g,f,m,T,p,w,b]}async observeElements(e={}){const t=await this.noTabNote();return t?this.noTabObservation(t,e.prefix):super.observeElements(e)}async observeImage(e={}){const t=await this.noTabNote();return t?this.noTabObservation(t,e.prefix):super.observeImage(e)}async noTabNote(){return this.closedItsLastTab?c:this.unloadedSelection?null:(await this.ensure(),this.active?null:u)}noTabObservation(e,t){const s=this.takePendingNote();return h(`${t?`${t}
1
+ import{Type as i}from"typebox";import{SurfaceUnreadableError as l,text as u}from"../types.js";import{NO_TAB_OPEN_NOTE as b,NO_TAB_YET_NOTE as p,userTabCloseRefusal as w}from"./messages.js";import{PASTE_VERB as f,SELECT_OPTION_VERB as m}from"../chromium/actions-value.js";import{SELECT_TEXT_VERB as g}from"../chromium/select-text.js";import{DIALOG_VERB as v,DRAG_VERB as y,UPLOAD_VERB as k}from"../chromium/actions.js";import{RUN_JS_CODE_DESC as S}from"../chromium/run-js.js";import{BrowserDriverVerbs as T}from"./driver-verbs.js";import{previewPage as O}from"../chromium/preview.js";import{withPageCapture as E}from"../chromium/shot.js";import{DOWNLOAD_VERB as I}from"./downloads.js";import{bridgeRequest as _,bridgeTabs as N,closeBridgeTab as R,holdOnlyBridgeTab as P,restoreBridgeTab as L}from"./bridge-tabs.js";import{pageTargetInfo as B}from"./tabs.js";const $=5e3,C=1500,D=/^(?:[a-z][a-z\d+.-]*:\/\/|(?:localhost|\d{1,3}(?:\.\d{1,3}){3}|\[[\da-f:]+\])(?::\d+)?(?:[/?#]|$)|[\w.-]+:\d+(?:[/?#]|$))/i;async function x(d){if(!d.url())return"";let e;try{return await Promise.race([d.title().catch(()=>""),new Promise(t=>{e=setTimeout(()=>t(""),C),e.unref?.()})])}finally{e&&clearTimeout(e)}}const c=/^tab:(\d+):(.*)$/;class K extends T{dormantSelection=null;borrowed=new Map;reported=null;holdUnsupported=!1;async activeTarget(){return this.dormantSelection?.id??super.activeTarget()}async ownershipResources(){return this.dormantSelection?[{key:`chromium:${this.dormantSelection.targetId}`,mode:"exclusive"}]:super.ownershipResources()}async prepareTarget(){const e=this.dormantSelection;if(!e)return;const{page:t,reloaded:s,viewport:r}=await L(this.browser,e.tabId,e.targetId,e.browserSession);if(this.tabIds.set(t,e.id),this.borrowed.set(t,e),this.active=t,this.reported=t,this.dormantSelection=null,this.resetObservationState(),!s)return;const o=await t.evaluate("({ width: innerWidth, height: innerHeight })");if(o.width<=0||o.height<=0){const a=await this.perform("viewport",r??{});if(!a.ok)throw new l(`The restored tab has no usable viewport: ${a.note??"resize failed"}`);this.appendNote("Chrome restored this background tab with a zero-size viewport; applied a temporary viewport, cleared when this driver disconnects.")}this.appendNote("Chrome had unloaded this tab; restored the same tab in the background. Read its current state before continuing.")}onBridge(){return!this.tab&&!this.attached&&!this.managedProfileDir&&this.browser?.version()==="mirasim-bridge"}async syncHeld(){if(this.holdUnsupported||!this.onBridge())return;const e=this.active&&!this.active.isClosed()?this.active:void 0;if(e!==this.reported){this.reported=e;for(const t of[...this.borrowed.keys()])t.isClosed()&&this.borrowed.delete(t);try{const t=e?(await B(e)).targetId:void 0,s=await P(this.browser,t);if(!e||!t||!s.held||s.tabId===void 0||!s.browserSession)return;const r=`tab:${s.tabId}:${s.browserSession}`;this.tabIds.has(e)||this.tabIds.set(e,r),this.borrowed.set(e,{id:this.tabIds.get(e),tabId:s.tabId,targetId:t,browserSession:s.browserSession})}catch(t){/wasn't found|not found|unknown method/i.test(String(t))&&(this.holdUnsupported=!0),this.reported=null}}}async releaseIdle(){await this.syncHeld();const e=this.active,t=e?this.borrowed.get(e):void 0;t&&(this.active=void 0,this.borrowed.clear(),await this.syncHeld(),this.dormantSelection=t,this.resetObservationState())}page(){if(this.dormantSelection)throw new l(`Tab ${this.dormantSelection.id} has not been restored; no other page was observed or operated.`);return super.page()}adoptWhenReal(e){this.dormantSelection||super.adoptWhenReal(e)}previewTarget(){return this.dormantSelection||!this.browser?.isConnected()||!this.active||this.active.isClosed()?Promise.resolve(null):O(this.active,this.cdpUrl,"browser")}platform="browser";async turnEnded(e){if(this.attached||e.keepBrowserTabs===!0)return;const t=await this.ownPages().catch(()=>[]);for(const s of t)await s.close().catch(()=>{});t.length&&(this.closedItsLastTab=!0,this.resetObservationState())}extraVerbs(){return[{kind:"open_url",schema:i.Object({kind:i.Literal("open_url"),url:i.String({description:"Full URL to open. Only URLs that appeared on a page or were given in the task \u2014 never guess domains."})})},{kind:"activate",schema:i.Object({kind:i.Literal("activate")},{description:"Bring this browser tab and its window to the foreground. This changes the user's foreground window: explain the effect and obtain permission unless it is already authorized. focus() only selects the target. Embedded writes normally wait for the person to yield the window; activate() is an explicit foreground handoff, revoked by subsequent human input. Do not use it to bypass an input wait without the user handing over control."})},{kind:"back",schema:i.Object({kind:i.Literal("back")})},{kind:"reload",schema:i.Object({kind:i.Literal("reload")})},{kind:"new_tab",schema:i.Object({kind:i.Literal("new_tab"),url:i.Optional(i.String({description:"URL to open in it; omit for a blank tab"}))},{description:"Open a NEW tab of this session's own and make it the target \u2014 the current tab keeps its state, and both are listed as targets afterwards."})},{kind:"close_tab",schema:i.Object({kind:i.Literal("close_tab"),id:i.Optional(i.String({description:"Tab id from computer.surfaces(); omit to close the one you are on"})),all:i.Optional(i.Boolean({description:"Close every tab this session opened or claimed \u2014 for a tidy-up the host's tab policy or the user asks for. Never the user's own, whatever else is set. Refused together with id."})),confirmed:i.Optional(i.Boolean({description:"Only for a tab marked (user's): you ASKED the user in the conversation and they said yes. Never set it because closing would be convenient \u2014 the first refusal tells you to ask, and asking is the point."}))},{description:"Close a tab: the tabs you open outlive your turn and nothing closes them for you. Leave them open unless the host states a tab policy that asks you to close them, or the user asks \u2014 the person may want to see the page, and a later turn may need it again. Never close a page that is waiting for the person (a sign-in, a security-key or 2FA prompt, a form for them to confirm). This session's own always; one of the user's (marked (user's) in the listing) only after you asked them in the conversation and they agreed \u2014 then pass confirmed: true."})},{kind:"viewport",schema:i.Object({kind:i.Literal("viewport"),width:i.Optional(i.Number({description:"Viewport width in CSS pixels (default 1200)"})),height:i.Optional(i.Number({description:"Viewport height in CSS pixels \u2014 this is the one that decides how much of a long list exists (default 1700)"})),reset:i.Optional(i.Boolean({description:"Put the tab back to the real window size"}))},{description:"Give THIS tab a viewport of your own size, so a virtualized list (a feed, a search page, a long transcript) mounts more of itself per read. Set it BEFORE opening the page: rows mount against the viewport in effect at load."})},{kind:"run_js",readOnly:!0,schema:i.Object({kind:i.Literal("run_js"),code:i.String({description:S})},{description:"Ask the PAGE something no observation carries \u2014 read its data in bulk, its stored state, or an API it exposes. This is a stranger's site holding the user's logged-in session: read and compute here, act through the interaction verbs."})},y,v,k,I,m,g,f]}async observeElements(e={}){const t=await this.noTabNote();return t?this.noTabObservation(t,e.prefix):super.observeElements(e)}async observeImage(e={}){const t=await this.noTabNote();return t?this.noTabObservation(t,e.prefix):super.observeImage(e)}async noTabNote(){return this.closedItsLastTab?b:this.dormantSelection?null:(await this.ensure(),this.active?null:p)}async heldNothing(){return this.closedItsLastTab?"closed":this.dormantSelection?null:(await this.ensure(),this.active?null:"not-yet")}noTabObservation(e,t){const s=this.takePendingNote();return u(`${t?`${t}
2
2
  `:""}${s?`NOTE: ${s}.
3
- `:""}${e}.`,{verdict:"unknown"})}staleRefCause(){return"it predates a navigation, a reload or a tab switch"}explainMiss(e){const t=e.trim();if(!P.test(t))return null;const s=JSON.stringify(/^[a-z][a-z\d+.-]*:\/\//i.test(t)?t:`http://${t}`);return`mirasim-cu: computer.surface() binds a tab that is already open, by its id or title \u2014 never by URL \u2014 and opens nothing. Bind a tab and go there: const w = await computer.surface('<id>'); await w.openUrl(${s}) \u2014 or await w.newTab(${s}) to leave that tab as it is.`}promptFragment(){return"Grounding: observations are text lists of interactive elements with refs, for the CURRENT viewport only. Act by ref from the latest observation. When what you want is NOT in the list: if you can name it, w.find('<name>') searches EVERY control on the page and hands back a handle \u2014 act on it directly, the click scrolls it into view, so do not scroll first. Scroll only to discover what you cannot name. Observations may show only changes since the previous one; refs stay valid across observations (until navigation). Use w.text() to read full page text."}async listTargets(){await this.ensure();const{unloaded:e,browserSession:t}=!this.tab&&!this.attached&&!this.managedProfileDir?await E(this.browser):{unloaded:[],browserSession:null};this.noteBridgeSession(t);const s=new Set(await this.ownPages()),o=await this.visiblePages(),a=await Promise.all(o.map(async r=>({id:this.tabId(r),kind:"tab",title:await L(r)||r.url(),active:!this.unloadedSelection&&r===this.active,own:s.has(r),mine:this.mine.has(r)})));for(const r of e){const n=`unloaded:${r.tabId}:${r.browserSession}`;a.push({id:n,kind:"tab",title:`${r.title||r.url} [unloaded \u2014 use this handle to restore in background]`,active:this.unloadedSelection?.id===n,own:!1,unloaded:!0})}return a}noteBridgeSession(e){const t=super.noteBridgeSession(e);return t&&(this.unloadedSelection=null),t}async actionResources(e,t){if(e!=="close_tab")return[];const s=typeof t.id=="string"?t.id.replace(/^browser\//,""):null,o=t.all?await this.ownPages():s?(await this.visiblePages()).filter(a=>this.tabId(a)===s):[this.page()];return Promise.all(o.filter(a=>!a.isClosed()).map(a=>this.pageOwnership(a)))}async focusTarget(e){await this.ensure();const t=/^unloaded:(\d+):(.*)$/.exec(e);if(t&&!this.tab&&!this.attached&&!this.managedProfileDir&&this.browser.version()==="mirasim-bridge"){const o=Number(t[1]),a=t[2],{targetId:r}=await O(this.browser,"Mirasim.tabTarget",{tabId:o,browserSession:a});return this.unloadedSelection={id:e,tabId:o,targetId:r,browserSession:a},this.closedItsLastTab=!1,{ok:!0,note:"Selected unloaded tab; restoration follows target ownership."}}let s={ok:!1,note:`no target with id ${e}; call computer.surfaces() for current ids`};for(const o of await this.visiblePages())if(!(o.isClosed()||this.tabId(o)!==e)){this.unloadedSelection=null,this.active=o,this.closedItsLastTab=!1,s={ok:!0};break}return this.journal?.publishAction("focus",{target:e},null,s),s}async close(){this.unloadedSelection=null,await this.downloads.close(),await this.journal?.drain(R);for(const e of this.overridden)e.isClosed()||await S(e,()=>this.cdpFor(e).then(t=>t.send("Emulation.clearDeviceMetricsOverride"))).catch(()=>{});if(this.overridden.clear(),!this.managedProfileDir&&!this.tab&&this.browser)for(const e of await this.ownPages().catch(()=>[]))e.url()==="about:blank"&&await e.close().catch(()=>{});await this.browser?.close().catch(()=>{}),this.browser=null,this.resetObservationState(),this.resetPageState(),await this.disconnected()}journalVerdict(e){this.journal?.publishVerdict(e)}renderHeadLines(e,t,s=!1,o){const a=super.renderHeadLines(e,t,s,o);if(e.url==="about:blank"&&e.elements.length===0&&!e.text){const r=t.some(n=>n.title!=="about:blank");a.push(r?"NOTE: this tab is blank \u2014 your page may be in another tab; computer.surfaces() lists it \u2014 act through its handle.":"NOTE: this tab is blank and nothing has been opened in it \u2014 navigate (openUrl({url})) instead of observing again.")}return a}async blankSurfaceHint(){try{return this.page().url()!=="about:blank"?"":(await this.ownPages()).some(t=>t!==this.page()&&t.url()!=="about:blank")?"the active tab is a blank page \u2014 your page may be in another tab; computer.surfaces() lists it \u2014 act through its handle":""}catch{return""}}}export{H as BrowserDriver};
3
+ `:""}${e}.`,{verdict:"unknown"})}staleRefCause(){return"it predates a navigation, a reload or a tab switch"}explainMiss(e){const t=e.trim();if(!D.test(t))return null;const s=JSON.stringify(/^[a-z][a-z\d+.-]*:\/\//i.test(t)?t:`http://${t}`);return`mirasim-cu: computer.surface() binds a tab that is already open, by its id or title \u2014 never by URL \u2014 and opens nothing. Bind a tab and go there: const w = await computer.surface('<id>'); await w.openUrl({url: ${s}}) \u2014 or await w.newTab({url: ${s}}) to leave that tab as it is.`}promptFragment(){return"Grounding: observations are text lists of interactive elements with refs, for the CURRENT viewport only. Act by ref from the latest observation. When what you want is NOT in the list: if you can name it, w.find('<name>') searches EVERY control on the page and hands back a handle \u2014 act on it directly, the click scrolls it into view, so do not scroll first. Scroll only to discover what you cannot name. Observations may show only changes since the previous one; refs stay valid across observations (until navigation). Use w.text() to read full page text."}async listTargets(){await this.ensure();const{dormant:e,browserSession:t}=!this.tab&&!this.attached&&!this.managedProfileDir?await N(this.browser):{dormant:[],browserSession:null};this.noteBridgeSession(t);const s=new Set(await this.ownPages()),r=await this.visiblePages(),o=await Promise.all(r.map(async a=>({id:this.tabId(a),kind:"tab",title:await x(a)||a.url(),active:!this.dormantSelection&&a===this.active,own:s.has(a),mine:this.mine.has(a)})));for(const a of e){const n=`tab:${a.tabId}:${a.browserSession}`,h=a.title||a.url;o.push({id:n,kind:"tab",title:a.unloaded?`${h} [unloaded \u2014 use this handle to restore in background]`:h,active:this.dormantSelection?.id===n,own:!1,...a.unloaded?{unloaded:!0}:{}})}return o}noteBridgeSession(e){const t=super.noteBridgeSession(e);return t&&(this.dormantSelection=null,this.borrowed.clear(),this.reported=null),t}async actionResources(e,t){if(e!=="close_tab")return[];const s=typeof t.id=="string"?t.id.replace(/^browser\//,""):null,r=t.all?await this.ownPages():s?(await this.visiblePages()).filter(o=>this.tabId(o)===s):[this.page()];return Promise.all(r.filter(o=>!o.isClosed()).map(o=>this.pageOwnership(o)))}async focusTarget(e){await this.ensure();const t=await this.selectTarget(e);return await this.syncHeld(),t}async perform(e,t){try{return await super.perform(e,t)}finally{await this.syncHeld()}}async selectTarget(e){let t={ok:!1,note:`no target with id ${e}; call computer.surfaces() for current ids`};for(const r of await this.visiblePages())if(!(r.isClosed()||this.tabId(r)!==e)){this.dormantSelection=null,this.active=r,this.closedItsLastTab=!1,t={ok:!0};break}const s=t.ok?null:c.exec(e);if(s&&!this.tab&&!this.attached&&!this.managedProfileDir&&this.browser.version()==="mirasim-bridge"){const r=Number(s[1]),o=s[2],{targetId:a}=await _(this.browser,"Mirasim.tabTarget",{tabId:r,browserSession:o});return this.dormantSelection={id:e,tabId:r,targetId:a,browserSession:o},this.active=void 0,this.closedItsLastTab=!1,{ok:!0}}return this.journal?.publishAction("focus",{target:e},null,t),t}async closeDormantTab(e,t){const s=c.exec(e);if(!s||!this.browser||this.browser.version()!=="mirasim-bridge")return null;if(!t)return{ok:!1,note:w(e)};this.dormantSelection?.id===e&&(this.dormantSelection=null);try{await R(this.browser,Number(s[1]),s[2])}catch(r){return{ok:!1,note:`could not close tab ${e}: ${r instanceof Error?r.message:String(r)}`}}return{ok:!0,note:`closed tab ${e} \u2014 the user's own, closed on the agreement you reported; the target is unchanged`}}async close(){this.dormantSelection=null,this.borrowed.clear(),this.reported=null,await this.downloads.close(),await this.journal?.drain($);for(const e of this.overridden)e.isClosed()||await E(e,()=>this.cdpFor(e).then(t=>t.send("Emulation.clearDeviceMetricsOverride"))).catch(()=>{});if(this.overridden.clear(),!this.managedProfileDir&&!this.tab&&this.browser)for(const e of await this.ownPages().catch(()=>[]))e.url()==="about:blank"&&await e.close().catch(()=>{});await this.browser?.close().catch(()=>{}),this.browser=null,this.resetObservationState(),this.resetPageState(),await this.disconnected()}journalVerdict(e){this.journal?.publishVerdict(e)}renderHeadLines(e,t,s=!1,r){const o=super.renderHeadLines(e,t,s,r);if(e.url==="about:blank"&&e.elements.length===0&&!e.text){const a=t.some(n=>n.title!=="about:blank");o.push(a?"NOTE: this tab is blank \u2014 your page may be in another tab; computer.surfaces() lists it \u2014 act through its handle.":"NOTE: this tab is blank and nothing has been opened in it \u2014 navigate (openUrl({url})) instead of observing again.")}return o}async blankSurfaceHint(){try{return this.page().url()!=="about:blank"?"":(await this.ownPages()).some(t=>t!==this.page()&&t.url()!=="about:blank")?"the active tab is a blank page \u2014 your page may be in another tab; computer.surfaces() lists it \u2014 act through its handle":""}catch{return""}}}export{K as BrowserDriver};
@@ -23,3 +23,4 @@ export declare const NO_TAB_OPEN_NOTE: string;
23
23
  export declare const NO_TAB_YET_NOTE: string;
24
24
  export declare const BROWSER_SWITCHED_NOTE: string;
25
25
  export declare const SURFACE_REIDENTIFIED_NOTE: string;
26
+ export declare function userTabCloseRefusal(id: string): string;
@@ -4,5 +4,5 @@ Fix one of:
4
4
  - point COMPUTER_USE_CHROME at a Chrome/Chromium binary
5
5
  - point COMPUTER_USE_CDP_URL at an already-running browser's CDP endpoint`}function s(e,t){return`mirasim-cu: cannot take over this Chrome \u2014 ${t}.
6
6
  This profile belongs to the user (${e}), so it will not be force-closed.
7
- Quit Chrome (\u2318Q) and retry.`}const r=()=>"its debugging port answers, but no usable page can be opened",i=()=>"its debugging port answers, but the handshake timed out (the browser may be frozen)",a=()=>"it is already running, but without a debugging port",h=()=>"mirasim-cu: could not open the agent's own window in the user's Chrome.";function d(e,t){return`mirasim-cu: cannot run the Chrome binary at ${e} (${t}). Point COMPUTER_USE_CHROME at a working Chrome/Chromium.`}function c(e){return`mirasim-cu: Chrome could not be started (${e}).`}const g=()=>"mirasim-cu: Chrome exited immediately instead of publishing a debugging port \u2014 another browser may already own this profile directory.",m=()=>"mirasim-cu: Chrome never published a debugging port within 15s.",b=()=>"mirasim-cu: Chrome started but its debugger refused to attach.",o="https://chromewebstore.google.com/detail/mirasim/khflooofmndpmafhofjdcpjfgnlpikhg";function l(){return`The mirasim bridge extension is not connected, so this browser cannot be reached. Nothing you can do from here fixes it and retrying will not help: ask the user to install or enable the Mirasim extension from the Chrome Web Store (${o}) and remove any older copy loaded unpacked at chrome://extensions, which no longer connects; then continue.`}function u(e){return`The Mirasim extension is connected to another program's browser bridge (${e}), not to this one \u2014 it serves one bridge at a time and that one comes first. Retrying will not help: ask the user to turn browser control off in that program (${e}), then continue.`}function f(){return`The browser bridge started moments ago and the Mirasim extension has not connected yet. An installed extension checks again within a minute; wait about a minute and retry once. If it still does not connect, ask the user to install or enable the Mirasim extension from the Chrome Web Store (${o}).`}function p(e){return/Bridge extension not connected|bridge extension did not connect/i.test(e instanceof Error?e.message:String(e))}function w(e,t){return`mirasim-cu: explicit CDP endpoint ${e} is unavailable.
8
- (${t})`}const x="the browser had been closed (most likely by the user), so a fresh one was opened in the background and the page is blank \u2014 re-observe, and navigate again before acting on refs",E="this surface's tab had been closed (most likely by the user), so a fresh blank tab was claimed \u2014 re-observe, and navigate again before acting on refs",C="this surface's tab stopped answering \u2014 it was closed, or the browser took its debugger away, in which case it is still open in the user's window but out of reach from here. A fresh blank tab was claimed \u2014 re-observe, and navigate again before acting on refs. If the same page drops again right after it loads, it cannot be driven through this browser: a password manager's autofill (measured: iCloud Passwords, on any page with a password field) puts its own extension frame into the page, and Chrome then refuses this extension's debugger there. Do not reopen it; tell the user \u2014 turning that extension off for the site, or the managed browser (its own profile), drives it";function y(e){const t=`this surface's tab is out of reach: it is still open in the user's window, but Chrome refused this extension's debugger on it ("${e}"). A fresh blank tab was claimed. `;return/chrome-extension:\/\/ URL of different extension/.test(e)?t+"Another extension has put its own frame into that page \u2014 a password manager's autofill does this on any page with a password field (measured: iCloud Passwords) \u2014 and while it is there no retry from here gets in. Do not reopen the page; tell the user that turning that extension off for this site, or the managed browser (its own profile), lets it be driven":t+"Reopening it will most likely be refused the same way \u2014 tell the user rather than retrying"}const _="this session had closed its last tab, so opening one started a fresh blank tab \u2014 nothing went wrong and nobody took the browser back; navigate before acting on refs",T="this session has no tab open \u2014 it closed its last one and nothing was reopened, so the browser is left as the user had it. open_url or new_tab starts a fresh tab when you need one",k="this session holds no tab yet \u2014 none is opened until asked. Bind one of the listed tabs to act in it, or new_tab opens one of the session's own",O="the browser behind the extension changed \u2014 a different browser, or this one after a restart, is connected now, and the tabs listed are ITS tabs. Nothing was done in it and this session holds no tab there; handles and refs from before belonged to the previous browser and are gone. If this is not the browser the task is about, stop and tell the user; otherwise bind one of the listed tabs, or new_tab opens one of the session's own",N="the connection to this browser dropped and came back, so this surface was re-identified \u2014 the page below is the one it is on now, and refs from before are dead; re-observe before acting";export{O as BROWSER_SWITCHED_NOTE,T as NO_TAB_OPEN_NOTE,k as NO_TAB_YET_NOTE,x as REOPENED_NOTE,N as SURFACE_REIDENTIFIED_NOTE,C as TAB_DETACHED_NOTE,_ as TAB_REOPENED_AFTER_SELF_CLOSE_NOTE,E as TAB_REOPENED_NOTE,p as isBridgeExtensionGone,s as msgAttachedDeadEnd,i as msgAttachedWhyHandshake,r as msgAttachedWhyNoPage,a as msgAttachedWhyNoPort,u as msgBridgeExtensionElsewhere,l as msgBridgeExtensionGone,f as msgBridgeJustStarted,d as msgChromeBinaryUnrunnable,g as msgChromeExitedImmediately,n as msgChromeMissing,m as msgChromeNoPort,b as msgChromeRefusedAttach,c as msgChromeStartFailed,w as msgEndpointUnavailable,h as msgOwnWindowFailed,y as tabRefusedNote};
7
+ Quit Chrome (\u2318Q) and retry.`}const r=()=>"its debugging port answers, but no usable page can be opened",i=()=>"its debugging port answers, but the handshake timed out (the browser may be frozen)",a=()=>"it is already running, but without a debugging port",h=()=>"mirasim-cu: could not open the agent's own window in the user's Chrome.";function d(e,t){return`mirasim-cu: cannot run the Chrome binary at ${e} (${t}). Point COMPUTER_USE_CHROME at a working Chrome/Chromium.`}function c(e){return`mirasim-cu: Chrome could not be started (${e}).`}const g=()=>"mirasim-cu: Chrome exited immediately instead of publishing a debugging port \u2014 another browser may already own this profile directory.",l=()=>"mirasim-cu: Chrome never published a debugging port within 15s.",u=()=>"mirasim-cu: Chrome started but its debugger refused to attach.",o="https://chromewebstore.google.com/detail/mirasim/khflooofmndpmafhofjdcpjfgnlpikhg";function m(){return`The mirasim bridge extension is not connected, so this browser cannot be reached. Nothing you can do from here fixes it and retrying will not help: ask the user to install or enable the Mirasim extension from the Chrome Web Store (${o}) and remove any older copy loaded unpacked at chrome://extensions, which no longer connects; then continue.`}function b(e){return`The Mirasim extension is connected to another program's browser bridge (${e}), not to this one \u2014 it serves one bridge at a time and that one comes first. Retrying will not help: ask the user to turn browser control off in that program (${e}), then continue.`}function f(){return`The browser bridge started moments ago and the Mirasim extension has not connected yet. An installed extension checks again within a minute; wait about a minute and retry once. If it still does not connect, ask the user to install or enable the Mirasim extension from the Chrome Web Store (${o}).`}function p(e){return/Bridge extension not connected|bridge extension did not connect/i.test(e instanceof Error?e.message:String(e))}function w(e,t){return`mirasim-cu: explicit CDP endpoint ${e} is unavailable.
8
+ (${t})`}const x="the browser had been closed (most likely by the user), so a fresh one was opened in the background and the page is blank \u2014 re-observe, and navigate again before acting on refs",E="this surface's tab had been closed (most likely by the user), so a fresh blank tab was claimed \u2014 re-observe, and navigate again before acting on refs",y="this surface's tab stopped answering \u2014 it was closed, or the browser took its debugger away, in which case it is still open in the user's window but out of reach from here. A fresh blank tab was claimed \u2014 re-observe, and navigate again before acting on refs. If the same page drops again right after it loads, it cannot be driven through this browser: a password manager's autofill (measured: iCloud Passwords, on any page with a password field) puts its own extension frame into the page, and Chrome then refuses this extension's debugger there. Do not reopen it; tell the user \u2014 turning that extension off for the site, or the managed browser (its own profile), drives it";function C(e){const t=`this surface's tab is out of reach: it is still open in the user's window, but Chrome refused this extension's debugger on it ("${e}"). A fresh blank tab was claimed. `;return/chrome-extension:\/\/ URL of different extension/.test(e)?t+"Another extension has put its own frame into that page \u2014 a password manager's autofill does this on any page with a password field (measured: iCloud Passwords) \u2014 and while it is there no retry from here gets in. Do not reopen the page; tell the user that turning that extension off for this site, or the managed browser (its own profile), lets it be driven":t+"Reopening it will most likely be refused the same way \u2014 tell the user rather than retrying"}const _="this session had closed its last tab, so opening one started a fresh blank tab \u2014 nothing went wrong and nobody took the browser back; navigate before acting on refs",T="this session has no tab open \u2014 it closed its last one and nothing was reopened, so the browser is left as the user had it. open_url or new_tab starts a fresh tab when you need one",k="this session holds no tab yet \u2014 none is opened until asked. Bind one of the listed tabs to act in it, or new_tab opens one of the session's own",O="the browser behind the extension changed \u2014 a different browser, or this one after a restart, is connected now, and the tabs listed are ITS tabs. Nothing was done in it and this session holds no tab there; handles and refs from before belonged to the previous browser and are gone. If this is not the browser the task is about, stop and tell the user; otherwise bind one of the listed tabs, or new_tab opens one of the session's own",N="the connection to this browser dropped and came back, so this surface was re-identified \u2014 the page below is the one it is on now, and refs from before are dead; re-observe before acting";function R(e){return`${e} is the user's own tab, not this session's. Ask them in the conversation whether to close it \u2014 say which page \u2014 and call close_tab again with confirmed: true if they agree. Reading it and acting in it needs no permission; only closing does`}export{O as BROWSER_SWITCHED_NOTE,T as NO_TAB_OPEN_NOTE,k as NO_TAB_YET_NOTE,x as REOPENED_NOTE,N as SURFACE_REIDENTIFIED_NOTE,y as TAB_DETACHED_NOTE,_ as TAB_REOPENED_AFTER_SELF_CLOSE_NOTE,E as TAB_REOPENED_NOTE,p as isBridgeExtensionGone,s as msgAttachedDeadEnd,i as msgAttachedWhyHandshake,r as msgAttachedWhyNoPage,a as msgAttachedWhyNoPort,b as msgBridgeExtensionElsewhere,m as msgBridgeExtensionGone,f as msgBridgeJustStarted,d as msgChromeBinaryUnrunnable,g as msgChromeExitedImmediately,n as msgChromeMissing,l as msgChromeNoPort,u as msgChromeRefusedAttach,c as msgChromeStartFailed,w as msgEndpointUnavailable,h as msgOwnWindowFailed,C as tabRefusedNote,R as userTabCloseRefusal};
@@ -1,5 +1,5 @@
1
- import{SCROLL_JUMP_JS as R,SCROLL_MEASURE_JS as N,SCROLL_TARGET_JS as V}from"./scroll-scripts.js";import{pageFn as M}from"./collect.js";import{readableFrames as j}from"./frames.js";import{refSelector as L}from"./page-refs.js";import{coverOf as D}from"./coverer-label.js";const C=350,A=1500,F=20,x=8,J=n=>`did not scroll \u2014 ${k(n)} is already at the end, or is not scrollable. Do not just repeat it: try ref-targeted scroll / to:'top'|'bottom' / a different container \u2014 or, if this is a list that loads more on reaching its end, wait({}) once and scroll again`,S=n=>new Promise(e=>setTimeout(e,n)),p=n=>({res:{ok:!1,note:n},point:null,moved:null}),k=n=>n.kind==="doc"?"the page":`\xAB${n.name}\xBB`;async function q(n,e,c,u=!1){const a=e.to==="top"||e.to==="bottom"?e.to:void 0,i=typeof e.direction=="string"?e.direction:"down",w=Math.min(Math.max(Math.trunc(Number(e.count))||1,1),F);if(a&&Number(e.count)>1)return p(`scroll takes to:'${a}' or count, not both \u2014 to:'${a}' already goes as far as the container scrolls; drop one`);const l=!a&&(i==="left"||i==="right"),d=a?a==="top":i==="up"||i==="left";let r=n.mainFrame(),f=null;if(typeof e.ref=="string"&&e.ref)if(e.ref==="window")f="window";else{f=String(e.ref);const o=await L(n,f);let h=null;for(const $ of j(n))try{if(await $.locator(o).count()){h=$;break}}catch{}if(!h)return p(`no element for ref ${e.ref} on the page any more \u2014 re-observe and use a current ref`);r=h}let t;try{t=await r.evaluate(M(V),[l,f])}catch(o){return p(`scroll probe failed: ${(o instanceof Error?o.message:String(o)).split(`
2
- `)[0]}`)}if(t.kind==="ambiguous"){const o=(t.candidates??[]).map(h=>`${h.ref??"(observe for a ref)"}: ${h.name}${h.position?` (${h.position})`:""}`).join("; ");return p(`multiple scrollable regions \u2014 nothing scrolled. Pass scroll({ref:'\u2026', ...}) using a region or a control inside it: ${o}`)}if(t.kind==="none")return t.reason==="modal"?p("an open modal dialog blocks scrolling the background \u2014 choose a scrollable region inside the dialog; nothing scrolled"):p(t.reason==="gone"?`no element for ref ${e.ref} on the page any more \u2014 re-observe and use a current ref`:`ref ${e.ref} has no scrollable ancestor \u2014 that part of the page does not scroll; pick a ref inside the list you want to scroll, or scroll without ref`);let s=0,b=0;if(r.parentFrame()){const o=await(await r.frameElement()).boundingBox().catch(()=>null);o&&(s=o.x,b=o.y)}const m={x:t.x+s,y:t.y+b},v=t.kind==="doc";if(a){let o;try{o=await r.evaluate(M(R),[a==="top",v])}catch(E){return p(`scroll jump failed: ${(E instanceof Error?E.message:String(E)).split(`
3
- `)[0]}`)}if(o.lost)return _(t,m);await S(C);const h=await T(r,l,v);if(!h)return _(t,m);const $=Math.abs(h.pos-t.before);return $<x?{res:{ok:!0,note:`did not scroll \u2014 ${k(t)} is already at the ${a}; do not repeat`,journal:{moved:0,container:t.name}},point:m,moved:$}:{res:{ok:!0,note:`scrolled to the ${a} of ${k(t)} (moved ${Math.round($)}px)`+O(r.url(),t,h,$,c,u,a==="top"),journal:{moved:Math.round($),container:t.name}},point:m,moved:$}}try{await n.mouse.move(m.x,m.y);const o=Math.max(40,Math.round(t.client*.85))*w;await n.mouse.wheel(l?d?-o:o:0,l?0:d?-o:o)}catch(o){return p(`wheel failed: ${(o instanceof Error?o.message:String(o)).split(`
4
- `)[0]}`)}await S(C);let y=await T(r,l,v);for(const o=Date.now()+A;y&&Math.abs(y.pos-t.before)<x&&Date.now()<o;)await S(100),y=await T(r,l,v);if(!y)return _(t,m);const g=Math.abs(y.pos-t.before);return g<x?{res:{ok:!0,note:J(t),journal:{moved:Math.round(g),container:t.name}},point:m,moved:g}:{res:{ok:!0,note:`scrolled ${i} ${Math.round(g)}px in ${k(t)}`+(w>1?` (${w} screens asked for)`:"")+O(r.url(),t,y,g,c,u,d),journal:{moved:Math.round(g),container:t.name}},point:m,moved:g}}async function T(n,e,c){try{const u=await n.evaluate(M(N),[e,c]);return u.lost?null:u}catch{return null}}function _(n,e){return{res:{ok:!0,note:"the scrolled container was re-rendered; effect could not be measured \u2014 check the next observation before scrolling again",journal:{container:n.name,containerLost:!0}},point:e,moved:null}}function O(n,e,c,u,a,i,w){const l=Math.max(1,c.client||e.client),d=Math.max(1,Math.ceil(c.total/l));if(d<=1)return"";const r=c.pos+l>=c.total-8,f=r?d:Math.max(1,Math.min(d,Math.floor(c.pos/l)+1)),t=`${n.split("#")[0]}::${e.id}`,s=a.get(t)??{maxScreen:0,lastTotal:0,scrollCount:0,growCount:0},b=[],m=Math.max(1,u),v=Math.max(0,Math.ceil(Math.max(0,c.total-c.pos-l)/m)),y=r?" (reached the end)":w?"":` (~${v} more scrolls to the end)`;if(b.push(`screen ${f}/${d} of this container${y}`),s.lastTotal&&d>s.lastTotal+1){s.growCount++;let g=`note: this list grows as you scroll (was ${s.lastTotal} screens, now ${d})`;s.growCount>=2&&(g+="; scrolling to the end may be impossible \u2014 consider reporting what you have"),b.push(g)}return s.maxScreen>=f+2&&b.push(`you already viewed up to screen ${s.maxScreen} of this container`),s.scrollCount++,s.scrollCount>=5&&!i&&b.push(`this is scroll #${s.scrollCount} in this container \u2014 if what you need has not appeared, try to:'top'|'bottom', the page's own search/filter, or report what you have`),s.maxScreen=Math.max(s.maxScreen,f),s.lastTotal=d,a.set(t,s),`; ${b.join("; ")}`}async function z(n,e){const c=await L(n,e);let u=null;for(const r of j(n))try{const f=r.locator(c);if(await f.count()){u=f.first();break}}catch{}if(!u)return p(`no element for ref ${e} on the page any more \u2014 re-observe and use a current ref`);let a;try{a=await u.evaluate(M(("((n=>{const e=n.getBoundingClientRect().top;n.scrollIntoView({block:\"center\",inline:\"nearest\",behavior:\"instant\"});const t=n.getBoundingClientRect(),i=n.ownerDocument.defaultView;return{moved:Math.abs(t.top-e),inView:t.width>0&&t.height>0&&t.bottom>0&&t.top<i.innerHeight}}))")),void 0,{timeout:2e3})}catch(r){return p(`scroll into ${e} failed: ${(r instanceof Error?r.message:String(r)).split(`
5
- `)[0]}`)}if(!a.inView)return p(`${e} is not rendered (no box on screen after scrolling to it) \u2014 re-observe`);await S(C);const i=await D(u),w=Math.round(a.moved),l=w>=x?`scrolled ${e} to the middle of the viewport (moved ${w}px)`:`${e} is already as near the middle as the page scrolls`;return{res:{ok:!0,note:i?i.pseudo?`${l}, but the ${i.pseudo} layer of \xAB${i.who}\xBB still covers it: page styling, nothing to close or scroll past \u2014 the keyboard still reaches it (Tab to it, then Enter or Space)`:`${l}, but \xAB${i.who}\xBB still covers it: dismiss that (find('close') reaches its close control when the page marks one; an unmarked one shows only in a screenshot)`:`${l}; nothing covers it now \u2014 act on it`,journal:{moved:w,into:e,...i?{covered:i.who}:{}}},point:null,moved:w}}export{q as performScroll,z as performScrollInto};
1
+ import{SCROLL_JUMP_JS as R,SCROLL_MEASURE_JS as N,SCROLL_TARGET_JS as V}from"./scroll-scripts.js";import{pageFn as M}from"./collect.js";import{readableFrames as j}from"./frames.js";import{refSelector as L}from"./page-refs.js";import{coverOf as D}from"./coverer-label.js";const C=350,A=1500,F=20,x=8,J=n=>`did not scroll \u2014 ${k(n)} is already at the end, or is not scrollable. Do not just repeat it: try ref-targeted scroll / to:'top'|'bottom' / a different container \u2014 or, if this is a list that loads more on reaching its end, wait({}) once and scroll again`,S=n=>new Promise(e=>setTimeout(e,n)),p=n=>({res:{ok:!1,note:n},point:null,moved:null}),k=n=>n.kind==="doc"?"the page":`\xAB${n.name}\xBB`;async function q(n,e,c,h=!1){const a=e.to==="top"||e.to==="bottom"?e.to:void 0,i=typeof e.direction=="string"?e.direction:"down",w=Math.min(Math.max(Math.trunc(Number(e.count))||1,1),F);if(a&&Number(e.count)>1)return p(`scroll takes to:'${a}' or count, not both \u2014 to:'${a}' already goes as far as the container scrolls; drop one`);const l=!a&&(i==="left"||i==="right"),d=a?a==="top":i==="up"||i==="left";let r=n.mainFrame(),f=null;if(typeof e.ref=="string"&&e.ref)if(e.ref==="window")f="window";else{f=String(e.ref);const o=await L(n,f);let u=null;for(const $ of j(n))try{if(await $.locator(o).count()){u=$;break}}catch{}if(!u)return p(`no element for ref ${e.ref} on the page any more \u2014 re-observe and use a current ref`);r=u}let t;try{t=await r.evaluate(M(V),[l,f])}catch(o){return p(`scroll probe failed: ${(o instanceof Error?o.message:String(o)).split(`
2
+ `)[0]}`)}if(t.kind==="ambiguous"){const o=(t.candidates??[]).map(u=>u.ref==="window"?"ref:'window' (the page itself)":`${u.ref?`ref:'${u.ref}'`:"(observe for its ref)"} ${u.name}${u.position?` (${u.position})`:""}`).join("; ");return p(`multiple scrollable regions \u2014 nothing scrolled. Pass scroll({ref, ...}) with one of them, or a control inside it: ${o}`)}if(t.kind==="none")return t.reason==="modal"?p("an open modal dialog blocks scrolling the background \u2014 choose a scrollable region inside the dialog; nothing scrolled"):p(t.reason==="gone"?`no element for ref ${e.ref} on the page any more \u2014 re-observe and use a current ref`:`ref ${e.ref} has no scrollable ancestor \u2014 that part of the page does not scroll; pick a ref inside the list you want to scroll, or scroll without ref`);let s=0,b=0;if(r.parentFrame()){const o=await(await r.frameElement()).boundingBox().catch(()=>null);o&&(s=o.x,b=o.y)}const m={x:t.x+s,y:t.y+b},v=t.kind==="doc";if(a){let o;try{o=await r.evaluate(M(R),[a==="top",v])}catch(E){return p(`scroll jump failed: ${(E instanceof Error?E.message:String(E)).split(`
3
+ `)[0]}`)}if(o.lost)return _(t,m);await S(C);const u=await T(r,l,v);if(!u)return _(t,m);const $=Math.abs(u.pos-t.before);return $<x?{res:{ok:!0,note:`did not scroll \u2014 ${k(t)} is already at the ${a}; do not repeat`,journal:{moved:0,container:t.name}},point:m,moved:$}:{res:{ok:!0,note:`scrolled to the ${a} of ${k(t)} (moved ${Math.round($)}px)`+O(r.url(),t,u,$,c,h,a==="top"),journal:{moved:Math.round($),container:t.name}},point:m,moved:$}}try{await n.mouse.move(m.x,m.y);const o=Math.max(40,Math.round(t.client*.85))*w;await n.mouse.wheel(l?d?-o:o:0,l?0:d?-o:o)}catch(o){return p(`wheel failed: ${(o instanceof Error?o.message:String(o)).split(`
4
+ `)[0]}`)}await S(C);let y=await T(r,l,v);for(const o=Date.now()+A;y&&Math.abs(y.pos-t.before)<x&&Date.now()<o;)await S(100),y=await T(r,l,v);if(!y)return _(t,m);const g=Math.abs(y.pos-t.before);return g<x?{res:{ok:!0,note:J(t),journal:{moved:Math.round(g),container:t.name}},point:m,moved:g}:{res:{ok:!0,note:`scrolled ${i} ${Math.round(g)}px in ${k(t)}`+(w>1?` (${w} screens asked for)`:"")+O(r.url(),t,y,g,c,h,d),journal:{moved:Math.round(g),container:t.name}},point:m,moved:g}}async function T(n,e,c){try{const h=await n.evaluate(M(N),[e,c]);return h.lost?null:h}catch{return null}}function _(n,e){return{res:{ok:!0,note:"the scrolled container was re-rendered; effect could not be measured \u2014 check the next observation before scrolling again",journal:{container:n.name,containerLost:!0}},point:e,moved:null}}function O(n,e,c,h,a,i,w){const l=Math.max(1,c.client||e.client),d=Math.max(1,Math.ceil(c.total/l));if(d<=1)return"";const r=c.pos+l>=c.total-8,f=r?d:Math.max(1,Math.min(d,Math.floor(c.pos/l)+1)),t=`${n.split("#")[0]}::${e.id}`,s=a.get(t)??{maxScreen:0,lastTotal:0,scrollCount:0,growCount:0},b=[],m=Math.max(1,h),v=Math.max(0,Math.ceil(Math.max(0,c.total-c.pos-l)/m)),y=r?" (reached the end)":w?"":` (~${v} more scrolls to the end)`;if(b.push(`screen ${f}/${d} of this container${y}`),s.lastTotal&&d>s.lastTotal+1){s.growCount++;let g=`note: this list grows as you scroll (was ${s.lastTotal} screens, now ${d})`;s.growCount>=2&&(g+="; scrolling to the end may be impossible \u2014 consider reporting what you have"),b.push(g)}return s.maxScreen>=f+2&&b.push(`you already viewed up to screen ${s.maxScreen} of this container`),s.scrollCount++,s.scrollCount>=5&&!i&&b.push(`this is scroll #${s.scrollCount} in this container \u2014 if what you need has not appeared, try to:'top'|'bottom', the page's own search/filter, or report what you have`),s.maxScreen=Math.max(s.maxScreen,f),s.lastTotal=d,a.set(t,s),`; ${b.join("; ")}`}async function z(n,e){const c=await L(n,e);let h=null;for(const r of j(n))try{const f=r.locator(c);if(await f.count()){h=f.first();break}}catch{}if(!h)return p(`no element for ref ${e} on the page any more \u2014 re-observe and use a current ref`);let a;try{a=await h.evaluate(M(("((n=>{const e=n.getBoundingClientRect().top;n.scrollIntoView({block:\"center\",inline:\"nearest\",behavior:\"instant\"});const t=n.getBoundingClientRect(),i=n.ownerDocument.defaultView;return{moved:Math.abs(t.top-e),inView:t.width>0&&t.height>0&&t.bottom>0&&t.top<i.innerHeight}}))")),void 0,{timeout:2e3})}catch(r){return p(`scroll into ${e} failed: ${(r instanceof Error?r.message:String(r)).split(`
5
+ `)[0]}`)}if(!a.inView)return p(`${e} is not rendered (no box on screen after scrolling to it) \u2014 re-observe`);await S(C);const i=await D(h),w=Math.round(a.moved),l=w>=x?`scrolled ${e} to the middle of the viewport (moved ${w}px)`:`${e} is already as near the middle as the page scrolls`;return{res:{ok:!0,note:i?i.pseudo?`${l}, but the ${i.pseudo} layer of \xAB${i.who}\xBB still covers it: page styling, nothing to close or scroll past \u2014 the keyboard still reaches it (Tab to it, then Enter or Space)`:`${l}, but \xAB${i.who}\xBB still covers it: dismiss that (find('close') reaches its close control when the page marks one; an unmarked one shows only in a screenshot)`:`${l}; nothing covers it now \u2014 act on it`,journal:{moved:w,into:e,...i?{covered:i.who}:{}}},point:null,moved:w}}export{q as performScroll,z as performScrollInto};
@@ -21,6 +21,7 @@ export declare abstract class ComputerUseDriverBase extends ComputerUseDriverRen
21
21
  actionResources(_kind: string, _args: Record<string, unknown>): Promise<OwnershipResource[]>;
22
22
  invalidateObservation(): void;
23
23
  releaseInputResources(): Promise<void>;
24
+ releaseIdle(): Promise<void>;
24
25
  extraVerbs(): VerbSpec[];
25
26
  abstract promptFragment(): string;
26
27
  interactionNotes(): readonly string[];
@@ -1 +1 @@
1
- import{text as o}from"./types.js";import{CHROMIUM_DIAGNOSTICS as c,DiagnosticsBuffer as l}from"./driver-diagnostics.js";import{ComputerUseDriverRender as u}from"./driver-render.js";import"./driver-settle.js";const a=15e3;class m extends u{interactionMode="background";screenshotScoped(t){return Promise.reject(new Error(`scoped screenshots are not supported on ${this.platform}`))}onExecutorStart;disconnectListeners=new Set;onDisconnect(t){this.disconnectListeners.add(t)}async disconnected(){for(const t of this.disconnectListeners)await t()}async ownershipResources(){const t=await this.activeTarget();if(!t)throw new Error("No active target to claim; list and bind a surface first.");return[{key:`${this.platform}:${t}`,mode:"exclusive"}]}async prepareTarget(){}async actionResources(t,e){return[]}invalidateObservation(){this.resetObservationState()}async releaseInputResources(){}extraVerbs(){return[]}interactionNotes(){return[]}screenshotMimeType="image/jpeg";async captureSnapshot(){return{snapshot:await this.snapshot()}}async bounded(t,e){let n;try{return await Promise.race([e,new Promise((s,r)=>{n=setTimeout(()=>r(new Error(`${t} timed out after ${a/1e3}s \u2014 the result is unknown. Re-observe before continuing; do not replay a preceding mutation.`)),a),n.unref()})])}finally{clearTimeout(n)}}prevSig=null;prevElSig=null;prevElSigSubstantive=null;lastActionSig=null;lastActionRef=null;prevActionSig=null;actionsInRound=0;pendingSurfaceChange=!1;lastScrollMoved=null;writeRound=null;lastRects=null;lastScrollY=null;noteAction(t,e){if(t==="wait"){this.observesSinceAction=0;return}this.prevActionSig=this.lastActionSig,this.lastActionRef=typeof e.ref=="string"?e.ref:null;const n=["ref","name","x","y","text","chord","direction","to","url","option","target","ms","package"].map(r=>`${e[r]??""}`).join("|"),s=Array.isArray(e.then)?e.then.map(r=>{const i=r;return`${i.do??""}:${i.ref??""}=${i.text??i.option??i.chord??""}`}).join(","):"";this.lastActionSig=`${t}|${n}|${s}`,this.actionsInRound++,this.observesSinceAction=0,t!=="wait"&&this.diag?.markAction()}noteWriteRound(t){this.writeRound=t}resetObservationState(){this.prevSig=null,this.prevElSig=null,this.prevElSigSubstantive=null,this.forgetRefs(),this.lastRects=null,this.lastScrollY=null,this.pendingSurfaceChange=!1,this.resetRenderState()}valueMatches(t,e){return t===e}supportsDiagnostics=!1;readsOffscreen=!0;diag=null;enableDiagnostics(){this.supportsDiagnostics&&!this.diag&&(this.diag=new l(this.diagnosticsAbout))}get diagnosticsAbout(){return c}get diagnosticsSink(){return this.diag}async currentUrl(){return""}async readDiagnostics(t={}){return this.diag?this.diag.render(t.id,await this.currentUrl().catch(()=>"")):o("diagnostics are not enabled for this session",{error:"diagnostics_off"})}trimSurvivors(){return[]}takePendingNote(){return null}takeNoteForResult(){return this.takePendingNote()}takeSurfaceChange(){const t=this.pendingSurfaceChange;return this.pendingSurfaceChange=!1,t}async activeTarget(){return(await this.listTargets()).find(t=>t.active)?.id??null}async isActiveTarget(t){return await this.activeTarget()===t}targetsNote(){return null}}export{m as ComputerUseDriverBase};
1
+ import{text as o}from"./types.js";import{CHROMIUM_DIAGNOSTICS as c,DiagnosticsBuffer as l}from"./driver-diagnostics.js";import{ComputerUseDriverRender as u}from"./driver-render.js";import"./driver-settle.js";const a=15e3;class m extends u{interactionMode="background";screenshotScoped(t){return Promise.reject(new Error(`scoped screenshots are not supported on ${this.platform}`))}onExecutorStart;disconnectListeners=new Set;onDisconnect(t){this.disconnectListeners.add(t)}async disconnected(){for(const t of this.disconnectListeners)await t()}async ownershipResources(){const t=await this.activeTarget();if(!t)throw new Error("No active target to claim; list and bind a surface first.");return[{key:`${this.platform}:${t}`,mode:"exclusive"}]}async prepareTarget(){}async actionResources(t,e){return[]}invalidateObservation(){this.resetObservationState()}async releaseInputResources(){}async releaseIdle(){}extraVerbs(){return[]}interactionNotes(){return[]}screenshotMimeType="image/jpeg";async captureSnapshot(){return{snapshot:await this.snapshot()}}async bounded(t,e){let n;try{return await Promise.race([e,new Promise((s,r)=>{n=setTimeout(()=>r(new Error(`${t} timed out after ${a/1e3}s \u2014 the result is unknown. Re-observe before continuing; do not replay a preceding mutation.`)),a),n.unref()})])}finally{clearTimeout(n)}}prevSig=null;prevElSig=null;prevElSigSubstantive=null;lastActionSig=null;lastActionRef=null;prevActionSig=null;actionsInRound=0;pendingSurfaceChange=!1;lastScrollMoved=null;writeRound=null;lastRects=null;lastScrollY=null;noteAction(t,e){if(t==="wait"){this.observesSinceAction=0;return}this.prevActionSig=this.lastActionSig,this.lastActionRef=typeof e.ref=="string"?e.ref:null;const n=["ref","name","x","y","text","chord","direction","to","url","option","target","ms","package"].map(r=>`${e[r]??""}`).join("|"),s=Array.isArray(e.then)?e.then.map(r=>{const i=r;return`${i.do??""}:${i.ref??""}=${i.text??i.option??i.chord??""}`}).join(","):"";this.lastActionSig=`${t}|${n}|${s}`,this.actionsInRound++,this.observesSinceAction=0,t!=="wait"&&this.diag?.markAction()}noteWriteRound(t){this.writeRound=t}resetObservationState(){this.prevSig=null,this.prevElSig=null,this.prevElSigSubstantive=null,this.forgetRefs(),this.lastRects=null,this.lastScrollY=null,this.pendingSurfaceChange=!1,this.resetRenderState()}valueMatches(t,e){return t===e}supportsDiagnostics=!1;readsOffscreen=!0;diag=null;enableDiagnostics(){this.supportsDiagnostics&&!this.diag&&(this.diag=new l(this.diagnosticsAbout))}get diagnosticsAbout(){return c}get diagnosticsSink(){return this.diag}async currentUrl(){return""}async readDiagnostics(t={}){return this.diag?this.diag.render(t.id,await this.currentUrl().catch(()=>"")):o("diagnostics are not enabled for this session",{error:"diagnostics_off"})}trimSurvivors(){return[]}takePendingNote(){return null}takeNoteForResult(){return this.takePendingNote()}takeSurfaceChange(){const t=this.pendingSurfaceChange;return this.pendingSurfaceChange=!1,t}async activeTarget(){return(await this.listTargets()).find(t=>t.active)?.id??null}async isActiveTarget(t){return await this.activeTarget()===t}targetsNote(){return null}}export{m as ComputerUseDriverBase};
@@ -25,6 +25,7 @@ export declare abstract class ComputerUseDriver extends ComputerUseDriverWaits {
25
25
  protected frozenExtra(): string | null;
26
26
  explainMiss(_query: string): string | null;
27
27
  unlistedTarget(_query: string): Promise<TargetInfo | null>;
28
+ heldNothing(): Promise<"closed" | "not-yet" | null>;
28
29
  waitedOnUserMs: number;
29
30
  endCall(): void;
30
31
  protected treeBlind(): boolean;
@@ -1,6 +1,6 @@
1
- import{SurfaceGoneError as k,SurfaceUnreadableError as C,text as m}from"./types.js";import"./driver-narrate.js";import{OBSERVE_FROZEN_NOTE as z}from"./driver-lines.js";import{disclosureClosed as L,effectAlreadyShown as F,elementSignature as N,onlyRefsLost as B,settleAfterStateAction as G,STATE_CHANGING_ACTIONS as I,substantiveElementSignature as v}from"./driver-settle.js";import{ComputerUseDriverWaits as U}from"./driver-waits.js";import{STATE_CHANGING_ACTIONS as J}from"./driver-settle.js";const Y=new Set(["textbox","searchbox"]);class W extends U{surfaceIdPrefix="";onPreview;previewTarget(){return Promise.resolve(null)}publishPreview(t,r={}){if(!this.onPreview||this.permissionRefusal)return;const c=Date.now(),e=typeof r.ref=="string"?this.lastRects?.get(r.ref):void 0,o=typeof r.x=="number"?r.x:e?e.x+e.w/2:void 0,i=typeof r.y=="number"?r.y:e?e.y+e.h/2:void 0;this.previewTarget().then(n=>{n&&!this.permissionRefusal&&this.onPreview?.({...n,...t&&o!==void 0&&i!==void 0?{pointer:{x:o,y:i,kind:t,ts:c}}:{}},c)}).catch(()=>{})}consentPurpose=null;permissionRefusal=null;async turnEnded(t){}async ensureSettleBaseline(t){if(!(!I.has(t)||this.prevElSigSubstantive!==null))try{const{snapshot:r}=await this.bounded("observe",this.captureSnapshot());this.prevElSigSubstantive=v(r.elements)}catch{}}async observeElements(t={}){let r;try{r=await this.bounded("observe",this.captureSnapshot())}catch(s){if(this.prevSig=null,this.takeSurfaceChange(),this.forgetDeltaBase(),this.noteObserveFailure(s.message),s instanceof k||s instanceof C)throw s;const h=`(observe failed: ${s.message})`,f=this.takePendingNote(),g=[t.prefix,f?`NOTE: ${f}.`:"",h].filter(Boolean).join(`
1
+ import{SurfaceGoneError as N,SurfaceUnreadableError as k,text as m}from"./types.js";import"./driver-narrate.js";import{OBSERVE_FROZEN_NOTE as z}from"./driver-lines.js";import{disclosureClosed as L,effectAlreadyShown as F,elementSignature as C,onlyRefsLost as B,settleAfterStateAction as G,STATE_CHANGING_ACTIONS as I,substantiveElementSignature as v}from"./driver-settle.js";import{ComputerUseDriverWaits as U}from"./driver-waits.js";import{STATE_CHANGING_ACTIONS as J}from"./driver-settle.js";const Y=new Set(["textbox","searchbox"]);class W extends U{surfaceIdPrefix="";onPreview;previewTarget(){return Promise.resolve(null)}publishPreview(t,r={}){if(!this.onPreview||this.permissionRefusal)return;const c=Date.now(),e=typeof r.ref=="string"?this.lastRects?.get(r.ref):void 0,o=typeof r.x=="number"?r.x:e?e.x+e.w/2:void 0,i=typeof r.y=="number"?r.y:e?e.y+e.h/2:void 0;this.previewTarget().then(n=>{n&&!this.permissionRefusal&&this.onPreview?.({...n,...t&&o!==void 0&&i!==void 0?{pointer:{x:o,y:i,kind:t,ts:c}}:{}},c)}).catch(()=>{})}consentPurpose=null;permissionRefusal=null;async turnEnded(t){}async ensureSettleBaseline(t){if(!(!I.has(t)||this.prevElSigSubstantive!==null))try{const{snapshot:r}=await this.bounded("observe",this.captureSnapshot());this.prevElSigSubstantive=v(r.elements)}catch{}}async observeElements(t={}){let r;try{r=await this.bounded("observe",this.captureSnapshot())}catch(s){if(this.prevSig=null,this.takeSurfaceChange(),this.forgetDeltaBase(),this.noteObserveFailure(s.message),s instanceof N||s instanceof k)throw s;const h=`(observe failed: ${s.message})`,f=this.takePendingNote(),g=[t.prefix,f?`NOTE: ${f}.`:"",h].filter(Boolean).join(`
2
2
  `),u=g.split(`
3
- `).length;return m(g,{verdict:"unknown",actionLines:u,headLines:u})}const c=t.afterAction==="click"&&this.lastActionRef!==null?r.snapshot.elements.find(s=>s.ref===this.lastActionRef):void 0;if(I.has(t.afterAction??"")&&!(c&&Y.has(c.role))){const s=this.lastRects?new Set(this.lastRects.keys()):null,h=this.prevElSig,f=this.lastScrollY,g=t.afterAction??"";r=await G({capture:r,previous:this.prevElSigSubstantive,signature:u=>v(u.snapshot.elements),unsettled:(u,p)=>{const d=p.snapshot.elements,$=f!==null&&Math.abs(p.snapshot.meta.scrollY-f)>=3;return u===v(d)?!F({action:g,target:d.find(b=>b.ref===this.lastActionRef)??d.find(b=>b.state.startsWith("focused")),scrolled:$,focusMoved:h!==null&&h!==N(d),geometryMoved:this.geometryChanges(p.snapshot)!==null}):$||s===null||!B(s,d)?!1:this.geometryChanges(p.snapshot)===null||L(h,d)},recapture:()=>this.bounded("observe",this.captureSnapshot())})}const e=r.snapshot;this.publishPreview();const o=this.signature(e),i=this.takeSurfaceChange(),n=i?"changed":this.prevSig===null?null:this.prevSig===o?"frozen":"changed";this.prevSig=o;const l=N(e.elements),S=this.prevElSig!==null&&this.prevElSig===l;this.prevElSig=l,this.prevElSigSubstantive=v(e.elements),this.lastGeomChange=this.geometryChanges(e),this.lastRects=new Map(e.elements.map(s=>[s.ref,s.rect])),this.lastScrollY=e.meta.scrollY,this.rememberRefs(e),n==="frozen"&&this.journalVerdict?.("frozen");const M=this.takeActionsInRound(),y=!t.afterAction&&this.takeContentRead();let a=t.prefix??"";const w=this.takePendingNote();if(w&&(a+=(a?`
3
+ `).length;return m(g,{verdict:"unknown",actionLines:u,headLines:u})}const c=t.afterAction==="click"&&this.lastActionRef!==null?r.snapshot.elements.find(s=>s.ref===this.lastActionRef):void 0;if(I.has(t.afterAction??"")&&!(c&&Y.has(c.role))){const s=this.lastRects?new Set(this.lastRects.keys()):null,h=this.prevElSig,f=this.lastScrollY,g=t.afterAction??"";r=await G({capture:r,previous:this.prevElSigSubstantive,signature:u=>v(u.snapshot.elements),unsettled:(u,p)=>{const d=p.snapshot.elements,$=f!==null&&Math.abs(p.snapshot.meta.scrollY-f)>=3;return u===v(d)?!F({action:g,target:d.find(b=>b.ref===this.lastActionRef)??d.find(b=>b.state.startsWith("focused")),scrolled:$,focusMoved:h!==null&&h!==C(d),geometryMoved:this.geometryChanges(p.snapshot)!==null}):$||s===null||!B(s,d)?!1:this.geometryChanges(p.snapshot)===null||L(h,d)},recapture:()=>this.bounded("observe",this.captureSnapshot())})}const e=r.snapshot;this.publishPreview();const o=this.signature(e),i=this.takeSurfaceChange(),n=i?"changed":this.prevSig===null?null:this.prevSig===o?"frozen":"changed";this.prevSig=o;const l=C(e.elements),S=this.prevElSig!==null&&this.prevElSig===l;this.prevElSig=l,this.prevElSigSubstantive=v(e.elements),this.lastGeomChange=this.geometryChanges(e),this.lastRects=new Map(e.elements.map(s=>[s.ref,s.rect])),this.lastScrollY=e.meta.scrollY,this.rememberRefs(e),n==="frozen"&&this.journalVerdict?.("frozen");const M=this.takeActionsInRound(),y=!t.afterAction&&this.takeContentRead();let a=t.prefix??"";const w=this.takePendingNote();if(w&&(a+=(a?`
4
4
  `:"")+`NOTE: ${w}.`),n==="frozen"&&!y){a+=(a?`
5
5
  `:"")+(t.afterAction?this.frozenNote(t.afterAction,this.frozenFacts(M)):z);const s=this.frozenExtra();s&&(a+=`
6
6
  ${s}`)}const x=this.diag?.takeNote(e.url);x&&(a+=(a?`
@@ -8,8 +8,8 @@ ${s}`)}const x=this.diag?.takeNote(e.url);x&&(a+=(a?`
8
8
  `:"")+`NOTE: ${this.observesSinceAction} observations in a row without acting, and the actionable elements are not changing \u2014 more looking will not reveal more. Act now (click / set_text / drag / scroll), or if the target truly is not on this screen, scroll or switch target.`);const A=await this.listTargets().catch(()=>[]),O=this.surfaceIdPrefix?A.map(s=>({...s,id:`${this.surfaceIdPrefix}${s.id}`})):A,{body:E,obsKind:P,headLines:_,effectNote:R}=this.renderObservation(e,O,n,{...t,surfaceChanged:i}),T=m(a?`${a}
9
9
  ${E}`:E,{url:e.url,verdict:n,elementCount:e.elements.length,obsKind:P,headLines:(a?a.split(`
10
10
  `).length:0)+_,actionLines:a?a.split(`
11
- `).length:0,keep:[...this.contextKeep,...this.trimSurvivors(),...R?[R]:[]],coverage:this.observationCoverage(e),...e.target?{target:e.target}:{},viewport:this.viewportPosition(e.meta),...e.meta.offscreen||e.meta.capped?{notListed:{offscreen:e.meta.offscreen??0,capped:e.meta.capped??0}}:{}});if(t.forceImage){const s=r.image??await this.screenshot().then(h=>({bytes:h,mimeType:this.screenshotMimeType})).catch(()=>null);s&&T.content.push({type:"image",data:s.bytes.toString("base64"),mimeType:s.mimeType})}return T}geometryChanges(t){const r=this.lastRects;if(!r)return null;const c=[];let e=0;const o=i=>Math.round(i);for(const i of t.elements){const n=r.get(i.ref);if(!n)continue;const l=Math.abs(i.rect.w-n.w)>=3||Math.abs(i.rect.h-n.h)>=3;if(!(!(Math.abs(i.rect.x-n.x)>=3||Math.abs(i.rect.y-n.y)>=3)&&!l)){if(c.length>=2){e++;continue}c.push(l?`${i.ref} resized ${o(n.w)}\xD7${o(n.h)} \u2192 ${o(i.rect.w)}\xD7${o(i.rect.h)}`:`${i.ref} moved (${o(n.x)},${o(n.y)}) \u2192 (${o(i.rect.x)},${o(i.rect.y)})`)}}return c.length===0?null:c.join(", ")+(e?` (+${e} more)`:"")}frozenExtra(){return null}explainMiss(t){return null}async unlistedTarget(t){return null}waitedOnUserMs=0;endCall(){}treeBlind(){return!1}takeActionsInRound(){const t=this.actionsInRound;return this.actionsInRound=0,t}frozenFacts(t){return{scrollMoved:this.lastScrollMoved,writeRound:this.writeRound,geomChange:this.lastGeomChange,repeated:this.lastActionSig!==null&&this.lastActionSig===this.prevActionSig,actionsInRound:t,treeBlind:this.treeBlind()}}async observeImage(t={}){const r=t.modelInitiated&&!this.takeContentRead()&&++this.observesSinceAction>=3?`
12
- NOTE: ${this.observesSinceAction} observations in a row without acting \u2014 more looking will not reveal more; act (click / set_text / drag / scroll) before looking again.`:"";if(t.scope){let n;try{n=await this.bounded("screenshot",this.screenshotScoped(t.scope))}catch(l){if(l instanceof k||l instanceof C)throw l;return m(`(screenshot failed: ${l.message})`,{error:"shot_failed"})}return{content:[{type:"text",text:`Screenshot captured (${n.note}). Scoped shots are for looking \u2014 point coordinates for actions live in FULL screenshots; use the returned mapping when provided, or act by ref.`+r},{type:"image",data:n.bytes.toString("base64"),mimeType:n.mimeType}],details:{scoped:!0,bytes:n.bytes.length,...n.capture?{capture:n.capture}:{}}}}const c=await this.bounded("screenshot",this.screenshot());this.takeSurfaceChange(),this.takeActionsInRound();let e=t.prefix??"";const o=this.takePendingNote();o&&(e+=(e?`
11
+ `).length:0,keep:[...this.contextKeep,...this.trimSurvivors(),...R?[R]:[]],coverage:this.observationCoverage(e),...e.target?{target:e.target}:{},viewport:this.viewportPosition(e.meta),...e.meta.offscreen||e.meta.capped?{notListed:{offscreen:e.meta.offscreen??0,capped:e.meta.capped??0}}:{}});if(t.forceImage){const s=r.image??await this.screenshot().then(h=>({bytes:h,mimeType:this.screenshotMimeType})).catch(()=>null);s&&T.content.push({type:"image",data:s.bytes.toString("base64"),mimeType:s.mimeType})}return T}geometryChanges(t){const r=this.lastRects;if(!r)return null;const c=[];let e=0;const o=i=>Math.round(i);for(const i of t.elements){const n=r.get(i.ref);if(!n)continue;const l=Math.abs(i.rect.w-n.w)>=3||Math.abs(i.rect.h-n.h)>=3;if(!(!(Math.abs(i.rect.x-n.x)>=3||Math.abs(i.rect.y-n.y)>=3)&&!l)){if(c.length>=2){e++;continue}c.push(l?`${i.ref} resized ${o(n.w)}\xD7${o(n.h)} \u2192 ${o(i.rect.w)}\xD7${o(i.rect.h)}`:`${i.ref} moved (${o(n.x)},${o(n.y)}) \u2192 (${o(i.rect.x)},${o(i.rect.y)})`)}}return c.length===0?null:c.join(", ")+(e?` (+${e} more)`:"")}frozenExtra(){return null}explainMiss(t){return null}async unlistedTarget(t){return null}async heldNothing(){return null}waitedOnUserMs=0;endCall(){}treeBlind(){return!1}takeActionsInRound(){const t=this.actionsInRound;return this.actionsInRound=0,t}frozenFacts(t){return{scrollMoved:this.lastScrollMoved,writeRound:this.writeRound,geomChange:this.lastGeomChange,repeated:this.lastActionSig!==null&&this.lastActionSig===this.prevActionSig,actionsInRound:t,treeBlind:this.treeBlind()}}async observeImage(t={}){const r=t.modelInitiated&&!this.takeContentRead()&&++this.observesSinceAction>=3?`
12
+ NOTE: ${this.observesSinceAction} observations in a row without acting \u2014 more looking will not reveal more; act (click / set_text / drag / scroll) before looking again.`:"";if(t.scope){let n;try{n=await this.bounded("screenshot",this.screenshotScoped(t.scope))}catch(l){if(l instanceof N||l instanceof k)throw l;return m(`(screenshot failed: ${l.message})`,{error:"shot_failed"})}return{content:[{type:"text",text:`Screenshot captured (${n.note}). Scoped shots are for looking \u2014 point coordinates for actions live in FULL screenshots; use the returned mapping when provided, or act by ref.`+r},{type:"image",data:n.bytes.toString("base64"),mimeType:n.mimeType}],details:{scoped:!0,bytes:n.bytes.length,...n.capture?{capture:n.capture}:{}}}}const c=await this.bounded("screenshot",this.screenshot());this.takeSurfaceChange(),this.takeActionsInRound();let e=t.prefix??"";const o=this.takePendingNote();o&&(e+=(e?`
13
13
  `:"")+`NOTE: ${o}.`);const i=this.diag?.takeNote(await this.currentUrl().catch(()=>""));return i&&(e+=(e?`
14
14
  `:"")+i),{content:[{type:"text",text:`${e?`${e}
15
15
  `:""}Screenshot captured. Anything without a ref can be acted on by x/y coordinates from this image.${r}`},{type:"image",data:c.toString("base64"),mimeType:this.screenshotMimeType}],details:{verdict:null,bytes:c.length}}}async observeFullText(){const t=await this.bounded("read text",this.readText());return m(t||"(no readable text \u2014 the page may still be rendering; wait, then observe again)",{chars:t.length})}}export{W as ComputerUseDriver,J as STATE_CHANGING_ACTIONS};
@@ -1 +1 @@
1
- import{STATE_CHANGING_ACTIONS as B}from"../driver.js";import{withoutBlankOptionals as G}from"./normalize.js";import{ambiguityNote as L,findScope as D,scopeLabel as U}from"./find-target.js";import{argComplaint as q}from"./validate.js";import{createTargeting as F}from"./api-target.js";import{waitVerb as H}from"./api-wait.js";import{expectVerb as W}from"./api-expect.js";import{checkShotPath as J,saveShot as Y}from"./shot-file.js";import{fnName as E,firstLine as x,firstText as O,noteOf as S,options as g}from"./api-contract.js";const z=600,j=150;function K(r,c,w){return r==="menu"&&!c.path||w.some($=>$.kind===r&&$.readOnly)}function ie(r,c,w={}){const $=r.extraVerbs(),V=new Map($.map(e=>[e.kind,e.schema])),C=[...V.keys()],{findWithMiss:v,asPoint:N,resolve:y,requireFocus:M}=F(r,c,w,x);let T=null;const k=(e,t)=>{const n=V.get(e);if(!n)return null;const o=G({kind:e,...t},n);return q(n,E(e),o)},R=async(e,t,n,o=!1)=>{c().charge(e);const s=k(e,t);if(s)throw c().push(e,n,!1,s,!1,"blocked"),new Error(s);const i=K(e,t,$);if(!i)try{await w.beforeAction?.(e,t)}catch(u){throw c().push(e,n,!1,u.message,!1,"blocked"),u}i||r.noteAction(e,t),i?r.noteContentRead():T=e,i||await r.ensureSettleBaseline(e);let a;try{a=e==="focus"?await r.focusTarget(String(t.target??"")):await r.perform(e,t)}catch(u){throw c().push(e,n,!1,`${u.message}; outcome unknown \u2014 observe before retrying`,i,i?void 0:"uncertain"),u}a.ok&&r.publishPreview(i?void 0:e,t);const l=(o||e==="type")&&a.ok&&a.verified===!1,f=e==="set_text"&&a.ok&&a.verified===void 0?`${S(a)||"write dispatched"}; read-back unavailable \u2014 the value was not verified`:S(a),m=a.mayHaveActed||a.verified===!1?"uncertain":a.ok?a.verified===!0?"verified":"dispatched":"blocked";c().push(e,n,a.ok&&!l,f,i,i?void 0:m);const p=c().round;if(p&&a.ok&&(e==="set_text"?(p.fields+=1,a.verified===!0&&(p.verified+=1)):p.ended=`${e}${t.chord?` ${String(t.chord)}`:""}${n?` ${n}`:""}`),!a.ok)throw new Error(`${e}${n?` ${n}`:""} failed: ${a.note??"no reason given"}`);if(l){const u=new Error(`${e==="type"?"type":"setText"} ${n}: ${a.note??"the field did not retain the requested text"}. The write was dispatched, but verification failed; subsequent actions were not run. `+(e==="type"?"Observe the focused field before continuing.":"Use setTextBestEffort only when you intend to handle a mismatched value yourself."));throw u.name="WriteVerificationError",u}return a},A=async(e,t,n,o)=>{const s=g("setText",o,"setText(target, text, {ifValue:previousValue})");if(Object.keys(s).some(f=>f!=="ifValue")||s.ifValue!==void 0&&typeof s.ifValue!="string")throw new Error("setText's only option is ifValue:string");const{ref:i,name:a}=await y(e,{write:!0});if(s.ifValue!==void 0&&(c().charge("precondition"),!r.readValue||!r.valueMatches(await r.readValue(i),s.ifValue))){const f="The field no longer matches ifValue, or cannot be read. Nothing was written; observe and reconcile the edit.";throw c().push("set_text",i,!1,f,!1,"blocked"),new Error(f)}return(await R("set_text",{ref:i,...a?{name:a}:{},text:String(t??"")},i,n)).verified},h=async(e,t,n)=>{const o=await R(e,t,n);return"value"in o?o.value:o.note||(n?`${e} ${n}`:e)},d=(e,t,n,o)=>async()=>{c().charge(e),r.noteContentRead();const s=await o();return r.publishPreview(),c().push(e,t,!0,n(s)),s},_=(e,t,n)=>async(...o)=>{if(o.length)throw new Error(`${e}() takes no arguments \u2014 ${t}`);return n()},I={observe:async(e={})=>{const t=g("observe",e,"observe() lists every element; observe({screenshot: true}) also attaches the picture"),n=Object.keys(t).filter(b=>b!=="screenshot");if(n.length)throw new Error(`observe() does not take ${n.join(", ")} \u2014 it already answers with EVERY element, on screen and beyond it (each one off-viewport carries \`offscreen: true\`); narrow it by filtering the array it returns. Its one option is screenshot: true.`);c().charge("observe");const{url:o,title:s,elements:i,groups:a,headings:l,partial:f,coverage:m,target:p}=await r.readElements();w.onRefs?.(i.map(b=>b.ref));let u="";if(t.screenshot){c().charge("screenshot");const b=await r.observeImage();for(const P of b.content)P.type==="image"&&(c().attach(P),u=" + image");u||(u=` (no image \u2014 ${x(b)})`)}return c().push("observe","",!0,`${i.length} elements${u}`),{url:o,title:s,elements:i,coverage:m,...p?{target:p}:{},...a?.length?{groups:a}:{},...l?.length?{headings:l}:{},...f?{partial:f}:{}}},find:async(e,t={})=>{c().charge("find");const n=D(t,r.readsOffscreen);let{h:o,miss:s}=await v(String(e??""),n);if(!o&&B.has(T??"")){T=null;for(let i=0;!o&&i<z;i+=j)await new Promise(a=>setTimeout(a,j)),{h:o,miss:s}=await v(String(e??""),n)}return c().push("find",`${String(e??"")}${U(n)}`,!0,o?L(o):s),o},text:_("text","it returns the surface's whole readable text",d("text","",e=>`${e.length} chars`,()=>r.readText())),value:async e=>{if(!r.readValue)throw new Error("value() is not supported on this platform \u2014 text() reads the whole surface instead");const t=await y(e);return d("value",t.ref,n=>`${n.length} chars`,()=>r.readValue(t.ref))()},page:async(e={})=>{const t=g("page",e,"page() reads the whole page and page({ref}) one node \u2014 to NAVIGATE, openUrl({url})");if(!r.readPage)throw new Error("page reading is not supported on this platform \u2014 use text()");if(t.ref&&!r.knownRef(t.ref))throw new Error(r.staleRefNote(t.ref));const n=d("page",t.ref??t.node??"",o=>x(o),()=>r.readPage(t));return O(await n())},screenshot:async(e={})=>{const{path:t,...n}=g("screenshot",e,"screenshot() takes the viewport; screenshot({ref}) / ({region}) / ({fullPage:true}) narrow it");if(n.ref&&!r.knownRef(n.ref))throw new Error(r.staleRefNote(n.ref));if(t!==void 0&&typeof t!="string")throw new Error("screenshot path must be a string");t&&J(t,r.screenshotMimeType);const o=!!(n.ref||n.region||n.fullPage||n.padding!==void 0);let s=null;const i=f=>s?`saved ${s.path}`:`no image \u2014 ${x(f)}`,l=await d("screenshot",n.ref??(n.fullPage?"full page":n.region?"region":""),i,async()=>{const f=await r.observeImage(o?{scope:n}:{}),m=f.content.find(p=>p.type==="image");return m&&m.type==="image"&&(s=Y(m.data,m.mimeType,t||void 0)),f})();for(const f of l.content)f.type==="image"&&c().attach(f);return s?{...s,note:x(l),...l.details.capture?{capture:l.details.capture}:{}}:{path:null,note:i(l)}},diagnostics:async(e={})=>{const t=g("diagnostics",e,"diagnostics() reads every channel, diagnostics({id}) one of them");return O(await d("diagnostics",t.id??"",x,async()=>r.readDiagnostics(t))())},targets:_("targets","it lists every surface there is",d("targets","",e=>`${e.length} targets`,()=>r.listTargets())),click:async(e,t={})=>{const n=N(e);if(n)return h("click",{...n,...t},`${n.x},${n.y}`);const{ref:o,name:s}=await y(e,{write:!0});return h("click",{ref:o,...s?{name:s}:{},...t},o)},hover:async e=>{const t=N(e);if(t)return h("hover",{...t},`${t.x},${t.y}`);const{ref:n,name:o}=await y(e);return h("hover",{ref:n,...o?{name:o}:{}},n)},setText:(e,t,n)=>A(e,t,!0,n),setTextBestEffort:(e,t,n)=>A(e,t,!1,n),keys:async(e,t,n)=>{const o=typeof t=="object"&&t!==null?t:n,s=typeof t=="number"?t:void 0;return o?.focus!==void 0&&await M(o.focus),h("keys",{chord:String(e??""),...s?{count:s}:{}},String(e??""))},scroll:async(e={})=>{const t=g("scroll",e,"scroll({direction:'down'}) or scroll({to:'bottom'})");if(t.into!==void 0){const n=typeof t.into=="string"?t.into:t.into?.ref;if(typeof n!="string")throw new Error("scroll({into}) takes a ref string or a find() handle");if(!r.scrollsInto)throw new Error("scroll({into}) is not available on this platform \u2014 scroll by direction, count or to:");const o=["direction","count","to","ref"].filter(i=>t[i]!==void 0);if(o.length)throw new Error(`scroll({into}) moves to one control and takes nothing else \u2014 drop ${o.join(", ")}`);w.onTarget?.(n);const s=await h("scroll",{into:n},n);return await r.readElements().catch(()=>{}),s}return typeof t.ref=="string"&&w.onTarget?.(t.ref),h("scroll",{...t},String(t.ref??t.direction??t.to??""))},wait:H(r,c),expect:W(r,c,async e=>{if(!r.readValue)throw new Error("This platform cannot read field values");const{ref:t}=await y(e);return r.readValue(t)}),focus:async e=>h("focus",{target:String(e??"")},String(e??"")),log:(...e)=>{c().charge("log"),c().push("log","",!0,e.map(Q).join(" ").slice(0,200))},act:async(e,t={})=>{if(typeof t.ref=="string"&&(w.onTarget?.(t.ref),!r.knownRef(t.ref)))throw new Error(r.staleRefNote(t.ref));return h(String(e??""),{...t},String(t.ref??""))}};for(const e of C)I[E(e)]=async(t={})=>{const n=g(E(e),t,`pass its arguments as an object, e.g. ${E(e)}({\u2026})`),o=n.ref!==void 0?await y(n.ref,{write:!0}):null;return h(e,{...n,...o?{ref:o.ref}:{}},String(n.ref??""))};return I}import{renderJournal as le,stringify as he,verbGate as pe}from"./api-run.js";import{stringify as Q}from"./api-run.js";export{ie as createComputerUseApi,E as fnName,K as isReadVerb,S as noteOf,le as renderJournal,he as stringify,pe as verbGate};
1
+ import{STATE_CHANGING_ACTIONS as B}from"../driver.js";import{withoutBlankOptionals as G}from"./normalize.js";import{ambiguityNote as L,findScope as D,scopeLabel as U}from"./find-target.js";import{argComplaint as q}from"./validate.js";import{createTargeting as F}from"./api-target.js";import{waitVerb as H}from"./api-wait.js";import{expectVerb as W}from"./api-expect.js";import{checkShotPath as J,saveShot as Y}from"./shot-file.js";import{fnName as E,firstLine as x,firstText as O,noteOf as S,options as d}from"./api-contract.js";const z=600,j=150;function K(r,c,w){return r==="menu"&&!c.path||w.some($=>$.kind===r&&$.readOnly)}function ie(r,c,w={}){const $=r.extraVerbs(),V=new Map($.map(e=>[e.kind,e.schema])),C=[...V.keys()],{findWithMiss:v,asPoint:N,resolve:g,requireFocus:M}=F(r,c,w,x);let T=null;const k=(e,t)=>{const n=V.get(e);if(!n)return null;const o=G({kind:e,...t},n);return q(n,E(e),o)},R=async(e,t,n,o=!1)=>{c().charge(e);const s=k(e,t);if(s)throw c().push(e,n,!1,s,!1,"blocked"),new Error(s);const i=K(e,t,$);if(!i)try{await w.beforeAction?.(e,t)}catch(u){throw c().push(e,n,!1,u.message,!1,"blocked"),u}i||r.noteAction(e,t),i?r.noteContentRead():T=e,i||await r.ensureSettleBaseline(e);let a;try{a=e==="focus"?await r.focusTarget(String(t.target??"")):await r.perform(e,t)}catch(u){throw c().push(e,n,!1,`${u.message}; outcome unknown \u2014 observe before retrying`,i,i?void 0:"uncertain"),u}a.ok&&r.publishPreview(i?void 0:e,t);const l=(o||e==="type")&&a.ok&&a.verified===!1,f=e==="set_text"&&a.ok&&a.verified===void 0?`${S(a)||"write dispatched"}; read-back unavailable \u2014 the value was not verified`:S(a),m=a.mayHaveActed||a.verified===!1?"uncertain":a.ok?a.verified===!0?"verified":"dispatched":"blocked";c().push(e,n,a.ok&&!l,f,i,i?void 0:m);const p=c().round;if(p&&a.ok&&(e==="set_text"?(p.fields+=1,a.verified===!0&&(p.verified+=1)):p.ended=`${e}${t.chord?` ${String(t.chord)}`:""}${n?` ${n}`:""}`),!a.ok)throw new Error(`${e}${n?` ${n}`:""} failed: ${a.note??"no reason given"}`);if(l){const u=new Error(`${e==="type"?"type":"setText"} ${n}: ${a.note??"the field did not retain the requested text"}. The write was dispatched, but verification failed; subsequent actions were not run. `+(e==="type"?"Observe the focused field before continuing.":"Use setTextBestEffort only when you intend to handle a mismatched value yourself."));throw u.name="WriteVerificationError",u}return a},A=async(e,t,n,o)=>{const s=d("setText",o,"setText(target, text, {ifValue:previousValue})");if(Object.keys(s).some(f=>f!=="ifValue")||s.ifValue!==void 0&&typeof s.ifValue!="string")throw new Error("setText's only option is ifValue:string");const{ref:i,name:a}=await g(e,{write:!0});if(s.ifValue!==void 0&&(c().charge("precondition"),!r.readValue||!r.valueMatches(await r.readValue(i),s.ifValue))){const f="The field no longer matches ifValue, or cannot be read. Nothing was written; observe and reconcile the edit.";throw c().push("set_text",i,!1,f,!1,"blocked"),new Error(f)}return(await R("set_text",{ref:i,...a?{name:a}:{},text:String(t??"")},i,n)).verified},h=async(e,t,n)=>{const o=await R(e,t,n);return"value"in o?o.value:o.note||(n?`${e} ${n}`:e)},y=(e,t,n,o)=>async()=>{c().charge(e),r.noteContentRead();const s=await o();return r.publishPreview(),c().push(e,t,!0,n(s)),s},_=(e,t,n)=>async(...o)=>{if(o.length)throw new Error(`${e}() takes no arguments \u2014 ${t}`);return n()},I={observe:async(e={})=>{const t=d("observe",e,"observe() lists every element; observe({screenshot: true}) also attaches the picture"),n=Object.keys(t).filter(b=>b!=="screenshot");if(n.length)throw new Error(`observe() does not take ${n.join(", ")} \u2014 it already answers with EVERY element, on screen and beyond it (each one off-viewport carries \`offscreen: true\`); narrow it by filtering the array it returns. Its one option is screenshot: true.`);c().charge("observe");const{url:o,title:s,elements:i,groups:a,headings:l,partial:f,coverage:m,target:p}=await r.readElements();w.onRefs?.(i.map(b=>b.ref));let u="";if(t.screenshot){c().charge("screenshot");const b=await r.observeImage();for(const P of b.content)P.type==="image"&&(c().attach(P),u=" + image");u||(u=` (no image \u2014 ${x(b)})`)}return c().push("observe","",!0,`${i.length} elements${u}`),{url:o,title:s,elements:i,coverage:m,...p?{target:p}:{},...a?.length?{groups:a}:{},...l?.length?{headings:l}:{},...f?{partial:f}:{}}},find:async(e,t={})=>{c().charge("find");const n=D(t,r.readsOffscreen);let{h:o,miss:s}=await v(String(e??""),n);if(!o&&B.has(T??"")){T=null;for(let i=0;!o&&i<z;i+=j)await new Promise(a=>setTimeout(a,j)),{h:o,miss:s}=await v(String(e??""),n)}return c().push("find",`${String(e??"")}${U(n)}`,!0,o?L(o):s),o},text:_("text","it returns the surface's whole readable text",y("text","",e=>`${e.length} chars`,()=>r.readText())),value:async e=>{if(!r.readValue)throw new Error("value() is not supported on this platform \u2014 text() reads the whole surface instead");const t=await g(e);return y("value",t.ref,n=>`${n.length} chars`,()=>r.readValue(t.ref))()},page:async(e={})=>{const t=d("page",e,"page() reads the whole page and page({ref}) one node \u2014 to NAVIGATE, openUrl({url})");if(!r.readPage)throw new Error("page reading is not supported on this platform \u2014 use text()");if(t.ref&&!r.knownRef(t.ref))throw new Error(r.staleRefNote(t.ref));const n=y("page",t.ref??t.node??"",o=>x(o),()=>r.readPage(t));return O(await n())},screenshot:async(e={})=>{const{path:t,...n}=d("screenshot",e,"screenshot() takes the viewport; screenshot({ref}) / ({region}) / ({fullPage:true}) narrow it");if(n.ref&&!r.knownRef(n.ref))throw new Error(r.staleRefNote(n.ref));if(t!==void 0&&typeof t!="string")throw new Error("screenshot path must be a string");t&&J(t,r.screenshotMimeType);const o=!!(n.ref||n.region||n.fullPage||n.padding!==void 0);let s=null;const i=f=>s?`saved ${s.path}`:`no image \u2014 ${x(f)}`,l=await y("screenshot",n.ref??(n.fullPage?"full page":n.region?"region":""),i,async()=>{const f=await r.observeImage(o?{scope:n}:{}),m=f.content.find(p=>p.type==="image");return m&&m.type==="image"&&(s=Y(m.data,m.mimeType,t||void 0)),f})();for(const f of l.content)f.type==="image"&&c().attach(f);return s?{...s,note:x(l),...l.details.capture?{capture:l.details.capture}:{}}:{path:null,note:i(l)}},diagnostics:async(e={})=>{const t=d("diagnostics",e,"diagnostics() reads every channel, diagnostics({id}) one of them");return O(await y("diagnostics",t.id??"",x,async()=>r.readDiagnostics(t))())},targets:_("targets","it lists every surface there is",y("targets","",e=>`${e.length} targets`,()=>r.listTargets())),click:async(e,t={})=>{const n=N(e);if(n)return h("click",{...n,...t},`${n.x},${n.y}`);const{ref:o,name:s}=await g(e,{write:!0});return h("click",{ref:o,...s?{name:s}:{},...t},o)},hover:async e=>{const t=N(e);if(t)return h("hover",{...t},`${t.x},${t.y}`);const{ref:n,name:o}=await g(e);return h("hover",{ref:n,...o?{name:o}:{}},n)},setText:(e,t,n)=>A(e,t,!0,n),setTextBestEffort:(e,t,n)=>A(e,t,!1,n),keys:async(e,t,n)=>{const o=typeof t=="object"&&t!==null?t:n,s=typeof t=="number"?t:void 0;return o?.focus!==void 0&&await M(o.focus),h("keys",{chord:String(e??""),...s?{count:s}:{}},String(e??""))},scroll:async(e={})=>{const t=d("scroll",e,"scroll({direction:'down'}) or scroll({to:'bottom'})");if(t.into!==void 0){const o=typeof t.into=="string"?t.into:t.into?.ref;if(typeof o!="string")throw new Error("scroll({into}) takes a ref string or a find() handle");if(!r.scrollsInto)throw new Error("scroll({into}) is not available on this platform \u2014 scroll by direction, count or to:");const s=["direction","count","to","ref"].filter(a=>t[a]!==void 0);if(s.length)throw new Error(`scroll({into}) moves to one control and takes nothing else \u2014 drop ${s.join(", ")}`);w.onTarget?.(o);const i=await h("scroll",{into:o},o);return await r.readElements().catch(()=>{}),i}let n=t.ref;return n!==void 0&&typeof n!="string"?n=(await g(n)).ref:typeof n=="string"&&w.onTarget?.(n),h("scroll",{...t,...n===void 0?{}:{ref:n}},String(n??t.direction??t.to??""))},wait:H(r,c),expect:W(r,c,async e=>{if(!r.readValue)throw new Error("This platform cannot read field values");const{ref:t}=await g(e);return r.readValue(t)}),focus:async e=>h("focus",{target:String(e??"")},String(e??"")),log:(...e)=>{c().charge("log"),c().push("log","",!0,e.map(Q).join(" ").slice(0,200))},act:async(e,t={})=>{if(typeof t.ref=="string"&&(w.onTarget?.(t.ref),!r.knownRef(t.ref)))throw new Error(r.staleRefNote(t.ref));return h(String(e??""),{...t},String(t.ref??""))}};for(const e of C)I[E(e)]=async(t={})=>{const n=d(E(e),t,`pass its arguments as an object, e.g. ${E(e)}({\u2026})`),o=n.ref!==void 0?await g(n.ref,{write:!0}):null;return h(e,{...n,...o?{ref:o.ref}:{}},String(n.ref??""))};return I}import{renderJournal as le,stringify as he,verbGate as pe}from"./api-run.js";import{stringify as Q}from"./api-run.js";export{ie as createComputerUseApi,E as fnName,K as isReadVerb,S as noteOf,le as renderJournal,he as stringify,pe as verbGate};
@@ -1,4 +1,4 @@
1
1
  import{fnName as u}from"./api.js";import{IDLE_RELEASE_MS as p}from"../ownership.js";const g="Run JavaScript in this session's PERSISTENT runtime to look at and act on the GUI \u2014 web pages in a browser, Electron apps and native macOS / Windows apps, whichever this server mounts \u2014 the one tool for both. The code is an async function body: `await` every verb, `return` a value to get it back as JSON, and top-level const/let/function declarations SURVIVE to later calls \u2014 keep handles and collected data in them instead of re-reading. Surfaces (tabs / windows / apps) are HANDLES and EVERY verb lives on one \u2014 there are no top-level verbs. Bind first: computer.surface('<id or title>') names one, computer.surface() alone takes the only surface, or the one this session opened when other tabs are open too, computer.surfaces() lists them. Then w.observe() w.find(name) w.click(t) w.setText(t, text) w.keys(chord, n) w.scroll({\u2026}) w.wait({\u2026}) w.text() w.page({\u2026}) w.screenshot({\u2026}) and this platform's own verbs, each switching to that surface as needed; a ref read on one surface is refused on another. Bind ONCE \u2014 keep the handle in a top-level variable and later calls reuse it. The first bound operation claims the target across calls; await w.release() when its task is done. Another runtime's claim refuses input before dispatch. The first result carries the full API reference (computer.help() returns it again) \u2014 use only what it documents. One verb at a time (one focus, one caret): a failed action or expect() throws and stops the run; wait() instead returns false on timeout, so check it or use expect() for a required condition. The result is the step journal plus one observation of the surface it acted on. Per call: 100 actions, 60 s. No require/import/fetch/process/timers. Before using an operation that may affect the user's foreground, real pointer, keyboard focus or system clipboard (including automatic fallback), explain its target and effects and obtain permission unless existing permission covers them. Do not silently escalate a background operation; inspect computer.help() for platform-specific effects. Before sending a message or publishing a post, let the user confirm its content and recipients or destination, unless they already approved those exact details.",v=`Reset computer_repl's persistent JavaScript runtime: every binding \u2014 handles, variables, functions \u2014 is discarded, and the next computer_repl call starts fresh and returns the API reference again. Nothing on the surfaces changes: no tab, window or app is closed. This runtime's target ownership is released after any active call stops. Use it only when the runtime is wedged or its state is wrong, never as a way to "start over" on the screen.`;function f(e,a){const o=a,s=Object.keys(o.properties??{}).filter(t=>t!=="kind"),n=new Set(o.required??[]),r=t=>n.has(t)?t:`${t}?`,h=s.filter(t=>t!=="ref"&&t!=="name").map(t=>o.properties?.[t]?.description).find(Boolean),i=o.description??h??"",c=/^.*?[.!?](?=\s+[A-Z(`]|$)/s.exec(i)?.[0]??i,d=s.filter(t=>t==="ref"||t==="name").flatMap(t=>o.properties?.[t]?.description?[` ${r(t)}: ${o.properties[t].description}`]:[]);return[` w.${u(e)}({${s.map(r).join(", ")}})${c?` \u2014 ${c.trim()}`:""}`,...d,...n.has("ref")&&s.includes("name")?[" name does not replace the required ref."]:[]].join(`
2
- `)}function l(e){const a=e.extraVerbs();return{head:["API reference \u2014 computer_repl (this text comes once per session; computer.help() returns it again)","","Everything is async \u2014 the verbs AND computer.surfaces() / computer.surface(\u2026): await each. A verb that","fails THROWS with the reason, which stops the run; the journal in the result says what ran. Refs (e12)","come from observe()/find() and stay valid until the surface changes; never invent one, and never carry one in from","another session \u2014 a fresh session re-mints them, so the same e12 names a different element. A target `t` is "+"a find() handle, a ref string, or a point {x, y} read off a screenshot"+".","","Bind a surface first \u2014 every verb below is a method on the handle it returns, never a name of its own:",' await computer.surfaces() \u2192 [handle] \u2014 every surface (tab / window / app) as {id, kind, title, active}; `id` is an opaque key, what the platform knows the surface by lives in `title` (macOS: "TextEdit (com.apple.TextEdit) \u2014 Untitled"; Windows: the window title, then the program name \u2014 "Untitled - Notepad \u2014 Notepad")'," await computer.surface(name) \u2192 handle \u2014 by id, by whole title, or by a piece of the title; several matches or none is a refusal that lists the candidates, never a guess",' await computer.surface() \u2192 handle \u2014 the ONE surface, when there is only one. Not "whatever is in front": with more than one open it refuses and lists them, so a script always says which it meant'," Bind once per surface and keep the handle in a top-level declaration \u2014 it stays valid for later calls, and re-binding costs a read each time."," A browser tab marked unloaded:true is still discoverable. Using its handle restores that exact tab in the background after acquiring ownership; merely listing tabs never reloads it.",""],reads:[...e.interactionNotes(),"Reads (return data to your code; none of them is shown to you except through `return` or computer.log):",e.readsOffscreen?" w.observe({screenshot?}) \u2192 {url, title, elements:[{ref, role, name, state, rect, offscreen?}], groups?, headings?, partial?, coverage, target?} \u2014 every control, viewport and beyond; filter the array rather than asking it to narrow. `rect` {x, y, w, h} is the control's box \u2014 compare boxes to answer layout questions (which row a control sits on, whether it moved) without a screenshot. `groups` is the page's OWN labelling of those controls \u2014 [{label, members:[ref]}] over that same set, off-viewport members included \u2014 present where the author wrote one, and the only place some facts exist (a feed labels each post's button row with its counts, bookmarks included, which no control name and no page text carries). `headings` (web surfaces) is [{name, level, rect}] for the page's headings, never listed as controls \u2014 the fixed point to measure a control's box against, since a click can scroll the container under every control in a card. `partial` (a reason) says the read stopped before covering the surface, so what it did not list may still be there. screenshot:true also attaches the picture of the same screen (one call, two reads), for when you need to SEE where something is":" w.observe({screenshot?}) \u2192 {url, title, elements:[{ref, role, name, state, rect}], coverage} \u2014 every control the window SHOWS (`rect` {x, y, w, h} is its box \u2014 compare boxes to answer layout questions without a screenshot); one scrolled out of view is counted in the observation and never listed, so no ref names it until a scroll brings it in; filter the array rather than asking it to narrow. screenshot:true also attaches the picture of the same screen"," coverage reports exact {offscreen, covered, capped, modal?} counts (null when unreported) even when repeated context notes are omitted. Electron target reports the CDP endpoint, windowId and, when its user agent carries them, the app's own name/version; never infer the app version from the tool or helper path."," w.find(name, {role?, within?, "+(e.readsOffscreen?"visible?, ":"")+"near?}) \u2192 {ref, name, matches, others, incidental?} | null \u2014 by visible name, "+(e.readsOffscreen?"whole surface":"everything the window shows \u2014 NOT what is scrolled out of it, which this platform's read does not contain")+" (an exact name beats a partial one, a partial the query is most of beats one it is only a fragment of, and a control beats a text label); a handle that re-resolves itself when used, so a loop survives a re-render; `matches` says how many controls carried the name and `others` names the runners-up, so an ambiguous name is visible BEFORE you act on the pick; `incidental: true` says NOTHING here is NAMED that and the pick is only a long name that happens to contain the word \u2014 the one `matches: 1` not to trust; narrow it with role (button / textbox / \u2026), within (a ref or handle \u2014 matches inside that element's box, e.g. one sheet or one message)"+(e.readsOffscreen?" visible (on screen only)":"")+" and near (a labelled group restricts candidates to its members; otherwise prefer the control closest to the named element). Duplicate group labels or same-named controls within a group still need disambiguation"," w.text() \u2192 the surface's full readable text",...e.readValue?[" w.value(t) \u2192 ONE control's whole value as a string \u2014 a long field, a clipped label, the text you are about to edit. The listing shows a value cut to what a line holds; this is the rest of it, for one element, where text() re-reads the entire surface"]:[],...e.readPage?[" w.page({ref?, depth?, node?}) \u2192 the content tree as text, with a completeness receipt on its first line"]:[]," w.screenshot({ref?, padding?|region:{x,y,width,height}?|fullPage?, path?}) \u2192 {path, host, width, height, note, capture?} \u2014 the image is attached to this result for you to see AND written to a file the code can keep: to `path` (absolute, never overwrites an existing file, extension must match the surface's format \u2014 .jpg on web surfaces, .png on macOS and Windows) or else to a rolling temp directory; `host` names the machine the path is on; no image \u2192 {path: null, note: why}. ref is a ref string and crops to a visible element's box; padding keeps its surroundings. fullPage is web-only. On macOS and Windows, ref/region preserve native detail; region uses FULL screenshot pixels, so take a full screenshot first. Windows padding and crop bounds use physical pixels; macOS padding uses full-screenshot pixels when available, otherwise window points. capture reports the window, crop, units, pointsPerPixel, and toFullScreenshot (null without a full screenshot or when window geometry changed). Windows refuses a region based on old window geometry; take a new full screenshot. Convert crop pixel p to full screenshot coordinates with toFullScreenshot.origin + p * toFullScreenshot.scale on each axis. A crop never changes point-action coordinates; use a current mapping or act by ref.",...e.supportsDiagnostics?[` w.diagnostics({id?}) \u2192 ${e.diagnosticsAbout.empty} \u2014 the newest 20, with the ones new since your last read marked "new"`]:[]," computer.log(\u2026) \u2192 a line in the journal; the one member that is not on a surface",""],actions:["Actions (every one is w.verb(\u2026) on a bound handle, which switches to that surface first):"," w.click(t, {button?, count?}) w.hover(t)"," w.setText(t, text) \u2014 replace the target field's ENTIRE value, even after selectText; never a selection/caret edit. Returns true when read-back matches; throws WriteVerificationError on mismatch, stopping the run unless caught."," w.setTextBestEffort(t, text) \u2014 the same whole-field replacement, returning true/false without throwing on mismatch. Both return undefined when read-back is unavailable; that is not verification."," click and hover also take a POINT \u2014 w.click({x, y}) \u2014 in the coordinates of the last screenshot, for a control you"," can see and cannot name. It checks no identity (no stale-ref gate, no name, no surface check) and so acts on"," whatever is at that spot NOW: take observe({screenshot: true}) and read the coordinates off that picture."," click / hover / keys / scroll / focus answer with what they did, as text \u2014 the same note the journal shows, e.g. which"," rung a click ended on. A verb that FAILS throws instead, so a returned string always describes something that happened."," w.keys(chord, count?, {focus?}) e.g. w.keys('Enter'), w.keys('Backspace', 3), w.keys('Enter', {focus: field}) \u2014 one key or chord, never a text string. setText replaces a whole field; use the platform's paste/type verb to edit at a selection or caret."," keys go wherever the surface has the keyboard focus. Pass focus: <target> to REQUIRE it be there: if it is not,"," nothing is pressed, including when focus is unknown or ambiguous. Omit focus only to send without that check. Focusing stays explicit (click / setText / selectText with a caret)."," w.scroll({direction?: up|down|left|right, count?: screens (default 1), to?: top|bottom, ref?}) w.wait({} | {ms} | {text} | {text_gone} | {control} | {control_gone} | {url} | {url_gone} | {loaded}) \u2192 ok"," count covers that many screens in ONE call \u2014 the note says how far it actually got, which is less once the end is reached;"," count and to: are refused together, since to: already goes as far as the surface scrolls."," Browser/Electron list scrollable regions with refs. With several regions, pass ref:<region or a control inside it>; a bare scroll refuses and lists the choices. ref:'window' explicitly scrolls the page. Reaching an edge never switches to another region.",...e.scrollsInto?[" w.scroll({into: t}) instead brings ONE control (a ref or a find() handle) to the middle of the viewport and says whether anything still covers it. It takes a covered control's ref too \u2014 the way out from under a sticky header or a bottom strip that a screen of scrolling overshoots."]:[]," a wait polls until the condition holds or it times out, and says which: {text\u2026} searches the readable text,",' {control\u2026} the CONTROLS by name ("until the Stop button is gone" \u2014 the honest way to wait out a long answer),'," {url}/{url_gone} where the surface IS (substring, so a query string needs no spelling out), and {loaded:true} that"," the DOCUMENT finished. Reach for it before reporting that a page has nothing on it: a document that never finishes"," reads as an empty page and no content condition can tell you which it was. It is NOT rendered \u2014 a single-page app"," reaches loaded with nothing drawn yet (measured), so follow it with the CONTENT wait, which is w.wait({}):",' it polls until the readable text stops changing, so it is the one "the page is ready" condition that needs no word'," from the page and no knowledge of the site. Use it after a navigation, a tab switch inside a page, or a search \u2014 a"," single-page app mounts its rows well after {loaded}, and an observation taken before that reads as an empty page."," Also after a click that REDRAWS content (a chart, a list) and before you screenshot or read it: the control's own state"," updates at once, the content a beat later, so a read straight after the click can pair the new button with the old view."," Every condition takes ms as its OWN polling budget (default 16s): w.wait({text:'Done', ms:3000}) gives up in three seconds,"," give or take the one read already in flight when it runs out \u2014 on a heavy surface a single read is seconds, and the result says so when that happened."," wait() returns true/false; awaiting it alone does NOT stop on timeout. Use expect() for a required postcondition, or check the result: if (!await w.wait({...})) throw new Error('condition not met');"," which is what a GUESS at the wording is worth \u2014 an unguarded guess that no page ever matches costs the full 16s."," w.focus() \u2014 select w as the target for this runtime; target selection is not an OS-activation guarantee (see platform interaction notes)",` w.release() \u2014 release this target and its input resources for other sessions. Claims span calls until release, reset, turn end, process exit or ${p/6e4} minutes with no call (the next call takes the target back unless another session has it by then). Observe again before acting after handoff; old refs are invalidated. Separate windows can still share app/account/document state; ownership does not isolate those or a human editor.`," w.expect({text, control, url, value, equals, ms}) \u2014 verify exactly ONE condition; value takes a ref/find handle and equals:string. Read-only polling, default 3000 ms, maximum 16000 ms. Returns true when verified; otherwise throws before subsequent steps. It never repeats the preceding action. Use it to verify save/submit results, not screen-change verdicts."," w.setText(t, text, {ifValue:previousValue}) \u2014 optional precondition: refuse before writing if the field changed or cannot be read. This is a checked precondition, not an atomic transaction with a human or another app.","Action journal evidence: [verified] confirms the stated value/condition; [dispatched] only confirms delivery; [blocked] means no dispatch; [uncertain] may already have acted. Never replay an uncertain send/submit/delete; inspect its actual result first."],extras:[...a.length?[" This platform's verbs \u2014 each returns what it did as text (the journal keeps only its first line, so a listing is read from the return value):"," In these signatures, ? marks an optional property; every unmarked property is required. Omit the ? in JavaScript calls.",...a.map(s=>f(s.kind,s.schema))]:[],...a.some(s=>s.kind==="select_option")?[" selectOption example: const choice = await w.find('Priority', {role:'combobox'}); if (!choice || choice.matches !== 1 || choice.incidental) throw new Error('Priority is missing or ambiguous'); await w.selectOption({ref:choice.ref, option:'High'});"]:[],...a.some(s=>s.kind==="run_js")?[' runJs is the exception: it returns the VALUE the code produced, whole (the journal shows a short preview); an exception thrown in the page comes back as the string "threw: \u2026" instead of stopping the run.']:[],...a.some(s=>s.kind==="download")?[` download also returns a VALUE: const watch = await w.download(); await w.click(await w.find('Download')); const result = await w.download({id:watch.id}); return result; result.downloads carries the actual path, filename, host and state. Read/upload only complete receipts. Query the same ID after a timeout; several candidates mean ambiguous, never the newest file. Re-arming this tab ends its previous collection; stop:true explicitly ends collection without canceling any download. On this session's own tabs a page's save dialog (showSaveFilePicker) never opens: its file arrives as a download marked source "save-picker".`]:[]],tail:[" w.act(kind, {\u2026}) \u2014 any verb by its wire name, arguments forwarded whole; returns its text like the platform verbs","","Observation changes use + for added controls, - for removed controls, and ~ for a surviving ref whose state or label changed. Scope and grouping explanations repeat only when their context changes; observe() still returns the complete available data.","The changed/frozen verdict compares this run's end with the previous run's end: a run whose last action puts the","surface back where it started reads as unchanged \u2014 return your own intermediate observe() results when you need proof.","","Rules that are enforced, not advised:"," \xB7 one verb in flight \u2014 a second one before the first resolves is refused by name (one focus, one caret)"," \xB7 a ref read on surface A is refused on surface B \u2014 act on it through A's handle"," \xB7 a handle whose surface is gone throws: re-acquire with computer.surfaces()"," \xB7 100 actions and 60 s per call; reads, find(), waits, verification and computer.log() also spend this budget"," \xB7 nothing outside this API exists: no require, import, fetch, process, timers, console","","Persistence: top-level const/let/var/function/class declarations live until computer_repl_reset. Keep a","handle in one and reuse it; collect results in a variable across calls; return only what you need.","Batch at verified task boundaries, with room for observation and recovery. A helper's nested verbs also count: a loop of 35 find/setText/expect sequences exceeds 100 actions. Split it into smaller calls.","Keep a top-level checkpoint after each confirmed stage. After an uncertain save/submit, inspect or expect its result before resuming; replaying the whole batch can submit twice. A dispatched checkpoint is not proof of success. Reset or runtime restart loses checkpoints, so recover from surface facts then.","Returned values are capped at 12000 characters and a cut JSON value is not parseable. Read a large result once into a top-level variable, return a small slice per call, and check the final count/identity; do not re-read or replay the task to retrieve the next slice."]}}function b(e){const a=l(e);return[...a.head,...a.reads,...a.actions,...a.extras,...a.tail].join(`
2
+ `)}function l(e){const a=e.extraVerbs();return{head:["API reference \u2014 computer_repl (this text comes once per session; computer.help() returns it again)","","Everything is async \u2014 the verbs AND computer.surfaces() / computer.surface(\u2026): await each. A verb that","fails THROWS with the reason, which stops the run; the journal in the result says what ran. Refs (e12)","come from observe()/find() and stay valid until the surface changes; never invent one, and never carry one in from","another session \u2014 a fresh session re-mints them, so the same e12 names a different element. A target `t` is "+"a find() handle, a ref string, or a point {x, y} read off a screenshot"+".","","Bind a surface first \u2014 every verb below is a method on the handle it returns, never a name of its own:",' await computer.surfaces() \u2192 [handle] \u2014 every surface (tab / window / app) as {id, kind, title, active}; `id` is an opaque key, what the platform knows the surface by lives in `title` (macOS: "TextEdit (com.apple.TextEdit) \u2014 Untitled"; Windows: the window title, then the program name \u2014 "Untitled - Notepad \u2014 Notepad")'," await computer.surface(name) \u2192 handle \u2014 by id, by whole title, or by a piece of the title; several matches or none is a refusal that lists the candidates, never a guess",' await computer.surface() \u2192 handle \u2014 the ONE surface, when there is only one. Not "whatever is in front": with more than one open it refuses and lists them, so a script always says which it meant'," Bind once per surface and keep the handle in a top-level declaration \u2014 it stays valid for later calls, and re-binding costs a read each time."," A browser tab marked unloaded:true is still discoverable. Using its handle restores that exact tab in the background after acquiring ownership; merely listing tabs never reloads it.",""],reads:[...e.interactionNotes(),"Reads (return data to your code; none of them is shown to you except through `return` or computer.log):",e.readsOffscreen?" w.observe({screenshot?}) \u2192 {url, title, elements:[{ref, role, name, state, rect, offscreen?}], groups?, headings?, partial?, coverage, target?} \u2014 every control, viewport and beyond; filter the array rather than asking it to narrow. `rect` {x, y, w, h} is the control's box \u2014 compare boxes to answer layout questions (which row a control sits on, whether it moved) without a screenshot. `groups` is the page's OWN labelling of those controls \u2014 [{label, members:[ref]}] over that same set, off-viewport members included \u2014 present where the author wrote one, and the only place some facts exist (a feed labels each post's button row with its counts, bookmarks included, which no control name and no page text carries). `headings` (web surfaces) is [{name, level, rect}] for the page's headings, never listed as controls \u2014 the fixed point to measure a control's box against, since a click can scroll the container under every control in a card. `partial` (a reason) says the read stopped before covering the surface, so what it did not list may still be there. screenshot:true also attaches the picture of the same screen (one call, two reads), for when you need to SEE where something is":" w.observe({screenshot?}) \u2192 {url, title, elements:[{ref, role, name, state, rect}], coverage} \u2014 every control the window SHOWS (`rect` {x, y, w, h} is its box \u2014 compare boxes to answer layout questions without a screenshot); one scrolled out of view is counted in the observation and never listed, so no ref names it until a scroll brings it in; filter the array rather than asking it to narrow. screenshot:true also attaches the picture of the same screen"," coverage reports exact {offscreen, covered, capped, modal?} counts (null when unreported) even when repeated context notes are omitted. Electron target reports the CDP endpoint, windowId and, when its user agent carries them, the app's own name/version; never infer the app version from the tool or helper path."," w.find(name, {role?, within?, "+(e.readsOffscreen?"visible?, ":"")+"near?}) \u2192 {ref, name, matches, others, incidental?} | null \u2014 by visible name, "+(e.readsOffscreen?"whole surface":"everything the window shows \u2014 NOT what is scrolled out of it, which this platform's read does not contain")+" (an exact name beats a partial one, a partial the query is most of beats one it is only a fragment of, and a control beats a text label); a handle that re-resolves itself when used, so a loop survives a re-render; `matches` says how many controls carried the name and `others` names the runners-up, so an ambiguous name is visible BEFORE you act on the pick; `incidental: true` says NOTHING here is NAMED that and the pick is only a long name that happens to contain the word \u2014 the one `matches: 1` not to trust; narrow it with role (button / textbox / \u2026), within (a ref or handle \u2014 matches inside that element's box, e.g. one sheet or one message)"+(e.readsOffscreen?" visible (on screen only)":"")+" and near (a labelled group restricts candidates to its members; otherwise prefer the control closest to the named element). Duplicate group labels or same-named controls within a group still need disambiguation"," w.text() \u2192 the surface's full readable text",...e.readValue?[" w.value(t) \u2192 ONE control's whole value as a string \u2014 a long field, a clipped label, the text you are about to edit. The listing shows a value cut to what a line holds; this is the rest of it, for one element, where text() re-reads the entire surface"]:[],...e.readPage?[" w.page({ref?, depth?, node?}) \u2192 the content tree as text, with a completeness receipt on its first line"]:[]," w.screenshot({ref?, padding?|region:{x,y,width,height}?|fullPage?, path?}) \u2192 {path, host, width, height, note, capture?} \u2014 the image is attached to this result for you to see AND written to a file the code can keep: to `path` (absolute, never overwrites an existing file, extension must match the surface's format \u2014 .jpg on web surfaces, .png on macOS and Windows) or else to a rolling temp directory; `host` names the machine the path is on; no image \u2192 {path: null, note: why}. ref is a ref string and crops to a visible element's box; padding keeps its surroundings. fullPage is web-only. On macOS and Windows, ref/region preserve native detail; region uses FULL screenshot pixels, so take a full screenshot first. Windows padding and crop bounds use physical pixels; macOS padding uses full-screenshot pixels when available, otherwise window points. capture reports the window, crop, units, pointsPerPixel, and toFullScreenshot (null without a full screenshot or when window geometry changed). Windows refuses a region based on old window geometry; take a new full screenshot. Convert crop pixel p to full screenshot coordinates with toFullScreenshot.origin + p * toFullScreenshot.scale on each axis. A crop never changes point-action coordinates; use a current mapping or act by ref.",...e.supportsDiagnostics?[` w.diagnostics({id?}) \u2192 ${e.diagnosticsAbout.empty} \u2014 the newest 20, with the ones new since your last read marked "new"`]:[]," computer.log(\u2026) \u2192 a line in the journal; the one member that is not on a surface",""],actions:["Actions (every one is w.verb(\u2026) on a bound handle, which switches to that surface first):"," w.click(t, {button?, count?}) w.hover(t)"," w.setText(t, text) \u2014 replace the target field's ENTIRE value, even after selectText; never a selection/caret edit. Returns true when read-back matches; throws WriteVerificationError on mismatch, stopping the run unless caught."," w.setTextBestEffort(t, text) \u2014 the same whole-field replacement, returning true/false without throwing on mismatch. Both return undefined when read-back is unavailable; that is not verification."," click and hover also take a POINT \u2014 w.click({x, y}) \u2014 in the coordinates of the last screenshot, for a control you"," can see and cannot name. It checks no identity (no stale-ref gate, no name, no surface check) and so acts on"," whatever is at that spot NOW: take observe({screenshot: true}) and read the coordinates off that picture."," click / hover / keys / scroll / focus answer with what they did, as text \u2014 the same note the journal shows, e.g. which"," rung a click ended on. A verb that FAILS throws instead, so a returned string always describes something that happened."," w.keys(chord, count?, {focus?}) e.g. w.keys('Enter'), w.keys('Backspace', 3), w.keys('Enter', {focus: field}) \u2014 one key or chord, never a text string. setText replaces a whole field; use the platform's paste/type verb to edit at a selection or caret."," keys go wherever the surface has the keyboard focus. Pass focus: <target> to REQUIRE it be there: if it is not,"," nothing is pressed, including when focus is unknown or ambiguous. Omit focus only to send without that check. Focusing stays explicit (click / setText / selectText with a caret)."," w.scroll({direction?: up|down|left|right, count?: screens (default 1), to?: top|bottom, ref?}) w.wait({} | {ms} | {text} | {text_gone} | {control} | {control_gone} | {url} | {url_gone} | {loaded}) \u2192 ok"," count covers that many screens in ONE call \u2014 the note says how far it actually got, which is less once the end is reached;"," count and to: are refused together, since to: already goes as far as the surface scrolls."," Browser/Electron list scrollable regions with refs. With several regions, pass ref:<region or a control inside it \u2014 a ref or a find() handle>; a bare scroll refuses and lists the choices. ref:'window' explicitly scrolls the page. Reaching an edge never switches to another region.",...e.scrollsInto?[" w.scroll({into: t}) instead brings ONE control (a ref or a find() handle) to the middle of the viewport and says whether anything still covers it. It takes a covered control's ref too \u2014 the way out from under a sticky header or a bottom strip that a screen of scrolling overshoots."]:[]," a wait polls until the condition holds or it times out, and says which: {text\u2026} searches the readable text,",' {control\u2026} the CONTROLS by name ("until the Stop button is gone" \u2014 the honest way to wait out a long answer),'," {url}/{url_gone} where the surface IS (substring, so a query string needs no spelling out), and {loaded:true} that"," the DOCUMENT finished. Reach for it before reporting that a page has nothing on it: a document that never finishes"," reads as an empty page and no content condition can tell you which it was. It is NOT rendered \u2014 a single-page app"," reaches loaded with nothing drawn yet (measured), so follow it with the CONTENT wait, which is w.wait({}):",' it polls until the readable text stops changing, so it is the one "the page is ready" condition that needs no word'," from the page and no knowledge of the site. Use it after a navigation, a tab switch inside a page, or a search \u2014 a"," single-page app mounts its rows well after {loaded}, and an observation taken before that reads as an empty page."," Also after a click that REDRAWS content (a chart, a list) and before you screenshot or read it: the control's own state"," updates at once, the content a beat later, so a read straight after the click can pair the new button with the old view."," Every condition takes ms as its OWN polling budget (default 16s): w.wait({text:'Done', ms:3000}) gives up in three seconds,"," give or take the one read already in flight when it runs out \u2014 on a heavy surface a single read is seconds, and the result says so when that happened."," wait() returns true/false; awaiting it alone does NOT stop on timeout. Use expect() for a required postcondition, or check the result: if (!await w.wait({...})) throw new Error('condition not met');"," which is what a GUESS at the wording is worth \u2014 an unguarded guess that no page ever matches costs the full 16s."," w.focus() \u2014 select w as the target for this runtime; target selection is not an OS-activation guarantee (see platform interaction notes)",` w.release() \u2014 release this target and its input resources for other sessions. Claims span calls until release, reset, turn end, process exit or ${p/6e4} minutes with no call (the next call takes the target back unless another session has it by then). Observe again before acting after handoff; old refs are invalidated. Separate windows can still share app/account/document state; ownership does not isolate those or a human editor.`," w.expect({text, control, url, value, equals, ms}) \u2014 verify exactly ONE condition; value takes a ref/find handle and equals:string. Read-only polling, default 3000 ms, maximum 16000 ms. Returns true when verified; otherwise throws before subsequent steps. It never repeats the preceding action. Use it to verify save/submit results, not screen-change verdicts."," w.setText(t, text, {ifValue:previousValue}) \u2014 optional precondition: refuse before writing if the field changed or cannot be read. This is a checked precondition, not an atomic transaction with a human or another app.","Action journal evidence: [verified] confirms the stated value/condition; [dispatched] only confirms delivery; [blocked] means no dispatch; [uncertain] may already have acted. Never replay an uncertain send/submit/delete; inspect its actual result first."],extras:[...a.length?[" This platform's verbs \u2014 each returns what it did as text (the journal keeps only its first line, so a listing is read from the return value):"," In these signatures, ? marks an optional property; every unmarked property is required. Omit the ? in JavaScript calls.",...a.map(s=>f(s.kind,s.schema))]:[],...a.some(s=>s.kind==="select_option")?[" selectOption example: const choice = await w.find('Priority', {role:'combobox'}); if (!choice || choice.matches !== 1 || choice.incidental) throw new Error('Priority is missing or ambiguous'); await w.selectOption({ref:choice.ref, option:'High'});"]:[],...a.some(s=>s.kind==="run_js")?[' runJs is the exception: it returns the VALUE the code produced, whole (the journal shows a short preview); an exception thrown in the page comes back as the string "threw: \u2026" instead of stopping the run.']:[],...a.some(s=>s.kind==="download")?[` download also returns a VALUE: const watch = await w.download(); await w.click(await w.find('Download')); const result = await w.download({id:watch.id}); return result; result.downloads carries the actual path, filename, host and state. Read/upload only complete receipts. Query the same ID after a timeout; several candidates mean ambiguous, never the newest file. Re-arming this tab ends its previous collection; stop:true explicitly ends collection without canceling any download. On this session's own tabs a page's save dialog (showSaveFilePicker) never opens: its file arrives as a download marked source "save-picker".`]:[]],tail:[" w.act(kind, {\u2026}) \u2014 any verb by its wire name, arguments forwarded whole; returns its text like the platform verbs","","Observation changes use + for added controls, - for removed controls, and ~ for a surviving ref whose state or label changed. Scope and grouping explanations repeat only when their context changes; observe() still returns the complete available data.","The changed/frozen verdict compares this run's end with the previous run's end: a run whose last action puts the","surface back where it started reads as unchanged \u2014 return your own intermediate observe() results when you need proof.","","Rules that are enforced, not advised:"," \xB7 one verb in flight \u2014 a second one before the first resolves is refused by name (one focus, one caret)"," \xB7 a ref read on surface A is refused on surface B \u2014 act on it through A's handle"," \xB7 a handle whose surface is gone throws: re-acquire with computer.surfaces()"," \xB7 100 actions and 60 s per call; reads, find(), waits, verification and computer.log() also spend this budget"," \xB7 nothing outside this API exists but URL and URLSearchParams: no require, import, fetch, process, timers, console","","Persistence: top-level const/let/var/function/class declarations live until computer_repl_reset. Keep a","handle in one and reuse it; collect results in a variable across calls; return only what you need.","Batch at verified task boundaries, with room for observation and recovery. A helper's nested verbs also count: a loop of 35 find/setText/expect sequences exceeds 100 actions. Split it into smaller calls.","Keep a top-level checkpoint after each confirmed stage. After an uncertain save/submit, inspect or expect its result before resuming; replaying the whole batch can submit twice. A dispatched checkpoint is not proof of success. Reset or runtime restart loses checkpoints, so recover from surface facts then.","Returned values are capped at 12000 characters and a cut JSON value is not parseable. Read a large result once into a top-level variable, return a small slice per call, and check the final count/identity; do not re-read or replay the task to retrieve the next slice."]}}function b(e){const a=l(e);return[...a.head,...a.reads,...a.actions,...a.extras,...a.tail].join(`
3
3
  `)}function y(e){const a=l(e);return[`Reaching a second platform: ${e.platform} \u2014 everything else in the reference above still holds, and these are the reads and verbs that are its own.`,"",...a.reads,...a.extras].join(`
4
4
  `)}export{g as REPL_DESCRIPTION,v as RESET_DESCRIPTION,b as apiReference,y as platformReference};
@@ -1,19 +1,19 @@
1
- import{createContext as Ue,runInContext as Fe}from"node:vm";import{Type as f}from"typebox";import{spec as ve,text as x}from"../types.js";import{MAX_ACTIONS as be,MAX_MS as ye,SYNC_MAX_MS as $e,RETURN_CAP as ie,MAX_REF_OWNERS as Je,trimObservation as Be}from"./repl-budget.js";import{createComputerUseApi as He,noteOf as Se,renderJournal as We,stringify as qe,verbGate as Ge}from"./api.js";import{prepareCode as Ke}from"./repl-code.js";import{apiReference as Re,platformReference as Xe,REPL_DESCRIPTION as ze,RESET_DESCRIPTION as Ye}from"./repl-docs.js";import{ComputerUseOwnership as Qe,IDLE_RELEASE_MS as Ve,OwnershipBusyError as ke}from"../ownership.js";const Ee=new Set(["observe","find","text","value","page","screenshot","diagnostics","targets","log","verb","release"]),Ze=new Set(["dialog","close_tab"]);function ct(F,D={}){if(!F.length)throw new Error("createReplTools needs at least one platform");let M=null;const b=D.ownership??new Qe,y=new WeakMap,X=new WeakMap;let N=null,j=!1,oe=Promise.resolve(),J=!1,C=null,B=null;const Te=D.idleReleaseMs??Ve;let H=null,P=null,$,z=!!D.docsDelivered,Y=!!D.docsDelivered,I=null;const k=()=>{if(!I)throw new Error("no computer_repl call is in progress \u2014 every verb must be awaited inside the call that started it");return I},v=new Map,O=new Map,L=new WeakMap;for(const{driver:e}of F)e.onDisconnect(async()=>{for(const n of O.values()){if(L.get(n)?.slice.driver!==e)continue;const t=y.get(n);t&&await b.release(t),y.delete(n);for(const[i,s]of v)s===n.id&&v.delete(i)}});const Q=F.length>1;let m=null,S=null;const Oe=e=>{const n=m?.id;if(n){for(const t of e)v.delete(t),v.set(t,n);for(const t of v.keys()){if(v.size<=Je)break;v.delete(t)}}},_e=e=>{if(!m)return;const n=v.get(e);if(n&&n!==m.id)throw new Error(`ref ${e} was read on surface ${n}, not on ${m.id} (${m.title}) \u2014 act on it through computer.surface('${n}'), or observe ${m.id} and use one of its refs`)},V=Ge(e=>I?.push("verb","",!1,e)),u=F.map(e=>({...e,id:e.driver.platform,api:He(e.driver,k,{onRefs:Oe,onTarget:_e,beforeAction:async(n,t)=>{if(!m)throw new Error("An action needs a bound surface.");const i=y.get(m);if(!i)throw new Error("The target was released; observe it before acting again.");try{await b.claim(i,await e.driver.actionResources(n,t))}catch(s){throw s instanceof ke&&(N=s.message),s}}}),docsDue:!D.docsDelivered,lastMiss:null}));if($=u[0],Q)for(const e of u)e.driver.surfaceIdPrefix=`${e.id}/`;const ae=u[0].api,Z=async()=>{const e=[],n=S?[S]:u;for(const t of n){let i;try{i=await t.driver.listTargets()}catch(s){if(n.length===1)throw s;const l=s instanceof Error?s.message:String(s);t.lastMiss!==l&&I?.push("targets",t.id,!1,`${t.id} is not reachable: ${l}`),t.lastMiss=l;continue}t.lastMiss=null;for(const s of i)e.push({...s,id:Q?`${t.id}/${s.id}`:s.id,slice:t,inner:s.id})}return e};class ee extends Error{}const te=async e=>{const n=L.get(e);if($=n.slice,P=e,await n.slice.driver.isActiveTarget(n.inner)){await le(e,!0);return}k().charge("focus");const t=await n.slice.driver.focusTarget(n.inner);if(k().push("focus",e.id,t.ok,Se(t)||`\u2192 ${e.title}`),!t.ok)throw O.delete(e.id),new ee(`surface ${e.id} (${e.title}) is gone from this session \u2014 closed, replaced or out of reach; when that is known, a NOTE in this result or the one before it names which (it is said once, when the loss is seen). Call computer.surfaces() and re-acquire what you need.`);await le(e,!1)},le=async(e,n)=>{const{slice:t,inner:i}=L.get(e),s=t.driver;let l=await s.ownershipResources();const c=y.get(e);if((!c||c!==l[0]?.key)&&!await s.isActiveTarget(i)){const d=await s.focusTarget(i);if(!d.ok)throw new ee(`Surface ${e.id} disappeared while binding; list surfaces and re-acquire it.`);if(k().push("focus",e.id,!0,Se(d)||`\u2192 ${e.title}`),l=await s.ownershipResources(),!await s.isActiveTarget(i))throw new Error("The target kept changing while binding; observe before continuing.")}const h=l[0]?.key;if(!h)throw new Error("The driver did not identify a resource to claim.");const E=!!c&&c!==h;if(E&&!l.some(d=>X.get(e)?.has(d.key)))throw O.delete(e.id),y.delete(e),X.delete(e),await b.release(c),new Error("This handle now resolves to a different target. List surfaces and bind the intended one again.");try{await b.claim(h,l)}catch(d){throw d instanceof ke&&(N=d.message),d}E&&await b.release(c),y.set(e,h),X.set(e,new Set(l.map(d=>d.key))),!c&&!n&&s.invalidateObservation(),await s.prepareTarget()},re=e=>{const n=O.get(e.id);if(n)return n.title=e.title,n.kind=e.kind,n.active=e.active,n.own=e.own,n.mine=e.mine,n.unloaded=e.unloaded,n;const t={id:e.id,kind:e.kind,title:e.title,active:e.active,...e.own===void 0?{}:{own:e.own},...e.mine===void 0?{}:{mine:e.mine},...e.unloaded?{unloaded:!0}:{}};L.set(t,{slice:e.slice,inner:e.inner});const i={};for(const[s,l]of Object.entries(e.slice.api))typeof l!="function"||s==="log"||s==="focus"||s==="targets"||(i[s]=async(...c)=>{m=t;try{try{await te(t)}catch(h){if(s==="wait"&&h instanceof ee)return`${t.id} is gone \u2014 closed or replaced, so there is nothing left on it to wait for. computer.surfaces() lists what remains`;throw h}return await l(...c)}finally{m=null}});return i.focus=async()=>{m=t;try{await te(t)}finally{m=null}},i.release=async()=>{k().charge("release");const s=y.get(t);await e.slice.driver.releaseInputResources(),s&&await b.release(s),y.delete(t),e.slice.driver.invalidateObservation();for(const[l,c]of v)c===t.id&&v.delete(l);return k().push("release",t.id,!0,"ownership released; observe again before continuing"),"Ownership released. Other sessions may now use this target; observe before continuing."},Object.assign(t,V(i)),O.set(e.id,t),t},xe=(e,n)=>{const t=a=>a.mine?" (yours \u2014 opened by this session)":a.own&&a.mine===!1?" (left open by an earlier session)":"",i=a=>a.mine?0:a.own?1:2,s=a=>[...a].sort((p,T)=>i(p)-i(T)).map(p=>` ${p.id} ${p.title}${t(p)}`).join(`
2
- `);if(!e.length)throw new Error("this driver has no surface right now");if(n===void 0){if(e.length===1)return e[0];const a=e.filter(T=>T.mine),p=a.filter(T=>T.active);if(p.length===1)return p[0];if(a.length===1)return a[0];throw new Error(`${e.length} surfaces are open \u2014 name one, computer.surface('<id or title>'):
3
- ${s(e)}`)}const l=String(n).trim(),c=e.filter(a=>a.id===l);if(c.length===1)return c[0];const h=l.toLowerCase(),E=e.filter(a=>a.title.toLowerCase()===h),d=E.length?E:e.filter(a=>a.title.toLowerCase().includes(h));if(d.length===1)return d[0];const q=d.filter(a=>a.primary);if(q.length===1)return q[0];if(!d.length){const a=[...new Set(u.map(p=>p.driver.explainMiss(l)).filter(Boolean))].join(`
4
- `);throw new Error(`no surface matches ${JSON.stringify(l)} \u2014 the open ones are:
5
- ${s(e)}${a?`
6
- ${a}`:""}`)}throw new Error(`${d.length} surfaces match ${JSON.stringify(l)} \u2014 name one exactly:
7
- ${s(d)}`)},Ce=async e=>{try{return await ce(e)}catch(n){const t=e.toLowerCase(),i=(await Z()).filter(s=>s.active&&(s.id===e||s.title.toLowerCase().includes(t)));if(i.length===1)return i[0];throw n}},ce=async e=>{const n=await Z(),t=e===void 0?"":String(e).trim();if(t&&!n.some(i=>i.id===t||i.title.toLowerCase().includes(t.toLowerCase())))for(const i of S?[S]:u){const s=await i.driver.unlistedTarget(t);if(s)return{...s,id:Q?`${i.id}/${s.id}`:s.id,slice:i,inner:s.id}}return xe(n,e)},Ie=V({surfaces:async()=>{k().charge("targets");const e=await Z();return k().push("targets","",!0,`${e.length} surfaces`),e.map(re)},surface:async e=>re(await ce(e)),log:ae.log,help:()=>H=u.map(e=>Re(e.driver)).join(`
1
+ import{createContext as Be,runInContext as We}from"node:vm";import{Type as h}from"typebox";import{spec as ve,text as L}from"../types.js";import{MAX_ACTIONS as be,MAX_MS as ye,SYNC_MAX_MS as $e,RETURN_CAP as oe,MAX_REF_OWNERS as qe,trimObservation as Ge}from"./repl-budget.js";import{createComputerUseApi as Ke,noteOf as Se,renderJournal as Xe,stringify as ze,verbGate as Ye}from"./api.js";import{prepareCode as Qe}from"./repl-code.js";import{apiReference as Re,platformReference as Ve,REPL_DESCRIPTION as Ze,RESET_DESCRIPTION as et}from"./repl-docs.js";import{ComputerUseOwnership as tt,IDLE_RELEASE_MS as nt,OwnershipBusyError as Ee}from"../ownership.js";const ke=new Set(["observe","find","text","value","page","screenshot","diagnostics","targets","log","verb","release"]),rt=new Set(["dialog","close_tab"]);function ht(B,D={}){if(!B.length)throw new Error("createReplTools needs at least one platform");let M=null;const E=D.ownership??new tt,m=new WeakMap,W=new WeakMap;let j=null,P=!1,ae=Promise.resolve(),q=!1,I=null,G=null;const Te=D.idleReleaseMs??nt;let K=null,N=null,k,Z=!!D.docsDelivered,ee=!!D.docsDelivered,C=null;const T=()=>{if(!C)throw new Error("no computer_repl call is in progress \u2014 every verb must be awaited inside the call that started it");return C},v=new Map,$=new Map,S=new WeakMap;for(const{driver:e}of B)e.onDisconnect(async()=>{for(const n of $.values()){if(S.get(n)?.slice.driver!==e)continue;const t=m.get(n);t&&await E.release(t),m.delete(n);for(const[s,i]of v)i===n.id&&v.delete(s)}});const U=B.length>1;let b=null,R=null;const Oe=e=>{const n=b?.id;if(n){for(const t of e)v.delete(t),v.set(t,n);for(const t of v.keys()){if(v.size<=qe)break;v.delete(t)}}},_e=e=>{if(!b)return;const n=v.get(e);if(n&&n!==b.id)throw new Error(`ref ${e} was read on surface ${n}, not on ${b.id} (${b.title}) \u2014 act on it through computer.surface('${n}'), or observe ${b.id} and use one of its refs`)},te=Ye(e=>C?.push("verb","",!1,e)),d=B.map(e=>({...e,id:e.driver.platform,api:Ke(e.driver,T,{onRefs:Oe,onTarget:_e,beforeAction:async(n,t)=>{if(!b)throw new Error("An action needs a bound surface.");const s=m.get(b);if(!s)throw new Error("The target was released; observe it before acting again.");try{await E.claim(s,await e.driver.actionResources(n,t))}catch(i){throw i instanceof Ee&&(j=i.message),i}}}),docsDue:!D.docsDelivered,lastMiss:null}));if(k=d[0],U)for(const e of d)e.driver.surfaceIdPrefix=`${e.id}/`;const le=d[0].api,F=async()=>{const e=[],n=R?[R]:d;for(const t of n){let s;try{s=await t.driver.listTargets()}catch(i){if(n.length===1)throw i;const a=i instanceof Error?i.message:String(i);t.lastMiss!==a&&C?.push("targets",t.id,!1,`${t.id} is not reachable: ${a}`),t.lastMiss=a;continue}t.lastMiss=null;for(const i of s)e.push({...i,id:U?`${t.id}/${i.id}`:i.id,slice:t,inner:i.id})}return e};class H extends Error{}const ne=async e=>{const n=S.get(e);if(k=n.slice,N=e,await n.slice.driver.isActiveTarget(n.inner)){await re(e,!0);return}T().charge("focus");const t=await n.slice.driver.focusTarget(n.inner);if(T().push("focus",e.id,t.ok,Se(t)||`\u2192 ${e.title}`),!t.ok)throw $.delete(e.id),new H(`surface ${e.id} (${e.title}) is gone from this session \u2014 closed, replaced or out of reach; when that is known, a NOTE in this result or the one before it names which (it is said once, when the loss is seen). Call computer.surfaces() and re-acquire what you need.`);await re(e,!1)},re=async(e,n)=>{const{slice:t,inner:s}=S.get(e),i=t.driver;let a=await i.ownershipResources();const l=m.get(e);if((!l||l!==a[0]?.key)&&!await i.isActiveTarget(s)){const f=await i.focusTarget(s);if(!f.ok)throw new H(`Surface ${e.id} disappeared while binding; list surfaces and re-acquire it.`);if(T().push("focus",e.id,!0,Se(f)||`\u2192 ${e.title}`),a=await i.ownershipResources(),!await i.isActiveTarget(s))throw new Error("The target kept changing while binding; observe before continuing.")}const u=a[0]?.key;if(!u)throw new Error("The driver did not identify a resource to claim.");const O=!!l&&l!==u;if(O&&!a.some(f=>W.get(e)?.has(f.key)))throw $.delete(e.id),m.delete(e),W.delete(e),await E.release(l),new Error("This handle now resolves to a different target. List surfaces and bind the intended one again.");try{await E.claim(u,a)}catch(f){throw f instanceof Ee&&(j=f.message),f}O&&await E.release(l),m.set(e,u),W.set(e,new Set(a.map(f=>f.key))),!l&&!n&&i.invalidateObservation(),await i.prepareTarget()},xe=new Set(["openUrl","newTab"]),Le=async e=>{const{slice:n,inner:t}=S.get(e);return await n.driver.heldNothing()?t?!(await n.driver.listTargets()).some(s=>s.id===t):!0:!1},Ie=async e=>{const n=S.get(e),t=(await n.slice.driver.listTargets()).find(a=>a.active);if(!t)throw new Error("the tab was opened, but none is active afterwards \u2014 computer.surfaces() lists what is open");const s=m.get(e);s&&await E.release(s),m.delete(e),W.delete(e);for(const[a,l]of v)l===e.id&&v.delete(a);$.get(e.id)===e&&$.delete(e.id);const i=U?`${n.slice.id}/${t.id}`:t.id;Object.assign(e,{id:i,kind:t.kind,title:t.title,active:!0,own:t.own,mine:t.mine}),S.set(e,{slice:n.slice,inner:t.id}),$.set(i,e),k=n.slice,N=e,await re(e,!0)},Ne=async()=>{const e=await F();if(e.some(n=>n.mine))return null;for(const n of R?[R]:d){const t=await n.driver.heldNothing();if(t==="closed"||t==="not-yet"&&!e.length)return X({id:U?`${n.id}/new`:"new",kind:"tab",title:"no tab open \u2014 openUrl({url}) or newTab({url}) on this handle opens one",active:!1,own:!0,mine:!0,slice:n,inner:""})}return null},X=e=>{const n=$.get(e.id);if(n)return n.title=e.title,n.kind=e.kind,n.active=e.active,n.own=e.own,n.mine=e.mine,n.unloaded=e.unloaded,n;const t={id:e.id,kind:e.kind,title:e.title,active:e.active,...e.own===void 0?{}:{own:e.own},...e.mine===void 0?{}:{mine:e.mine},...e.unloaded?{unloaded:!0}:{}};S.set(t,{slice:e.slice,inner:e.inner});const s={};for(const[i,a]of Object.entries(e.slice.api))typeof a!="function"||i==="log"||i==="focus"||i==="targets"||(s[i]=async(...l)=>{b=t;try{if(xe.has(i)&&await Le(t)){m.has(t)||m.set(t,`opening:${t.id}`);const u=await a(...l);return await Ie(t),u}if(!S.get(t).inner)throw new Error(`${t.id} holds no tab yet \u2014 openUrl({url}) or newTab({url}) on it opens one; nothing else can act until then`);try{await ne(t)}catch(u){if(i==="wait"&&u instanceof H)return`${t.id} is gone \u2014 closed or replaced, so there is nothing left on it to wait for. computer.surfaces() lists what remains`;throw u instanceof H&&await S.get(t).slice.driver.heldNothing()?new H(`${u.message} This session holds no tab now: openUrl({url}) or newTab({url}) on this handle opens a fresh one.`):u}return await a(...l)}finally{b=null}});return s.focus=async()=>{b=t;try{await ne(t)}finally{b=null}},s.release=async()=>{T().charge("release");const i=m.get(t);await e.slice.driver.releaseInputResources(),i&&await E.release(i),m.delete(t),e.slice.driver.invalidateObservation();for(const[a,l]of v)l===t.id&&v.delete(a);return T().push("release",t.id,!0,"ownership released; observe again before continuing"),"Ownership released. Other sessions may now use this target; observe before continuing."},Object.assign(t,te(s)),$.set(e.id,t),t},Ce=(e,n)=>{const t=c=>c.mine?" (yours \u2014 opened by this session)":c.own&&c.mine===!1?" (left open by an earlier session)":"",s=c=>c.mine?0:c.own?1:2,i=c=>[...c].sort((p,_)=>s(p)-s(_)).map(p=>` ${p.id} ${p.title}${t(p)}`).join(`
2
+ `);if(!e.length)throw new Error("this driver has no surface right now");if(n===void 0){if(e.length===1)return e[0];const c=e.filter(_=>_.mine),p=c.filter(_=>_.active);if(p.length===1)return p[0];if(c.length===1)return c[0];throw new Error(`${e.length} surfaces are open \u2014 name one, computer.surface('<id or title>'):
3
+ ${i(e)}`)}const a=String(n).trim(),l=e.filter(c=>c.id===a);if(l.length===1)return l[0];const u=a.toLowerCase(),O=e.filter(c=>c.title.toLowerCase()===u),f=O.length?O:e.filter(c=>c.title.toLowerCase().includes(u));if(f.length===1)return f[0];const Y=f.filter(c=>c.primary);if(Y.length===1)return Y[0];if(!f.length){const c=[...new Set(d.map(p=>p.driver.explainMiss(a)).filter(Boolean))].join(`
4
+ `);throw new Error(`no surface matches ${JSON.stringify(a)} \u2014 the open ones are:
5
+ ${i(e)}${c?`
6
+ ${c}`:""}`)}throw new Error(`${f.length} surfaces match ${JSON.stringify(a)} \u2014 name one exactly:
7
+ ${i(f)}`)},Ae=async e=>{try{return await ce(e)}catch(n){const t=e.toLowerCase(),s=(await F()).filter(i=>i.active&&(i.id===e||i.title.toLowerCase().includes(t)));if(s.length===1)return s[0];throw n}},ce=async e=>{const n=await F(),t=e===void 0?"":String(e).trim();if(t&&!n.some(s=>s.id===t||s.title.toLowerCase().includes(t.toLowerCase())))for(const s of R?[R]:d){const i=await s.driver.unlistedTarget(t);if(i)return{...i,id:U?`${s.id}/${i.id}`:i.id,slice:s,inner:i.id}}return Ce(n,e)},De=te({surfaces:async()=>{T().charge("targets");const e=await F();return T().push("targets","",!0,`${e.length} surfaces`),e.map(X)},surface:async e=>(e===void 0?await Ne():null)??X(await ce(e)),log:le.log,help:()=>K=d.map(e=>Re(e.driver)).join(`
8
8
 
9
- `)}),Le=new Set(Object.keys(ae)),de=(e,n)=>{const t=/^(?:ReferenceError: )?(\w+) is not defined$/.exec(e);return t?Le.has(t[1])?`${e} \u2014 verbs live on a surface handle, not on the top level: const w = await computer.surface('<id or title>'); await w.${t[1]}(\u2026). computer.surface() with no argument binds the only surface, or the one this session opened among several; computer.surfaces() lists them.`:`${e} \u2014 `+(n?"this call started a FRESH runtime, so nothing declared in an earlier call survives (the server was restarted, or its host reaped it while idle). ":"no call on this runtime declared it at the top level, and nothing is predefined but computer. ")+"If it is a surface handle, bind it: const w = await computer.surface('<id or title>').":e},Ae=()=>Ue({computer:Ie,console:void 0}),De=e=>Object.entries(e).flatMap(([n,t])=>t&&typeof t=="object"&&L.has(t)&&O.get(t.id)===t?[`${n} \u2192 ${t.id} \xAB${t.title}\xBB`]:[]),W=ve({name:"computer_repl",label:"Computer REPL",description:ze,promptSnippet:"computer_repl runs JavaScript over the GUI in a persistent session: verbs, surface handles, one observation per call.",promptGuidelines:["Plan several actions per computer_repl call \u2014 one call costs one step and one observation whatever it does inside.","Keep surface handles and collected data in top-level declarations; they persist across calls.","Re-find by name inside a loop rather than reusing a ref you read earlier: find() handles re-resolve, raw refs do not survive a re-render."],parameters:f.Object({code:f.String({description:"JavaScript, run as the body of an async function in the persistent runtime. `await` every verb; `return` what you want back. Bind once \u2014 first call: `const w = await computer.surface();` \u2014 binding opens nothing (a browser page is opened with `await w.openUrl(url)` once bound), then later calls use `w` directly: `for (const e of (await w.observe()).elements.filter(e => e.role === 'checkbox')) await w.click(e.ref);`"}),purpose:f.Optional(f.String({maxLength:500,description:"Briefly describe the intended task when requesting app access. Shown to the user as the agent's stated purpose; it does not narrow the app permission. Do not include secrets."})),interaction:f.Optional(f.Union([f.Literal("background"),f.Literal("desktop")],{description:"Native apps default to background \u2014 macOS: driver mouse/activation/clipboard routes are blocked, including fallback; Windows: limited UIA setText/click/scroll by ref, with automatic focus and real-input fallback disabled; unsupported routes are refused. Use desktop only after explaining effects and obtaining permission covering this call. This option is not user consent."})),platform:f.Optional(f.String({description:'For a HOST that mounts several platforms in one session: the platform this call is about ("browser", "electron", "macos", "windows"). computer.surfaces() and computer.surface() then see only its surfaces, and the observation is of it; the other platforms stay mounted and untouched.'})),target:f.Optional(f.String({description:"Bind this surface before the code runs \u2014 the same string computer.surface() takes (an id, a whole title, or a piece of one). For a HOST that decides which surface a call is about: the code can still bind whatever it likes, and this only settles where a run that binds nothing lands."})),observation:f.Optional(f.Union([f.Literal("full"),f.Literal("brief"),f.Literal("none")],{description:'How much of the end-of-run observation to render. "full" (default) is the element list and the text excerpt; "brief" keeps the head (title, targets, verdict) and drops the listing; "none" leaves only the action journal. The observation is always TAKEN either way \u2014 the change verdict and the stale-ref bookkeeping are unaffected \u2014 and a NOTE line always survives, so a swallowed click is never hidden by this. Reach for it when the window holds content you do not want in the transcript, or when your own `return` value is the answer.'}))}),async execute(e){N=null,H=null;const n=String(e.code??"");if(!n.trim())return x("computer_repl needs code to run.",{error:"bad_args"});const t=String(e.platform??"").trim();if(S=t?u.find(r=>r.id===t)??null:null,t&&!S)return x(`platform ${JSON.stringify(t)} is not mounted in this session (mounted: ${u.map(r=>r.id).join(", ")}).`,{error:"bad_args"});S&&($=S);const i=e.purpose;for(const r of u)r.driver.interactionMode=e.interaction==="desktop"?"desktop":"background",r.driver.consentPurpose=typeof i=="string"&&i.trim().slice(0,500)||null,r.driver.permissionRefusal=null;const s=e.observation,l=s==="brief"||s==="none"?s:"full",c=[],h=[],E=Date.now(),d=()=>u.reduce((r,o)=>r+o.driver.waitedOnUserMs,0),q=d();let a=0,p=null,T=!1,G=null;const ue={fields:0,verified:0,ended:null};I={round:ue,charge(r){if(G??=Date.now(),j)throw new Error("The runtime was reset; this run stops before its next operation.");if(++a>be)throw new Error(`step cap reached (${be} actions) at ${r} \u2014 split the work across calls, or narrow the loop`);if(Date.now()-E-(d()-q)>ye)throw new Error(`time cap reached (${ye/1e3}s) at ${r} \u2014 the surface may be slower than the code assumes`)},push(r,o,R,_,we,ge){!we&&R&&!Ee.has(r)&&!Ze.has(r)&&(p=r),!we&&!R&&!Ee.has(r)&&(T=!0);const me=G===null?void 0:Date.now()-G;G=null,c.push({i:c.length+1,kind:r,target:o,ok:R,note:_,...ge?{outcome:ge}:{},...me!==void 0?{ms:me}:{}})},attach(r){h.push(r)}};const fe=!M;M??=Ae();let K,w=null;try{const r=String(e.target??"").trim();r&&await te(re(await Ce(r))),K=await Fe(Ke(n),M,{filename:"computer_repl",timeout:$e})}catch(r){r.code==="ERR_SCRIPT_EXECUTION_TIMEOUT"&&(r=new Error(`the code ran for ${$e/1e3}s without awaiting anything and was stopped \u2014 a loop that never awaits blocks the whole runtime; await a verb (or w.wait) inside it`)),w=de(r instanceof Error?`${r.name==="Error"?"":`${r.name}: `}${r.message}`:String(r),fe)}finally{try{await V.drain()}catch(r){w??=de(r instanceof Error?r.message:String(r),fe)}I=null}const he=Date.now()-E,pe=(he/1e3).toFixed(1),U=r=>({runMs:he,...r!==void 0?{observeMs:r}:{},steps:c.map(o=>({kind:o.kind,target:o.target,ok:o.ok,...o.outcome?{outcome:o.outcome}:{},...o.ms!==void 0?{ms:o.ms}:{},note:o.note.slice(0,160)}))}),g=[w?`Run STOPPED after ${c.length} steps (${pe}s): ${w}`:`Run completed ${c.length} steps in ${pe}s.`,...We(c)];if(w&&c.length>0&&g.push("Nothing was returned: the throw ended the call before `return`. What survives a stop \u2014 top-level bindings (read them in your next call) and anything already passed to log() (in the journal above)."),w){const r=De(M);r.length&&g.push(`Still bound: ${r.join(", ")} \u2014 use ${r.length===1?"it":"them"} directly in the next call.`)}if(K!==void 0){const r=qe(K);g.push(`Returned: ${r.length>ie&&K!==H?`${r.slice(0,ie)}\u2026 (cut at ${ie} of ${r.length} chars \u2014 return a slice, a count or a filtered list instead of the whole thing)`:r}`)}const A=u.filter(r=>r.docsDue&&(r===$||u.length===1)).map(r=>{r.docsDue=!1;const o=!z;return z=!0,{type:"text",text:`
10
- ${o?Re(r.driver):Xe(r.driver)}`,pinned:!0}});Y&&(Y=!1,A.push({type:"text",text:`
11
- This computer_repl runtime was restarted (idle), so top-level bindings and refs from before are gone; the API reference you already have still applies \u2014 computer.help() returns it again.`,pinned:!0})),p?$.driver.noteWriteRound(ue):T&&$.driver.noteContentRead();const Pe=p?{afterAction:p}:{},se=u.find(r=>r.driver.permissionRefusal)?.driver.permissionRefusal;if(H&&c.length===0){const r=x(g.join(`
12
- `),{verdict:"unknown",ledgerLines:g.length,timing:U(),...w?{error:"run_stopped"}:{}});return r.content.push(...A),r}if(se||N){const r=se||N;w?.includes(r)||g.push(`Observation skipped: ${r}`);const o=x(g.join(`
13
- `),{verdict:"unknown",ledgerLines:g.length,timing:U(),...w?{error:"run_stopped"}:{},...se?{permissionRefused:!0}:{ownershipRefused:!0}});return o.content.push(...h,...A),o}try{if(j||P&&(!y.has(P)||!await $.driver.isActiveTarget(L.get(P).inner))){const R=$.driver.takeNoteForResult(),_=x(`${g.join(`
14
- `)}${R?`
15
- NOTE: ${R}.`:""}
16
- Observation skipped: the bound target was released, closed or replaced. List surfaces and bind the intended target before continuing.`,{verdict:"unknown",ledgerLines:g.length,timing:U(),...w?{error:"run_stopped"}:{}});return _.content.push(...h,...A),_}const r=Date.now(),o=await $.driver.observeElements({prefix:g.join(`
17
- `),...Pe});return o.details.ledgerLines=g.length,o.details.timing=U(Date.now()-r),Be(o,l),o.content.push(...h,...A),w&&(o.details.error="run_stopped"),o}catch(r){if(r.surfaceGone)throw r;const o=r.message,R=w&&w.includes(o)?"for the same reason":`${o} \u2014 observe again before the next action`,_=x(`${g.join(`
9
+ `)}),Me=new Set(Object.keys(le)),de=(e,n)=>{const t=/^(?:ReferenceError: )?(\w+) is not defined$/.exec(e);return t?Me.has(t[1])?`${e} \u2014 verbs live on a surface handle, not on the top level: const w = await computer.surface('<id or title>'); await w.${t[1]}(\u2026). computer.surface() with no argument binds the only surface, or the one this session opened among several; computer.surfaces() lists them.`:`${e} \u2014 `+(n?"this call started a FRESH runtime, so nothing declared in an earlier call survives (the server was restarted, or its host reaped it while idle). ":"no call on this runtime declared it at the top level, and nothing is predefined but computer, URL and URLSearchParams. ")+"If it is a surface handle, bind it: const w = await computer.surface('<id or title>').":e},je=()=>Be({computer:De,console:void 0,URL,URLSearchParams}),Pe=e=>Object.entries(e).flatMap(([n,t])=>t&&typeof t=="object"&&S.has(t)&&$.get(t.id)===t?[`${n} \u2192 ${t.id} \xAB${t.title}\xBB`]:[]),z=ve({name:"computer_repl",label:"Computer REPL",description:Ze,promptSnippet:"computer_repl runs JavaScript over the GUI in a persistent session: verbs, surface handles, one observation per call.",promptGuidelines:["Plan several actions per computer_repl call \u2014 one call costs one step and one observation whatever it does inside.","Keep surface handles and collected data in top-level declarations; they persist across calls.","Re-find by name inside a loop rather than reusing a ref you read earlier: find() handles re-resolve, raw refs do not survive a re-render."],parameters:h.Object({code:h.String({description:"JavaScript, run as the body of an async function in the persistent runtime. `await` every verb; `return` what you want back. Bind once \u2014 first call: `const w = await computer.surface();` \u2014 binding opens nothing (a browser page is opened with `await w.openUrl({url})` once bound), then later calls use `w` directly: `for (const e of (await w.observe()).elements.filter(e => e.role === 'checkbox')) await w.click(e.ref);`"}),purpose:h.Optional(h.String({maxLength:500,description:"Briefly describe the intended task when requesting app access. Shown to the user as the agent's stated purpose; it does not narrow the app permission. Do not include secrets."})),interaction:h.Optional(h.Union([h.Literal("background"),h.Literal("desktop")],{description:"Native apps default to background \u2014 macOS: driver mouse/activation/clipboard routes are blocked, including fallback; Windows: limited UIA setText/click/scroll by ref, with automatic focus and real-input fallback disabled; unsupported routes are refused. Use desktop only after explaining effects and obtaining permission covering this call. This option is not user consent."})),platform:h.Optional(h.String({description:'For a HOST that mounts several platforms in one session: the platform this call is about ("browser", "electron", "macos", "windows"). computer.surfaces() and computer.surface() then see only its surfaces, and the observation is of it; the other platforms stay mounted and untouched.'})),target:h.Optional(h.String({description:"Bind this surface before the code runs \u2014 the same string computer.surface() takes (an id, a whole title, or a piece of one). For a HOST that decides which surface a call is about: the code can still bind whatever it likes, and this only settles where a run that binds nothing lands."})),observation:h.Optional(h.Union([h.Literal("full"),h.Literal("brief"),h.Literal("none")],{description:'How much of the end-of-run observation to render. "full" (default) is the element list and the text excerpt; "brief" keeps the head (title, targets, verdict) and drops the listing; "none" leaves only the action journal. The observation is always TAKEN either way \u2014 the change verdict and the stale-ref bookkeeping are unaffected \u2014 and a NOTE line always survives, so a swallowed click is never hidden by this. Reach for it when the window holds content you do not want in the transcript, or when your own `return` value is the answer.'}))}),async execute(e){j=null,K=null;const n=String(e.code??"");if(!n.trim())return L("computer_repl needs code to run.",{error:"bad_args"});const t=String(e.platform??"").trim();if(R=t?d.find(r=>r.id===t)??null:null,t&&!R)return L(`platform ${JSON.stringify(t)} is not mounted in this session (mounted: ${d.map(r=>r.id).join(", ")}).`,{error:"bad_args"});R&&(k=R);const s=e.purpose;for(const r of d)r.driver.interactionMode=e.interaction==="desktop"?"desktop":"background",r.driver.consentPurpose=typeof s=="string"&&s.trim().slice(0,500)||null,r.driver.permissionRefusal=null;const i=e.observation,a=i==="brief"||i==="none"?i:"full",l=[],u=[],O=Date.now(),f=()=>d.reduce((r,o)=>r+o.driver.waitedOnUserMs,0),Y=f();let c=0,p=null,_=!1,Q=null;const ue={fields:0,verified:0,ended:null};C={round:ue,charge(r){if(Q??=Date.now(),P)throw new Error("The runtime was reset; this run stops before its next operation.");if(++c>be)throw new Error(`step cap reached (${be} actions) at ${r} \u2014 split the work across calls, or narrow the loop`);if(Date.now()-O-(f()-Y)>ye)throw new Error(`time cap reached (${ye/1e3}s) at ${r} \u2014 the surface may be slower than the code assumes`)},push(r,o,y,x,we,ge){!we&&y&&!ke.has(r)&&!rt.has(r)&&(p=r),!we&&!y&&!ke.has(r)&&(_=!0);const me=Q===null?void 0:Date.now()-Q;Q=null,l.push({i:l.length+1,kind:r,target:o,ok:y,note:x,...ge?{outcome:ge}:{},...me!==void 0?{ms:me}:{}})},attach(r){u.push(r)}};const fe=!M;M??=je();let V,w=null;try{const r=String(e.target??"").trim();r&&await ne(X(await Ae(r))),V=await We(Qe(n),M,{filename:"computer_repl",timeout:$e})}catch(r){r.code==="ERR_SCRIPT_EXECUTION_TIMEOUT"&&(r=new Error(`the code ran for ${$e/1e3}s without awaiting anything and was stopped \u2014 a loop that never awaits blocks the whole runtime; await a verb (or w.wait) inside it`)),w=de(r instanceof Error?`${r.name==="Error"?"":`${r.name}: `}${r.message}`:String(r),fe)}finally{try{await te.drain()}catch(r){w??=de(r instanceof Error?r.message:String(r),fe)}C=null}const he=Date.now()-O,pe=(he/1e3).toFixed(1),J=r=>({runMs:he,...r!==void 0?{observeMs:r}:{},steps:l.map(o=>({kind:o.kind,target:o.target,ok:o.ok,...o.outcome?{outcome:o.outcome}:{},...o.ms!==void 0?{ms:o.ms}:{},note:o.note.slice(0,160)}))}),g=[w?`Run STOPPED after ${l.length} steps (${pe}s): ${w}`:`Run completed ${l.length} steps in ${pe}s.`,...Xe(l)];if(w&&l.length>0&&g.push("Nothing was returned: the throw ended the call before `return`. What survives a stop \u2014 top-level bindings (read them in your next call) and anything already passed to log() (in the journal above)."),w){for(const o of await F().catch(()=>[])){const y=$.get(o.id);y&&(y.title=o.title)}const r=Pe(M);r.length&&g.push(`Still bound: ${r.join(", ")} \u2014 use ${r.length===1?"it":"them"} directly in the next call.`)}if(V!==void 0){const r=ze(V);g.push(`Returned: ${r.length>oe&&V!==K?`${r.slice(0,oe)}\u2026 (cut at ${oe} of ${r.length} chars \u2014 return a slice, a count or a filtered list instead of the whole thing)`:r}`)}const A=d.filter(r=>r.docsDue&&(r===k||d.length===1)).map(r=>{r.docsDue=!1;const o=!Z;return Z=!0,{type:"text",text:`
10
+ ${o?Re(r.driver):Ve(r.driver)}`,pinned:!0}});ee&&(ee=!1,A.push({type:"text",text:`
11
+ This computer_repl runtime was restarted (idle), so top-level bindings and refs from before are gone; the API reference you already have still applies \u2014 computer.help() returns it again.`,pinned:!0})),p?k.driver.noteWriteRound(ue):_&&k.driver.noteContentRead();const Je=p?{afterAction:p}:{},se=d.find(r=>r.driver.permissionRefusal)?.driver.permissionRefusal;if(K&&l.length===0){const r=L(g.join(`
12
+ `),{verdict:"unknown",ledgerLines:g.length,timing:J(),...w?{error:"run_stopped"}:{}});return r.content.push(...A),r}if(se||j){const r=se||j;w?.includes(r)||g.push(`Observation skipped: ${r}`);const o=L(g.join(`
13
+ `),{verdict:"unknown",ledgerLines:g.length,timing:J(),...w?{error:"run_stopped"}:{},...se?{permissionRefused:!0}:{ownershipRefused:!0}});return o.content.push(...u,...A),o}try{if(P||N&&(!m.has(N)||!await k.driver.isActiveTarget(S.get(N).inner))){const y=k.driver.takeNoteForResult(),x=L(`${g.join(`
14
+ `)}${y?`
15
+ NOTE: ${y}.`:""}
16
+ Observation skipped: the bound target was released, closed or replaced. List surfaces and bind the intended target before continuing.`,{verdict:"unknown",ledgerLines:g.length,timing:J(),...w?{error:"run_stopped"}:{}});return x.content.push(...u,...A),x}const r=Date.now(),o=await k.driver.observeElements({prefix:g.join(`
17
+ `),...Je});return o.details.ledgerLines=g.length,o.details.timing=J(Date.now()-r),Ge(o,a),o.content.push(...u,...A),w&&(o.details.error="run_stopped"),o}catch(r){if(r.surfaceGone)throw r;const o=r.message,y=w&&w.includes(o)?"for the same reason":`${o} \u2014 observe again before the next action`,x=L(`${g.join(`
18
18
  `)}
19
- (re-observation failed: ${R}.)`,{verdict:"unknown",ledgerLines:g.length,timing:U(),...w?{error:"run_stopped"}:{}});return _.content.push(...h,...A),_}}}),Me=W.execute.bind(W),ne=()=>{C&&clearTimeout(C),C=null},Ne=()=>{C=null,!J&&(B=(async()=>{try{for(const e of u)await e.driver.releaseInputResources();await b.releaseAll()}catch{}})().finally(()=>{B=null}))};W.execute=async e=>{ne(),B&&await B,J=!0;let n;oe=new Promise(t=>{n=t});try{return await Me(e)}finally{try{if(j){for(const t of u)await t.driver.releaseInputResources();await b.releaseAll(),j=!1}S=null;for(const t of u)t.driver.endCall(),t.driver.consentPurpose=null,t.driver.permissionRefusal=null}finally{J=!1,n(),ne(),C=setTimeout(Ne,Te),C.unref?.()}}};const je=ve({name:"computer_repl_reset",label:"Computer REPL reset",description:Ye,promptSnippet:"computer_repl_reset discards the persistent JavaScript bindings; surfaces are untouched.",parameters:f.Object({}),async execute(e){const n=oe;if(ne(),J)j=!0;else{for(const t of u)await t.driver.releaseInputResources();await b.releaseAll()}M=null,P=null,O.clear(),v.clear();for(const t of u)t.docsDue=!0;return z=!1,Y=!1,e?.waitForIdle&&await n,x("The JavaScript runtime was reset: every binding is gone. The next computer_repl call starts fresh and returns the API reference. No surface was touched \u2014 tabs, windows and apps are as they were.",{reset:!0})}});return[W,je]}export{ct as createReplTools};
19
+ (re-observation failed: ${y}.)`,{verdict:"unknown",ledgerLines:g.length,timing:J(),...w?{error:"run_stopped"}:{}});return x.content.push(...u,...A),x}}}),Ue=z.execute.bind(z),ie=()=>{I&&clearTimeout(I),I=null},Fe=()=>{I=null,!q&&(G=(async()=>{try{for(const e of d)await e.driver.releaseInputResources();await E.releaseAll();for(const e of d)await e.driver.releaseIdle().catch(()=>{})}catch{}})().finally(()=>{G=null}))};z.execute=async e=>{ie(),G&&await G,q=!0;let n;ae=new Promise(t=>{n=t});try{return await Ue(e)}finally{try{if(P){for(const t of d)await t.driver.releaseInputResources();await E.releaseAll(),P=!1}R=null;for(const t of d)t.driver.endCall(),t.driver.consentPurpose=null,t.driver.permissionRefusal=null}finally{q=!1,n(),ie(),I=setTimeout(Fe,Te),I.unref?.()}}};const He=ve({name:"computer_repl_reset",label:"Computer REPL reset",description:et,promptSnippet:"computer_repl_reset discards the persistent JavaScript bindings; surfaces are untouched.",parameters:h.Object({}),async execute(e){const n=ae;if(ie(),q)P=!0;else{for(const t of d)await t.driver.releaseInputResources();await E.releaseAll()}M=null,N=null,$.clear(),v.clear();for(const t of d)t.docsDue=!0;return Z=!1,ee=!1,e?.waitForIdle&&await n,L("The JavaScript runtime was reset: every binding is gone. The next computer_repl call starts fresh and returns the API reference. No surface was touched \u2014 tabs, windows and apps are as they were.",{reset:!0})}});return[z,He]}export{ht as createReplTools};
@@ -70,6 +70,12 @@ export class MirasimTabGroup {
70
70
  * (the only record of ownership), and with no current window throw "No current window".
71
71
  */
72
72
  this._userTabIds = new Set();
73
+ /**
74
+ * Tabs the BRIDGE created (relay.js `ontabcreatedforagent`): the agent's, whatever the race
75
+ * below decided. A new tab's first `onUpdated` can arrive before the bridge has attached it,
76
+ * and `_onTabUpdated` would then file it as the user's and keep it out of the group.
77
+ */
78
+ this._agentCreatedTabIds = new Set();
73
79
  this._onTabCreatedListener = this._onTabCreated.bind(this);
74
80
  this._onTabUpdatedListener = this._onTabUpdated.bind(this);
75
81
  this._onTabRemovedListener = this._onTabRemoved.bind(this);
@@ -81,6 +87,10 @@ export class MirasimTabGroup {
81
87
  this._connection.onclose = () => this._onConnectionClose();
82
88
  this._connection.ontabattached = (tabId) => this._onTabAttached(tabId);
83
89
  this._connection.ontabdetached = (tabId) => this._onTabDetached(tabId);
90
+ this._connection.ontabcreatedforagent = (tabId) => {
91
+ this._agentCreatedTabIds.add(tabId);
92
+ this._userTabIds.delete(tabId);
93
+ };
84
94
  chrome.tabs.onCreated.addListener(this._onTabCreatedListener);
85
95
  chrome.tabs.onUpdated.addListener(this._onTabUpdatedListener);
86
96
  chrome.tabs.onRemoved.addListener(this._onTabRemovedListener);
@@ -148,7 +158,7 @@ export class MirasimTabGroup {
148
158
  // Any tab, not just the group's: a tab opened after connect becomes a surface on its first
149
159
  // navigation. Group membership decides OWNERSHIP, not reachability.
150
160
  else if (!isNonDebuggableUrl(tab.url)) {
151
- if (!this._groupTabIds.has(tabId)) this._userTabIds.add(tabId);
161
+ if (!this._groupTabIds.has(tabId) && !this._agentCreatedTabIds.has(tabId)) this._userTabIds.add(tabId);
152
162
  this._connection.attachTab(tab);
153
163
  }
154
164
  }
@@ -173,6 +183,7 @@ export class MirasimTabGroup {
173
183
  _onTabRemoved(tabId) {
174
184
  this._groupTabIds.delete(tabId);
175
185
  this._userTabIds.delete(tabId);
186
+ this._agentCreatedTabIds.delete(tabId);
176
187
  }
177
188
 
178
189
  /** Newer Chrome versions replace the tab id when discarding its renderer. */
@@ -81,6 +81,9 @@ export class RelayConnection {
81
81
  this.ontabattached = undefined;
82
82
  /** @type {((tabId: number) => void) | undefined} */
83
83
  this.ontabdetached = undefined;
84
+ /** A tab the bridge created (`Target.createTarget`): the agent's by definition.
85
+ * @type {((tabId: number) => void) | undefined} */
86
+ this.ontabcreatedforagent = undefined;
84
87
 
85
88
  this._lastInbound = Date.now();
86
89
  this._inflightCommands = 0;
@@ -333,6 +336,8 @@ export class RelayConnection {
333
336
  return {};
334
337
  }
335
338
  const result = await invokeChromeMethod(message.method, args);
339
+ // Said before the reply leaves: the bridge attaches the tab only once it has the reply.
340
+ if (message.method === "chrome.tabs.create" && result?.id !== undefined) this.ontabcreatedforagent?.(result.id);
336
341
  return result ?? {};
337
342
  }
338
343
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mirasim/computer-use",
3
- "version": "0.1.0-rc.5",
3
+ "version": "0.1.0-rc.6",
4
4
  "description": "Computer use for any agent: an MCP server that drives browsers, macOS apps, Windows apps and Electron apps through their accessibility trees.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -70,9 +70,9 @@
70
70
  "access": "public"
71
71
  },
72
72
  "optionalDependencies": {
73
- "@mirasim/computer-use-darwin-arm64": "0.1.0-rc.5",
74
- "@mirasim/computer-use-darwin-x64": "0.1.0-rc.5",
75
- "@mirasim/computer-use-win32-x64": "0.1.0-rc.5",
76
- "@mirasim/computer-use-win32-arm64": "0.1.0-rc.5"
73
+ "@mirasim/computer-use-darwin-arm64": "0.1.0-rc.6",
74
+ "@mirasim/computer-use-darwin-x64": "0.1.0-rc.6",
75
+ "@mirasim/computer-use-win32-x64": "0.1.0-rc.6",
76
+ "@mirasim/computer-use-win32-arm64": "0.1.0-rc.6"
77
77
  }
78
78
  }