From 9e21bef739c1b151669bb8773a98567bca5204d6 Mon Sep 17 00:00:00 2001 From: Aviral Shukla Date: Thu, 5 Nov 2026 18:00:57 +0530 Subject: [PATCH] refactor: centralize device ownership and retryable native cleanup --- README.md | 6 + dist/gui.cjs | 36 +-- dist/server.js | 29 +- src/cancellation.ts | 16 ++ src/controller.test.ts | 124 +++++++- src/controller.ts | 315 +++++++++++--------- src/deadlines.ts | 3 + src/main.ts | 12 +- src/native/android.ts | 35 ++- src/native/automation.ts | 9 +- src/native/backends.ts | 43 ++- src/native/helpers.ts | 67 +++-- src/native/hub-connection.ts | 132 +++++++++ src/native/ios.ts | 23 +- src/native/operations.ts | 236 +++++++++++++++ src/native/resources.ts | 79 +++++ src/native/runtime.test.ts | 542 +++++++++++++++++++++++++++++++++++ src/native/runtime.ts | 486 +++++++++++-------------------- src/operation-queue.test.ts | 52 ++++ src/operation-queue.ts | 25 ++ src/server.test.ts | 148 +++++----- src/server.ts | 51 ++-- 22 files changed, 1838 insertions(+), 631 deletions(-) create mode 100644 src/cancellation.ts create mode 100644 src/deadlines.ts create mode 100644 src/native/hub-connection.ts create mode 100644 src/native/operations.ts create mode 100644 src/native/resources.ts create mode 100644 src/native/runtime.test.ts create mode 100644 src/operation-queue.test.ts create mode 100644 src/operation-queue.ts diff --git a/README.md b/README.md index 8225230..d92a87e 100644 --- a/README.md +++ b/README.md @@ -49,10 +49,16 @@ without detaching the device, so agents can keep working. Use `device.close` to release it; deleting or moving the session also releases it. A failed device switch keeps the previous attachment. +Refresh retries failed video capture without changing the attached device. If a helper exits, +the plugin restarts capture on the next stream request. + Agent access starts enabled. Use the switch to revoke access for the current session. Agents can only use a device you have opened. Installing an app or shutting down a device requires a separate permission prompt. +Cancelling a dispatched action cannot undo device input already sent. Read device state before +retrying an action whose outcome is unknown. + Tools use `device_*` names, or `device.*` in Code Mode. They can inspect the screen, launch apps, enter text, interact with elements and take screenshots. diff --git a/dist/gui.cjs b/dist/gui.cjs index cdc23ac..fd0ff7b 100644 --- a/dist/gui.cjs +++ b/dist/gui.cjs @@ -1,23 +1,23 @@ -"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const X=require("solid-js/web"),ne=require("solid-js"),Et=require("@opencode/plugin/gui"),ta=require("@opencode/schema/rpc"),f=require("effect"),na=8*1024*1024,Ve=f.Schema.optional,we=f.Schema.String.check(f.Schema.isMaxLength(2048)),eo=f.Schema.String.check(f.Schema.isMaxLength(1e5)),Lt=f.Schema.Int.check(f.Schema.isGreaterThanOrEqualTo(0)),to=f.Schema.Int.check(f.Schema.isBetween({minimum:0,maximum:1e6})),Re=f.Schema.Literals(["ios","android"]).annotate({identifier:"Device.Platform"}),ra=f.Schema.Literals(["shutdown","booting","booted","unavailable"]).annotate({identifier:"Device.DeviceState"}),oa=f.Schema.Literals(["available","owned-by-this-host","owned-by-other","unknown"]).annotate({identifier:"Device.Ownership"}),no=f.Schema.Literals(["apps","launch","install","snapshot","press","fill","swipe","key","screenshot","stream"]).annotate({identifier:"Device.Capability"}),ia=f.Schema.Literals(["home","back","recents","lock","volumeUp","volumeDown"]).annotate({identifier:"Device.NavigationKey"}),Tn=Lt.annotate({identifier:"Device.Generation"}),On=f.Schema.String.check(f.Schema.isPattern(/^dh_[a-f0-9-]{36}$/u)).pipe(f.Schema.brand("Device.HandleToken")).annotate({identifier:"Device.HandleToken"}),Ee=f.Schema.Struct({token:On,platform:Re,deviceId:we,generation:Tn}).annotate({identifier:"Device.DeviceHandle"}),ho=f.Schema.String.check(f.Schema.isPattern(/^@?e[1-9][0-9]*$/u)).pipe(f.Schema.brand("Device.ElementRef")).annotate({identifier:"Device.ElementRef"}),ro=f.Schema.Struct({token:On,generation:Tn,revision:Lt,ref:ho}).annotate({identifier:"Device.SnapshotReference"}),sa=f.Schema.Struct({ref:Ve(ho),role:we,name:Ve(eo),value:Ve(eo),enabled:Ve(f.Schema.Boolean),focused:Ve(f.Schema.Boolean)}).annotate({identifier:"Device.AccessibilityNode"}),aa=f.Schema.Struct({token:On,generation:Tn,revision:Lt,interactiveOnly:f.Schema.Boolean,complete:f.Schema.Boolean,nodes:f.Schema.Array(sa)}).annotate({identifier:"Device.Snapshot"}),En=f.Schema.Struct({x:to,y:to}).annotate({identifier:"Device.LogicalPoint"}),ca=f.Schema.Struct({id:we,name:we,platform:Re,osVersion:Ve(we),state:ra,ownership:oa}).annotate({identifier:"Device.DeviceInfo"}),la=f.Schema.Struct({supported:f.Schema.Array(no),unsupported:f.Schema.Array(f.Schema.Struct({capability:no,reason:we}))}).annotate({identifier:"Device.Capabilities"}),da=f.Schema.Struct({id:we,name:Ve(we)}).annotate({identifier:"Device.AppInfo"}),ha=f.Schema.Struct({app:we,state:f.Schema.Literals(["launched","foreground","unknown"])}).annotate({identifier:"Device.LaunchResult"}),ua=f.Schema.Struct({installed:f.Schema.Boolean,app:Ve(we)}).annotate({identifier:"Device.InstallResult"}),fa=f.Schema.Struct({data:f.Schema.Uint8ArrayFromBase64.check(f.Schema.isMaxLength(na)),width:Lt,height:Lt,deviceId:we,platform:Re}).annotate({identifier:"Device.Screenshot"}),pa=f.Schema.Literal("h264").annotate({identifier:"Device.StreamProtocol"}),ma=f.Schema.Struct({platform:Re,deviceId:we,baseUrl:we,token:we,protocol:pa,loopback:f.Schema.Literal(!0)}).annotate({identifier:"Device.StreamDescriptor"}),ga=f.Schema.Literals(["missing-prerequisite","permission-denied","device-busy","detached","stale-reference","unsupported","not-found","invalid-artifact","timeout","helper-failed","internal"]).annotate({identifier:"Device.DeviceErrorCode"}),An=f.Schema.Struct({code:ga,message:we,mayHaveExecuted:f.Schema.Boolean}).annotate({identifier:"Device.DeviceError"}),it=f.Schema.optional,Me=f.Schema.String.check(f.Schema.isMaxLength(2048)),uo=Me.check(f.Schema.isMinLength(1)),Dn={sessionID:uo,connectionID:f.Schema.String},oo={...Dn,requestID:f.Schema.String},io=f.Schema.Number.check(f.Schema.isInt(),f.Schema.isBetween({minimum:1,maximum:1e6})),so=f.Schema.optional(f.Schema.Struct({width:io,height:io})),st=f.Schema.Struct({sessionID:uo,serverID:Me,location:f.Schema.optionalKey(f.Schema.Struct({directory:Me}))}),zt=f.Schema.Struct({handle:Ee,selection:f.Schema.optionalKey(f.Schema.Struct({platform:Re,deviceId:Me})),capabilities:la,agentAccess:f.Schema.Boolean}).annotate({identifier:"DeviceBridge.Attachment"}),fo=f.Schema.Union([f.Schema.Struct({type:f.Schema.Literal("list"),platform:it(Re)}),f.Schema.Struct({type:f.Schema.Literal("state")}),f.Schema.Struct({type:f.Schema.Literal("open"),platform:Re,deviceId:Me,boot:it(f.Schema.Boolean)}),f.Schema.Struct({type:f.Schema.Literal("apps"),handle:Ee}),f.Schema.Struct({type:f.Schema.Literal("launch"),handle:Ee,app:Me,deepLink:it(Me),relaunch:it(f.Schema.Boolean)}),f.Schema.Struct({type:f.Schema.Literal("install"),handle:Ee,artifactPath:Me}),f.Schema.Struct({type:f.Schema.Literal("snapshot"),handle:Ee,interactiveOnly:it(f.Schema.Boolean)}),f.Schema.Struct({type:f.Schema.Literal("press"),handle:Ee,reference:ro}),f.Schema.Struct({type:f.Schema.Literal("tap"),handle:Ee,point:En,viewport:so}),f.Schema.Struct({type:f.Schema.Literal("fill"),handle:Ee,reference:ro,text:Me}),f.Schema.Struct({type:f.Schema.Literal("swipe"),handle:Ee,from:En,to:En,viewport:so}),f.Schema.Struct({type:f.Schema.Literal("key"),handle:Ee,key:ia}),f.Schema.Struct({type:f.Schema.Literal("screenshot"),handle:Ee}),f.Schema.Struct({type:f.Schema.Literal("close"),handle:Ee,shutdown:it(f.Schema.Boolean)})]).pipe(f.Schema.toTaggedUnion("type")).annotate({identifier:"DeviceBridge.Operation"}),va=f.Schema.Union([f.Schema.Struct({type:f.Schema.Literal("devices"),devices:f.Schema.Array(ca)}),f.Schema.Struct({type:f.Schema.Literal("state"),state:f.Schema.NullOr(zt)}),f.Schema.Struct({type:f.Schema.Literal("attachment"),attachment:zt}),f.Schema.Struct({type:f.Schema.Literal("apps"),apps:f.Schema.Array(da)}),f.Schema.Struct({type:f.Schema.Literal("launch"),result:ha}),f.Schema.Struct({type:f.Schema.Literal("install"),result:ua}),f.Schema.Struct({type:f.Schema.Literal("snapshot"),snapshot:aa}),f.Schema.Struct({type:f.Schema.Literal("screenshot"),screenshot:fa}),f.Schema.Struct({type:f.Schema.Literal("done")})]).pipe(f.Schema.toTaggedUnion("type")).annotate({identifier:"DeviceBridge.Value"}),qt=f.Schema.Union([f.Schema.Struct({type:f.Schema.Literal("success"),value:va}),f.Schema.Struct({type:f.Schema.Literal("failure"),error:An})]).pipe(f.Schema.toTaggedUnion("type")).annotate({identifier:"DeviceBridge.Outcome"}),Sa=f.Schema.Union([f.Schema.Struct({type:f.Schema.Literal("success"),stream:ma}),f.Schema.Struct({type:f.Schema.Literal("failure"),error:An})]).pipe(f.Schema.toTaggedUnion("type")).annotate({identifier:"DeviceBridge.StreamOutcome"}),ya=f.Schema.Union([f.Schema.Struct({type:f.Schema.Literal("attached"),connectionID:f.Schema.String,version:f.Schema.Literal(1)}),f.Schema.Struct({type:f.Schema.Literal("command"),connectionID:f.Schema.String,requestID:f.Schema.String}),f.Schema.Struct({type:f.Schema.Literal("cancel"),connectionID:f.Schema.String,requestID:f.Schema.String})]).pipe(f.Schema.toTaggedUnion("type")).annotate({identifier:"DeviceBridge.Control"}),Be=f.Schema.toStandardSchemaV1,Vt={unavailable:Be(An)};ta.Rpc.define({id:"device.control",methods:{attach:{input:Be(f.Schema.Struct({...Dn,version:f.Schema.Literal(1)})),output:Be(f.Schema.Literals(["closed","replaced"])),errors:Vt},state:{input:Be(f.Schema.Struct({...Dn,state:f.Schema.NullOr(zt)})),output:Be(f.Schema.Void),errors:Vt},command:{input:Be(f.Schema.Struct(oo)),output:Be(fo),errors:Vt},result:{input:Be(f.Schema.Struct({...oo,outcome:qt})),output:Be(f.Schema.Void),errors:Vt}},events:{control:{schema:Be(ya)}}});const ao=f.Schema.String.check(f.Schema.isMaxLength(2048)),wa=Et.Ipc.define({id:"demul.device.bridge.v2",state:f.Schema.Array(f.Schema.Struct({scope:st,state:f.Schema.NullOr(zt)})),methods:{connect:{input:st},disconnect:{input:st},list:{input:f.Schema.Struct({platform:f.Schema.optional(Re),serverID:ao,sessionID:f.Schema.optionalKey(ao)}),output:qt},access:{input:f.Schema.Struct({scope:st,enabled:f.Schema.Boolean}),output:qt},manual:{input:f.Schema.Struct({scope:st,operation:fo}),output:qt},stream:{input:f.Schema.Struct({scope:st,handle:Ee}),output:Sa}}});function co(y){let x;return{async run(O,B){x?.abort();const z=new AbortController;x=z,y.pending(!0);try{const o=await O(z.signal);x===z&&B(o)}catch(o){x===z&&!z.signal.aborted&&y.error(o)}finally{x===z&&(x=void 0,y.pending(!1))}},cancel(){x?.abort(),x=void 0,y.pending(!1)}}}const ba=1397050709,_a={1:16,2:24},lo=new Uint8Array([0,0,0,1]);function ka(y){const x=y instanceof Uint8Array?y:new Uint8Array(y);if(x.byteLength<4)return;const O=new DataView(x.buffer,x.byteOffset,x.byteLength);return O.getUint32(0)===ba?xa(x,O):po(x,performance.now()*1e3,!1)}function xa(y,x){if(y.byteLength<16)return;const O=_a[x.getUint8(4)];if(!O||y.byteLength<=O)return;const B=x.getBigUint64(8);if(!(B>BigInt(Number.MAX_SAFE_INTEGER)))return po(y.subarray(O),Number(B),(x.getUint8(5)&1)===1,O===24?Number(x.getBigUint64(16))/1e3:void 0)}function po(y,x,O,B){const z=In(y,0);if(z===void 0||z>=y.byteLength)return;const o=Ea(y);return{bytes:y,...o,key:o.key||o.picture&&O,timestamp:x,serverTimestamp:B}}function Ea(y){let x=!1,O=!1,B,z;for(const o of La(y)){const ce=o[0]&31;ce===5&&(x=!0),ce>=1&&ce<=5&&(O=!0),ce===7&&(B=o),ce===8&&(z=o)}return{key:x,picture:O,codec:B&&Da(B),sps:B,pps:z}}function*La(y){let x=In(y,0);if(x!==void 0){for(let O=x;Ox&&(yield y.subarray(x,O)),x=B,O=B-1)}xx.toString(16).padStart(2,"0")).join("")}`}class Ia{codec;sps;pps;update(x){x.sps&&(this.sps=x.sps.slice(),this.codec=x.codec),x.pps&&(this.pps=x.pps.slice())}bytes(x){if(!x.key)return x.bytes;const O=[];if(!x.sps&&this.sps&&O.push(lo,this.sps),!x.pps&&this.pps&&O.push(lo,this.pps),!O.length)return x.bytes;O.push(x.bytes);const B=new Uint8Array(O.reduce((o,ce)=>o+ce.byteLength,0));let z=0;for(const o of O)B.set(o,z),z+=o.byteLength;return B}clear(){this.codec=void 0,this.sps=void 0,this.pps=void 0}}async function Ta(y,x){const O=await VideoDecoder.isConfigSupported(y);if(!x.current())return;if(!O.supported)throw new Error(`H.264 codec ${y.codec} is not supported by this browser`);const B=new VideoDecoder({output(z){try{x.current()&&x.output(z)}catch(o){x.error(`Device video draw failed: ${o instanceof Error?o.message:String(o)}`)}finally{z.close()}},error(z){x.current()&&x.error(`H.264 decoder failed: ${z.message}`)}});try{B.configure(O.config??y)}catch(z){throw B.close(),new Error(`H.264 decoder configuration failed: ${z instanceof Error?z.message:String(z)}`)}return B}function Oa(y,x,O,B){if(!(!y||y!==x||O!=="playing"||!Gt(B)))return{token:y,width:B.width,height:B.height}}function Aa(y,x){return!!x&&y.token===x.token&&y.width===x.width&&y.height===x.height}function Ca(y){let x=!1,O={type:"idle"};const B=()=>{try{return Ba(y)}catch(o){return y.onState("paused"),y.onError(o instanceof Error?o.message:String(o)),()=>{}}},z=()=>{if(O.type==="destroyed")return;const o=x&&document.visibilityState==="visible";if(o&&O.type==="idle"){O={type:"running",stop:B()};return}o||O.type!=="running"||(O.stop(),O={type:"idle"},y.onState(x?"paused":"idle"))};return document.addEventListener("visibilitychange",z),{active(o){O.type!=="destroyed"&&(x=o,z())},destroy(){if(O.type==="destroyed")return;const o=O;O={type:"destroyed"},document.removeEventListener("visibilitychange",z),o.type==="running"&&o.stop(),y.canvas.width=0,y.canvas.height=0,y.onState("idle")}}}function Na(y,x,O){if(![y.left,y.top,y.width,y.height,x.width,x.height,O.x,O.y].every(Number.isFinite)||y.width<=0||y.height<=0||!Gt(x))return;const B=Math.min(y.width/x.width,y.height/x.height),z=x.width*B,o=x.height*B,ce=O.x-y.left-(y.width-z)/2,ee=O.y-y.top-(y.height-o)/2;if(!(ce<0||ee<0||ce>=z||ee>=o))return{x:Math.min(x.width-1,Math.floor(ce/B)),y:Math.min(x.height-1,Math.floor(ee/B))}}function Ba(y){if(typeof VideoDecoder!="function"||typeof EncodedVideoChunk!="function")return y.onState("paused"),y.onError("H.264 device video requires WebCodecs support in this browser"),()=>{};const x=new URL("/video?frame-meta=1",new URL(y.stream.baseUrl).origin);x.protocol=x.protocol==="https:"?"wss:":"ws:";const O=new WebSocket(x,`opencode-device.${y.stream.token}`);let B={type:"waiting"};const z=new Ia;let o,ce;const ee=()=>{clearTimeout(o),o=setTimeout(()=>fe("Device video did not produce a screen within 15 seconds. Refresh devices to retry."),15e3)};O.binaryType="arraybuffer",y.onState("connecting");const Q=()=>{if(O.readyState===WebSocket.OPEN)try{O.send(JSON.stringify({type:"reset-video",ack:!1}))}catch{}},le=(te=!1)=>{clearTimeout(ce),ce=void 0;const se=B;B={type:"waiting"},"decoder"in se&&se.decoder.state!=="closed"&&se.decoder.close(),te&&se.type==="playing"&&(ee(),y.onState("connecting")),te&&Q()},G=()=>{B.type!=="stopped"&&(le(),B={type:"stopped"},clearTimeout(o),O.close())},fe=te=>{B.type!=="stopped"&&(G(),y.onError(te.replaceAll(y.stream.token,"[redacted]").slice(0,512)),y.onState("paused"))},be=(te,se)=>{if(!(!("decoder"in B)||B.decoder!==se||se.state!=="configured")){B.waitingKeyframe=!1;try{ce??=setTimeout(()=>{ce=void 0,le(!0)},15e3),se.decode(new EncodedVideoChunk({type:te.key?"key":"delta",timestamp:te.timestamp,data:z.bytes(te)}))}catch(he){fe(`H.264 decode failed: ${he instanceof Error?he.message:String(he)}`)}}},qe=(te,se)=>{le();const he={type:"configuring",codec:se,missed:!1};B=he;const xe=()=>B===he||"decoder"in B&&B.setup===he;Ta({codec:se,optimizeForLatency:!0,hardwareAcceleration:"prefer-hardware"},{current:xe,output(ge){if(!("decoder"in B))return;const Ue=B;clearTimeout(ce),ce=void 0,Pa(y,ge,ge.displayWidth,ge.displayHeight),!(B!==Ue||Ue.type==="playing")&&(B={...Ue,type:"playing"},clearTimeout(o),y.onState("playing"))},error:fe}).then(ge=>{if(ge){if(B!==he){ge.close();return}B={type:"decoding",setup:he,decoder:ge,waitingKeyframe:!0},be(te,ge),xe()&&"decoder"in B&&he.missed&&(B.waitingKeyframe=!0,Q())}}).catch(ge=>{xe()&&fe(`H.264 decoder setup failed: ${ge instanceof Error?ge.message:String(ge)}`)})},De=()=>B.type==="configuring"?B.codec:"decoder"in B?B.setup.codec:void 0,ze=te=>{z.update(te);const se=De();se&&z.codec!==se&&le(!0)},Ie=te=>{const se=z.codec??De();if(!se)return Q();if("decoder"in B)return be(te,B.decoder);if(!te.key)return le(!0);qe(te,se)},Pe=te=>{if(ze(te),!!te.picture){if(B.type==="configuring"){B.missed=!0;return}if("decoder"in B&&B.decoder.decodeQueueSize>2)return le(!0);!te.key&&(!("decoder"in B)||B.waitingKeyframe)||Ie(te)}};return ee(),O.addEventListener("open",Q),O.addEventListener("message",te=>{if(B.type==="stopped")return;if(typeof te.data=="string"){const he=Ma(te.data);he?.type==="error"&&fe(`Device video failed: ${he.message}`),he?.type==="reset"&&(z.clear(),le(!0));return}if(!(te.data instanceof ArrayBuffer))return;const se=ka(te.data);se&&Pe(se)}),O.addEventListener("error",()=>fe("Device video connection failed")),O.addEventListener("close",te=>{fe(`Device video connection closed (code ${te.code}). Refresh devices to retry.`)}),G}function Pa(y,x,O,B){if(!Gt({width:O,height:B}))throw new Error("Device video produced invalid screen dimensions");(y.canvas.width!==O||y.canvas.height!==B)&&(y.canvas.width=O,y.canvas.height=B);const z=y.canvas.getContext("2d",{alpha:!1});if(!z)throw new Error("Device video canvas is unavailable");z.drawImage(x,0,0,O,B)}function Ma(y){try{const x=JSON.parse(y);if(!x||typeof x!="object")return;if("ok"in x&&x.ok===!1)return{type:"error",message:"error"in x&&typeof x.error=="string"?x.error:"the capture session failed"};if(Ra(x))return{type:"reset"}}catch{return}}function Ra(y){return"type"in y&&y.type==="video-session"&&"size"in y&&y.size!==null&&typeof y.size=="object"&&Gt(y.size)}function Gt(y){return!("width"in y)||!("height"in y)?!1:typeof y.width=="number"&&typeof y.height=="number"&&Number.isSafeInteger(y.width)&&Number.isSafeInteger(y.height)&&y.width>0&&y.height>0}const Ua=".device-panel{box-sizing:border-box;display:flex;flex-direction:column;min-width:0;min-height:0;height:100%;color:var(--text-base);background:var(--background-base);overflow:hidden}.device-panel__picker{box-sizing:border-box;display:flex;align-items:center;justify-content:center;flex:none;gap:8px;width:100%;padding:12px}.device-panel__platforms{box-sizing:border-box;display:flex;flex:none;height:28px;border:1px solid var(--border-weak-base);border-radius:4px}.device-panel__platform,.device-panel__action{color:var(--text-base);background:transparent;cursor:pointer}.device-panel__platform:disabled,.device-panel__action:disabled{opacity:.5;cursor:default}.device-panel__platform{padding:0 8px;border:0;border-radius:3px;font:inherit;font-size:12px}.device-panel__platform[aria-pressed=true],.device-panel__platform:hover:not(:disabled){color:var(--text-strong);background:var(--background-stronger)}.device-panel__action{box-sizing:border-box;display:grid;place-items:center;flex:none;width:28px;height:28px;padding:0;border:1px solid var(--border-weak-base);border-radius:4px}.device-panel__action:hover:not(:disabled){background:var(--background-stronger)}.device-panel__device-select{flex:0 1 260px;width:260px}.device-panel select{box-sizing:border-box;min-width:0;height:28px;padding:0 24px 0 8px;color:var(--text-strong);background:var(--background-stronger);border:1px solid var(--border-weak-base);border-radius:4px;font:inherit;font-size:12px;text-overflow:ellipsis}.device-panel select:focus-visible,.device-panel__platform:focus-visible,.device-panel__action:focus-visible,.device-panel canvas:focus-visible{outline:2px solid var(--border-interactive-base);outline-offset:2px}.device-panel__error{position:absolute;inset:auto 12px 12px;display:flex;align-items:flex-start;gap:6px;padding:8px;color:var(--text-on-critical-weak);background:var(--surface-critical-weak);border-radius:4px;font-size:12px;overflow-wrap:anywhere}.device-panel__viewer{position:relative;display:grid;place-items:center;flex:1;min-width:0;min-height:0;overflow:hidden}.device-panel__viewer canvas{position:absolute;inset:0;display:block;width:100%;height:100%;object-fit:contain;touch-action:none;cursor:pointer}.device-panel__viewer-state{box-sizing:border-box;position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:8px;padding:16px;color:var(--text-weak);background:color-mix(in srgb,var(--background-base) 88%,transparent);font-size:12px;text-align:center;overflow-wrap:anywhere;pointer-events:none}.device-panel__viewer-state--pending{inset:12px auto auto;padding:8px 12px;border-radius:4px;background:var(--background-base)}";var ja=X.template('