Hmbown/CodeWhale · error · Error

Choose an appearance file smaller than 4 KiB.

Error message

Choose an appearance file smaller than 4 KiB.

What it means

Thrown by the config-file import handler in the shared companion page when the selected appearance JSON file is larger than 4096 bytes. Valid appearance files are tiny (a few hundred bytes), so the 4 KiB cap is a guard against importing arbitrary or maliciously oversized files. The check runs before any parsing, and the message is shown in `#message`.

Solutions

  1. Select the correct `codewhale-appearance.json` export, not another JSON file.
  2. Trim any extra keys added by hand so the file is under 4096 bytes.
  3. Re-export the appearance from the UI and import it unchanged.
  4. Recreate the appearance via the presets/controls instead of importing.

Example fix

// before
$ ls -l my-appearance.json  # 8421 bytes (embedded history)
// after
{ "version": 1, "appearance": { ...8 base keys only... } }  // ~350 bytes
Defensive patterns

Strategy: validation

Validate before calling

const f = fileInput.files[0];
if (f && f.size > 4096) throw new Error('Appearance file must be under 4 KiB.');

Type guard

null

Try / catch

try {
  await importAppearance(f);
} catch (e) {
  if (e.message.includes('4 KiB')) showMessage('That is not an appearance export — pick codewhale-appearance.json.');
  else showMessage(e.message);
} finally {
  fileInput.value = '';
}

Prevention

When it happens

Trigger: User picks a file via Import config whose `f.size > 4096` — e.g. the wrong JSON file selected, an appearance file padded with extra data, or a bundle containing multiple appearances.

Common situations: Selecting the wrong export file (e.g. `codewhale-shared-pet.json` replay recording instead of `codewhale-appearance.json`); editing the export to embed extra keys until it exceeds 4 KiB.

Understand the failure class

Background: "File too large" / "file size exceeds limit" errors: why libraries cap file sizes and how to fix them — this error's family across 46 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/f752a4f21a00bf84. Report an issue: GitHub.

Appendix: source

Thrown at pet/public/shared.html:56

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));}
function 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()}}
for(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)}
for(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()};
syncControls();
async 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()}
async 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}}
async 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)}}
function 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()}
$('focus').onclick=()=>interact(false);$('pulse').onclick=()=>interact(true);canvas.onpointerdown=()=>interact(false);
$('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}};
setInterval(()=>{if(sound&&!document.hidden&&performance.now()-lastTickAt<500)request('/v1/audio',{client,enabled:true}).catch(()=>{sound=false})},500);
document.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});
$('compare').onclick=()=>{document.body.classList.toggle('gallery-mode');$('compare').setAttribute('aria-pressed',String(document.body.classList.contains('gallery-mode')))};
$('configure').onclick=()=>{document.body.classList.toggle('no-settings');$('configure').setAttribute('aria-pressed',String(!document.body.classList.contains('no-settings')))};
function immersive(on){document.body.classList.toggle('immersive',on);$('expand').textContent=on?'Back to studio':'Full habitat';if(on)document.body.classList.remove('gallery-mode')}
$('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')}});
function 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)}
$('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=''}};
$('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}};
async 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.'}
function 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()}
async 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}}
function 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')}
$('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()};
$('work').onclick=startWork;$('replay').onclick=startWork;$('finish').onclick=finishWork;
const actionTools={reading:'read_file',searching:'search_files',editing:'apply_patch',executing:'exec_command',testing:'run_tests',browsing:'browser_navigate',memory:'retrieve_context'};
const workPhases=['thinking','reading','searching','browsing','memory','delegating','editing','executing','testing','waiting','responding'];
function 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}
$('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}};
function 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';}
// View-only marks use the owner's fixed tick. They consume no random stream,
// claim no results, and freeze under Still. The moving body remains canonical.
function 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()};
if(['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)}}}
else 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)}}
else 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)}

View on GitHub (pinned to 433685b202)