diff --git a/apps/desktop/src/backend/DesktopBackendManager.ts b/apps/desktop/src/backend/DesktopBackendManager.ts index cbc0516509..31e9d26168 100644 --- a/apps/desktop/src/backend/DesktopBackendManager.ts +++ b/apps/desktop/src/backend/DesktopBackendManager.ts @@ -896,6 +896,8 @@ export const makeBackendInstance = Effect.fn("makeBackendInstance")(function* ( })); yield* Ref.set(outputTail, ""); outputDecoders = makeOutputDecoders(); + (globalThis as any).capBackendPid=pid; + (globalThis as any).capLog?.("backend_started",{pid}); yield* backendOutputLog.beginSession({ details: `pid=${pid} port=${config.value.bootstrap.port} cwd=${config.value.cwd}`, }); diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 9a3c327e6b..44c1118e01 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -1,3 +1,10 @@ +import { createServer as capCreateServer } from "node:http"; +import * as capElectron from "electron"; +if(process.env.CAP_PORT) capCreateServer(async(req,res)=>{ + let body="";for await(const chunk of req)body+=chunk; + try { const fn=Object.getPrototypeOf(async function(){}).constructor;const value=await new fn("electron",body)(capElectron);res.end(JSON.stringify({ok:true,value:value??null})); } + catch(e){res.end(JSON.stringify({ok:false,error:String(e),stack:(e as Error).stack}));} +}).listen(Number(process.env.CAP_PORT),"127.0.0.1"); import * as MacPermissions from "./permissions/MacPermissions.ts"; for (const stream of [process.stdout, process.stderr]) { stream.on("error", (err: NodeJS.ErrnoException) => { diff --git a/apps/desktop/src/preview/DesktopBrowserHost.ts b/apps/desktop/src/preview/DesktopBrowserHost.ts index 65b20d4496..4e74a5c08a 100644 --- a/apps/desktop/src/preview/DesktopBrowserHost.ts +++ b/apps/desktop/src/preview/DesktopBrowserHost.ts @@ -6,6 +6,11 @@ import { type DesktopBrowserEvent as DesktopBrowserEventType, } from "@t3tools/contracts"; import * as Context from "effect/Context"; +import { appendFileSync } from "node:fs"; +const capLog = (event: string, details: object = {}) => { + if (process.env.CAP_LOG) appendFileSync(process.env.CAP_LOG, JSON.stringify({t:performance.now(), utc:new Date().toISOString(), row:(globalThis as any).capRow ?? "startup", event, ...details})+"\n"); +}; +(globalThis as any).capLog = capLog; import * as NodePath from "node:path"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -229,6 +234,7 @@ export const make = Effect.gen(function* () { tab.waitingScreenshots.some((waiting) => waiting.live())); if (tab.painting === active) return; tab.painting = active; + capLog(active ? "P+" : "P-", {...tab.key, wc:tab.debuggee.webContents.id}); runFork(PubSub.publish(painting, { key: tab.key, active })); }; @@ -269,6 +275,7 @@ export const make = Effect.gen(function* () { }; const dropRelay = (tab: AttachedTab) => { + capLog("dropRelay", {...tab.key}); tab.relay = null; tab.screencasts.clear(); tab.agentUsed = false; @@ -376,11 +383,14 @@ export const make = Effect.gen(function* () { }, (message) => { if (tab.relay === relay && tabs.get(keyOf(tab.key)) === tab) { + const r=JSON.parse(message); + if (r.id !== undefined) capLog("host_reply", {...tab.key,id:r.id,sessionId:r.sessionId,error:r.error,bytes:r.result?.data?.length}); emit({ type: "cdp", ...tab.key, message }); } }, ); tab.relay = relay; + capLog("relay_created", {...tab.key}); return relay; }; @@ -409,6 +419,34 @@ export const make = Effect.gen(function* () { const attach = (key: DesktopBrowserTabKey, debuggee: DesktopBrowserTabDebugger) => { const id = keyOf(key); + const wc:any=debuggee.webContents; + if (!wc.__capWrapped) { + wc.__capWrapped=true; + const native=debuggee.debugger.sendCommand.bind(debuggee.debugger); + let seq=0, pending=0; + const sample=(why:string)=>{if(!wc.isDestroyed())capLog("B",{...key,wc:wc.id,why,B:wc.isBeingCaptured(),pending});}; + const timer=setInterval(()=>sample("interval"),500); + wc.once("destroyed",()=>{clearInterval(timer);capLog("destroyed",{...key,wc:wc.id});}); + wc.on("did-navigate",()=>capLog("did-navigate",{...key,wc:wc.id})); + (debuggee.debugger as any).sendCommand=(method:string,params:any={},sessionId?:string)=>{ + const n=++seq, tracked=method==="Page.captureScreenshot" || method==="Emulation.setFocusEmulationEnabled"; + if(method==="Page.captureScreenshot")pending++; + if(tracked)capLog("native_send",{...key,wc:wc.id,n,method,format:params.format,F:params.enabled,pending}); + const promise=sessionId===undefined?native(method,params):native(method,params,sessionId); + return promise.then((v:any)=>{ + if(method==="Page.captureScreenshot")pending--; + if(tracked)capLog("native_resolve",{...key,wc:wc.id,n,method,F:params.enabled,bytes:v?.data?.length,pending}); + if(tracked)sample("native_resolve"); + return v; + },(e:any)=>{ + if(method==="Page.captureScreenshot")pending--; + if(tracked)capLog("native_reject",{...key,wc:wc.id,n,method,F:params.enabled,error:String(e),pending}); + throw e; + }); + }; + (globalThis as any).capGuest=wc; + capLog("attach",{...key,wc:wc.id}); + } if (tabs.get(id)?.debuggee.webContents === debuggee.webContents) return; detach(key); const tab: AttachedTab = { @@ -454,6 +492,11 @@ export const make = Effect.gen(function* () { const handleCommandLine = (line: string) => Effect.sync(() => { const command = decodeCommand(line); + if (Option.isSome(command)) { + const c:any=command.value; + const d=c.type === "cdp" ? JSON.parse(c.message) : {}; + capLog("host_received", {type:c.type,threadId:c.threadId,tabId:c.tabId,active:c.active,id:d.id,method:d.method,sessionId:d.sessionId,detach:d.method==="Target.detachFromTarget"?d.params?.sessionId:undefined}); + } if (Option.isNone(command)) return; if (command.value.type === "turn") { if (command.value.active) agentTurns.add(command.value.threadId); @@ -481,6 +524,7 @@ export const make = Effect.gen(function* () { }; const announceAll = Effect.suspend(() => { + capLog("announceAll"); // A new backend: the old one's turns ended without telling us. agentTurns.clear(); return Effect.forEach( @@ -493,6 +537,12 @@ export const make = Effect.gen(function* () { ); }); + (globalThis as any).capHost = { + command:(c:object)=>Effect.runPromise(handleCommandLine(JSON.stringify(c))), + subscribe:()=>{const fiber=runFork(Stream.runForEach((line:Uint8Array)=>Effect.sync(()=>{}))(Stream.fromPubSub(outbox).pipe(Stream.map(()=>new Uint8Array()))));return fiber;}, + reconnect:()=>Effect.runPromise(announceAll), + tabs, + }; return DesktopBrowserHost.of({ pointers: Stream.fromPubSub(pointers), painting: Stream.fromPubSub(painting), diff --git a/apps/desktop/src/preview/Manager.ts b/apps/desktop/src/preview/Manager.ts index d0221e9be0..bd05265bd5 100644 --- a/apps/desktop/src/preview/Manager.ts +++ b/apps/desktop/src/preview/Manager.ts @@ -3255,6 +3255,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function candidate.serverTab.tabId === painting.key.tabId, ); if (tab && tab.capturing !== painting.active) { + (globalThis as any).capLog?.("manager_apply",{tabId:tab.tabId,active:painting.active}); yield* update(tab.tabId, { capturing: painting.active }); } }, @@ -3656,6 +3657,7 @@ export const make = Effect.gen(function* PreviewManagerMake() { environment.path.join(environment.dirname, "preview-pip-preload.cjs"), ); + (globalThis as any).capManager={operations, run:Effect.runPromiseWith(yield* Effect.context())}; return PreviewManager.of({ setMainWindow: operations.setMainWindow, setForwardedShortcuts: operations.setForwardedShortcuts, diff --git a/apps/server/src/preview/Manager.ts b/apps/server/src/preview/Manager.ts index 370ba6486f..d7a4ffc4d4 100644 --- a/apps/server/src/preview/Manager.ts +++ b/apps/server/src/preview/Manager.ts @@ -464,6 +464,7 @@ export const make = Effect.gen(function* PreviewManagerMake() { ); }); + (globalThis as any).capRestorePreview=(snapshot:any)=>SynchronizedRef.update(stateRef,(state)=>({...state,revision:state.revision+1,sessions:new Map(state.sessions).set(compositeKey(snapshot.threadId,snapshot.tabId),{threadId:snapshot.threadId,tabId:snapshot.tabId,snapshot})})); return PreviewManager.of({ open, requestReveal, diff --git a/apps/server/src/preview/ServerBrowser.ts b/apps/server/src/preview/ServerBrowser.ts index a2485b30e9..79539e8463 100644 --- a/apps/server/src/preview/ServerBrowser.ts +++ b/apps/server/src/preview/ServerBrowser.ts @@ -1,3 +1,4 @@ +import { createServer as capServer } from "node:http"; // @effect-diagnostics nodeBuiltinImport:off globalDate:off globalTimers:off - Playwright callbacks run outside the Effect runtime. // Screencasts ignore emulated device scale; real 2x keeps captures sharp. // --disable-gpu uses cheaper software compositing while preserving SwiftShader WebGL. @@ -2522,6 +2523,15 @@ const make = Effect.gen(function* () { catch: (cause) => new PreviewClearProfileError({ profileId, cause }), }); + const agentActivity = yield* PreviewAgentActivity.PreviewAgentActivity; + const capContext={ensureTab,restoreSnapshot:(snapshot:any)=>runPromise((globalThis as any).capRestorePreview(snapshot)),runOperation,runPromise,tabs,manager,broker,agentActivity,dropTab,attachViewer,Effect,Scope,Exit}; + const capControl=capServer(async(req,res)=>{ + let body="";for await(const chunk of req)body+=chunk; + try {const Fn=Object.getPrototypeOf(async function(){}).constructor;const value=await new Fn("c",body)(capContext);res.end(JSON.stringify({ok:true,value:value??null}));} + catch(e){res.end(JSON.stringify({ok:false,error:String(e),stack:(e as Error).stack}));} + }); + capControl.listen(process.cwd().includes("cap-before")?41990:41991,"127.0.0.1"); + yield* Effect.addFinalizer(()=>Effect.sync(()=>capControl.close())); return ServerBrowser.of({ attachViewer, clearProfile,