Describe the symptom — "devvm is unreachable", "disk full", "ssh hangs"
— and it will connect over SSH, investigate, and stream its work here.
For a hard power action when the agent can't help, use Direct VM control.
`),pi=X(`
`),mi=X(``),hi=X(``),gi=X(`
`),_i=X(``),vi=X(``),yi=X(`
`),bi=X(`
`),xi=X(`
agent working — streaming live
`),Si=X(`
Recovery agentSSHes into the devvm to diagnose & repair
`);function Ci(e,t){He(t,!0);let n=ti(t,`sessionId`,3,``),r=ti(t,`sessionReady`,3,!1),i=ti(t,`onLiveSession`,3,e=>{}),a=ti(t,`onStreamingChange`,3,e=>{}),o=P(Zt([])),s=P(``),c=P(!1),l,u,d=!0,f=ht(()=>r()&&!Y(c)&&Y(s).trim().length>0);function p(){l&&(d=l.scrollHeight-l.scrollTop-l.clientHeight<60)}async function m(e=!1){!e&&!d||(await rr(),l&&(l.scrollTop=l.scrollHeight))}function h(){return Y(o)[Y(o).length-1]}function g(e){let t=h().parts,n=t[t.length-1];n&&n.type===`text`?n.text+=e:t.push({type:`text`,text:e}),F(o,Y(o),!0)}function _(e){switch(e?.kind){case`session`:i()(e.session_id);break;case`text`:e.text&&g(e.text);break;case`tool`:{let t=e.input&&typeof e.input.command==`string`?e.input.command:``;h().parts.push({type:`tool`,name:e.name||`tool`,command:t}),F(o,Y(o),!0);break}case`result`:h().result={is_error:!!e.is_error,text:typeof e.result==`string`?e.result:``,duration_ms:typeof e.duration_ms==`number`?e.duration_ms:null},F(o,Y(o),!0);break;case`error`:h().error=e.error||`unknown error`,F(o,Y(o),!0);break;case`done`:break;default:break}m()}async function v(){let e=Y(s).trim();if(!(!e||Y(c)||!r())){Y(o).push({role:`user`,text:e}),Y(o).push({role:`assistant`,parts:[]}),F(o,Y(o),!0),F(s,``),F(c,!0),a()(!0),d=!0,await m(!0);try{await oi({session_id:n(),prompt:e},_)}catch(e){let t=h();t&&t.role===`assistant`&&!t.error&&(t.error=(e instanceof Error?e.message:String(e))+` — the connection to the agent failed.`,F(o,Y(o),!0))}finally{F(c,!1),a()(!1),await m(),u?.focus()}}}function y(e){e.key===`Enter`&&!e.shiftKey&&(e.preventDefault(),v())}function b(e){return e==null?``:e<1e3?`${e} ms`:`${(e/1e3).toFixed(+(e<1e4))} s`}let x=ht(()=>Y(o).length===0);var S=Si(),C=R(L(S),2),w=L(C),T=e=>{Z(e,fi())};$(w,e=>{Y(x)&&e(T)}),Pr(R(w,2),17,()=>Y(o),Ar,(e,t)=>{var n=br(),r=on(n),i=e=>{var n=pi(),r=L(n),i=L(r,!0);k(r),k(n),z(()=>Q(i,Y(t).text)),Z(e,n)},a=e=>{var n=bi(),r=L(n),i=L(r),a=e=>{Z(e,mi())};$(i,e=>{Y(t).parts.length===0&&!Y(t).result&&!Y(t).error&&e(a)});var o=R(i,2);Pr(o,17,()=>Y(t).parts,Ar,(e,t)=>{var n=br(),r=on(n),i=e=>{var n=hi(),r=L(n,!0);k(n),z(()=>Q(r,Y(t).text)),Z(e,n)},a=e=>{di(e,{get name(){return Y(t).name},get command(){return Y(t).command}})};$(r,e=>{Y(t).type===`text`?e(i):e(a,-1)}),Z(e,n)});var s=R(o,2),c=e=>{var n=gi(),r=L(n);k(n),z(()=>Q(r,`⚠ ${Y(t).error??``}`)),Z(e,n)},l=e=>{var n=yi(),r=L(n),i=L(r,!0);k(r);var a=R(r,2),o=e=>{var n=_i(),r=L(n,!0);k(n),z(()=>Q(r,Y(t).result.text)),Z(e,n)};$(a,e=>{Y(t).result.text&&e(o)});var s=R(a,2),c=e=>{var n=vi(),r=L(n,!0);k(n),z(e=>Q(r,e),[()=>b(Y(t).result.duration_ms)]),Z(e,n)};$(s,e=>{Y(t).result.duration_ms!=null&&e(c)}),k(n),z(()=>{Hr(n,1,`turn-note ${Y(t).result.is_error?`turn-note--error`:`turn-note--ok`}`,`svelte-1bi93vx`),Q(i,Y(t).result.is_error?`failed`:`done`)}),Z(e,n)};$(s,e=>{Y(t).error?e(c):Y(t).result&&e(l,1)}),k(r),k(n),Z(e,n)};$(r,e=>{Y(t).role===`user`?e(i):e(a,-1)}),Z(e,n)}),k(C),ei(C,e=>l=e,()=>l);var ee=R(C,2),te=L(ee),ne=e=>{Z(e,xi())};$(te,e=>{Y(c)&&e(ne)});var re=R(te,2),ie=L(re);dn(ie),ei(ie,e=>u=e,()=>u);var ae=R(ie,2),oe=L(ae,!0);k(ae),k(re),k(ee),k(S),z(()=>{Kr(ie,`placeholder`,r()?`Describe the problem… (Enter to send · Shift+Enter for a new line)`:`Waiting for a session…`),ie.disabled=!r()||Y(c),ae.disabled=!Y(f),Q(oe,Y(c)?`…`:`Send`)}),dr(`scroll`,C,p),dr(`submit`,ee,e=>{e.preventDefault(),v()}),fr(`keydown`,ie,y),Xr(ie,()=>Y(s),e=>F(s,e)),Z(e,S),Ue()}pr([`keydown`]);var wi=X(`
No AI in the path — these reach the Proxmox host over a
- forced-command SSH key and work even when the agent is down.
`);function Vi(e,t){He(t,!0);let n={status:{label:`status`,blurb:`qm status — is the VM up?`},forensics:{label:`forensics`,blurb:`capture live diagnostic state`},start:{label:`start`,blurb:`power on a stopped VM`},stop:{label:`stop`,blurb:`hard power-off (pulls the plug)`},reset:{label:`reset`,blurb:`warm reboot — reuses the QEMU process`},cycle:{label:`cycle`,blurb:`stop → start; applies staged config; fixes a wedged QEMU`,headline:!0}},r=[`status`,`forensics`,`start`,`stop`,`reset`,`cycle`],i=P(`loading`),a=P(``),o=P(Zt([])),s=P(``),c=P(``),l=P(null),u=P(``),d=ht(()=>Y(c)!==``);Or(async()=>{try{let{verbs:e,mutating:t}=await si(),a=new Set(t),s=e.filter(e=>n[e]);F(o,[...r.filter(e=>s.includes(e)),...s.filter(e=>!r.includes(e))].map(e=>({name:e,mutating:a.has(e),...n[e]})),!0),F(i,`ready`)}catch(e){F(i,`error`),F(a,e instanceof Error?e.message:String(e),!0)}});let f=ht(()=>Y(o).filter(e=>!e.mutating)),p=ht(()=>Y(o).filter(e=>e.mutating));function m(e){Y(d)||(e.mutating?F(s,Y(s)===e.name?``:e.name,!0):g(e.name))}function h(){F(s,``)}async function g(e){F(s,``),F(u,``),F(l,null),F(c,e,!0);try{F(l,await ci(e),!0)}catch(e){F(u,e instanceof Error?e.message:String(e),!0)}finally{F(c,``)}}let _=ht(()=>!!Y(l)&&(Y(l).rejected||Y(l).exit_code!=null&&Y(l).exit_code!==0));var v=Bi(),y=R(L(v),2),b=e=>{Z(e,wi())},x=e=>{var t=Ti(),n=L(t),r=R(n);k(t),z(()=>Q(n,`Couldn't load the VM controls — ${Y(a)??``}. `)),fr(`click`,r,()=>location.reload()),Z(e,t)},S=e=>{var t=zi(),n=on(t),r=R(L(n),2);Pr(r,21,()=>Y(f),e=>e.name,(e,t)=>{var n=Di(),r=L(n),i=e=>{Z(e,Ei())};$(r,e=>{Y(c)===Y(t).name&&e(i)});var a=R(r,2),o=L(a,!0);k(a),k(n),z(()=>{n.disabled=Y(d),Kr(n,`title`,Y(t).blurb),Q(o,Y(t).label)}),fr(`click`,n,()=>m(Y(t))),Z(e,n)}),k(r),k(n);var i=R(n,2),a=R(L(i),2);Pr(a,21,()=>Y(p),e=>e.name,(e,t)=>{var n=ji(),r=L(n),i=L(r),a=e=>{Z(e,Oi())};$(i,e=>{Y(c)===Y(t).name&&e(a)});var o=R(i,2),l=L(o,!0);k(o);var u=R(o,2),f=e=>{Z(e,ki())};$(u,e=>{Y(t).headline&&e(f)}),k(r);var p=R(r,2),_=L(p,!0);k(p);var v=R(p,2),y=e=>{var n=Ai(),r=L(n),i=R(L(r)),a=L(i,!0);k(i),Ne(),k(r);var o=R(r,2),s=L(o),c=R(s,2);k(o),k(n),z(()=>{Kr(n,`aria-label`,`Confirm ${Y(t).name??``}`),Q(a,Y(t).name),s.disabled=Y(d),c.disabled=Y(d)}),fr(`click`,s,()=>g(Y(t).name)),fr(`click`,c,h),Z(e,n)};$(v,e=>{Y(s)===Y(t).name&&e(y)}),k(n),z(()=>{Hr(n,1,`danger-item ${Y(t).headline?`danger-item--headline`:``}`,`svelte-1qihpg4`),Hr(r,1,`vbtn vbtn--danger ${Y(t).headline?`vbtn--headline`:``}`,`svelte-1qihpg4`),r.disabled=Y(d),Kr(r,`aria-expanded`,Y(s)===Y(t).name),Q(l,Y(t).label),Q(_,Y(t).blurb)}),fr(`click`,r,()=>m(Y(t))),Z(e,n)}),k(a),k(i);var o=R(i,2),v=e=>{var t=Mi(),n=L(t);k(t),z(()=>Q(n,`⚠ Command failed to reach the host — ${Y(u)??``}`)),Z(e,t)};$(o,e=>{Y(u)&&e(v)});var y=R(o,2),b=e=>{var t=Ri(),n=L(t),r=L(n),i=L(r,!0);k(r);var a=R(r,2),o=e=>{Z(e,Ni())},s=e=>{var t=Pi(),n=L(t);k(t),z(()=>{Hr(t,1,`out-status ${Y(_)?`out-status--fail`:`out-status--ok`}`,`svelte-1qihpg4`),Q(n,`exit ${Y(l).exit_code??``}`)}),Z(e,t)};$(a,e=>{Y(l).rejected?e(o):e(s,-1)}),k(n);var c=R(n,2),u=e=>{var t=Fi(),n=L(t,!0);k(t),z(()=>Q(n,Y(l).stdout)),Z(e,t)};$(c,e=>{Y(l).stdout&&e(u)});var d=R(c,2),f=e=>{var t=Ii(),n=R(on(t),2),r=L(n,!0);k(n),z(()=>Q(r,Y(l).stderr)),Z(e,t)};$(d,e=>{Y(l).stderr&&e(f)});var p=R(d,2),m=e=>{Z(e,Li())};$(p,e=>{!Y(l).stdout&&!Y(l).stderr&&e(m)}),k(t),z(()=>{Hr(t,1,`out ${Y(_)?`out--fail`:`out--ok`}`,`svelte-1qihpg4`),Q(i,Y(l).verb)}),Z(e,t)};$(y,e=>{Y(l)&&e(b)}),Z(e,t)};$(y,e=>{Y(i)===`loading`?e(b):Y(i)===`error`?e(x,1):e(S,-1)}),k(v),Z(e,v),Ue()}pr([`click`]);var Hi=X(`session unavailable`),Ui=X(`opening session…`),Wi=X(`· agent working`),Gi=X(`session `,1),Ki=X(`
`),qi=X(`
🔧
devvm breakglass
emergency recovery
`);function Ji(e,t){He(t,!0);let n=P(``),r=P(`connecting`),i=P(``),a=P(!1);async function o(){F(r,`connecting`),F(i,``);try{F(n,await ai(),!0),F(r,`ready`)}catch(e){F(r,`error`),F(i,e instanceof Error?e.message:String(e),!0)}}Or(o);function s(e){e&&F(n,e,!0)}let c=ht(()=>Y(n)?Y(n).slice(0,8):`────────`),l=ht(()=>Y(r)===`error`?`error`:Y(a)?`busy`:Y(r)===`ready`?`ready`:`idle`);var u=qi(),d=L(u),f=R(L(d),2),p=L(f),m=R(p,2),h=L(m),g=e=>{Z(e,Hi())},_=e=>{Z(e,Ui())},v=e=>{var t=Gi(),r=R(on(t),2),i=L(r,!0);k(r);var o=R(r,2),s=e=>{Z(e,Wi())};$(o,e=>{Y(a)&&e(s)}),z(()=>{Kr(r,`title`,Y(n)),Q(i,Y(c))}),Z(e,t)};$(h,e=>{Y(r)===`error`?e(g):Y(r)===`connecting`?e(_,1):e(v,-1)}),k(m);var y=R(m,2);k(f),k(d);var b=R(d,2),x=e=>{var t=Ki(),n=L(t);k(t),z(()=>Q(n,`Could not reach the breakglass backend — ${Y(i)??``}. The cluster or
- network may be down. The manual VM controls below still work independently
- of the chat agent.`)),Z(e,t)};$(b,e=>{Y(r)===`error`&&e(x)});var S=R(b,2),C=L(S),w=L(C);{let e=ht(()=>Y(r)===`ready`);Ci(w,{get sessionId(){return Y(n)},get sessionReady(){return Y(e)},onLiveSession:s,onStreamingChange:e=>F(a,e,!0)})}k(C);var T=R(C,2);Vi(L(T),{}),k(T),k(S),k(u),z(()=>{Hr(p,1,`dot dot--${Y(l)??``}`,`svelte-1n46o8q`),y.disabled=Y(a)||Y(r)===`connecting`,Kr(y,`title`,Y(a)?`wait for the current turn to finish`:`start a fresh session`)}),fr(`click`,y,o),Z(e,u),Ue()}pr([`click`]),Cr(Ji,{target:document.getElementById(`app`)});
\ No newline at end of file
+ forced-command SSH key and work even when the agent is down. `);function Vi(e,t){He(t,!0);let n={status:{label:`status`,blurb:`qm status — is the VM up?`},forensics:{label:`forensics`,blurb:`capture live diagnostic state`},start:{label:`start`,blurb:`power on a stopped VM`},stop:{label:`stop`,blurb:`hard power-off (pulls the plug)`},reset:{label:`reset`,blurb:`warm reboot — reuses the QEMU process`},cycle:{label:`cycle`,blurb:`stop → start; applies staged config; fixes a wedged QEMU`,headline:!0}},r=[`status`,`forensics`,`start`,`stop`,`reset`,`cycle`],i=P(`loading`),a=P(``),o=P(Zt([])),s=P(``),c=P(``),l=P(null),u=P(``),d=ht(()=>Y(c)!==``);Or(async()=>{try{let{verbs:e,mutating:t}=await si(),a=new Set(t),s=e.filter(e=>n[e]);F(o,[...r.filter(e=>s.includes(e)),...s.filter(e=>!r.includes(e))].map(e=>({name:e,mutating:a.has(e),...n[e]})),!0),F(i,`ready`)}catch(e){F(i,`error`),F(a,e instanceof Error?e.message:String(e),!0)}});let f=ht(()=>Y(o).filter(e=>!e.mutating)),p=ht(()=>Y(o).filter(e=>e.mutating));function m(e){Y(d)||(e.mutating?F(s,Y(s)===e.name?``:e.name,!0):g(e.name))}function h(){F(s,``)}async function g(e){F(s,``),F(u,``),F(l,null),F(c,e,!0);try{F(l,await ci(e),!0)}catch(e){F(u,e instanceof Error?e.message:String(e),!0)}finally{F(c,``)}}let _=ht(()=>!!Y(l)&&(Y(l).rejected||Y(l).exit_code!=null&&Y(l).exit_code!==0));var v=Bi(),y=R(L(v),2),b=e=>{Z(e,wi())},x=e=>{var t=Ti(),n=L(t),r=R(n);k(t),z(()=>Q(n,`Couldn't load the VM controls — ${Y(a)??``}. `)),fr(`click`,r,()=>location.reload()),Z(e,t)},S=e=>{var t=zi(),n=on(t),r=R(L(n),2);Pr(r,21,()=>Y(f),e=>e.name,(e,t)=>{var n=Di(),r=L(n),i=e=>{Z(e,Ei())};$(r,e=>{Y(c)===Y(t).name&&e(i)});var a=R(r,2),o=L(a,!0);k(a),k(n),z(()=>{n.disabled=Y(d),Kr(n,`title`,Y(t).blurb),Q(o,Y(t).label)}),fr(`click`,n,()=>m(Y(t))),Z(e,n)}),k(r),k(n);var i=R(n,2),a=R(L(i),2);Pr(a,21,()=>Y(p),e=>e.name,(e,t)=>{var n=ji(),r=L(n),i=L(r),a=e=>{Z(e,Oi())};$(i,e=>{Y(c)===Y(t).name&&e(a)});var o=R(i,2),l=L(o,!0);k(o);var u=R(o,2),f=e=>{Z(e,ki())};$(u,e=>{Y(t).headline&&e(f)}),k(r);var p=R(r,2),_=L(p,!0);k(p);var v=R(p,2),y=e=>{var n=Ai(),r=L(n),i=R(L(r)),a=L(i,!0);k(i),Ne(),k(r);var o=R(r,2),s=L(o),c=R(s,2);k(o),k(n),z(()=>{Kr(n,`aria-label`,`Confirm ${Y(t).name??``}`),Q(a,Y(t).name),s.disabled=Y(d),c.disabled=Y(d)}),fr(`click`,s,()=>g(Y(t).name)),fr(`click`,c,h),Z(e,n)};$(v,e=>{Y(s)===Y(t).name&&e(y)}),k(n),z(()=>{Hr(n,1,`danger-item ${Y(t).headline?`danger-item--headline`:``}`,`svelte-1qihpg4`),Hr(r,1,`vbtn vbtn--danger ${Y(t).headline?`vbtn--headline`:``}`,`svelte-1qihpg4`),r.disabled=Y(d),Kr(r,`aria-expanded`,Y(s)===Y(t).name),Q(l,Y(t).label),Q(_,Y(t).blurb)}),fr(`click`,r,()=>m(Y(t))),Z(e,n)}),k(a),k(i);var o=R(i,2),v=e=>{var t=Mi(),n=L(t);k(t),z(()=>Q(n,`⚠ Command failed to reach the host — ${Y(u)??``}`)),Z(e,t)};$(o,e=>{Y(u)&&e(v)});var y=R(o,2),b=e=>{var t=Ri(),n=L(t),r=L(n),i=L(r,!0);k(r);var a=R(r,2),o=e=>{Z(e,Ni())},s=e=>{var t=Pi(),n=L(t);k(t),z(()=>{Hr(t,1,`out-status ${Y(_)?`out-status--fail`:`out-status--ok`}`,`svelte-1qihpg4`),Q(n,`exit ${Y(l).exit_code??``}`)}),Z(e,t)};$(a,e=>{Y(l).rejected?e(o):e(s,-1)}),k(n);var c=R(n,2),u=e=>{var t=Fi(),n=L(t,!0);k(t),z(()=>Q(n,Y(l).stdout)),Z(e,t)};$(c,e=>{Y(l).stdout&&e(u)});var d=R(c,2),f=e=>{var t=Ii(),n=R(on(t),2),r=L(n,!0);k(n),z(()=>Q(r,Y(l).stderr)),Z(e,t)};$(d,e=>{Y(l).stderr&&e(f)});var p=R(d,2),m=e=>{Z(e,Li())};$(p,e=>{!Y(l).stdout&&!Y(l).stderr&&e(m)}),k(t),z(()=>{Hr(t,1,`out ${Y(_)?`out--fail`:`out--ok`}`,`svelte-1qihpg4`),Q(i,Y(l).verb)}),Z(e,t)};$(y,e=>{Y(l)&&e(b)}),Z(e,t)};$(y,e=>{Y(i)===`loading`?e(b):Y(i)===`error`?e(x,1):e(S,-1)}),k(v),Z(e,v),Ue()}pr([`click`]);var Hi=X(`offline`),Ui=X(`connecting…`),Wi=X(``),Gi=X(`
⚡ VM power controls still work without the chat.
`),Ki=X(`
🔧
devvm breakglass
`);function qi(e,t){He(t,!0);let n=P(``),r=P(`connecting`),i=P(``),a=P(!1),o=P(!1);async function s(){F(r,`connecting`),F(i,``);try{F(n,await ai(),!0),F(r,`ready`)}catch(e){F(r,`error`),F(i,e instanceof Error?e.message:String(e),!0)}}Or(s);function c(e){e&&F(n,e,!0)}let l=ht(()=>Y(n)?Y(n).slice(0,8):`────────`),u=ht(()=>Y(r)===`error`?`error`:Y(a)?`busy`:Y(r)===`ready`?`ready`:`idle`);var d=Ki(),f=L(d),p=R(L(f),2),m=L(p),h=L(m),g=R(h,2),_=e=>{Z(e,Hi())},v=e=>{Z(e,Ui())},y=e=>{var t=Wi(),r=L(t,!0);k(t),z(()=>{Kr(t,`title`,Y(n)),Q(r,Y(l))}),Z(e,t)};$(g,e=>{Y(r)===`error`?e(_):Y(r)===`connecting`?e(v,1):e(y,-1)}),k(m);var b=R(m,2),x=R(b,2);k(p),k(f);var S=R(f,2),C=e=>{var t=Gi(),n=L(t);Ne(2),k(t),z(()=>Q(n,`Can't reach the breakglass backend — ${Y(i)??``}. The cluster or network
+ may be down. The `)),Z(e,t)};$(S,e=>{Y(r)===`error`&&e(C)});var w=R(S,2),T=L(w),ee=L(T);{let e=ht(()=>Y(r)===`ready`);Ci(ee,{get sessionId(){return Y(n)},get sessionReady(){return Y(e)},onLiveSession:c,onStreamingChange:e=>F(a,e,!0)})}k(T);var te=R(T,2);let ne;var re=R(L(te),2),ie=R(L(re),2);k(re),Vi(R(re,2),{}),k(te),k(w);var ae=R(w,2);let oe;k(d),z(()=>{Hr(h,1,`dot dot--${Y(u)??``}`,`svelte-1n46o8q`),x.disabled=Y(a)||Y(r)===`connecting`,Kr(x,`title`,Y(a)?`wait for the current turn to finish`:`start a fresh session`),ne=Hr(te,1,`controls-pane svelte-1n46o8q`,null,ne,{open:Y(o)}),oe=Hr(ae,1,`sheet-backdrop svelte-1n46o8q`,null,oe,{show:Y(o)})}),fr(`click`,b,()=>F(o,!0)),fr(`click`,x,s),fr(`click`,ie,()=>F(o,!1)),fr(`click`,ae,()=>F(o,!1)),Z(e,d),Ue()}pr([`click`]),Cr(qi,{target:document.getElementById(`app`)});
\ No newline at end of file
diff --git a/app/breakglass/static/assets/index-DKeuidum.css b/app/breakglass/static/assets/index-DKeuidum.css
deleted file mode 100644
index 4f1675a..0000000
--- a/app/breakglass/static/assets/index-DKeuidum.css
+++ /dev/null
@@ -1 +0,0 @@
-:root{--bg-0:#07090c;--bg-1:#0c1015;--bg-2:#11171e;--bg-3:#161d26;--bg-term:#06080a;--line:#1d2630;--line-strong:#2a3744;--ink:#e6edf3;--ink-dim:#9bb0c0;--ink-faint:#5d7185;--cyan:#3dd1d6;--cyan-dim:#1f6f72;--amber:#f5b657;--green:#5ddb8e;--green-dim:#1f5f3d;--danger:#ff4d4d;--danger-bright:#ff6363;--danger-deep:#7a1717;--danger-glow:#ff4d4d59;--radius:10px;--radius-sm:7px;--mono:ui-monospace, "JetBrains Mono", "SF Mono", "Cascadia Code", "Fira Code", Menlo, Consolas, "Liberation Mono", monospace;--sans:ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;--shadow-panel:0 1px 0 #ffffff05 inset, 0 16px 40px -24px #000000e6;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}html,body{height:100%;margin:0}body{background-color:var(--bg-0);color:var(--ink);font-family:var(--sans);-webkit-font-smoothing:antialiased;text-rendering:optimizelegibility;background-image:radial-gradient(120% 80% at 85% -10%,#3dd1d612,#0000 55%),radial-gradient(90% 70% at 10% 110%,#f5b6570a,#0000 50%),repeating-linear-gradient(0deg,#ffffff03 0 1px,#0000 1px 3px);background-attachment:fixed}#app{height:100%}button{font-family:var(--mono);cursor:pointer}button:disabled{cursor:not-allowed}::selection{background:#3dd1d647}*{scrollbar-width:thin;scrollbar-color:var(--line-strong) transparent}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-thumb{background:var(--line-strong);background-clip:content-box;border:2px solid #0000;border-radius:99px}::-webkit-scrollbar-thumb:hover{background:#3a4a5a padding-box content-box}@media (prefers-reduced-motion:reduce){*,:before,:after{transition-duration:.001ms!important;animation-duration:.001ms!important;animation-iteration-count:1!important}}.chip.svelte-2zgsrv{background:var(--bg-3);border:1px solid var(--line-strong);border-left:2px solid var(--cyan-dim);max-width:100%;font-family:var(--mono);vertical-align:baseline;border-radius:6px;align-items:baseline;gap:6px;margin:3px 4px 3px 0;padding:3px 9px;font-size:12px;line-height:1.45;display:inline-flex}.cog.svelte-2zgsrv{color:var(--cyan);font-size:11px;transform:translateY(1px)}.name.svelte-2zgsrv{color:var(--ink);font-weight:600}.sep.svelte-2zgsrv{color:var(--ink-faint)}.cmd.svelte-2zgsrv{color:var(--amber);font-family:var(--mono);text-overflow:ellipsis;white-space:nowrap;max-width:100%;overflow:hidden}.chat.svelte-1bi93vx{background:var(--bg-1);border:1px solid var(--line);border-radius:var(--radius);height:100%;min-height:0;box-shadow:var(--shadow-panel);flex-direction:column;display:flex;overflow:hidden}.chat-head.svelte-1bi93vx{border-bottom:1px solid var(--line);background:linear-gradient(#ffffff04,#0000);align-items:baseline;gap:12px;padding:13px 18px;display:flex}.chat-head-label.svelte-1bi93vx{font-family:var(--mono);text-transform:uppercase;letter-spacing:.2em;color:var(--cyan);font-size:11px}.chat-head-hint.svelte-1bi93vx{color:var(--ink-faint);font-size:12px}.stream.svelte-1bi93vx{scroll-behavior:smooth;flex-direction:column;flex:1;gap:14px;min-height:0;padding:20px 18px 8px;display:flex;overflow-y:auto}.empty.svelte-1bi93vx{text-align:center;max-width:460px;color:var(--ink-dim);margin:auto;padding:28px 12px}.empty-mark.svelte-1bi93vx{color:var(--cyan-dim);text-shadow:0 0 24px #3dd1d640;margin-bottom:14px;font-size:40px;line-height:1}.empty-title.svelte-1bi93vx{font-family:var(--mono);color:var(--ink);margin:0 0 8px;font-size:15px}.empty-sub.svelte-1bi93vx{color:var(--ink-faint);margin:0;font-size:13px;line-height:1.6}.empty-sub.svelte-1bi93vx strong:where(.svelte-1bi93vx){color:var(--ink-dim);font-weight:600}.row.svelte-1bi93vx{display:flex}.row--user.svelte-1bi93vx{justify-content:flex-end}.row--assistant.svelte-1bi93vx{justify-content:flex-start}.bubble.svelte-1bi93vx{word-wrap:break-word;overflow-wrap:anywhere;border-radius:13px;max-width:86%;padding:11px 14px;font-size:14px;line-height:1.6}.bubble--user.svelte-1bi93vx{border:1px solid var(--cyan-dim);color:#d8f6f7;white-space:pre-wrap;font-family:var(--sans);background:linear-gradient(#15333a,#0f262c);border-bottom-right-radius:4px}.bubble--assistant.svelte-1bi93vx{background:var(--bg-2);border:1px solid var(--line-strong);color:var(--ink);border-bottom-left-radius:4px}.prose.svelte-1bi93vx{white-space:pre-wrap}.thinking.svelte-1bi93vx,.working-dots.svelte-1bi93vx{align-items:center;gap:4px;display:inline-flex}.thinking.svelte-1bi93vx span:where(.svelte-1bi93vx),.working-dots.svelte-1bi93vx span:where(.svelte-1bi93vx){background:var(--amber);opacity:.4;border-radius:50%;width:6px;height:6px;animation:1.2s ease-in-out infinite svelte-1bi93vx-blink}.thinking.svelte-1bi93vx span:where(.svelte-1bi93vx):nth-child(2),.working-dots.svelte-1bi93vx span:where(.svelte-1bi93vx):nth-child(2){animation-delay:.18s}.thinking.svelte-1bi93vx span:where(.svelte-1bi93vx):nth-child(3),.working-dots.svelte-1bi93vx span:where(.svelte-1bi93vx):nth-child(3){animation-delay:.36s}@keyframes svelte-1bi93vx-blink{0%,80%,to{opacity:.25;transform:translateY(0)}40%{opacity:1;transform:translateY(-2px)}}.turn-note.svelte-1bi93vx{border-radius:var(--radius-sm);font-family:var(--mono);white-space:pre-wrap;overflow-wrap:anywhere;flex-wrap:wrap;align-items:baseline;gap:8px;margin-top:10px;padding:7px 10px;font-size:12px;line-height:1.5;display:flex}.turn-note--ok.svelte-1bi93vx{border:1px solid var(--green-dim);color:#bff5d3;background:#5ddb8e12}.turn-note--error.svelte-1bi93vx{border:1px solid var(--danger-deep);color:#ffd5d5;background:#ff4d4d14}.turn-note-tag.svelte-1bi93vx{text-transform:uppercase;letter-spacing:.14em;opacity:.85;border:1px solid;border-radius:4px;padding:1px 6px;font-size:10px}.turn-note-body.svelte-1bi93vx{flex:1;min-width:0}.turn-note-time.svelte-1bi93vx{color:var(--ink-faint);margin-left:auto}.composer.svelte-1bi93vx{border-top:1px solid var(--line);background:linear-gradient(#0000,#ffffff03);padding:12px}.working-bar.svelte-1bi93vx{font-family:var(--mono);color:var(--amber);letter-spacing:.02em;align-items:center;gap:10px;padding:0 4px 9px;font-size:12px;display:flex}.composer-row.svelte-1bi93vx{align-items:flex-end;gap:10px;display:flex}textarea.svelte-1bi93vx{resize:none;background:var(--bg-2);min-height:44px;max-height:168px;color:var(--ink);border:1px solid var(--line-strong);border-radius:var(--radius-sm);font-family:var(--sans);field-sizing:content;outline:none;flex:1;padding:11px 13px;font-size:14px;line-height:1.5;transition:border-color .15s,box-shadow .15s}textarea.svelte-1bi93vx::placeholder{color:var(--ink-faint)}textarea.svelte-1bi93vx:focus{border-color:var(--cyan-dim);box-shadow:0 0 0 3px #3dd1d61f}textarea.svelte-1bi93vx:disabled{opacity:.55}.send.svelte-1bi93vx{border-radius:var(--radius-sm);border:1px solid var(--cyan-dim);color:#d8f6f7;letter-spacing:.04em;background:linear-gradient(#19474b,#103539);flex:none;align-self:stretch;min-width:78px;padding:0 18px;font-size:13px;font-weight:600;transition:filter .15s,border-color .15s,opacity .15s}.send.svelte-1bi93vx:hover:not(:disabled){filter:brightness(1.22);border-color:var(--cyan)}.send.svelte-1bi93vx:disabled{opacity:.4;background:var(--bg-2);border-color:var(--line-strong);color:var(--ink-faint)}.panel.svelte-1qihpg4{background:var(--bg-1);border:1px solid var(--line);border-top:2px solid var(--danger-deep);border-radius:var(--radius);height:100%;min-height:0;box-shadow:var(--shadow-panel);flex-direction:column;display:flex;overflow-y:auto}.panel-head.svelte-1qihpg4{border-bottom:1px solid var(--line);padding:14px 16px 12px}.panel-head-row.svelte-1qihpg4{align-items:center;gap:9px;display:flex}.hazard.svelte-1qihpg4{color:var(--danger);filter:drop-shadow(0 0 8px var(--danger-glow));font-size:15px}h2.svelte-1qihpg4{font-family:var(--mono);text-transform:uppercase;letter-spacing:.12em;color:var(--ink);margin:0;font-size:13px}.panel-sub.svelte-1qihpg4{color:var(--ink-faint);margin:9px 0 0;font-size:11.5px;line-height:1.55}.loading.svelte-1qihpg4{font-family:var(--mono);color:var(--ink-faint);padding:22px 16px;font-size:12px}.group.svelte-1qihpg4{border-bottom:1px solid var(--line);padding:14px 16px}.group-label.svelte-1qihpg4{font-family:var(--mono);text-transform:uppercase;letter-spacing:.18em;color:var(--ink-faint);align-items:center;gap:8px;margin-bottom:11px;font-size:10.5px;display:flex}.group-label--danger.svelte-1qihpg4{color:var(--danger-bright)}.group-tag.svelte-1qihpg4{letter-spacing:.1em;border:1px solid var(--line-strong);color:var(--ink-faint);border-radius:4px;padding:2px 6px;font-size:9.5px}.group-tag--danger.svelte-1qihpg4{border-color:var(--danger-deep);color:var(--danger-bright);background:#ff4d4d0f}.btn-row.svelte-1qihpg4{flex-wrap:wrap;gap:9px;display:flex}.vbtn.svelte-1qihpg4{border-radius:var(--radius-sm);letter-spacing:.05em;text-transform:lowercase;justify-content:center;align-items:center;gap:8px;padding:9px 15px;font-size:13px;font-weight:600;transition:filter .14s,border-color .14s,background .14s,transform 60ms;display:inline-flex}.vbtn.svelte-1qihpg4:active:not(:disabled){transform:translateY(1px)}.vbtn.svelte-1qihpg4:disabled{opacity:.4}.vbtn-label.svelte-1qihpg4{line-height:1}.vbtn--safe.svelte-1qihpg4{background:var(--bg-2);color:var(--ink);border:1px solid var(--line-strong)}.vbtn--safe.svelte-1qihpg4:hover:not(:disabled){border-color:var(--cyan-dim);background:var(--bg-3)}.danger-list.svelte-1qihpg4{flex-direction:column;gap:12px;display:flex}.danger-item.svelte-1qihpg4{border-radius:var(--radius-sm);border:1px solid #0000}.danger-item--headline.svelte-1qihpg4{border-color:var(--danger-deep);background:#ff4d4d0b;padding:11px}.vbtn--danger.svelte-1qihpg4{width:100%;color:var(--danger-bright);border:1px solid var(--danger-deep);border-left:3px solid var(--danger);text-shadow:0 0 12px var(--danger-glow);background:linear-gradient(#ff4d4d29,#ff4d4d12)}.vbtn--danger.svelte-1qihpg4:hover:not(:disabled){background:linear-gradient(180deg, var(--danger), var(--danger-bright));color:#1a0606;border-color:var(--danger-bright);text-shadow:none;filter:drop-shadow(0 4px 14px var(--danger-glow))}.vbtn--headline.svelte-1qihpg4{padding:12px 15px;font-size:14px}.headline-badge.svelte-1qihpg4{text-transform:uppercase;letter-spacing:.14em;background:var(--danger);color:#1a0606;border-radius:999px;padding:2px 7px;font-size:9px;font-weight:700}.danger-blurb.svelte-1qihpg4{color:var(--ink-faint);margin:7px 2px 0;font-size:11.5px;line-height:1.5}.danger-item--headline.svelte-1qihpg4 .danger-blurb:where(.svelte-1qihpg4){color:#f0b0b0}.confirm.svelte-1qihpg4{border:1px solid var(--danger);border-radius:var(--radius-sm);background:#ff4d4d1a;margin-top:10px;padding:11px 12px;animation:.16s ease-out svelte-1qihpg4-confirm-in}@keyframes svelte-1qihpg4-confirm-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.confirm-text.svelte-1qihpg4{color:#ffe0e0;margin-bottom:10px;font-size:12.5px;line-height:1.5;display:block}.confirm-text.svelte-1qihpg4 strong:where(.svelte-1qihpg4){color:#fff;font-family:var(--mono);text-transform:uppercase;letter-spacing:.04em}.confirm-actions.svelte-1qihpg4{gap:9px;display:flex}.confirm-yes.svelte-1qihpg4{border-radius:var(--radius-sm);border:1px solid var(--danger-bright);background:var(--danger);color:#1a0606;letter-spacing:.06em;text-transform:uppercase;flex:1;padding:9px;font-size:13px;font-weight:700;transition:filter .14s}.confirm-yes.svelte-1qihpg4:hover:not(:disabled){filter:brightness(1.12)}.confirm-no.svelte-1qihpg4{border-radius:var(--radius-sm);border:1px solid var(--line-strong);background:var(--bg-2);color:var(--ink-dim);letter-spacing:.04em;text-transform:uppercase;flex:1;padding:9px;font-size:13px;transition:border-color .14s,color .14s}.confirm-no.svelte-1qihpg4:hover:not(:disabled){border-color:var(--ink-faint);color:var(--ink)}.confirm-yes.svelte-1qihpg4:disabled,.confirm-no.svelte-1qihpg4:disabled{opacity:.5}.spin.svelte-1qihpg4{border:2px solid #e6edf340;border-top-color:var(--cyan);border-radius:50%;flex:none;width:13px;height:13px;animation:.7s linear infinite svelte-1qihpg4-spin}.spin--danger.svelte-1qihpg4{border-color:#ff4d4d4d;border-top-color:var(--danger-bright)}@keyframes svelte-1qihpg4-spin{to{transform:rotate(360deg)}}.out.svelte-1qihpg4{border-radius:var(--radius-sm);border:1px solid var(--line-strong);background:var(--bg-term);margin:14px 16px 16px;overflow:hidden}.out--ok.svelte-1qihpg4{border-color:var(--green-dim)}.out--fail.svelte-1qihpg4{border-color:var(--danger-deep)}.out-head.svelte-1qihpg4{border-bottom:1px solid var(--line);background:#ffffff05;justify-content:space-between;align-items:center;padding:8px 11px;display:flex}.out-verb.svelte-1qihpg4{font-family:var(--mono);color:var(--ink);letter-spacing:.04em;font-size:12px}.out-verb.svelte-1qihpg4:before{content:"$ pve ";color:var(--ink-faint)}.out-status.svelte-1qihpg4{font-family:var(--mono);text-transform:uppercase;letter-spacing:.1em;border:1px solid;border-radius:4px;padding:2px 7px;font-size:10.5px}.out-status--ok.svelte-1qihpg4{color:var(--green)}.out-status--fail.svelte-1qihpg4{color:var(--danger-bright)}.out-pre.svelte-1qihpg4{font-family:var(--mono);color:#c7d6e2;white-space:pre-wrap;overflow-wrap:anywhere;max-height:320px;margin:0;padding:11px 12px;font-size:12px;line-height:1.55;overflow-y:auto}.out-stderr-label.svelte-1qihpg4{font-family:var(--mono);text-transform:uppercase;letter-spacing:.16em;color:var(--danger-bright);padding:6px 12px 0;font-size:10px}.out-pre--stderr.svelte-1qihpg4{color:#f3b6b6}.out-pre--empty.svelte-1qihpg4{color:var(--ink-faint);font-style:italic}.block-error.svelte-1qihpg4{border:1px solid var(--danger-deep);border-left:3px solid var(--danger);border-radius:var(--radius-sm);color:#ffd5d5;background:#ff4d4d12;margin:14px 16px;padding:11px 13px;font-size:12.5px;line-height:1.5}.retry.svelte-1qihpg4{border:1px solid var(--danger-deep);color:var(--danger-bright);background:0 0;border-radius:5px;margin-left:8px;padding:3px 9px;font-size:11px}.retry.svelte-1qihpg4:hover{background:#ff4d4d1f}.shell.svelte-1n46o8q{flex-direction:column;max-width:1500px;height:100%;margin:0 auto;padding:0 18px 18px;display:flex}.rail.svelte-1n46o8q{border-bottom:1px solid var(--line);flex-wrap:wrap;justify-content:space-between;align-items:center;gap:16px;padding:16px 4px 14px;display:flex}.rail-title.svelte-1n46o8q{align-items:baseline;gap:12px;display:flex}.glyph.svelte-1n46o8q{filter:saturate(.85);font-size:19px;transform:translateY(2px)}h1.svelte-1n46o8q{font-family:var(--mono);letter-spacing:.02em;color:var(--ink);margin:0;font-size:19px;font-weight:600}.accent.svelte-1n46o8q{color:var(--cyan);text-shadow:0 0 18px #3dd1d659}.rail-tag.svelte-1n46o8q{font-family:var(--mono);text-transform:uppercase;letter-spacing:.22em;color:var(--ink-faint);border:1px solid var(--line-strong);border-radius:999px;padding:3px 9px;font-size:10.5px}.rail-status.svelte-1n46o8q{font-family:var(--mono);align-items:center;gap:14px;font-size:13px;display:flex}.rail-session.svelte-1n46o8q{white-space:nowrap;align-items:baseline;gap:7px;display:inline-flex}.session-label.svelte-1n46o8q{color:var(--ink-faint);text-transform:uppercase;letter-spacing:.16em;font-size:11px}.session-id.svelte-1n46o8q{color:var(--cyan);font-family:var(--mono);letter-spacing:.04em}.session-meta.svelte-1n46o8q{color:var(--amber);font-size:12px}.session-bad.svelte-1n46o8q{color:var(--danger-bright)}.dot.svelte-1n46o8q{background:var(--ink-faint);border-radius:50%;flex:none;width:9px;height:9px;box-shadow:0 0 #0000}.dot--ready.svelte-1n46o8q{background:var(--cyan);animation:3.4s ease-in-out infinite svelte-1n46o8q-breathe;box-shadow:0 0 10px 1px #3dd1d699}.dot--busy.svelte-1n46o8q{background:var(--amber);animation:1s ease-in-out infinite svelte-1n46o8q-pulse;box-shadow:0 0 10px 1px #f5b657b3}.dot--error.svelte-1n46o8q{background:var(--danger);box-shadow:0 0 10px 1px var(--danger-glow)}@keyframes svelte-1n46o8q-breathe{0%,to{opacity:.55}50%{opacity:1}}@keyframes svelte-1n46o8q-pulse{0%,to{opacity:.7;transform:scale(.82)}50%{opacity:1;transform:scale(1.15)}}.new-session.svelte-1n46o8q{background:var(--bg-2);color:var(--ink-dim);border:1px solid var(--line-strong);border-radius:var(--radius-sm);letter-spacing:.02em;padding:7px 13px;font-size:12px;transition:border-color .15s,color .15s,background .15s}.new-session.svelte-1n46o8q:hover:not(:disabled){border-color:var(--cyan-dim);color:var(--ink);background:var(--bg-3)}.new-session.svelte-1n46o8q:disabled{opacity:.45}.rail-error.svelte-1n46o8q{border:1px solid var(--danger-deep);color:#ffd5d5;border-radius:var(--radius-sm);background:#ff4d4d12;border-left-width:3px;margin:12px 0 0;padding:11px 14px;font-size:13px;line-height:1.5}.grid.svelte-1n46o8q{flex:1;grid-template-columns:minmax(0,1fr) 376px;gap:18px;min-height:0;padding-top:16px;display:grid}.col.svelte-1n46o8q{flex-direction:column;min-width:0;min-height:0;display:flex}@media (width<=940px){.grid.svelte-1n46o8q{grid-template-columns:1fr;grid-auto-rows:minmax(0,auto);overflow:auto}.col--chat.svelte-1n46o8q{min-height:60vh}}
diff --git a/app/breakglass/static/index.html b/app/breakglass/static/index.html
index 2c18803..a417b88 100644
--- a/app/breakglass/static/index.html
+++ b/app/breakglass/static/index.html
@@ -6,8 +6,8 @@
devvm breakglass
-
-
+
+
diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte
index f72cfe2..e1376d5 100644
--- a/frontend/src/App.svelte
+++ b/frontend/src/App.svelte
@@ -5,13 +5,14 @@
import VmControls from './VmControls.svelte';
// ── session lifecycle ────────────────────────────────────────────────────
- // sessionId is the id we POST with. The backend also reports an authoritative
- // id in the first {kind:"session"} frame of a turn; Chat bubbles that up so
- // the rail always shows what the agent is actually resuming.
let sessionId = $state('');
let sessionState = $state('connecting'); // connecting | ready | error
let sessionError = $state('');
- let streaming = $state(false); // a chat turn is in flight (drives the rail dot)
+ let streaming = $state(false);
+
+ // Mobile: the VM controls live in a slide-up sheet. Desktop: a side column
+ // (CSS hides the toggle and pins the sheet open as a column ≥900px).
+ let showControls = $state(false);
async function newSession() {
sessionState = 'connecting';
@@ -27,7 +28,6 @@
onMount(newSession);
- // Chat reports the live session id from the stream's session frame.
function onLiveSession(id) {
if (id) sessionId = id;
}
@@ -43,55 +43,75 @@
- Could not reach the breakglass backend — {sessionError}. The cluster or
- network may be down. The manual VM controls below still work independently
- of the chat agent.
+ Can't reach the breakglass backend — {sessionError}. The cluster or network
+ may be down. The ⚡ VM power controls still work without the chat.
{/if}
-
-
+
+ (streaming = v)}
/>
-
+
+
+
diff --git a/frontend/src/Chat.svelte b/frontend/src/Chat.svelte
index 2c6ada6..1a1d17a 100644
--- a/frontend/src/Chat.svelte
+++ b/frontend/src/Chat.svelte
@@ -443,14 +443,16 @@
flex: 1;
resize: none;
max-height: 168px;
- min-height: 44px;
+ min-height: 48px;
background: var(--bg-2);
color: var(--ink);
border: 1px solid var(--line-strong);
border-radius: var(--radius-sm);
- padding: 11px 13px;
+ padding: 12px 13px;
font-family: var(--sans);
- font-size: 14px;
+ /* 16px: anything smaller makes iOS Safari auto-zoom on focus (mobile is the
+ primary client) — the zoom then shifts the composer out of view. */
+ font-size: 16px;
line-height: 1.5;
outline: none;
transition: border-color 0.15s, box-shadow 0.15s;
diff --git a/frontend/src/app.css b/frontend/src/app.css
index 4d5352d..9e82129 100644
--- a/frontend/src/app.css
+++ b/frontend/src/app.css
@@ -55,6 +55,10 @@ html,
body {
margin: 0;
height: 100%;
+ /* The page itself never scrolls — the chat stream scrolls internally. This
+ keeps the composer pinned and stops iOS rubber-banding the whole UI. */
+ overflow: hidden;
+ overscroll-behavior: none;
}
body {
@@ -79,7 +83,11 @@ body {
}
#app {
- height: 100%;
+ /* 100dvh (dynamic viewport height) — NOT 100vh/100% — so the composer at the
+ bottom is never hidden behind a mobile browser's address/tool bar. Mobile is
+ the primary client for this tool. 100vh is the fallback for old engines. */
+ height: 100vh;
+ height: 100dvh;
}
button {