From f286570e0fc503e0f36c09f464a40e84f32c1e63 Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Thu, 17 Sep 2026 21:52:46 -0700 Subject: [PATCH] give donkey bubbles task context and use provider dropdowns --- easel/desktop/package-lock.json | 4 ++-- easel/desktop/package.json | 2 +- easel/desktop/renderer.js | 21 ++++++++++++++++----- easel/desktop/scripts/linux-download.html | 4 ++-- easel/package.json | 2 +- easel/src/public-activity.mjs | 5 +++-- easel/src/tui.mjs | 11 +++++++---- easel/test/notebook-layout.electron.cjs | 10 +++++++++- easel/test/public-activity.test.mjs | 17 ++++++++++++++++- system/public/aesel/index.html | 4 ++-- 10 files changed, 59 insertions(+), 21 deletions(-) diff --git a/easel/desktop/package-lock.json b/easel/desktop/package-lock.json index 8145ba2676..7eef5ab5a0 100644 --- a/easel/desktop/package-lock.json +++ b/easel/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "aesel-desktop", - "version": "0.7.9", + "version": "0.7.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aesel-desktop", - "version": "0.7.9", + "version": "0.7.10", "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 46943631de..291d0a8fc6 100644 --- a/easel/desktop/package.json +++ b/easel/desktop/package.json @@ -1,7 +1,7 @@ { "name": "aesel-desktop", "productName": "aesel", - "version": "0.7.9", + "version": "0.7.10", "private": true, "main": "main.cjs", "scripts": { diff --git a/easel/desktop/renderer.js b/easel/desktop/renderer.js index bcf7300418..1aceb601b0 100644 --- a/easel/desktop/renderer.js +++ b/easel/desktop/renderer.js @@ -634,7 +634,10 @@ boot().catch(error => { document.body.dataset.notebookReady='true';document.getE button.addEventListener('click',action);menu.append(button);return button; } function rebuildMenu(){ - const nextKey=JSON.stringify([provider,credits,buildInfo]);if(nextKey===menuKey)return;menuKey=nextKey; + const nextKey=JSON.stringify([provider,credits,buildInfo]);if(nextKey===menuKey)return; + // Live activity must not tear down an open native dropdown mid-selection. + if(menu.open&&menu.contains(document.activeElement)&&document.activeElement.tagName==='SELECT'&&!provider?.busy)return; + menuKey=nextKey; const focused=document.activeElement?.textContent; const scroll=menu.scrollTop;const previewHost=menu.querySelector('.history-preview-host');const historyOpen=menu.querySelector('details')?.open??true; menu.replaceChildren();const n=balance(); @@ -643,10 +646,17 @@ boot().catch(error => { document.body.dataset.notebookReady='true';document.getE if(buildInfo){const status=document.createElement('p');status.className='build-status';status.dataset.status=buildInfo.status;status.title=buildInfo.revision?`Source commit ${buildInfo.revision}`:'';const channel=buildInfo.channel==='dev'?'Dev':buildInfo.channel==='release'?'Release':'Local';const state={current:'Up to date',ready:'Update ready',modified:'Local changes · sync paused',checking:'Checking…',syncing:'Downloading dev build…',downloading:'Downloading update…',unknown:'Unable to verify'}[buildInfo.status]||'Unable to verify';status.textContent=[channel,buildInfo.version,(buildInfo.tree||buildInfo.revision)?.slice(0,8),state].filter(Boolean).join(' · ');menu.append(status);item('Check for updates',()=>window.aesel.checkBuildUpdates());} const current=document.createElement('p');current.textContent=[provider?.backend==='ac'?'Braincells · automatic model':provider?.backend==='codex'?'Codex':'Claude',provider?.backend==='ac'?'':provider?.model].filter(Boolean).join(' · ');menu.append(current); - for(const [index,id,name] of [[0,'ac','AC'],[1,'claude','Claude'],[2,'codex','Codex']]){ - const choice=item(name,()=>{close();if(provider?.backend!==id)window.aesel.input(`\x1b[99;${index}~`);},{selected:provider?.backend===id,disabled:!!provider?.busy}); - const icon=new Image();icon.src=`assets/provider-${id}.svg`;icon.alt='';icon.className='provider-mark';choice.prepend(icon); + const providerLabel=document.createElement('label');providerLabel.textContent='Provider';providerLabel.className='settings-field'; + const providerSelect=document.createElement('select');providerSelect.setAttribute('aria-label','Provider');providerSelect.disabled=!!provider?.busy; + for(const [index,id,name] of [[0,'ac','AC · Braincells'],[1,'claude','Claude'],[2,'codex','Codex']]){ + const option=document.createElement('option');option.value=String(index);option.textContent=name;option.selected=provider?.backend===id;providerSelect.append(option); } + providerSelect.addEventListener('change',()=>{ + const index=Number(providerSelect.value),id=['ac','claude','codex'][index]; + if(provider?.busy||!id||provider?.backend===id)return; + close();window.aesel.input(`\x1b[99;${index}~`); + }); + providerLabel.append(providerSelect);menu.append(providerLabel); if(provider?.backend!=='ac'){ const modelLabel=document.createElement('label');modelLabel.textContent='Model';modelLabel.className='settings-field'; const modelSelect=document.createElement('select');modelSelect.setAttribute('aria-label','Model');modelSelect.disabled=!!provider?.busy||!provider?.models?.length; @@ -656,7 +666,7 @@ boot().catch(error => { document.body.dataset.notebookReady='true';document.getE } if(![...(provider?.models||[])].some(model=>model.id===(provider.selectedModel??provider.model)))modelSelect.selectedIndex=0; const backendIndex=['ac','claude','codex'].indexOf(provider?.backend); - modelSelect.addEventListener('change',()=>{window.aesel.input(`\x1b[99;4;${backendIndex};${modelSelect.value}~`);}); + modelSelect.addEventListener('change',()=>{if(provider?.busy)return;window.aesel.input(`\x1b[99;4;${backendIndex};${modelSelect.value}~`);}); modelLabel.append(modelSelect);menu.append(modelLabel); } @@ -688,6 +698,7 @@ boot().catch(error => { document.body.dataset.notebookReady='true';document.getE if(menu.open)[...menu.querySelectorAll('button')].find(button=>button.textContent===focused)?.focus({preventScroll:true}); menu.scrollTop=scroll; } + menu.addEventListener('focusout',()=>queueMicrotask(rebuildMenu)); async function render(){ const n=balance();const hosted=provider?.backend==='ac'; footer.hidden=!provider; diff --git a/easel/desktop/scripts/linux-download.html b/easel/desktop/scripts/linux-download.html index 51e64c201a..f7f2703b17 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/package.json b/easel/package.json index 7fe7d9a796..ce80dfcf72 100644 --- a/easel/package.json +++ b/easel/package.json @@ -1,6 +1,6 @@ { "name": "aesel", - "version": "0.7.9", + "version": "0.7.10", "private": true, "type": "module", "scripts": { diff --git a/easel/src/public-activity.mjs b/easel/src/public-activity.mjs index 98f72eac7e..5ad73d68c9 100644 --- a/easel/src/public-activity.mjs +++ b/easel/src/public-activity.mjs @@ -19,6 +19,7 @@ export function observeToolActivity(state, method, item) { state.activityTools ||= new Map(); if (method === 'item/started') { state.activityTools.set(item.id, toolActivity(item)); + if (state.activityText) state.activityIntent = state.activityText; state.activityText = ''; state.activityMessageId = null; } else if (method === 'item/completed') { @@ -36,12 +37,12 @@ export function publicActivity(state) { if (state.status === 'interrupting') return "I'm stopping"; const text = String(state.activityText || '').replace(/\s+/g, ' ').trim(); if (text) return text; - return state.activityStage || ({ + return state.activityStage || String(state.activityIntent || '').replace(/\s+/g, ' ').trim() || ({ preparing: "I'm on my way", connecting: "I'm connecting", waiting: "I'm waiting for a response", generating: "I'm putting my reply together", composing: "I'm preparing my next step", writing: "I'm saving the changes", - }[state.status]) || "I'm working on it"; + }[state.status]) || "I'm considering the next move"; } diff --git a/easel/src/tui.mjs b/easel/src/tui.mjs index 588c9f5f6b..c55658d6ae 100755 --- a/easel/src/tui.mjs +++ b/easel/src/tui.mjs @@ -383,7 +383,7 @@ function toolInstructions() { } function developerInstructions() { - const replyStyle = "Default to one short sentence, usually under 25 words, about the visible result. Start work silently, including on the first request for a new piece. Do not send a greeting, acknowledgement, plan, or pre-tool preamble such as 'I will look at the current piece first', 'Let me read the file', or 'I will make that change'. Reading source and checking the preview are internal work; the interface already shows activity. For an actionable request, speak after making the change, or when a concrete blocker, necessary question, or consent decision requires the user. A direct question may be answered immediately without inventing work. Use plain, warm language. Speak in first person as the donkey, with natural contractions (for example, “I made the circle smaller”). Your public words stream into the donkey’s thought bubble. Do not wrap them in parentheses or narrate the donkey in third person. Describe only actions and results supported by the current tool evidence; do not invent progress or claim a visual check you have not made. Do not summarize the request, list changes, announce success, discuss your process, or end with an offer. Ask one gentle question only when it helps the user explore or make a necessary choice. Never add a question just to sound Socratic. Expand only when the user asks for an explanation or essential evidence requires it. Omit routine URLs, commits, hashes, file paths, tool names, tests, and publishing details. Report material failures, limitations, costs, and required consent honestly and briefly. When discussing a tunable color or numeric constant, use its exact source color literal or constant identifier, preferably as inline code, so the notebook can bind it to an editor. Mention only values useful to the request; do not dump a palette or parameter list. Aesel renders Markdown tables, LaTeX math in $...$ or $$...$$, mermaid fenced diagrams, and static svg fenced vector figures. Prefer concise mathematical notation (for example ×, →, θ, fractions, or a short equation), a small diagram, or a meaningful symbol when it explains an idea more directly than words. Define unfamiliar symbols briefly. Do not add decorative icons or longer explanations just to exercise the renderer. Use one compact visual when requested or when it explains more clearly than prose; do not add decorative headings or restate the visual. This is rich chat rendering, not a full LaTeX document compiler. Do not output image URLs or HTML for figures."; + const replyStyle = "Default to one short sentence, usually under 25 words, about the visible result. During an actionable request, give one short public bubble line before the first edit, then another only when the concrete approach changes or new evidence matters. Keep each line around 6–14 words, in first person, about the particular object and action in this request. Be playful when it fits: “I'm giving those wheels a little swagger” or “I'm untangling that roof overlap.” These are examples of tone, not stock phrases to repeat. Say what you are about to try, not that it has already worked. Read the existing source before describing an edit whose details you do not know yet. Stream the bubble line as ordinary public text, then call the tool in the same response; do not pause for acknowledgement. No generic acknowledgements, task restatements, step lists, or hidden reasoning. Intermediate lines live only in the bubble; finish with a separate brief reply about the supported result. A direct question may be answered immediately without inventing work. Use plain, warm language. Speak in first person as the donkey, with natural contractions (for example, “I made the circle smaller”). Your public words stream into the donkey’s thought bubble. Do not wrap them in parentheses or narrate the donkey in third person. Describe only actions and results supported by the current tool evidence; do not invent progress or claim a visual check you have not made. Do not summarize the request, list changes, announce generic success, narrate routine tool mechanics, or end with an offer. Ask one gentle question only when it helps the user explore or make a necessary choice. Never add a question just to sound Socratic. Expand only when the user asks for an explanation or essential evidence requires it. Omit routine URLs, commits, hashes, file paths, tool names, tests, and publishing details. Report material failures, limitations, costs, and required consent honestly and briefly. When discussing a tunable color or numeric constant, use its exact source color literal or constant identifier, preferably as inline code, so the notebook can bind it to an editor. Mention only values useful to the request; do not dump a palette or parameter list. Aesel renders Markdown tables, LaTeX math in $...$ or $$...$$, mermaid fenced diagrams, and static svg fenced vector figures. Prefer concise mathematical notation (for example ×, →, θ, fractions, or a short equation), a small diagram, or a meaningful symbol when it explains an idea more directly than words. Define unfamiliar symbols briefly. Do not add decorative icons or longer explanations just to exercise the renderer. Use one compact visual when requested or when it explains more clearly than prose; do not add decorative headings or restate the visual. This is rich chat rendering, not a full LaTeX document compiler. Do not output image URLs or HTML for figures."; if (state.medium !== 'piece') return [ replyStyle, `You are in aesel making a ${state.medium}. Use the artifact tools to edit the selected artifact, not write_piece or direct filesystem edits.`, @@ -913,7 +913,7 @@ function handleNotification({ method, params = {} }) { } switch (method) { case "turn/started": - state.activityText="";state.activityStage="";state.activityMessageId=null;state.activityTools?.clear(); + state.activityText="";state.activityIntent="";state.activityStage="";state.activityMessageId=null;state.activityTools?.clear(); state.requestStartedAt ||= Date.now(); state.busy = true; startDance(); @@ -927,7 +927,10 @@ function handleNotification({ method, params = {} }) { break; case "turn/progress": state.status = params.phase || "working"; - if (["connecting", "waiting", "composing"].includes(state.status)) state.activityText = ""; + if (["connecting", "waiting", "composing"].includes(state.status)) { + if (state.activityText) state.activityIntent = state.activityText; + state.activityText = ""; + } state.progressBytes = params.bytes || state.progressBytes || 0; break; case "item/agentMessage/delta": @@ -979,7 +982,7 @@ function handleNotification({ method, params = {} }) { // message of its own, so the meter reads it from here when it is there. if (params.turn?.usage) state.energy.add(params.turn.model || state.model || model, params.turn.usage); const finalReply=state.entries.find(e=>e.id===state.activityMessageId);if(finalReply)delete finalReply.activityOnly; - state.activityText="";state.activityStage="";state.activityMessageId=null;state.activityTools?.clear(); + state.activityText="";state.activityIntent="";state.activityStage="";state.activityMessageId=null;state.activityTools?.clear(); state.busy = false; state.status = params.turn?.status === "failed" ? "failed" : "ready"; engine.turnId = null; diff --git a/easel/test/notebook-layout.electron.cjs b/easel/test/notebook-layout.electron.cjs index 7746d37781..b72a1636a0 100644 --- a/easel/test/notebook-layout.electron.cjs +++ b/easel/test/notebook-layout.electron.cjs @@ -8,6 +8,7 @@ const assert = require("node:assert/strict"); const root = process.cwd(); const errors = []; let opened = ""; + const inputs=[];ipcMain.on("input",(_event,text)=>inputs.push(text)); ipcMain.handle("native-prox-title", (_, value) => JSON.parse( require("node:child_process").execFileSync( @@ -146,7 +147,7 @@ const assert = require("node:assert/strict"); ); assert.equal( await win.webContents.executeJavaScript( - `document.querySelector('#provider-menu select')`, + `document.querySelector('#provider-menu select[aria-label="Model"]')`, ), null, ); @@ -162,6 +163,13 @@ const assert = require("node:assert/strict"); ), "Remote inference · thinking · Reading the piece", ); + assert.deepEqual(await win.webContents.executeJavaScript(`Array.from(document.querySelector('select[aria-label="Provider"]').options,o=>o.textContent)`),['AC · Braincells','Claude','Codex']); + await win.webContents.executeJavaScript(`window.providerDropdown=document.querySelector('select[aria-label="Provider"]');providerDropdown.focus();window.updateProviderFooter({backend:'ac',model:'Luna',status:'ready',mode:'remote',activity:'A new status',models:[],versions:[]})`); + assert.equal(await win.webContents.executeJavaScript(`document.querySelector('select[aria-label="Provider"]')===window.providerDropdown`),true,'Status updates preserve the open dropdown'); + await win.webContents.executeJavaScript(`providerDropdown.value='2';providerDropdown.dispatchEvent(new Event('change'))`); + await delay(40);assert(inputs.includes('\x1b[99;2~')); + await win.webContents.executeJavaScript(`window.updateProviderFooter({backend:'claude',model:'sonnet',selectedModel:'sonnet',models:[{id:'sonnet',label:'Sonnet'},{id:'opus',label:'Opus'}],versions:[]});document.getElementById('credit-label').click();const m=document.querySelector('select[aria-label="Model"]');m.value='1';m.dispatchEvent(new Event('change'))`); + await delay(40);assert(inputs.includes('\x1b[99;4;1;1~')); await win.webContents.executeJavaScript( `document.querySelector('#provider-menu header button').click()`, ); diff --git a/easel/test/public-activity.test.mjs b/easel/test/public-activity.test.mjs index 74613ee652..d53741a39a 100644 --- a/easel/test/public-activity.test.mjs +++ b/easel/test/public-activity.test.mjs @@ -34,7 +34,7 @@ test('parallel tools remain accurate as calls finish out of order', () => { observeToolActivity(state,'item/completed',{id:'advice',type:'dynamicToolCall',tool:'jev'}); assert.equal(publicActivity(state), "I'm looking at the preview"); observeToolActivity(state,'item/completed',frame); - assert.equal(publicActivity(state), "I'm working on it"); + assert.equal(publicActivity(state), 'old reply'); assert.equal(state.status,'working'); }); @@ -44,3 +44,18 @@ test('only known operations receive specific captions; arguments never appear', assert.equal(toolActivity({type:'dynamicToolCall',tool:'Bash · secret command'}), "I'm running a command"); assert.equal(toolActivity({type:'mcpToolCall',tool:'unknown',arguments:{secret:'private'}}), "I'm using a tool"); }); + + +test('contextual public intent survives tool preparation, but cannot override inspection or a stop', () => { + const state = {busy:true,status:'composing',activityIntent:"I'm untangling that roof overlap"}; + assert.equal(publicActivity(state), state.activityIntent); + const frame = {id:'frame',type:'dynamicToolCall',tool:'ac_frame'}; + observeToolActivity(state,'item/started',frame); + assert.equal(publicActivity(state), "I'm looking at the preview"); + observeToolActivity(state,'item/completed',frame); + assert.equal(publicActivity(state), state.activityIntent); + state.status='interrupting'; + assert.equal(publicActivity(state), "I'm stopping"); + state.busy=false; + assert.equal(publicActivity(state), ''); +}); diff --git a/system/public/aesel/index.html b/system/public/aesel/index.html index b0d61908bd..0d68df79b2 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.9 · 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.10 · Linux 0.7.0 (Easel)

-- 2.51.2