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 @@ -Aesel -
+Aesel +
diff --git a/easel/desktop/main.cjs b/easel/desktop/main.cjs index 7841a7086f..b58f4b054a 100644 --- a/easel/desktop/main.cjs +++ b/easel/desktop/main.cjs @@ -14,7 +14,7 @@ const { tmpdir, homedir } = require('node:os'); const devHome=process.env.AESEL_DEV_HOME||''; app.setName(devHome?'Aesel Dev':'aesel'); app.setPath('userData', join(app.getPath('appData'), devHome?'Aesel Dev':'Easel')); -let window, terminal, timer, quitting = false; +let window, terminal, timer, quitting = false, windowPainted = false; let lastVisibleState = null, keepPreviewOnStart = false; let terminalSize = {cols:100,rows:32}; let currentTheme = FALLBACK; @@ -77,13 +77,13 @@ function releaseAddress(){ } app.once('will-quit',()=>{releaseAddress();try{rmSync(dragDir,{recursive:true,force:true});}catch{}}); process.once('exit',releaseAddress); -app.on('second-instance',()=>{if(!window||window.isDestroyed())return;if(window.isMinimized())window.restore();window.show();window.focus();}); +app.on('second-instance',()=>{if(!window||window.isDestroyed()||!windowPainted)return;if(window.isMinimized())window.restore();window.show();window.focus();}); // `open -a Easel` activates an existing macOS process without necessarily // launching a second instance. A checkpointed restart can therefore leave the // process healthy but its previously hidden window unreachable unless the app // handles the ordinary Dock/reopen activation itself. app.on('activate', () => { - if (!window || window.isDestroyed()) return; + if (!window || window.isDestroyed() || !windowPainted) return; if (window.isMinimized()) window.restore(); window.show(); window.focus(); @@ -236,7 +236,7 @@ function openNewWindow(){ const cwd=join(app.getPath('userData'),'projects',id); mkdirSync(cwd,{recursive:true,mode:0o700}); const env={...process.env};delete env.ELECTRON_RUN_AS_NODE;delete env.NODE_OPTIONS; - const args=[...(app.isPackaged?[]:[app.getAppPath()]),'--instance',id,'--cwd',cwd]; + const args=[...(app.isPackaged?[]:[app.getAppPath()]),'--instance',id,'--cwd',cwd,'--window-requested-at',String(Date.now())]; const child=spawn(process.execPath,args,{detached:true,stdio:'ignore',env}); child.on('error',error=>send('desktop-notice',`Could not open a window: ${error.message}`));child.unref(); } @@ -264,17 +264,13 @@ app.whenReady().then(() => { { label: 'Bitmap Font', type: 'radio', checked: true, accelerator: 'CmdOrCtrl+Shift+B', click: () => send('font', true) }, { label: 'Smooth Font', type: 'radio', accelerator: 'CmdOrCtrl+Shift+M', click: () => send('font', false) }, {type:'separator'}, - {label:'Preview',submenu:[ - {label:'Compact',type:'radio',click:()=>send('preview-mode','compact')}, - {label:'Zoom on Hover',type:'radio',checked:true,click:()=>send('preview-mode','hover')}, - {label:'Always Open',type:'radio',click:()=>send('preview-mode','pinned')}, - ]}, ] }, { role: 'windowMenu' }, ]; Menu.setApplicationMenu(Menu.buildFromTemplate(applicationMenuTemplate)); - window = new BrowserWindow({ width: 760, height: 540, title: devHome?'Aesel Dev':'aesel', backgroundColor: '#463264', - webPreferences: { preload: join(__dirname, 'preload.cjs'), contextIsolation: true, nodeIntegration: false, sandbox: true, webviewTag: true, plugins:true } }); + window = new BrowserWindow({ show:false, width: 760, height: 540, title: devHome?'Aesel Dev':'aesel', backgroundColor: currentTheme.background, + webPreferences: { additionalArguments:[`--aesel-initial-theme=${JSON.stringify(currentTheme)}`], preload: join(__dirname, 'preload.cjs'), contextIsolation: true, nodeIntegration: false, sandbox: true, webviewTag: true, plugins:true } }); + window.once('ready-to-show',()=>{windowPainted=true;window.show();console.log(JSON.stringify({event:'window-presented',ms:Date.now()-startedAt,requestedMs:option('--window-requested-at')?Date.now()-Number(option('--window-requested-at')):undefined}));}); const creditLabel = require('./credit-label.cjs').startCreditLabel({app, window, root}); require('./credit-checkout.cjs').startCreditCheckout({app,window,root,ipcMain,shell,refresh:creditLabel.refresh}); require('./native-title.cjs').registerNativeTitle({ipcMain, window, app}); diff --git a/easel/desktop/notebook-focus.js b/easel/desktop/notebook-focus.js index 39b8e9a234..077a814c45 100644 --- a/easel/desktop/notebook-focus.js +++ b/easel/desktop/notebook-focus.js @@ -1,6 +1,6 @@ // Route ordinary window clicks back to typing without swallowing UI actions. window.installNotebookFocus = ({ focusInput, sendInput, preview }) => { - const controls='button,a,input,textarea,select,summary,[contenteditable="true"],[role="button"],[role="menu"],dialog'; + const controls='button,a,input,textarea,select,summary,[contenteditable="true"],[role="button"],[role="menu"],dialog,webview,#artifact-shell'; const blocked=()=>document.body.classList.contains('preview-fullscreen')||!!document.querySelector('dialog[open]')||!!document.querySelector('#aesel-context-menu:not([hidden])'); const focus=()=>{if(!blocked())focusInput();}; let down=null; @@ -31,8 +31,7 @@ window.installNotebookFocus = ({ focusInput, sendInput, preview }) => { window.addEventListener('focus',()=>{ if(!document.activeElement?.closest?.(controls))requestAnimationFrame(focus); }); - // Embedded pages do not bubble pointer events into the notebook document. - preview?.addEventListener('focus',()=>requestAnimationFrame(focus)); + // A focused preview owns its keyboard. Clicking the notebook returns to typing. }; window.setNotebookInk = background => { diff --git a/easel/desktop/notebook-layout.js b/easel/desktop/notebook-layout.js index 5128d9dbe0..58cb3fdca5 100644 --- a/easel/desktop/notebook-layout.js +++ b/easel/desktop/notebook-layout.js @@ -60,7 +60,7 @@ }); new MutationObserver(window.layoutNotebookPreview).observe(document.body, { attributes: true, - attributeFilter: ["class", "data-preview-mode", "data-preview-engaged"], + attributeFilter: ["class"], }); view.addEventListener("scroll", window.layoutNotebookPreview, { passive: true, diff --git a/easel/desktop/package-lock.json b/easel/desktop/package-lock.json index 7ef415eadc..9ccc3fcd39 100644 --- a/easel/desktop/package-lock.json +++ b/easel/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "aesel-desktop", - "version": "0.7.5", + "version": "0.7.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aesel-desktop", - "version": "0.7.5", + "version": "0.7.6", "dependencies": { "@xterm/addon-fit": "0.10.0", "@xterm/addon-webgl": "0.18.0", diff --git a/easel/desktop/package.json b/easel/desktop/package.json index 3bdb4b115f..1146115abf 100644 --- a/easel/desktop/package.json +++ b/easel/desktop/package.json @@ -1,7 +1,7 @@ { "name": "aesel-desktop", "productName": "aesel", - "version": "0.7.5", + "version": "0.7.6", "private": true, "main": "main.cjs", "scripts": { diff --git a/easel/desktop/preload.cjs b/easel/desktop/preload.cjs index 86d046b672..791f062bed 100644 --- a/easel/desktop/preload.cjs +++ b/easel/desktop/preload.cjs @@ -1,5 +1,8 @@ const { contextBridge, ipcRenderer } = require('electron'); +let initialTheme; +try { const value=JSON.parse(process.argv.find(arg=>arg.startsWith('--aesel-initial-theme='))?.slice('--aesel-initial-theme='.length)||'null');if(value&&['background','foreground','cursor'].every(key=>/^#[0-9a-f]{6}$/i.test(value[key])))initialTheme=value; } catch {} contextBridge.exposeInMainWorld('aesel', { + initialTheme, renderProxTitle: (text, size) => ipcRenderer.invoke('native-prox-title', {text, size}), nativeContextMenu: items => ipcRenderer.invoke('notebook-context-menu',items), buyCredits: () => ipcRenderer.invoke('buy-ac-credits'), @@ -18,7 +21,6 @@ contextBridge.exposeInMainWorld('aesel', { openPiece: url => { if (typeof url === 'string') ipcRenderer.send('open-piece',url); }, openPaper: () => ipcRenderer.send('open-paper'), dragPreview: () => ipcRenderer.send('preview-drag'), - onPreviewMode: fn => ipcRenderer.on('preview-mode', (_event, mode) => { if (['compact', 'hover', 'pinned'].includes(mode)) fn(mode); }), onNotice: fn => ipcRenderer.on('desktop-notice', (_event, message) => { if (typeof message === 'string') fn(message.slice(0, 2000)); }), onFullscreenState: fn => ipcRenderer.on('fullscreen-state', (_event, state) => { if (state && typeof state.app === 'boolean' && typeof state.preview === 'boolean') fn({ app: state.app, preview: state.preview }); diff --git a/easel/desktop/preview-resize.js b/easel/desktop/preview-resize.js index d27ff67dbf..b03eb3927c 100644 --- a/easel/desktop/preview-resize.js +++ b/easel/desktop/preview-resize.js @@ -1,46 +1,125 @@ -// The picture owns hover enlargement; only its frame owns dragging. -window.installPreviewResize=()=>{ - const shell=document.getElementById('artifact-shell'),artifact=document.getElementById('artifact'); - let drag=null,custom=null,frame=0,leaveTimer=null,compactWidth=null; - const apply=()=>{ - if(!custom)return; - const right=Math.max(8,Math.min(innerWidth-104,custom.right)),top=Math.max(8,Math.min(innerHeight-80,custom.top)); - const width=Math.max(96,Math.min(innerWidth-right-8,custom.width)),height=Math.max(72,Math.min(innerHeight-top-40,custom.height)); - const expanded=drag?.expanded??custom.expanded??false; - custom={width,height,right,top,expanded}; - if(!expanded)compactWidth=width; - const aspect=(width-12)/(height-12); - const smallWidth=Math.min(compactWidth||188,innerWidth-right-8,(innerHeight-top-40-12)*aspect+12); - const largeWidth=expanded?width:Math.min((smallWidth-12)*3+12,innerWidth-right-8,(innerHeight-top-40-12)*aspect+12); - shell.style.setProperty('--resized-preview-compact-width',`${smallWidth}px`); - shell.style.setProperty('--resized-preview-compact-height',`${(smallWidth-12)/aspect+12}px`); - shell.style.setProperty('--resized-preview-drag-width',`${width}px`); - shell.style.setProperty('--resized-preview-drag-height',`${height}px`); - window.previewUserDimensions=[128*(width-12)/(height-12),128];window.setPreviewDimensions(...window.previewUserDimensions); - for(const [key,value]of Object.entries({width:largeWidth,height:(largeWidth-12)/aspect+12,right,top}))shell.style.setProperty('--resized-preview-'+key,`${value}px`); - shell.dataset.resized='true'; - }; - artifact.addEventListener('pointerenter',()=>{if(drag)return;clearTimeout(leaveTimer);document.body.dataset.previewZone='picture';document.body.dataset.previewEngaged='true';}); - shell.addEventListener('pointerenter',()=>clearTimeout(leaveTimer)); - shell.addEventListener('pointerleave',()=>{if(drag)return;leaveTimer=setTimeout(()=>{if(!drag&&!shell.matches(':hover')){document.body.dataset.previewEngaged='false';document.body.dataset.previewZone='';}},160);}); - for(const direction of ['n','e','s','w','nw','ne','se','sw']){ - const grip=document.createElement('button');grip.type='button';grip.className='preview-resize-edge';grip.dataset.edge=direction;if(direction==='sw')grip.id='preview-resize-grip';grip.setAttribute('aria-label',`Resize preview ${direction}`);grip.title='Drag to reshape preview';shell.append(grip); - grip.addEventListener('pointerenter',()=>{document.body.dataset.previewZone='resize';}); - grip.addEventListener('pointerdown',event=>{ - if(event.button!==0||document.body.classList.contains('preview-fullscreen')||window.currentPreviewMedium&&window.currentPreviewMedium!=='piece')return; - event.preventDefault();event.stopPropagation();clearTimeout(leaveTimer); - const rect=shell.getBoundingClientRect();drag={x:event.clientX,y:event.clientY,width:rect.width,height:rect.height,right:innerWidth-rect.right,top:rect.top,direction,expanded:document.body.dataset.previewEngaged==='true'||document.body.dataset.previewMode==='pinned'}; - custom={width:rect.width,height:rect.height,right:innerWidth-rect.right,top:rect.top,expanded:drag.expanded};apply(); - shell.dataset.resizing='true';grip.setPointerCapture(event.pointerId); +// A single preview size: moving over the picture never changes its bounds. +window.installPreviewResize = () => { + const shell = document.getElementById("artifact-shell"), + artifact = document.getElementById("artifact"); + let drag = null, + custom = null, + frame = 0; + const apply = () => { + if (!custom) return; + const right = Math.max(8, Math.min(innerWidth - 104, custom.right)), + top = Math.max(8, Math.min(innerHeight - 80, custom.top)); + const width = Math.max(96, Math.min(innerWidth - right - 8, custom.width)), + height = Math.max(72, Math.min(innerHeight - top - 40, custom.height)); + custom = { width, height, right, top }; + if ( + !window.currentPreviewMedium || + window.currentPreviewMedium === "piece" + ) { + window.previewUserDimensions = [ + (128 * (width - 12)) / (height - 12), + 128, + ]; + window.setPreviewDimensions(...window.previewUserDimensions); + } + for (const [key, value] of Object.entries(custom)) + shell.style.setProperty("--resized-preview-" + key, `${value}px`); + shell.dataset.resized = "true"; + }; + artifact.addEventListener("pointerenter", () => { + if (!drag) document.body.dataset.previewZone = "picture"; }); - grip.addEventListener('pointermove',event=>{ - if(!drag)return;const dx=event.clientX-drag.x,dy=event.clientY-drag.y,d=drag.direction; - custom={width:drag.width+(d.includes('w')?-dx:d.includes('e')?dx:0),height:drag.height+(d.includes('n')?-dy:d.includes('s')?dy:0),right:drag.right-(d.includes('e')?dx:0),top:drag.top+(d.includes('n')?dy:0),expanded:drag.expanded}; - if(!frame)frame=requestAnimationFrame(()=>{frame=0;apply();}); + shell.addEventListener("pointerleave", () => { + if (!drag) document.body.dataset.previewZone = ""; + }); + for (const direction of ["n", "e", "s", "w", "nw", "ne", "se", "sw"]) { + const grip = document.createElement("button"); + grip.type = "button"; + grip.className = "preview-resize-edge"; + grip.dataset.edge = direction; + if (direction === "sw") grip.id = "preview-resize-grip"; + grip.setAttribute("aria-label", `Resize preview ${direction}`); + grip.title = "Drag to reshape preview"; + shell.append(grip); + grip.addEventListener("pointerenter", () => { + document.body.dataset.previewZone = "resize"; + }); + const allowed = () => + !document.body.classList.contains("preview-fullscreen"); + const bounds = () => { + const rect = shell.getBoundingClientRect(); + return { + width: rect.width, + height: rect.height, + right: innerWidth - rect.right, + top: rect.top, + }; + }; + grip.addEventListener("pointerdown", (event) => { + if (event.button !== 0 || !allowed()) return; + event.preventDefault(); + event.stopPropagation(); + custom = bounds(); + drag = { x: event.clientX, y: event.clientY, ...custom, direction }; + shell.dataset.resizing = "true"; + grip.setPointerCapture(event.pointerId); + }); + grip.addEventListener("pointermove", (event) => { + if (!drag) return; + const dx = event.clientX - drag.x, + dy = event.clientY - drag.y, + d = drag.direction; + custom = { + width: drag.width + (d.includes("w") ? -dx : d.includes("e") ? dx : 0), + height: + drag.height + (d.includes("n") ? -dy : d.includes("s") ? dy : 0), + right: drag.right - (d.includes("e") ? dx : 0), + top: drag.top + (d.includes("n") ? dy : 0), + }; + if (!frame) + frame = requestAnimationFrame(() => { + frame = 0; + apply(); + }); + }); + const end = () => { + if (!drag) return; + cancelAnimationFrame(frame); + frame = 0; + apply(); + drag = null; + delete shell.dataset.resizing; + document.body.dataset.previewZone = ""; + }; + for (const name of ["pointerup", "pointercancel", "lostpointercapture"]) + grip.addEventListener(name, end); + grip.addEventListener("keydown", (event) => { + if ( + !allowed() || + !["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key) + ) + return; + event.preventDefault(); + event.stopPropagation(); + custom = bounds(); + const dx = + event.key === "ArrowLeft" ? -16 : event.key === "ArrowRight" ? 16 : 0; + const dy = + event.key === "ArrowUp" ? -16 : event.key === "ArrowDown" ? 16 : 0; + if (direction.includes("w")) custom.width -= dx; + if (direction.includes("e")) { + custom.width += dx; + custom.right -= dx; + } + if (direction.includes("n")) { + custom.height -= dy; + custom.top += dy; + } + if (direction.includes("s")) custom.height += dy; + apply(); + }); + } + window.addEventListener("resize", () => { + if (custom) apply(); }); - const end=()=>{if(!drag)return;drag=null;delete shell.dataset.resizing;apply();if(!shell.matches(':hover'))shell.dispatchEvent(new PointerEvent('pointerleave'));}; - for(const name of ['pointerup','pointercancel','lostpointercapture'])grip.addEventListener(name,end); - grip.addEventListener('keydown',event=>{if(!['ArrowLeft','ArrowRight','ArrowUp','ArrowDown'].includes(event.key))return;event.preventDefault();event.stopPropagation();const rect=shell.getBoundingClientRect();custom={width:rect.width+(event.key==='ArrowLeft'?16:event.key==='ArrowRight'?-16:0),height:rect.height+(event.key==='ArrowDown'?16:event.key==='ArrowUp'?-16:0),right:innerWidth-rect.right,top:rect.top};apply();}); - } - window.addEventListener('resize',()=>{if(custom)apply();}); }; diff --git a/easel/desktop/qr-label.js b/easel/desktop/qr-label.js index 7804c7dda1..45df061211 100644 --- a/easel/desktop/qr-label.js +++ b/easel/desktop/qr-label.js @@ -32,7 +32,7 @@ label.setAttribute('aria-label', text); const previousText=label.dataset.title||''; const previousLetters=Array.from(label.children); - const retain=id==='version'&&previousLetters.every(node=>node.classList.contains('qr-letter')); + const retain=id==='version'&&previousLetters.length>0&&previousLetters.every(node=>node.classList.contains('qr-letter')); label.dataset.title = text; const fallback = document.createElement('span'); fallback.textContent = text; diff --git a/easel/desktop/renderer.js b/easel/desktop/renderer.js index 4611ea8376..cc8c34c3c9 100644 --- a/easel/desktop/renderer.js +++ b/easel/desktop/renderer.js @@ -1,7 +1,7 @@ async function boot() { const donkey = {setScale(){},update(){},destroy(){}}; window.addEventListener("beforeunload",()=>donkey.destroy(),{once:true}); -await document.fonts.load('16px "AC aesel Unifont"'); +void document.fonts.load('16px "AC aesel Unifont"').catch(()=>{}); let savedTextSize = 16; try { const size = Number(localStorage.getItem('easel-text-size')); if (size >= 8 && size <= 48) savedTextSize = size; } catch {} let bitmapFont = false; @@ -67,16 +67,11 @@ function sizePreviewBox() { // Keep the page above the mascot/shelf region. The preview is absolutely // positioned, so this changes only its visual footprint—not terminal or // bottom-interface column widths. - const paperExpanded=Math.min(innerWidth*.88,availablePreviewHeight*previewAspect); - const expanded=window.currentPreviewMedium==='paper' - ? Math.max(compact,paperExpanded) - : Math.min(compact*3,innerWidth*.78,availablePreviewHeight*previewAspect); layoutProperty("--preview-qr-size",`${Math.max(1,Math.floor((Math.min(compact,compact/previewAspect)-4)*dpr/modules))*modules/dpr}px`); layoutProperty("--donkey-qr-size",`${largeQr}px`); layoutProperty("--donkey-qr-bottom",`${stageHeight+20}px`); for (const [key,value] of Object.entries({ 'preview-width':compact+12,'preview-height':compact/previewAspect+12, - 'preview-expanded-width':expanded+12,'preview-expanded-height':expanded/previewAspect+12, 'preview-logical-width':previewWidth,'preview-logical-height':previewHeight, 'preview-fullscreen-width':Math.min(innerWidth,innerHeight*previewAspect), 'preview-fullscreen-height':Math.min(innerWidth/previewAspect,innerHeight), @@ -180,11 +175,13 @@ const terminalElement = document.getElementById('terminal'); const promptLine=document.createElement('div');promptLine.id='prose-prompt';promptLine.hidden=true; promptLine.setAttribute('aria-hidden','true');document.getElementById('notebook-page').append(promptLine); const promptFeedback=document.createElement('span');promptFeedback.id='prompt-feedback';promptFeedback.hidden=true;document.body.append(promptFeedback); +const activityCaption=document.createElement('span');activityCaption.id='activity-caption';activityCaption.hidden=true;activityCaption.setAttribute('role','status');activityCaption.setAttribute('aria-live','off'); window.installNotebookDonkey(promptFeedback); window.placeNotebookActivity=()=>{ const last=Array.from(document.querySelectorAll('#notebook-page article[data-kind="user"]')).at(-1); const target=promptState?.text?promptLine:(last?.lastElementChild||last||promptLine); if(promptFeedback.parentElement!==target)target.append(promptFeedback); + if(activityCaption.parentElement!==target||promptFeedback.nextSibling!==activityCaption)promptFeedback.after(activityCaption); }; let promptState=null; function positionPrompt(){ @@ -196,8 +193,11 @@ function positionPrompt(){ } function updatePrompt(value){ + document.body.dataset.notebookReady='true'; const draftChanged=promptState&&(promptState.text!==value.text||promptState.cursor!==value.cursor); promptState=value;promptLine.hidden=!!value.hidden;positionPrompt(); + activityCaption.hidden=!!value.hidden||!value.feedback||!value.activity; + activityCaption.textContent=activityCaption.hidden?'':`(${value.activity})`; promptFeedback.textContent='';promptFeedback.setAttribute('role','img');promptFeedback.setAttribute('aria-label',value.feedback||'Idle');promptFeedback.hidden=!!value.hidden||!value.feedback; const chars=Array.from(value.text||''),index=Math.max(0,Math.min(chars.length,value.cursor??chars.length)); const before=document.createTextNode(chars.slice(0,index).join('')); @@ -449,13 +449,13 @@ window.installPreviewShutter(artifact); document.getElementById('version').setAttribute('aria-label','Piece version'); const previewViewport = document.getElementById('preview-viewport'); // The guest keeps a display-aspect viewport, 128 pixels high. Only its composited surface -// scales; hover/pinned/fullscreen never change the piece's layout or resolution. +// scales; fullscreen never changes the piece's layout or resolution. function scalePreview() { const rect = artifact.getBoundingClientRect(); const border = fullscreenState.preview ? 0 : 2; const width = rect.width - border, height = rect.height - border; if((!window.currentPreviewMedium||window.currentPreviewMedium==='piece')&&width>0&&height>0){ - // Reframe only when the user changes the logical viewport. Hover and + // Reframe only when the user changes the logical viewport. // notebook redraws composite its existing surface without guest resizes. previewViewport.style.transform=`scale(${width/previewWidth},${height/previewHeight})`;return; } @@ -471,10 +471,7 @@ window.addEventListener('resize', () => { resizeEndTimer=setTimeout(()=>document.body.classList.remove('window-resizing'),150); resize(); }); -window.aesel.onPreviewMode(mode => { - document.body.dataset.previewMode = mode; - scalePreview(); -}); + sizePreviewBox(); scalePreview(); let focusBeforePreview = null; window.aesel.onFullscreenState(state => { @@ -602,7 +599,7 @@ window.aesel.ready(); const reportConnection=()=>window.aesel.input(navigator.onLine?'\x1b[99;9;1~':'\x1b[99;9;0~');window.addEventListener('online',reportConnection);window.addEventListener('offline',reportConnection);if(!navigator.onLine)reportConnection(); terminal.focus(); } -boot().catch(error => { document.getElementById('terminal').textContent = `Could not start terminal: ${error.message}`; }); +boot().catch(error => { document.body.dataset.notebookReady='true';document.getElementById('terminal').textContent = `Could not start terminal: ${error.message}`; }); // The footer is the provider selector. Only hosted AC work displays a balance. (() => { diff --git a/easel/desktop/scripts/linux-download.html b/easel/desktop/scripts/linux-download.html index 2cfc0c105d..adc267cb47 100644 --- a/easel/desktop/scripts/linux-download.html +++ b/easel/desktop/scripts/linux-download.html @@ -12,8 +12,8 @@ sudo apt install ./Easel-0.7.0-linux-amd64.deb
chmod +x Easel-0.7.0-linux-x86_64.AppImage
 ./Easel-0.7.0-linux-x86_64.AppImage

macOS

-

Download for Apple silicon

-

ZIP archive · signed and notarized · macOS 12+

+

Download for Apple silicon

+

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 Mac
Apple silicon · macOS 12+ · Linux & more
@@ -24,7 +24,7 @@
-

Downloads

Sign in with Aesthetic Computer.
Use AC, Claude, or Codex.

Mac Apple silicon · DMG↓Linux Ubuntu / Debian · x86-64↓AppImage Other Linux desktops · x86-64↓
Terminalcurl -fsSL https://aesthetic.computer/aesel.sh | sh

Requires Node.js 22+. Run ac after installing.

Mac 0.7.5 · Linux 0.7.0 (Easel)

+

Downloads

Sign in with Aesthetic Computer.
Use AC, Claude, or Codex.

Mac Apple silicon · DMG↓Linux Ubuntu / Debian · x86-64↓AppImage Other Linux desktops · x86-64↓
Terminalcurl -fsSL https://aesthetic.computer/aesel.sh | sh

Requires Node.js 22+. Run ac after installing.

Mac 0.7.6 · Linux 0.7.0 (Easel)