diff --git a/.github/releases/v0.4.0.md b/.github/releases/v0.4.0.md index 77fd880..a1834b4 100644 --- a/.github/releases/v0.4.0.md +++ b/.github/releases/v0.4.0.md @@ -15,7 +15,7 @@ - Failed initial state publication closes the desktop connection so a fresh connection can retry. Late readiness completion cannot resurrect a closed connection. - Failed or ended session-event subscriptions reconnect with cancellable backoff, capped at five seconds. - Cleanup after session deletion or movement remains retryable and does not block another session's cleanup. -- Shutdown intent survives failed cleanup. Physical-device aliases cannot bypass ownership or cause rollback to shut down another session's device. +- Shutdown intent survives failed cleanup when the retry omits the flag. Explicit `shutdown: false` releases the attachment without another power-off attempt. Physical-device aliases cannot bypass ownership or cause rollback to shut down another session's device. - Disposal cancels pending discovery and recovery. Destroyed viewers cannot reconnect or retain decode timers. Overlapping old tests were consolidated into transition tables, and the previous all-in-one desktop test was split into focused cases. Production function complexity is limited to nine. No runtime dependency or FSM framework was added. @@ -33,6 +33,6 @@ Update the plugin pin in your global OpenCode configuration, preserving other se Plugin exports and tool names are unchanged. Android still requires SDK Platform-Tools, Emulator, and a configured AVD. iOS requires Xcode and an iOS Simulator runtime on macOS. Both require Node 22.19+ on the 22.x line, or Node 24+, with npm. -Validation includes lint, formatting, typechecking, 296 passing tests and one existing Bun-only skip, rebuilt bundles, and installed-package loading. Each implementation PR passed Linux CI before merging; lifecycle suites also passed three consecutive local runs. Two fresh helper installations, native module loading, offline reuse, and stale-cache rejection passed on macOS arm64. +Validation includes lint, formatting, typechecking, 297 passing tests and one existing Bun-only skip, rebuilt bundles, and installed-package loading. Each implementation PR passed Linux CI before merging; lifecycle suites also passed three consecutive local runs. Two fresh helper installations, native module loading, offline reuse, and stale-cache rejection passed on macOS arm64. Full changelog: https://github.com/viralcodex/opencode-demul/compare/v0.3.0...v0.4.0 diff --git a/dist/gui.cjs b/dist/gui.cjs index ee98c84..39df4a9 100644 --- a/dist/gui.cjs +++ b/dist/gui.cjs @@ -1,4 +1,4 @@ -"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const Y=require("solid-js/web"),ne=require("solid-js"),Ot=require("@opencode/plugin/gui"),pa=require("@opencode/schema/rpc"),p=require("effect"),ma=8*1024*1024,qe=p.Schema.optional,we=p.Schema.String.check(p.Schema.isMaxLength(2048)),si=p.Schema.String.check(p.Schema.isMaxLength(1e5)),At=p.Schema.Int.check(p.Schema.isGreaterThanOrEqualTo(0)),ai=p.Schema.Int.check(p.Schema.isBetween({minimum:0,maximum:1e6})),je=p.Schema.Literals(["ios","android"]).annotate({identifier:"Device.Platform"}),ga=p.Schema.Literals(["shutdown","booting","booted","unavailable"]).annotate({identifier:"Device.DeviceState"}),Sa=p.Schema.Literals(["available","owned-by-this-host","owned-by-other","unknown"]).annotate({identifier:"Device.Ownership"}),ci=p.Schema.Literals(["apps","launch","install","snapshot","press","fill","swipe","key","screenshot","stream"]).annotate({identifier:"Device.Capability"}),ya=p.Schema.Literals(["home","back","recents","lock","volumeUp","volumeDown"]).annotate({identifier:"Device.NavigationKey"}),Nn=At.annotate({identifier:"Device.Generation"}),Mn=p.Schema.String.check(p.Schema.isPattern(/^dh_[a-f0-9-]{36}$/u)).pipe(p.Schema.brand("Device.HandleToken")).annotate({identifier:"Device.HandleToken"}),Ie=p.Schema.Struct({token:Mn,platform:je,deviceId:we,generation:Nn}).annotate({identifier:"Device.DeviceHandle"}),gi=p.Schema.String.check(p.Schema.isPattern(/^@?e[1-9][0-9]*$/u)).pipe(p.Schema.brand("Device.ElementRef")).annotate({identifier:"Device.ElementRef"}),li=p.Schema.Struct({token:Mn,generation:Nn,revision:At,ref:gi}).annotate({identifier:"Device.SnapshotReference"}),va=p.Schema.Struct({ref:qe(gi),role:we,name:qe(si),value:qe(si),enabled:qe(p.Schema.Boolean),focused:qe(p.Schema.Boolean)}).annotate({identifier:"Device.AccessibilityNode"}),wa=p.Schema.Struct({token:Mn,generation:Nn,revision:At,interactiveOnly:p.Schema.Boolean,complete:p.Schema.Boolean,nodes:p.Schema.Array(va)}).annotate({identifier:"Device.Snapshot"}),An=p.Schema.Struct({x:ai,y:ai}).annotate({identifier:"Device.LogicalPoint"}),ba=p.Schema.Struct({id:we,name:we,platform:je,osVersion:qe(we),state:ga,ownership:Sa}).annotate({identifier:"Device.DeviceInfo"}),_a=p.Schema.Struct({supported:p.Schema.Array(ci),unsupported:p.Schema.Array(p.Schema.Struct({capability:ci,reason:we}))}).annotate({identifier:"Device.Capabilities"}),ka=p.Schema.Struct({id:we,name:qe(we)}).annotate({identifier:"Device.AppInfo"}),xa=p.Schema.Struct({app:we,state:p.Schema.Literals(["launched","foreground","unknown"])}).annotate({identifier:"Device.LaunchResult"}),Ea=p.Schema.Struct({installed:p.Schema.Boolean,app:qe(we)}).annotate({identifier:"Device.InstallResult"}),Ia=p.Schema.Struct({data:p.Schema.Uint8ArrayFromBase64.check(p.Schema.isMaxLength(ma)),width:At,height:At,deviceId:we,platform:je}).annotate({identifier:"Device.Screenshot"}),Da=p.Schema.Literal("h264").annotate({identifier:"Device.StreamProtocol"}),La=p.Schema.Struct({platform:je,deviceId:we,baseUrl:we,token:we,protocol:Da,loopback:p.Schema.Literal(!0)}).annotate({identifier:"Device.StreamDescriptor"}),Ta=p.Schema.Literals(["missing-prerequisite","permission-denied","device-busy","detached","stale-reference","unsupported","not-found","invalid-artifact","timeout","helper-failed","internal"]).annotate({identifier:"Device.DeviceErrorCode"}),Rn=p.Schema.Struct({code:Ta,message:we,mayHaveExecuted:p.Schema.Boolean}).annotate({identifier:"Device.DeviceError"}),ct=p.Schema.optional,Ue=p.Schema.String.check(p.Schema.isMaxLength(2048)),Si=Ue.check(p.Schema.isMinLength(1)),Bn={sessionID:Si,connectionID:p.Schema.String},di={...Bn,requestID:p.Schema.String},hi=p.Schema.Number.check(p.Schema.isInt(),p.Schema.isBetween({minimum:1,maximum:1e6})),ui=p.Schema.optional(p.Schema.Struct({width:hi,height:hi})),lt=p.Schema.Struct({sessionID:Si,serverID:Ue,location:p.Schema.optionalKey(p.Schema.Struct({directory:Ue}))}),Yt=p.Schema.Struct({handle:Ie,selection:p.Schema.optionalKey(p.Schema.Struct({platform:je,deviceId:Ue})),capabilities:_a,agentAccess:p.Schema.Boolean}).annotate({identifier:"DeviceBridge.Attachment"}),yi=p.Schema.Union([p.Schema.Struct({type:p.Schema.Literal("list"),platform:ct(je)}),p.Schema.Struct({type:p.Schema.Literal("state")}),p.Schema.Struct({type:p.Schema.Literal("open"),platform:je,deviceId:Ue,boot:ct(p.Schema.Boolean)}),p.Schema.Struct({type:p.Schema.Literal("apps"),handle:Ie}),p.Schema.Struct({type:p.Schema.Literal("launch"),handle:Ie,app:Ue,deepLink:ct(Ue),relaunch:ct(p.Schema.Boolean)}),p.Schema.Struct({type:p.Schema.Literal("install"),handle:Ie,artifactPath:Ue}),p.Schema.Struct({type:p.Schema.Literal("snapshot"),handle:Ie,interactiveOnly:ct(p.Schema.Boolean)}),p.Schema.Struct({type:p.Schema.Literal("press"),handle:Ie,reference:li}),p.Schema.Struct({type:p.Schema.Literal("tap"),handle:Ie,point:An,viewport:ui}),p.Schema.Struct({type:p.Schema.Literal("fill"),handle:Ie,reference:li,text:Ue}),p.Schema.Struct({type:p.Schema.Literal("swipe"),handle:Ie,from:An,to:An,viewport:ui}),p.Schema.Struct({type:p.Schema.Literal("key"),handle:Ie,key:ya}),p.Schema.Struct({type:p.Schema.Literal("screenshot"),handle:Ie}),p.Schema.Struct({type:p.Schema.Literal("close"),handle:Ie,shutdown:ct(p.Schema.Boolean)})]).pipe(p.Schema.toTaggedUnion("type")).annotate({identifier:"DeviceBridge.Operation"}),Oa=p.Schema.Union([p.Schema.Struct({type:p.Schema.Literal("devices"),devices:p.Schema.Array(ba)}),p.Schema.Struct({type:p.Schema.Literal("state"),state:p.Schema.NullOr(Yt)}),p.Schema.Struct({type:p.Schema.Literal("attachment"),attachment:Yt}),p.Schema.Struct({type:p.Schema.Literal("apps"),apps:p.Schema.Array(ka)}),p.Schema.Struct({type:p.Schema.Literal("launch"),result:xa}),p.Schema.Struct({type:p.Schema.Literal("install"),result:Ea}),p.Schema.Struct({type:p.Schema.Literal("snapshot"),snapshot:wa}),p.Schema.Struct({type:p.Schema.Literal("screenshot"),screenshot:Ia}),p.Schema.Struct({type:p.Schema.Literal("done")})]).pipe(p.Schema.toTaggedUnion("type")).annotate({identifier:"DeviceBridge.Value"}),Xt=p.Schema.Union([p.Schema.Struct({type:p.Schema.Literal("success"),value:Oa}),p.Schema.Struct({type:p.Schema.Literal("failure"),error:Rn})]).pipe(p.Schema.toTaggedUnion("type")).annotate({identifier:"DeviceBridge.Outcome"}),Aa=p.Schema.Union([p.Schema.Struct({type:p.Schema.Literal("success"),stream:La}),p.Schema.Struct({type:p.Schema.Literal("failure"),error:Rn})]).pipe(p.Schema.toTaggedUnion("type")).annotate({identifier:"DeviceBridge.StreamOutcome"}),Ca=p.Schema.Union([p.Schema.Struct({type:p.Schema.Literal("attached"),connectionID:p.Schema.String,version:p.Schema.Literal(1)}),p.Schema.Struct({type:p.Schema.Literal("command"),connectionID:p.Schema.String,requestID:p.Schema.String}),p.Schema.Struct({type:p.Schema.Literal("cancel"),connectionID:p.Schema.String,requestID:p.Schema.String})]).pipe(p.Schema.toTaggedUnion("type")).annotate({identifier:"DeviceBridge.Control"}),Me=p.Schema.toStandardSchemaV1,Jt={unavailable:Me(Rn)};pa.Rpc.define({id:"device.control",methods:{attach:{input:Me(p.Schema.Struct({...Bn,version:p.Schema.Literal(1)})),output:Me(p.Schema.Literals(["closed","replaced"])),errors:Jt},state:{input:Me(p.Schema.Struct({...Bn,state:p.Schema.NullOr(Yt)})),output:Me(p.Schema.Void),errors:Jt},command:{input:Me(p.Schema.Struct(di)),output:Me(yi),errors:Jt},result:{input:Me(p.Schema.Struct({...di,outcome:Xt})),output:Me(p.Schema.Void),errors:Jt}},events:{control:{schema:Me(Ca)}}});const fi=p.Schema.String.check(p.Schema.isMaxLength(2048)),Ba=Ot.Ipc.define({id:"demul.device.bridge.v2",state:p.Schema.Array(p.Schema.Struct({scope:lt,state:p.Schema.NullOr(Yt)})),methods:{connect:{input:lt},disconnect:{input:lt},list:{input:p.Schema.Struct({platform:p.Schema.optional(je),serverID:fi,sessionID:p.Schema.optionalKey(fi)}),output:Xt},access:{input:p.Schema.Struct({scope:lt,enabled:p.Schema.Boolean}),output:Xt},manual:{input:p.Schema.Struct({scope:lt,operation:yi}),output:Xt},stream:{input:p.Schema.Struct({scope:lt,handle:Ie}),output:Aa}}});function pi(S){let I;return{async run(T,B){I?.abort();const q=new AbortController;I=q,S.pending(!0);try{const o=await T(q.signal);I===q&&B(o)}catch(o){I===q&&!q.signal.aborted&&S.error(o)}finally{I===q&&(I=void 0,S.pending(!1))}},cancel(){I?.abort(),I=void 0,S.pending(!1)}}}const Pa=1397050709,Na={1:16,2:24},mi=new Uint8Array([0,0,0,1]);function Ma(S){const I=S instanceof Uint8Array?S:new Uint8Array(S);if(I.byteLength<4)return;const T=new DataView(I.buffer,I.byteOffset,I.byteLength);return T.getUint32(0)===Pa?Ra(I,T):vi(I,performance.now()*1e3,!1)}function Ra(S,I){if(S.byteLength<16)return;const T=Na[I.getUint8(4)];if(!T||S.byteLength<=T)return;const B=I.getBigUint64(8);if(!(B>BigInt(Number.MAX_SAFE_INTEGER)))return vi(S.subarray(T),Number(B),(I.getUint8(5)&1)===1,T===24?Number(I.getBigUint64(16))/1e3:void 0)}function vi(S,I,T,B){const q=Pn(S,0);if(q===void 0||q>=S.byteLength)return;const o=Ua(S);return{bytes:S,...o,key:o.key||o.picture&&T,timestamp:I,serverTimestamp:B}}function Ua(S){let I=!1,T=!1,B,q;for(const o of ja(S)){const le=o[0]&31;le===5&&(I=!0),le>=1&&le<=5&&(T=!0),le===7&&(B=o),le===8&&(q=o)}return{key:I,picture:T,codec:B&&$a(B),sps:B,pps:q}}function*ja(S){let I=Pn(S,0);if(I!==void 0){for(let T=I;TI&&(yield S.subarray(I,T)),I=B,T=B-1)}II.toString(16).padStart(2,"0")).join("")}`}class Fa{codec;sps;pps;update(I){I.sps&&(this.sps=I.sps.slice(),this.codec=I.codec),I.pps&&(this.pps=I.pps.slice())}bytes(I){if(!I.key)return I.bytes;const T=[];if(!I.sps&&this.sps&&T.push(mi,this.sps),!I.pps&&this.pps&&T.push(mi,this.pps),!T.length)return I.bytes;T.push(I.bytes);const B=new Uint8Array(T.reduce((o,le)=>o+le.byteLength,0));let q=0;for(const o of T)B.set(o,q),q+=o.byteLength;return B}clear(){this.codec=void 0,this.sps=void 0,this.pps=void 0}}async function Wa(S,I){const T=await VideoDecoder.isConfigSupported(S);if(!I.current())return;if(!T.supported)throw new Error(`H.264 codec ${S.codec} is not supported by this browser`);const B=new VideoDecoder({output(q){try{I.current()&&I.output(q)}catch(o){I.error(`Device video draw failed: ${o instanceof Error?o.message:String(o)}`)}finally{q.close()}},error(q){I.current()&&I.error(`H.264 decoder failed: ${q.message}`)}});try{B.configure(T.config??S)}catch(q){throw B.close(),new Error(`H.264 decoder configuration failed: ${q instanceof Error?q.message:String(q)}`)}return B}function Ha(S,I,T,B){if(!(!S||S!==I||T!=="playing"||!Zt(B)))return{token:S,width:B.width,height:B.height}}function Va(S,I){return!!I&&S.token===I.token&&S.width===I.width&&S.height===I.height}function za(S){let I=!1,T={type:"idle"};const B=()=>{try{return Ga(S)}catch(o){return S.onState("paused"),S.onError(o instanceof Error?o.message:String(o)),()=>{}}},q=()=>{if(T.type==="destroyed")return;const o=I&&document.visibilityState==="visible";if(o&&T.type==="idle"){T={type:"running",stop:B()};return}o||T.type!=="running"||(T.stop(),T={type:"idle"},S.onState(I?"paused":"idle"))};return document.addEventListener("visibilitychange",q),{active(o){T.type!=="destroyed"&&(I=o,q())},destroy(){if(T.type==="destroyed")return;const o=T;T={type:"destroyed"},document.removeEventListener("visibilitychange",q),o.type==="running"&&o.stop(),S.canvas.width=0,S.canvas.height=0,S.onState("idle")}}}function qa(S,I,T){if(![S.left,S.top,S.width,S.height,I.width,I.height,T.x,T.y].every(Number.isFinite)||S.width<=0||S.height<=0||!Zt(I))return;const B=Math.min(S.width/I.width,S.height/I.height),q=I.width*B,o=I.height*B,le=T.x-S.left-(S.width-q)/2,ee=T.y-S.top-(S.height-o)/2;if(!(le<0||ee<0||le>=q||ee>=o))return{x:Math.min(I.width-1,Math.floor(le/B)),y:Math.min(I.height-1,Math.floor(ee/B))}}function Ga(S){if(typeof VideoDecoder!="function"||typeof EncodedVideoChunk!="function")return S.onState("paused"),S.onError("H.264 device video requires WebCodecs support in this browser"),()=>{};const I=new URL("/video?frame-meta=1",new URL(S.stream.baseUrl).origin);I.protocol=I.protocol==="https:"?"wss:":"ws:";const T=new WebSocket(I,`opencode-device.${S.stream.token}`);let B={type:"waiting"};const q=new Fa;let o,le;const ee=()=>{clearTimeout(o),o=setTimeout(()=>fe("Device video did not produce a screen within 15 seconds. Refresh devices to retry."),15e3)};T.binaryType="arraybuffer",S.onState("connecting");const Z=()=>{if(T.readyState===WebSocket.OPEN)try{T.send(JSON.stringify({type:"reset-video",ack:!1}))}catch{}},de=(te=!1)=>{clearTimeout(le),le=void 0;const ae=B;B={type:"waiting"},"decoder"in ae&&ae.decoder.state!=="closed"&&ae.decoder.close(),te&&ae.type==="playing"&&(ee(),S.onState("connecting")),te&&Z()},G=()=>{B.type!=="stopped"&&(de(),B={type:"stopped"},clearTimeout(o),T.close())},fe=te=>{B.type!=="stopped"&&(G(),S.onError(te.replaceAll(S.stream.token,"[redacted]").slice(0,512)),S.onState("paused"))},be=(te,ae)=>{if(!(!("decoder"in B)||B.decoder!==ae||ae.state!=="configured")){B.waitingKeyframe=!1;try{le??=setTimeout(()=>{le=void 0,de(!0)},15e3),ae.decode(new EncodedVideoChunk({type:te.key?"key":"delta",timestamp:te.timestamp,data:q.bytes(te)}))}catch(he){fe(`H.264 decode failed: ${he instanceof Error?he.message:String(he)}`)}}},Ge=(te,ae)=>{de();const he={type:"configuring",codec:ae,missed:!1};B=he;const Ee=()=>B===he||"decoder"in B&&B.setup===he;Wa({codec:ae,optimizeForLatency:!0,hardwareAcceleration:"prefer-hardware"},{current:Ee,output(me){if(!("decoder"in B))return;const $e=B;clearTimeout(le),le=void 0,Ka(S,me,me.displayWidth,me.displayHeight),!(B!==$e||$e.type==="playing")&&(B={...$e,type:"playing"},clearTimeout(o),S.onState("playing"))},error:fe}).then(me=>{if(me){if(B!==he){me.close();return}B={type:"decoding",setup:he,decoder:me,waitingKeyframe:!0},be(te,me),Ee()&&"decoder"in B&&he.missed&&(B.waitingKeyframe=!0,Z())}}).catch(me=>{Ee()&&fe(`H.264 decoder setup failed: ${me instanceof Error?me.message:String(me)}`)})},De=()=>B.type==="configuring"?B.codec:"decoder"in B?B.setup.codec:void 0,Ke=te=>{q.update(te);const ae=De();ae&&q.codec!==ae&&de(!0)},Oe=te=>{const ae=q.codec??De();if(!ae)return Z();if("decoder"in B)return be(te,B.decoder);if(!te.key)return de(!0);Ge(te,ae)},Re=te=>{if(Ke(te),!!te.picture){if(B.type==="configuring"){B.missed=!0;return}if("decoder"in B&&B.decoder.decodeQueueSize>2)return de(!0);!te.key&&(!("decoder"in B)||B.waitingKeyframe)||Oe(te)}};return ee(),T.addEventListener("open",Z),T.addEventListener("message",te=>{if(B.type==="stopped")return;if(typeof te.data=="string"){const he=Ja(te.data);he?.type==="error"&&fe(`Device video failed: ${he.message}`),he?.type==="reset"&&(q.clear(),de(!0));return}if(!(te.data instanceof ArrayBuffer))return;const ae=Ma(te.data);ae&&Re(ae)}),T.addEventListener("error",()=>fe("Device video connection failed")),T.addEventListener("close",te=>{fe(`Device video connection closed (code ${te.code}). Refresh devices to retry.`)}),G}function Ka(S,I,T,B){if(!Zt({width:T,height:B}))throw new Error("Device video produced invalid screen dimensions");(S.canvas.width!==T||S.canvas.height!==B)&&(S.canvas.width=T,S.canvas.height=B);const q=S.canvas.getContext("2d",{alpha:!1});if(!q)throw new Error("Device video canvas is unavailable");q.drawImage(I,0,0,T,B)}function Ja(S){try{const I=JSON.parse(S);if(!I||typeof I!="object")return;if("ok"in I&&I.ok===!1)return{type:"error",message:"error"in I&&typeof I.error=="string"?I.error:"the capture session failed"};if(Xa(I))return{type:"reset"}}catch{return}}function Xa(S){return"type"in S&&S.type==="video-session"&&"size"in S&&S.size!==null&&typeof S.size=="object"&&Zt(S.size)}function Zt(S){return!("width"in S)||!("height"in S)?!1:typeof S.width=="number"&&typeof S.height=="number"&&Number.isSafeInteger(S.width)&&Number.isSafeInteger(S.height)&&S.width>0&&S.height>0}const Ya=".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 Za=Y.template('