diff --git a/apple/whistlegraph/README.md b/apple/whistlegraph/README.md index 1e0b251fd5..f66c902470 100644 --- a/apple/whistlegraph/README.md +++ b/apple/whistlegraph/README.md @@ -98,3 +98,7 @@ node --test system/tests/easel-input-images.test.mjs system/tests/easel-paid-cre ## App icon The app icon depicts **Butterfly Cosplayer (IMAB)** by Jeffrey Alan Scudder. `Artwork/imab-icon-goopy.png` is a generated glossy, thick adaptation of the existing drawing in `pop/hellsine/assets/whistlegraph-butterfly.png`, using the original performance glyph at `system/public/whistlegraph.org/glyphs/imab.jpg` as a color reference. It is not the original score image. The flat adaptation remains in `Artwork/imab-icon.png`; built-in imagegen prompts are retained in `Artwork/imab-icon.prompt.txt` and `Artwork/imab-icon-goopy.prompt.txt`. `./build-icon.sh` packages the adaptation as an opaque 1,024-pixel iOS icon. + +Build 98: the brain button below the preview opens pixel size (eye), model selection, provider, daily/purchased braincells, and request usage. New Piece and Your Pieces remain in the piece-name menu, alongside the selected model/provider. Model selection persists per verified account. Jeffrey defaults to hosted Claude Opus 5 with 16,384 output tokens, 4,096 thinking tokens, 12 tool rounds and a 180-second generation/check deadline; other accounts retain their existing budgets. Both still have one shared repair attempt and mandatory source/runtime/picture checks. This is OpenRouter inference, not a remote Claude/Codex subscription connection. + +The code ticker highlights JavaScript. The preview compiles completed paint statements during streaming using a temporary closing brace, and complete revision-checked replacements during edits. Provisional errors wait for more source; partial previews never enter the saved version history. The final tool source must still pass checks. diff --git a/apple/whistlegraph/Resources/Web/engine.mjs b/apple/whistlegraph/Resources/Web/engine.mjs index 4c4edfbaf3..b6dca1a29f 100644 --- a/apple/whistlegraph/Resources/Web/engine.mjs +++ b/apple/whistlegraph/Resources/Web/engine.mjs @@ -19,8 +19,8 @@ import {musicalPrompt} from './musical-input.mjs'; import {PieceVersions} from './piece-versions.mjs'; // The same inference/tool loop as Aesel, with a piece-first streaming renderer. import {AcServer} from '/easel/src/ac-server.mjs'; -import {DEFAULT_MODEL,REPAIR_MODEL,GENERATION_INSTRUCTIONS} from './generation-policy.mjs'; -import {runnablePrefix} from './stream-preview.mjs'; +import {DEFAULT_MODEL,generationProfile,modelChoices,MODEL_LABELS,GENERATION_INSTRUCTIONS} from './generation-policy.mjs'; +import {runnablePrefix,partialString,streamedEdits,streamedCode} from './stream-preview.mjs'; import * as vfs from '/easel/phone/shim/fs.mjs'; const post = body => window.webkit.messageHandlers.walkie.postMessage({id:'engine',...body}); @@ -38,17 +38,40 @@ try{lastAttempt=JSON.parse(localStorage.getItem(storageKey+'-attempt')||'null'); let versions=null,turnSucceeded=false,turnRequest='',turnParent=null,turnRuntimeFailed=false,turnError=''; let token = '', busy = false, server, source = '', previous = '', pending = '', checkpoints = 0; let feedback = null, lastPaintedSource = '', previewSource = '', provisional = '', compileTimer = null; +let streamTool='',streamBase='',streamRevision='',rejectedPreview=''; let outputStream='',reasoningStream='',code = '', codeItem = '', firstDelta = false, started = 0, timer, ready = false, painted = false; const events = []; const signIn=document.createElement('button');signIn.id='connect-ac';signIn.textContent='Account';signIn.onclick=()=>post({action:'signIn'});const identity=document.createElement('div');identity.id='walkieware-identity';const codeLabel=document.createElement('span');codeLabel.id='walkieware-thread';identity.append(signIn,codeLabel);document.body.append(identity); -let accountToken='',accountHandle='',accountPalette=[]; +let accountToken='',accountHandle='',accountPalette=[],accountVerification=Promise.resolve(); +let turnHandle='',turnModel='',activeModel='',braincells=null,braincellsError='',creditsRequest=0; +function selectedModel(handle=accountHandle){try{return localStorage.getItem('whistlegraph-model-'+handle)||'';}catch{return '';}} +function profile(repair=false){return generationProfile(busy?turnHandle:accountHandle,{repair,model:busy?turnModel:selectedModel()});} +async function refreshBraincells(){ + const currentToken=token,request=++creditsRequest; + if(!currentToken){braincells=null;braincellsError='Sign in to view braincells';nativeSnapshot();return;} + braincellsError='';nativeSnapshot(); + try{ + const response=await fetch('https://aesthetic.computer/api/easel-credits',{headers:{Authorization:'Bearer '+currentToken},signal:AbortSignal.timeout(8000)}); + if(!response.ok)throw Error('Braincells unavailable'); + const value=await response.json(); + if(![value.remaining,value.used,value.limit,value.purchased].every(v=>Number.isFinite(v)&&v>=0))throw Error('Braincells unavailable'); + if(token!==currentToken||request!==creditsRequest)return; + braincells=value;braincellsError=''; + }catch{if(token!==currentToken||request!==creditsRequest)return;braincells=null;braincellsError='Braincells unavailable';} + nativeSnapshot(); +} +function inferenceSnapshot(){ + const model=activeModel||profile().model,receipt=activeReceipt?.value||receipts.rows.at(-1)?.receipt; + return {model,label:MODEL_LABELS[model]||model,provider:'OpenRouter',selection:profile().model,models:modelChoices(accountHandle),braincells,braincellsError, + usage:receipt?{inputTokens:receipt.rounds.reduce((n,r)=>n+(r.usage?.inputTokens||0),0),outputTokens:receipt.rounds.reduce((n,r)=>n+(r.usage?.outputTokens||0),0),rounds:receipt.rounds.length,repairs:receipt.repairs,status:receipt.status}:null}; +} function paintHandle(handle,colors=handleCharacterColors('@'+handle)){ accountPalette=colors; signIn.replaceChildren(...Array.from('@'+handle,(character,index)=>{const span=document.createElement('span');span.textContent=character;span.style.color='rgb('+colors[index].join(',')+')';return span;}));nativeSnapshot(); } function accountIdentity(value){ - if(value===accountToken)return;accountToken=value;accountHandle='';signIn.textContent=value?'…':'Sign in'; - if(value)void verifyAccount(value).then(account=>{if(accountToken!==value)return;accountHandle=account.handle;if(!accountHandle){signIn.textContent='Set handle';return;}paintHandle(accountHandle);const handle=accountHandle;void fetchHandleColors('@'+handle).then(colors=>{if(accountToken===value&&accountHandle===handle)paintHandle(handle,colors);}).catch(()=>{});}).catch(()=>{if(accountToken===value)signIn.textContent='Retry sign-in';}); + if(value===accountToken)return;accountToken=value;accountHandle='';braincells=null;braincellsError='';signIn.textContent=value?'…':'Sign in'; + accountVerification=value?verifyAccount(value).then(account=>{if(accountToken!==value)return;accountHandle=account.handle;if(!accountHandle){signIn.textContent='Set handle';return;}paintHandle(accountHandle);void refreshBraincells();const handle=accountHandle;void fetchHandleColors('@'+handle).then(colors=>{if(accountToken===value&&accountHandle===handle)paintHandle(handle,colors);}).catch(()=>{});}).catch(()=>{if(accountToken===value)signIn.textContent='Retry sign-in';}):Promise.resolve(); } const $ = id => document.getElementById(id); const ui = document.createElement('section'); ui.id = 'live-work'; ui.hidden = true; @@ -118,7 +141,7 @@ function nativeSnapshot(){ const displayVersion=presentedVersion===null?versions?.head:versions?.value.versions.find(v=>v.id===presentedVersion); if(captionSource!==displayVersion?.source){captionSource=displayVersion?.source;caption=pieceCaption(captionSource||'');} const phaseText=$('live-phase').textContent; - const snapshot={output:outputStream,caption,code:thread?.identity.code||'',handle:accountHandle,colors:accountPalette,head:displayVersion?.id||0,hasPiece:!!source.trim(),hasPreview:!!source.trim()||!!provisional.trim(),busy,phase:phaseText,error:!busy&&/error|unavailable|could not|sign.in|loading|no piece/i.test(phaseText)?phaseText:'',attempt:lastAttempt?{request:lastAttempt.request.slice(0,1000),status:lastAttempt.status,error:lastAttempt.error||''}:null}; + const snapshot={inference:inferenceSnapshot(),output:outputStream,caption,code:thread?.identity.code||'',handle:accountHandle,colors:accountPalette,head:displayVersion?.id||0,hasPiece:!!source.trim(),hasPreview:!!source.trim()||!!provisional.trim(),busy,phase:phaseText,error:!busy&&/error|unavailable|could not|sign.in|loading|no piece/i.test(phaseText)?phaseText:'',attempt:lastAttempt?{request:lastAttempt.request.slice(0,1000),status:lastAttempt.status,error:lastAttempt.error||''}:null}; const serialized=JSON.stringify(snapshot);if(serialized===nativeLast&&!historyChanged)return;nativeLast=serialized; if(historyChanged)snapshot.versions=nativeRevisions; post({action:'snapshot',snapshot}); @@ -126,6 +149,11 @@ function nativeSnapshot(){ },80); } window.walkiewareNativeCommand=command=>{ + if(command.action==='refreshBraincells')void refreshBraincells(); + if(command.action==='setModel'&&!busy&&accountHandle&&modelChoices(accountHandle).some(m=>m.id===command.text)){ + try{localStorage.setItem('whistlegraph-model-'+accountHandle,command.text);}catch{return;} + server?.close();server=null;activeModel='';nativeSnapshot(); + } if(command.action==='ask'&&typeof command.text==='string'&&(command.text.trim()||command.drawing)&&Array.from(new Intl.Segmenter(undefined,{granularity:'grapheme'}).segment(command.text)).length<=96&&!busy)void window.walkiewareAskDrawing(command.text.trim(),command.drawing); if(command.action==='checkout'&&!busy){presentedVersion=null;narrationPending=command.version;jumpVersion(command.version);} if(command.action==='presentVersion'&&!busy){ @@ -218,7 +246,7 @@ async function checkVisualResult() { const reviewUsage={}; const verdict=await reviewVisualResult({evidence,sourceHash:hash,renderID:id,source:target, request:inferenceRequest(turnRequest),history:selectedBranch(versions.value),drawing:chalk, - model:window.__walkiewareModel||DEFAULT_MODEL,token,signal, + model:window.__walkiewareModel||profile().model,token,signal, onHeaders:response=>{if(round)activeReceipt?.headers(round,response);}, onEvent:e=>{ if(e.message?.model||e.model)activeReceipt?.notify('model/reported',{reported:e.message?.model||e.model}); @@ -245,61 +273,51 @@ function compileStream() { compileTimer=setTimeout(()=>{ compileTimer=null; if(!busy)return; - const candidate=runnablePrefix(partialSource(code)); - if(!candidate||candidate===previewSource)return; + const candidate=streamTool==='edit_piece'?streamedEdits(code,streamBase,streamRevision):runnablePrefix(partialString(code).value); + if(!candidate||candidate===previewSource||candidate===rejectedPreview)return; provisional=candidate; document.body.classList.add('live-preview'); benchmark('firstIncrementalCompile');log('Running streamed code'); render(candidate); - },80); + },160); } function saved() { try { localStorage.setItem(storageKey,source); } catch {} updateFeed(); } function review(show) { $('speak').disabled=busy; $('speak-label').textContent=busy?'Working…':'Hold to talk'; } -function end() { clearTimeout(compileTimer);compileTimer=null; if(provisional && previewSource!==source){render(source||'export function paint({wipe}) {wipe("black");}');provisional='';} busy=false; updateFeed(); clearInterval(timer); $('live-stop').hidden=true; review(source!==previous); window.walkiewareWorkFinished?.(); } +function end() { clearTimeout(compileTimer);compileTimer=null; if(provisional && previewSource!==source){render(source||'export function paint({wipe}) {wipe("black");}');provisional='';} busy=false;activeModel='';void refreshBraincells(); updateFeed(); clearInterval(timer); $('live-stop').hidden=true; review(source!==previous); window.walkiewareWorkFinished?.(); } function delta(text) { outputStream=(outputStream+text).slice(-6000); if (!firstDelta) {firstDelta=true; log('First model output');benchmark('firstModelOutput');} phase('Writing…'); } -function partialSource(json,field='source') { - const start=json.match(field==='replace'?/"replace"\s*:\s*"/:/"source"\s*:\s*"/);if(!start)return ''; - const raw=json.slice(start.index+start[0].length); - let out='';for(let i=0;i{ if(path!==file)return; - clearTimeout(compileTimer);compileTimer=null;provisional=''; + clearTimeout(compileTimer);compileTimer=null;const wasProvisional=!!provisional;provisional='';rejectedPreview=''; source=value;checkpoints++;benchmark('firstCheckpoint'); turnRuntimeFailed=false; document.body.classList.add('live-preview'); $('initial').hidden=true; $('play-deck').hidden=true; $('live-code').textContent=value; phase(ready?'Evaluating…':'Loading preview…'); log(`Checkpoint ${checkpoints} · valid JavaScript`); - if(value!==previewSource)render(value); + if(wasProvisional||value!==previewSource)render(value); }); const guides = vfs.preload(['pieces.md','screen.md','hand.md','kidlisp.md','api.json'].map(name=>'/easel/context/'+name)); function makeServer({repair=false}={}){ - const value=new AcServer({cwd:'/piece',piece:{file,checkpoint:async()=>{const target=source;for(let i=0;i<100;i++){if(painted&&lastPaintedSource===target)return;await new Promise(resolve=>setTimeout(resolve,20));}}},frameCapture:false,layeredEdits:true,token:()=>token,model:repair?(window.__walkiewareRepairModel||REPAIR_MODEL):(window.__walkiewareModel||DEFAULT_MODEL), + const settings=profile(repair);activeModel=(repair?window.__walkiewareRepairModel:window.__walkiewareModel)||settings.model;nativeSnapshot(); + const value=new AcServer({cwd:'/piece',piece:{file,checkpoint:async()=>{const target=source;for(let i=0;i<100;i++){if(painted&&lastPaintedSource===target)return;await new Promise(resolve=>setTimeout(resolve,20));}}},frameCapture:false,layeredEdits:true,token:()=>token,model:activeModel, fetch:async(url,options)=>{ benchmark('requestDispatched');const recorder=activeReceipt,round=recorder?.request(); - const body=JSON.parse(options.body);body.max_tokens=4096;options={...options,body:JSON.stringify(body)}; + const body=JSON.parse(options.body);body.max_tokens=settings.maxTokens;options={...options,body:JSON.stringify(body)}; const response=await globalThis.fetch(url,options);if(round)recorder.headers(round,response); benchmark('inferenceHeaders',{status:response.status});return response; - },preview:true,rounds:repair?2:4,outputContinuations:repair?0:1,reasoning:repair?{max_tokens:1024}:{effort:'none'},thinking:repair?{type:'enabled',budget_tokens:1024}:{type:'disabled'}, + },preview:true,rounds:settings.rounds,outputContinuations:settings.outputContinuations,reasoning:settings.reasoning,thinking:settings.thinking, developerInstructions:GENERATION_INSTRUCTIONS}); value.runtimeFeedback=()=>feedback; value.on('notification',({method,params})=>{ activeReceipt?.notify(method,params); if(method==='turn/progress' && !firstDelta) phase(params.phase==='connecting'?'Connecting…':'Waiting for model…'); if(method==='item/modelCode/delta'){ - delta('');if(codeItem!==params.itemId){benchmark('layerStarted',{tool:params.tool||'write_piece'});code='';codeItem=params.itemId;} - code+=params.delta;const visibleCode=params.tool==='edit_piece'?partialSource(code,'replace'):partialSource(code);$('live-code').textContent=visibleCode;outputStream=visibleCode.slice(-6000);nativeSnapshot();if(params.tool!=='edit_piece')compileStream();else phase('Editing…'); + delta('');if(codeItem!==params.itemId){benchmark('layerStarted',{tool:params.tool||'write_piece'});code='';codeItem=params.itemId;streamTool=params.tool||'write_piece';streamBase=source;streamRevision=value.revisionForSource(source);rejectedPreview='';} + code+=params.delta;const visibleCode=streamedCode(code,params.tool);$('live-code').textContent=visibleCode;outputStream=visibleCode.slice(-6000);nativeSnapshot();compileStream();if(params.tool==='edit_piece')phase('Editing…'); $('live-details').open=true; } if(method==='item/agentMessage/delta'){delta(params.delta);$('live-request').textContent+=params.delta;} @@ -308,7 +326,7 @@ function makeServer({repair=false}={}){ if(method==='item/reasoning/delta'){reasoningStream=(reasoningStream+params.delta).slice(-6000);if(!firstDelta){benchmark('firstReasoning');log('Model thinking');}if(!code)outputStream=reasoningStream;phase('Thinking…');nativeSnapshot();} if(method==='item/started')benchmark('toolStarted',{tool:params.item?.tool||params.item?.type}); if(method==='item/completed' && params.item?.status?.startsWith('failed')) {log(params.item.status);benchmark('toolFailed',{message:params.item.status});} - if(method==='turn/usage') log('Usage · '+(params.usage.output_tokens??0)+' output tokens'); + if(method==='turn/usage'){log('Usage · '+(params.usage.output_tokens??0)+' output tokens');nativeSnapshot();} if(method==='turn/completed'){turnSucceeded=!params.turn.error&¶ms.turn.status==='completed';turnError=params.turn.error?.message||(params.turn.status==='interrupted'?'Stopped':''); if(params.turn.error){phase('Could not finish');log(params.turn.error.message);} else if(params.turn.status==='interrupted'){phase('Stopped');log('Stopped by you');} @@ -335,7 +353,8 @@ async function ask(text,displayText=text,advice=null,starter=null,localText=text let noChange=false; validationChecks=[];runtimeErrors.length=0; try{ - activeReceipt=new AttemptReceipt({requestID:activeAttempt.id,parent:turnParent,parentHash:await hashSource(previous),path:'compiled',model:window.__walkiewareModel||DEFAULT_MODEL,journal:receipts}); + await accountVerification;if(turnCancelled)throw Error('Stopped');turnHandle=accountToken===token?accountHandle:'';turnModel=selectedModel(turnHandle); + activeReceipt=new AttemptReceipt({requestID:activeAttempt.id,parent:turnParent,parentHash:await hashSource(previous),path:'compiled',model:window.__walkiewareModel||profile().model,journal:receipts}); const drawing=inputData(text)?.drawing; const chalkImage=drawing?drawingImage(drawing):null; text=inferenceRequest(text); @@ -377,7 +396,7 @@ async function ask(text,displayText=text,advice=null,starter=null,localText=text benchmark('guidesReady'); vfs.mount(file,source||'export function paint({wipe}) { wipe("black"); }'); const prompt=compileEditContract({request:text,...selectedBranch(versions.value),source}); - const deadline=setTimeout(()=>{turnCancelled=true;turnError='Edit check timed out';server?.interrupt();},75000); + const deadline=setTimeout(()=>{turnCancelled=true;turnError='Edit check timed out';server?.interrupt();},turnHandle==='jeffrey'?180000:75000); try{ const result=await runEditExperiment({prompt,cancelled:()=>turnCancelled, onRepair:()=>{activeReceipt.value.repairs=1;activeReceipt.save();phase('Repairing…');}, @@ -446,8 +465,15 @@ window.walkiewareEngineEvent=event=>{ if(event.kind==='previewReady'){ready=true;log('AC runtime ready');} if(event.kind==='previewEvent'){ if(event.event?.sourceHash!==previewHash||event.event?.requestID!==renderID)return; - activeReceipt?.observe(event.event); - if(event.event.kind==='painted'){if(turnStarter&&previewSource===turnStarter&&!starterPainted){starterPainted=true;benchmark('starterPainted');}if(previewSource.trimEnd()!==previous.trimEnd())window.__walkiewareSequenceEvent?.('painted');painted=true;lastPaintedSource=previewSource;if(busy&&activeAttempt&&source===previewSource&&!turnRuntimeFailed){activeAttempt={...activeAttempt,checkpoint:source};try{saveAttempt(localStorage,storageKey,activeAttempt);}catch(error){log('Could not persist checkpoint: '+error.message);}}feedback={...feedback,rendered:true,updatedAt:new Date().toISOString()};if(activeReceipt&&previewSource.trimEnd()!==previous.trimEnd())activeReceipt.painted();log('Checkpoint painted');phase(busy?'Building…':lastAttempt?.status==='failed'?'Could not finish · previous version restored':'Ready to play');if(narrationPending!==null){post({action:'narrationReady',version:narrationPending});narrationPending=null;}void resumeAttempt();} + const streamingPreview=!!provisional&&previewSource===provisional; + if(streamingPreview&&(event.event.kind==='invalidated'||event.event.kind==='console'&&(event.event.event?.level==='error'||/\b(?:Paint|Sim|Boot) failure\b/i.test(event.event.event?.message||'')))){ + rejectedPreview=provisional;log('Waiting for more source'); + const fallback=lastPaintedSource!==previewSource?lastPaintedSource:source; + if(fallback&&fallback!==previewSource)render(fallback); + return; + } + if(!streamingPreview)activeReceipt?.observe(event.event); + if(event.event.kind==='painted'){if(turnStarter&&previewSource===turnStarter&&!starterPainted){starterPainted=true;benchmark('starterPainted');}if(previewSource.trimEnd()!==previous.trimEnd())window.__walkiewareSequenceEvent?.('painted');painted=true;lastPaintedSource=previewSource;if(busy&&activeAttempt&&source===previewSource&&!turnRuntimeFailed){activeAttempt={...activeAttempt,checkpoint:source};try{saveAttempt(localStorage,storageKey,activeAttempt);}catch(error){log('Could not persist checkpoint: '+error.message);}}feedback={...feedback,rendered:true,updatedAt:new Date().toISOString()};if(!streamingPreview&&activeReceipt&&previewSource.trimEnd()!==previous.trimEnd())activeReceipt.painted();log('Checkpoint painted');phase(busy?'Building…':lastAttempt?.status==='failed'?'Could not finish · previous version restored':'Ready to play');if(narrationPending!==null){post({action:'narrationReady',version:narrationPending});narrationPending=null;}void resumeAttempt();} if(event.event.kind==='invalidated'){turnRuntimeFailed=true;window.__walkiewareSequenceEvent?.('runtimeError',{message:'Preview invalidated'});painted=false;feedback={...feedback,rendered:false,logs:[...(feedback?.logs||[]),{level:'error',text:'Preview invalidated'}],updatedAt:new Date().toISOString()};log('Preview failed; inspect activity');phase('Preview error');if(lastPaintedSource && lastPaintedSource!==previewSource){render(lastPaintedSource);log('Restored last painted checkpoint');}} if(event.event.kind==='console'&&['error','warn'].includes(event.event.event?.level)){ const entry={level:event.event.event.level,text:event.event.event.message||'Runtime error'}; diff --git a/apple/whistlegraph/Resources/Web/generation-policy.mjs b/apple/whistlegraph/Resources/Web/generation-policy.mjs index 27087baf3c..4f3d560cd0 100644 --- a/apple/whistlegraph/Resources/Web/generation-policy.mjs +++ b/apple/whistlegraph/Resources/Web/generation-policy.mjs @@ -1,3 +1,20 @@ export const DEFAULT_MODEL = 'deepseek/deepseek-v4.1-flash'; export const REPAIR_MODEL = 'deepseek/deepseek-v4-pro'; -export const GENERATION_INSTRUCTIONS = 'This surface is Whistlegraph. The person watches the piece being made. A local starter may already be visible: its source is the real current piece; refine it instead of discarding it. For an existing piece, make small revision-checked edit_piece calls instead of resending unchanged code. Use write_piece to create the initial piece or for a necessary rewrite. No prose preamble. Each ask becomes one internal version; complete edit checkpoints are preview layers within it. Make the first visible working drawing in 3–6 lines whenever possible. The UI compiles runnable module prefixes during streaming: put required declarations and a complete exported paint function first; avoid long data tables or helper code before that first drawing. Then refine in small complete write_piece checkpoints only as needed to satisfy the request. Do not add unrequested animation or features. For animation use Number(paintCount) from the paint API, or explicit state advanced in sim; there is no numeric frame API. A request for a spoken narrator needs audible speak(), not just written notes; use the speech guide. Use numeric RGB ink(r,g,b) or six-digit hex colors like "#ffffff"; three-digit CSS hex colors are unsupported. Use AC ink alpha on the 0–255 scale and size geometry relative to the screen. Every saved piece must have working helpers and a caption; never finish with placeholder or empty drawing helpers. line(x1,y1,x2,y2) draws ONE segment; its fifth argument is thickness, never another coordinate. For a path, loop over adjacent point pairs with separate four-argument line calls; never spread three or more point pairs into line. Use the native oval(x,y,radiusX,radiusY,filled) for ellipses instead of approximating them with overlapping circles. Fit the entire composition, including radii and stroke thickness, inside the screen with margins; use at most 90% of the fitting scale and center it. Do not repeat the prompt or add interaction instructions, help text, usage hints, or captions inside or outside the picture unless explicitly requested. Use the default write font for other in-piece text unless the person requests another. Use ac_preview for runtime observations; this phone has no desktop frame-capture service. Once the requested change is painted and runtime-checked, finish the generation turn. The app then captures four timed frames of the actual phone preview and reviews them against the latest request and selected branch before saving. A failed visual check can return concrete mismatches for one narrow repair; do not claim visual success yourself. Never fake progress or tests. Keep one line `export const caption = "…"` in the piece: a plain sentence under 120 characters naming the subject, its mood, what actually moves and what the person can actually do, if anything; never invent motion or interactions in the caption. Update it whenever an edit changes any of that. It is the piece\'s memory of itself and comes back to you with every follow-up. Preserve the current piece and the original intent of the selected branch on revisions. Short follow-ups modify that existing world; do not replace its subject, characters, message, interactions, or sound without a request to do so. Treat uncertain speech transcriptions conservatively in context instead of inventing a new subject from an ambiguous word. Adjectives of culture, nationality or place (Latina, Japanese, Nordic, tropical) describe how things look: palette, patterns, dress, flora, light. They never change the language of any text or speech unless the request mentions words, text, language or voice. Do not turn ordinary spoken requests into text posters, transcript timelines, waveform charts, or word-timed animation unless explicitly requested. No publishing: these are private local previews.'; +export const MODEL_LABELS = { + 'deepseek/deepseek-v4.1-flash':'DeepSeek V4.1 Flash', + 'deepseek/deepseek-v4-pro':'DeepSeek V4 Pro', + 'anthropic/claude-opus-5':'Claude Opus 5', +}; +export function modelChoices(handle) { + return Object.entries(MODEL_LABELS).filter(([id])=>handle==='jeffrey'||id!=='anthropic/claude-opus-5').map(([id,label])=>({id,label})); +} +// The handle comes from verifyAccount, never from a preference or command. +export function generationProfile(handle, {repair=false,model=''}={}) { + const personal=handle==='jeffrey'; + const selected=modelChoices(handle).some(option=>option.id===model)?model:''; + const chosen=selected||(personal?'anthropic/claude-opus-5':DEFAULT_MODEL); + const resolved=repair&&chosen===DEFAULT_MODEL?REPAIR_MODEL:chosen; + if(personal)return {model:resolved,maxTokens:16384,rounds:repair?4:12,outputContinuations:repair?1:2,reasoning:{max_tokens:4096},thinking:{type:'enabled',budget_tokens:4096}}; + return {model:resolved,maxTokens:4096,rounds:repair?2:4,outputContinuations:repair?0:1,reasoning:repair?{max_tokens:1024}:{effort:'none'},thinking:repair?{type:'enabled',budget_tokens:1024}:{type:'disabled'}}; +} +export const GENERATION_INSTRUCTIONS = 'This surface is Whistlegraph. The person watches the piece being made. A local starter may already be visible: its source is the real current piece; refine it instead of discarding it. For an existing piece, make small revision-checked edit_piece calls instead of resending unchanged code. Use write_piece to create the initial piece or for a necessary rewrite. No prose preamble. Each ask becomes one internal version; complete edit checkpoints are preview layers within it. Make the first visible working drawing in 3–6 lines whenever possible. The UI previews complete paint statements and exact replacement checkpoints while streaming: put required declarations and a complete exported paint function first; avoid long data tables or helper code before that first drawing. Then refine in small complete write_piece checkpoints only as needed to satisfy the request. Do not add unrequested animation or features. For animation use Number(paintCount) from the paint API, or explicit state advanced in sim; there is no numeric frame API. A request for a spoken narrator needs audible speak(), not just written notes; use the speech guide. Use numeric RGB ink(r,g,b) or six-digit hex colors like "#ffffff"; three-digit CSS hex colors are unsupported. Use AC ink alpha on the 0–255 scale and size geometry relative to the screen. Every saved piece must have working helpers and a caption; never finish with placeholder or empty drawing helpers. line(x1,y1,x2,y2) draws ONE segment; its fifth argument is thickness, never another coordinate. For a path, loop over adjacent point pairs with separate four-argument line calls; never spread three or more point pairs into line. Use the native oval(x,y,radiusX,radiusY,filled) for ellipses instead of approximating them with overlapping circles. Fit the entire composition, including radii and stroke thickness, inside the screen with margins; use at most 90% of the fitting scale and center it. Do not repeat the prompt or add interaction instructions, help text, usage hints, or captions inside or outside the picture unless explicitly requested. Use the default write font for other in-piece text unless the person requests another. Use ac_preview for runtime observations; this phone has no desktop frame-capture service. Once the requested change is painted and runtime-checked, finish the generation turn. The app then captures four timed frames of the actual phone preview and reviews them against the latest request and selected branch before saving. A failed visual check can return concrete mismatches for one narrow repair; do not claim visual success yourself. Never fake progress or tests. Keep one line `export const caption = "…"` in the piece: a plain sentence under 120 characters naming the subject, its mood, what actually moves and what the person can actually do, if anything; never invent motion or interactions in the caption. Update it whenever an edit changes any of that. It is the piece\'s memory of itself and comes back to you with every follow-up. Preserve the current piece and the original intent of the selected branch on revisions. Short follow-ups modify that existing world; do not replace its subject, characters, message, interactions, or sound without a request to do so. Treat uncertain speech transcriptions conservatively in context instead of inventing a new subject from an ambiguous word. Adjectives of culture, nationality or place (Latina, Japanese, Nordic, tropical) describe how things look: palette, patterns, dress, flora, light. They never change the language of any text or speech unless the request mentions words, text, language or voice. Do not turn ordinary spoken requests into text posters, transcript timelines, waveform charts, or word-timed animation unless explicitly requested. No publishing: these are private local previews.'; diff --git a/apple/whistlegraph/Resources/Web/stream-preview.mjs b/apple/whistlegraph/Resources/Web/stream-preview.mjs index e62975e759..1f1b42c08a 100644 --- a/apple/whistlegraph/Resources/Web/stream-preview.mjs +++ b/apple/whistlegraph/Resources/Web/stream-preview.mjs @@ -1,18 +1,61 @@ import {parse} from './easel/src/vendor/acorn.mjs'; +import {applyPieceEdits} from './easel/src/piece-edits.mjs'; -// Compile complete module prefixes only. Never invent missing brackets or run -// an unfinished paint body. Runtime failures retain the last painted checkpoint. +const options={ecmaVersion:'latest',sourceType:'module'}; +const isPaint=node=>node.type==='ExportNamedDeclaration'&&node.declaration?.type==='FunctionDeclaration'&&node.declaration.id?.name==='paint'; +// A temporary closing brace lets complete paint statements appear before the +// function finishes streaming. Never close strings, expressions or nested blocks. +// This source is preview-only; only the actual completed tool can save a version. export function runnablePrefix(source) { if(!source || source.length>100000)return null; const ends=[source.length]; - for(let i=source.length-1;i>=0&&ends.length<24;i--) { + for(let i=source.length-1;i>=0&&ends.length<80;i--) { if(source[i]==='}'||source[i]===';')ends.push(i+1); } for(const end of ends){ const candidate=source.slice(0,end); - let tree;try{tree=parse(candidate,{ecmaVersion:'latest',sourceType:'module'});}catch{continue;} - const paint=tree.body.some(node=>node.type==='ExportNamedDeclaration'&&node.declaration?.type==='FunctionDeclaration'&&node.declaration.id?.name==='paint'); - if(paint)return candidate; + try {if(parse(candidate,options).body.some(isPaint))return candidate;}catch{} + if(!/[;}]$/.test(candidate.trimEnd()))continue; + try { + const body=parse(candidate+'\n}',options).body, last=body.at(-1); + if(isPaint(last)&&last.declaration.body.body.length)return candidate+'\n}'; + }catch{} } return null; } + +export function partialString(json,field='source') { + const start=json.match(new RegExp('"'+field+'"\\s*:\\s*"'));if(!start)return {value:'',complete:false}; + const raw=json.slice(start.index+start[0].length); + let value='';for(let i=0;i200000)return null; + const supplied=partialString(json,'revision'); + if(!supplied.complete||supplied.value!==revision)return null; + const start=json.match(/"edits"\s*:\s*\[/);if(!start)return null; + let quoted=false,escaped=false,depth=0,begin=-1;const edits=[]; + for(let i=start.index+start[0].length;ipartialString(json.slice(match.index),'replace').value).join('\n'); +} diff --git a/apple/whistlegraph/Sources/BrainSettings.swift b/apple/whistlegraph/Sources/BrainSettings.swift new file mode 100644 index 0000000000..ba3e0c01e8 --- /dev/null +++ b/apple/whistlegraph/Sources/BrainSettings.swift @@ -0,0 +1,107 @@ +import SwiftUI + +struct InferenceSnapshot: Decodable { + struct Model: Decodable, Identifiable { let id: String; let label: String } + struct Braincells: Decodable { + let remaining: Double + let used: Double + let limit: Double + let purchased: Double + let resetsAt: String? + } + struct Usage: Decodable { + let inputTokens: Int + let outputTokens: Int + let rounds: Int + let repairs: Int + let status: String + } + let model: String + let label: String + let provider: String + let selection: String + let models: [Model] + let braincells: Braincells? + let braincellsError: String + let usage: Usage? +} + +struct BrainButton: View { + @ObservedObject var session: WhistlegraphSession + let beforeOpening: () -> Void + @State private var showing = false + var body: some View { + Button { + beforeOpening(); ButtonSounds.play(.pop) + session.command("refreshBraincells"); showing = true + } label: { + Image(systemName: "brain").font(.system(size: 25, weight: .semibold)) + .frame(width: 44, height: 44) + } + .accessibilityLabel("Brain settings").accessibilityIdentifier("brain-settings") + .sheet(isPresented: $showing) { BrainSettings(session: session) } + } +} + +struct BrainSettings: View { + @ObservedObject var session: WhistlegraphSession + @Environment(\.dismiss) private var dismiss + private var disabled: Bool { session.snapshot.busy || session.capturePhase != .idle } + private func cells(_ number: Double) -> String { number.formatted(.number.precision(.fractionLength(0))) } + var body: some View { + NavigationStack { + List { + Section { + Picker("Pixel size", selection: Binding(get: { session.pixelSize }, set: { session.setPixelSize($0) })) { + ForEach(1...4, id: \.self) { Text("\($0)×").tag($0) } + }.pickerStyle(.segmented).disabled(disabled).accessibilityIdentifier("brain-pixel-size") + } header: { Label("Pixel size", systemImage: "eye") } + if let inference = session.snapshot.inference { + Section { + Picker("Model", selection: Binding(get: { inference.selection }, set: { session.command("setModel", text: $0) })) { + ForEach(inference.models) { Text($0.label).tag($0.id) } + }.disabled(disabled || session.snapshot.handle.isEmpty).accessibilityIdentifier("brain-model") + LabeledContent("Service provider", value: inference.provider) + if session.snapshot.busy { LabeledContent("Running", value: inference.label) } + } + Section("Braincells") { + if let balance = inference.braincells { + LabeledContent("Daily remaining", value: cells(balance.remaining) + " / " + cells(balance.limit)) + .accessibilityIdentifier("brain-balance") + ProgressView(value: min(balance.used, balance.limit), total: max(1, balance.limit)) + .accessibilityLabel("Daily braincells used").accessibilityValue(cells(balance.used)) + LabeledContent("Purchased", value: cells(balance.purchased)) + if let reset = balance.resetsAt.flatMap({ ISO8601DateFormatter().date(from: $0) ?? ISO8601DateFormatter.fullPrecision.date(from: $0) }) { + LabeledContent("Daily reset", value: reset.formatted(date: .omitted, time: .shortened)) + } + } else if inference.braincellsError.isEmpty { + ProgressView("Loading braincells…") + } else { + Text(inference.braincellsError).foregroundStyle(.secondary) + } + Button("Refresh") { session.command("refreshBraincells") } + } + if let usage = inference.usage { + Section(session.snapshot.busy ? "This request" : "Last request") { + LabeledContent("Input tokens", value: usage.inputTokens.formatted()) + LabeledContent("Output tokens", value: usage.outputTokens.formatted()) + LabeledContent("Inference calls", value: usage.rounds.formatted()) + LabeledContent("Repairs", value: usage.repairs.formatted()) + } + } + } + } + .navigationTitle("Brain") + .navigationBarTitleDisplayMode(.inline) + .toolbar { ToolbarItem(placement: .confirmationAction) { Button("Done") { dismiss() } } } + }.presentationDetents([.medium, .large]) + } +} + +private extension ISO8601DateFormatter { + static var fullPrecision: ISO8601DateFormatter { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter + } +} diff --git a/apple/whistlegraph/Sources/CodeSyntax.swift b/apple/whistlegraph/Sources/CodeSyntax.swift new file mode 100644 index 0000000000..da5ed77a0a --- /dev/null +++ b/apple/whistlegraph/Sources/CodeSyntax.swift @@ -0,0 +1,17 @@ +import Foundation + +/// A tolerant display lexer: unfinished strings/comments remain colored while +/// source arrives. It does not determine whether code is safe to compile. +enum CodeSyntax { + struct Token { let range: NSRange; let kind: String } + private static let expression = try! NSRegularExpression(pattern: #"(?//[^\n\r]*|/\*[\s\S]*?(?:\*/|$))|(?"(?:\\[\s\S]|[^"\\])*(?:"|$)|'(?:\\[\s\S]|[^'\\])*(?:'|$)|`(?:\\[\s\S]|[^`\\])*(?:`|$))|(?\b(?:0[xX][\da-fA-F]+|\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)\b)|(?\b(?:export|function|const|let|var|if|else|return|for|while|do|switch|case|break|continue|new|class|extends|async|await|import|from|default|try|catch|throw|finally|true|false|null|undefined|typeof|of|in)\b)|(?\b[A-Za-z_$][\w$]*(?=\s*\())"#) + static func tokens(_ source: String) -> [Token] { + expression.matches(in: source, range: NSRange(source.startIndex..., in: source)).compactMap { match in + for kind in ["comment", "string", "number", "keyword", "call"] { + let range = match.range(withName: kind) + if range.location != NSNotFound { return Token(range: range, kind: kind) } + } + return nil + } + } +} diff --git a/apple/whistlegraph/Sources/CodeTicker.swift b/apple/whistlegraph/Sources/CodeTicker.swift index 543c37499d..ecbce079b3 100644 --- a/apple/whistlegraph/Sources/CodeTicker.swift +++ b/apple/whistlegraph/Sources/CodeTicker.swift @@ -5,8 +5,29 @@ struct CodeTicker: View { /// Reasoning scrolls dimmed and plain; code is bold and full strength. var thinking = false @Environment(\.accessibilityReduceMotion) private var reduceMotion - private var line: String { - String(output.suffix(1800)).replacingOccurrences(of: "\n", with: " ").replacingOccurrences(of: "\r", with: " ") + private var line: AttributedString { + // Lex before flattening newlines so // comments end on their real line. + let source = String(output.suffix(6000)) + let styled = NSMutableAttributedString(string: source, attributes: [.foregroundColor: UIColor(red: 0.96, green: 0.94, blue: 0.87, alpha: 1)]) + if !thinking { + for token in CodeSyntax.tokens(source) { + let color: UIColor + switch token.kind { + case "keyword": color = UIColor(red: 1, green: 0.56, blue: 0.83, alpha: 1) + case "string": color = UIColor(red: 0.67, green: 0.9, blue: 0.57, alpha: 1) + case "number": color = UIColor(red: 1, green: 0.8, blue: 0.47, alpha: 1) + case "call": color = UIColor(red: 0.48, green: 0.88, blue: 0.94, alpha: 1) + default: color = UIColor(red: 0.7, green: 0.65, blue: 0.76, alpha: 1) + } + styled.addAttribute(.foregroundColor, value: color, range: token.range) + } + } + for index in stride(from: styled.length - 1, through: 0, by: -1) { + if [10, 13].contains((styled.string as NSString).character(at: index)) { + styled.replaceCharacters(in: NSRange(location: index, length: 1), with: " ") + } + } + return (try? AttributedString(styled, including: \.uiKit)) ?? AttributedString(source) } var body: some View { ScrollViewReader { proxy in diff --git a/apple/whistlegraph/Sources/WhistlegraphApp.swift b/apple/whistlegraph/Sources/WhistlegraphApp.swift index 3b676dda2a..53d6260c71 100644 --- a/apple/whistlegraph/Sources/WhistlegraphApp.swift +++ b/apple/whistlegraph/Sources/WhistlegraphApp.swift @@ -192,7 +192,7 @@ final class WhistlegraphSession: NSObject, ObservableObject, WKScriptMessageHand } func command(_ action: String, version: Int? = nil, text: String? = nil, piece: String? = nil) { - guard ["checkout", "newPiece", "openPiece", "stop", "signIn", "ask", "retry", "presentVersion", "endPresentation"].contains(action) else { return } + guard ["checkout", "newPiece", "openPiece", "stop", "signIn", "ask", "retry", "presentVersion", "endPresentation", "setModel", "refreshBraincells"].contains(action) else { return } if action == "newPiece" || action == "openPiece" { guard engineReady, !snapshot.busy, capturePhase == .idle else { return } if action == "openPiece" { guard let piece, pieces.contains(where: { $0.id == piece && !$0.current }) else { return } } @@ -200,6 +200,10 @@ final class WhistlegraphSession: NSObject, ObservableObject, WKScriptMessageHand previewSource = ""; engineReady = false } var value: [String: Any] = ["action": action] + if action == "setModel" { + guard !snapshot.busy, capturePhase == .idle, let text else { return } + value["text"] = text + } if let version { value["version"] = version } if action == "openPiece", let piece { value["piece"] = piece } if action == "ask" { diff --git a/apple/whistlegraph/Sources/WhistlegraphHeader.swift b/apple/whistlegraph/Sources/WhistlegraphHeader.swift index 4695a52d62..2ae70926ea 100644 --- a/apple/whistlegraph/Sources/WhistlegraphHeader.swift +++ b/apple/whistlegraph/Sources/WhistlegraphHeader.swift @@ -22,7 +22,7 @@ struct PieceSummary: Decodable, Identifiable { struct PiecesSheet: View { let pieces: [PieceSummary] let colors: [[Double]] - @Binding var pixelSize: Int + let inference: InferenceSnapshot? let disabled: Bool let open: (String) -> Void let newPiece: () -> Void @@ -36,15 +36,8 @@ struct PiecesSheet: View { .font(.custom("ComicRelief-Bold", size: 20, relativeTo: .title3)) }.disabled(disabled).accessibilityIdentifier("pieces-new") } - Section("Pixel size") { - Picker("Pixel size", selection: $pixelSize) { - ForEach(1...4, id: \.self) { size in - Text("\(size)×").tag(size) - } - } - .pickerStyle(.segmented) - .disabled(disabled) - .accessibilityIdentifier("pieces-pixel-size") + if let inference { + Section { LabeledContent(inference.label, value: inference.provider) } } Section(pieces.count == 1 ? "Your piece" : "Your pieces") { ForEach(pieces) { piece in @@ -164,7 +157,7 @@ struct IdentityHeader: View { } .sheet(isPresented: $showingPieces) { PiecesSheet(pieces: session.pieces, colors: session.snapshot.colors, - pixelSize: Binding(get: { session.pixelSize }, set: { session.setPixelSize($0) }), + inference: session.snapshot.inference, disabled: session.snapshot.busy || session.capturePhase != .idle, open: { session.command("openPiece", piece: $0) }, newPiece: { session.command("newPiece") }) diff --git a/apple/whistlegraph/Sources/WhistlegraphScreen.swift b/apple/whistlegraph/Sources/WhistlegraphScreen.swift index 7448abf559..c2ab8bf4a8 100644 --- a/apple/whistlegraph/Sources/WhistlegraphScreen.swift +++ b/apple/whistlegraph/Sources/WhistlegraphScreen.swift @@ -4,6 +4,7 @@ struct PieceSnapshot: Decodable { var code = "" var caption: String? var output: String? + var inference: InferenceSnapshot? var handle = "" var colors: [[Double]] = [] var head = 0 @@ -15,7 +16,7 @@ struct PieceSnapshot: Decodable { var attempt: PieceAttempt? var revisions: [PieceRevision]? = [] var versions: [PieceRevision] { get { revisions ?? [] } set { revisions = newValue } } - enum CodingKeys: String, CodingKey { case code, caption, output, handle, colors, head, hasPiece, hasPreview, error, busy, phase, attempt; case revisions = "versions" } + enum CodingKeys: String, CodingKey { case code, caption, output, inference, handle, colors, head, hasPiece, hasPreview, error, busy, phase, attempt; case revisions = "versions" } var hasHistory: Bool { versions.count > 1 } } struct PieceAttempt: Decodable { let request: String; let status: String; let error: String } @@ -146,6 +147,7 @@ struct WhistlegraphScreen: View { .accessibilityLabel("Send chalk").accessibilityIdentifier("drawing-send") .disabled(!canTalk || session.capturePhase != .idle) } + BrainButton(session: session) { showComposer = false } }.font(.title3).buttonStyle(.plain).frame(minHeight: 44) if drawing.full { Text("Chalk full · send or undo a stroke").font(.caption) } if session.capturePhase == .recording || session.capturePhase == .opening { diff --git a/apple/whistlegraph/Tests/CodeSyntaxCheck.swift b/apple/whistlegraph/Tests/CodeSyntaxCheck.swift new file mode 100644 index 0000000000..b50c4ba7a0 --- /dev/null +++ b/apple/whistlegraph/Tests/CodeSyntaxCheck.swift @@ -0,0 +1,15 @@ +import Foundation + +@main enum CodeSyntaxCheck { + static func main() { + let source = "// export 42\nconst bee = \"🐝\"; paint(12); /* still typing" + let tokens = CodeSyntax.tokens(source) + let values = tokens.map { (source as NSString).substring(with: $0.range) } + precondition(values == ["// export 42", "const", "\"🐝\"", "paint", "12", "/* still typing"]) + precondition(tokens.map(\.kind) == ["comment", "keyword", "string", "call", "number", "comment"]) + precondition(CodeSyntax.tokens("const color = \"pi").last?.kind == "string") + precondition(CodeSyntax.tokens("const s = `hello ${name}").last?.kind == "string") + precondition(CodeSyntax.tokens("").isEmpty) + print("PASS source highlighting: Unicode, comments, calls, numbers, and unfinished strings.") + } +} diff --git a/apple/whistlegraph/Tests/build98-checks.json b/apple/whistlegraph/Tests/build98-checks.json new file mode 100644 index 0000000000..db65fd2b43 --- /dev/null +++ b/apple/whistlegraph/Tests/build98-checks.json @@ -0,0 +1,14 @@ +{ + "build": 98, + "date": "2026-10-03", + "deviceBuild": "Signed iOS build passed and installed on the paired iPhone.", + "javascriptTests": {"passed": 50, "command": "node --test apple/whistlegraph/Tests/*.test.mjs"}, + "browserTests": ["--native-shell --streaming", "--native-shell --streaming --jeffrey", "--native-shell --checked-edits", "--native-shell --checked-edits --repair-fails"], + "browserScope": "Mock inference and paint events verified streamed paint statements, incomplete-helper recovery, exact replacement previews, final review, verified-account model selection, persisted model settings, allowance display, single repair and rollback. No browser client errors in streaming checks.", + "nativeLexer": "CodeSyntax.swift and CodeSyntaxCheck.swift compiled with swiftc; Unicode, comments, calls, numbers and unfinished strings passed.", + "liveInference": "Authenticated OpenRouter-backed Opus 5 request returned HTTP 200, the requested model, OK, and end_turn with the demo token/thinking configuration. This small probe does not establish generated-art quality or latency.", + "allowance": "Authenticated easel-credits endpoint returned valid daily and purchased balances. Daily billing limits were not changed.", + "wwTenef": "The failed attempt stopped after four tool rounds and restored v0; reported glyph-cache messages were warnings. Later cloud state showed saved v1 before installation.", + "nativeUI": "Brain sheet and syntax colors compiled but were not visually exercised on the locked physical phone. Header XCTest navigation was updated but not run.", + "remoteAgents": "No remote Claude/Codex subscription bridge was connected. This build uses hosted inference." +} diff --git a/apple/whistlegraph/Tests/generation-policy.test.mjs b/apple/whistlegraph/Tests/generation-policy.test.mjs new file mode 100644 index 0000000000..c4531a7aa2 --- /dev/null +++ b/apple/whistlegraph/Tests/generation-policy.test.mjs @@ -0,0 +1,17 @@ +import assert from 'node:assert/strict'; +import {generationProfile,DEFAULT_MODEL,REPAIR_MODEL} from '../Resources/Web/generation-policy.mjs'; +for(const handle of ['', 'fixture', 'Jeffrey', 'jeffrey-other']) { + assert.equal(generationProfile(handle).model,DEFAULT_MODEL); + assert.equal(generationProfile(handle).rounds,4); + assert.equal(generationProfile(handle).maxTokens,4096); + assert.equal(generationProfile(handle,{repair:true}).model,REPAIR_MODEL); +} +assert.equal(generationProfile('jeffrey').model,'anthropic/claude-opus-5'); +assert.equal(generationProfile('jeffrey').rounds,12); +assert.equal(generationProfile('jeffrey').maxTokens,16384); +assert.equal(generationProfile('jeffrey',{repair:true}).rounds,4); +console.log('PASS personal demo profile; other handles retain existing limits.'); + +assert.equal(generationProfile('fixture',{model:'anthropic/claude-opus-5'}).model,DEFAULT_MODEL); +assert.equal(generationProfile('jeffrey',{model:DEFAULT_MODEL}).model,DEFAULT_MODEL); +assert.equal(generationProfile('jeffrey',{model:DEFAULT_MODEL,repair:true}).model,REPAIR_MODEL); diff --git a/apple/whistlegraph/Tests/native-bridge.test.cjs b/apple/whistlegraph/Tests/native-bridge.test.cjs index a8b4c41b7b..90bab66c30 100644 --- a/apple/whistlegraph/Tests/native-bridge.test.cjs +++ b/apple/whistlegraph/Tests/native-bridge.test.cjs @@ -21,7 +21,23 @@ const server = http.createServer(async (req, res) => { send({type:'content_block_delta',delta:{type:'text_delta',text:JSON.stringify({passed:!failed,observations:'Fixture-only visual evidence; not a physical capture.',findings:failed?['Requested subject is missing.']:[]})}}); send({type:'message_delta',delta:{stop_reason:'end_turn'}});res.end();return; } - if(process.argv.includes('--visual-fails')) { + if(process.argv.includes('--streaming')) { + const tool=requests===1?'write_piece':'edit_piece'; + const source='export function paint({wipe}) {wipe("pink"); helper();}\nfunction helper(){return 1;}'; + const revision=JSON.stringify(parsed.messages).match(/Current revision: ([^.]+)\./)?.[1]; + const body=JSON.stringify(requests===1?{source}:{revision,edits:[{search:'"pink"',replace:'"navy"'},{search:'return 1;',replace:'return 2;'}]}); + send({type:'content_block_start',index:0,content_block:{type:'tool_use',id:'stream-'+requests,name:tool}}); + const cut=requests===1?body.indexOf(' helper();'):body.indexOf('},{')+1; + send({type:'content_block_delta',index:0,delta:{type:'input_json_delta',partial_json:body.slice(0,cut)}}); + await pause(400); + if(requests===1){ + const more=body.indexOf('return 1;'); + send({type:'content_block_delta',index:0,delta:{type:'input_json_delta',partial_json:body.slice(cut,more)}}); + await pause(400); + send({type:'content_block_delta',index:0,delta:{type:'input_json_delta',partial_json:body.slice(more)}}); + }else send({type:'content_block_delta',index:0,delta:{type:'input_json_delta',partial_json:body.slice(cut)}}); + send({type:'content_block_stop',index:0});send({type:'message_delta',delta:{stop_reason:requests===1?'tool_use':'end_turn'}}); + } else if(process.argv.includes('--visual-fails')) { send({type:'content_block_start',index:0,content_block:{type:'tool_use',id:'visual-candidate-'+requests,name:'write_piece'}}); send({type:'content_block_delta',index:0,delta:{type:'input_json_delta',partial_json:JSON.stringify({source:'export function paint({wipe}) {wipe('+requests+');}'})}}); send({type:'content_block_stop',index:0});send({type:'message_delta',delta:{stop_reason:'end_turn'}}); @@ -74,19 +90,55 @@ const server = http.createServer(async (req, res) => { await page.setViewport({width:430,height:850}); await page.setRequestInterception(true);page.on('request',r=>r.isNavigationRequest()&&r.url().startsWith('https://aesthetic.computer/')?r.respond({status:200,contentType:'text/html',body:'Preview fixture'}):r.continue()); const guideSeed=Object.fromEntries(await Promise.all(['pieces.md','screen.md','hand.md','kidlisp.md','api.json'].map(async name=>['/easel/context/'+name,await readFile(resolve(root,'easel/context',name),'utf8')]))); - await page.evaluateOnNewDocument((seed,nativeShell)=>{ + await page.evaluateOnNewDocument((seed,nativeShell,personal)=>{ window.__walkiewareNativeShell=nativeShell; window.__aeselGuides=seed; window.__walkiewareDisableThread=true; window.__guideFetches=0; const fetchOriginal=window.fetch.bind(window); - window.fetch=(url,init)=>{if(String(url).includes('/api/handle-colors'))return Promise.resolve(Response.json({colors:[{r:200,g:100,b:255}]}));if(String(url).includes('/userinfo'))return Promise.resolve(Response.json({sub:'fixture-user'}));if(String(url).includes('/handle?for='))return Promise.resolve(Response.json({handle:'fixture'}));if(String(url).includes('/api/easel-musical-jev')){const b=JSON.parse(init.body);return Promise.resolve(Response.json({schema:'walkieware-decision/v1',sessionId:b.sessionId,sequence:b.sequence,choice:'follow_speech',confidence:.95}));}if(String(url).startsWith('/easel/context/')){window.__guideFetches++;return Promise.resolve({ok:false,status:0});}return fetchOriginal(String(url).includes('/api/easel-inference')?'/mock-inference':url,init);}; + window.fetch=(url,init)=>{if(String(url).includes('/api/easel-credits'))return Promise.resolve(Response.json({remaining:180000,used:20000,limit:200000,purchased:50000}));if(String(url).includes('/api/handle-colors'))return Promise.resolve(Response.json({colors:[{r:200,g:100,b:255}]}));if(String(url).includes('/userinfo'))return Promise.resolve(Response.json({sub:'fixture-user'}));if(String(url).includes('/handle?for='))return Promise.resolve(Response.json({handle:personal?'jeffrey':'fixture'}));if(String(url).includes('/api/easel-musical-jev')){const b=JSON.parse(init.body);return Promise.resolve(Response.json({schema:'walkieware-decision/v1',sessionId:b.sessionId,sequence:b.sequence,choice:'follow_speech',confidence:.95}));}if(String(url).startsWith('/easel/context/')){window.__guideFetches++;return Promise.resolve({ok:false,status:0});}return fetchOriginal(String(url).includes('/api/easel-inference')?'/mock-inference':url,init);}; window.__nativeMessages=[]; window.webkit={messageHandlers:{walkie:{postMessage:m=>{window.__nativeMessages.push(m);if(m.action==='visualCapture'){const canvas=document.createElement('canvas');canvas.width=32;canvas.height=24;canvas.getContext('2d').fillRect(0,0,32,24);setTimeout(()=>window.walkiewareEngineEvent({kind:'visualCapture',captureID:m.captureID,sourceHash:window.__staleVisual?'old':m.sourceHash,renderID:m.renderID,frames:[0,800,1600,2400].map(atMs=>({atMs,width:32,height:24,png:canvas.toDataURL('image/png').split(',')[1]}))}),10);}if(m.action==='render')setTimeout(async()=>{const digest=await crypto.subtle.digest('SHA-256',new TextEncoder().encode(m.source));const sourceHash=[...new Uint8Array(digest)].map(b=>b.toString(16).padStart(2,'0')).join('');window.walkiewareEngineEvent({kind:'previewEvent',event:{kind:'painted',sourceHash,requestID:m.renderID}});},20);}}}}; - },guideSeed,process.argv.includes('--native-shell')); + },guideSeed,process.argv.includes('--native-shell'),process.argv.includes('--jeffrey')); await page.goto('http://127.0.0.1:'+server.address().port+'/index.html?walkie=1'); await page.waitForFunction(()=>typeof window.walkiewareAsk==='function'); assert.deepEqual(errors,[]); + if(process.argv.includes('--streaming')) { + await page.evaluate(()=>{ + const post=window.webkit.messageHandlers.walkie.postMessage; + window.webkit.messageHandlers.walkie.postMessage=m=>{ + // A later helper has not arrived: simulate a real provisional runtime + // failure. It must not consume the one repair or poison the final code. + if(m.action==='render'&&m.source.includes('helper();')&&!m.source.includes('function helper')){ + window.__nativeMessages.push(m); + setTimeout(async()=>{const d=await crypto.subtle.digest('SHA-256',new TextEncoder().encode(m.source));const sourceHash=[...new Uint8Array(d)].map(v=>v.toString(16).padStart(2,'0')).join('');walkiewareEngineEvent({kind:'previewEvent',event:{kind:'console',sourceHash,requestID:m.renderID,event:{level:'error',message:'Paint failure: helper is not defined'}}});},20); + }else post(m); + }; + walkiewareEngineEvent({kind:'account',token:'fixture-only'});walkiewareEngineEvent({kind:'previewReady'}); + }); + await page.evaluate(()=>walkiewareAsk('Draw a clover with a bee')); + const state=await page.evaluate(()=>({ledger:JSON.parse(localStorage.getItem('walkieware-source-versions')),messages:__nativeMessages,phase:document.getElementById('live-phase').textContent})); + assert.equal(state.ledger.head,1,JSON.stringify(state)); + assert.equal(inferenceBodies[0].model,process.argv.includes('--jeffrey')?'anthropic/claude-opus-5':'deepseek/deepseek-v4.1-flash'); + assert.equal(inferenceBodies[0].max_tokens,process.argv.includes('--jeffrey')?16384:4096); + assert.equal(requests,2,'provisional failures do not buy repair inference'); + assert.equal(visualRequests,1,'final candidate is still visually checked'); + const renders=state.messages.filter(m=>m.action==='render').map(m=>m.source); + assert.ok(renders.includes('export function paint({wipe}) {wipe("pink");\n}'),'first statement runs before paint finishes streaming'); + assert.ok(renders.some(s=>s.includes('"navy"')&&s.includes('return 1;')),'first exact edit runs before the second edit arrives'); + assert.match(state.ledger.versions.at(-1).source,/return 2/); + assert.ok(!state.ledger.versions.at(-1).source.includes('\n}'),'provisional closing brace is never committed'); + await page.waitForFunction(()=>__nativeMessages.some(m=>m.action==='snapshot'&&m.snapshot.inference?.braincells)); + const settings=await page.evaluate(()=>__nativeMessages.filter(m=>m.action==='snapshot').at(-1).snapshot.inference); + assert.equal(settings.provider,'OpenRouter');assert.equal(settings.braincells.remaining,180000); + await page.evaluate(()=>walkiewareNativeCommand({action:'setModel',text:'deepseek/deepseek-v4-pro'})); + await page.waitForFunction(()=>__nativeMessages.filter(m=>m.action==='snapshot').at(-1).snapshot.inference.selection==='deepseek/deepseek-v4-pro'); + await page.reload();await page.waitForFunction(()=>typeof walkiewareAsk==='function'); + await page.evaluate(()=>walkiewareEngineEvent({kind:'account',token:'fixture-only'})); + await page.waitForFunction(()=>__nativeMessages.some(m=>m.action==='snapshot'&&m.snapshot.inference?.selection==='deepseek/deepseek-v4-pro')); + assert.deepEqual(errors,[]); + console.log('PASS streaming: early paint statements, missing-helper recovery, exact edits before tool completion, and mandatory final review. Mock provider and paint events.');return; + } if(process.argv.includes('--visual-fails')||process.argv.includes('--visual-stale')) { await page.evaluate(stale=>{window.__staleVisual=stale;walkiewareEngineEvent({kind:'account',token:'fixture-only'});walkiewareEngineEvent({kind:'previewReady'});},process.argv.includes('--visual-stale')); const before=await page.evaluate(()=>JSON.parse(localStorage.getItem('walkieware-source-versions'))); diff --git a/apple/whistlegraph/Tests/stream-preview.test.mjs b/apple/whistlegraph/Tests/stream-preview.test.mjs index e36194701e..500594b967 100644 --- a/apple/whistlegraph/Tests/stream-preview.test.mjs +++ b/apple/whistlegraph/Tests/stream-preview.test.mjs @@ -1,11 +1,26 @@ import assert from 'node:assert/strict'; -import {runnablePrefix} from '../Resources/Web/stream-preview.mjs'; +import {runnablePrefix,partialString,streamedEdits,streamedCode} from '../Resources/Web/stream-preview.mjs'; const paint='export function paint({wipe}) {wipe("pink");}'; assert.equal(runnablePrefix(paint+'\nexport function act({event}) {'),paint); -assert.equal(runnablePrefix('export function paint({wipe}) {wipe("pink");'),null); +assert.equal(runnablePrefix('export function paint({wipe}) {wipe("pink");'), 'export function paint({wipe}) {wipe("pink");\n}'); assert.equal(runnablePrefix('const text="export function paint() {}";'),null); assert.equal(runnablePrefix(paint+'\nconst x="unfinished'),paint); assert.equal(runnablePrefix('export function boot() {}'),null); assert.equal(runnablePrefix(paint),paint); -assert.equal(runnablePrefix('export function paint(){ const x="}";'),null); -console.log('PASS incremental compiler: complete paint prefixes; unfinished functions and strings rejected; no fabricated syntax.'); +assert.equal(runnablePrefix('export function paint(){ const x="}";'), 'export function paint(){ const x="}";\n}'); +assert.equal(runnablePrefix('export function paint({wipe}) {wipe("pink"); ink('), 'export function paint({wipe}) {wipe("pink");\n}'); +assert.equal(runnablePrefix('export function paint({wipe}) {wipe("pink"); if (true) { circle('), 'export function paint({wipe}) {wipe("pink");\n}'); +assert.equal(runnablePrefix('export function paint(){ const x="; }'), null); +assert.equal(runnablePrefix('export function paint(){ // wipe(0);'), null); +assert.equal(runnablePrefix('export function paint(){ let x ='), null); +assert.deepEqual(partialString('{"source":"ink(\\"pink\\");\\n'),{value:'ink("pink");\n',complete:false}); +const edits={revision:'r1',edits:[{search:'pink',replace:'blue'},{search:'export function',replace:'export async function'}]}; +const json=JSON.stringify(edits), first=json.indexOf('},{')+1; +assert.equal(streamedEdits(json.slice(0,first),paint,'r1'),paint.replace('pink','blue')); +assert.equal(streamedEdits(json.slice(0,first-2),paint,'r1'),null); +assert.equal(streamedEdits(json,paint,'stale'),null); +assert.equal(streamedEdits(json,paint,'r1'),paint.replace('pink','blue').replace('export function','export async function')); +assert.equal(streamedEdits(JSON.stringify({revision:'r1',edits:[{search:'no match',replace:''}]}),paint,'r1'),null); +console.log('PASS live paint statements and complete revision-checked edits; incomplete expressions, replacements and stale edits are withheld.'); + +assert.equal(streamedCode(json,'edit_piece'),'blue\nexport async function'); diff --git a/apple/whistlegraph/UITests/HeaderSheetsTests.swift b/apple/whistlegraph/UITests/HeaderSheetsTests.swift index ae842ec613..a280a52481 100644 --- a/apple/whistlegraph/UITests/HeaderSheetsTests.swift +++ b/apple/whistlegraph/UITests/HeaderSheetsTests.swift @@ -24,6 +24,7 @@ final class HeaderSheetsTests: XCTestCase { code.tap() XCTAssertTrue(app.buttons["pieces-new"].waitForExistence(timeout: 10), "the pieces sheet offers New piece") XCTAssertTrue(app.buttons["piece-wwDemo"].exists, "the open piece is listed by its code") + XCTAssertFalse(app.segmentedControls["brain-pixel-size"].exists, "pixel size belongs to Brain settings") app.buttons["Done"].tap() XCTAssertTrue(waitForDisappearance(of: app.buttons["pieces-new"]), "Done closes the pieces sheet") } @@ -41,17 +42,17 @@ final class HeaderSheetsTests: XCTestCase { func testPixelSizePersistsAcrossLaunches() { let app = launch() - let code = app.buttons["workspace-settings"] + let code = app.buttons["brain-settings"] XCTAssertTrue(code.waitForExistence(timeout: 20)) code.tap() - let picker = app.segmentedControls["pieces-pixel-size"] + let picker = app.segmentedControls["brain-pixel-size"] XCTAssertTrue(picker.waitForExistence(timeout: 10)) for size in [1, 2, 3, 4] { picker.buttons["\(size)×"].tap() XCTAssertTrue(picker.buttons["\(size)×"].isSelected) } let image = XCTAttachment(screenshot: app.screenshot()) - image.name = "Pixel size in the piece menu"; image.lifetime = .keepAlways; add(image) + image.name = "Pixel size in Brain settings"; image.lifetime = .keepAlways; add(image) app.terminate() app.launch() XCTAssertTrue(code.waitForExistence(timeout: 20)) diff --git a/apple/whistlegraph/Whistlegraph.xcodeproj/project.pbxproj b/apple/whistlegraph/Whistlegraph.xcodeproj/project.pbxproj index 2313d79ca9..20398bc44e 100644 --- a/apple/whistlegraph/Whistlegraph.xcodeproj/project.pbxproj +++ b/apple/whistlegraph/Whistlegraph.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 0300D7A47360F4B79AB5B31F /* SwipeToTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 212791F2E33DED2E1C368651 /* SwipeToTypeTests.swift */; }; 03C5B25FC742430AFD02AD1E /* StoryMovieSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 250E0B1999C7810299ACB7A9 /* StoryMovieSheet.swift */; }; + 0438F503606B4A37262B7CCC /* BrainSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E61EF96840670F2F894F970 /* BrainSettings.swift */; }; 10C0A16D4803EC60EAC29531 /* RecordingTrim.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC1EFF8BAE7678E320E3DCA0 /* RecordingTrim.swift */; }; 123228FADA6696AB354306C7 /* InputButtonLabels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35643029A3E82EEDEAC8DED6 /* InputButtonLabels.swift */; }; 13604BE92157CDBFDD32AA19 /* WorkspaceCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 147EAC245AD250B9B652F903 /* WorkspaceCoordinator.swift */; }; @@ -26,6 +27,7 @@ 4B35B066F49BDD9BD8B90B5A /* WhistlegraphScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = E66B3ADC4D6D837FABC53B8F /* WhistlegraphScreen.swift */; }; 5646BD41AA620CC47BF89361 /* Web in Resources */ = {isa = PBXBuildFile; fileRef = 4BA75145769FC4401A11BBE8 /* Web */; }; 603B85FC4E28D17FF1E5F457 /* StoryCardStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 477B640525BC627AFB40D7A7 /* StoryCardStyle.swift */; }; + 620B74996F82DC86FA07B6DB /* CodeSyntax.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C2AB1140648295BB7905317 /* CodeSyntax.swift */; }; 73873BDDC483417E2F6E1143 /* WhistlegraphHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BC2C1DC5E036F689EA59561 /* WhistlegraphHeader.swift */; }; 749AC25AE02AAE4BCDA5683A /* VisualCapture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF0A171A5771A7A1FD56852 /* VisualCapture.swift */; }; 77291EEE768914D5F5EDB84B /* PlaybackWord.swift in Sources */ = {isa = PBXBuildFile; fileRef = E97104E13862EE30444D07C9 /* PlaybackWord.swift */; }; @@ -75,12 +77,14 @@ 4BA75145769FC4401A11BBE8 /* Web */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Web; path = Resources/Web; sourceTree = SOURCE_ROOT; }; 4BBA0E45689010CB41A6A8FF /* NetworkBenchmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkBenchmark.swift; sourceTree = ""; }; 4D3B156A3435E646EE0E6EEA /* PlaybackCaption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackCaption.swift; sourceTree = ""; }; + 4E61EF96840670F2F894F970 /* BrainSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrainSettings.swift; sourceTree = ""; }; 5ABFC6C11146A1540B635EE6 /* ButtonSounds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonSounds.swift; sourceTree = ""; }; 5B20A9A8612C57068C67239A /* NativeScreenFixture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeScreenFixture.swift; sourceTree = ""; }; 5D233DBB6F87184A91C13D41 /* MusicalInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicalInput.swift; sourceTree = ""; }; 5DF0A171A5771A7A1FD56852 /* VisualCapture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualCapture.swift; sourceTree = ""; }; 6D137824491A01D4C19417B8 /* VersionNarrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionNarrator.swift; sourceTree = ""; }; 88B494AD8CD86FCC5DE0E4CC /* StoryExport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryExport.swift; sourceTree = ""; }; + 8C2AB1140648295BB7905317 /* CodeSyntax.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodeSyntax.swift; sourceTree = ""; }; 8C42750EF4F7E96146B03585 /* StoryControls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryControls.swift; sourceTree = ""; }; 8F9B92C3457F07429C8C2600 /* UtteranceRecording.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtteranceRecording.swift; sourceTree = ""; }; 99D9A8C801D3B9D1192D1143 /* WhistlegraphDrawing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhistlegraphDrawing.swift; sourceTree = ""; }; @@ -126,7 +130,9 @@ isa = PBXGroup; children = ( 1CC14C186FE1CB5B19AD1D72 /* AudioBenchmark.swift */, + 4E61EF96840670F2F894F970 /* BrainSettings.swift */, 5ABFC6C11146A1540B635EE6 /* ButtonSounds.swift */, + 8C2AB1140648295BB7905317 /* CodeSyntax.swift */, 10E2AAA0A3F2BA1A5AEBED58 /* CodeTicker.swift */, 35643029A3E82EEDEAC8DED6 /* InputButtonLabels.swift */, 5D233DBB6F87184A91C13D41 /* MusicalInput.swift */, @@ -286,7 +292,9 @@ buildActionMask = 2147483647; files = ( 2F15D5A580A90A18E92D90F1 /* AudioBenchmark.swift in Sources */, + 0438F503606B4A37262B7CCC /* BrainSettings.swift in Sources */, 2F01E350A7FF55359B002344 /* ButtonSounds.swift in Sources */, + 620B74996F82DC86FA07B6DB /* CodeSyntax.swift in Sources */, 9DBA1AF2AD7E3EDF69C8132A /* CodeTicker.swift in Sources */, 123228FADA6696AB354306C7 /* InputButtonLabels.swift in Sources */, 14FFBE48767E2576693FB7B3 /* MusicalInput.swift in Sources */, @@ -428,7 +436,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 97; + CURRENT_PROJECT_VERSION = 98; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = FB5948YR3S; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -513,7 +521,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 97; + CURRENT_PROJECT_VERSION = 98; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = FB5948YR3S; ENABLE_NS_ASSERTIONS = NO; diff --git a/apple/whistlegraph/project.yml b/apple/whistlegraph/project.yml index c61b40e9a7..04301ed594 100644 --- a/apple/whistlegraph/project.yml +++ b/apple/whistlegraph/project.yml @@ -9,7 +9,7 @@ settings: CODE_SIGN_STYLE: Automatic SWIFT_VERSION: "5.0" MARKETING_VERSION: "0.1.0" - CURRENT_PROJECT_VERSION: "97" + CURRENT_PROJECT_VERSION: "98" targets: Whistlegraph: type: application