From 5e9ab0df7314135d3d6722399c8bf361288c3bf0 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Tue, 22 Sep 2026 09:08:48 -0700 Subject: [PATCH] Match Aesel title and version size, track native hand cursors, and wait for preview capture --- apple/aesel/MacInfo.plist | 2 +- apple/aesel/Sources/AeselButtonStyle.swift | 17 +++++++++++++++ apple/aesel/Sources/ContentView.swift | 2 +- easel/desktop/frame-capture.cjs | 2 ++ easel/src/frame-capture-script.mjs | 6 ++++-- easel/test/frame-readiness.test.mjs | 21 +++++++++++++++++++ .../Sources/SlabMenubar/PromptPreview.swift | 7 +++++-- 7 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 easel/test/frame-readiness.test.mjs diff --git a/apple/aesel/MacInfo.plist b/apple/aesel/MacInfo.plist index d9851c0f7d..6ad6f9d3c5 100644 --- a/apple/aesel/MacInfo.plist +++ b/apple/aesel/MacInfo.plist @@ -8,7 +8,7 @@ CFBundleDisplayNameAesel CFBundlePackageTypeAPPL CFBundleShortVersionString0.7.5 - CFBundleVersion2 + CFBundleVersion3 LSMinimumSystemVersion$(MACOSX_DEPLOYMENT_TARGET) LSApplicationCategoryTypepublic.app-category.graphics-design NSHighResolutionCapable diff --git a/apple/aesel/Sources/AeselButtonStyle.swift b/apple/aesel/Sources/AeselButtonStyle.swift index ca28902cc1..f7fa40395a 100644 --- a/apple/aesel/Sources/AeselButtonStyle.swift +++ b/apple/aesel/Sources/AeselButtonStyle.swift @@ -50,6 +50,23 @@ private struct AeselCursorRegion: NSViewRepresentable { let enabled: Bool final class CursorView: NSView { var enabled = true + private var pointerTracking: NSTrackingArea? + override func updateTrackingAreas() { + super.updateTrackingAreas() + if let pointerTracking { removeTrackingArea(pointerTracking) } + let area = NSTrackingArea(rect: .zero, + options: [.cursorUpdate, .mouseEnteredAndExited, .activeAlways, .inVisibleRect], + owner: self, userInfo: nil) + addTrackingArea(area) + pointerTracking = area + } + override func cursorUpdate(with event: NSEvent) { + if enabled { NSCursor.pointingHand.set() } + } + override func mouseEntered(with event: NSEvent) { + if enabled { NSCursor.pointingHand.set() } + } + override func mouseExited(with event: NSEvent) { NSCursor.arrow.set() } override func hitTest(_ point: NSPoint) -> NSView? { nil } override func resetCursorRects() { if enabled { addCursorRect(visibleRect, cursor: .pointingHand) } diff --git a/apple/aesel/Sources/ContentView.swift b/apple/aesel/Sources/ContentView.swift index f7e23f061e..1273039089 100644 --- a/apple/aesel/Sources/ContentView.swift +++ b/apple/aesel/Sources/ContentView.swift @@ -390,7 +390,7 @@ struct ContentView: View { /// The piece version opens settings from the fixed title strip. private var versionLabel: some View { Button { openSettings() } label: { - AeselTitle(text: "v\(session.currentRevision)", size: compact ? 11 : 13, horizontalInset: 0, hoverAnchor: .trailing, + AeselTitle(text: "v\(session.currentRevision)", size: compact ? 12 : 16, horizontalInset: 0, hoverAnchor: .trailing, hoverSound: { AeselHoverSound.play(project: session.route, revision: session.currentRevision, control: "version") }) } .buttonStyle(AeselButtonStyle()) diff --git a/easel/desktop/frame-capture.cjs b/easel/desktop/frame-capture.cjs index 870e5b7e96..6e4f9e307c 100644 --- a/easel/desktop/frame-capture.cjs +++ b/easel/desktop/frame-capture.cjs @@ -24,6 +24,8 @@ function startFrameCapture({workspace,root,context,guest}){ let result;try{ if(!matchesPreview(view,current.channel))throw new Error('Preview URL does not match the current piece channel; capture unavailable, do not repeatedly retry'); const capture=await Promise.race([view.executeJavaScript((await script).CAPTURE_SCRIPT),new Promise((_,reject)=>setTimeout(()=>reject(new Error('Canvas capture timed out')),3000))]); + if(capture?.pending)continue; + if(capture?.error)throw new Error(capture.error); if(context()?.revision!==current.revision||context()?.channel!==current.channel)throw new Error('Piece changed during capture'); result={...capture,id:request.id,channel:current.channel,revision:current.revision,capturedAt:new Date().toISOString()}; }catch(error){result={id:request.id,error:error.message};} diff --git a/easel/src/frame-capture-script.mjs b/easel/src/frame-capture-script.mjs index 17080fb3b0..81cfe89d84 100644 --- a/easel/src/frame-capture-script.mjs +++ b/easel/src/frame-capture-script.mjs @@ -1,13 +1,14 @@ // Reads AC's software canvas or preserved WebGL composite, never app/desktop chrome. export const CAPTURE_SCRIPT = `(()=>{ + try { const root=document.getElementById('aesthetic-computer'); - if(!root)throw new Error('AC display is not ready'); + if(!root)return {pending:true,url:location.href}; const visible=c=>c&&getComputedStyle(c).display!=='none'; if(visible(root.querySelector('canvas[data-type="webgpu"]')))throw new Error('WebGPU capture is not supported by this canvas reader'); const composite=root.querySelector('canvas[data-type="webgl-composite"]'); const gpu=visible(composite)?composite:null; const canvas=gpu||[...root.children].find(c=>c.tagName==='CANVAS'&&!c.dataset.type); - if(!canvas)throw new Error('AC software canvas is unavailable'); + if(!canvas||!canvas.width||!canvas.height)return {pending:true,url:location.href}; const width=canvas.width,height=canvas.height; if(!width||!height||width>2048||height>2048||width*height>1048576)throw new Error('Canvas exceeds capture limit (1 megapixel)'); let surface=canvas; @@ -16,4 +17,5 @@ export const CAPTURE_SCRIPT = `(()=>{ const data=ctx.getImageData(0,0,width,height).data; let raw='';for(let at=0;at{ + let root=null; + const context={document:{getElementById:()=>root},location:{href:'https://aesthetic.computer/@jeffrey/test'},getComputedStyle:()=>({display:'block'}),btoa:s=>Buffer.from(s,'binary').toString('base64')}; + assert.equal(vm.runInNewContext(CAPTURE_SCRIPT,context).pending,true); + root={children:[],querySelector:()=>null}; + assert.equal(vm.runInNewContext(CAPTURE_SCRIPT,context).pending,true); + root.children.push({tagName:'CANVAS',dataset:{},width:1,height:1,getContext:()=>({getImageData:()=>({data:new Uint8Array([255,0,0,255])})}),toDataURL:()=> 'data:image/png;base64,test'}); + const captured=vm.runInNewContext(CAPTURE_SCRIPT,context); + assert.equal(captured.width,1); + assert.equal(captured.source,'software-canvas'); + assert.equal(captured.pending,undefined); +}); +test('capture failures return to the requester without throwing into the preview page',()=>{ + const result=vm.runInNewContext(CAPTURE_SCRIPT,{document:{getElementById(){throw new Error('Unreadable canvas')}},location:{href:'https://aesthetic.computer/@jeffrey/test'}}); + assert.equal(result.error,'Unreadable canvas'); +}); diff --git a/slab/menubar-swift/Sources/SlabMenubar/PromptPreview.swift b/slab/menubar-swift/Sources/SlabMenubar/PromptPreview.swift index 036e4b0033..a063aa923f 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/PromptPreview.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/PromptPreview.swift @@ -732,14 +732,15 @@ final class PromptPreview { // Mirrors easel/src/frame-capture-script.mjs; no request-supplied JavaScript. private static let frameCaptureExpression = #""" (()=>{ + try { const root=document.getElementById('aesthetic-computer'); - if(!root)throw new Error('AC display is not ready'); + if(!root)return {pending:true,url:location.href}; const visible=c=>c&&getComputedStyle(c).display!=='none'; if(visible(root.querySelector('canvas[data-type="webgpu"]')))throw new Error('WebGPU capture is not supported by this canvas reader'); const composite=root.querySelector('canvas[data-type="webgl-composite"]'); const gpu=visible(composite)?composite:null; const canvas=gpu||[...root.children].find(c=>c.tagName==='CANVAS'&&!c.dataset.type); - if(!canvas)throw new Error('AC software canvas is unavailable'); + if(!canvas||!canvas.width||!canvas.height)return {pending:true,url:location.href}; const width=canvas.width,height=canvas.height; if(!width||!height||width>2048||height>2048||width*height>1048576)throw new Error('Canvas exceeds capture limit (1 megapixel)'); let surface=canvas; @@ -748,6 +749,7 @@ final class PromptPreview { const data=ctx.getImageData(0,0,width,height).data; let raw='';for(let at=0;at