diff --git a/easel/README.md b/easel/README.md index 691420c00f..aee7268b12 100644 --- a/easel/README.md +++ b/easel/README.md @@ -72,7 +72,12 @@ credit. The equivalent uses the server's pack rate ($5 per million braincells), not the underlying provider's token price. Claude and Codex keep their model controls. Desktop notebook text wraps beside the preview and returns to full width below it; -scrolling and preview resizing update the available space. +scrolling and preview resizing update the available space. The preview has one +manually resized footprint, with no hover zoom. Clicking it gives the piece its +keyboard; clicking the notebook returns to message input. The donkey's temporary +parenthetical thought bubble follows public activity and clears when the run ends. +New desktop windows show a themed notebook on first paint, before engine startup; +terminal initialization stays hidden. `/about`, or clicking **AESEL**, opens the feature map. Click **@handle** to open your profile in a browser. Header targets highlight on hover in terminals that diff --git a/easel/desktop/donkey.css b/easel/desktop/donkey.css index b8a07f2cf7..b47ac8fed6 100644 --- a/easel/desktop/donkey.css +++ b/easel/desktop/donkey.css @@ -269,16 +269,11 @@ body::before{left:10px} .preview-resize-edge[data-edge="ne"]{right:-4px;top:-4px;width:16px;height:16px;cursor:nesw-resize} .preview-resize-edge[data-edge="se"]{right:-4px;bottom:-4px;width:16px;height:16px;cursor:nwse-resize} .preview-resize-edge[data-edge="sw"]{left:-4px;bottom:-4px;width:16px;height:16px;cursor:nesw-resize} -body[data-preview-engaged="true"] .preview-resize-edge,body[data-preview-mode="pinned"] .preview-resize-edge,#artifact-shell[data-resizing] .preview-resize-edge{pointer-events:auto} -body.preview-fullscreen .preview-resize-edge,body[data-preview-medium]:not([data-preview-medium="piece"]):not([data-preview-medium=""]) .preview-resize-edge{display:none} -body:not(.preview-fullscreen)[data-preview-engaged="true"] #artifact-shell[data-resized],body:not(.preview-fullscreen)[data-preview-mode="pinned"] #artifact-shell[data-resized],body:not(.preview-fullscreen) #artifact-shell[data-resizing]{width:var(--resized-preview-width);height:var(--resized-preview-height);right:var(--resized-preview-right);top:var(--resized-preview-top)} +body.preview-fullscreen .preview-resize-edge{display:none} #artifact-shell{box-sizing:border-box} .preview-resize-edge{pointer-events:auto} -body:not(.preview-fullscreen) #artifact-shell[data-resized]{width:var(--resized-preview-compact-width);height:var(--resized-preview-compact-height);right:var(--resized-preview-right);top:var(--resized-preview-top)} -body:not(.preview-fullscreen)[data-preview-engaged="true"] #artifact-shell[data-resized],body:not(.preview-fullscreen)[data-preview-mode="pinned"] #artifact-shell[data-resized]{width:var(--resized-preview-width);height:var(--resized-preview-height)} -body:not(.preview-fullscreen) #artifact-shell[data-resizing]{width:var(--resized-preview-drag-width)!important;height:var(--resized-preview-drag-height)!important;transition:none!important} #prompt-feedback{width:0;height:1em;vertical-align:baseline} @@ -308,3 +303,11 @@ body:not(.preview-fullscreen) #artifact-shell{box-shadow:1px 1px 0 #282130,3px 3 #prompt-feedback{width:calc(var(--donkey-size)*512/416 + 16px)} #provider-menu .build-status{font-size:12px;opacity:.8;overflow-wrap:anywhere}#provider-menu .build-status[data-status=ready],#provider-menu .build-status[data-status=modified]{color:var(--aesel-accent);opacity:1} + +/* Preview sizing belongs to the frame grips, never hover or keyboard focus. */ +body:not(.preview-fullscreen) #artifact-shell[data-resized]{width:var(--resized-preview-width);height:var(--resized-preview-height);right:var(--resized-preview-right);top:var(--resized-preview-top);transition:none} +#activity-caption{position:relative;display:inline-block;vertical-align:middle;max-width:min(34ch,calc(100% - 100px));margin-left:8px;padding:.35em .65em;border:1px solid color-mix(in srgb,var(--aesel-foreground) 25%,transparent);border-radius:1em;background:color-mix(in srgb,var(--aesel-background) 94%,var(--aesel-foreground));opacity:.85} +#activity-caption::before,#activity-caption::after{content:"";position:absolute;border:1px solid color-mix(in srgb,var(--aesel-foreground) 25%,transparent);border-radius:50%;background:var(--aesel-background);top:60%} +#activity-caption::before{width:5px;height:5px;left:-9px} +#activity-caption::after{width:2px;height:2px;left:-15px;top:72%} +#activity-caption[hidden]{display:none} diff --git a/easel/desktop/index.html b/easel/desktop/index.html index 26d34d28a1..4c2a951be1 100644 --- a/easel/desktop/index.html +++ b/easel/desktop/index.html @@ -1,8 +1,8 @@
-chmod +x Easel-0.7.0-linux-x86_64.AppImage ./Easel-0.7.0-linux-x86_64.AppImage
ZIP archive · signed and notarized · macOS 12+
+ +ZIP archive · signed and notarized · macOS 12+
If FUSE is unavailable, try --appimage-extract-and-run. On Ubuntu, use the .deb above to install the required sandbox. Do not disable the sandbox.
aesel requires sharing conversations and artifact revision references with authorized Aesthetic Computer staff for product improvement, including when using your own provider. Read and accept the disclosure before use. Hosted generation requires account access; bringing your own Claude or Codex requires its CLI and account.
Tested on Ubuntu 24.04 x86-64. ARM builds are not included. SHA-256 checksums · Aesthetic Computer
diff --git a/easel/desktop/spriteland.js b/easel/desktop/spriteland.js index 5c967c7c8f..17f4817119 100644 --- a/easel/desktop/spriteland.js +++ b/easel/desktop/spriteland.js @@ -22,7 +22,7 @@ } return c; } - const wood=woodTile();document.documentElement.style.setProperty('--aesel-wood-tile',`url(${wood.toDataURL()})`);let theme='#463264',last=''; + const wood=woodTile();document.documentElement.style.setProperty('--aesel-wood-tile',`url(${wood.toDataURL()})`);let theme=window.aesel?.initialTheme?.background||'#463264',last=''; // The DOM ruling follows transcript scroll; this canvas supplies paper color. function tile(){const c=make(32,32),g=c.getContext('2d');g.fillStyle=theme;g.fillRect(0,0,32,32);return c;} diff --git a/easel/desktop/startup.js b/easel/desktop/startup.js new file mode 100644 index 0000000000..de9c6334da --- /dev/null +++ b/easel/desktop/startup.js @@ -0,0 +1,8 @@ +// Apply the host palette before the document's first paint. +(() => { + const theme=window.aesel?.initialTheme; + if(!theme)return; + for(const [name,value] of Object.entries({background:theme.background,foreground:theme.foreground,accent:theme.cursor})){ + document.documentElement.style.setProperty('--aesel-'+name,value); + } +})(); diff --git a/easel/desktop/style.css b/easel/desktop/style.css index 62b441bba7..77d2050396 100644 --- a/easel/desktop/style.css +++ b/easel/desktop/style.css @@ -1,9 +1,8 @@ @font-face{font-family:"AC aesel Unifont";src:url("fonts/ac-easel-unifont.woff") format("woff");font-style:normal;font-weight:400;font-display:swap} -html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#463264;color:white;font-family:"AC aesel Unifont",monospace} +html,body{margin:0;width:100%;height:100%;overflow:hidden;background:var(--aesel-background,#463264);color:var(--aesel-foreground,white);font-family:"AC aesel Unifont",monospace} #terminal{height:calc(100% - 20px);padding:10px} -#artifact-shell{position:absolute;top:14px;right:14px;width:var(--preview-width,178px);height:var(--preview-height,119.33px);transition:width .18s,height .18s;z-index:2} -#artifact{position:relative;width:100%;height:100%;box-sizing:border-box;overflow:hidden;background:#1c1728;border:1px solid #ffffff55;border-radius:0;box-shadow:2px 2px #0006;transition:width .18s,height .18s;z-index:2} -body[data-preview-mode="hover"][data-preview-engaged="true"] #artifact-shell,body[data-preview-mode="hover"] #artifact-shell:focus-within,body[data-preview-mode="pinned"] #artifact-shell{width:var(--preview-expanded-width,434px);height:var(--preview-expanded-height,290px)} +#artifact-shell{position:absolute;top:14px;right:14px;width:var(--preview-width,178px);height:var(--preview-height,119.33px);transition:none;z-index:2} +#artifact{position:relative;width:100%;height:100%;box-sizing:border-box;overflow:hidden;background:#1c1728;border:1px solid #ffffff55;border-radius:0;box-shadow:2px 2px #0006;transition:none;z-index:2} #preview-viewport{position:absolute;left:0;top:0;width:var(--preview-logical-width,192px);height:var(--preview-logical-height,128px);transform-origin:0 0;image-rendering:pixelated;overflow:hidden} #piece{display:flex;width:var(--preview-logical-width,192px);height:var(--preview-logical-height,128px);image-rendering:pixelated} #version{position:absolute;right:0;top:calc(100% + 4px);color:#fff;text-shadow:1px 1px #000;font:calc(var(--aesel-qr-label-size,16px) * .75) "AC aesel Unifont",monospace;pointer-events:none;white-space:nowrap} @@ -20,7 +19,7 @@ body[data-preview-mode="hover"][data-preview-engaged="true"] #artifact-shell,bod .fullscreen-control:focus-visible{outline:2px solid white;outline-offset:2px} #preview-fullscreen{position:absolute;top:5px;right:5px;width:24px;height:24px;padding:2px;font-size:16px} body.preview-fullscreen #terminal,body.preview-fullscreen #qr-card,body.preview-fullscreen #qr-label,body.preview-fullscreen #version{display:none} -body.preview-fullscreen #artifact-shell,body.preview-fullscreen[data-preview-mode] #artifact-shell:hover,body.preview-fullscreen[data-preview-mode] #artifact-shell:focus-within{position:fixed;inset:auto;left:50%;top:50%;transform:translate(-50%,-50%);width:var(--preview-fullscreen-width);height:var(--preview-fullscreen-height);z-index:10;transition:none} +body.preview-fullscreen #artifact-shell{position:fixed;inset:auto;left:50%;top:50%;transform:translate(-50%,-50%);width:var(--preview-fullscreen-width);height:var(--preview-fullscreen-height);z-index:10;transition:none} body.preview-fullscreen #artifact{border:0;border-radius:0;box-shadow:none;animation:none;transition:none} body.preview-fullscreen #preview-fullscreen{top:12px;right:12px} @media(prefers-reduced-motion:reduce){#artifact-shell{transition:none}} @@ -91,3 +90,5 @@ body.preview-fullscreen #artifact-shell{padding:0;border:0;box-shadow:none;backg #aesel-context-menu button:hover:not(:disabled),#aesel-context-menu button:focus-visible{background:#69436e;outline:none} #aesel-context-menu button:disabled{opacity:.4;cursor:default} #aesel-context-menu hr{border:0;border-top:1px solid #dc9ccc40;margin:4px 5px} + +body:not([data-notebook-ready]) #terminal{opacity:0} diff --git a/easel/desktop/test/preview-smoke.cjs b/easel/desktop/test/preview-smoke.cjs index 3385f0afa4..ce44ede7a6 100644 --- a/easel/desktop/test/preview-smoke.cjs +++ b/easel/desktop/test/preview-smoke.cjs @@ -50,7 +50,7 @@ app.whenReady().then(async () => { assert.equal(await win.webContents.executeJavaScript("document.getElementById('qr-card').tagName"),'BUTTON'); win.webContents.sendInputEvent({type:'mouseMove',x:50,y:50}); await delay(300); - assert((await geometry()).width>compact.width,'Hover must enlarge the preview'); + assert.deepEqual(await geometry(),compact,'Hover must preserve preview size'); win.webContents.sendInputEvent({type:'mouseMove',x:700,y:450}); await delay(300); assert.deepEqual(await geometry(),compact,'Leaving must restore compact preview'); @@ -60,7 +60,7 @@ app.whenReady().then(async () => { win.webContents.send('preview-mode','pinned'); await delay(250); const enlarged = await geometry(); - assert(enlarged.width>compact.width); + assert.equal(enlarged.width,compact.width); assert(Math.abs(enlarged.canvasWidth-(enlarged.width-2))<.1); assert(Math.abs(enlarged.canvasHeight-(enlarged.height-2))<.1); win.webContents.send('preview-mode','compact'); diff --git a/easel/desktop/transcript.js b/easel/desktop/transcript.js index f9955b5f9e..3a8cb99a0d 100644 --- a/easel/desktop/transcript.js +++ b/easel/desktop/transcript.js @@ -1,6 +1,6 @@ // Native-resolution prose above the terminal's input and approval controls. (() => { - const view=document.createElement('section');view.id='conversation';view.hidden=true; + const view=document.createElement('section');view.id='conversation';view.hidden=false; view.setAttribute('aria-label','Conversation');view.setAttribute('role','log'); view.setAttribute('aria-live','off');document.body.append(view); const title=document.getElementById('qr-label');if(title)view.append(title); diff --git a/easel/package.json b/easel/package.json index a2a0718279..74366473e1 100644 --- a/easel/package.json +++ b/easel/package.json @@ -1,6 +1,6 @@ { "name": "aesel", - "version": "0.7.5", + "version": "0.7.6", "private": true, "type": "module", "scripts": { diff --git a/easel/test/desktop-interaction.electron.cjs b/easel/test/desktop-interaction.electron.cjs new file mode 100644 index 0000000000..0ab4387918 --- /dev/null +++ b/easel/test/desktop-interaction.electron.cjs @@ -0,0 +1,227 @@ +// Real webview focus and native pointer resize, without a PTY or account. +const { app, BrowserWindow, ipcMain, webContents } = require("electron"); +const assert = require("node:assert/strict"), + path = require("node:path"), + fs = require("node:fs"); +const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +let win; +(async () => { + await app.whenReady(); + const root = path.resolve(__dirname, "../desktop"); + const inputs = [], + errors = []; + ipcMain.on("input", (_event, text) => inputs.push(text)); + ipcMain.handle("native-prox-title", () => ({ glyphs: [] })); + const theme = { + background: "#152331", + foreground: "#fffbea", + cursor: "#eeaacc", + }; + const start = Date.now(); + win = new BrowserWindow({ + show: false, + width: 900, + height: 650, + backgroundColor: theme.background, + webPreferences: { + preload: path.join(root, "preload.cjs"), + webviewTag: true, + contextIsolation: true, + sandbox: true, + backgroundThrottling: false, + additionalArguments: ["--aesel-initial-theme=" + JSON.stringify(theme)], + }, + }); + win.webContents.on("console-message", (event) => { + if (event.level === "error") errors.push(event.message); + }); + const js = (code) => win.webContents.executeJavaScript(code); + const painted = new Promise((resolve) => win.once("ready-to-show", resolve)); + await win.loadFile(path.join(root, "index.html")); + await painted; + assert.equal( + await js(`getComputedStyle(document.body).backgroundColor`), + "rgb(21, 35, 49)", + ); + assert.equal( + await js(`getComputedStyle(document.getElementById('terminal')).opacity`), + "0", + ); + assert.equal( + await js(`document.getElementById('conversation').hidden`), + false, + ); + console.log( + "Styled notebook ready in", + Date.now() - start, + "ms, before engine startup", + ); + win.show(); + win.focus(); + await js( + `window.updateConversation({entries:[{id:'u',kind:'user',text:'Make a piano.'}]});document.body.dataset.phase='ready';document.getElementById('artifact-shell').hidden=false;`, + ); + win.webContents.send( + "output", + "\x1b]777;easel-prompt:" + + JSON.stringify({ + text: "", + cursor: 0, + activity: "testing the keys", + feedback: "Working", + hidden: false, + }) + + "\x07", + ); + await delay(100); + assert.equal( + await js(`document.getElementById('activity-caption').textContent`), + "(testing the keys)", + ); + assert.equal( + await js( + `getComputedStyle(document.getElementById('activity-caption')).display`, + ), + "inline-block", + ); + fs.writeFileSync( + "/tmp/aesel-thought-bubble.png", + (await win.webContents.capturePage()).toPNG(), + ); + const rect = () => + js( + `(()=>{const r=document.getElementById('artifact-shell').getBoundingClientRect();return {x:r.x,y:r.y,width:r.width,height:r.height}})()`, + ); + const before = await rect(); + win.webContents.sendInputEvent({ + type: "mouseMove", + x: Math.round(before.x + 30), + y: Math.round(before.y + 30), + }); + await delay(250); + assert.deepEqual( + await rect(), + before, + "Hover must never enlarge the preview", + ); + const grip = await js( + `(()=>{const r=document.querySelector('[data-edge="sw"]').getBoundingClientRect();return {x:Math.round(r.x+r.width/2),y:Math.round(r.y+r.height/2)}})()`, + ); + win.webContents.sendInputEvent({ type: "mouseMove", ...grip }); + win.webContents.sendInputEvent({ + type: "mouseDown", + button: "left", + clickCount: 1, + ...grip, + }); + win.webContents.sendInputEvent({ + type: "mouseMove", + x: grip.x - 110, + y: grip.y + 65, + button: "left", + }); + await delay(80); + win.webContents.sendInputEvent({ + type: "mouseUp", + button: "left", + clickCount: 1, + x: grip.x - 110, + y: grip.y + 65, + }); + await delay(100); + const resized = await rect(); + assert(resized.width > before.width + 90); + assert(resized.height > before.height + 45); + win.webContents.sendInputEvent({ type: "mouseMove", x: 50, y: 400 }); + await delay(200); + assert.deepEqual(await rect(), resized, "Leaving retains the resized bounds"); + await js(`document.querySelector('[data-edge="sw"]').focus()`); + await delay(100); + assert.deepEqual(await rect(), resized, "Focus does not zoom"); + const page = + ''; + const guestReady = new Promise((resolve) => + win.webContents.once("did-attach-webview", (_event, guest) => + guest.once("did-finish-load", () => resolve(guest)), + ), + ); + await js( + `document.getElementById('piece').src=${JSON.stringify("data:text/html," + encodeURIComponent(page))}`, + ); + const guest = await guestReady; + await js(`document.getElementById('piece').focus()`); + await delay(150); + assert.equal( + await js(`document.activeElement.id`), + "piece", + "Preview keeps focus instead of returning it to xterm", + ); + const count = inputs.length; + // Dispatch through Electron's actual focused WebContents, as native keys do. + const recipient = webContents.getFocusedWebContents(); + assert.equal( + recipient?.id, + guest.id, + "Native keyboard focus belongs to the guest", + ); + recipient.sendInputEvent({ type: "keyDown", keyCode: "A" }); + recipient.sendInputEvent({ type: "keyUp", keyCode: "A" }); + await delay(100); + assert.deepEqual(await guest.executeJavaScript("window.keys"), [ + "down:a", + "up:a", + ]); + assert.equal( + inputs.length, + count, + "Playing a note must not type into the prompt", + ); + win.webContents.sendInputEvent({ + type: "mouseDown", + button: "left", + clickCount: 1, + x: 55, + y: 420, + }); + win.webContents.sendInputEvent({ + type: "mouseUp", + button: "left", + clickCount: 1, + x: 55, + y: 420, + }); + await delay(100); + assert.equal( + await js( + `document.activeElement.classList.contains('xterm-helper-textarea')`, + ), + true, + "Notebook click restores typing", + ); + win.webContents.sendInputEvent({ type: "keyDown", keyCode: "B" }); + win.webContents.sendInputEvent({ type: "char", keyCode: "b" }); + win.webContents.sendInputEvent({ type: "keyUp", keyCode: "B" }); + await delay(100); + assert( + inputs.slice(count).some((text) => text.includes("b")), + "Notebook receives prompt input", + ); + await js(`window.currentPreviewMedium='picture';document.body.dataset.previewMedium='picture';window.setPreviewDimensions(96,144)`); + const pictureBefore = await rect(); + await js(`document.querySelector('[data-edge="sw"]').dispatchEvent(new KeyboardEvent('keydown',{key:'ArrowLeft',bubbles:true,cancelable:true}))`); + await delay(100); + assert((await rect()).width > pictureBefore.width, 'Other media also retain manual resizing'); + assert.deepEqual(await js(`[getComputedStyle(document.getElementById('preview-viewport')).width,getComputedStyle(document.getElementById('preview-viewport')).height]`), ['96px','144px'], 'Resizing preserves image source pixels'); + assert.deepEqual(errors, []); + console.log( + "PASS thought bubble, stable hover, manual resize, guest keyboard and notebook focus", + ); +})() + .catch((error) => { + console.error(error); + process.exitCode = 1; + }) + .finally(() => { + win?.destroy(); + app.quit(); + }); diff --git a/easel/test/notebook-layout.electron.cjs b/easel/test/notebook-layout.electron.cjs index 8f0684e80a..61bb06a466 100644 --- a/easel/test/notebook-layout.electron.cjs +++ b/easel/test/notebook-layout.electron.cjs @@ -78,6 +78,43 @@ const assert = require("node:assert/strict"); await win.webContents.executeJavaScript( `document.querySelector('#activity-caption')?.textContent||''`, ), + "(Reading the piece)", + ); + assert.equal( + await win.webContents.executeJavaScript( + `document.querySelector('#activity-caption').previousSibling.id`, + ), + "prompt-feedback", + ); + assert.equal( + await win.webContents.executeJavaScript( + `document.querySelectorAll('#notebook-page article').length`, + ), + 2, + ); + win.webContents.send( + "output", + "\x1b]777;easel-prompt:" + + JSON.stringify({ + text: "", + cursor: 0, + activity: "Stale activity", + feedback: "", + hidden: false, + }) + + "\x07", + ); + await delay(60); + assert.equal( + await win.webContents.executeJavaScript( + `document.querySelector('#activity-caption').hidden`, + ), + true, + ); + assert.equal( + await win.webContents.executeJavaScript( + `document.querySelector('#activity-caption').textContent`, + ), "", ); win.webContents.send("credits", { @@ -142,11 +179,11 @@ const assert = require("node:assert/strict"); ]) { if (name === "expanded") await win.webContents.executeJavaScript( - `document.body.dataset.previewEngaged='true'`, + `{const shell=document.getElementById('artifact-shell');shell.dataset.resized='true';for(const [key,value] of Object.entries({width:540,height:364,right:14,top:14}))shell.style.setProperty('--resized-preview-'+key,value+'px')}`, ); if (name === "resized") await win.webContents.executeJavaScript( - `document.body.dataset.previewEngaged='false';const shell=document.getElementById('artifact-shell');shell.dataset.resized='true';for(const [key,value] of Object.entries({'compact-width':280,'compact-height':210,right:30,top:70}))shell.style.setProperty('--resized-preview-'+key,value+'px')`, + `{const shell=document.getElementById('artifact-shell');shell.dataset.resized='true';for(const [key,value] of Object.entries({width:280,height:210,right:30,top:70}))shell.style.setProperty('--resized-preview-'+key,value+'px')}`, ); await win.webContents.executeJavaScript( `document.getElementById('conversation').scrollTop=${scroll}`, diff --git a/easel/test/preview-geometry.electron.cjs b/easel/test/preview-geometry.electron.cjs index a3c0cf1d8b..cb78fac085 100644 --- a/easel/test/preview-geometry.electron.cjs +++ b/easel/test/preview-geometry.electron.cjs @@ -6,8 +6,9 @@ const delay=ms=>new Promise(r=>setTimeout(r,ms)); (async()=>{ await app.whenReady(); temporary=await fs.mkdtemp(path.join(os.tmpdir(),'easel-preview-test-')); const preload=path.join(temporary,'preload.cjs'); - await fs.writeFile(preload,`const {contextBridge}=require('electron');const callbacks={};const api={};for(const name of ['Theme','Display','Notice','TextSize','Font','Output','SelectAll','Paste','FullscreenState','PreviewMode','State'])api['on'+name]=fn=>callbacks[name]=fn;for(const name of ['size','input','ready','contextMenu','copyText','requestPaste','fullscreen','openPiece'])api[name]=()=>{};contextBridge.exposeInMainWorld('aesel',api);contextBridge.exposeInMainWorld('previewTest',{emit:(name,data)=>callbacks[name]?.(data)});`); + await fs.writeFile(preload,`const {contextBridge}=require('electron');const callbacks={};const api={};for(const name of ['Theme','Display','Notice','TextSize','Font','Output','SelectAll','Paste','FullscreenState','PreviewMode','State','InstanceLabel','NativeTitle','SystemTextSize','Credits','BuildStatus'])api['on'+name]=fn=>callbacks[name]=fn;for(const name of ['size','input','ready','contextMenu','copyText','requestPaste','fullscreen','openPiece'])api[name]=()=>{};api.renderProxTitle=async()=>null;contextBridge.exposeInMainWorld('aesel',api);contextBridge.exposeInMainWorld('previewTest',{emit:(name,data)=>callbacks[name]?.(data)});`); win=new BrowserWindow({show:false,width:900,height:700,webPreferences:{preload,contextIsolation:true,sandbox:true,backgroundThrottling:false}}); + win.webContents.on('console-message', (_event, level, message) => { if(level >= 2) console.error('renderer:',message); }); await win.loadFile(path.resolve(__dirname,'../desktop/index.html')); const js=code=>win.webContents.executeJavaScript(code); for(let i=0;i<50&&!await js('!!window.setPreviewDimensions');i++)await delay(50); @@ -17,12 +18,12 @@ const delay=ms=>new Promise(r=>setTimeout(r,ms)); let compact=await measure();assert.equal(compact.w,320);assert.equal(compact.h,640);assert.ok(Math.abs(compact.boxW/compact.boxH-.5)<.001); const hoverPoint=await js(`(()=>{const r=document.getElementById('artifact').getBoundingClientRect();return {x:Math.round(r.right-10),y:Math.round(r.top+10)}})()`); win.webContents.sendInputEvent({type:'mouseMove',...hoverPoint});await delay(300); - let hover=await measure();assert.ok(hover.visibleW>compact.visibleW);assert.equal(hover.w,320);assert.equal(hover.h,640);assert.ok(Math.abs(hover.visibleW/hover.visibleH-.5)<.001); - await js(`previewTest.emit('PreviewMode','pinned')`);await delay(250); + let hover=await measure();assert.equal(hover.visibleW,compact.visibleW);assert.equal(hover.w,320);assert.equal(hover.h,640);assert.ok(Math.abs(hover.visibleW/hover.visibleH-.5)<.001); + await delay(50); let pinned=await measure();assert.equal(pinned.w,320);assert.equal(pinned.h,640); await js(`previewTest.emit('FullscreenState',{app:true,preview:true})`);await delay(250); let full=await measure();assert.equal(full.w,320);assert.equal(full.h,640);assert.ok(Math.abs(full.visibleW/full.visibleH-.5)<.001); - await js(`previewTest.emit('FullscreenState',{app:false,preview:false});window.setPreviewDimensions(640,320);previewTest.emit('PreviewMode','hover')`);await delay(250); + await js(`previewTest.emit('FullscreenState',{app:false,preview:false});window.setPreviewDimensions(640,320)`);await delay(250); let landscape=await measure();assert.equal(landscape.w,640);assert.equal(landscape.h,320);assert.ok(Math.abs(landscape.boxW/landscape.boxH-2)<.001); await js(`(()=>{const canvas=document.createElement('canvas');canvas.width=96;canvas.height=144;const c=canvas.getContext('2d');c.fillStyle='#ff64ff';c.fillRect(0,0,96,144);c.fillStyle='#fff';c.fillRect(0,0,8,8);c.fillRect(88,136,8,8);window.renderMediaPreview({medium:'picture',preview:{artifactId:'test',version:1},localPreview:{mime:'image/png',data:canvas.toDataURL()}});})()`);await delay(300); const picture=await measure();assert.equal(picture.w,96);assert.equal(picture.h,144);assert.ok(Math.abs(picture.boxW/picture.boxH-2/3)<.001); @@ -35,7 +36,7 @@ const delay=ms=>new Promise(r=>setTimeout(r,ms)); const layout=await js(`(()=>{const rect=id=>{const r=document.getElementById(id).getBoundingClientRect();return {x:r.x,y:r.y,right:r.right,bottom:r.bottom}};return {name:rect('qr-label'),tv:rect('artifact'),qr:rect('qr-card'),version:rect('version'),w:innerWidth,h:innerHeight}})()`); assert.ok(layout.name.x<30&&layout.name.y<30); assert.ok(layout.tv.right>layout.w-30&&layout.tv.y<30); - assert.ok(layout.qr.right>layout.w/2&&layout.qr.y<50); + assert.equal(await js(`getComputedStyle(document.getElementById('qr-card')).display`),'none'); assert.equal(await js(`getComputedStyle(document.getElementById('version')).display`),'block'); assert.equal(await js(`document.getElementById('version').textContent`),'v7'); diff --git a/system/public/aesel/index.html b/system/public/aesel/index.html index 1dc3fe567c..680a6a340d 100644 --- a/system/public/aesel/index.html +++ b/system/public/aesel/index.html @@ -14,7 +14,7 @@Make art and software with AI.
- Download for Mac + Download for MacSign in with Aesthetic Computer.
Use AC, Claude, or Codex.
curl -fsSL https://aesthetic.computer/aesel.sh | shRequires Node.js 22+. Run ac after installing.
Mac 0.7.5 · Linux 0.7.0 (Easel)
Sign in with Aesthetic Computer.
Use AC, Claude, or Codex.
curl -fsSL https://aesthetic.computer/aesel.sh | shRequires Node.js 22+. Run ac after installing.
Mac 0.7.6 · Linux 0.7.0 (Easel)