From fd45b50033d4cd8922d1a78bec0e3ccec9221a9f Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 17 Sep 2026 19:50:15 -0700 Subject: [PATCH] Release Aesel 0.7.4 with quiet notebook UI and working phone sessions --- apple/aesel/Info.plist | 2 +- easel/context/api.json | 5 +++-- easel/desktop/donkey.css | 4 ++-- easel/desktop/package-lock.json | 4 ++-- easel/desktop/package.json | 2 +- easel/desktop/renderer.js | 5 ++--- easel/desktop/scripts/linux-download.html | 4 ++-- easel/desktop/transcript.js | 2 +- easel/package.json | 2 +- easel/phone/host.html | 1 + easel/phone/session.mjs | 2 ++ easel/phone/shim/absent.mjs | 1 + easel/src/notebook-conversation.mjs | 4 ++++ easel/src/publish.mjs | 4 +++- easel/src/render.mjs | 3 ++- easel/src/tui.mjs | 7 ++++--- easel/test/notebook-conversation.test.mjs | 24 +++++++++++++++++++++++ easel/test/phone-loader.mjs | 3 ++- easel/test/phone-session.test.mjs | 24 +++++++++++++++++++++++ system/public/aesel/index.html | 4 ++-- 20 files changed, 84 insertions(+), 23 deletions(-) create mode 100644 easel/src/notebook-conversation.mjs create mode 100644 easel/test/notebook-conversation.test.mjs diff --git a/apple/aesel/Info.plist b/apple/aesel/Info.plist index 16f73214dc..f38ec4b9f5 100644 --- a/apple/aesel/Info.plist +++ b/apple/aesel/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.7.3 + 0.7.4 CFBundleVersion 2 UIAppFonts diff --git a/easel/context/api.json b/easel/context/api.json index 7e2c68991c..585483446b 100644 --- a/easel/context/api.json +++ b/easel/context/api.json @@ -120,7 +120,8 @@ "source": "lib/graph.mjs:2797", "examples": [ "disks/flap.mjs:50 if (store[`flap~${num}`]) stamp(store[`flap~${num}`], screen.width / 2, screen.height / 2);", - "disks/graphics.mjs:28 stamp(" + "disks/graphics.mjs:28 stamp(", + "disks/kokazo.mjs:243 stamp(ink, color, x, y, jx, jy, r);" ] }, { @@ -730,7 +731,7 @@ "examples": [ "disks/bgm.mjs:26 if (params.length === 0) params[0] = num.randInt(trackCount - 1);", "disks/crayon.mjs:86 let numDots = num.randInt(minNumDots, maxNumDots);", - "disks/doodle.mjs:76 const x = points[i][0] + -1 + num.randInt(2);" + "disks/dafu.mjs:101 const speed = num.randInt(20) / 10 + 0.4;" ] }, { diff --git a/easel/desktop/donkey.css b/easel/desktop/donkey.css index 94fb9468e0..9a4b3ef68f 100644 --- a/easel/desktop/donkey.css +++ b/easel/desktop/donkey.css @@ -222,7 +222,7 @@ body::before{left:10px} /* Keep hover anchored to the lettering rather than the padded button. */ #conversation>#qr-label{cursor:pointer;transform-origin:left center} -#credit-label{transform-origin:right center} +#credit-label{transform-origin:center center} .provider-mark{display:inline-block;width:25px;height:25px;object-fit:contain;vertical-align:middle;margin-right:9px;flex:none} #provider-menu>button{display:flex;align-items:center} #provider-menu .braincell-balance{display:flex;align-items:center} @@ -285,7 +285,7 @@ body:not(.preview-fullscreen) #artifact-shell[data-resizing]{width:var(--resized :root{--corner-inset:14px} #conversation>#qr-label,#conversation>#qr-label[data-swift-letters="true"]{top:var(--corner-inset);margin-top:var(--corner-inset);margin-left:var(--corner-inset)} body:not(.preview-fullscreen) #artifact-shell:not([data-resized]){top:var(--corner-inset);right:var(--corner-inset)} -#credit-label{right:var(--corner-inset);bottom:var(--corner-inset)} +#credit-label{right:8px;bottom:var(--corner-inset)} #credit-label #version{padding:0} #activity-caption{display:inline;font:italic .75em/1.3 Helvetica,Arial,sans-serif;opacity:.62;overflow-wrap:anywhere;white-space:normal;pointer-events:none} #activity-caption[hidden]{display:none} diff --git a/easel/desktop/package-lock.json b/easel/desktop/package-lock.json index 8965fc6540..f23887b843 100644 --- a/easel/desktop/package-lock.json +++ b/easel/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "aesel-desktop", - "version": "0.7.3", + "version": "0.7.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aesel-desktop", - "version": "0.7.3", + "version": "0.7.4", "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 b21f754eef..90c601d900 100644 --- a/easel/desktop/package.json +++ b/easel/desktop/package.json @@ -1,7 +1,7 @@ { "name": "aesel-desktop", "productName": "aesel", - "version": "0.7.3", + "version": "0.7.4", "private": true, "main": "main.cjs", "scripts": { diff --git a/easel/desktop/renderer.js b/easel/desktop/renderer.js index 948242cf93..f455cc93bd 100644 --- a/easel/desktop/renderer.js +++ b/easel/desktop/renderer.js @@ -180,13 +180,11 @@ 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(){ @@ -200,7 +198,6 @@ function positionPrompt(){ function updatePrompt(value){ const draftChanged=promptState&&(promptState.text!==value.text||promptState.cursor!==value.cursor); promptState=value;promptLine.hidden=!!value.hidden;positionPrompt(); - activityCaption.textContent=value.activity?`(${value.activity})`:'';activityCaption.hidden=!!value.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('')); @@ -654,6 +651,8 @@ boot().catch(error => { document.getElementById('terminal').textContent = `Could modelSelect.addEventListener('change',()=>{window.aesel.input(`\x1b[99;4;${backendIndex};${modelSelect.value}~`);}); modelLabel.append(modelSelect);menu.append(modelLabel); + const stateLine=document.createElement('p');stateLine.className='inference-status';stateLine.textContent=[provider?.mode==='local'?'Local inference':'Remote inference',provider?.status,provider?.activity].filter(Boolean).join(' · ');menu.append(stateLine); + if(provider?.notice){const notice=document.createElement('p');notice.className='session-notice';notice.textContent=provider.notice;menu.append(notice);} if(provider?.backend==='ac'){const count=document.createElement('p');count.className='braincell-balance';const icon=new Image();icon.src='assets/braincell.svg';icon.alt='';icon.className='provider-mark';count.append(icon,`${n===null?'—':n.toLocaleString('en-US')} braincells`);menu.append(count);} if(provider?.backend==='ac'&&credits?.offer){ const buy=item('Buy 1,000,000 braincells · $5',async()=>{ diff --git a/easel/desktop/scripts/linux-download.html b/easel/desktop/scripts/linux-download.html index 9fd2630b00..1f97e8bd73 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/transcript.js b/easel/desktop/transcript.js index c3ddd07378..8af10bcf3e 100644 --- a/easel/desktop/transcript.js +++ b/easel/desktop/transcript.js @@ -58,7 +58,7 @@ const newestUser=value.entries.filter(entry=>entry.kind==='user'&&entry.text?.trim()).at(-1)?.id; for(const entry of value.entries){ if(typeof entry.id!=='string'||typeof entry.text!=='string')continue; - if(['command','change','tool','publish'].includes(entry.kind)||!entry.text.trim())continue; + if(!['user','assistant','error'].includes(entry.kind)||!entry.text.trim())continue; if(entry.kind==='notice'&&/^New [^\n]+\nPrevious thread saved: /.test(entry.text))continue; if(entry.kind==='notice'&&/^(?:Engine|Model|Settings|Medium|Artifact) · .* · current piece and recent conversation carried over$/.test(entry.text))continue; if(entry.kind==='notice'&&/^(?:Desktop thread restored|Desktop (?:restart|update|home) queued|Ran: |Queued(?: \(\d+ queued\))? · )/.test(entry.text))continue; diff --git a/easel/package.json b/easel/package.json index a39e16e379..0a9905ae7f 100644 --- a/easel/package.json +++ b/easel/package.json @@ -1,6 +1,6 @@ { "name": "aesel", - "version": "0.7.3", + "version": "0.7.4", "private": true, "type": "module", "scripts": { diff --git a/easel/phone/host.html b/easel/phone/host.html index c076d28ea7..d89bfb855d 100644 --- a/easel/phone/host.html +++ b/easel/phone/host.html @@ -39,6 +39,7 @@ "node:os": "/easel/phone/shim/absent.mjs", "node:http": "/easel/phone/shim/absent.mjs", "node:child_process": "/easel/phone/shim/absent.mjs", + "node:readline": "/easel/phone/shim/absent.mjs", "/easel/src/preview-frame.mjs": "/easel/phone/shim/preview-frame.mjs", "/easel/src/jev-advisor.mjs": "/easel/phone/shim/jev-advisor.mjs", "/easel/src/revisions.mjs": "/easel/phone/shim/revisions.mjs" diff --git a/easel/phone/session.mjs b/easel/phone/session.mjs index 40fa929851..0ccd8c8f10 100644 --- a/easel/phone/session.mjs +++ b/easel/phone/session.mjs @@ -268,6 +268,8 @@ export function createSession({ storage = memoryStore(), emit = () => {} } = {}) try { say("status", { text: "publishing", kind: "working" }); await publishPiece({ + // Phone threads preserve drafts, but have no desktop revision ledger. + version: null, file: state.file, slug: state.slug, cwd: "/piece", diff --git a/easel/phone/shim/absent.mjs b/easel/phone/shim/absent.mjs index f8eee33f4f..53576ea35c 100644 --- a/easel/phone/shim/absent.mjs +++ b/easel/phone/shim/absent.mjs @@ -14,6 +14,7 @@ export const spawn = absent("spawn"); export const execFile = absent("execFile"); export const execFileSync = absent("execFileSync"); export const createServer = absent("createServer"); +export const createInterface = absent("createInterface"); export const homedir = () => "/home"; export const tmpdir = () => "/tmp"; export const networkInterfaces = () => ({}); diff --git a/easel/src/notebook-conversation.mjs b/easel/src/notebook-conversation.mjs new file mode 100644 index 0000000000..1760b8a008 --- /dev/null +++ b/easel/src/notebook-conversation.mjs @@ -0,0 +1,4 @@ +// Operational notices stay in Settings; the page belongs to the conversation. +export function notebookConversationEntry(entry) { + return ['user', 'assistant', 'error'].includes(entry.kind) && !entry.activityOnly; +} diff --git a/easel/src/publish.mjs b/easel/src/publish.mjs index 705ad5ec62..3cb5700125 100644 --- a/easel/src/publish.mjs +++ b/easel/src/publish.mjs @@ -57,6 +57,8 @@ export async function publishPiece({ site = SITE, onStep = () => {}, source: snapshot, + // Hosts without a desktop revision ledger can explicitly supply null. + version: snapshotVersion, }) { const handle = session.handle; if (!handle) { @@ -102,7 +104,7 @@ export async function publishPiece({ } catch {} let registration=null; - const savedVersion=new PieceRevisions(plan.path).list().findLast(v=>v.revision===createHash('sha256').update(source).digest('hex'))?.version; + const savedVersion=snapshotVersion === undefined ? new PieceRevisions(plan.path).list().findLast(v=>v.revision===createHash('sha256').update(source).digest('hex'))?.version : snapshotVersion; if(verified&&Number.isInteger(savedVersion)&&savedVersion>=2){ try{const response=await fetch(`${site}/api/register-piece`,{method:'POST',headers:{Authorization:`Bearer ${token}`,'Content-Type':'application/json','User-Agent':USER_AGENT},body:JSON.stringify({version:savedVersion,slug:plan.slug,ext:plan.extension.slice(1),revision:createHash('sha256').update(source).digest('hex')}),signal:AbortSignal.timeout(10000)});const data=await response.json();registration=response.ok?data:{error:data.error||'Feed registration unavailable'};} catch{registration={error:'Feed registration unavailable'};} diff --git a/easel/src/render.mjs b/easel/src/render.mjs index a49cc7ff84..d7ef38f051 100644 --- a/easel/src/render.mjs +++ b/easel/src/render.mjs @@ -617,7 +617,8 @@ export function renderFrame(state, columns = 80, rows = 24, useColor = true) { // status, or the thing being typed. const controls=modelControls(state,width); const controlLine=controls.length?' '+controls.map(c=>paint(useColor,c.selected||state.hover===c.action?'block bold':c.tone+' bold',c.text.padEnd(c.width))).join(''):pathLine; - const lines = [...body, rule, header, controlLine, prompt, help]; + const desktop = state.desktop || state.desktopProsePrompt; + const lines = [...body, desktop ? "" : rule, desktop ? "" : header, desktop ? "" : controlLine, prompt, desktop ? "" : help]; return lines .slice(0, height) .map((line,index) => {const fitted=fit(line,width);return `${ground}${useColor && !process.env.EASEL_DESKTOP && index>=height-4?woodgrain(fitted,index-(height-4)):fitted}${reset}`;}) diff --git a/easel/src/tui.mjs b/easel/src/tui.mjs index c08b5b7ac8..bdd250765e 100755 --- a/easel/src/tui.mjs +++ b/easel/src/tui.mjs @@ -6,6 +6,7 @@ import {takeSubmittedBatch,inputBatchDelay} from './input-batch.mjs'; import {startNativeGamepad} from './native-gamepad.mjs'; import {requestFeedback} from './request-feedback.mjs'; import {publicActivity,toolActivity} from './public-activity.mjs'; +import {notebookConversationEntry} from './notebook-conversation.mjs'; import {connectionFailure,conciseFailure} from './connection-status.mjs'; import {ApprovalQueue, approvalFor, approvalShape, defaultApprovalResponse} from "./approvals.mjs"; import {readProviderPreferences,saveProviderPreferences,chooseProviderPreferences} from './provider-preferences.mjs'; @@ -562,7 +563,7 @@ function redraw() { if(historyKey!==desktopHistoryKey){desktopHistoryKey=historyKey;try{desktopHistory=live.history.list().map(({version,updatedAt,restoredFrom,summary})=>({version,updatedAt,restoredFrom,summary}));}catch{desktopHistory=[];}} const bindingKey=state.medium==='piece'?`${live.file}:${live.revision?.revision||''}`:''; if(bindingKey!==bindingSnapshotKey){bindingSnapshotKey=bindingKey;bindingSnapshot=state.medium==='piece'&&live.revision?.source?notebookBindings(live.revision.source,live.file):{revision:'',bindings:[]};} - const provider = JSON.stringify({feedPending:!!state.feedPending,notebookBindings:bindingSnapshot,backend:backend.id,model:state.model||model,effort,busy:state.busy,selectedModel:model,models:pickerModels({backend:backend.id,model,catalog:modelCatalog||[]}),versions:desktopHistory}); + const provider = JSON.stringify({status:state.status,mode:state.mode,activity:publicActivity(state),notice:state.entries.findLast(e=>e.kind==='notice')?.text||'',feedPending:!!state.feedPending,notebookBindings:bindingSnapshot,backend:backend.id,model:state.model||model,effort,busy:state.busy,selectedModel:model,models:pickerModels({backend:backend.id,model,catalog:modelCatalog||[]}),versions:desktopHistory}); if (provider !== lastProvider) { lastProvider = provider; process.stdout.write(`\x1b]777;easel-provider:${provider}\x07`); } } if (closing || drawing || splashing) return; @@ -582,10 +583,10 @@ function redraw() { if (process.env.EASEL_DESKTOP) { const prompt=JSON.stringify({text:state.input,cursor:state.cursor,activity:publicActivity(state),feedback:state.busy?requestFeedback(state):state.queued.length?'Gathering your messages':'',hidden:!!(state.approval||state.settings||state.about)}); if(prompt!==lastPrompt){lastPrompt=prompt;process.stdout.write(`\x1b]777;easel-prompt:${prompt}\x07`);} - const conversation=JSON.stringify({hidden:!!(state.settings||state.about),entries:state.entries.filter(e=>e.id!=='feed-registration'&&!e.activityOnly&&!(e.kind==='error'&&(connectionFailure(e.text)||/^Live push failed: Incomplete or invalid JavaScript/.test(e.text)))&&(e.id!=='autopublish'||e.kind==='error')).map(e=>({id:e.id,kind:e.kind,text:e.kind==='error'?conciseFailure(e.text):e.text}))}); + const conversation=JSON.stringify({hidden:!!(state.settings||state.about),entries:state.entries.filter(e=>notebookConversationEntry(e)&&e.id!=='feed-registration'&&!(e.kind==='error'&&(connectionFailure(e.text)||/^Live push failed: Incomplete or invalid JavaScript/.test(e.text)))&&(e.id!=='autopublish'||e.kind==='error')).map(e=>({id:e.id,kind:e.kind,text:e.kind==='error'?conciseFailure(e.text):e.text}))}); if(conversation!==lastConversation){lastConversation=conversation;process.stdout.write(`\x1b]777;easel-conversation:${conversation}\x07`);} } - const frame = renderFrame(process.env.EASEL_DESKTOP && !state.settings && !state.about ? {...state,entries:[],desktopProsePrompt:!state.approval} : state, process.stdout.columns, process.stdout.rows, process.env.NO_COLOR !== "1"); + const frame = renderFrame(process.env.EASEL_DESKTOP && !state.settings && !state.about ? {...state,desktop:true,entries:[],desktopProsePrompt:!state.approval} : {...state,desktop:!!process.env.EASEL_DESKTOP}, process.stdout.columns, process.stdout.rows, process.env.NO_COLOR !== "1"); const output = frameDiff.update(frame, process.stdout.columns); if (process.env.EASEL_DESKTOP) { const layout = JSON.stringify(frameLayout(state, process.stdout.rows)); diff --git a/easel/test/notebook-conversation.test.mjs b/easel/test/notebook-conversation.test.mjs new file mode 100644 index 0000000000..89d1da3601 --- /dev/null +++ b/easel/test/notebook-conversation.test.mjs @@ -0,0 +1,24 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {notebookConversationEntry} from '../src/notebook-conversation.mjs'; +import {renderFrame} from '../src/render.mjs'; + +test('desktop notebook excludes operational notices and unfinished inference chatter', () => { + const entries = [ + {kind:'notice',text:'REMOTE INFERENCE'}, {kind:'notice',text:'Transcript: shared'}, + {kind:'tool',text:'Reading source'}, {kind:'assistant',text:'Looking now',activityOnly:true}, + {kind:'user',text:'Make it orange'}, {kind:'assistant',text:'The circle is orange.'}, + {kind:'error',text:'Sign in to continue'}, + ]; + assert.deepEqual(entries.filter(notebookConversationEntry).map(e=>e.text), + ['Make it orange','The circle is orange.','Sign in to continue']); +}); + +test('desktop status lives outside the terminal footer without hiding approvals', () => { + const state={desktop:true,desktopProsePrompt:true,mode:'remote',status:'thinking',account:'@test',workspace:'/private/work',entries:[],input:''}; + assert.equal(renderFrame(state,80,24,false).trim(),''); + const approval=renderFrame({...state,desktopProsePrompt:false,approval:{subject:'Write piece'}},80,24,false); + assert.match(approval,/ALLOW Write piece/); + assert.doesNotMatch(approval,/REMOTE|THINKING|\/private\/work|@test/); + assert.match(renderFrame({...state,desktop:false,desktopProsePrompt:false},80,24,false),/REMOTE · THINKING/); +}); diff --git a/easel/test/phone-loader.mjs b/easel/test/phone-loader.mjs index 97842865f0..9fe6dc1751 100644 --- a/easel/test/phone-loader.mjs +++ b/easel/test/phone-loader.mjs @@ -1,9 +1,10 @@ // Resolve the phone's browser import map in a focused Node test process. const root = new URL('../../', import.meta.url); -const shims = {'node:events':'events','node:fs':'fs','node:path':'path','node:url':'url','node:crypto':'crypto','node:os':'absent','node:http':'absent','node:child_process':'absent'}; +const shims = {'node:events':'events','node:fs':'fs','node:path':'path','node:url':'url','node:crypto':'crypto','node:os':'absent','node:http':'absent','node:child_process':'absent','node:readline':'absent'}; export async function resolve(specifier, context, next) { if (context.parentURL?.startsWith(new URL('easel/', root).href)) { if (shims[specifier]) return {url: new URL(`easel/phone/shim/${shims[specifier]}.mjs`, root).href, shortCircuit:true}; + if (specifier.startsWith('node:')) throw Error(`Phone import map has no shim for ${specifier}`); for (const name of ['revisions','preview-frame','jev-advisor']) if (specifier === `./${name}.mjs`) return {url:new URL(`easel/phone/shim/${name}.mjs`, root).href,shortCircuit:true}; } if (specifier.startsWith('/easel/')) return {url:new URL(specifier.slice(1), root).href,shortCircuit:true}; diff --git a/easel/test/phone-session.test.mjs b/easel/test/phone-session.test.mjs index b516dcf769..8eb22038af 100644 --- a/easel/test/phone-session.test.mjs +++ b/easel/test/phone-session.test.mjs @@ -4,6 +4,30 @@ import {register} from 'node:module'; register(new URL('./phone-loader.mjs', import.meta.url)); const {createSession} = await import('../phone/session.mjs'); +test('phone publication verifies uploaded source without desktop revision storage', async () => { + const source='export function paint({wipe}) {wipe("orange")}'; + const originalFetch=globalThis.fetch; + const calls=[]; + globalThis.fetch=async (url,options={})=>{ + calls.push(url); + if(url.includes('/presigned-upload-url/'))return Response.json({uploadURL:'https://upload.test/piece'}); + if(options.method==='PUT')return new Response(''); + return new Response(source); + }; + try { + const values=new Map([['session',JSON.stringify({slug:'phone-test',source})]]); + const events=[]; + const session=createSession({storage:{get:k=>values.get(k),set:(k,v)=>values.set(k,v)},emit:e=>events.push(e)}); + await session.open(); + session.state.token='test-token';session.state.handle='test'; + await session.publish(); + assert.equal(session.state.published,true); + assert.equal(calls.length,3); + assert.equal(events.some(e=>e.type==='bad'),false); + assert(events.some(e=>e.type==='preview')); + }finally{globalThis.fetch=originalFetch;} +}); + test('phone threads preserve source, transcript and engine context without account credentials', async () => { const source = 'export function paint({wipe}) {wipe("orange")}'; const values = new Map([['session', JSON.stringify({slug:'legacy',source,published:true,token:'fake-token',handle:'test'})]]); diff --git a/system/public/aesel/index.html b/system/public/aesel/index.html index 4e3e686461..e11f7c03bc 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.3 · 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.4 · Linux 0.7.0 (Easel)

-- 2.51.2