{"record":{"id":"14b7f9dd7221ae29","repo":"Hmbown/CodeWhale","slug":"choose-an-appearance-file-smaller-than-4-kib","errorCode":null,"errorMessage":"Choose an appearance file smaller than 4 KiB.","messagePattern":"Choose an appearance file smaller than 4 KiB\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/tui/pet_watch/shared.html","lineNumber":56,"sourceCode":"function syncControls(){for(const k of ['background','backgroundTop','particle'])$(k).value=hex(appearance[k]);for(const k of ['eventColors','environment'])$(k).checked=appearance[k];for(const k of ['brightness','dotScale','glow']){$(k).value=appearance[k];$(k+'-value').value=k==='glow'?Math.round(appearance[k]*100)+'%':appearance[k].toFixed(2)+'×'}for(const b of $('presets').children)b.setAttribute('aria-pressed',String(b.textContent===selected));}\nfunction saveAppearance(){try{localStorage.setItem('codewhale-pet-appearance-v1',JSON.stringify(appearance))}catch{}if(preview){$('message').textContent='Appearance saved in this preview. Export it to keep a portable copy.'}else{nextAppearance=clean(appearance);$('message').textContent='Saving appearance to the companion…';void flushAction()}}\nfor(const a of presets){const b=document.createElement('button');b.type='button';b.setAttribute('aria-pressed','false');const sw=document.createElement('span');sw.className='swatch';sw.style.background=hex(a.background);const dot=document.createElement('i');dot.style.background=hex(a.particle);sw.append(dot);b.append(sw,document.createTextNode(a.name));b.onclick=()=>{appearance=clean(a);selected=a.name;syncControls();saveAppearance()};$('presets').append(b);const f=document.createElement('figure'),c=document.createElement('canvas'),label=document.createElement('figcaption');c.setAttribute('role','img');c.setAttribute('aria-label',a.name+' appearance');label.textContent=a.name;label.style.color=readable(a.background);f.append(c,label);$('gallery').append(f)}\nfor(const key of Object.keys(base))$(key).oninput=()=>{appearance[key]=['background','backgroundTop','particle'].includes(key)?rgb($(key).value):['eventColors','environment'].includes(key)?$(key).checked:Number($(key).value);if(key==='particle')appearance.eventColors=false;selected='Custom';syncControls();saveAppearance()};\nsyncControls();\nasync function request(path,body,headers={}){const r=await fetch(path,{method:body===undefined?'GET':'POST',headers:{'Content-Type':'application/json',...headers},body:body===undefined?undefined:JSON.stringify(body),signal:AbortSignal.timeout(2500)});if(!r.ok){let e;try{e=await r.json()}catch{}const error=Error(e?.error||'Local pet unavailable. Reopen from Codewhale or use the preview.');error.rejected=r.status===409&&!error.message.includes('storage');throw error}return r.json()}\nasync function flushAction(){if(busy||!current||preview)return;if(!pending&&nextAppearance){pending={identity:current.identity,client,seq:seq+1,source_revision:current.sourceRevision,action:{kind:'appearance',appearance:nextAppearance}};nextAppearance=null}if(!pending)return;busy=true;try{await request('/v1/action',pending);seq=pending.seq;pending=null;$('message').textContent='Appearance and interactions saved by the shared companion.'}catch(e){if(e.rejected)pending=null;$('message').textContent=e.message}finally{busy=false}}\nasync function poll(){if(preview)return;try{const frame=await request('/v1/frame');if(frame.version!==1||!Array.isArray(frame.points)||frame.points.length!==980)throw Error('Unsupported pet snapshot');if(current?.epoch!==frame.epoch){previous=null;lastTickAt=performance.now()}else if(frame.tick!==current.tick){previous=current;lastTickAt=performance.now()}current=frame;received=performance.now();if(!nextAppearance&&!pending&&valid(frame.appearance)&&JSON.stringify(appearance)!==JSON.stringify(frame.appearance)){appearance=clean(frame.appearance);selected=presetName(appearance);syncControls()}$('identity').textContent='Pet '+frame.identity+' · '+frame.source+' · tick '+frame.tick+' · '+frame.digest;if(frame.audioUnavailable&&sound){sound=false;$('sound').textContent='Sound unavailable';$('sound').setAttribute('aria-pressed','false')}if(pending||nextAppearance)await flushAction()}catch(e){$('message').textContent=e.message}finally{if(!preview)setTimeout(poll,33)}}\nfunction interact(food){if(preview){pet?.interact(food?'food':'attention',.2,-.15);return}if(!current||performance.now()-lastTickAt>800||pending)return;pending={identity:current.identity,client,seq:seq+1,source_revision:current.sourceRevision,action:{kind:'interact',food,x:.2,y:-.15}};void flushAction()}\n$('focus').onclick=()=>interact(false);$('pulse').onclick=()=>interact(true);canvas.onpointerdown=()=>interact(false);\n$('sound').onclick=async()=>{if(preview)return;try{const r=await request('/v1/audio',{client,enabled:!sound});sound=r.granted;$('sound').textContent=sound?'Sound on':'Sound off';$('sound').setAttribute('aria-pressed',String(sound))}catch(e){$('message').textContent=e.message}};\nsetInterval(()=>{if(sound&&!document.hidden&&performance.now()-lastTickAt<500)request('/v1/audio',{client,enabled:true}).catch(()=>{sound=false})},500);\ndocument.addEventListener('visibilitychange',()=>{if(document.hidden&&sound){sound=false;void request('/v1/audio',{client,enabled:false});$('sound').textContent='Sound off';$('sound').setAttribute('aria-pressed','false')}lastDraw=0;accumulator=0});\n$('compare').onclick=()=>{document.body.classList.toggle('gallery-mode');$('compare').setAttribute('aria-pressed',String(document.body.classList.contains('gallery-mode')))};\n$('configure').onclick=()=>{document.body.classList.toggle('no-settings');$('configure').setAttribute('aria-pressed',String(!document.body.classList.contains('no-settings')))};\nfunction immersive(on){document.body.classList.toggle('immersive',on);$('expand').textContent=on?'Back to studio':'Full habitat';if(on)document.body.classList.remove('gallery-mode')}\n$('expand').onclick=()=>immersive(!document.body.classList.contains('immersive'));window.addEventListener('keydown',e=>{if(e.key==='Escape'){immersive(false);document.body.classList.remove('gallery-mode');$('compare').setAttribute('aria-pressed','false')}});\nfunction download(name,data){const u=URL.createObjectURL(new Blob([JSON.stringify(data,null,2)],{type:'application/json'})),a=document.createElement('a');a.href=u;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(u),1000)}\n$('export-config').onclick=()=>download('codewhale-appearance.json',{version:1,appearance:clean(appearance)});$('import-config').onclick=()=>$('config-file').click();$('config-file').onchange=async()=>{try{const f=$('config-file').files[0];if(!f)return;if(f.size>4096)throw Error('Choose an appearance file smaller than 4 KiB.');const value=JSON.parse(await f.text());if(value.version!==1)throw Error('Unsupported appearance version.');appearance=clean(value.appearance);selected='Custom';syncControls();saveAppearance()}catch(e){$('message').textContent=e.message}finally{$('config-file').value=''}};\n$('save').onclick=async()=>{try{download(preview?'codewhale-preview-replay.json':'codewhale-shared-pet.json',preview?JSON.parse(pet.recording(true)):await request('/v1/export'))}catch(e){$('message').textContent=e.message}};\nasync function makePreview(){if(!points){const r=await fetch('whale-points.tsv');if(!r.ok)throw Error('Preview points could not load.');points=JSON.stringify((await r.text()).trim().split('\\n').map(row=>row.trim().split(/\\s+/).map(Number)))}if(!preview){previewSaved=true;if(sound){sound=false;void request('/v1/audio',{client,enabled:false})}}preview=true;$('return-live').hidden=!previewSaved;pet=new PetNative(points,'','[]',true);demoTick=0;actionTick=0;previous=null;current=null;$('connection-label').textContent='Isolated preview · no task running';$('sound').disabled=true;$('sound').textContent='Preview is silent';$('identity').textContent='Preview world · canonical 980-particle core · no live session';$('message').textContent='This preview is isolated. Its colors can be exported; it does not change a live pet.'}\nfunction showResult(on){const el=$('result');if(!on&&el.contains(document.activeElement))$('work').focus();el.inert=!on;el.classList.toggle('visible',on);el.setAttribute('aria-hidden',String(!on));if(on&&document.activeElement===$('finish'))el.focus()}\nasync function startWork(){try{await makePreview();actionChoice='thinking';demoRunning=true;demoDone=false;showResult(false);document.body.classList.add('preview-running');immersive(true)}catch(e){$('message').textContent=e.message}}\nfunction finishWork(){if(!pet)return;demoRunning=false;demoDone=true;pet.observeEngine(JSON.stringify({event:'turn_complete'}),demoTick*1000/30);showResult(true);document.body.classList.remove('preview-running')}\n$('return-live').onclick=()=>{preview=false;pet=null;demoDone=false;demoRunning=false;showResult(false);$('return-live').hidden=true;$('sound').disabled=false;$('sound').textContent='Sound off';$('connection-label').textContent='Shared habitat';document.body.classList.remove('preview-running');void poll()};\n$('work').onclick=startWork;$('replay').onclick=startWork;$('finish').onclick=finishWork;\nconst actionTools={reading:'read_file',searching:'search_files',editing:'apply_patch',executing:'exec_command',testing:'run_tests',browsing:'browser_navigate',memory:'retrieve_context'};\nconst workPhases=['thinking','reading','searching','browsing','memory','delegating','editing','executing','testing','waiting','responding'];\nfunction beginAction(kind,at){pet.observeEngine(JSON.stringify({event:'turn_started'}),at);const emit=e=>pet.observeEngine(JSON.stringify(e),at);if(actionTools[kind])emit({event:'tool_call_started',tool_call_id:'preview-tool',tool_name:actionTools[kind]});else if(kind==='waiting')emit({event:'user_input_required',id:'preview-question'});else if(kind==='delegating'){for(let i=0;i<3;i++)emit({event:'agent_spawned',id:'preview-worker-'+i})}else if(kind==='thinking'||kind==='responding')emit({event:kind==='thinking'?'thinking_started':'message_started',index:0});else if(kind==='error')emit({event:'error'});actionTick=0}\n$('action-preview').onchange=async()=>{try{if(!preview)await makePreview();demoRunning=false;demoDone=false;actionChoice=$('action-preview').value;beginAction(actionChoice,demoTick*1000/30);showResult(false);document.body.classList.remove('preview-running')}catch(e){$('message').textContent=e.message}};\nfunction advancePreview(){const at=demoTick*1000/30;if(!demoDone){if(demoRunning){const phase=Math.min(workPhases.length-1,Math.floor(demoTick/120));if(demoTick%120===0){actionChoice=workPhases[phase];$('action-preview').value=actionChoice;beginAction(actionChoice,at)}}else if(demoTick===0)beginAction(actionChoice,at);if(actionTick%6===0){const emit=e=>pet.observeEngine(JSON.stringify(e),at);if(actionTools[actionChoice])emit({event:'tool_call_heartbeat'});else if(actionChoice==='delegating'){for(let i=0;i<3;i++)emit({event:'agent_progress',id:'preview-worker-'+i,worker_status:'running'})}else if(actionChoice==='thinking'||actionChoice==='responding')emit({event:'response_delta',index:0,channel:actionChoice==='thinking'?'reasoning':'text'});else if(actionChoice==='error')emit({event:'error'})}}actionTick++;pet.advanceEngine(++demoTick*1000/30,true,!demoDone&&actionChoice==='waiting');if(demoRunning&&demoTick>=120*workPhases.length)finishWork();previous=current;current=JSON.parse(pet.presentation());lastTickAt=received=performance.now();current.producerConnected=!demoDone&&actionChoice!=='unknown';current.epoch='preview';}\n// View-only marks use the owner's fixed tick. They consume no random stream,\n// claim no results, and freeze under Still. The moving body remains canonical.\nfunction actionMarks(ctx,kind,t,w,h,ink,parallel){const cx=w*.5,cy=h*.55,r=Math.min(w*.29,h*.26),phase=t*.8;ctx.save();ctx.strokeStyle=ctx.fillStyle=ink;ctx.lineWidth=1;ctx.globalAlpha=.24;const line=(x1,y1,x2,y2)=>{ctx.beginPath();ctx.moveTo(x1,y1);ctx.lineTo(x2,y2);ctx.stroke()},circle=(x,y,r)=>{ctx.beginPath();ctx.arc(x,y,r,0,Math.PI*2);ctx.stroke()};\nif(['reading','memory','files'].includes(kind)){for(const side of [-1,1]){const x=cx+side*r*1.35;for(let j=0;j<6;j++){const y=cy-42+j*15;ctx.globalAlpha=.1+.16*(.5+.5*Math.sin(phase-j*.7));line(x-21,y,x+21-(j%3)*6,y)}}}\nelse if(kind==='searching'||kind==='browsing'||kind==='network'){circle(cx,cy,r*1.3);const a=phase%(Math.PI*2);line(cx+Math.cos(a)*r,cy+Math.sin(a)*r,cx+Math.cos(a)*r*1.42,cy+Math.sin(a)*r*1.42);for(let i=0;i<5;i++){const a=i*Math.PI*2/5;circle(cx+Math.cos(a)*r*1.3,cy+Math.sin(a)*r*1.3,3+i%2)}}\nelse if(['editing','executing','tool'].includes(kind)){for(const side of [-1,1]){const x=cx+side*r*1.3;line(x,cy-32,x,cy+32);line(x,cy-32,x-side*12,cy-32);line(x,cy+32,x-side*12,cy+32)}ctx.globalAlpha=.2+.2*(.5+.5*Math.sin(phase*3));line(cx+r*.9,cy+44,cx+r*.9+18,cy+44)}","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/tui/pet_watch/shared.html#L38-L74","documentation":"The config-file import handler refuses appearance files larger than 4096 bytes before parsing. The appearance schema is tiny, so an oversized file is almost certainly the wrong file; the limit bounds work and prevents junk imports.","triggerScenarios":"Selecting a file in the appearance import picker whose size exceeds 4 KiB — e.g. a replay JSON, an export bundle, or any non-appearance JSON document.","commonSituations":"Importing codewhale-shared-pet.json or codewhale-preview-replay.json instead of codewhale-appearance.json; a hand-grown JSON with extra data; picking the wrong file in the dialog.","solutions":["Export a fresh appearance via 'Export' (codewhale-appearance.json) and import that file","Trim the JSON to just {version:1, appearance:{...}} with only the base keys","Verify file size < 4096 bytes before importing"],"exampleFix":"// before: import codewhale-shared-pet.json (large bundle) → error\n// after: export codewhale-appearance.json from the studio and import that\ndownload('codewhale-appearance.json', {version:1, appearance:clean(appearance)})","handlingStrategy":"validation","validationCode":"const f = input.files[0];\nif (f && f.size > 4096) { showError('Choose an appearance file smaller than 4 KiB.'); return; }","typeGuard":null,"tryCatchPattern":"try { await importAppearance(file) } catch (e) { if (String(e.message).includes('4 KiB')) message.textContent = e.message; else throw e }","preventionTips":["Import only codewhale-appearance.json files produced by Export","Check file size before posting","Do not import replay/shared-pet bundles into the appearance slot"],"tags":["validation","file-size","frontend"],"backgroundTag":"file-size-limit-exceeded","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T11:17:16.035Z"}