返回 CodeWhale
shared.html
根目录 / crates / tui / src / tui / pet_watch / shared.html
1 <!doctype html>
2 <html lang="en">
3 <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
4 <title>Codewhale · Habitat studio</title>
5 <style>
6 :root{color-scheme:dark;font:14px system-ui,sans-serif;background:#101619;color:#dae2e2;--line:#344044;--muted:#a4b3b5;--accent:#a3dcd4}*{box-sizing:border-box}[hidden]{display:none!important}body{margin:0}button,input,select{font:inherit}button{color:inherit;background:transparent;border:1px solid var(--line);border-radius:5px;padding:9px 13px;cursor:pointer}button:hover{background:#273338}button:disabled{opacity:.5;cursor:wait}button[aria-pressed=true]{border-color:var(--accent);background:#263b3a}button:focus-visible,input:focus-visible,select:focus-visible,canvas:focus-visible{outline:2px solid var(--accent);outline-offset:3px}::selection{background:#98d7cf;color:#142522}a{color:var(--accent);text-underline-offset:4px}header{height:74px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 30px;gap:16px}header strong{font-size:18px;font-weight:550;letter-spacing:-.025em}header span{font-size:12px;color:var(--muted);margin-left:15px}nav{display:flex;gap:8px}main{display:grid;grid-template-columns:minmax(0,1fr) 290px;min-height:calc(100vh - 74px)}.stage{padding:28px;min-width:0}h1{font-size:30px;letter-spacing:-.03em;font-weight:500;margin:0 0 8px}p{color:var(--muted);line-height:1.6;margin:0 0 22px;max-width:68ch}.habitat{position:relative;height:calc(100vh - 282px);min-height:350px;background:#080f15;border:1px solid #374347;border-radius:5px;overflow:hidden}canvas{display:block;width:100%;height:100%;outline:none}.caption{position:absolute;left:25px;bottom:22px;right:25px;display:flex;justify-content:space-between;gap:12px;font:11px ui-monospace,monospace;color:#d1e1e2;pointer-events:none;font-variant-numeric:tabular-nums}.caption small{font:inherit}.controls{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:16px}.controls label{margin:0 8px}.identity{font:10px ui-monospace,monospace;color:var(--muted);margin-top:15px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.settings{border-left:1px solid var(--line);padding:28px 24px;overflow-y:auto;max-height:calc(100vh - 74px);scrollbar-color:#55686a #101619}.settings h2{font-size:16px;font-weight:550;margin:0 0 8px}.settings p{font-size:12px;line-height:1.55;margin-bottom:22px}.presets{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-bottom:24px}.presets button{padding:9px;text-align:left;font-size:12px;display:flex;align-items:center;gap:8px}.swatch{width:18px;height:18px;display:inline-grid;place-items:center;border-radius:50%;border:1px solid #788887}.swatch i{width:7px;height:7px;border-radius:50%}.field{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:17px 0;font-size:12px}.field input[type=color]{width:40px;height:26px;padding:0;border:1px solid var(--line);background:none;border-radius:3px;cursor:pointer}.range{display:block}.range span{display:flex;justify-content:space-between;margin-bottom:8px}.range input{width:100%;accent-color:var(--accent)}input[type=checkbox]{accent-color:var(--accent)}input[type=file]{max-width:100%;font-size:11px}output{font-variant-numeric:tabular-nums;color:var(--muted)}.divider{border-top:1px solid var(--line);margin-top:22px;padding-top:8px}.config-actions{display:flex;gap:8px;margin-top:20px}.config-actions button{font-size:11px;padding:8px}.status{min-height:32px;font-size:11px!important;margin-top:16px!important}.result{position:absolute;left:clamp(24px,9vw,160px);right:clamp(24px,9vw,160px);top:50%;max-height:44%;overflow:auto;padding:24px 30px;background:#111b20f2;color:#e4eeec;border-radius:6px;opacity:0;transform:translateY(18px);pointer-events:none;transition:opacity .65s cubic-bezier(.16,1,.3,1),transform .65s cubic-bezier(.16,1,.3,1);scrollbar-color:#65817f #111b20}.result.visible{opacity:1;transform:translateY(0);pointer-events:auto}.result h2{font-size:21px;font-weight:500;letter-spacing:-.02em;margin:0 0 12px}.result p{color:#b9cecc;font-size:14px;line-height:1.7;margin:0 0 14px}.result .label{font:11px ui-monospace,monospace;color:#94beb8;margin:0 0 12px}.result button{font-size:12px}.immersive header,.immersive .settings,.immersive .intro,.immersive .identity{display:none}.immersive main{display:block}.immersive .stage{padding:0}.immersive .habitat{height:calc(100dvh - 64px);min-height:200px;border:0;border-radius:0}.immersive .controls{height:64px;margin:0;padding:10px 22px}.gallery{display:none;grid-template-columns:repeat(4,minmax(0,1fr));gap:18px;margin:24px 0}.gallery figure{margin:0;height:205px;position:relative;border-radius:5px;overflow:hidden}.gallery figcaption{position:absolute;left:16px;bottom:14px;font-size:12px}.gallery-mode .gallery{display:grid}.gallery-mode .habitat{display:none}.gallery-mode .stage> .controls{display:none}.gallery-mode .settings{position:sticky;top:0}.connection{margin-top:12px;font-size:11px}.connection a{display:inline-block;margin-top:8px}.work-label{font:11px ui-monospace,monospace;color:var(--muted);margin-left:auto}#finish{display:none}.preview-running #finish{display:block}.no-settings main{grid-template-columns:minmax(0,1fr)}.no-settings .settings{display:none}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important}.result{transition:none}}@media(max-width:1100px){.gallery{grid-template-columns:1fr 1fr}}@media(max-width:800px){header{padding:0 18px}header span{display:none}main{grid-template-columns:1fr}.stage{padding:20px}.habitat{height:50vh;min-height:300px}.settings{border-left:0;border-top:1px solid var(--line);max-height:none;display:grid;grid-template-columns:1fr 1fr;gap:0 24px}.settings>h2,.settings>p,.presets,.config-actions,.status,.connection{grid-column:1/-1}.presets{grid-template-columns:repeat(4,1fr)}.gallery figure{height:220px}.result{left:22px;right:22px;padding:20px}.controls{gap:7px}.controls button{font-size:12px}.work-label{display:none}}@media(max-width:480px){header{height:66px}header strong{font-size:16px}nav button{font-size:11px;padding:8px}h1{font-size:26px}.stage{padding:17px}.presets{grid-template-columns:1fr 1fr}.settings{display:block;padding:22px}.gallery{grid-template-columns:1fr}.result{top:41%;max-height:52%}.result p{font-size:13px}.caption{left:18px;right:18px;font-size:10px}.caption small{display:none}.immersive .controls{height:auto;min-height:64px}.immersive .habitat{height:calc(100dvh - 105px)}}
7
8 .activity{position:absolute;top:48px;left:30px;right:30px;color:#d4e5e5;pointer-events:none}.activity .eyebrow{font:12px ui-monospace,monospace;opacity:.75;margin-bottom:8px}.activity h2{font-size:clamp(22px,2.8vw,34px);font-weight:500;letter-spacing:-.035em;margin:0 0 8px}.activity .detail{font:12px ui-monospace,monospace;line-height:1.6;overflow-wrap:anywhere}.activity .parallel{display:block;margin-top:7px;font:12px system-ui,sans-serif}.result.visible~.activity{opacity:1}.action-field{display:block;font-size:12px;margin:18px 0 10px}.action-field select{display:block;width:100%;margin-top:8px;color:inherit;background:#182226;border:1px solid var(--line);padding:9px;border-radius:4px}.settings .behavior-note{font-size:12px;margin-bottom:24px}.caption{font-size:12px}.identity{font-size:11px}.status,.config-actions button,.connection{font-size:12px!important}.gallery figcaption{font-size:13px}@media(max-width:480px){.activity{top:38px;left:22px;right:22px}.activity h2{font-size:23px}.activity .detail{font-size:12px}.caption{font-size:12px}.result.visible~.activity{display:none}.immersive .habitat{height:calc(100dvh - 112px)}}
9
10 /* A reliable backing protects labels even when the two user colors have
11 opposite luminance. Particle colors remain entirely configurable. */
12 .activity{width:max-content;max-width:calc(100% - 60px);padding:12px 15px;background:#10191df2;border-radius:5px;color:#eaf2f0}.activity .eyebrow{margin:0 0 8px;opacity:1;color:#b9ceca}.caption{padding:8px 10px;background:#10191df2;border-radius:4px;color:#eaf2f0}.result:focus{outline:2px solid #a3dcd4;outline-offset:3px}@media(max-width:480px){.activity{max-width:calc(100% - 44px);padding:12px}.activity h2{font-size:22px}}
13 </style>
14 <header><div><strong>Codewhale pet</strong><span id="connection-label">Shared habitat</span></div><nav aria-label="View options"><button id="compare" aria-pressed="false">Compare looks</button><button id="configure" aria-pressed="true">Appearance</button></nav></header>
15 <main><section class="stage"><div class="intro"><h1>A world that feels like yours.</h1><p>Change the light, the material, the surroundings. The whale stays itself.</p></div>
16 <div class="habitat"><canvas id="tank" tabindex="0" role="img" aria-label="Codewhale habitat"></canvas><article class="result" id="result" tabindex="-1" aria-live="polite" aria-hidden="true" inert><h2>The work is ready.</h2><div class="label">Preview result · no task was run</div><p>The whale held the space while the task was running. Now the answer comes forward, with the habitat still present behind it.</p><p>In Codewhale, this space will show the completed assistant response from your existing conversation. Your draft, history, and selection stay intact.</p><button id="replay">Replay transition</button></article><div class="activity" id="activity" aria-live="polite" aria-atomic="true"><h2 id="activity-title">A moment between tasks.</h2><div class="eyebrow" id="activity-evidence">Local companion</div><div class="detail" id="activity-detail">Activity unobserved</div><span class="parallel" id="activity-parallel"></span></div><div class="caption"><strong id="caption">Connecting…</strong><small id="clock"></small></div></div>
17 <div class="gallery" id="gallery" aria-label="The same whale across eight appearances"></div>
18 <div class="controls"><button id="focus">Focus</button><button id="pulse">Pulse</button><button id="sound" aria-pressed="false">Sound off</button><label><input id="still" type="checkbox"> Still</label><button id="expand">Full habitat</button><button id="work">Preview work → result</button><button id="finish">Finish preview now</button><button id="return-live" hidden>Return to live pet</button><span class="work-label" id="work-label">Escape returns</span></div><div class="identity" id="identity"></div></section>
19 <aside class="settings" aria-label="Appearance settings"><h2>Behavior</h2><label class="action-field">Try a preview action<select id="action-preview"><option value="thinking">Thinking</option><option value="reading">Reading files</option><option value="searching">Searching</option><option value="editing">Editing files</option><option value="executing">Running a command</option><option value="testing">Running tests</option><option value="browsing">Using the browser</option><option value="memory">Retrieving context</option><option value="delegating">Three parallel agents</option><option value="waiting">Waiting for you</option><option value="responding">Writing a response</option><option value="error">An operation failed</option><option value="unknown">No telemetry</option></select></label><p class="behavior-note">Preview actions are simulated. Motion follows a seed; your Focus and Pulse interactions become part of the replay.</p><h2>Appearance</h2><p>Presets are starting points. Every color is yours to change.</p><div class="presets" id="presets"></div>
20 <label class="field">Background <input type="color" id="background" value="#080f15"></label><label class="field">Upper light <input type="color" id="backgroundTop" value="#182d38"></label><label class="field">Particle color <input type="color" id="particle" value="#87dddb"></label><label class="field"><span>Colors follow activity</span><input type="checkbox" id="eventColors" checked></label>
21 <label class="field range"><span>Brightness <output id="brightness-value"></output></span><input type="range" id="brightness" min="0.25" max="2" step="0.05" value="1.25"></label><label class="field range"><span>Dot size <output id="dotScale-value"></output></span><input type="range" id="dotScale" min="0.65" max="1.8" step="0.05" value="1"></label><label class="field range"><span>Glow <output id="glow-value"></output></span><input type="range" id="glow" min="0" max="1" step="0.05" value="0.5"></label><label class="field"><span>Water and horizon</span><input type="checkbox" id="environment" checked></label>
22 <div class="config-actions"><button id="export-config">Save appearance</button><button id="import-config">Load appearance</button><input id="config-file" type="file" accept="application/json,.json" hidden></div><p class="status" id="message" role="status">Opening the habitat…</p><div class="connection"><button id="save">Save pet replay</button><br><a href="pet.html" id="join">Join a live pet from the standalone viewer</a></div></aside></main>
23 <script src="pet-native.js"></script>
24 <script>
25 'use strict';
26 const $=id=>document.getElementById(id), canvas=$('tank');
27 const rgb=h=>h.match(/[a-f\d]{2}/gi).map(n=>parseInt(n,16)),hex=c=>'#'+c.map(n=>n.toString(16).padStart(2,'0')).join('');
28 const base={background:[8,15,21],backgroundTop:[24,45,56],particle:[135,221,219],eventColors:true,brightness:1.25,dotScale:1,glow:.5,environment:true};
29 const presets=[['Ocean','#080f15','#182d38','#87dddb'],['Chalk','#e8edee','#fbfdfb','#285967'],['Graphite','#141518','#28292e','#d9e0e3'],['Linen','#e9e1d2','#f8f2e7','#79573a'],['Forest','#0c1916','#20382d','#c5db9b'],['Plum','#201a29','#3b2e4b','#e7b8d0'],['Ember','#211610','#49291b','#edb177'],['Cobalt','#101934','#263b60','#b2d7f2']].map(([name,b,t,p])=>({name,...base,background:rgb(b),backgroundTop:rgb(t),particle:rgb(p),eventColors:false,brightness:name==='Chalk'||name==='Linen'?1.8:1.35}));
30 let appearance={...base}, selected='Ocean', current, previous, lastTickAt=0, received=0, client=crypto.randomUUID(), seq=0, pending=null, nextAppearance=null, busy=false, sound=false;
31 let preview=new URLSearchParams(location.search).has('preview'), pet, points, demoTick=0, demoRunning=false, demoDone=false, actionChoice='thinking', actionTick=0, lastDraw=0, accumulator=0, galleryTick=0, previewSaved=false;
32 const media=matchMedia('(prefers-reduced-motion: reduce)');$('still').checked=media.matches;media.onchange=()=>{$('still').checked=media.matches};
33 function valid(a){return a&&Object.keys(base).every(k=>k in a)&&['background','backgroundTop','particle'].every(k=>Array.isArray(a[k])&&a[k].length===3&&a[k].every(n=>Number.isInteger(n)&&n>=0&&n<=255))&&typeof a.eventColors==='boolean'&&typeof a.environment==='boolean'&&[['brightness',.25,2],['dotScale',.65,1.8],['glow',0,1]].every(([k,lo,hi])=>Number.isFinite(a[k])&&a[k]>=lo&&a[k]<=hi)}
34 function clean(a){if(!valid(a))throw Error('This appearance has invalid colors or control ranges.');return Object.fromEntries(Object.keys(base).map(k=>[k,a[k]]))}
35 try{const saved=JSON.parse(localStorage.getItem('codewhale-pet-appearance-v1'));if(valid(saved))appearance=clean(saved)}catch{}
36 function presetName(a){return presets.find(p=>JSON.stringify(clean(p))===JSON.stringify(clean(a)))?.name||'Custom'}
37 selected=presetName(appearance);
38 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));}
39 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()}}
40 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)}
41 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()};
42 syncControls();
43 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()}
44 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}}
45 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)}}
46 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()}
47 $('focus').onclick=()=>interact(false);$('pulse').onclick=()=>interact(true);canvas.onpointerdown=()=>interact(false);
48 $('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}};
49 setInterval(()=>{if(sound&&!document.hidden&&performance.now()-lastTickAt<500)request('/v1/audio',{client,enabled:true}).catch(()=>{sound=false})},500);
50 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});
51 $('compare').onclick=()=>{document.body.classList.toggle('gallery-mode');$('compare').setAttribute('aria-pressed',String(document.body.classList.contains('gallery-mode')))};
52 $('configure').onclick=()=>{document.body.classList.toggle('no-settings');$('configure').setAttribute('aria-pressed',String(!document.body.classList.contains('no-settings')))};
53 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')}
54 $('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')}});
55 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)}
56 $('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=''}};
57 $('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}};
58 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.'}
59 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()}
60 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}}
61 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')}
62 $('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()};
63 $('work').onclick=startWork;$('replay').onclick=startWork;$('finish').onclick=finishWork;
64 const actionTools={reading:'read_file',searching:'search_files',editing:'apply_patch',executing:'exec_command',testing:'run_tests',browsing:'browser_navigate',memory:'retrieve_context'};
65 const workPhases=['thinking','reading','searching','browsing','memory','delegating','editing','executing','testing','waiting','responding'];
66 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}
67 $('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}};
68 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';}
69 // View-only marks use the owner's fixed tick. They consume no random stream,
70 // claim no results, and freeze under Still. The moving body remains canonical.
71 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()};
72 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)}}}
73 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)}}
74 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)}
75 else if(kind==='testing'){for(let i=0;i<12;i++){const a=i*Math.PI/6;ctx.globalAlpha=.08+.35*Math.max(0,Math.cos(a-phase));line(cx+Math.cos(a)*r*1.25,cy+Math.sin(a)*r*1.25,cx+Math.cos(a)*r*1.38,cy+Math.sin(a)*r*1.38)}}
76 else if(kind==='waiting'){for(let i=0;i<3;i++){ctx.globalAlpha=.08+.05*(.5+.5*Math.sin(phase-i));circle(cx,cy,r*(1.12+i*.14))}}
77 else if(kind==='responding'){for(let i=0;i<5;i++){const x=cx-r*.7+i*r*.35;ctx.globalAlpha=.12+.22*(.5+.5*Math.sin(phase*2-i));line(x,cy+r*1.3,x+r*.22,cy+r*1.3)}}
78 if(parallel){for(let i=0;i<Math.min(6,parallel);i++){const x=cx+(i-(parallel-1)/2)*42,y=h*.83;ctx.globalAlpha=.4;circle(x,y,7);circle(x,y,2);if(i)line(x-34,y,x-9,y)}}ctx.restore()}
79
80 function readable(c){return c[0]*.2126+c[1]*.7152+c[2]*.0722>145?'#28383d':'#d4e5e5'}
81 function paint(c,f,a,now,small=false){const w=c.clientWidth,h=c.clientHeight,d=small?1:Math.min(devicePixelRatio||1,2);if(!w||!h)return;if(c.width!==Math.round(w*d)||c.height!==Math.round(h*d)){c.width=Math.round(w*d);c.height=Math.round(h*d)}const ctx=c.getContext('2d');ctx.setTransform(d,0,0,d,0,0);const gradient=ctx.createRadialGradient(w*.5,0,0,w*.5,h*.18,w*.9);gradient.addColorStop(0,hex(a.backgroundTop));gradient.addColorStop(1,hex(a.background));ctx.fillStyle=gradient;ctx.fillRect(0,0,w,h);if(!f)return;const still=$('still').checked,pose=still?f.still:f,state=pose.state,style=pose.style,stale=!preview&&(now-lastTickAt>800||now-received>800),t=still?0:f.timeMs/1000;
82 if(a.environment){ctx.strokeStyle=readable(a.background);ctx.globalAlpha=.12;ctx.lineWidth=.7;ctx.beginPath();ctx.moveTo(20,26);ctx.lineTo(w-20,26);ctx.stroke();for(let lane=0;lane<6;lane++){ctx.globalAlpha=.025;ctx.beginPath();for(let x=0;x<=w;x+=8){const y=h*.85+Math.sin(x/110+lane+t*.18)*7+lane*5;x?ctx.lineTo(x,y):ctx.moveTo(x,y)}ctx.stroke()}}ctx.globalAlpha=1;
83 const complete=demoDone&&!small,bounds=pose.points.reduce((b,p)=>[Math.max(b[0],Math.abs(p[0])),Math.max(b[1],Math.abs(p[1]))],[.7,.7]),scale=Math.min(w*.38/bounds[0],h*(complete?.18:.28)/bounds[1]),ox=w*(.5+(still?0:state.roamX)*.06),oy=h*(complete?.25:.56),flip=state.flip,mix=previous&&!still&&!stale?Math.min(1,(now-lastTickAt)/33.334):1;
84 if(!small&&!complete&&!stale&&f.activity?.observed)actionMarks(ctx,f.activity.kind,t,w,h,readable(a.background),f.activity.parallel);
85 const color=a.eventColors?[style.r,style.g,style.b]:a.particle,material='rgb('+color.map(Math.round).join(',')+')',radius=Math.max(.65,Math.min(w,h)*.0026)*a.dotScale,hollow=stale||!f.producerConnected||state.observed<.92;
86 const alpha=preview?Math.max(style.alpha,hollow?.32:.6)*a.brightness:style.alpha/(f.appearance?.brightness||1)*a.brightness;
87 for(let i=0;i<980;i++){const q=pose.points[i],p=previous?.points[i],x=ox+(p&&!still?p[0]+(q[0]-p[0])*mix:q[0])*scale*flip,y=oy+(p&&!still?p[1]+(q[1]-p[1])*mix:q[1])*scale,depth=.65+.35*(i*37%101)/100;ctx.fillStyle=ctx.strokeStyle=material;ctx.globalAlpha=Math.min(1,alpha*depth*(stale?.5:1));ctx.lineWidth=Math.max(.65,radius*.5);ctx.beginPath();ctx.arc(x,y,radius*depth,0,Math.PI*2);hollow?ctx.stroke():ctx.fill();if(!hollow&&a.glow>0&&i%5===0){ctx.globalAlpha=.055*a.glow;ctx.beginPath();ctx.arc(x,y,radius*(1+a.glow*5),0,Math.PI*2);ctx.fill()}}ctx.globalAlpha=1;}
88 function draw(now){requestAnimationFrame(draw);if(document.hidden)return;const dt=lastDraw?Math.min(.1,(now-lastDraw)/1000):0;lastDraw=now;if(preview&&pet){accumulator+=dt;let count=0;while(accumulator>=1/30&&count++<3){advancePreview();accumulator-=1/30}}if(document.body.classList.contains('gallery-mode')){if(++galleryTick%3===0)Array.from($('gallery').querySelectorAll('canvas')).forEach((c,i)=>paint(c,current,presets[i],now,true))}else paint(canvas,current,appearance,now);if(current){const p=$('still').checked?current.still:current,activity=current.activity,known=activity?.observed&&(preview||now-lastTickAt<800),title=demoDone?'Settling back into the habitat.':known?activity.label:'A moment between tasks.',detail=demoDone?'Preview complete':known?activity.tool||(preview?'Simulated lifecycle event':'Observed lifecycle event'):'Activity unobserved',evidence=preview?'Simulated preview':known?'Observed in Codewhale':'Local companion';for(const [id,text] of [['activity-title',title],['activity-detail',detail],['activity-evidence',evidence]])if($(id).textContent!==text)$(id).textContent=text;$('activity-parallel').textContent=known&&activity.parallel?activity.parallel+' parallel agent'+(activity.parallel===1?'':'s')+' active':'';$('caption').textContent=(preview?'Preview · ':'')+p.style.channel+' · '+p.style.arch+(current.state.observed<.92?' · unobserved':'')+(demoDone?' · complete':'');$('clock').textContent=Math.floor(current.timeMs/60000)+':'+String(Math.floor(current.timeMs/1000)%60).padStart(2,'0');canvas.setAttribute('aria-label',title+'; '+detail+'; '+$('caption').textContent+'; '+(selected==='Custom'?'custom':selected)+' appearance')}}
89 async function connect(){if(preview){await makePreview();return}const token=location.hash.slice(1);history.replaceState(null,'',location.pathname);if(token){if(!/^[a-f0-9]{64}$/i.test(token))throw Error('Invalid local connection');await request('/v1/attach',{}, {Authorization:'Bearer '+token})}$('join').hidden=true;$('message').textContent='One live pet. Appearance changes are shared; preview work stays isolated.';await poll()}
90 requestAnimationFrame(draw);connect().catch(e=>{$('message').textContent=e.message});
91 </script></html>
92
92 lines HTML