diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..20f3885 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,14 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "rules": { + "complexity": ["error", 9] + }, + "overrides": [ + { + "files": ["**/*.test.ts"], + "rules": { + "complexity": "off" + } + } + ] +} diff --git a/dist/gui.cjs b/dist/gui.cjs index fd0ff7b..ee98c84 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"),Tt=require("@opencode/plugin/gui"),fa=require("@opencode/schema/rpc"),p=require("effect"),pa=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)),Ot=p.Schema.Int.check(p.Schema.isGreaterThanOrEqualTo(0)),oi=p.Schema.Int.check(p.Schema.isBetween({minimum:0,maximum:1e6})),je=p.Schema.Literals(["ios","android"]).annotate({identifier:"Device.Platform"}),ma=p.Schema.Literals(["shutdown","booting","booted","unavailable"]).annotate({identifier:"Device.DeviceState"}),ga=p.Schema.Literals(["available","owned-by-this-host","owned-by-other","unknown"]).annotate({identifier:"Device.Ownership"}),ai=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=Ot.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"}),Ee=p.Schema.Struct({token:Mn,platform:je,deviceId:we,generation:Nn}).annotate({identifier:"Device.DeviceHandle"}),mi=p.Schema.String.check(p.Schema.isPattern(/^@?e[1-9][0-9]*$/u)).pipe(p.Schema.brand("Device.ElementRef")).annotate({identifier:"Device.ElementRef"}),ci=p.Schema.Struct({token:Mn,generation:Nn,revision:Ot,ref:mi}).annotate({identifier:"Device.SnapshotReference"}),Sa=p.Schema.Struct({ref:qe(mi),role:we,name:qe(si),value:qe(si),enabled:qe(p.Schema.Boolean),focused:qe(p.Schema.Boolean)}).annotate({identifier:"Device.AccessibilityNode"}),va=p.Schema.Struct({token:Mn,generation:Nn,revision:Ot,interactiveOnly:p.Schema.Boolean,complete:p.Schema.Boolean,nodes:p.Schema.Array(Sa)}).annotate({identifier:"Device.Snapshot"}),An=p.Schema.Struct({x:oi,y:oi}).annotate({identifier:"Device.LogicalPoint"}),wa=p.Schema.Struct({id:we,name:we,platform:je,osVersion:qe(we),state:ma,ownership:ga}).annotate({identifier:"Device.DeviceInfo"}),ba=p.Schema.Struct({supported:p.Schema.Array(ai),unsupported:p.Schema.Array(p.Schema.Struct({capability:ai,reason:we}))}).annotate({identifier:"Device.Capabilities"}),_a=p.Schema.Struct({id:we,name:qe(we)}).annotate({identifier:"Device.AppInfo"}),ka=p.Schema.Struct({app:we,state:p.Schema.Literals(["launched","foreground","unknown"])}).annotate({identifier:"Device.LaunchResult"}),xa=p.Schema.Struct({installed:p.Schema.Boolean,app:qe(we)}).annotate({identifier:"Device.InstallResult"}),Ea=p.Schema.Struct({data:p.Schema.Uint8ArrayFromBase64.check(p.Schema.isMaxLength(pa)),width:Ot,height:Ot,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"}),Ia=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:Ia,message:we,mayHaveExecuted:p.Schema.Boolean}).annotate({identifier:"Device.DeviceError"}),ct=p.Schema.optional,Ue=p.Schema.String.check(p.Schema.isMaxLength(2048)),gi=Ue.check(p.Schema.isMinLength(1)),Pn={sessionID:gi,connectionID:p.Schema.String},li={...Pn,requestID:p.Schema.String},di=p.Schema.Number.check(p.Schema.isInt(),p.Schema.isBetween({minimum:1,maximum:1e6})),hi=p.Schema.optional(p.Schema.Struct({width:di,height:di})),lt=p.Schema.Struct({sessionID:gi,serverID:Ue,location:p.Schema.optionalKey(p.Schema.Struct({directory:Ue}))}),Xt=p.Schema.Struct({handle:Ee,selection:p.Schema.optionalKey(p.Schema.Struct({platform:je,deviceId:Ue})),capabilities:ba,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:Ee}),p.Schema.Struct({type:p.Schema.Literal("launch"),handle:Ee,app:Ue,deepLink:ct(Ue),relaunch:ct(p.Schema.Boolean)}),p.Schema.Struct({type:p.Schema.Literal("install"),handle:Ee,artifactPath:Ue}),p.Schema.Struct({type:p.Schema.Literal("snapshot"),handle:Ee,interactiveOnly:ct(p.Schema.Boolean)}),p.Schema.Struct({type:p.Schema.Literal("press"),handle:Ee,reference:ci}),p.Schema.Struct({type:p.Schema.Literal("tap"),handle:Ee,point:An,viewport:hi}),p.Schema.Struct({type:p.Schema.Literal("fill"),handle:Ee,reference:ci,text:Ue}),p.Schema.Struct({type:p.Schema.Literal("swipe"),handle:Ee,from:An,to:An,viewport:hi}),p.Schema.Struct({type:p.Schema.Literal("key"),handle:Ee,key:ya}),p.Schema.Struct({type:p.Schema.Literal("screenshot"),handle:Ee}),p.Schema.Struct({type:p.Schema.Literal("close"),handle:Ee,shutdown:ct(p.Schema.Boolean)})]).pipe(p.Schema.toTaggedUnion("type")).annotate({identifier:"DeviceBridge.Operation"}),Ta=p.Schema.Union([p.Schema.Struct({type:p.Schema.Literal("devices"),devices:p.Schema.Array(wa)}),p.Schema.Struct({type:p.Schema.Literal("state"),state:p.Schema.NullOr(Xt)}),p.Schema.Struct({type:p.Schema.Literal("attachment"),attachment:Xt}),p.Schema.Struct({type:p.Schema.Literal("apps"),apps:p.Schema.Array(_a)}),p.Schema.Struct({type:p.Schema.Literal("launch"),result:ka}),p.Schema.Struct({type:p.Schema.Literal("install"),result:xa}),p.Schema.Struct({type:p.Schema.Literal("snapshot"),snapshot:va}),p.Schema.Struct({type:p.Schema.Literal("screenshot"),screenshot:Ea}),p.Schema.Struct({type:p.Schema.Literal("done")})]).pipe(p.Schema.toTaggedUnion("type")).annotate({identifier:"DeviceBridge.Value"}),Yt=p.Schema.Union([p.Schema.Struct({type:p.Schema.Literal("success"),value:Ta}),p.Schema.Struct({type:p.Schema.Literal("failure"),error:Rn})]).pipe(p.Schema.toTaggedUnion("type")).annotate({identifier:"DeviceBridge.Outcome"}),Oa=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"}),Aa=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)};fa.Rpc.define({id:"device.control",methods:{attach:{input:Me(p.Schema.Struct({...Pn,version:p.Schema.Literal(1)})),output:Me(p.Schema.Literals(["closed","replaced"])),errors:Jt},state:{input:Me(p.Schema.Struct({...Pn,state:p.Schema.NullOr(Xt)})),output:Me(p.Schema.Void),errors:Jt},command:{input:Me(p.Schema.Struct(li)),output:Me(yi),errors:Jt},result:{input:Me(p.Schema.Struct({...li,outcome:Yt})),output:Me(p.Schema.Void),errors:Jt}},events:{control:{schema:Me(Aa)}}});const ui=p.Schema.String.check(p.Schema.isMaxLength(2048)),Ca=Tt.Ipc.define({id:"demul.device.bridge.v2",state:p.Schema.Array(p.Schema.Struct({scope:lt,state:p.Schema.NullOr(Xt)})),methods:{connect:{input:lt},disconnect:{input:lt},list:{input:p.Schema.Struct({platform:p.Schema.optional(je),serverID:ui,sessionID:p.Schema.optionalKey(ui)}),output:Yt},access:{input:p.Schema.Struct({scope:lt,enabled:p.Schema.Boolean}),output:Yt},manual:{input:p.Schema.Struct({scope:lt,operation:yi}),output:Yt},stream:{input:p.Schema.Struct({scope:lt,handle:Ee}),output:Oa}}});function fi(y){let x;return{async run(L,C){x?.abort();const q=new AbortController;x=q,y.pending(!0);try{const s=await L(q.signal);x===q&&C(s)}catch(s){x===q&&!q.signal.aborted&&y.error(s)}finally{x===q&&(x=void 0,y.pending(!1))}},cancel(){x?.abort(),x=void 0,y.pending(!1)}}}const Pa=1397050709,Ba={1:16,2:24},pi=new Uint8Array([0,0,0,1]);function Na(y){const x=y instanceof Uint8Array?y:new Uint8Array(y);if(x.byteLength<4)return;const L=new DataView(x.buffer,x.byteOffset,x.byteLength);return L.getUint32(0)===Pa?Ma(x,L):Si(x,performance.now()*1e3,!1)}function Ma(y,x){if(y.byteLength<16)return;const L=Ba[x.getUint8(4)];if(!L||y.byteLength<=L)return;const C=x.getBigUint64(8);if(!(C>BigInt(Number.MAX_SAFE_INTEGER)))return Si(y.subarray(L),Number(C),(x.getUint8(5)&1)===1,L===24?Number(x.getBigUint64(16))/1e3:void 0)}function Si(y,x,L,C){const q=Bn(y,0);if(q===void 0||q>=y.byteLength)return;const s=Ra(y);return{bytes:y,...s,key:s.key||s.picture&&L,timestamp:x,serverTimestamp:C}}function Ra(y){let x=!1,L=!1,C,q;for(const s of Ua(y)){const ae=s[0]&31;ae===5&&(x=!0),ae>=1&&ae<=5&&(L=!0),ae===7&&(C=s),ae===8&&(q=s)}return{key:x,picture:L,codec:C&&ja(C),sps:C,pps:q}}function*Ua(y){let x=Bn(y,0);if(x!==void 0){for(let L=x;Lx&&(yield y.subarray(x,L)),x=C,L=C-1)}xx.toString(16).padStart(2,"0")).join("")}`}class $a{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 L=[];if(!x.sps&&this.sps&&L.push(pi,this.sps),!x.pps&&this.pps&&L.push(pi,this.pps),!L.length)return x.bytes;L.push(x.bytes);const C=new Uint8Array(L.reduce((s,ae)=>s+ae.byteLength,0));let q=0;for(const s of L)C.set(s,q),q+=s.byteLength;return C}clear(){this.codec=void 0,this.sps=void 0,this.pps=void 0}}async function Fa(y,x){const L=await VideoDecoder.isConfigSupported(y);if(!x.current())return;if(!L.supported)throw new Error(`H.264 codec ${y.codec} is not supported by this browser`);const C=new VideoDecoder({output(q){try{x.current()&&x.output(q)}catch(s){x.error(`Device video draw failed: ${s instanceof Error?s.message:String(s)}`)}finally{q.close()}},error(q){x.current()&&x.error(`H.264 decoder failed: ${q.message}`)}});try{C.configure(L.config??y)}catch(q){throw C.close(),new Error(`H.264 decoder configuration failed: ${q instanceof Error?q.message:String(q)}`)}return C}function Wa(y,x,L,C){if(!(!y||y!==x||L!=="playing"||!Qt(C)))return{token:y,width:C.width,height:C.height}}function Ha(y,x){return!!x&&y.token===x.token&&y.width===x.width&&y.height===x.height}function Va(y){let x=!1,L={type:"idle"};const C=()=>{try{return qa(y)}catch(s){return y.onState("paused"),y.onError(s instanceof Error?s.message:String(s)),()=>{}}},q=()=>{if(L.type==="destroyed")return;const s=x&&document.visibilityState==="visible";if(s&&L.type==="idle"){L={type:"running",stop:C()};return}s||L.type!=="running"||(L.stop(),L={type:"idle"},y.onState(x?"paused":"idle"))};return document.addEventListener("visibilitychange",q),{active(s){L.type!=="destroyed"&&(x=s,q())},destroy(){if(L.type==="destroyed")return;const s=L;L={type:"destroyed"},document.removeEventListener("visibilitychange",q),s.type==="running"&&s.stop(),y.canvas.width=0,y.canvas.height=0,y.onState("idle")}}}function za(y,x,L){if(![y.left,y.top,y.width,y.height,x.width,x.height,L.x,L.y].every(Number.isFinite)||y.width<=0||y.height<=0||!Qt(x))return;const C=Math.min(y.width/x.width,y.height/x.height),q=x.width*C,s=x.height*C,ae=L.x-y.left-(y.width-q)/2,ee=L.y-y.top-(y.height-s)/2;if(!(ae<0||ee<0||ae>=q||ee>=s))return{x:Math.min(x.width-1,Math.floor(ae/C)),y:Math.min(x.height-1,Math.floor(ee/C))}}function qa(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 L=new WebSocket(x,`opencode-device.${y.stream.token}`);let C={type:"waiting"};const q=new $a;let s,ae;const ee=()=>{clearTimeout(s),s=setTimeout(()=>ue("Device video did not produce a screen within 15 seconds. Refresh devices to retry."),15e3)};L.binaryType="arraybuffer",y.onState("connecting");const Q=()=>{if(L.readyState===WebSocket.OPEN)try{L.send(JSON.stringify({type:"reset-video",ack:!1}))}catch{}},ce=(te=!1)=>{clearTimeout(ae),ae=void 0;const oe=C;C={type:"waiting"},"decoder"in oe&&oe.decoder.state!=="closed"&&oe.decoder.close(),te&&oe.type==="playing"&&(ee(),y.onState("connecting")),te&&Q()},G=()=>{C.type!=="stopped"&&(ce(),C={type:"stopped"},clearTimeout(s),L.close())},ue=te=>{C.type!=="stopped"&&(G(),y.onError(te.replaceAll(y.stream.token,"[redacted]").slice(0,512)),y.onState("paused"))},be=(te,oe)=>{if(!(!("decoder"in C)||C.decoder!==oe||oe.state!=="configured")){C.waitingKeyframe=!1;try{ae??=setTimeout(()=>{ae=void 0,ce(!0)},15e3),oe.decode(new EncodedVideoChunk({type:te.key?"key":"delta",timestamp:te.timestamp,data:q.bytes(te)}))}catch(de){ue(`H.264 decode failed: ${de instanceof Error?de.message:String(de)}`)}}},Ge=(te,oe)=>{ce();const de={type:"configuring",codec:oe,missed:!1};C=de;const xe=()=>C===de||"decoder"in C&&C.setup===de;Fa({codec:oe,optimizeForLatency:!0,hardwareAcceleration:"prefer-hardware"},{current:xe,output(pe){if(!("decoder"in C))return;const $e=C;clearTimeout(ae),ae=void 0,Ga(y,pe,pe.displayWidth,pe.displayHeight),!(C!==$e||$e.type==="playing")&&(C={...$e,type:"playing"},clearTimeout(s),y.onState("playing"))},error:ue}).then(pe=>{if(pe){if(C!==de){pe.close();return}C={type:"decoding",setup:de,decoder:pe,waitingKeyframe:!0},be(te,pe),xe()&&"decoder"in C&&de.missed&&(C.waitingKeyframe=!0,Q())}}).catch(pe=>{xe()&&ue(`H.264 decoder setup failed: ${pe instanceof Error?pe.message:String(pe)}`)})},Te=()=>C.type==="configuring"?C.codec:"decoder"in C?C.setup.codec:void 0,Ke=te=>{q.update(te);const oe=Te();oe&&q.codec!==oe&&ce(!0)},Oe=te=>{const oe=q.codec??Te();if(!oe)return Q();if("decoder"in C)return be(te,C.decoder);if(!te.key)return ce(!0);Ge(te,oe)},Re=te=>{if(Ke(te),!!te.picture){if(C.type==="configuring"){C.missed=!0;return}if("decoder"in C&&C.decoder.decodeQueueSize>2)return ce(!0);!te.key&&(!("decoder"in C)||C.waitingKeyframe)||Oe(te)}};return ee(),L.addEventListener("open",Q),L.addEventListener("message",te=>{if(C.type==="stopped")return;if(typeof te.data=="string"){const de=Ka(te.data);de?.type==="error"&&ue(`Device video failed: ${de.message}`),de?.type==="reset"&&(q.clear(),ce(!0));return}if(!(te.data instanceof ArrayBuffer))return;const oe=Na(te.data);oe&&Re(oe)}),L.addEventListener("error",()=>ue("Device video connection failed")),L.addEventListener("close",te=>{ue(`Device video connection closed (code ${te.code}). Refresh devices to retry.`)}),G}function Ga(y,x,L,C){if(!Qt({width:L,height:C}))throw new Error("Device video produced invalid screen dimensions");(y.canvas.width!==L||y.canvas.height!==C)&&(y.canvas.width=L,y.canvas.height=C);const q=y.canvas.getContext("2d",{alpha:!1});if(!q)throw new Error("Device video canvas is unavailable");q.drawImage(x,0,0,L,C)}function Ka(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(Ja(x))return{type:"reset"}}catch{return}}function Ja(y){return"type"in y&&y.type==="video-session"&&"size"in y&&y.size!==null&&typeof y.size=="object"&&Qt(y.size)}function Qt(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 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 Xa=X.template('