@mirasim/computer-use 0.1.0-rc.4 → 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.
Files changed (36) hide show
  1. package/README.md +17 -0
  2. package/dist/platform/browser/bridge/client.d.ts +2 -0
  3. package/dist/platform/browser/bridge/client.js +1 -1
  4. package/dist/platform/browser/bridge/model-commands.d.ts +1 -1
  5. package/dist/platform/browser/bridge/model-commands.js +1 -1
  6. package/dist/platform/browser/bridge/model-core.d.ts +8 -1
  7. package/dist/platform/browser/bridge/model-core.js +1 -1
  8. package/dist/platform/browser/bridge-tabs.d.ts +9 -1
  9. package/dist/platform/browser/bridge-tabs.js +1 -1
  10. package/dist/platform/browser/cdp-connection.js +1 -1
  11. package/dist/platform/browser/driver-tabs.js +1 -1
  12. package/dist/platform/browser/driver-verbs.d.ts +1 -0
  13. package/dist/platform/browser/driver-verbs.js +2 -2
  14. package/dist/platform/browser/driver.d.ts +12 -1
  15. package/dist/platform/browser/driver.js +2 -2
  16. package/dist/platform/browser/messages.d.ts +1 -0
  17. package/dist/platform/browser/messages.js +2 -2
  18. package/dist/platform/chromium/scroll.js +5 -5
  19. package/dist/platform/driver-base.d.ts +1 -0
  20. package/dist/platform/driver-base.js +1 -1
  21. package/dist/platform/driver-narrate.js +3 -3
  22. package/dist/platform/driver.d.ts +1 -0
  23. package/dist/platform/driver.js +4 -4
  24. package/dist/platform/ownership.d.ts +3 -0
  25. package/dist/platform/ownership.js +1 -1
  26. package/dist/platform/tools/api.js +1 -1
  27. package/dist/platform/tools/index.d.ts +1 -0
  28. package/dist/platform/tools/index.js +1 -1
  29. package/dist/platform/tools/repl-docs.js +4 -4
  30. package/dist/platform/tools/repl-state.d.ts +1 -0
  31. package/dist/platform/tools/repl.d.ts +1 -0
  32. package/dist/platform/tools/repl.js +17 -17
  33. package/dist/platform/types.d.ts +1 -0
  34. package/extension/group.js +12 -1
  35. package/extension/relay.js +5 -0
  36. package/package.json +5 -5
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};
@@ -1 +1 @@
1
- import"playwright-core";import{userWantsVisible as f}from"./acquire.js";import{pageContextId as l,pageTargetInfo as h}from"./tabs.js";import{msgOwnWindowFailed as m,NO_TAB_OPEN_NOTE as u,NO_TAB_YET_NOTE as g}from"./messages.js";import{SAVE_PICKER_JS as d}from"./save-picker.js";import{SurfaceUnreadableError as c}from"../types.js";import{ADOPTED_WHY as n,APPEARED_WHY as w,BrowserDriverCore as I,REIDENTIFIED_WHY as v}from"./driver-core.js";const b=r=>new Promise(t=>setTimeout(t,r));class D extends I{claim(t){this.mine.add(t),this.rememberIdentity(t),this.armSavePicker(t)}armSavePicker(t){this.savePickerPages.has(t)||(this.savePickerPages.add(t),t.addInitScript(d).catch(()=>{}),t.evaluate(d).catch(()=>{}))}rememberIdentity(t){h(t).then(({targetId:e})=>{e&&(this.ownedTargetIds.add(e),this.active===t&&(this.activeTargetId=e))}).catch(()=>{})}async recognise(t){const{targetId:e}=await h(t).catch(()=>({targetId:""}));return!e||!this.ownedTargetIds.has(e)?null:(this.mine.add(t),this.armSavePicker(t),e===this.activeTargetId?"same":"ours")}async ownsIsolatedPage(t){let e=this.pageCtxIds.get(t);return e===void 0&&(e=await l(t).catch(()=>""),this.pageCtxIds.set(t,e)),e!==""&&e===this.ctxId}adoptWhenReal(t){const e=()=>{if(this.tab?.isolated){this.ownsIsolatedPage(t).then(s=>{s&&(this.claim(t),this.makeActive(t,n))}).catch(()=>{});return}if(this.tab){t.opener().then(s=>{s&&this.mine.has(s)&&(this.claim(t),this.makeActive(t,n))}).catch(()=>{});return}if(!this.attached){if(this.mine.has(t)){this.makeActive(t,n);return}this.recognise(t).then(s=>this.makeActive(t,s==="same"?v:s==="ours"?n:w)).catch(()=>this.makeActive(t,w));return}this.ownsPage(t).then(s=>{s&&this.makeActive(t,n)}).catch(()=>{})};if(t.url()!=="about:blank"){e();return}const i=s=>{s!==t.mainFrame()||t.isClosed()||t.url()==="about:blank"||(clearTimeout(a),t.off("framenavigated",i),e())},a=setTimeout(()=>t.off("framenavigated",i),1500);a.unref?.(),t.on("framenavigated",i)}page(){if(this.closedItsLastTab)throw new c(u);if(!this.active)throw new c(g);if(this.active.isClosed()){const t=this.context.pages().filter(i=>!i.isClosed()&&(this.tab?this.tab.isolated?this.pageCtxIds.get(i)===this.ctxId:this.mine.has(i):!this.attached||this.ownWindowId===null||this.windowIds.get(i)===this.ownWindowId)),e=t.filter(i=>i.url()!=="about:blank");this.makeActive(e[e.length-1]??t[t.length-1]??this.active,"the tab it was on was closed")}return this.active}async openOwnWindow(){const t=new Set(this.context.pages()),e=await this.browser.newBrowserCDPSession();try{await e.send("Target.createTarget",{url:"about:blank",newWindow:!0,background:!0})}finally{await e.detach().catch(()=>{})}let i;for(let s=0;s<40&&(i=this.context.pages().find(o=>!t.has(o)&&!o.isClosed()),!i);s++)await b(25);if(!i)throw new Error(m());const a=await this.context.newCDPSession(i);try{const{windowId:s}=await a.send("Browser.getWindowForTarget");this.ownWindowId=s,this.windowIds.set(i,s),this.cfg.window==="offscreen"&&!(this.managedProfileDir&&f(this.managedProfileDir))&&await a.send("Browser.setWindowBounds",{windowId:s,bounds:{left:-4e3,top:200}})}catch{}finally{await a.detach().catch(()=>{})}return i}async ownsPage(t){if(!this.attached||this.ownWindowId===null)return!0;let e=this.windowIds.get(t);if(e===void 0)try{const i=await this.context.newCDPSession(t);try{e=(await i.send("Browser.getWindowForTarget")).windowId}finally{await i.detach().catch(()=>{})}this.windowIds.set(t,e)}catch{return!1}return e===this.ownWindowId}async ownPages(){const t=this.context.pages().filter(i=>!i.isClosed());if(this.tab?.isolated){const i=[];for(const a of t)await this.ownsIsolatedPage(a)&&i.push(a);return i}if(this.tab)return t.filter(i=>this.mine.has(i));if(!this.managedProfileDir)return t.filter(i=>this.mine.has(i));if(!this.attached||this.ownWindowId===null)return t;const e=[];for(const i of t)await this.ownsPage(i)&&e.push(i);return e}async visiblePages(){return this.managedProfileDir||this.attached||this.tab?this.ownPages():this.context.pages().filter(t=>!t.isClosed())}tabId(t){let e=this.tabIds.get(t);return e||(e=`t${this.nextTabId++}`,this.tabIds.set(t,e)),e}}export{D as BrowserDriverTabs};
1
+ import"playwright-core";import{userWantsVisible as w}from"./acquire.js";import{pageContextId as f,pageTargetInfo as h}from"./tabs.js";import{msgOwnWindowFailed as l,NO_TAB_OPEN_NOTE as m,NO_TAB_YET_NOTE as g}from"./messages.js";import{SAVE_PICKER_JS as d}from"./save-picker.js";import{SurfaceUnreadableError as c}from"../types.js";import{ADOPTED_WHY as r,APPEARED_WHY as u,BrowserDriverCore as I,REIDENTIFIED_WHY as v}from"./driver-core.js";const b=o=>new Promise(t=>setTimeout(t,o));class D extends I{claim(t){this.mine.add(t),this.rememberIdentity(t),this.armSavePicker(t)}armSavePicker(t){this.savePickerPages.has(t)||(this.savePickerPages.add(t),t.addInitScript(d).catch(()=>{}),t.evaluate(d).catch(()=>{}))}rememberIdentity(t){h(t).then(({targetId:e})=>{e&&(this.ownedTargetIds.add(e),this.active===t&&(this.activeTargetId=e))}).catch(()=>{})}async recognise(t){const{targetId:e}=await h(t).catch(()=>({targetId:""}));return!e||!this.ownedTargetIds.has(e)?null:(this.mine.add(t),this.armSavePicker(t),e===this.activeTargetId?"same":"ours")}async ownsIsolatedPage(t){let e=this.pageCtxIds.get(t);return e===void 0&&(e=await f(t).catch(()=>""),this.pageCtxIds.set(t,e)),e!==""&&e===this.ctxId}adoptWhenReal(t){const e=()=>{if(this.tab?.isolated){this.ownsIsolatedPage(t).then(s=>{s&&(this.claim(t),this.makeActive(t,r))}).catch(()=>{});return}if(this.tab){t.opener().then(s=>{s&&this.mine.has(s)&&(this.claim(t),this.makeActive(t,r))}).catch(()=>{});return}if(!this.attached){if(this.mine.has(t)){this.makeActive(t,r);return}const s=()=>{this.managedProfileDir&&this.claim(t),this.makeActive(t,u)};this.recognise(t).then(n=>n?this.makeActive(t,n==="same"?v:r):s()).catch(s);return}this.ownsPage(t).then(s=>{s&&(this.claim(t),this.makeActive(t,r))}).catch(()=>{})};if(t.url()!=="about:blank"){e();return}const i=s=>{s!==t.mainFrame()||t.isClosed()||t.url()==="about:blank"||(clearTimeout(a),t.off("framenavigated",i),e())},a=setTimeout(()=>t.off("framenavigated",i),1500);a.unref?.(),t.on("framenavigated",i)}page(){if(this.closedItsLastTab)throw new c(m);if(!this.active)throw new c(g);if(this.active.isClosed()){const t=this.context.pages().filter(i=>!i.isClosed()&&(this.tab?this.tab.isolated?this.pageCtxIds.get(i)===this.ctxId:this.mine.has(i):!this.attached||this.ownWindowId===null||this.windowIds.get(i)===this.ownWindowId)),e=t.filter(i=>i.url()!=="about:blank");this.makeActive(e[e.length-1]??t[t.length-1]??this.active,"the tab it was on was closed")}return this.active}async openOwnWindow(){const t=new Set(this.context.pages()),e=await this.browser.newBrowserCDPSession();try{await e.send("Target.createTarget",{url:"about:blank",newWindow:!0,background:!0})}finally{await e.detach().catch(()=>{})}let i;for(let s=0;s<40&&(i=this.context.pages().find(n=>!t.has(n)&&!n.isClosed()),!i);s++)await b(25);if(!i)throw new Error(l());const a=await this.context.newCDPSession(i);try{const{windowId:s}=await a.send("Browser.getWindowForTarget");this.ownWindowId=s,this.windowIds.set(i,s),this.cfg.window==="offscreen"&&!(this.managedProfileDir&&w(this.managedProfileDir))&&await a.send("Browser.setWindowBounds",{windowId:s,bounds:{left:-4e3,top:200}})}catch{}finally{await a.detach().catch(()=>{})}return i}async ownsPage(t){if(!this.attached||this.ownWindowId===null)return!0;let e=this.windowIds.get(t);if(e===void 0)try{const i=await this.context.newCDPSession(t);try{e=(await i.send("Browser.getWindowForTarget")).windowId}finally{await i.detach().catch(()=>{})}this.windowIds.set(t,e)}catch{return!1}return e===this.ownWindowId}async ownPages(){const t=this.context.pages().filter(i=>!i.isClosed());if(this.tab?.isolated){const i=[];for(const a of t)await this.ownsIsolatedPage(a)&&i.push(a);return i}if(this.tab)return t.filter(i=>this.mine.has(i));if(!this.managedProfileDir)return t.filter(i=>this.mine.has(i));if(!this.attached||this.ownWindowId===null)return t;const e=[];for(const i of t)await this.ownsPage(i)&&e.push(i);return e}async visiblePages(){return this.managedProfileDir||this.attached||this.tab?this.ownPages():this.context.pages().filter(t=>!t.isClosed())}tabId(t){let e=this.tabIds.get(t);return e||(e=`t${this.nextTabId++}`,this.tabIds.set(t,e)),e}}export{D as BrowserDriverTabs};
@@ -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,13 +30,18 @@ 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;
36
+ explainMiss(query: string): string | null;
29
37
  promptFragment(): string;
30
38
  listTargets(): Promise<TargetInfo[]>;
31
39
  protected noteBridgeSession(session: string | null): boolean;
32
40
  actionResources(kind: string, args: Record<string, unknown>): Promise<OwnershipResource[]>;
33
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>;
34
45
  close(): Promise<void>;
35
46
  protected journalVerdict(verdict: Verdict): void;
36
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 P=5e3,N=1500;async function R(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 G 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"}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 R(r)||r.url(),active:!this.unloadedSelection&&r===this.active,own:s.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(P);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{G 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};