<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sparrow — webview console</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%23f0674a'/%3E%3Ctext x='16' y='23' font-size='20' text-anchor='middle' fill='%23fff'%3E%F0%9F%90%A6%3C/text%3E%3C/svg%3E">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root{
--bg:#0e0b08;--panel:#16120d;--panel2:#1b1611;--line:#2c251c;--line-hot:#3a3122;
--dim:#897d6c;--dimmer:#5c5346;--fg:#ece2cf;
--brand:#f2a93c;--coral:#f0674a;--agent:#4ec9b0;--planner:#6fa6e6;--verifier:#c9a14e;
--add:#74c258;--rem:#d96a63;--steel:#b9b0a3;--gold:#f2c94c;
--sup:#74c258;--tru:#f2a93c;--aut:#d96a63;
--ui-font:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Segoe UI",Roboto,Helvetica,Arial,sans-serif;
--mono-font:"SF Mono","IBM Plex Mono","Cascadia Code","JetBrains Mono",ui-monospace,Menlo,Consolas,monospace;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{font-family:var(--ui-font);color:var(--fg);min-height:100%;
background:radial-gradient(1100px 700px at 72% -12%,rgba(242,169,60,.13),transparent 60%),
radial-gradient(900px 600px at 10% 114%,rgba(240,103,74,.09),transparent 55%),var(--bg);
overflow:hidden;letter-spacing:.2px}
body::after{content:"";position:fixed;inset:0;pointer-events:none;opacity:.05;z-index:99;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")}
.ember{position:fixed;bottom:-10px;width:3px;height:3px;border-radius:50%;opacity:0;z-index:1;filter:blur(.4px);animation:drift linear infinite}
@keyframes drift{0%{transform:translateY(0)}12%{opacity:.5}88%{opacity:.35}100%{transform:translateY(-106vh) translateX(36px);opacity:0}}
.win{width:100vw;height:100dvh;background:linear-gradient(180deg,#14110c,#0f0c09);
border:1px solid var(--line);border-radius:0;overflow:hidden;display:grid;grid-template-rows:auto auto auto minmax(0,1fr) auto;
box-shadow:0 44px 130px -34px rgba(0,0,0,.86),0 0 0 1px rgba(242,169,60,.04),inset 0 1px 0 rgba(255,255,255,.03);
animation:rise .7s cubic-bezier(.2,.8,.2,1) both}
@keyframes rise{from{opacity:0;transform:translateY(14px) scale(.99)}to{opacity:1;transform:none}}
.chrome{display:flex;align-items:center;gap:9px;min-height:46px;padding:8px 15px;border-bottom:1px solid var(--line);
background:linear-gradient(180deg,#1a150f,#15110b)}
.dot{width:11px;height:11px;border-radius:50%}
.dot.r{background:#e15a52}.dot.y{background:#e0a93a}.dot.g{background:#62b85a}
.chrome .nm{margin-left:8px;color:var(--dim);font-size:12.5px}.chrome .nm b{color:var(--brand)}
.chrome .right{margin-left:auto;display:flex;gap:8px;align-items:center;justify-content:flex-end;min-width:0}
.btn{font-family:var(--mono-font);font-size:11px;letter-spacing:.5px;color:var(--brand);cursor:pointer;
background:color-mix(in srgb,var(--brand) 10%,transparent);border:1px solid color-mix(in srgb,var(--brand) 40%,transparent);
min-height:32px;padding:5px 12px;border-radius:7px;transition:.15s;display:inline-flex;align-items:center;justify-content:center}
.btn:hover{background:color-mix(in srgb,var(--brand) 22%,transparent)}
.btn.sm{font-size:10px;padding:5px 10px}
.cockpit{display:grid;grid-template-columns:minmax(190px,auto) minmax(220px,1fr) repeat(5,max-content) max-content;align-items:center;gap:0;padding:0 clamp(16px,2vw,30px);
border-bottom:1px solid var(--line);background:linear-gradient(180deg,#1b150d,#161108);font-size:12px;min-width:0;overflow:hidden}
.cmark{display:flex;align-items:center;gap:8px;padding:9px 16px 9px 0;border-right:1px solid var(--line);min-width:0}
.cmark svg{flex:0 0 auto}
.cmark .w,.word{font-weight:700;letter-spacing:11px;font-size:13px;
background:linear-gradient(90deg,#f2a93c,#f0674a,#f2a93c);background-size:220% 100%;
-webkit-background-clip:text;background-clip:text;color:transparent;
animation:wordin .6s ease 1.0s both,shimmer 3.2s linear 1.6s infinite}
@keyframes shimmer{to{background-position:220% 0}}
@keyframes wordin{from{opacity:0;letter-spacing:24px;filter:blur(6px)}to{opacity:1;letter-spacing:11px;filter:none}}
.logo-eye-open{transform-box:fill-box;transform-origin:center;animation:logo-blink 4.4s infinite}
@keyframes logo-blink{0%,93%,100%{transform:scaleY(1)}96.5%{transform:scaleY(.08)}}
.logo-tool{transform-box:fill-box;transform-origin:bottom center;animation:logo-tap 2.6s ease-in-out infinite}
@keyframes logo-tap{0%,100%{rotate:26deg}50%{rotate:34deg}}
.stat{display:flex;align-items:center;gap:7px;min-width:0;height:100%;padding:9px 12px;border-right:1px solid var(--line);color:var(--dim)}
.stat:last-child{border-right:none}
.stat .k{color:var(--dimmer);font-size:10px;letter-spacing:.7px;text-transform:uppercase}
.route-stat{overflow:hidden}
.route{display:block;min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--planner);font-weight:500}
.route .hop{color:var(--planner)}
.route .arrow{display:inline-block;margin:0 5px;color:var(--brand);animation:route-step 1.7s ease-in-out infinite}
.route .arrow:nth-of-type(2n){animation-delay:.28s}
@keyframes route-step{0%,100%{opacity:.45;transform:translateX(0)}50%{opacity:1;transform:translateX(2px)}}
.budget-track{width:44px;height:5px;border:1px solid var(--line);border-radius:999px;background:#0d0a07;overflow:hidden;flex-shrink:0}
.budget-fill{display:block;height:100%;width:0;background:var(--add);transition:width .4s ease}
.budget-fill.warn{background:var(--coral)}
.budget-fill.danger{background:var(--rem)}
.cost{color:var(--brand);font-weight:600;font-variant-numeric:tabular-nums;transition:color .3s,text-shadow .3s,transform .3s}
.cost.up{animation:flash-cost .55s ease-out}
@keyframes flash-cost{0%{filter:brightness(1.8) saturate(1.4);text-shadow:0 0 12px var(--brand);transform:translateY(-1px) scale(1.05)}100%{filter:none;text-shadow:none;transform:none}}
.tok{font-variant-numeric:tabular-nums;transition:color .3s,text-shadow .3s,transform .3s;display:inline-block}
.tok.up{color:var(--brand);animation:flash-tok .55s ease-out}
@keyframes flash-tok{0%{filter:brightness(1.6);text-shadow:0 0 10px var(--brand);transform:translateY(-1px)}100%{filter:none;text-shadow:none;transform:none}}
.stat .pill{transition:.4s}
.hero-providers-pulse{animation:pulse-providers 1.2s ease-out}
@keyframes pulse-providers{0%{transform:scale(1)}30%{transform:scale(1.15);color:var(--add);text-shadow:0 0 10px var(--add)}100%{transform:scale(1)}}
.pill{display:inline-flex;align-items:center;gap:6px;padding:2px 10px;border-radius:999px;font-size:10.5px;font-weight:600;letter-spacing:.5px;transition:.3s}
.pill .led{width:6px;height:6px;border-radius:50%}
.swarm-cockpit{display:flex;flex-wrap:wrap;gap:0;border-bottom:1px solid var(--line);
background:linear-gradient(180deg,#17120d,#120f0b);font-size:12px;
max-height:200px;overflow-y:auto}
.swarm-cockpit::-webkit-scrollbar{width:5px}
.swarm-cockpit::-webkit-scrollbar-thumb{background:#2c2419;border-radius:6px}
.lane{flex:1 1 180px;min-width:160px;max-width:240px;
display:flex;flex-direction:column;gap:2px;
padding:6px 12px;
border-right:1px solid var(--line);
border-bottom:1px solid color-mix(in srgb,var(--line) 60%, transparent);
animation:lanein .35s ease both}
.lane.idle-card .msg{display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden;font-size:10px;opacity:.7}
.lane.idle-card{padding:5px 10px;max-width:160px;min-width:120px}
.lane.idle-card .lane-head .who{font-size:10.5px}
.lane.working,.lane.done{flex:1 1 240px;max-width:340px}
.lane.working .msg,.lane.done .msg{-webkit-line-clamp:3;font-size:11px;opacity:1}
.lane .lane-head{display:flex;align-items:center;gap:8px}
.lane .who{font-weight:700;letter-spacing:.4px;font-size:11px}
.lane.planner .who{color:var(--planner)}
.lane.coder .who{color:var(--agent)}
.lane.verifier .who{color:var(--verifier)}
.lane.gold .who{color:var(--gold)}
.lane.coral .who{color:var(--coral)}
.lane.steel .who{color:var(--steel)}
.lane .st{display:flex;align-items:center;gap:6px;font-size:10.5px;color:var(--dim);margin-left:auto;white-space:nowrap}
.lane .msg{color:var(--dim);font-size:11px;line-height:1.45;
overflow-wrap:anywhere;word-break:break-word;white-space:normal}
.lane .msg b{color:var(--fg);font-weight:500}
.lane.working .st{color:var(--agent)}.lane.done .st{color:var(--add)}.lane.idle .st{color:var(--dimmer)}.lane.error .st{color:var(--rem)}
.lane.idle-card{opacity:.65}
.lane .led-mini{width:7px;height:7px;border-radius:50%;background:currentColor;box-shadow:0 0 9px currentColor}
.lane.working .led-mini{animation:pulse 1.3s ease-in-out infinite}
@keyframes lanein{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}
.lane-more{flex:0 0 auto;align-self:stretch;display:flex;align-items:center;justify-content:center;
padding:0 14px;font-size:10.5px;color:var(--brand);cursor:pointer;
background:color-mix(in srgb,var(--brand) 7%, transparent);
border-left:1px solid var(--line-hot);font-family:inherit}
.lane-more:hover{background:color-mix(in srgb,var(--brand) 14%, transparent)}
@media(max-width:820px){
.cockpit{grid-template-columns:1fr 1fr}
.cmark{grid-column:1/-1;border-right:none;border-bottom:1px solid var(--line)}
.route-stat{grid-column:1/-1;border-right:none;border-bottom:1px solid var(--line)}
.lane{flex:1 1 100%;border-right:none;border-bottom:1px solid var(--line)}
}
#term{min-height:0;overflow-y:auto;padding:18px clamp(18px,2vw,34px) 22px;font-size:13px;line-height:1.62;scroll-behavior:smooth}
#term::-webkit-scrollbar{width:9px}#term::-webkit-scrollbar-thumb{background:#2c2419;border-radius:6px}
.ln{white-space:pre-wrap;word-break:break-word;animation:in .18s ease}
@keyframes in{from{opacity:0;transform:translateY(3px)}to{opacity:1}}
.muted{color:var(--dim)}.dimd{color:var(--dimmer)}
.prompt{color:var(--brand)}.cmd{color:var(--fg)}
.ok{color:var(--add)}.warn{color:var(--verifier)}.err{color:var(--rem)}.acc{color:var(--brand)}
.planner{color:var(--planner)}.coder{color:var(--agent)}.verifier{color:var(--verifier)}
.tg{color:var(--planner)}.b{color:var(--fg);font-weight:500}
.cur,.cur2{display:inline-block;width:8px;height:15px;background:var(--brand);vertical-align:-2px;animation:bl 1s steps(1) infinite}
.cur2{background:var(--fg)}
.caret{display:inline-block;width:7px;height:13px;background:var(--agent);margin-left:2px;vertical-align:-2px;animation:bl 1.05s steps(1) infinite}
@keyframes bl{50%{opacity:0}}
.sp{display:inline-block;color:var(--brand)}
@keyframes spin{to{transform:rotate(360deg)}}
.spin-star{display:inline-block;animation:spin 1s steps(8) infinite;color:var(--brand)}
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.7)}}
.led-pulse{display:inline-block;width:7px;height:7px;border-radius:50%;animation:pulse 1.3s ease-in-out infinite;background:var(--agent)}
.verb{display:inline-block;color:var(--brand);min-width:68px;margin-left:4px}
.sec{color:var(--dimmer);font-size:10.5px;letter-spacing:2px;text-transform:uppercase;margin:14px 0 4px}
.home-panel{display:grid;grid-template-columns:minmax(280px,.9fr) minmax(360px,1.1fr);gap:0;margin-bottom:18px;
border:1px solid color-mix(in srgb,var(--brand) 52%,var(--line));border-radius:8px;overflow:hidden;
background:linear-gradient(180deg,rgba(242,169,60,.045),rgba(15,12,9,.44));box-shadow:inset 0 1px 0 rgba(255,255,255,.03)}
.home-left{display:grid;grid-template-columns:112px minmax(0,1fr);gap:18px;align-items:center;padding:22px 26px;border-right:1px solid var(--line)}
.hero-logo{width:112px;height:112px;filter:drop-shadow(0 0 18px rgba(242,169,60,.28));animation:logo-float 4.6s ease-in-out infinite}
@keyframes logo-float{0%,100%{transform:translateY(0)}50%{transform:translateY(-4px)}}
.home-title{color:var(--fg);font-weight:700;font-size:18px;margin-bottom:8px}
.home-sub{color:var(--dim);font-size:12.5px;line-height:1.55}
.home-sub b{color:var(--brand);font-weight:700;letter-spacing:8px}
.home-path{color:var(--dimmer);font-size:12px;margin-top:8px;white-space:normal;overflow-wrap:anywhere}
.home-right{display:grid;grid-template-rows:auto 1fr;padding:18px 22px;gap:12px}
.home-block+.home-block{border-top:1px solid color-mix(in srgb,var(--brand) 44%,var(--line));padding-top:12px}
.home-block h3{color:var(--brand);font-size:13px;letter-spacing:.7px;margin-bottom:7px}
.home-block p{color:var(--dim);font-size:12.5px;line-height:1.48;max-width:100%;overflow-wrap:anywhere}
@media(max-width:900px){
.home-panel{grid-template-columns:1fr}
.home-left{border-right:none;border-bottom:1px solid var(--line);grid-template-columns:84px 1fr;padding:18px}
.hero-logo{width:84px;height:84px}
}
.status{position:fixed;bottom:48px;right:16px;font-size:10px;color:var(--dimmer);z-index:10}
.status.on{color:var(--add)}.status.off{color:var(--rem)}
.input-bar{display:grid;grid-template-columns:auto minmax(0,1fr) minmax(190px,270px) auto;gap:8px;padding:8px clamp(16px,2vw,30px) 12px;border-top:1px solid var(--line);align-items:end;background:linear-gradient(180deg,#15110c,#100d09)}
.input-bar textarea{width:100%;min-height:32px;max-height:190px;background:var(--panel);border:1px solid var(--line);border-radius:7px;color:var(--fg);
font-family:inherit;font-size:12px;line-height:1.45;padding:7px 10px;outline:none;resize:none;overflow-y:auto}
.input-bar textarea:focus{border-color:var(--brand)}
.composer-wrap{position:relative;min-width:0}
.composer-wrap textarea{position:relative;z-index:1;background:transparent}
.composer-wrap::before{content:"";position:absolute;inset:0;border-radius:7px;background:var(--panel)}
.composer-ghost{position:absolute;inset:0;z-index:0;padding:7px 10px;border:1px solid transparent;border-radius:7px;
font-family:inherit;font-size:12px;line-height:1.45;white-space:pre-wrap;word-break:break-word;
overflow:hidden;pointer-events:none;color:transparent}
.composer-ghost .g-rest{color:var(--dimmer);opacity:.85}
html[data-view="focus"] .composer-ghost{font-size:15px;padding:11px 13px;border-radius:8px}
.input-bar button{font-family:inherit;font-size:11px;min-height:32px;padding:6px 14px;cursor:pointer}
.attach-btn{display:inline-flex;align-items:center;justify-content:center;width:34px;height:31px;border-radius:7px;color:var(--steel);
border:1px solid var(--line);background:var(--panel);cursor:pointer;transition:.15s}
.attach-btn:hover{color:var(--brand);border-color:color-mix(in srgb,var(--brand) 45%,var(--line))}
.attach-btn svg{width:15px;height:15px}
#fileInput{display:none}
.context-side{display:grid;gap:4px;min-width:0}
.context-label{display:flex;justify-content:space-between;gap:8px;color:var(--dimmer);font-size:10px;text-transform:uppercase;letter-spacing:.7px}
.context-label b{color:var(--steel);font-weight:500;text-transform:none;letter-spacing:0;font-variant-numeric:tabular-nums}
.context-track{height:5px;border:1px solid var(--line);border-radius:999px;background:#0d0a07;overflow:hidden}
.context-fill{display:block;height:100%;width:0;background:linear-gradient(90deg,var(--agent),var(--brand),var(--coral));transition:width .25s ease}
.attachments{grid-column:1/-1;display:none;gap:6px;align-items:center;flex-wrap:wrap;min-height:0}
.attachments.show{display:flex}
.file-chip{display:inline-flex;align-items:center;gap:7px;max-width:360px;padding:4px 8px;border:1px solid var(--line);border-radius:7px;background:rgba(185,176,163,.07);color:var(--dim);font-size:11px}
.file-chip span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.file-chip button{border:0;background:transparent;color:var(--rem);font-size:13px;padding:0;cursor:pointer}
.drop-zone{position:fixed;inset:0;z-index:80;display:none;place-items:center;background:color-mix(in srgb,var(--bg) 68%,transparent);backdrop-filter:blur(8px);border:2px dashed color-mix(in srgb,var(--brand) 70%,var(--line));color:var(--brand);font-size:15px;font-weight:700;letter-spacing:1.4px;text-transform:uppercase}
.drop-zone.show{display:grid}
@media(max-width:920px){
.input-bar{grid-template-columns:auto minmax(0,1fr) auto}
.context-side{grid-column:1/-1;grid-row:2}
.attachments{grid-row:3}
}
.win{
display:grid;
grid-template-columns:48px 280px minmax(0,1fr) auto;
grid-template-rows:auto 1fr;
height:100dvh;width:100vw;
margin:0;border-radius:0;border:none;
}
.win .chrome{grid-column:1 / -1;grid-row:1}
.win .rail{grid-column:1;grid-row:2}
.win .drawer{grid-column:2;grid-row:2}
.win .main{grid-column:3;grid-row:2;display:grid;grid-template-rows:auto auto minmax(0,1fr) auto auto;min-width:0;min-height:0}
.win .rightbar{grid-column:4;grid-row:2}
.rail{
background:#0c0907;border-right:1px solid var(--line);
display:flex;flex-direction:column;align-items:center;gap:3px;
padding:8px 0 8px;font-family:inherit;position:relative;overflow:hidden;
}
.rail-logo{
width:32px;height:32px;margin-bottom:6px;
filter:drop-shadow(0 0 6px rgba(242,169,60,.45))
}
.rail .ico{width:36px;height:36px;border-radius:8px;display:flex;align-items:center;justify-content:center;
color:var(--dim);font-size:14px;cursor:pointer;transition:.15s;position:relative;
background:transparent;border:none;font-family:inherit}
.rail .ico:hover{color:var(--fg);background:color-mix(in srgb,var(--brand) 8%,transparent)}
.rail .ico.active{color:var(--brand);background:color-mix(in srgb,var(--brand) 14%,transparent)}
.rail .ico.active::before{content:"";position:absolute;left:-1px;top:7px;bottom:7px;width:2px;background:var(--brand);border-radius:2px}
.rail .ico .badge{position:absolute;top:3px;right:2px;min-width:13px;height:13px;border-radius:7px;padding:0 4px;
background:var(--coral);color:#fff;font-size:8.5px;font-weight:700;
display:flex;align-items:center;justify-content:center}
.rail .sep{width:22px;height:1px;background:var(--line);margin:5px 0}
.rail .spacer{flex:1}
.drawer{
background:linear-gradient(180deg,var(--panel),color-mix(in srgb,var(--bg) 80%,var(--panel)));
border-right:1px solid var(--line);
overflow-y:auto;padding:14px 13px 18px;min-width:0;min-height:0;
animation:drawer-in .28s ease both;
}
@keyframes drawer-in{from{opacity:.35;transform:translateX(-8px)}to{opacity:1;transform:none}}
.drawer::-webkit-scrollbar{width:6px}
.drawer::-webkit-scrollbar-thumb{background:color-mix(in srgb,var(--line-hot) 70%,var(--bg));border-radius:6px}
.drawer h3{font-size:9.5px;letter-spacing:1.7px;text-transform:uppercase;color:var(--dimmer);
margin:14px 4px 8px;font-weight:700;display:flex;align-items:center;gap:6px}
.drawer h3:first-of-type{margin-top:0}
.drawer h3 .count{margin-left:auto;background:color-mix(in srgb,var(--brand) 14%,transparent);
color:var(--brand);padding:1px 6px;border-radius:4px;font-size:9px;letter-spacing:.8px}
.drawer .panel{display:none !important;padding:6px 4px}
.drawer .panel.active{display:block !important;border-left:2px solid var(--brand);background:color-mix(in srgb,var(--brand) 5%, transparent)}
.drawer .panel + .panel{margin-top:6px}
@keyframes fade{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}
.drw-row{display:flex;flex-direction:column;gap:3px;padding:8px 10px;border-radius:7px;
background:var(--panel2);border:1px solid transparent;cursor:pointer;transition:.15s;margin-bottom:5px}
.drw-row:hover{border-color:var(--line-hot)}
.drw-row.cur{border-color:color-mix(in srgb,var(--brand) 55%,var(--line));background:color-mix(in srgb,var(--brand) 10%,var(--panel))}
.drw-row{overflow:hidden}
.drw-row .ttl{font-size:11.5px;color:var(--fg);font-weight:500;display:block;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
.drw-row .ttl .led{display:inline-block;vertical-align:middle;margin-right:6px}
.drw-row .ttl .led{width:6px;height:6px;border-radius:50%;background:var(--add);box-shadow:0 0 6px var(--add)}
.drw-row .meta{font-size:10px;color:var(--dim);display:flex;gap:8px;flex-wrap:wrap}
.drw-row .meta b{color:var(--fg);font-weight:500}
.drw-empty{color:var(--dimmer);font-size:11px;font-style:italic;padding:8px 10px;text-align:center}
.mem-doc{padding:8px 10px;border-radius:7px;background:var(--panel2);font-size:11px;margin-bottom:5px}
.mem-doc .label{color:var(--dim);font-size:9.5px;letter-spacing:.7px;text-transform:uppercase;
margin-bottom:5px;display:flex;align-items:center}
.mem-doc .label b{margin-left:auto;color:var(--brand);font-variant-numeric:tabular-nums;
font-weight:600;letter-spacing:0;text-transform:none}
.mem-doc .bar{height:4px;border-radius:3px;background:color-mix(in srgb,var(--line-hot) 50%,var(--bg));
overflow:hidden;margin-top:3px}
.mem-doc .bar > span{display:block;height:100%;background:linear-gradient(90deg,var(--brand),var(--coral));
border-radius:3px;transition:width .5s ease}
.sec-score{display:flex;align-items:center;gap:10px;padding:10px;border-radius:8px;
background:var(--panel2);margin-bottom:8px}
.sec-score .num{font-size:24px;font-weight:700;font-variant-numeric:tabular-nums;color:var(--add)}
.sec-score .num.warn{color:var(--verifier)}.sec-score .num.crit{color:var(--rem)}
.sec-finding{padding:7px 9px;border-radius:6px;background:var(--panel2);font-size:10.5px;
margin-bottom:4px;border-left:3px solid var(--dimmer)}
.sec-finding.warn{border-left-color:var(--verifier)}
.sec-finding.crit{border-left-color:var(--rem)}
.sec-finding .cat{color:var(--dim);text-transform:uppercase;letter-spacing:.6px;font-size:8.5px}
.sec-finding .msg{color:var(--fg);margin-top:2px;overflow-wrap:anywhere}
@media(max-width:1100px){
.win{grid-template-columns:48px 240px minmax(0,1fr) auto}
}
@media(max-width:980px){
.win{grid-template-columns:48px 0 minmax(0,1fr) auto}
.drawer{position:fixed;top:42px;left:48px;bottom:0;width:280px;z-index:25;
transform:translateX(-100%);transition:transform .25s cubic-bezier(.2,.8,.2,1);
border-right:1px solid var(--line-hot)}
body.drawer-open .drawer{transform:translateX(0);box-shadow:0 12px 40px -8px rgba(0,0,0,.6)}
}
@media(max-width:720px){
.win{grid-template-columns:42px 0 minmax(0,1fr) auto}
.rail{width:42px}.rail .ico{width:32px;height:32px;font-size:13px}
.rail-logo{width:28px;height:28px}
body.drawer-open .drawer{width:min(280px,calc(100vw - 42px))}
}
@media(max-width:640px){
.win{grid-template-columns:42px minmax(0,1fr)}
.win .main{grid-column:2;grid-template-rows:auto auto minmax(0,1fr) auto auto}
.win .rightbar{grid-column:2}
.cockpit{display:flex;gap:0;overflow-x:auto;overscroll-behavior-x:contain;padding:0 8px}
.cockpit::-webkit-scrollbar{height:4px}
.cockpit::-webkit-scrollbar-thumb{background:var(--line-hot);border-radius:999px}
.cmark,.route-stat,.stat{flex:0 0 auto;min-height:40px;border-bottom:0}
.cmark{min-width:150px}
.route-stat{min-width:210px}
.stat{padding:8px 10px}
.swarm-cockpit{max-height:128px}
.lane{flex:1 1 100%;min-width:100%;max-width:none;border-right:0}
.input-bar{grid-template-columns:auto minmax(0,1fr) auto;padding:8px 10px 10px}
.context-side{display:none}
}
.rightbar{
width:0;overflow:hidden;min-height:0;
background:linear-gradient(180deg,var(--panel),color-mix(in srgb,var(--bg) 80%,var(--panel)));
border-left:1px solid var(--line);
transition:width .28s cubic-bezier(.2,.8,.2,1);
}
body.rightbar-open .rightbar{width:clamp(300px,26vw,380px)}
.rb-inner{width:clamp(300px,26vw,380px);height:100%;display:flex;flex-direction:column;min-height:0}
.rb-head{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid var(--line);flex:0 0 auto}
.rb-head .rb-title{flex:1;font-size:11px;letter-spacing:1.6px;text-transform:uppercase;color:var(--dim);font-weight:700;
overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rb-head button{border:none;background:transparent;color:var(--dimmer);cursor:pointer;font-size:14px;
min-width:32px;min-height:32px;padding:5px 8px;border-radius:6px;font-family:inherit;transition:.12s;line-height:1}
.rb-head button:hover{color:var(--fg);background:var(--panel2)}
.rb-head .rb-pin.on{color:var(--brand)}
.rb-body{flex:1;overflow-y:auto;min-height:0;padding:10px 12px}
.rb-body::-webkit-scrollbar{width:6px}
.rb-body::-webkit-scrollbar-thumb{background:color-mix(in srgb,var(--line-hot) 70%,var(--bg));border-radius:6px}
.rb-foot{display:flex;align-items:center;gap:8px;padding:7px 12px;border-top:1px solid var(--line);
font-size:10px;color:var(--dimmer);flex:0 0 auto}
.rb-foot .rb-led{width:6px;height:6px;border-radius:50%;background:var(--dimmer);flex:0 0 auto}
.rb-foot.running .rb-led{background:var(--agent);box-shadow:0 0 7px var(--agent);animation:pulse 1.3s ease-in-out infinite}
.rb-foot .rb-time{margin-left:auto;font-variant-numeric:tabular-nums}
.rb-menu-item{display:flex;align-items:center;gap:10px;width:100%;padding:9px 11px;margin-bottom:4px;
border:1px solid transparent;border-radius:8px;background:transparent;color:var(--fg);
font-family:inherit;font-size:12.5px;cursor:pointer;text-align:left;transition:.13s}
.rb-menu-item:hover{background:var(--panel2);border-color:var(--line)}
.rb-menu-item .mi-ico{width:18px;text-align:center;color:var(--dim);font-size:13px;flex:0 0 auto}
.rb-menu-item .mi-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rb-menu-item .mi-badge{min-width:16px;height:16px;border-radius:8px;padding:0 5px;display:none;
align-items:center;justify-content:center;font-size:9px;font-weight:700;
background:color-mix(in srgb,var(--brand) 16%,transparent);color:var(--brand)}
.rb-menu-item .mi-badge.show{display:inline-flex}
.rb-menu-item .mi-badge.err{background:color-mix(in srgb,var(--rem) 16%,transparent);color:var(--rem)}
.rb-menu-item kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;
padding:1px 6px;font-family:inherit;font-size:9.5px;color:var(--dimmer);white-space:nowrap}
.rb-empty{color:var(--dimmer);font-size:11.5px;font-style:italic;padding:18px 8px;text-align:center;line-height:1.6}
.rb-row{display:flex;flex-direction:column;gap:3px;padding:8px 10px;border-radius:7px;
background:var(--panel2);border:1px solid transparent;margin-bottom:5px;transition:.13s}
.rb-row.click{cursor:pointer}
.rb-row.click:hover{border-color:var(--line-hot)}
.rb-row .r-top{display:flex;align-items:center;gap:8px;min-width:0}
.rb-row .r-name{flex:1;font-size:11.5px;color:var(--fg);font-weight:500;
overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rb-row .r-meta{font-size:10px;color:var(--dim);display:flex;gap:8px;flex-wrap:wrap;overflow-wrap:anywhere}
.rb-row .r-meta b{color:var(--fg);font-weight:500}
.rb-st{display:inline-flex;align-items:center;gap:5px;font-size:9.5px;font-weight:600;
letter-spacing:.6px;text-transform:uppercase;white-space:nowrap}
.rb-st::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}
.rb-st.running{color:var(--agent)}.rb-st.running::before{box-shadow:0 0 7px currentColor;animation:pulse 1.3s ease-in-out infinite}
.rb-st.completed{color:var(--add)}
.rb-st.failed{color:var(--rem)}
.rb-st.waiting{color:var(--gold)}
.rb-sec{font-size:9.5px;letter-spacing:1.6px;text-transform:uppercase;color:var(--dimmer);
font-weight:700;margin:12px 2px 7px}
.rb-sec:first-child{margin-top:0}
.rb-term-line{font-family:var(--mono-font);font-size:10.5px;line-height:1.5;padding:6px 9px;border-radius:6px;
background:var(--panel2);margin-bottom:5px;overflow-wrap:anywhere}
.rb-term-line .cmdline{color:var(--brand);font-weight:600}
.rb-term-line.err{border-left:2px solid var(--rem)}
.rb-term-line.err .cmdline{color:var(--rem)}
.rb-term-line .out{color:var(--dim);white-space:pre-wrap;display:block;max-height:130px;overflow-y:auto;margin-top:3px}
.rb-plan-step{display:flex;align-items:flex-start;gap:8px;padding:6px 9px;border-radius:6px;
background:var(--panel2);margin-bottom:4px;font-size:11.5px;line-height:1.45}
.rb-plan-step .ps-mark{flex:0 0 auto;width:15px;text-align:center}
.rb-plan-step.completed .ps-mark{color:var(--add)}
.rb-plan-step.in_progress .ps-mark{color:var(--agent)}
.rb-plan-step.pending .ps-mark{color:var(--dimmer)}
.rb-plan-step.cancelled{opacity:.5}.rb-plan-step.cancelled .ps-txt{text-decoration:line-through}
.rb-plan-step.completed .ps-txt{color:var(--dim)}
.rb-plan-step .ps-txt{color:var(--fg);overflow-wrap:anywhere}
.rb-objective{padding:9px 11px;border-radius:8px;margin-bottom:10px;font-size:11.5px;line-height:1.5;color:var(--fg);
background:color-mix(in srgb,var(--brand) 6%,var(--panel2));border:1px solid color-mix(in srgb,var(--brand) 25%,var(--line))}
.rb-objective .ob-label{font-size:9px;letter-spacing:1.5px;text-transform:uppercase;color:var(--dimmer);margin-bottom:3px}
.rb-preview-frame{width:100%;height:46vh;border:1px solid var(--line);border-radius:8px;background:#fff;margin-top:8px}
.rb-btn{font-family:inherit;font-size:10.5px;min-height:30px;padding:5px 10px;border-radius:6px;cursor:pointer;transition:.13s;
color:var(--brand);background:color-mix(in srgb,var(--brand) 9%,transparent);
border:1px solid color-mix(in srgb,var(--brand) 35%,transparent)}
.rb-btn:hover{background:color-mix(in srgb,var(--brand) 18%,transparent)}
@media(max-width:980px){
.rightbar{position:fixed;top:42px;right:0;bottom:0;width:min(340px,92vw);z-index:26;
transform:translateX(100%);transition:transform .25s cubic-bezier(.2,.8,.2,1);
border-left:1px solid var(--line-hot)}
body.rightbar-open .rightbar{width:min(340px,92vw);transform:translateX(0);
box-shadow:0 12px 40px -8px rgba(0,0,0,.6)}
.rb-inner{width:min(340px,92vw)}
}
.kbd-help{position:fixed;inset:0;z-index:4000;display:none;align-items:center;justify-content:center;
background:color-mix(in srgb,var(--bg) 70%,transparent);backdrop-filter:blur(6px);padding:20px}
.kbd-help.show{display:flex}
.kbd-help .kh-card{width:min(680px,94vw);max-height:84vh;overflow-y:auto;background:var(--panel);
border:1px solid var(--line-hot);border-radius:12px;box-shadow:0 30px 90px rgba(0,0,0,.5);padding:18px 22px}
.kbd-help h3{font-size:12px;letter-spacing:1.8px;text-transform:uppercase;color:var(--brand);margin-bottom:12px}
.kbd-help .kh-grid{display:grid;grid-template-columns:1fr 1fr;gap:4px 26px}
@media(max-width:640px){.kbd-help .kh-grid{grid-template-columns:1fr}}
.kbd-help .kh-sec{grid-column:1/-1;font-size:9.5px;letter-spacing:1.6px;text-transform:uppercase;
color:var(--dimmer);font-weight:700;margin:10px 0 4px}
.kbd-help .kh-row{display:flex;align-items:center;gap:10px;font-size:11.5px;color:var(--dim);padding:3px 0}
.kbd-help .kh-row .desc{flex:1}
.kbd-help kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;
padding:1px 7px;font-family:inherit;font-size:10.5px;color:var(--fg);white-space:nowrap}
.qa-chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
.qa-chips button{font-family:inherit;font-size:11.5px;padding:6px 12px;border-radius:999px;cursor:pointer;
color:var(--fg);background:color-mix(in srgb,var(--brand) 7%,transparent);
border:1px solid color-mix(in srgb,var(--brand) 30%,var(--line));transition:.15s}
.qa-chips button:hover{background:color-mix(in srgb,var(--brand) 16%,transparent);transform:translateY(-1px)}
:root[data-theme="white"]{
--bg:#ffffff;--panel:#ffffff;--panel2:#f7f7f8;
--line:#e5e7eb;--line-hot:#d1d5db;
--dim:#6b7280;--dimmer:#9ca3af;--fg:#111827;
--brand:#111827;--coral:#b91c1c;
--agent:#0f766e;--planner:#1d4ed8;--verifier:#92400e;
--add:#15803d;--rem:#b91c1c;--gold:#a16207;--steel:#4b5563;
--sup:#15803d;--tru:#a16207;--aut:#b91c1c;
}
:root[data-theme="white"] body{background:var(--bg)}
:root[data-theme="white"] body::after{opacity:.012}
:root[data-theme="white"] .ember{display:none!important}
:root[data-theme="white"] .win{background:#fff;box-shadow:none}
:root[data-theme="white"] .chrome,
:root[data-theme="white"] .cockpit{background:#fff}
:root[data-theme="white"] .swarm-cockpit{background:var(--panel2)}
:root[data-theme="white"] .input-bar,
:root[data-theme="white"] .focus-actions{background:#fff}
:root[data-theme="white"] .rail{background:#f9fafb}
:root[data-theme="white"] .drawer,
:root[data-theme="white"] .rightbar{background:#fff}
:root[data-theme="white"] .budget-track,
:root[data-theme="white"] .context-track{background:#f3f4f6}
:root[data-theme="white"] .hero .h-logo{filter:drop-shadow(0 0 8px rgba(17,24,39,.18))}
:root[data-theme="white"] .word,:root[data-theme="white"] .b-word{
background:linear-gradient(90deg,#111827,#4b5563,#111827);background-size:220% 100%;
-webkit-background-clip:text;background-clip:text}
:root[data-theme="white"] #term::-webkit-scrollbar-thumb,
:root[data-theme="white"] .swarm-cockpit::-webkit-scrollbar-thumb{background:#d1d5db}
:root[data-theme="white"] .ln code,:root[data-theme="white"] .code-block{color:#374151}
:root[data-theme="white"] .diff-panel{background:#fff}
:root[data-theme="paper"]{
--bg:#f3eee1;--panel:#e7e0cc;--panel2:#ded5bd;
--line:#c6bca3;--line-hot:#aea38a;
--dim:#6a604c;--dimmer:#9a907c;--fg:#2a2418;
--brand:#a65a1a;--coral:#a63a2a;
--agent:#2f7d67;--planner:#2e5a9c;--verifier:#7a5e2c;
--add:#3a7d2e;--rem:#a63a2a;--gold:#a6801a;--steel:#5e5547;
--sup:#3a7d2e;--tru:#a65a1a;--aut:#a63a2a;
}
:root[data-theme="paper"] body{
background:
radial-gradient(1100px 700px at 72% -12%, rgba(166,90,26,.10), transparent 60%),
radial-gradient(900px 600px at 10% 114%, rgba(166,58,42,.08), transparent 55%),
var(--bg);
}
:root[data-theme="paper"] body::after{opacity:.025}
:root[data-theme="paper"] .ember{opacity:.18!important}
:root[data-theme="paper"] .win{background:linear-gradient(180deg,#ede5cf,#e7e0cc)}
:root[data-theme="paper"] .chrome,
:root[data-theme="paper"] .cockpit{background:linear-gradient(180deg, color-mix(in srgb,var(--brand) 6%, var(--panel)), var(--panel))}
:root[data-theme="paper"] .swarm-cockpit{background:var(--panel)}
:root[data-theme="paper"] .input-bar{background:linear-gradient(180deg, color-mix(in srgb,var(--panel) 60%, var(--bg)), var(--panel))}
:root[data-theme="paper"] .rail{background:#ebe3cd}
:root[data-theme="paper"] .context-track{background:color-mix(in srgb,var(--line-hot) 42%,var(--bg));height:4px}
.chip-btn{font-family:inherit;font-size:11px;min-height:32px;padding:5px 10px;border-radius:6px;
background:transparent;color:var(--dim);border:1px solid var(--line);cursor:pointer;
display:inline-flex;align-items:center;justify-content:center;gap:5px;transition:.15s;letter-spacing:.3px;white-space:nowrap}
.chip-btn:hover{color:var(--fg);border-color:var(--line-hot);background:color-mix(in srgb,var(--brand) 5%, transparent)}
.chip-btn.solid{background:color-mix(in srgb,var(--brand) 9%,transparent);
border-color:color-mix(in srgb,var(--brand) 32%,transparent);color:var(--brand)}
.chip-btn.solid:hover{background:color-mix(in srgb,var(--brand) 20%,transparent)}
.live-tag{color:var(--add);display:inline-flex;align-items:center;gap:5px;font-size:11px;margin-left:4px}
.live-tag::before{content:"";width:6px;height:6px;border-radius:50%;background:var(--add);box-shadow:0 0 8px var(--add);animation:pulse 1.3s ease-in-out infinite}
.live-tag.offline{color:var(--dimmer)}
.live-tag.offline::before{background:var(--dimmer);box-shadow:none;animation:none}
.hero{display:flex;align-items:center;gap:18px;padding:10px 4px 16px;margin-bottom:8px;
border-bottom:1px solid color-mix(in srgb,var(--line) 60%, transparent);
animation:fade-in .45s ease both}
.hero .h-logo{flex:0 0 auto;filter:drop-shadow(0 0 14px rgba(242,169,60,.42))}
:root[data-theme="paper"] .hero .h-logo{filter:drop-shadow(0 0 10px rgba(166,90,26,.32))}
.hero .h-text{flex:1;min-width:0}
.hero .hi{font-size:15px;color:var(--fg);font-weight:500;margin-bottom:4px}
.hero .hi b{background:linear-gradient(90deg,var(--brand),var(--coral));-webkit-background-clip:text;background-clip:text;color:transparent;font-weight:700;letter-spacing:1.5px}
.hero .tag{color:var(--dim);font-size:12px;letter-spacing:.7px;margin-bottom:7px}
.hero .meta{display:flex;gap:14px;flex-wrap:wrap;color:var(--dimmer);font-size:10.5px;letter-spacing:.4px}
.hero .meta span b{color:var(--fg);font-weight:500}
.hero .meta .ok-dot{color:var(--add)}
@keyframes fade-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
.composer-hints{display:flex;align-items:center;gap:14px;font-size:10px;color:var(--dimmer);
letter-spacing:.3px;padding:0 clamp(16px,2vw,30px) 4px}
.composer-hints .kbds{margin-left:auto;display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.composer-hints kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;
padding:1px 5px;font-family:inherit;font-size:10px;color:var(--dim)}
.route-hop{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid var(--line);transition:.15s}
.route-hop:last-child{border-bottom:none}
.route-hop.active{background:color-mix(in srgb,var(--planner) 7%,transparent)}
.route-hop .rh-led{width:8px;height:8px;border-radius:50%;flex:0 0 auto}
.route-hop.active .rh-led{background:var(--add);box-shadow:0 0 8px var(--add);animation:pulse 1.3s ease-in-out infinite}
.route-hop.standby .rh-led{background:var(--dimmer)}
.route-hop.error .rh-led{background:var(--rem)}
.route-hop .rh-info{flex:1;min-width:0}
.route-hop .rh-name{font-size:11.5px;color:var(--fg);font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.route-hop .rh-name.active{color:var(--planner)}
.route-hop .rh-meta{font-size:9.5px;color:var(--dimmer);margin-top:2px;display:flex;gap:8px}
.route-hop .rh-ctx{color:var(--agent)}.route-hop .rh-cost{color:var(--brand)}
.route-hop .rh-pos{font-size:10px;color:var(--dimmer);font-weight:600}
.route-current-box{padding:10px 12px;border-bottom:1px solid var(--line);background:color-mix(in srgb,var(--brand) 5%,transparent)}
.route-current-box .rc-label{font-size:9px;text-transform:uppercase;letter-spacing:1.5px;color:var(--dimmer);margin-bottom:4px}
.route-current-box .rc-model{font-size:13px;font-weight:600;color:var(--brand)}
.route-current-box .rc-ctx{font-size:10px;color:var(--agent);margin-top:3px}
.mp-drop{position:absolute;bottom:calc(100% + 8px);left:0;z-index:200;
background:var(--panel);border:1px solid var(--line-hot);border-radius:12px;
box-shadow:0 24px 64px -12px rgba(0,0,0,.8);width:340px;overflow:hidden;
animation:in .14s ease}
.mp-drop-head{padding:9px 14px 7px;font-size:9px;letter-spacing:2px;text-transform:uppercase;
color:var(--dimmer);border-bottom:1px solid var(--line)}
.mp-section{padding:4px 0}
.mp-provider{padding:4px 14px 2px;font-size:9px;letter-spacing:1.8px;text-transform:uppercase;color:var(--dimmer)}
.mp-item{display:flex;align-items:center;gap:10px;padding:6px 14px;cursor:pointer;transition:.12s}
.mp-item:hover{background:color-mix(in srgb,var(--brand) 8%,transparent)}
.mp-item.selected{background:color-mix(in srgb,var(--brand) 12%,transparent)}
.mp-item.selected .mp-name{color:var(--brand)}
.mp-name{font-size:11.5px;color:var(--fg);font-weight:500;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mp-ctx{font-size:9.5px;color:var(--dimmer);white-space:nowrap}
.mp-cost{font-size:9px;color:var(--agent);white-space:nowrap}
.mp-rec{font-size:8px;color:var(--brand);border:1px solid color-mix(in srgb,var(--brand) 40%,transparent);border-radius:999px;padding:0 5px;letter-spacing:.5px}
.mp-auto{padding:6px 14px;border-top:1px solid var(--line);font-size:10.5px;color:var(--dim);cursor:pointer;transition:.12s}
.mp-auto:hover{color:var(--fg);background:color-mix(in srgb,var(--brand) 5%,transparent)}
.boot-overlay{position:fixed;inset:0;background:radial-gradient(900px 600px at 50% 50%, color-mix(in srgb,var(--brand) 15%, var(--bg)), var(--bg) 70%);
z-index:5000;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;
animation:fade-in .25s ease both}
.boot-overlay[hidden]{display:none!important}
.boot-overlay.fading{animation:fade-out .35s ease forwards}
@keyframes fade-out{to{opacity:0;visibility:hidden}}
.boot-overlay .b-logo{filter:drop-shadow(0 0 24px rgba(242,169,60,.45));animation:bob 2.4s ease-in-out infinite}
@keyframes bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-4px)}}
.boot-overlay .b-word{font-weight:700;letter-spacing:14px;font-size:18px;
background:linear-gradient(90deg,var(--brand),var(--coral),var(--brand));background-size:220% 100%;
-webkit-background-clip:text;background-clip:text;color:transparent;
animation:b-wordin .6s ease both, shimmer 2.6s linear .8s infinite}
@keyframes b-wordin{from{opacity:0;letter-spacing:30px;filter:blur(8px)}to{opacity:1;letter-spacing:14px;filter:none}}
@keyframes shimmer{to{background-position:220% 0}}
.boot-overlay .b-tag{color:var(--dim);font-size:12px;letter-spacing:1.2px;animation:fade-in .35s ease .35s both}
.boot-overlay .b-status{display:flex;flex-direction:column;gap:3px;font-size:11px;color:var(--dim);
min-width:280px;margin-top:8px}
.boot-overlay .b-line{opacity:0;animation:fade-in .25s ease forwards;display:flex;gap:8px}
.boot-overlay .b-line .l-key{color:var(--dimmer);letter-spacing:.4px;min-width:80px}
.boot-overlay .b-line .l-val{color:var(--fg)}
.boot-overlay .b-skip{position:absolute;bottom:24px;color:var(--dimmer);font-size:10px;letter-spacing:.5px}
.boot-overlay .b-skip kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;
padding:1px 5px;color:var(--dim);font-family:inherit;font-size:10px}
.tool-card{margin:6px 0 8px;border:1px solid var(--line-hot);border-radius:8px;background:var(--panel);overflow:hidden}
.tool-card summary{list-style:none;cursor:pointer;padding:7px 11px;display:flex;align-items:center;gap:8px;font-size:11px;color:var(--dim)}
.tool-card summary::-webkit-details-marker{display:none}
.tool-card summary .g{color:var(--brand);font-size:12px}
.tool-card summary .nm{color:var(--fg);font-weight:500;font-size:11.5px}
.tool-card summary .ok{margin-left:auto;color:var(--add);font-size:10.5px}
.tool-card summary .chev{color:var(--dimmer);transition:transform .2s;font-size:9px}
.tool-card[open] summary .chev{transform:rotate(90deg)}
.tool-card[open] summary{border-bottom:1px solid var(--line)}
.tool-card.running summary .g{display:inline-block;animation:spin .7s linear infinite}
.tool-card.running summary .ok{color:var(--brand);animation:blink-ok 1.1s ease-in-out infinite}
@keyframes blink-ok{0%,100%{opacity:.55}50%{opacity:1}}
.tool-card.done summary .ok{animation:flash-ok .5s ease-out}
.tool-card.error summary .ok{animation:flash-ok .5s ease-out;color:var(--rem)}
@keyframes flash-ok{0%{filter:brightness(2.5) saturate(2)}100%{filter:none}}
.tool-card .det{padding:9px 12px;font-size:11px;color:var(--dim);background:var(--panel2);animation:fold-in .18s ease both}
.tool-card .det code{color:var(--fg);background:transparent;font-family:inherit;display:block;white-space:pre-wrap;line-height:1.5;overflow-wrap:anywhere}
.tool-card .det .lbl{color:var(--dimmer);text-transform:uppercase;font-size:9px;letter-spacing:1.2px;margin-bottom:3px}
@keyframes fold-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}
.diff-card{margin:6px 0 8px;border:1px solid var(--line-hot);border-radius:8px;background:var(--panel);overflow:hidden}
.diff-card .h{display:flex;gap:8px;padding:7px 11px;border-bottom:1px solid var(--line);font-size:10.5px;color:var(--dim)}
.diff-card .h .p{color:var(--fg);font-weight:500}.diff-card .h .m{margin-left:auto}
.diff-card .h .pl{color:var(--add)}.diff-card .h .mn{color:var(--rem)}
.diff-card pre{font-size:11.5px;line-height:1.6;padding:6px 0;overflow-x:auto;font-family:inherit}
.diff-card .dl{display:block;padding:0 10px;border-left:2px solid transparent;white-space:pre}
.diff-card .num{display:inline-block;width:30px;color:var(--dimmer);user-select:none}
.diff-card .ctx{color:var(--dim)}
.diff-card .pls{color:var(--add);background:color-mix(in srgb,var(--add) 9%,transparent);border-left-color:var(--add)!important}
.diff-card .mns{color:var(--rem);background:color-mix(in srgb,var(--rem) 9%,transparent);border-left-color:var(--rem)!important}
.diff-card .hunk{border-top:1px solid var(--line)}
.diff-card .hunk:first-child{border-top:0}
.diff-card .hunk-head{display:flex;align-items:center;gap:8px;padding:5px 10px;background:color-mix(in srgb,var(--panel2) 70%,transparent);color:var(--dimmer);font-size:10px}
.diff-card .hunk-head .meta{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.diff-card .hunk-actions{display:flex;gap:5px}
.diff-card .hunk-actions button{font-family:inherit;font-size:9px;padding:2px 7px;border-radius:999px;border:1px solid var(--line);background:transparent;color:var(--dim);cursor:pointer}
.diff-card .hunk-actions button.accept{color:var(--add);border-color:color-mix(in srgb,var(--add) 45%,transparent)}
.diff-card .hunk-actions button.reject{color:var(--rem);border-color:color-mix(in srgb,var(--rem) 45%,transparent)}
.diff-card .hunk[data-state="accepted"] .hunk-head{background:color-mix(in srgb,var(--add) 10%,var(--panel2))}
.diff-card .hunk[data-state="rejected"] .hunk-head{background:color-mix(in srgb,var(--rem) 10%,var(--panel2))}
.diff-card .hunk[data-state="rejected"] .dl{opacity:.45}
.code-card,.code-block{margin:7px 0 8px 10px;border:1px solid color-mix(in srgb,var(--line-hot) 82%,transparent);
border-radius:10px;background:linear-gradient(180deg,color-mix(in srgb,var(--panel) 92%,transparent),color-mix(in srgb,var(--panel2) 96%,transparent));
overflow:hidden;box-shadow:0 14px 32px -28px rgba(0,0,0,.68),inset 0 1px 0 rgba(255,255,255,.045)}
.code-card summary,.code-block summary{list-style:none;cursor:pointer;padding:7px 10px;display:flex;align-items:center;gap:8px;
font-family:var(--ui-font);font-size:11px;color:var(--dim);background:linear-gradient(180deg,color-mix(in srgb,var(--fg) 4%,transparent),transparent)}
.code-card summary::-webkit-details-marker,.code-block summary::-webkit-details-marker{display:none}
.code-card .cc-icon,.code-block .cb-chev{color:var(--brand);transition:transform .18s ease;font-size:10px}
.code-card[open] summary .cc-icon,.code-block[open] summary .cb-chev{transform:rotate(90deg)}
.code-card .cc-lang,.code-block .cb-lbl{color:var(--fg);font-weight:700;font-size:11.5px;letter-spacing:.2px;text-transform:none}
.code-card .cc-meta,.code-block .cb-size{margin-left:auto;color:var(--dimmer);font-size:10px;font-variant-numeric:tabular-nums}
.code-card .cc-meta.live{color:var(--brand);animation:tokpulse .8s ease-in-out infinite}
.code-card .cc-copy{font-family:var(--ui-font);font-size:10px;font-weight:600;color:var(--brand);cursor:pointer;
background:color-mix(in srgb,var(--brand) 9%,transparent);border:1px solid color-mix(in srgb,var(--brand) 28%,transparent);
border-radius:999px;padding:3px 9px;transition:.15s}
.code-card .cc-copy:hover{background:color-mix(in srgb,var(--brand) 18%,transparent);transform:translateY(-1px)}
.code-card pre,.code-block .cb-body{margin:0;padding:10px 12px;background:color-mix(in srgb,var(--bg) 76%,#000);
border-top:1px solid var(--line);animation:fold-in .18s ease both;overflow-x:auto}
.code-card code,.code-block .cb-body code{font-family:var(--mono-font);font-size:12.5px;line-height:1.58;color:var(--steel);white-space:pre-wrap;tab-size:2}
.code-card code .syn-key{color:#ffb454;font-weight:650}
.code-card code .syn-str{color:#aad94c}
.code-card code .syn-num{color:#d2a6ff}
.code-card code .syn-com{color:#7f8c98;font-style:italic}
.code-card code .syn-fn{color:#7dcfff}
.code-card code .syn-type{color:#73d0ff}
.code-card code .syn-op{color:#f29e74}
.code-card code .syn-prop{color:#ffd580}
:root[data-theme="paper"] .code-card code{color:#3f3629;font-weight:520}
:root[data-theme="paper"] .code-card code .syn-key{color:#a94700;font-weight:700}
:root[data-theme="paper"] .code-card code .syn-str{color:#3f7800}
:root[data-theme="paper"] .code-card code .syn-num{color:#6d3db8}
:root[data-theme="paper"] .code-card code .syn-com{color:#786f61}
:root[data-theme="paper"] .code-card code .syn-fn{color:#006cae}
:root[data-theme="paper"] .code-card code .syn-type{color:#087d8d}
:root[data-theme="paper"] .code-card code .syn-op{color:#9b4317}
:root[data-theme="paper"] .code-card code .syn-prop{color:#8a5700}
:root[data-theme="paper"] .code-card,:root[data-theme="paper"] .code-block{box-shadow:0 18px 42px -34px rgba(64,42,12,.36),inset 0 1px 0 rgba(255,255,255,.55)}
:root[data-theme="paper"] .code-card pre,:root[data-theme="paper"] .code-block .cb-body{background:color-mix(in srgb,var(--panel) 82%,#fff)}
@keyframes fold-in{from{opacity:0;max-height:0}to{opacity:1;max-height:600px}}
.compact-banner{margin:8px 0;padding:7px 12px;border-radius:8px;
background:color-mix(in srgb,var(--brand) 9%,transparent);
border:1px solid color-mix(in srgb,var(--brand) 30%,transparent);
color:var(--dim);font-size:11px;display:flex;align-items:center;gap:9px}
.compact-banner .g{color:var(--brand);font-size:12px}
.compact-banner b{color:var(--fg);font-weight:500}
.compact-banner a{color:var(--brand);text-decoration:none;margin-left:auto;font-size:10.5px}
.compact-banner a:hover{text-decoration:underline}
.checkpoint-timeline{display:flex;align-items:center;gap:7px;margin:8px 0 10px 18px;min-height:20px;color:var(--dimmer);font-size:10px}
.checkpoint-timeline::before{content:"checkpoints";text-transform:uppercase;letter-spacing:1.4px;margin-right:3px}
.checkpoint-node{width:10px;height:10px;border-radius:50%;background:var(--brand);box-shadow:0 0 0 1px color-mix(in srgb,var(--brand) 45%,transparent),0 0 14px rgba(242,169,60,.35);animation:checkpoint-drop .25s ease both,checkpoint-ping .7s ease-out .05s 1}
@keyframes checkpoint-drop{from{opacity:0;transform:translateY(-8px) scale(.6)}to{opacity:1;transform:none}}
@keyframes checkpoint-ping{0%{box-shadow:0 0 0 0 rgba(242,169,60,.35)}100%{box-shadow:0 0 0 12px transparent}}
.skill-pop{display:inline-block;padding:2px 8px;border-radius:5px;
background:color-mix(in srgb,var(--gold) 12%,transparent);
border:1px solid color-mix(in srgb,var(--gold) 35%,transparent);
color:var(--gold);font-size:10.5px;font-weight:500;animation:pop-in .3s cubic-bezier(.2,.8,.2,1) both}
@keyframes pop-in{from{opacity:0;transform:scale(.7)}to{opacity:1;transform:scale(1)}}
.streaming{display:inline}
.streaming::after{content:"";display:inline-block;width:6px;height:11px;background:var(--agent);
margin-left:1px;vertical-align:-1px;animation:caret-blink 1.05s steps(1) infinite}
@keyframes caret-blink{50%{opacity:0}}
.palette{position:fixed;inset:0;align-items:flex-start;justify-content:center;
background:color-mix(in srgb,var(--bg) 60%,transparent);backdrop-filter:blur(6px);
z-index:1000;padding-top:14vh;display:none}
.palette.open{display:flex;animation:fadein .2s ease both}
@keyframes fadein{from{opacity:0}to{opacity:1}}
.palette .box{width:min(780px,94vw);background:var(--panel);border:1px solid var(--line-hot);
border-radius:12px;box-shadow:0 30px 80px -20px rgba(0,0,0,.75);overflow:hidden;
animation:popin .22s cubic-bezier(.2,.8,.2,1) both}
@keyframes popin{from{opacity:0;transform:translateY(-8px) scale(.98)}to{opacity:1;transform:none}}
.palette .pinput{display:flex;align-items:center;gap:10px;padding:14px 17px;border-bottom:1px solid var(--line)}
.palette .pinput svg{flex:0 0 auto;filter:drop-shadow(0 0 6px rgba(242,169,60,.45))}
.palette .pinput input{flex:1;background:transparent;border:none;outline:none;color:var(--fg);
font-family:inherit;font-size:14px;letter-spacing:.2px}
.palette .pinput input::placeholder{color:var(--dim)}
.palette .res-list{max-height:50vh;overflow-y:auto;padding:6px 6px 10px}
.palette .res-list::-webkit-scrollbar{width:6px}
.palette .res-list::-webkit-scrollbar-thumb{background:color-mix(in srgb,var(--line-hot) 70%,var(--bg));border-radius:6px}
.palette .res{display:grid;grid-template-columns:minmax(135px,190px) auto minmax(220px,1fr);align-items:center;gap:10px;padding:8px 12px;border-radius:7px;cursor:pointer;font-size:12.5px}
.palette .res.sel,.palette .res:hover{background:color-mix(in srgb,var(--brand) 12%,transparent)}
.palette .res .nm{color:var(--fg);font-weight:600;overflow-wrap:anywhere;word-break:break-word}
.palette .res .src{color:var(--dimmer);font-size:9px;letter-spacing:.8px;text-transform:uppercase;
padding:1px 6px;border-radius:4px;background:color-mix(in srgb,var(--line-hot) 60%,transparent)}
.palette .res.builtin .src{background:color-mix(in srgb,var(--brand) 18%,transparent);color:var(--brand)}
.palette .res.skill .src{background:color-mix(in srgb,var(--gold) 18%,transparent);color:var(--gold)}
.palette .res.plugin .src{background:color-mix(in srgb,var(--agent) 18%,transparent);color:var(--agent)}
.palette .res .desc{color:var(--dim);font-size:11px;line-height:1.35;overflow-wrap:anywhere;word-break:break-word}
.palette .res .desc b{display:block;color:var(--fg);font-size:11px;font-weight:500;margin-bottom:2px}
.palette .res .desc .usage{display:block;color:var(--dimmer);font-size:10px;font-family:var(--mono-font);line-height:1.35}
@media(max-width:640px){.palette .res{grid-template-columns:1fr auto}.palette .res .desc{grid-column:1/-1}}
.palette .pfoot{display:flex;gap:14px;padding:8px 16px;border-top:1px solid var(--line);
font-size:10.5px;color:var(--dimmer);align-items:center}
.palette .pfoot kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;
padding:1px 5px;color:var(--dim);font-family:inherit;font-size:10px}
.palette .pempty{padding:18px;text-align:center;color:var(--dimmer);font-size:12px;font-style:italic}
.picker{position:absolute;background:var(--panel);border:1px solid var(--line-hot);
border-radius:9px;box-shadow:0 12px 40px -10px rgba(0,0,0,.6);
min-width:240px;max-width:340px;padding:5px;z-index:200;
animation:popin .18s cubic-bezier(.2,.8,.2,1) both}
.picker .head{font-size:9px;color:var(--dimmer);letter-spacing:1.4px;text-transform:uppercase;padding:5px 8px 7px}
.picker .opt{display:flex;align-items:center;gap:8px;padding:7px 9px;border-radius:6px;cursor:pointer;font-size:11.5px}
.picker .opt.sel,.picker .opt:hover{background:color-mix(in srgb,var(--brand) 12%,transparent)}
.picker .opt .at{color:var(--brand);font-weight:600}
.picker .opt .nm{color:var(--fg);font-weight:500}
.picker .opt .role{margin-left:auto;color:var(--dim);font-size:10px}
.picker.hidden{display:none}
.config-panel{display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:200;
background:var(--panel);border:1px solid var(--line);border-radius:14px;width:min(920px,94vw);max-height:88vh;
overflow-y:auto;padding:18px 20px;box-shadow:0 30px 80px rgba(0,0,0,.9)}
.cfg-provider{border:1px solid var(--line);border-radius:10px;margin:6px 0;background:var(--panel2);overflow:hidden;transition:.15s}
.cfg-provider.has-key{border-color:color-mix(in srgb,var(--add) 35%,var(--line))}
.cfg-provider summary{cursor:pointer;padding:10px 14px;display:flex;align-items:center;gap:10px;list-style:none;font-size:11.5px}
.cfg-provider summary::-webkit-details-marker{display:none}
.cfg-provider summary:hover{background:color-mix(in srgb,var(--brand) 5%,transparent)}
.cfg-provider .led{width:8px;height:8px;border-radius:50%;flex:0 0 auto}
.cfg-provider.has-key .led{background:var(--add);box-shadow:0 0 6px var(--add)}
.cfg-provider:not(.has-key) .led{background:var(--dimmer)}
.cfg-provider .lbl{font-weight:600;color:var(--fg);font-size:12px}
.cfg-provider .adp{font-size:9.5px;color:var(--dimmer);background:var(--bg);padding:1px 6px;border-radius:4px;letter-spacing:.5px}
.cfg-provider .count{margin-left:auto;font-size:10px;color:var(--dim)}
.cfg-provider .chev{color:var(--dimmer);transition:transform .2s;font-size:9px}
.cfg-provider[open] summary .chev{transform:rotate(90deg)}
.cfg-provider .body{padding:6px 14px 12px;border-top:1px solid var(--line);background:var(--bg)}
.cfg-provider .notes{color:var(--dim);font-size:10px;line-height:1.5;margin-bottom:8px;padding:6px 9px;border-radius:6px;background:color-mix(in srgb,var(--brand) 4%,transparent);border-left:2px solid color-mix(in srgb,var(--brand) 40%,transparent)}
.cfg-provider .models{display:flex;flex-direction:column;gap:4px;margin-top:6px}
.cfg-provider .mrow{display:flex;align-items:center;gap:8px;padding:5px 9px;border:1px solid var(--line);border-radius:6px;font-size:11px;background:var(--panel)}
.cfg-provider .mrow .mname{font-weight:500;color:var(--fg)}
.cfg-provider .mrow .mctx{font-size:9.5px;color:var(--agent);margin-left:auto}
.cfg-provider .mrow .mcost{font-size:9px;color:var(--brand)}
.cfg-provider .mrow .mstar{font-size:9px;color:var(--brand)}
.cfg-provider .mrow .mset{font-size:9px;padding:2px 7px;border-radius:5px;cursor:pointer;background:transparent;border:1px solid var(--line);color:var(--dim);transition:.12s;font-family:inherit}
.cfg-provider .mrow .mset:hover{color:var(--brand);border-color:color-mix(in srgb,var(--brand) 40%,var(--line))}
.cfg-provider .mrow.is-default{background:color-mix(in srgb,var(--add) 8%,var(--panel));border-color:color-mix(in srgb,var(--add) 35%,var(--line))}
.cfg-provider .mrow.is-default .mset{color:var(--add);border-color:color-mix(in srgb,var(--add) 40%,var(--line))}
.cfg-provider .keyrow{display:flex;gap:8px;align-items:center;margin-top:8px}
.cfg-provider .keyrow input{flex:1;background:var(--bg);border:1px solid var(--line);border-radius:6px;color:var(--fg);font-family:inherit;font-size:11px;padding:5px 8px;outline:none}
.cfg-provider .keyrow input:focus{border-color:var(--brand)}
.cfg-provider .keyrow button{font-family:inherit;font-size:10px;padding:5px 12px;border-radius:6px;cursor:pointer;background:color-mix(in srgb,var(--brand) 12%,transparent);border:1px solid color-mix(in srgb,var(--brand) 40%,transparent);color:var(--brand);transition:.12s}
.cfg-provider .keyrow button:hover{background:color-mix(in srgb,var(--brand) 22%,transparent)}
.cfg-provider .keyrow .envhint{font-size:9.5px;color:var(--dimmer);font-family:inherit}
.cfg-section{margin-top:14px;padding-top:12px;border-top:1px solid var(--line)}
.cfg-section h4{font-size:9px;text-transform:uppercase;letter-spacing:2px;color:var(--dimmer);margin-bottom:10px}
.cfg-tab-row{display:flex;gap:4px;margin-bottom:14px;border-bottom:1px solid var(--line)}
.cfg-tab{font-family:inherit;background:transparent;border:none;color:var(--dimmer);font-size:11px;padding:8px 14px;cursor:pointer;border-bottom:2px solid transparent;letter-spacing:.5px;transition:.15s}
.cfg-tab:hover{color:var(--dim)}
.cfg-tab.active{color:var(--brand);border-bottom-color:var(--brand)}
.cfg-search{width:100%;background:var(--bg);border:1px solid var(--line);border-radius:7px;color:var(--fg);font-family:inherit;font-size:11px;padding:7px 10px;outline:none;margin-bottom:8px}
.cfg-search:focus{border-color:var(--brand)}
.cfg-summary-row{display:flex;gap:14px;margin-bottom:10px;font-size:10px;color:var(--dim)}
.cfg-summary-row b{color:var(--brand);font-weight:600}
.config-panel.show{display:block;animation:in .2s ease}
.config-panel h3{color:var(--brand);font-size:14px;letter-spacing:2px;margin-bottom:14px}
.config-panel .row{display:flex;gap:8px;align-items:center;margin:6px 0}
.config-panel label{color:var(--dim);font-size:10px;text-transform:uppercase;letter-spacing:1px;min-width:70px}
.config-panel select,.config-panel input{background:var(--bg);border:1px solid var(--line);border-radius:6px;color:var(--fg);
font-family:inherit;font-size:11px;padding:5px 8px;flex:1;outline:none}
.config-panel select:focus,.config-panel input:focus{border-color:var(--brand)}
.config-panel .badge{font-size:9px;padding:3px 8px;border-radius:999px;letter-spacing:.5px}
.config-panel .badge.found{background:rgba(116,194,88,.15);color:var(--add);border:1px solid rgba(116,194,88,.3)}
.config-panel .badge.missing{background:rgba(217,106,99,.12);color:var(--rem);border:1px solid rgba(217,106,99,.25)}
.config-panel .tags{display:flex;gap:4px;flex-wrap:wrap;margin-top:4px}
.config-panel .tag{font-size:9px;padding:2px 7px;border-radius:999px;background:rgba(185,176,163,.1);color:var(--steel)}
.config-panel .presets{display:flex;gap:8px;margin-bottom:14px}
.config-panel .preset{cursor:pointer;padding:8px 14px;border:1px solid var(--line);border-radius:8px;font-size:11px;color:var(--dim);transition:.15s}
.config-panel .preset:hover,.config-panel .preset.sel{border-color:var(--brand);color:var(--brand);background:rgba(242,169,60,.08)}
.config-panel .close{position:absolute;top:10px;right:14px;cursor:pointer;color:var(--dim);font-size:16px}
.overlay{display:none;position:fixed;inset:0;z-index:199;background:rgba(0,0,0,.7)}
.overlay.show{display:block}
.token-meter{display:inline-flex;gap:10px;align-items:center;margin:3px 0 5px 18px;color:var(--dim);font-size:11px}
.token-meter b{font-weight:600;color:var(--steel);font-variant-numeric:tabular-nums}
.token-meter .in b{color:var(--brand)}
.token-meter .out b{color:var(--add)}
.token-meter.pulse b{animation:tokpulse .44s ease-out}
@keyframes tokpulse{0%{filter:brightness(1);transform:translateY(0)}35%{filter:brightness(1.7);transform:translateY(-1px)}100%{filter:brightness(1);transform:translateY(0)}}
.approval-actions{display:flex;gap:8px;margin:7px 0 10px 18px}
.approval-actions button{font-family:inherit;font-size:11px;padding:5px 12px;border-radius:7px;cursor:pointer}
.approval-actions .approve{color:var(--add);background:rgba(116,194,88,.1);border:1px solid rgba(116,194,88,.35)}
.approval-actions .deny{color:var(--rem);background:rgba(217,106,99,.1);border:1px solid rgba(217,106,99,.35)}
.diff-panel{position:fixed;top:0;right:0;bottom:0;width:min(480px,48vw);z-index:500;
background:var(--panel);border-left:1px solid var(--line-hot);
display:flex;flex-direction:column;
transform:translateX(100%);transition:transform .26s cubic-bezier(.2,.8,.2,1);
box-shadow:-20px 0 60px -12px rgba(0,0,0,.7)}
.diff-panel.open{transform:none}
.diff-panel-head{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid var(--line);flex:0 0 auto}
.diff-panel-head .dp-path{flex:1;font-size:11px;color:var(--fg);font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.diff-panel-head .dp-close{border:none;background:transparent;color:var(--dimmer);cursor:pointer;font-size:16px;padding:2px 6px;font-family:inherit;border-radius:4px;transition:.12s}
.diff-panel-head .dp-close:hover{color:var(--fg);background:var(--panel2)}
.diff-panel-body{flex:1;overflow:auto;padding:4px 0;font-size:11px;line-height:1.6;font-family:inherit}
.diff-panel-body::-webkit-scrollbar{width:6px}.diff-panel-body::-webkit-scrollbar-thumb{background:#2c2419;border-radius:6px}
.diff-panel-stats{padding:6px 14px;border-top:1px solid var(--line);font-size:10px;color:var(--dimmer);flex:0 0 auto;display:flex;gap:14px}
.approval-modal{position:fixed;inset:0;z-index:1100;display:none;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bg) 68%,transparent);backdrop-filter:blur(8px)}
.approval-modal.show{display:flex;animation:fadein .16s ease both}
.approval-card{width:min(520px,92vw);border:1px solid var(--line-hot);border-radius:12px;background:var(--panel);box-shadow:0 30px 90px rgba(0,0,0,.75);padding:18px;animation:popin .2s cubic-bezier(.2,.8,.2,1) both}
.approval-card h3{font-size:13px;color:var(--brand);letter-spacing:1.8px;text-transform:uppercase;margin-bottom:8px}
.approval-card p{font-size:12px;color:var(--dim);line-height:1.55;overflow-wrap:anywhere}
.approval-card .risk{display:inline-flex;margin:10px 0 14px;padding:2px 8px;border-radius:999px;border:1px solid var(--line);color:var(--gold);font-size:10px;text-transform:uppercase;letter-spacing:1px}
.approval-card .actions{display:flex;gap:10px;justify-content:flex-end}
.approval-card button{font-family:inherit;font-size:12px;border-radius:8px;padding:8px 14px;cursor:pointer}
.approval-card .approve{color:var(--add);background:rgba(116,194,88,.12);border:1px solid rgba(116,194,88,.4)}
.approval-card .deny{color:var(--rem);background:rgba(217,106,99,.12);border:1px solid rgba(217,106,99,.38)}
.run-summary{margin:10px 0;border:1px solid var(--line-hot);border-radius:10px;background:var(--panel);overflow:hidden;animation:in .2s ease}
.run-summary .rs-head{display:flex;align-items:center;gap:10px;padding:8px 13px;border-bottom:1px solid var(--line);font-size:10.5px}
.run-summary summary.rs-head{list-style:none;cursor:pointer}
.run-summary summary.rs-head::-webkit-details-marker{display:none}
.run-summary[open] .rs-head{border-bottom:1px solid var(--line)}
.run-summary .rs-head .rs-ok{color:var(--add);font-size:13px}
.run-summary .rs-head .rs-status{color:var(--fg);font-weight:500;flex:1}
.run-summary .rs-head .rs-dur{color:var(--dimmer);font-size:10px}
.run-summary .rs-grid{display:grid;grid-template-columns:repeat(4,1fr);padding:10px 13px;gap:6px}
.run-summary .rs-stat{display:flex;flex-direction:column;gap:2px}
.run-summary .rs-stat .k{font-size:9px;text-transform:uppercase;letter-spacing:1.2px;color:var(--dimmer)}
.run-summary .rs-stat .v{font-size:12px;font-weight:600;font-variant-numeric:tabular-nums}
.run-summary .rs-stat .v.cost{color:var(--brand)}
.run-summary .rs-stat .v.tok{color:var(--agent)}
.run-summary .rs-stat .v.files{color:var(--planner)}
.run-summary .rs-stat .v.checks{color:var(--gold)}
.error-banner{margin:8px 0;padding:9px 12px;border-radius:8px;background:rgba(217,106,99,.1);border:1px solid rgba(217,106,99,.32);color:var(--dim);font-size:11px;line-height:1.45}
.error-banner .title{color:var(--rem);font-weight:700;text-transform:uppercase;letter-spacing:1px;font-size:10px;margin-bottom:3px}
.error-banner .fix{color:var(--steel);margin-top:4px}
.error-banner a{color:var(--brand);text-decoration:none}
.learn-toast{animation:learnpop 4.2s ease}
@keyframes learnpop{0%{opacity:0;transform:translateY(5px)}10%{opacity:1;transform:none}82%{opacity:1}100%{opacity:0;transform:translateY(-5px)}}
.view-toggle{display:inline-flex;align-items:center;gap:4px;min-height:32px;border:1px solid var(--line);border-radius:8px;background:var(--panel);padding:2px}
.view-toggle button{font-family:inherit;font-size:10.5px;min-height:26px;border:0;border-radius:6px;background:transparent;color:var(--dim);padding:5px 10px;cursor:pointer}
.view-toggle button[aria-pressed="true"]{color:var(--brand);background:color-mix(in srgb,var(--brand) 16%,transparent)}
.focus-actions{display:none;align-items:center;justify-content:center;gap:10px;padding:10px clamp(16px,2vw,30px);border-top:1px solid var(--line);background:color-mix(in srgb,var(--panel) 88%,var(--bg))}
.focus-actions button{min-width:104px;font-size:12px;padding:7px 14px}
.focus-actions .ok-action{color:var(--add);background:color-mix(in srgb,var(--add) 12%,transparent);border-color:color-mix(in srgb,var(--add) 44%,var(--line))}
.focus-actions .undo-action{color:var(--rem);background:color-mix(in srgb,var(--rem) 10%,transparent);border-color:color-mix(in srgb,var(--rem) 44%,var(--line))}
.focus-actions .explain-action{color:var(--planner);background:color-mix(in srgb,var(--planner) 10%,transparent);border-color:color-mix(in srgb,var(--planner) 44%,var(--line))}
.mic-btn{display:inline-flex;align-items:center;justify-content:center;width:34px;height:31px;border-radius:7px;color:var(--steel);border:1px solid var(--line);background:var(--panel);cursor:pointer;transition:.15s}
.mic-btn:hover,.mic-btn.listening{color:var(--brand);border-color:color-mix(in srgb,var(--brand) 45%,var(--line))}
.mic-btn.listening{animation:abort-pulse 1.4s ease-in-out infinite}
.tour-pop{position:fixed;z-index:10000;max-width:min(300px,calc(100vw - 28px));border:1px solid color-mix(in srgb,var(--brand) 48%,var(--line));border-radius:8px;background:var(--panel);box-shadow:0 18px 60px rgba(0,0,0,.55);padding:12px;color:var(--fg);font-size:12px;line-height:1.45}
.tour-pop b{display:block;color:var(--brand);margin-bottom:4px;font-size:11px;text-transform:uppercase;letter-spacing:.8px}
.tour-pop .tour-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
.tour-pop button{font-family:inherit;font-size:11px;border-radius:7px;border:1px solid var(--line);background:transparent;color:var(--dim);padding:5px 10px;cursor:pointer}
.tour-pop button.primary{color:var(--brand);border-color:color-mix(in srgb,var(--brand) 40%,var(--line));background:color-mix(in srgb,var(--brand) 12%,transparent)}
html[data-view="focus"] .win{grid-template-columns:minmax(0,1fr)}
html[data-view="focus"] .rail,
html[data-view="focus"] .drawer,
html[data-view="focus"] .cockpit,
html[data-view="focus"] .swarm-cockpit,
html[data-view="focus"] #newConversation{display:none!important}
html[data-view="focus"] .win .main{grid-column:1;grid-row:2;grid-template-rows:minmax(0,1fr) auto auto auto}
html[data-view="focus"] .win .rightbar{grid-column:1;grid-row:2;justify-self:end;z-index:28}
html[data-view="focus"] #term{font-size:calc(16px * var(--read-scale,1));line-height:1.72;padding:28px max(22px,calc((100% - 860px)/2)) 24px}
html[data-view="focus"] .ln{max-width:860px;margin-left:auto;margin-right:auto}
html[data-view="focus"] .composer-hints{display:none}
html[data-view="focus"] .focus-actions{display:flex}
html[data-view="focus"] .input-bar{grid-template-columns:auto auto minmax(0,860px) auto;justify-content:center;padding:12px max(18px,calc((100% - 980px)/2)) 18px}
html[data-view="focus"] .context-side{display:none}
html[data-view="focus"] .input-bar textarea{min-height:46px;font-size:15px;padding:11px 13px;border-radius:8px}
@media(max-width:720px){
.chrome{align-items:flex-start;gap:6px;padding:7px 9px}
.chrome .right{margin-left:0;flex:1 1 auto;gap:5px;overflow-x:auto;flex-wrap:nowrap;max-width:calc(100vw - 42px);justify-content:flex-start;padding-bottom:2px}
.chrome .right::-webkit-scrollbar{height:4px}
.chrome .right::-webkit-scrollbar-thumb{background:var(--line-hot);border-radius:999px}
.chrome .nm{display:none}
.chip-btn,.btn,.view-toggle{flex:0 0 auto}
.chip-btn,.btn{min-height:36px}
.view-toggle{min-height:36px}
.view-toggle button{min-height:30px}
html[data-view="focus"] .input-bar{grid-template-columns:auto auto minmax(0,1fr) auto}
.focus-actions{gap:6px}.focus-actions button{min-width:0;flex:1;padding:7px 9px}
}
.boot-line{opacity:0;animation:fin .35s ease forwards}
@keyframes fin{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
.sp{display:inline-block;color:var(--brand);width:1.2em;text-align:center;animation:sp-dot .7s linear infinite}
@keyframes sp-dot{to{transform:rotate(360deg)}}
.tool-ico{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;font-size:11px;flex-shrink:0}
.inline-tok{display:inline-flex;gap:5px;font-size:10px;margin-left:7px;white-space:nowrap;font-variant-numeric:tabular-nums;vertical-align:middle;background:color-mix(in srgb,var(--brand) 6%,var(--bg));padding:0 5px;border-radius:4px}
.inline-tok .up-c{color:var(--add);animation:tk-pulse .35s ease-out}
.inline-tok .dn-c{color:var(--rem);animation:tk-pulse .35s ease-out}
@keyframes tk-pulse{0%{filter:brightness(1.6);text-shadow:0 0 6px currentColor}100%{filter:none;text-shadow:none}}
.bird{line-height:1.18;font-size:14px;margin:4px 0 6px;white-space:pre;font-family:'IBM Plex Mono',ui-monospace,monospace}
.bird .fr{color:var(--brand)} .bird .eye{color:#fff} .bird .patch{color:#1c150c;background:#1c150c;border-radius:2px;display:inline-block;min-width:1px}
.bird .beak{color:var(--coral)} .bird .tool{color:var(--steel)} .bird .gold{color:var(--gold)} .bird .belly{color:#f7d089}
.bird .brow{color:#5a4326} .bird .cheek{color:#f0674a}
.verb-think{font-style:italic;color:var(--dimmer);animation:verb-in .25s ease;margin-top:1px;font-size:11px}
@keyframes verb-in{from{opacity:0;transform:translateX(-3px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion: reduce){
*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}
.ember{display:none!important}
.streaming::after{animation:none!important;opacity:1}
.boot-overlay{display:none!important}
.caret,.cur,.cur2{animation:none!important;opacity:1}
.sp{animation:none!important}
}
.ln{white-space:pre-wrap;word-break:break-word;animation:in .18s ease;
padding:5px 0;line-height:1.62;max-width:900px;letter-spacing:0}
.ln+.ln{margin-top:4px}
.ln .user{color:var(--brand);font-weight:600;font-size:12px;letter-spacing:0}
.ln .agent{color:var(--agent);font-weight:600;font-size:12px;letter-spacing:0}
.ln .muted{color:var(--dimmer);font-weight:400;font-size:11px;letter-spacing:0}
.ln b{color:var(--fg);font-weight:600}
.ln > code:not(.md-ic),.code-block{font-family:var(--mono-font);font-size:12.5px;
background:var(--panel2);border-left:2px solid var(--brand);
padding:11px 13px;margin:12px 0;display:block;border-radius:0 7px 7px 0;
overflow-x:auto;line-height:1.55;color:var(--steel);letter-spacing:0}
.code-block .kw{color:var(--planner)}
.code-block .fn{color:var(--agent)}
.code-block .str{color:var(--add)}
.code-block .cm{color:var(--dimmer);font-style:italic}
.code-block .num{color:var(--gold)}
.code-block .ty{color:var(--coral)}
.diff-add{color:var(--add);background:rgba(116,194,88,.08);
display:block;padding:1px 8px;margin:1px 0;border-radius:3px}
.diff-del{color:var(--rem);background:rgba(217,106,99,.08);
display:block;padding:1px 8px;margin:1px 0;border-radius:3px}
.diff-hdr{color:var(--planner);font-weight:600;font-size:11px}
.cost-badge{display:inline-block;background:rgba(242,169,60,.12);
color:var(--brand);padding:1px 8px;border-radius:999px;
font-size:10.5px;font-weight:600;letter-spacing:.3px}
.tool-call{background:var(--panel);border:1px solid var(--line);
border-radius:9px;padding:10px 12px;margin:12px 0;max-width:900px}
.tool-call .tool-name{color:var(--planner);font-weight:700;font-size:11.5px;
letter-spacing:0;text-transform:uppercase}
.tool-call .tool-args{color:var(--dim);font-size:11.5px;line-height:1.5;font-family:var(--mono-font)}
.lane{padding:4px 10px;gap:1px}
.lane .msg{font-size:10.5px;line-height:1.4}
.lane .lane-head{gap:5px}
.lane .who{font-size:10.5px}
.lane .st{font-size:10px}
.approval-card{padding:14px!important}
.approval-card h3{font-size:12px!important;margin-bottom:5px!important}
.approval-card p{font-size:11px!important}
.approval-card button{font-size:11px!important;padding:6px 12px!important}
#term{padding:20px clamp(24px,2.6vw,34px) 22px;font-size:15px;line-height:1.62;letter-spacing:0}
html[data-view="focus"] #term{font-size:calc(15px * var(--read-scale,1));line-height:1.62;padding:26px max(26px,calc((100% - 920px)/2)) 24px}
html[data-view="focus"] .ln{max-width:920px}
.ln{padding:5px 0;min-height:unset;line-height:1.62;max-width:900px;letter-spacing:0}
.ln+.ln{border-top:none}
.ln .prompt{color:var(--brand);font-weight:600;font-size:12px;letter-spacing:0}
.ln .cmd{color:var(--fg);font-weight:400;font-size:15px;line-height:1.62;letter-spacing:0}
.ln .muted{color:var(--dimmer);font-size:11px;line-height:1.45;letter-spacing:0}
.tool-card{margin:12px 0;border-radius:9px}
.tool-card summary{min-height:38px;padding:0 12px;font-size:13px;letter-spacing:0}
.tool-card summary .nm{font-size:13px}
.tool-card summary .ok{font-size:11px}
.tool-card .det{padding:10px 12px;font-size:11.5px;line-height:1.5}
.code-card,.code-block{margin:12px 0;border-radius:9px}
.code-card summary,.code-block summary{min-height:34px;padding:0 10px;font-size:11.5px;letter-spacing:0}
.code-card pre,.code-block .cb-body{padding:11px 13px;font-size:12.5px;max-height:340px;overflow:auto}
.code-card code,.code-block .cb-body code{font-size:12.5px;line-height:1.55;letter-spacing:0}
.code-card:not([open]){margin:8px 0;padding:0}
.diff-card{margin:10px 0;font-size:12px}
.diff-card pre{padding:9px 11px;font-size:12px;line-height:1.45;max-height:260px;overflow:auto}
.verbose{font-size:11px;line-height:1.35;color:var(--dimmer);padding:2px 0;border:none;opacity:.78}
.verbose-tick{font-size:11px;line-height:1.35;margin:3px 0;opacity:.72}
.agent-activity{font-size:11px;line-height:1.35;padding:1px 7px;margin-left:7px}
.lane.idle-card{display:none}
.lane{flex:0 0 auto;min-width:unset;max-width:unset;padding:2px 6px}
.lane.working,.lane.done{flex:0 0 auto;max-width:200px}
</style>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<defs>
<linearGradient id="sp-body" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#f8bf57"/><stop offset="1" stop-color="#ef9b2f"/>
</linearGradient>
<linearGradient id="sp-wing" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#e8901f"/><stop offset="1" stop-color="#d2761d"/>
</linearGradient>
<linearGradient id="sp-belly" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#fde6bd"/><stop offset="1" stop-color="#f7d089"/>
</linearGradient>
<linearGradient id="sp-steel" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#cdc6ba"/><stop offset="1" stop-color="#8f877a"/>
</linearGradient>
<symbol id="sparrow" viewBox="0 0 240 240">
<path d="M150 150 q58 6 60 52 q-46 -2 -64 -28 z" fill="url(#sp-wing)"/>
<path d="M120 50 C168 50 182 92 182 132 C182 184 154 204 120 204
C86 204 58 184 58 132 C58 92 72 50 120 50 Z"
fill="url(#sp-body)" stroke="#7a4f1c" stroke-width="3.5"/>
<path d="M112 52 q-4 -22 8 -28 q3 12 0 27 z" fill="url(#sp-wing)" stroke="#7a4f1c" stroke-width="2.4"/>
<path d="M122 50 q6 -20 18 -20 q-2 12 -12 24 z" fill="url(#sp-wing)" stroke="#7a4f1c" stroke-width="2.4"/>
<ellipse cx="118" cy="150" rx="40" ry="46" fill="url(#sp-belly)"/>
<path d="M70 110 C58 132 60 168 86 182 C92 156 88 128 84 112 Z"
fill="url(#sp-wing)" stroke="#7a4f1c" stroke-width="3"/>
<path d="M104 124 L130 121 L113 142 Z" fill="#f1862a" stroke="#7a4f1c" stroke-width="2.4" stroke-linejoin="round"/>
<ellipse cx="92" cy="138" rx="9" ry="6" fill="#f0674a" opacity=".45"/>
<g class="logo-eye-open">
<circle cx="100" cy="106" r="17" fill="#fff" stroke="#7a4f1c" stroke-width="2.6"/>
<circle cx="103" cy="108" r="8.5" fill="#2a1c0e"/>
<circle cx="99" cy="103" r="3.4" fill="#fff"/>
</g>
<path d="M86 86 C110 74 150 80 168 100" fill="none" stroke="#221910" stroke-width="6" stroke-linecap="round"/>
<ellipse cx="146" cy="108" rx="20" ry="17" fill="#221910"/>
<ellipse cx="146" cy="106" rx="20" ry="15" fill="#2c2114"/>
<circle cx="150" cy="138" r="7" fill="none" stroke="#f2c94c" stroke-width="3.4"/>
<path d="M138 150 C166 150 178 168 168 192 C150 188 138 172 134 156 Z"
fill="url(#sp-wing)" stroke="#7a4f1c" stroke-width="3"/>
<g transform="translate(156,176)">
<g class="logo-tool" transform="rotate(28)">
<rect x="-5" y="-44" width="10" height="46" rx="5" fill="url(#sp-steel)" stroke="#5f584d" stroke-width="2"/>
<path d="M-11 -44 L11 -44 L11 -58 L4.5 -58 L4.5 -50 L-4.5 -50 L-4.5 -58 L-11 -58 Z"
fill="url(#sp-steel)" stroke="#5f584d" stroke-width="2" stroke-linejoin="round"/>
<rect x="-2" y="-40" width="2.6" height="34" rx="1.3" fill="#fff" opacity=".35"/>
</g>
</g>
<g stroke="#e07e26" stroke-width="5" stroke-linecap="round" fill="none">
<path d="M108 200 L108 214 M101 218 L108 214 L115 218 M108 214 L108 220"/>
<path d="M132 200 L132 214 M125 218 L132 214 L139 218 M132 214 L132 220"/>
</g>
</symbol>
</defs>
</svg>
<div class="overlay" id="overlay"></div>
<div class="config-panel" id="configPanel">
<span class="close" id="closeConfig">×</span>
<h3>⚙ providers · models · routing</h3>
<div class="cfg-tab-row">
<button class="cfg-tab active" data-cfg-tab="providers">providers & models</button>
<button class="cfg-tab" data-cfg-tab="routing">routing & autonomy</button>
<button class="cfg-tab" data-cfg-tab="permissions">permissions</button>
<button class="cfg-tab" data-cfg-tab="appearance">appearance</button>
<button class="cfg-tab" data-cfg-tab="memory">memory & telemetry</button>
<button class="cfg-tab" data-cfg-tab="mcp">MCP & hooks</button>
</div>
<div data-cfg-pane="providers">
<div class="cfg-summary-row">
<span><b id="cfgProvCount">0</b> providers</span>
<span><b id="cfgKeyCount" style="color:var(--add)">0</b> configured</span>
<span><b id="cfgModelCount">0</b> models available</span>
<span style="margin-left:auto;color:var(--dimmer)">click ▸ to expand · set default · paste key</span>
</div>
<input type="text" class="cfg-search" id="cfgSearch" placeholder="search provider or model…" oninput="filterProviders(this.value)">
<div id="providerList"></div>
</div>
<div data-cfg-pane="routing" style="display:none">
<div class="row">
<label>preferred provider</label>
<select id="preferredProviderSelect" style="max-width:220px">
<option value="">-- auto (smart routing per tier) --</option>
</select>
</div>
<div class="row">
<label>preferred model</label>
<input id="preferredModelSelect" type="text" placeholder="e.g. deepseek-v4-pro" style="max-width:220px">
<span class="muted" style="font-size:10px">leave empty for all models from provider</span>
</div>
<div class="row">
<label>routing mode</label>
<select id="routingModeSelect">
<option value="auto">auto: tier-based + free fallback</option>
<option value="manual">manual: exact provider/model, zero fallback</option>
</select>
</div>
<div class="row">
<label>auto-discover models</label>
<input type="checkbox" id="autoDiscoverToggle" checked>
<span class="muted" style="font-size:10px">scan provider APIs for available models on startup</span>
</div>
<div class="row">
<button class="cfg-save-btn" onclick="saveRouting()">💾 save routing</button>
<span class="save-status" id="routingSaveStatus" style="display:none;color:var(--add);font-size:10px;margin-left:8px">✓ saved</span>
</div>
<div class="row">
<label>autonomy</label>
<select id="autonomySelect">
<option value="supervised">supervised · ask before writes/exec</option>
<option value="trusted">trusted · checkpoint then act</option>
<option value="autonomous">autonomous · act within limits</option>
</select>
</div>
<div class="row">
<label>sandbox</label>
<select id="sandboxSelect">
<option value="local">local</option>
<option value="local-hardened">local-hardened</option>
<option value="docker">docker</option>
</select>
</div>
<div class="row">
<label>budget</label>
<input type="number" id="budgetSession" placeholder="session $" style="width:120px">
<input type="number" id="budgetDaily" placeholder="daily $" style="width:120px">
</div>
<div class="cfg-section">
<h4>quick presets</h4>
<div class="presets" id="presets">
<div class="preset" data-preset="local">🏠 local / free</div>
<div class="preset" data-preset="cloud">☁ cloud cheap</div>
<div class="preset" data-preset="strong">⚡ cloud strong</div>
</div>
</div>
</div>
<div data-cfg-pane="permissions" style="display:none">
<div class="row">
<label>permit</label>
<select id="permissionSelect">
<option value="read-only">read-only · no mutation</option>
<option value="plan">plan · no tools</option>
<option value="supervised">supervised · ask by risk</option>
<option value="trusted">trusted · checkpoint guard</option>
<option value="autonomous">autonomous · bounded action</option>
<option value="emergency-stop">emergency-stop · deny all</option>
</select>
</div>
<div class="cfg-section">
<h4>per-task model override</h4>
<p style="font-size:10.5px;color:var(--dim);line-height:1.5;margin-bottom:8px">Click <b>set default</b> next to any model above to force it for new tasks. Click <b>auto</b> below to clear and let the router decide.</p>
<button class="btn sm" onclick="setModelOverride(null)">⟳ clear override · auto-route</button>
<div style="margin-top:8px;font-size:10.5px;color:var(--dim)">current: <span id="cfgCurrentModel" style="color:var(--brand)">auto</span></div>
</div>
</div>
<div data-cfg-pane="appearance" style="display:none">
<div class="row"><label>theme</label>
<select id="cfgThemeSelect" onchange="document.documentElement.setAttribute('data-theme',this.value);localStorage.setItem('sparrow-theme',this.value)">
<option value="captain">captain (dark)</option>
<option value="paper">paper (light)</option>
<option value="white">white (clean light)</option>
</select>
</div>
<div class="row"><label>auto-open tools panel</label>
<input type="checkbox" id="cfgRightbarAuto" checked onchange="rbSetAutoOpen(this.checked)">
<span class="muted" style="font-size:10px">open the right panel automatically on diffs, tasks and plan updates</span>
</div>
<div class="row"><label>typography</label>
<select id="cfgFontSelect" onchange="document.documentElement.style.setProperty('--font-size-base',this.value+'px');localStorage.setItem('sparrow-font',this.value)">
<option value="12">12 px · compact</option>
<option value="13">13 px · default</option>
<option value="14">14 px · comfortable</option>
<option value="15">15 px · large</option>
</select>
</div>
<div class="row"><label>verbose chat</label>
<input type="checkbox" id="cfgVerboseToggle" onchange="VERBOSE=this.checked;localStorage.setItem('sparrow-verbose',this.checked?'1':'0')">
<span class="muted" style="font-size:10px">show sub-agent status, tool args, token ticks inline</span>
</div>
<div class="row"><label>chirp on events</label>
<input type="checkbox" id="cfgSoundToggle" onchange="SOUND_ON=this.checked;localStorage.setItem('sparrow-sound',this.checked?'1':'0')">
<span class="muted" style="font-size:10px">discreet WebAudio blip on checkpoints & skills</span>
</div>
</div>
<div data-cfg-pane="memory" style="display:none">
<div class="cfg-section">
<h4>persistent memory</h4>
<div class="row"><label>auto-summarize sessions</label>
<input type="checkbox" id="cfgAutoSummary" checked>
<span class="muted" style="font-size:10px">distill long runs into MEMORY.md on close</span>
</div>
<div class="row"><label>max memory size</label>
<input type="number" id="cfgMemMax" value="16000" min="2000" max="200000" step="1000" style="width:120px"><span class="muted" style="font-size:10px">chars · older facts pruned by score</span>
</div>
<div class="row"><label>checkpoints retention</label>
<input type="number" id="cfgCheckRetain" value="30" min="1" max="365" style="width:80px"><span class="muted" style="font-size:10px">days · older checkpoints pruned</span>
</div>
</div>
<div class="cfg-section">
<h4>telemetry & privacy</h4>
<div class="row"><label>anonymous usage</label>
<input type="checkbox" id="cfgTelemetry">
<span class="muted" style="font-size:10px">opt-in · counts only, never your prompts</span>
</div>
<div class="row"><label>redact secrets in logs</label>
<input type="checkbox" id="cfgRedact" checked>
<span class="muted" style="font-size:10px">strip api keys / tokens from event stream</span>
</div>
<div class="row"><label>session db path</label>
<input type="text" id="cfgSessionDb" readonly placeholder="(loaded from /status)" style="flex:1;background:var(--panel2);color:var(--dim)">
</div>
</div>
</div>
<div data-cfg-pane="mcp" style="display:none">
<div class="cfg-section">
<h4>MCP connectors</h4>
<p style="font-size:10.5px;color:var(--dim);line-height:1.5;margin-bottom:6px">External servers exposing tools/resources. Manage them with <code>sparrow mcp add</code> or edit <code>mcp_servers.json</code> in your config dir.</p>
<button class="btn sm" onclick="loadMcpList()">refresh list</button>
<div id="mcpList" style="margin-top:8px;font-size:10.5px;color:var(--dim);max-height:200px;overflow:auto">click refresh</div>
</div>
<div class="cfg-section">
<h4>hooks</h4>
<p style="font-size:10.5px;color:var(--dim);line-height:1.5;margin-bottom:6px">Run shell commands on lifecycle events (PreToolUse, OnApprovalRequested, PostCompact…). Configure them in your <code>config.toml</code> under <code>[[hooks]]</code>.</p>
<button class="btn sm" onclick="loadHooksList()">refresh hooks</button>
<div id="hooksList" style="margin-top:8px;font-size:10.5px;color:var(--dim);max-height:200px;overflow:auto">click refresh</div>
</div>
</div>
<div class="row" style="margin-top:14px;border-top:1px solid var(--line);padding-top:12px">
<button class="btn" id="saveConfig">save all</button>
<span class="dimd" id="saveMsg" style="font-size:11px"></span>
</div>
<div style="display:none">
<select id="provSelect"></select>
<select id="modelSelect"></select>
<input type="password" id="apiKey" autocomplete="off">
<span id="credBadge" class="badge missing">no key</span>
<div id="modelTags"></div>
</div>
</div>
<div class="win">
<div class="chrome">
<span class="dot r"></span><span class="dot y"></span><span class="dot g"></span>
<span class="nm"><b>sparrow</b> · webview console <span class="live-tag" id="conn-tag">connecting…</span></span>
<span class="right">
<span class="view-toggle" role="group" aria-label="View mode">
<button id="focusModeBtn" type="button" aria-pressed="true">Focus</button>
<button id="cockpitModeBtn" type="button" aria-pressed="false">Cockpit</button>
</span>
<button class="chip-btn" id="cmdkBtn" title="Cmd+K commands">⌘K commands</button>
<button class="chip-btn" id="kbdBtn" title="Keyboard shortcuts" aria-label="Keyboard shortcuts">⌘/</button>
<button class="chip-btn solid" id="replayBtn" title="Replay last run">▸ replay</button>
<button class="chip-btn" id="abortChromeBtn" title="Abort the running task" style="display:none;color:var(--rem);background:color-mix(in srgb,var(--rem) 12%,transparent);border-color:color-mix(in srgb,var(--rem) 50%,transparent)">◼ abort</button>
<button class="chip-btn" id="soundBtn" title="Cmd+M mute sounds">🔊</button>
<button class="chip-btn" id="verboseBtn" title="Verbose mode — show tool args, token ticks, internal reasoning" aria-pressed="false">⊕ verbose</button>
<button class="chip-btn" id="fontDownBtn" title="Smaller text" aria-label="Decrease text size">A−</button>
<button class="chip-btn" id="fontUpBtn" title="Larger text" aria-label="Increase text size">A+</button>
<button class="chip-btn" id="themeBtn" title="Cmd+Shift+L toggle theme">☾</button>
<button class="chip-btn" id="rightbarBtn" title="Open tools panel" aria-label="Open tools panel" aria-pressed="false" aria-controls="rightbar">⧉ tools</button>
<button class="btn sm" id="cfgBtn">config</button>
<span id="appVersion" style="font-size:11px;color:var(--dimmer)">v—</span>
</span>
</div>
<nav class="rail" aria-label="Sparrow panels">
<svg class="rail-logo" viewBox="0 0 240 240" aria-label="Sparrow"><use href="#sparrow"/></svg>
<button class="ico" data-panel="crew" title="Crew — persistent agents" id="crewRailBtn"><span aria-hidden="true">◈</span></button>
<button class="ico" data-panel="sessions" title="Sessions"><span aria-hidden="true">◷</span></button>
<button class="ico" data-panel="memory" title="Memory"><span aria-hidden="true">✦</span></button>
<button class="ico" data-panel="plugins" title="Plugins"><span aria-hidden="true">⌗</span></button>
<button class="ico" data-panel="tools" title="Tools"><span aria-hidden="true">⚒</span></button>
<button class="ico" data-panel="skills" title="Skills"><span aria-hidden="true">✸</span></button>
<button class="ico" data-panel="permissions" title="Permissions"><span aria-hidden="true">◉</span></button>
<button class="ico" data-panel="security" title="Security"><span aria-hidden="true">⛨</span></button>
<button class="ico" data-panel="route" title="Route Explorer"><span aria-hidden="true">⇄</span></button>
<button class="ico" data-panel="artifacts" title="Artifacts"><span aria-hidden="true">◇</span></button>
<div class="sep"></div>
<div class="spacer"></div>
<button class="ico" id="rail-settings" title="Settings"><span aria-hidden="true">⚙</span></button>
</nav>
<aside class="drawer" id="drawer" aria-label="Drawer">
<div class="panel" data-panel-body="crew">
<h3>Crew <span class="count" id="drw-crew-count">0</span></h3>
<div id="drw-crew-list"><div class="drw-empty">loading agents…</div></div>
</div>
<div class="panel" data-panel-body="sessions">
<h3>Sessions <span class="count" id="drw-sessions-count">0</span></h3>
<div id="drw-sessions-list"><div class="drw-empty">_none yet_ · start your first run</div></div>
</div>
<div class="panel" data-panel-body="memory">
<h3>Memory</h3>
<div id="drw-memory-body"><div class="drw-empty">_none yet_ · MEMORY.md is empty — Sparrow will learn as you go</div></div>
</div>
<div class="panel" data-panel-body="plugins">
<h3>Plugins <span class="count" id="drw-plugins-count">0</span></h3>
<div id="drw-plugins-list"><div class="drw-empty">_none yet_ · no plugins installed</div></div>
</div>
<div class="panel" data-panel-body="tools">
<h3>Tools <span class="count" id="drw-tools-count">0</span></h3>
<div id="drw-tools-list"><div class="drw-empty">_none yet_ · no tools loaded</div></div>
</div>
<div class="panel" data-panel-body="skills">
<h3>Skills <span class="count" id="drw-skills-count">0</span></h3>
<div id="drw-skills-list"><div class="drw-empty">_none yet_ · skills library is empty</div></div>
</div>
<div class="panel" data-panel-body="permissions">
<h3>Permissions</h3>
<div id="drw-permissions-body"><div class="drw-empty">_none yet_ · connect to load</div></div>
</div>
<div class="panel" data-panel-body="security">
<h3>Security</h3>
<div id="drw-security-body"><div class="drw-empty">_none yet_ · connect to audit</div></div>
</div>
<div class="panel" data-panel-body="route">
<h3>Route Explorer</h3>
<div id="drw-route-body"><div class="drw-empty">start a run to see the active route chain</div></div>
</div>
<div class="panel" data-panel-body="artifacts">
<h3>Artifacts <span class="count" id="drw-artifacts-count">0</span></h3>
<div id="drw-artifacts-list"><div class="drw-empty">_none yet_ · drag a file onto the page to upload</div></div>
</div>
</aside>
<button id="newConversation" title="Start a fresh conversation (clears retained context)"
style="position:absolute;top:88px;right:14px;font-family:inherit;font-size:10px;padding:4px 10px;
border-radius:7px;cursor:pointer;color:var(--dim);background:var(--panel);border:1px solid var(--line);
z-index:30;letter-spacing:.3px;transition:.15s"
onclick="newConversation()">⟲ new conversation</button>
<section class="main">
<div class="cockpit">
<div class="cmark"><svg width="32" height="32" aria-label="Sparrow canonical logo" style="filter:drop-shadow(0 0 8px rgba(242,169,60,.52))"><use href="#sparrow"/></svg><span class="w word">SPARROW</span></div>
<div class="stat route-stat"><span class="k">route</span><span class="route" id="route">idle</span></div>
<div class="stat"><span class="k">cost</span><span class="cost" id="cost">$0.000</span></div>
<div class="stat"><span class="k">in</span><span class="tok" id="tokIn">0</span></div>
<div class="stat"><span class="k">out</span><span class="tok" id="tokOut">0</span></div>
<div class="stat"><span class="k">total</span><span class="tok" id="tok">0</span></div>
<div class="stat"><span class="k">session</span><span class="tok" id="tokSession">0 tok · $0.000</span></div>
<div class="stat" title="Daily budget usage"><span class="k">budget</span><span class="budget-track"><span class="budget-fill" id="budgetFill"></span></span><span class="tok" id="budget" style="color:var(--gold);margin-left:6px">0% / —</span></div>
<div class="stat"><span class="k">autonomy</span><span class="pill" id="auto"><span class="led"></span><span id="autotxt">—</span></span></div>
</div>
<div class="swarm-cockpit" id="swarm-cockpit" aria-label="Sparrow swarm cockpit">
<div class="lane planner idle" id="swarm-planner" title="planner — orchestrator triad">
<div class="lane-head"><span class="who">planner</span><span class="st"><span class="led-mini"></span><span data-role-state>idle</span></span></div>
<span class="msg" data-role-msg>waiting for task decomposition</span>
</div>
<div class="lane coder idle" id="swarm-coder" title="coder — orchestrator triad">
<div class="lane-head"><span class="who">coder</span><span class="st"><span class="led-mini"></span><span data-role-state>idle</span></span></div>
<span class="msg" data-role-msg>ready to edit with checkpoints</span>
</div>
<div class="lane verifier idle" id="swarm-verifier" title="verifier — orchestrator triad">
<div class="lane-head"><span class="who">verifier</span><span class="st"><span class="led-mini"></span><span data-role-state>idle</span></span></div>
<span class="msg" data-role-msg>standing by for adversarial review</span>
</div>
<span id="swarm-extras-anchor" hidden></span>
<div class="lane-more" id="swarm-more" hidden></div>
<button id="swarm-new-agent" class="lane-more" title="Create a new persistent agent" style="cursor:pointer;border:0;border-left:1px dashed var(--line-hot);background:transparent;color:var(--brand);font-size:14px;padding:0 16px" onclick="openAgentCreator()">+ new agent</button>
</div>
<div id="agentCreatorBackdrop" onclick="if(event.target===this)closeAgentCreator()" style="position:fixed;inset:0;z-index:1200;display:none;align-items:center;justify-content:center;background:color-mix(in srgb,var(--bg) 70%,transparent);backdrop-filter:blur(6px);padding:20px;overflow:auto">
<div id="agentCreator" role="dialog" aria-modal="true" aria-labelledby="agentCreatorTitle" style="width:min(620px,96vw);max-height:min(88vh,820px);background:var(--panel);border:1px solid var(--line-hot);border-radius:12px;display:flex;flex-direction:column;box-shadow:0 30px 90px rgba(0,0,0,.6);overflow:hidden">
<div style="display:flex;align-items:center;justify-content:space-between;padding:14px 18px;border-bottom:1px solid var(--line);flex:0 0 auto">
<h3 id="agentCreatorTitle" style="margin:0;font-size:13px;letter-spacing:1.6px;text-transform:uppercase;color:var(--brand)">+ new persistent agent</h3>
<button onclick="closeAgentCreator()" aria-label="Close" style="background:transparent;border:0;color:var(--dim);font-size:20px;cursor:pointer;line-height:1">×</button>
</div>
<div style="padding:16px 20px;overflow-y:auto;flex:1 1 auto;display:flex;flex-direction:column;gap:12px;font-size:12px">
<label style="display:flex;flex-direction:column;gap:4px"><span style="color:var(--dim);font-size:10.5px;letter-spacing:1px;text-transform:uppercase">name <span style="color:var(--rem)">*</span></span><input id="ag-name" placeholder="e.g. reviewer" style="padding:7px 9px;border-radius:6px;border:1px solid var(--line);background:var(--panel2);color:var(--fg);font-family:inherit"></label>
<label style="display:flex;flex-direction:column;gap:4px"><span style="color:var(--dim);font-size:10.5px;letter-spacing:1px;text-transform:uppercase">role / title</span><input id="ag-role" placeholder="e.g. security reviewer" style="padding:7px 9px;border-radius:6px;border:1px solid var(--line);background:var(--panel2);color:var(--fg);font-family:inherit"></label>
<label style="display:flex;flex-direction:column;gap:4px"><span style="color:var(--dim);font-size:10.5px;letter-spacing:1px;text-transform:uppercase">short description</span><textarea id="ag-desc" rows="2" placeholder="What this agent does in one or two sentences" style="padding:7px 9px;border-radius:6px;border:1px solid var(--line);background:var(--panel2);color:var(--fg);font-family:inherit;resize:vertical"></textarea></label>
<div style="display:flex;gap:10px">
<label style="flex:1;display:flex;flex-direction:column;gap:4px"><span style="color:var(--dim);font-size:10.5px;letter-spacing:1px;text-transform:uppercase">preferred model</span><input id="ag-model" placeholder="provider:model or auto" style="padding:7px 9px;border-radius:6px;border:1px solid var(--line);background:var(--panel2);color:var(--fg);font-family:inherit"></label>
<label style="flex:0 0 130px;display:flex;flex-direction:column;gap:4px"><span style="color:var(--dim);font-size:10.5px;letter-spacing:1px;text-transform:uppercase">accent color</span>
<select id="ag-color" style="padding:7px 9px;border-radius:6px;border:1px solid var(--line);background:var(--panel2);color:var(--fg);font-family:inherit">
<option value="steel">steel</option><option value="gold">gold</option>
<option value="coral">coral</option><option value="planner">planner-blue</option>
<option value="coder">coder-green</option><option value="verifier">verifier-purple</option>
</select>
</label>
</div>
<label style="display:flex;flex-direction:column;gap:4px"><span style="color:var(--dim);font-size:10.5px;letter-spacing:1px;text-transform:uppercase">allowed tools (comma-separated)</span><input id="ag-tools" placeholder="fs_read, fs_write, edit, search … (blank = inherit)" style="padding:7px 9px;border-radius:6px;border:1px solid var(--line);background:var(--panel2);color:var(--fg);font-family:inherit"></label>
<label style="display:flex;flex-direction:column;gap:4px"><span style="color:var(--dim);font-size:10.5px;letter-spacing:1px;text-transform:uppercase">soul.toml override (advanced)</span><textarea id="ag-soul" rows="4" placeholder="Leave blank to auto-generate from fields above" style="padding:7px 9px;border-radius:6px;border:1px solid var(--line);background:var(--panel2);color:var(--fg);font-family:'JetBrains Mono',monospace;font-size:11px;resize:vertical"></textarea></label>
<label style="display:flex;flex-direction:column;gap:4px"><span style="color:var(--dim);font-size:10.5px;letter-spacing:1px;text-transform:uppercase">agent.md (long-form persona / instructions)</span><textarea id="ag-md" rows="6" placeholder="# Persona Background, tone, goals, examples…" style="padding:7px 9px;border-radius:6px;border:1px solid var(--line);background:var(--panel2);color:var(--fg);font-family:'JetBrains Mono',monospace;font-size:11px;resize:vertical"></textarea></label>
<div id="ag-feedback" style="font-size:11px;color:var(--dim);min-height:16px"></div>
</div>
<div style="display:flex;gap:10px;padding:12px 20px;border-top:1px solid var(--line);justify-content:flex-end;flex:0 0 auto;background:color-mix(in srgb,var(--panel) 92%,transparent)">
<button onclick="closeAgentCreator()" style="padding:7px 14px;border-radius:7px;border:1px solid var(--line);background:transparent;color:var(--dim);cursor:pointer;font-family:inherit">cancel</button>
<button onclick="saveNewAgent()" id="ag-save" style="padding:7px 16px;border-radius:7px;border:1px solid rgba(116,194,88,.4);background:rgba(116,194,88,.18);color:var(--add);cursor:pointer;font-family:inherit;font-weight:600">create agent</button>
</div>
</div>
</div>
<div id="term"></div>
<div class="composer-hints">
<span><span class="ok-dot" style="color:var(--add)">●</span> context <b id="ctxHintPct" style="color:var(--fg)">—</b></span>
<div style="position:relative">
<button id="modelPickerBtn" title="Switch model" style="font-family:inherit;font-size:10px;color:var(--planner);background:color-mix(in srgb,var(--planner) 10%,transparent);border:1px solid color-mix(in srgb,var(--planner) 35%,transparent);padding:2px 9px;border-radius:999px;cursor:pointer;letter-spacing:.3px;transition:.15s" onclick="toggleModelPicker(event)">⬡ <span id="modelPickerLabel">auto</span></button>
<div class="mp-drop" id="modelPickerDrop" style="display:none">
<div class="mp-drop-head">select model</div>
<div id="modelPickerList"><div style="padding:12px 14px;color:var(--dimmer);font-size:11px">loading…</div></div>
<div class="mp-auto" onclick="setModelOverride(null)">⟳ auto-route (recommended)</div>
</div>
</div>
<span class="kbds">
<kbd>⌘K</kbd>commands
<kbd>@</kbd>agent
<kbd>Tab</kbd>complete
<kbd>Alt+F</kbd>focus
<kbd>⇧⏎</kbd>newline
<kbd>⌘/</kbd>shortcuts
</span>
</div>
<div class="focus-actions" aria-label="Focus quick actions">
<button class="btn ok-action" id="focusOkBtn" type="button">OK</button>
<button class="btn undo-action" id="focusUndoBtn" type="button">Undo</button>
<button class="btn explain-action" id="focusExplainBtn" type="button">Explain</button>
</div>
<div class="input-bar">
<label class="attach-btn" for="fileInput" title="Attach files">
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M21.4 11.3 12 20.7a6 6 0 0 1-8.5-8.5l9.8-9.8a4 4 0 0 1 5.7 5.7L9.2 17.9a2 2 0 1 1-2.8-2.8l8.9-8.9" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</label>
<input type="file" id="fileInput" multiple>
<button class="mic-btn" id="micBtn" type="button" title="Dictate with microphone" aria-label="Dictate with microphone">●</button>
<div class="composer-wrap">
<div class="composer-ghost" id="composerGhost" aria-hidden="true"></div>
<textarea id="taskInput" rows="1" placeholder="Type a task and press Enter…" autofocus></textarea>
</div>
<div class="context-side" aria-label="Context usage">
<div class="context-label"><span>context</span><b id="contextText">0% · 0 / 32k tok</b></div>
<div class="context-track"><span class="context-fill" id="contextFill"></span></div>
</div>
<button class="btn" id="runBtn">run</button>
<div class="attachments" id="attachments"></div>
</div>
</section>
<aside class="rightbar" id="rightbar" aria-label="Tools panel" aria-hidden="true">
<div class="rb-inner">
<div class="rb-head">
<button class="rb-back" id="rbBack" title="Back to menu" aria-label="Back to tools menu" style="display:none">‹</button>
<span class="rb-title" id="rbTitle">Tools</span>
<button class="rb-pin" id="rbPin" title="Auto-open: on" aria-label="Toggle auto-open" aria-pressed="true">◉</button>
<button class="rb-close" id="rbClose" title="Close tools panel" aria-label="Close tools panel">✕</button>
</div>
<div class="rb-body" id="rbBody"></div>
<div class="rb-foot" id="rbFoot">
<span class="rb-led" aria-hidden="true"></span>
<span id="rbFootState">idle</span>
<span class="rb-time" id="rbFootTime"></span>
</div>
</div>
</aside>
</div>
<div class="diff-panel" id="diffPanel" aria-label="Diff viewer">
<div class="diff-panel-head">
<span style="color:var(--coral);font-size:12px">◇</span>
<span class="dp-path" id="dpPath">—</span>
<span id="dpStats" style="font-size:9.5px;color:var(--dimmer)"></span>
<button class="dp-close" onclick="closeDiffPanel()" title="Close">✕</button>
</div>
<div class="diff-panel-body" id="dpBody"></div>
</div>
<div class="boot-overlay" id="bootOverlay" hidden>
<svg class="b-logo" width="120" height="120" aria-label="Sparrow"><use href="#sparrow"/></svg>
<div class="b-word">SPARROW</div>
<div class="b-tag">one cli · grows with you</div>
<div class="b-status" id="bootStatus"></div>
<div class="b-skip">click anywhere or press <kbd>esc</kbd> to skip</div>
</div>
<div class="palette" id="palette" role="dialog" aria-label="Slash command palette">
<div class="box">
<div class="pinput">
<svg width="22" height="22" aria-label="Sparrow"><use href="#sparrow"/></svg>
<input type="text" id="paletteInput" placeholder="Type / to filter commands, skills, agents; Shift+Enter runs…" autocomplete="off">
</div>
<div class="res-list" id="paletteResults"></div>
<div class="pfoot">
<span><kbd>↑↓</kbd> nav</span>
<span><kbd>↵</kbd> insert</span>
<span><kbd>⇧↵</kbd> run command</span>
<span style="margin-left:auto"><kbd>esc</kbd> close</span>
</div>
</div>
</div>
<div class="picker hidden" id="agentPicker" role="listbox" aria-label="Agent picker"></div>
<div class="approval-modal" id="approvalModal" role="dialog" aria-modal="true" aria-label="Approval required">
<div class="approval-card">
<h3>Approval required</h3>
<p id="approvalSummary">Sparrow is asking before it acts.</p>
<span class="risk" id="approvalRisk">risk: unknown</span>
<div class="actions">
<button class="deny" id="approvalDeny">deny</button>
<button class="approve" id="approvalApprove">approve</button>
</div>
</div>
</div>
<div class="kbd-help" id="kbdHelp" role="dialog" aria-modal="true" aria-label="Keyboard shortcuts" onclick="if(event.target===this)toggleKbdHelp(false)">
<div class="kh-card">
<h3>⌨ keyboard shortcuts</h3>
<div class="kh-grid" id="kbdHelpGrid"></div>
</div>
</div>
<div class="status off" id="status">● disconnected</div>
<div class="drop-zone" id="dropZone">drop files to attach to Sparrow</div>
<script>
const term=document.getElementById('term');
if(term)term.addEventListener('click',e=>{
const link=e.target.closest&&e.target.closest('.file-link[data-file-preview]');
if(!link)return;
e.preventDefault();
const path=link.getAttribute('data-file-preview');
if(path&&typeof loadFileInPanel==='function')loadFileInPanel(path);
});
const $=id=>document.getElementById(id);
let costV=0,tokV=0,tokInV=0,tokOutV=0,sessionTok=0,sessionCost=0;
let runStartMs=0,runDiffsApplied=0,runCheckpoints=0;
let _verifierRan=false; let actualInV=0,actualOutV=0,estInV=0,estOutV=0;
let tokenMeter=null,tokenMeterTimer=null;
let demoTokenInterval=null;
let attachedFiles=[];
let HISTORY_INPUTS=[];
let HISTORY_IDX=null;
let HISTORY_DRAFT='';
let contextLimit=32000;
const ATTACHMENT_CHAR_LIMIT=120000;
const MAX_ATTACHMENT_BYTES=10*1024*1024;
const FLIGHT_VERBS=['Soaring','Gliding','Diving','Scouting','Perching','Foraging','Wheeling','Lofting','Skimming','Swooping'];
let _vi=0;
const BIRD=
'<span class="fr"> ^^</span>\n'+
'<span class="fr"> .-~~~-.</span>\n'+
'<span class="fr"> /</span><span class="brow">__</span><span class="fr"> \\</span>\n'+
'<span class="fr"> | </span><span class="eye">o</span><span class="fr"> </span><span class="patch">██</span><span class="fr"> |</span>\n'+
'<span class="fr"> | </span><span class="beak">v</span><span class="fr"> |</span>\n'+
'<span class="fr"> | </span><span class="cheek">.</span><span class="fr"> |</span>\n'+
'<span class="fr"> \\ </span><span class="belly">\\__/</span><span class="fr"> /</span>\n'+
'<span class="fr"> \'-..-\'</span>\n'+
'<span class="fr"> /| |\\</span> <span class="tool">╤━</span><span class="gold">o</span>\n'+
'<span class="fr"> \'_| |_\'</span>';
const TOOL_ICONS={
read:'📖',search:'🔍',grep:'🎯',find:'🔍',
write:'✏️',edit:'🖊️',patch:'🩹',create:'✨',
run:'🏃',exec:'⚡',bash:'💻',terminal:'🖥️',
delete:'🗑️',remove:'🗑️',rm:'🧹',
list:'📂',ls:'📁',glob:'📁',
web:'🌐',fetch:'📡',http:'🔌',curl:'🌍',
diff:'📊',checkpoint:'💠',
memory:'🧠',remember:'📝',recall:'🔮',
think:'💭',ask:'❓',plan:'📋',
default:'⚒'
};
function toolIcon(name){
if(!name)return TOOL_ICONS.default;
const n=name.toLowerCase();
for(const [k,v] of Object.entries(TOOL_ICONS)){
if(n.startsWith(k)||n.includes(k))return v;
}
return TOOL_ICONS.default;
}
function startVerbCycle(elId){
const el=document.getElementById(elId);
if(!el)return;
setInterval(()=>{el.textContent=FLIGHT_VERBS[_vi++%FLIGHT_VERBS.length]},2200);
}
setInterval(()=>{document.querySelectorAll('.verb').forEach(v=>{v.textContent=FLIGHT_VERBS[_vi++%FLIGHT_VERBS.length]})},2200);
const LEARNED_SKILLS=['fix_revoked_token','run_auth_suite','patch_session','audit_emit','write-and-fix-tests','refactor-safely','debug-systematically'];
let _si=0;
function toast(msg, kind){
kind = kind || 'info';
let host = document.getElementById('sparrow-toast-host');
if(!host){
host = document.createElement('div');
host.id = 'sparrow-toast-host';
host.style.cssText = 'position:fixed;bottom:18px;right:18px;z-index:99999;display:flex;flex-direction:column;gap:8px;pointer-events:none;max-width:380px';
document.body.appendChild(host);
}
const palette = {
error: ['var(--rem,#d44)', 'rgba(212,68,68,.12)', 'rgba(212,68,68,.45)'],
ok: ['var(--add,#3a8)', 'rgba(58,168,136,.12)', 'rgba(58,168,136,.45)'],
info: ['var(--fg,#888)', 'rgba(120,120,120,.10)', 'rgba(120,120,120,.40)'],
}[kind] || ['#888','rgba(120,120,120,.10)','rgba(120,120,120,.40)'];
const el = document.createElement('div');
el.style.cssText = `pointer-events:auto;padding:9px 13px;border-radius:8px;font:500 12px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;color:${palette[0]};background:${palette[1]};border:1px solid ${palette[2]};box-shadow:0 4px 12px rgba(0,0,0,.18);opacity:0;transform:translateY(6px);transition:opacity .18s,transform .18s;cursor:pointer`;
el.textContent = String(msg);
el.addEventListener('click', ()=>el.remove());
host.appendChild(el);
requestAnimationFrame(()=>{el.style.opacity='1';el.style.transform='translateY(0)';});
setTimeout(()=>{
el.style.opacity='0';el.style.transform='translateY(6px)';
setTimeout(()=>el.remove(), 220);
}, 4000);
}
function pulseLearnToast(elId){
const el=document.getElementById(elId);
if(!el)return;
function pulse(){
el.querySelector('b').textContent=LEARNED_SKILLS[_si++%LEARNED_SKILLS.length];
el.classList.remove('learn-toast');void el.offsetWidth;el.classList.add('learn-toast');
}
setTimeout(()=>{pulse();setInterval(pulse,9000)},5500);
}
function startTokenCounter(elId){
const el=document.getElementById(elId);
if(!el)return;
let n=0;
demoTokenInterval=setInterval(()=>{
if(actualInV||actualOutV||estInV||estOutV)return;
n+=Math.floor(Math.random()*41);
el.textContent=n.toLocaleString('en-US');
el.classList.add('up');
setTimeout(()=>el.classList.remove('up'),300);
},850);
}
function addBootLine(parent,html,index){
const d=document.createElement('div');
d.className='boot-line';
d.innerHTML=html;
d.style.animationDelay=(index*250)+'ms';
parent.appendChild(d);
term.scrollTop=term.scrollHeight;
return d;
}
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function typeCmd(text,container){
const d=document.createElement('div');
d.className='ln';
d.innerHTML='<span class="prompt">sparrow ›</span> <span class="cmd"></span><span class="cur2"></span>';
container.appendChild(d);
const cmd=d.querySelector('.cmd'),cur=d.querySelector('.cur2');
for(const ch of text){cmd.textContent+=ch;container.scrollTop=container.scrollHeight;await sleep(26+Math.random()*36)}
cur.remove();
return d;
}
function line(html,cls=''){const d=document.createElement('div');d.className='ln '+cls;d.innerHTML=html;term.appendChild(d);term.scrollTop=term.scrollHeight;return d;}
function setRoute(t){
const el=$('route');
const parts=String(t||'idle').split(/\s*(?:→|->)\s*/).filter(Boolean);
if(parts.length>1){
el.innerHTML=parts.map((p,i)=>`${i?'<span class="arrow">→</span>':''}<span class="hop">${esc(p)}</span>`).join('');
}else{
el.textContent=t||'idle';
}
el.title=t;
}
function updateSession(){
$('tokSession').textContent=sessionTok.toLocaleString('en-US')+' tok · $'+sessionCost.toFixed(4);
}
let _budgetDailyUsd=5; function updateBudget(){
const el=$('budget');if(!el)return;
const pct=_budgetDailyUsd>0?Math.min(100,Math.round(sessionCost/_budgetDailyUsd*100)):0;
el.textContent=pct+'% / $'+_budgetDailyUsd+' day';
el.style.color=pct>=85?'var(--rem)':pct>=60?'var(--coral)':pct>=30?'var(--gold)':'var(--add)';
const fill=$('budgetFill');if(!fill)return;
fill.style.width=pct+'%';
fill.className='budget-fill'+(pct>=80?' danger':pct>=60?' warn':'');
}
function setCost(c){
const next=Math.max(0,Number(c)||0);
sessionCost+=next-costV;
costV=next;
const el=$('cost');
el.textContent='$'+costV.toFixed(4);
el.classList.remove('up');void el.offsetWidth;el.classList.add('up');
setTimeout(()=>el.classList.remove('up'),600);
updateBudget();
if(tokenMeter)updateTokenMeter();
updateSession();
}
function countUp(el,to,ms=420){
if(!el)return;
const fromTxt=String(el.textContent||'').replace(/[^\d.-]/g,'');
const from=parseFloat(fromTxt)||0;
if(el._countUpTimer)clearInterval(el._countUpTimer);
if(from===to){el.textContent=to.toLocaleString('en-US');return;}
const start=Date.now();
const tick=()=>{
const t=Math.min(1,(Date.now()-start)/ms);
const eased=1-Math.pow(1-t,3); const val=Math.round(from+(to-from)*eased);
el.textContent=val.toLocaleString('en-US');
if(t>=1){clearInterval(el._countUpTimer);el._countUpTimer=null;el.textContent=to.toLocaleString('en-US');}
};
el._countUpTimer=setInterval(tick,16); tick();
}
function refreshTokens(){
const prev=tokV;
tokInV=actualInV+estInV;tokOutV=actualOutV+estOutV;tokV=tokInV+tokOutV;
sessionTok+=tokV-prev;
const elIn=$('tokIn');if(elIn)elIn.textContent=tokInV.toLocaleString('en-US');
const elOut=$('tokOut');if(elOut)elOut.textContent=tokOutV.toLocaleString('en-US');
const elTok=$('tok');if(elTok)elTok.textContent=tokV.toLocaleString('en-US');
if(prev!==tokV)countUp(elTok,tokV,400);
updateSession();
updateTokenMeter();
updateContextMeter();
['tokIn','tokOut','tok'].forEach(id=>{
const el=$(id);if(!el)return;
el.classList.remove('up');void el.offsetWidth;el.classList.add('up');
setTimeout(()=>el.classList.remove('up'),600);
});
}
function addTokEstimate(input=0,output=0){estInV+=input||0;estOutV+=output||0;refreshTokens();}
function addTokActual(input=0,output=0){
actualInV+=input||0;actualOutV+=output||0;
estInV=Math.max(0,estInV-(input||0));estOutV=Math.max(0,estOutV-(output||0));
refreshTokens();
}
function resetRunMetrics(){
if(demoTokenInterval){clearInterval(demoTokenInterval);demoTokenInterval=null}
costV=0;tokV=0;tokInV=0;tokOutV=0;actualInV=0;actualOutV=0;estInV=0;estOutV=0;
$('cost').textContent='$0.000';$('tokIn').textContent='0';$('tokOut').textContent='0';$('tok').textContent='0';
tokenMeter=null;
updateContextMeter();
}
function ensureTokenMeter(){
if(tokenMeter)return tokenMeter;
tokenMeter=line('<span class="token-meter"><span class="in">sent <b data-tok-in>0</b> tok</span><span class="out">recv <b data-tok-out>0</b> tok</span><span class="cost">live $0.0000</span></span>');
return tokenMeter;
}
function updateTokenMeter(){
const row=ensureTokenMeter().querySelector('.token-meter');
row.querySelector('[data-tok-in]').textContent=tokInV.toLocaleString('en-US');
row.querySelector('[data-tok-out]').textContent=tokOutV.toLocaleString('en-US');
row.querySelector('.cost').textContent='live $'+costV.toFixed(4);
row.classList.remove('pulse');void row.offsetWidth;row.classList.add('pulse');
clearTimeout(tokenMeterTimer);tokenMeterTimer=setTimeout(()=>row.classList.remove('pulse'),460);
}
function estimateTokens(text){return Math.ceil(String(text||'').length/4);}
function draftContextTokens(){
const input=$('taskInput')?.value||'';
const attachments=attachedFiles.reduce((sum,f)=>sum+estimateTokens(f.content||f.note||f.name),0);
return tokV+estimateTokens(input)+attachments;
}
function fmtCtx(n){if(n>=1000000)return(n/1000000).toFixed(n%1000000===0?0:1)+'M';if(n>=1000)return Math.round(n/1000)+'k';return String(n);}
function updateContextMeter(){
const used=Math.min(contextLimit,draftContextTokens());
const pct=Math.min(100,Math.round((used/contextLimit)*100));
if($('contextText'))$('contextText').textContent=`${pct}% · ${used.toLocaleString('en-US')} / ${fmtCtx(contextLimit)} tok`;
if($('contextFill'))$('contextFill').style.width=pct+'%';
}
function setAuto(level){
const map={supervised:'--sup',trusted:'--tru',autonomous:'--aut'};
const col=`var(${map[level]})`;const p=$('auto');
p.style.color=col;p.style.border='1px solid color-mix(in srgb,'+col+' 45%,transparent)';
p.style.background='color-mix(in srgb,'+col+' 13%,transparent)';
p.querySelector('.led').style.background=col;p.querySelector('.led').style.boxShadow='0 0 8px '+col;
$('autotxt').textContent=level.toUpperCase();
}
function cleanModelReason(reason){
const text=String(reason||'');
if(text.includes('Ollama API error 404')&&text.includes('model')){
return 'modèle local indisponible';
}
if(text.toLowerCase().includes('ollama')){
return 'provider local indisponible';
}
return text;
}
function routeSwitchLabel(from,to,reason){
const clean=cleanModelReason(reason);
if(clean==='modèle local indisponible'||clean==='provider local indisponible'){
return `↳ modèle local indisponible → routage modèle cloud <span class="planner">${esc(to)}</span>`;
}
return `↳ fallback: ${esc(from)} → ${esc(to)} <span class="dimd">${esc(clean)}</span>`;
}
function summarizeChain(chain,limit=5){
const list=Array.isArray(chain)?chain:[];
if(!list.length)return 'aucun modèle disponible';
const visible=list.slice(0,limit);
if(list.length>limit)visible.push(`+${list.length-limit} autres fallbacks`);
return visible.join(' → ');
}
function compactChain(chain){
const list=Array.isArray(chain)?chain:[];
if(!list.length)return 'idle';
if(list.length===1)return list[0];
return `${list[0]} → ${list[1]}${list.length>2?` · +${list.length-2}`:''}`;
}
function setSwarm(role,state,msg){
let lane=$('swarm-'+role);
if(!lane){
const slug=String(role||'').toLowerCase().replace(/[^a-z0-9-]+/g,'-').replace(/^-+|-+$/g,'');
lane=document.getElementById('swarm-extra-'+slug);
}
if(!lane){
const anchor=document.getElementById('swarm-extras-anchor');
const more=document.getElementById('swarm-more');
if(anchor && more){
lane=document.createElement('div');
lane.className='lane steel working';
lane.id='swarm-extra-'+String(role||'agent').toLowerCase().replace(/[^a-z0-9-]+/g,'-');
lane.title=role||'sub-agent';
const head=document.createElement('div');head.className='lane-head';
const who=document.createElement('span');who.className='who';who.textContent=role||'agent';
const st=document.createElement('span');st.className='st';
const led=document.createElement('span');led.className='led-mini';
const txt=document.createElement('span');txt.setAttribute('data-role-state','');txt.textContent='working';
st.append(led,txt);head.append(who,st);
const m=document.createElement('span');m.className='msg';m.setAttribute('data-role-msg','');m.textContent=msg||'spawned';
lane.append(head,m);
anchor.parentNode.insertBefore(lane,more);
} else return;
}
const cleanState=String(state||'idle').toLowerCase();
lane.classList.remove('idle','working','done','error');
lane.classList.add(cleanState.includes('error')?'error':cleanState.includes('done')?'done':cleanState.includes('work')||cleanState.includes('think')?'working':'idle');
lane.querySelector('[data-role-state]').textContent=cleanState.includes('think')?'thinking':cleanState.includes('work')?'working':cleanState.includes('done')?'done':cleanState.includes('error')?'error':'idle';
lane.querySelector('[data-role-msg]').innerHTML=esc(msg||'ready');
lane.style.animation='none';void lane.offsetWidth;lane.style.animation='';
}
function resetSwarm(){
setSwarm('planner','idle','waiting for task decomposition');
setSwarm('coder','idle','ready to edit with checkpoints');
setSwarm('verifier','idle','standing by for adversarial review');
}
function injectHero(){
const term=document.getElementById('term');if(!term)return;
if(term.querySelector('.hero'))return;
const hero=document.createElement('div');
hero.className='hero';
hero.innerHTML=
'<svg class="h-logo" width="84" height="84" aria-label="Sparrow"><use href="#sparrow"/></svg>'+
'<div class="h-text">'+
'<div class="hi">Welcome back, <b>'+escHtml(navigator.userAgent.includes('Windows')?'Captain':'friend')+'</b></div>'+
'<div class="tag">one cli · grows with you</div>'+
'<div class="meta">'+
'<span><span class="ok-dot">●</span> router · <b id="hero-providers">…</b> providers · <b id="hero-active" style="color:var(--add)">…</b> active</span>'+
'<span><span class="ok-dot">●</span> skills · <b id="hero-skills">…</b></span>'+
'<span><span class="ok-dot">●</span> facts · <b id="hero-facts">…</b></span>'+
'<span><span class="ok-dot">●</span> autonomy · <b id="hero-auto">…</b></span>'+
'</div>'+
'</div>';
term.prepend(hero);
}
async function hydrateHero(){
try{
const [cfgR,memR,permR]=await Promise.all([fetch('/config'),fetch('/memory'),fetch('/permissions')]);
const cfg=await cfgR.json(),mem=await memR.json(),perm=await permR.json();
const setIf=(id,val)=>{const el=document.getElementById(id);if(el)el.textContent=val;};
const allProviders=Array.isArray(cfg.providers)?cfg.providers:[];
const activeProviders=allProviders.filter(p=>p.has_credential||p.configured);
setIf('hero-providers',allProviders.length||'—');
const heroActive=document.getElementById('hero-active');
if(heroActive){
const prev=heroActive.textContent;heroActive.textContent=activeProviders.length||'0';
if(prev!==String(activeProviders.length||'0')){heroActive.classList.remove('hero-providers-pulse');void heroActive.offsetWidth;heroActive.classList.add('hero-providers-pulse');}
}
if(cfg.budget?.daily_usd){_budgetDailyUsd=cfg.budget.daily_usd;updateBudget();}
setIf('hero-facts',(mem.stats&&mem.stats.facts!==undefined)?mem.stats.facts:(mem.facts||[]).length);
setIf('hero-skills',(cfg.skills_count!==undefined)?cfg.skills_count:'—');
setIf('hero-auto',(perm.permissions&&perm.permissions.mode)||'—');
window._configuredProviders=new Set(activeProviders.map(p=>p.name||p.id));
const hp=document.getElementById('homePath');
if(hp)hp.textContent=cfg.workdir||location.host;
const ms=document.getElementById('memorySummary');
if(ms){
const facts=(mem.stats&&mem.stats.facts!==undefined)?mem.stats.facts:(mem.facts||[]).length;
const mode=(perm.permissions&&perm.permissions.mode)||'supervised';
ms.textContent=`${facts} facts stored · permission mode: ${mode}`;
}
if(activeProviders.length===0){
showOnboardingBanner();
}
}catch(_){}
}
function showOnboardingBanner(){
if(document.getElementById('onboarding-banner'))return;
const b=document.createElement('div');
b.id='onboarding-banner';
b.style.cssText='margin:10px 0;padding:12px 16px;border:1px solid color-mix(in srgb,var(--brand) 40%,var(--line));border-radius:10px;background:color-mix(in srgb,var(--brand) 7%,transparent);font-size:11.5px;line-height:1.6';
b.innerHTML=`<div style="font-weight:700;color:var(--brand);letter-spacing:.5px;margin-bottom:6px">⬡ No providers configured</div>
<div style="color:var(--dim)">Sparrow needs at least one API key to route tasks. Run setup to configure providers:</div>
<div style="margin-top:10px;display:flex;gap:8px;flex-wrap:wrap">
<button onclick="document.getElementById('taskInput').value='sparrow setup';document.getElementById('runBtn').click()" style="font-family:inherit;font-size:10.5px;padding:4px 12px;border-radius:7px;cursor:pointer;color:var(--brand);background:color-mix(in srgb,var(--brand) 12%,transparent);border:1px solid color-mix(in srgb,var(--brand) 40%,transparent)">▸ run sparrow setup</button>
<button onclick="openPanel('permissions')" style="font-family:inherit;font-size:10.5px;padding:4px 12px;border-radius:7px;cursor:pointer;color:var(--dim);background:var(--panel);border:1px solid var(--line)">◉ open permissions</button>
<button onclick="this.closest('#onboarding-banner').remove()" style="font-family:inherit;font-size:10.5px;padding:4px 12px;border-radius:7px;cursor:pointer;color:var(--dimmer);background:transparent;border:1px solid var(--line)">dismiss</button>
</div>
<div style="margin-top:8px;font-size:10px;color:var(--dimmer)">Or set <code style="color:var(--steel)">NVIDIA_API_KEY</code>, <code style="color:var(--steel)">ANTHROPIC_API_KEY</code>, or <code style="color:var(--steel)">OPENAI_API_KEY</code> in your environment and restart.</div>`;
const term=document.getElementById('term');if(term)term.prepend(b);
}
const BOOT_STATUS_LINES=[
['router','providers discovered'],
['surfaces','cli · webview · gateway'],
['sandbox','workdir guards armed'],
['skills','library indexed'],
['memory','MEMORY.md + USER.md loaded'],
['autonomy','dial ready'],
];
function runBootAnimation(){
if(new URLSearchParams(location.search).get('boot')==='0')return;
if(sessionStorage.getItem('sparrow-booted')==='1')return;
sessionStorage.setItem('sparrow-booted','1');
const ov=document.getElementById('bootOverlay');if(!ov)return;
ov.hidden=false;ov.classList.remove('fading');
const tag=ov.querySelector('.b-tag');if(tag){var txt=tag.textContent;tag.textContent='';(async function(){for(var i=0;i<txt.length;i++){tag.textContent+=txt[i];await sleep(40+Math.random()*30);}})();}
const host=document.getElementById('bootStatus');host.innerHTML='';
BOOT_STATUS_LINES.forEach((pair,i)=>{
const div=document.createElement('div');
div.className='b-line';
div.style.animationDelay=(.25+i*.18)+'s';
div.innerHTML=`<span class="l-key">${escHtml(pair[0])}</span><span class="l-val">${escHtml(pair[1])}</span>`;
host.appendChild(div);
});
const skip=()=>{
if(ov.classList.contains('fading'))return;
ov.classList.add('fading');
setTimeout(()=>{ov.hidden=true;document.removeEventListener('keydown',onKey);ov.removeEventListener('click',skip);},420);
};
const onKey=(e)=>{if(e.key==='Escape'||e.key==='Enter'||e.key===' ')skip();};
ov.addEventListener('click',skip);
document.addEventListener('keydown',onKey);
setTimeout(skip,2400); if(typeof chirp==='function'){chirp(880,.06);setTimeout(()=>chirp(1320,.06),140);setTimeout(()=>chirp(1760,.08),280);}
}
function isFastStart(){
return new URLSearchParams(location.search).get('fast')==='1';
}
function scheduleIdle(fn,timeout){
if(typeof requestIdleCallback==='function'){
requestIdleCallback(()=>fn(),{timeout:timeout||1800});
}else{
setTimeout(fn,timeout||250);
}
}
let _termBaseFont=13;
function getReadScale(){return parseFloat(localStorage.getItem('sparrow-read-scale')||'1')||1}
function applyTermFont(){
const term=document.getElementById('term');
if(term)term.style.fontSize=(_termBaseFont*getReadScale()).toFixed(1)+'px';
}
function setReadScale(s){
const v=Math.max(0.85,Math.min(1.6,Math.round(s*100)/100));
localStorage.setItem('sparrow-read-scale',String(v));
applyTermFont();
}
function initReadScale(){applyTermFont();}
const THEMES=['captain','paper','white'];
function getTheme(){return document.documentElement.getAttribute('data-theme')||'captain'}
function setTheme(t){
if(!THEMES.includes(t))t='captain';
document.documentElement.setAttribute('data-theme',t);
const btn=document.getElementById('themeBtn');
if(btn)btn.textContent=t==='captain'?'☾':t==='paper'?'☀':'◻';
const sel=document.getElementById('cfgThemeSelect');
if(sel&&sel.value!==t)sel.value=t;
localStorage.setItem('sparrow-theme',t);
}
function cycleTheme(){
setTheme(THEMES[(THEMES.indexOf(getTheme())+1)%THEMES.length]);
}
function initTheme(){
const queryTheme=new URLSearchParams(location.search).get('theme');
if(THEMES.includes(queryTheme)){
setTheme(queryTheme);
return;
}
const stored=localStorage.getItem('sparrow-theme');
const prefersLight=window.matchMedia('(prefers-color-scheme: light)').matches;
setTheme(stored||(prefersLight?'paper':'captain'));
}
let audioCtx=null;
let muted=localStorage.getItem('sparrow-muted')==='1';
let userGestured=false;
function armAudio(){
userGestured=true;
if(audioCtx&&audioCtx.state==='suspended')audioCtx.resume().catch(()=>{});
}
['pointerdown','keydown'].forEach(ev=>window.addEventListener(ev,armAudio,{once:true}));
function chirp(freq=900,dur=0.08){
if(muted||!userGestured)return;
try{
if(!audioCtx)audioCtx=new (window.AudioContext||window.webkitAudioContext)();
const o=audioCtx.createOscillator(),g=audioCtx.createGain();
o.type='triangle';o.frequency.setValueAtTime(freq,audioCtx.currentTime);
o.frequency.exponentialRampToValueAtTime(freq*1.8,audioCtx.currentTime+dur);
g.gain.setValueAtTime(.16,audioCtx.currentTime);
g.gain.exponentialRampToValueAtTime(.001,audioCtx.currentTime+dur);
o.connect(g).connect(audioCtx.destination);
o.start();o.stop(audioCtx.currentTime+dur);
}catch(_){}
}
function syncSoundBtn(){const b=document.getElementById('soundBtn');if(b)b.textContent=muted?'🔇':'🔊'}
async function loadMcpList(){
const el=document.getElementById('mcpList');if(!el)return;
el.innerHTML='<span style="color:var(--dimmer)">loading…</span>';
try{
const j=await fetch('/mcp/list').then(r=>r.json());
const servers=j.servers||[];
if(!servers.length){
el.innerHTML=`<div class="drw-empty">No MCP connectors configured.<br><span style="font-size:10px">Add one: <code>sparrow mcp add <name> --cmd "npx -y @modelcontextprotocol/server-filesystem"</code></span></div>`;
return;
}
el.innerHTML=servers.map(s=>`<div class="drw-row" style="cursor:default">
<div class="ttl">${esc(s.name)} <span style="color:var(--dimmer);font-size:9px;border:1px solid var(--line);border-radius:999px;padding:0 6px;margin-left:6px">${esc(s.transport||'stdio')}</span></div>
<div class="meta"><span>${esc(s.command?[s.command,...(s.args||[])].join(' '):(s.url||''))}</span>${(s.allow_tools||[]).length?`<span>allow: ${esc(s.allow_tools.join(', '))}</span>`:''}</div>
</div>`).join('');
}catch(_){el.innerHTML='<div class="drw-empty">MCP list unavailable (console offline).</div>';}
}
async function loadHooksList(){
const el=document.getElementById('hooksList');if(!el)return;
el.innerHTML='<span style="color:var(--dimmer)">loading…</span>';
try{
const j=await fetch('/hooks').then(r=>r.json());
const hooks=j.hooks||[];
if(!hooks.length){
el.innerHTML='<div class="drw-empty">No hooks configured.<br><span style="font-size:10px">Add <code>[[hooks]]</code> entries (id, event, command) to your config.toml.</span></div>';
return;
}
el.innerHTML=hooks.map(h=>`<div class="drw-row" style="cursor:default">
<div class="ttl">${esc(h.id||'(hook)')} <span style="color:${h.enabled===false?'var(--rem)':'var(--add)'};font-size:9px;margin-left:6px">${h.enabled===false?'disabled':'enabled'}</span></div>
<div class="meta"><span>${esc(typeof h.event==='string'?h.event:JSON.stringify(h.event))}</span><span>${esc(h.command||'')}</span>${h.matcher?`<span>match: ${esc(h.matcher)}</span>`:''}${h.blocking?'<span>blocking</span>':''}</div>
</div>`).join('');
}catch(_){el.innerHTML='<div class="drw-empty">Hooks list unavailable (console offline).</div>';}
}
async function replayRun(runId){
try{
const r=await fetch('/replay'+(runId?'?run_id='+encodeURIComponent(runId):''));
const j=await r.json();
if(j.ok&&Array.isArray(j.events)&&j.events.length){
term.innerHTML='';
line(`<span class="sec">— replay · ${esc((j.task||j.run_id||'').slice(0,90))} —</span>`);
j.events.forEach(ev=>{try{handleEvent(ev)}catch(_){}});
line('<span class="sec">— end of replay —</span>');
return true;
}
toast(j.message||'No recorded runs yet — run a task first.','error');
}catch(e){toast('Replay unavailable: '+e.message,'error');}
return false;
}
function toggleReplayPicker(anchor){
const old=document.getElementById('replayDrop');
if(old){old.remove();return;}
const drop=document.createElement('div');
drop.id='replayDrop';drop.className='mp-drop';
const r=anchor.getBoundingClientRect();
drop.style.cssText=`position:fixed;top:${r.bottom+6}px;right:${Math.max(8,innerWidth-r.right)}px;left:auto;bottom:auto;width:380px;z-index:1500`;
drop.innerHTML='<div class="mp-drop-head">replay a recorded run</div><div id="replayDropList"><div style="padding:12px 14px;color:var(--dimmer);font-size:11px">loading…</div></div>';
document.body.appendChild(drop);
const close=e=>{if(!drop.contains(e.target)&&e.target!==anchor){drop.remove();document.removeEventListener('pointerdown',close);}};
document.addEventListener('pointerdown',close);
fetch('/replays').then(r=>r.json()).then(j=>{
const host=document.getElementById('replayDropList');if(!host)return;
const items=(j.replays||[]).slice(0,12);
if(!items.length){host.innerHTML='<div style="padding:12px 14px;color:var(--dimmer);font-size:11px">No recorded runs yet — finish a task and it appears here.</div>';return;}
host.innerHTML=items.map(it=>
`<div class="mp-item" data-replay-id="${escAttr(it.run_id)}">
<span class="mp-name" title="${escAttr(rbShortText(it.task||it.run_id,140))}">${esc(rbShortText(it.task||it.run_id,60)||it.run_id)}</span>
<span class="mp-ctx">${esc(String(it.event_count||0))} ev</span>
<span class="mp-cost">${esc((it.created_at||'').slice(5,16))}</span>
</div>`).join('');
host.querySelectorAll('[data-replay-id]').forEach(el=>el.addEventListener('click',()=>{
drop.remove();replayRun(el.dataset.replayId);
}));
}).catch(()=>{
const host=document.getElementById('replayDropList');
if(host)host.innerHTML='<div style="padding:12px 14px;color:var(--dimmer);font-size:11px">Replay list unavailable (console offline).</div>';
});
}
function bindChromeChips(){
const focusBtn=document.getElementById('focusModeBtn');
const cockpitBtn=document.getElementById('cockpitModeBtn');
if(focusBtn)focusBtn.addEventListener('click',()=>setViewMode('focus'));
if(cockpitBtn)cockpitBtn.addEventListener('click',()=>setViewMode('cockpit'));
const cmdk=document.getElementById('cmdkBtn');if(cmdk)cmdk.addEventListener('click',()=>{chirp(1000,.04);paletteOpen()});
const kbd=document.getElementById('kbdBtn');if(kbd)kbd.addEventListener('click',()=>{chirp(900,.04);toggleKbdHelp();});
const rb=document.getElementById('replayBtn');if(rb)rb.addEventListener('click',async()=>{
chirp(800,.05);setTimeout(()=>chirp(1200,.05),80);
toggleReplayPicker(rb);
});
const sb=document.getElementById('soundBtn');if(sb)sb.addEventListener('click',()=>{
muted=!muted;localStorage.setItem('sparrow-muted',muted?'1':'0');syncSoundBtn();if(!muted)chirp(1200,.06);
});
const tb=document.getElementById('themeBtn');if(tb)tb.addEventListener('click',()=>{
cycleTheme();chirp(900,.05);
});
const fd=document.getElementById('fontDownBtn');if(fd)fd.addEventListener('click',()=>{setReadScale(getReadScale()-0.1);chirp(700,.03);});
const fu=document.getElementById('fontUpBtn');if(fu)fu.addEventListener('click',()=>{setReadScale(getReadScale()+0.1);chirp(1100,.03);});
document.addEventListener('keydown',e=>{
if((e.metaKey||e.ctrlKey)&&e.shiftKey&&e.key.toLowerCase()==='l'){e.preventDefault();if(tb)tb.click();}
if((e.metaKey||e.ctrlKey)&&!e.shiftKey&&e.key.toLowerCase()==='m'){e.preventDefault();if(sb)sb.click();}
if((e.metaKey||e.ctrlKey)&&(e.key==='+'||e.key==='=')){e.preventDefault();if(fu)fu.click();}
if((e.metaKey||e.ctrlKey)&&(e.key==='-'||e.key==='_')){e.preventDefault();if(fd)fd.click();}
if(e.altKey&&e.key.toLowerCase()==='f'){e.preventDefault();toggleViewMode();}
if(e.altKey&&e.key.toLowerCase()==='a'){e.preventDefault();focusAccept();}
if(e.altKey&&e.key.toLowerCase()==='u'){e.preventDefault();focusUndo();}
if(e.altKey&&e.key.toLowerCase()==='e'){e.preventDefault();focusExplain();}
const mod=e.metaKey||e.ctrlKey;
const k=e.key.toLowerCase();
if(mod&&e.shiftKey&&k==='s'){e.preventDefault();toggleRightSidebar();}
else if(mod&&!e.shiftKey&&k==='p'){e.preventDefault();openRightSidebar('preview','shortcut');}
else if(mod&&e.shiftKey&&k==='p'){e.preventDefault();openRightSidebar('plan','shortcut');}
else if(mod&&e.shiftKey&&k==='d'){e.preventDefault();openRightSidebar('diff','shortcut');}
else if(mod&&e.shiftKey&&k==='f'){e.preventDefault();openRightSidebar('files','shortcut');}
else if(e.ctrlKey&&!e.shiftKey&&!e.altKey&&e.key==='`'){e.preventDefault();openRightSidebar('terminal','shortcut');}
else if(mod&&!e.shiftKey&&e.key==='/'){e.preventDefault();toggleKbdHelp();}
else if(e.key==='Escape'&&document.getElementById('kbdHelp')?.classList.contains('show')){toggleKbdHelp(false);}
});
}
function setViewMode(mode){
mode=mode==='cockpit'?'cockpit':'focus';
if(mode!=='focus'&&typeof window.__sparrowDismissFocusTour==='function')window.__sparrowDismissFocusTour();
document.documentElement.dataset.view=mode;
localStorage.setItem('sparrow-view-mode',mode);
const focusBtn=document.getElementById('focusModeBtn');
const cockpitBtn=document.getElementById('cockpitModeBtn');
if(focusBtn)focusBtn.setAttribute('aria-pressed',String(mode==='focus'));
if(cockpitBtn)cockpitBtn.setAttribute('aria-pressed',String(mode==='cockpit'));
setTimeout(()=>document.getElementById('taskInput')?.focus(),30);
}
function toggleViewMode(){
setViewMode((document.documentElement.dataset.view||'focus')==='focus'?'cockpit':'focus');
}
function initViewMode(){
setViewMode(localStorage.getItem('sparrow-view-mode')||'focus');
}
function focusAccept(){
const modal=document.getElementById('approvalModal');
if(modal&&modal.classList.contains('show')){document.getElementById('approvalApprove')?.click();return;}
const inline=document.querySelector('.approval-actions .approve:not(:disabled)');
if(inline){inline.click();return;}
if(runActive){toast('Approval sent to the active run.','ok');return;}
runTask();
}
async function focusUndo(){
await runWebviewCliCommand('/annule');
}
async function focusExplain(){
const input=document.getElementById('taskInput');
const selected=(window.getSelection&&String(window.getSelection()).trim())||'';
input.value=selected?`Explain this simply: ${selected}`:'Explain simply what is happening and what I should do next.';
autoResizeComposer();
await runTask();
}
let micRecognition=null;
let micListening=false;
function toggleMicDictation(){
const SpeechRecognition=window.SpeechRecognition||window.webkitSpeechRecognition;
const btn=document.getElementById('micBtn');
if(!SpeechRecognition){
toast('Microphone unavailable in this browser. Use `sparrow voice transcribe` in the terminal.','error');
return;
}
if(micListening&&micRecognition){
micRecognition.stop();
return;
}
micRecognition=new SpeechRecognition();
micRecognition.lang=(navigator.language||'fr-FR');
micRecognition.interimResults=true;
micRecognition.continuous=false;
let finalText='';
micRecognition.onstart=()=>{micListening=true;if(btn){btn.classList.add('listening');btn.setAttribute('aria-pressed','true');}};
micRecognition.onend=()=>{micListening=false;if(btn){btn.classList.remove('listening');btn.setAttribute('aria-pressed','false');}};
micRecognition.onerror=ev=>toast('Microphone stopped: '+(ev.error||'unknown error'),'error');
micRecognition.onresult=ev=>{
let interim='';
for(let i=ev.resultIndex;i<ev.results.length;i++){
const chunk=ev.results[i][0]?.transcript||'';
if(ev.results[i].isFinal)finalText+=chunk;
else interim+=chunk;
}
const input=document.getElementById('taskInput');
const base=(input.dataset.micBase||input.value||'').trim();
input.value=[base,finalText.trim(),interim.trim()].filter(Boolean).join(' ');
input.dataset.micBase=base;
autoResizeComposer();
updateContextMeter();
};
const input=document.getElementById('taskInput');
input.dataset.micBase=input.value||'';
micRecognition.start();
}
function initFocusTour(){
if(localStorage.getItem('sparrow-focus-tour-done')==='1')return;
const steps=[
['#taskInput','Your starting point','Type or dictate what you want to fix. Sparrow prepares the rest.'],
['#focusOkBtn','One confirmation','When Sparrow asks for approval, this button answers without hunting through panels.'],
['#focusUndoBtn','Go back fast','Undo the last action quickly if the result is not right.'],
['#cockpitModeBtn','Cockpit mode','Switch to Cockpit when you want details, routing, and metrics.']
];
let i=0;
const pop=document.createElement('div');
pop.className='tour-pop';
pop.setAttribute('role','dialog');
pop.setAttribute('aria-live','polite');
document.body.appendChild(pop);
function done(){
localStorage.setItem('sparrow-focus-tour-done','1');
window.removeEventListener('resize',render);
window.removeEventListener('scroll',render,true);
pop.remove();
}
window.__sparrowDismissFocusTour=done;
function placeTour(target){
const r=target.getBoundingClientRect();
const margin=12;
const guards=['.focus-actions','.input-bar']
.map(sel=>document.querySelector(sel)?.getBoundingClientRect())
.filter(rect=>rect&&rect.height>0&&rect.top>0)
.map(rect=>rect.top);
const bottomGuard=(guards.length?Math.min(...guards):window.innerHeight)-margin;
const popW=Math.min(300,window.innerWidth-28);
const popH=Math.min(190,pop.offsetHeight||150);
const candidates=[
{top:r.top,left:r.right+margin},
{top:r.bottom+margin,left:r.left},
{top:r.top,left:r.left-popW-margin},
{top:r.top-popH-margin,left:r.left},
];
let best=candidates.find(p=>
p.left>=margin&&p.left+popW<=window.innerWidth-margin&&
p.top>=margin&&p.top+popH<=bottomGuard
)||candidates[0];
best.left=Math.min(window.innerWidth-popW-margin,Math.max(margin,best.left));
best.top=Math.min(bottomGuard-popH,Math.max(margin,best.top));
if(!Number.isFinite(best.top)||best.top<margin)best.top=margin;
pop.style.left=best.left+'px';
pop.style.top=best.top+'px';
}
function render(){
const [sel,title,body]=steps[i];
const target=document.querySelector(sel);
if(!target){done();return;}
pop.innerHTML=`<b>${escHtml(title)}</b><div>${escHtml(body)}</div><div class="tour-actions"><button type="button" data-tour="skip">Skip</button><button class="primary" type="button" data-tour="next">${i===steps.length-1?'Done':'Next'}</button></div>`;
placeTour(target);
pop.querySelector('[data-tour="skip"]').addEventListener('click',done);
pop.querySelector('[data-tour="next"]').addEventListener('click',()=>{i++;if(i>=steps.length)done();else render();});
}
window.addEventListener('resize',render);
window.addEventListener('scroll',render,true);
setTimeout(render,650);
}
function mirrorContextPct(){
const src=document.getElementById('contextText');const dst=document.getElementById('ctxHintPct');
if(!src||!dst)return;
const m=src.textContent.match(/^([\d.]+%)/);if(m)dst.textContent=m[1];
}
const _mirrorIv=setInterval(mirrorContextPct,800);
const _heroRefreshIv=setInterval(()=>{hydrateHero();},15000);
initTheme();
initReadScale();
initViewMode();
syncSoundBtn();
(function(){
const termEl=document.getElementById('term');
if(!termEl)return;
const ro=new ResizeObserver(function(entries){
for(var i=0;i<entries.length;i++){
var w=entries[i].contentRect.width;
var base=Math.round(11+Math.min(3,Math.max(0,(w-360)/210)));
_termBaseFont=base;
applyTermFont();
termEl.style.padding=w<520?'12px 14px 18px':'';
}
});
ro.observe(termEl);
})();
bindChromeChips();
const PANEL_LOADERS={
route:loadRoutePanel,
crew:loadCrewPanel,
sessions:loadSessionsPanel,
memory:loadMemoryPanel,
plugins:loadPluginsPanel,
tools:loadToolsPanel,
skills:loadSkillsPanel,
permissions:loadPermissionsPanel,
security:loadSecurityPanel,
artifacts:loadArtifactsPanel,
};
function openPanel(name,opts){
opts=opts||{};
if(opts.reveal!==false&&window.innerWidth<=980)document.body.classList.add('drawer-open');
document.querySelectorAll('.rail .ico').forEach(b=>b.classList.toggle('active',b.dataset.panel===name));
document.querySelectorAll('.drawer .panel').forEach(p=>p.classList.toggle('active',p.dataset.panelBody===name));
localStorage.setItem('sparrow-active-panel',name);
const loader=PANEL_LOADERS[name];if(loader)loader().catch(()=>{});
const tgt=document.querySelector('.drawer .panel[data-panel-body="'+name+'"]');
if(tgt&&tgt.scrollIntoView)tgt.scrollIntoView({behavior:'smooth',block:'start'});
}
document.addEventListener('click',e=>{
const ico=e.target.closest('.rail .ico[data-panel]');
if(ico)openPanel(ico.dataset.panel);
});
function restoreActivePanel(){
const stored=localStorage.getItem('sparrow-active-panel');
const name=(stored&&PANEL_LOADERS[stored])?stored:'sessions';
openPanel(name,{reveal:window.innerWidth>980});
if(!isFastStart()){
scheduleIdle(()=>Object.values(PANEL_LOADERS).forEach(fn=>fn().catch(()=>{})),1200);
}
setTimeout(()=>{
const active=document.querySelectorAll('.drawer .panel.active');
if(active.length!==1){
document.querySelectorAll('.drawer .panel').forEach(p=>p.classList.remove('active'));
const target=document.querySelector(`.drawer .panel[data-panel-body="${name}"]`);
if(target)target.classList.add('active');
}
},0);
}
function escHtml(s){return String(s||'').replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">",'"':""","'":"'"}[c]))}
let _currentRouteChain=[];let _currentRouteCtx=32000;
async function loadRoutePanel(){
const host=document.getElementById('drw-route-body');if(!host)return;
let configured=window._configuredProviders||new Set();
try{
const cfg=await fetch('/config').then(r=>r.json());
configured=new Set((cfg.providers||[]).filter(p=>p.has_credential||p.configured).map(p=>p.name||p.id));
window._configuredProviders=configured;
}catch{}
if(!_currentRouteChain.length){
host.innerHTML='<div class="drw-empty">start a run to see the active route chain</div>';
return;
}
const chain=_currentRouteChain;
const ctxFmt=fmtCtx(_currentRouteCtx);
let modelCosts={};
try{
const md=await fetch('/models').then(r=>r.json());
(md.providers||[]).forEach(p=>{
p.models.forEach(m=>{modelCosts[p.id+':'+m.name]={ctx:m.context_window,cost_in:m.cost_input_per_mtok,cost_out:m.cost_output_per_mtok};});
});
}catch{}
const currentBox=`<div class="route-current-box">
<div class="rc-label">active model</div>
<div class="rc-model">${escHtml(chain[0]||'—')}</div>
<div class="rc-ctx">context ${escHtml(ctxFmt)} · ${escHtml(chain.length)} hop${chain.length!==1?'s':''} in chain</div>
</div>`;
const hops=chain.map((id,i)=>{
const info=modelCosts[id]||{};
const active=i===0;
const hasKey=configured.has(id.split(':')[0]);
const ledClass=active?'active':(hasKey?'standby':'error');
const ctxStr=info.ctx?fmtCtx(info.ctx):'—';
const costStr=info.cost_in!==undefined?(info.cost_in===0?'free':'$'+info.cost_in+'/M'):'—';
return `<div class="route-hop ${ledClass}">
<span class="rh-led"></span>
<div class="rh-info">
<div class="rh-name${active?' active':''}">${escHtml(id)}</div>
<div class="rh-meta"><span class="rh-ctx">ctx ${escHtml(ctxStr)}</span><span class="rh-cost">${escHtml(costStr)}</span>${!hasKey?'<span style="color:var(--rem);font-size:9px">no key</span>':''}</div>
</div>
<span class="rh-pos">#${i+1}</span>
</div>`;
}).join('');
host.innerHTML=currentBox+hops;
}
function updateRoutePanel(chain,ctxWindow){
_currentRouteChain=chain||[];_currentRouteCtx=ctxWindow||_currentRouteCtx;
const active=document.querySelector('.drawer .panel.active[data-panel-body="route"]');
if(active)loadRoutePanel();
}
function updateCrewLiveStatus(name,status,note){
const list=document.getElementById('drw-crew-list');if(!list)return;
const items=list.querySelectorAll('[data-agent-name]');
items.forEach(el=>{
if(el.dataset.agentName===name){
const pill=el.querySelector('[data-crew-status]');
if(pill){pill.textContent=status;
const stMap={working:'var(--agent)',done:'var(--add)',error:'var(--rem)',idle:'var(--dimmer)'};
pill.style.color=stMap[(status||'').toLowerCase()]||'var(--steel)';
if(note){const nd=el.querySelector('[data-crew-note]');if(nd)nd.textContent=note.slice(0,40);}
}
}
});
}
async function newConversation(){
if(!confirm('Clear retained conversation context and start fresh?'))return;
try{
await fetch('/conversation/reset',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'});
localStorage.removeItem('sparrow-turn-count');
const term=document.getElementById('term');
const banner=document.createElement('div');
banner.style.cssText='margin:10px 0;padding:8px 14px;border-radius:8px;background:color-mix(in srgb,var(--add) 10%,transparent);border:1px solid color-mix(in srgb,var(--add) 35%,transparent);color:var(--add);font-size:11px';
banner.textContent='⟲ conversation cleared · context retained from this point forward';
term.appendChild(banner);term.scrollTop=term.scrollHeight;
}catch(e){toast('reset failed: '+e.message,'error');}
}
let ACTIVE_AGENT = null;
function activateAgent(name) {
document.querySelectorAll('[data-agent-name]').forEach(el => {
if (el.dataset.agentName === name) {
el.style.background = 'color-mix(in srgb,var(--brand) 12%,transparent)';
el.style.borderLeft = '2px solid var(--brand)';
} else {
el.style.background = 'transparent';
el.style.borderLeft = '';
}
});
ACTIVE_AGENT = name;
const inp = document.getElementById('taskInput');
if (inp && name) {
inp.value = '@' + name + ' ' + inp.value.replace(/^@\S+\s*/, '');
inp.focus();
}
const coderWho = document.querySelector('#swarm-coder .who');
if (coderWho) coderWho.textContent = name;
}
async function loadCrewPanel(){
const host=document.getElementById('drw-crew-list');
try{
const r=await fetch('/agents');const j=await r.json();
const agents=Array.isArray(j.agents)?j.agents:[];
document.getElementById('drw-crew-count').textContent=agents.length;
if(!agents.length){host.innerHTML='<div class="drw-empty">no agents installed · create a <code>.agent.md</code> in <code>~/.config/sparrow/agents/</code></div>';return;}
const colorMap={planner:'var(--planner)',coder:'var(--agent)',verifier:'var(--verifier)',gold:'var(--gold)',coral:'var(--coral)',steel:'var(--steel)'};
host.innerHTML=agents.map(a=>{
const col=colorMap[a.color_key]||'var(--steel)';
return `<div data-agent-name="${escHtml(a.name)}" onclick="activateAgent('${escHtml(a.name)}')" style="cursor:pointer;padding:9px 10px;border-bottom:1px solid var(--line);display:flex;flex-direction:column;gap:3px;transition:background .15s" onmouseenter="this.style.background='color-mix(in srgb,var(--brand) 8%,transparent)'" onmouseleave="this.style.background='transparent'">
<div style="display:flex;align-items:center;gap:7px">
<span style="width:7px;height:7px;border-radius:50%;background:${col};box-shadow:0 0 7px ${col};flex:0 0 auto"></span>
<span style="font-weight:600;font-size:11.5px;color:${col}">${escHtml(a.name)}</span>
<span style="margin-left:auto;font-size:9.5px;color:var(--dimmer);letter-spacing:.5px">${escHtml(a.role)}</span>
</div>
${a.description?`<div style="font-size:10.5px;color:var(--dim);padding-left:14px;line-height:1.4">${escHtml(a.description)}</div>`:''}
<div style="padding-left:14px;margin-top:3px;display:flex;gap:6px;align-items:center">
<span data-crew-status style="font-size:9px;color:var(--dimmer);background:color-mix(in srgb,${col} 10%,transparent);border:1px solid color-mix(in srgb,${col} 25%,transparent);padding:1px 7px;border-radius:999px">${escHtml(a.status||'idle')}</span>
<span data-crew-note style="font-size:9.5px;color:var(--dimmer)"></span>
</div>
</div>`;
}).join('');
}catch{host.innerHTML='<div class="drw-empty">⚠ could not reach /agents</div>';}
}
async function loadSessionsPanel(){
const host=document.getElementById('drw-sessions-list');
try{
const r=await fetch('/sessions');const j=await r.json();
const sess=Array.isArray(j.sessions)?j.sessions:[];
document.getElementById('drw-sessions-count').textContent=sess.length;
if(!sess.length){host.innerHTML='<div class="drw-empty">_none yet_ · start your first run.</div>';return;}
host.innerHTML=sess.slice(0,20).map(s=>{
const ts=s.updated_at?new Date(s.updated_at*1000).toLocaleString('fr-FR',{hour:'2-digit',minute:'2-digit',day:'2-digit',month:'2-digit'}):'';
const turns=(s.messages_json||'[]').match(/"role":/g)?.length||0;
return `<div class="drw-row" data-session-id="${escHtml(s.id)}" title="Click to load · ${escHtml(s.id)}" style="cursor:pointer"><div class="ttl">${escHtml(s.name||s.id)}</div><div class="meta"><span><b>${turns}</b> turns</span><span>${escHtml(s.status||'')}</span><span>${ts}</span></div></div>`;
}).join('');
host.querySelectorAll('.drw-row[data-session-id]').forEach(row=>{
row.addEventListener('click',async ()=>{
const id=row.getAttribute('data-session-id');
host.querySelectorAll('.drw-row').forEach(r=>r.classList.remove('cur'));
row.classList.add('cur');
try{
const rr=await fetch('/sessions/load',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id})});
const jj=await rr.json();
if(jj.ok){
const termEl=document.getElementById('term');
if(termEl) termEl.innerHTML='';
localStorage.setItem('sparrow-turn-count','0');
const session=sess.find(s=>s.id===id);
let msgs=[];
try{msgs=JSON.parse(session?.messages_json||'[]');}catch(_){}
msgs.forEach(m=>{
const role=(m.role||'').toLowerCase();
const text=(typeof m.content==='string'?m.content:JSON.stringify(m.content||'')).trim();
if(!text)return;
const cls=role==='user'?'user':role==='assistant'?'agent':'muted';
const label=role==='user'?'you':(role==='assistant'||!role||role==='guard')?'sparrow':('sparrow-'+role);
line(`<span class="${cls}"><b>${esc(label)}</b> · ${esc(text).slice(0,4000).replace(/\n/g,'<br>')}</span>`);
});
line(`<span class="muted">↳ session restored · ${esc(session?.name||id)} · ${msgs.length} turns</span>`);
toast(`session loaded · ${id.slice(0,18)}`,'ok');
}
else{toast('load failed: '+(jj.message||''),'error');}
}catch(e){toast('load failed: '+e.message,'error');}
});
});
}catch(_){host.innerHTML='<div class="drw-empty">_none yet_ · unable to load /sessions</div>';}
}
async function loadMemoryPanel(){
const host=document.getElementById('drw-memory-body');
try{
const r=await fetch('/memory');const j=await r.json();
const facts=Array.isArray(j.facts)?j.facts:[];
const docs=Array.isArray(j.docs)?j.docs:[];
let html='';
docs.forEach(d=>{
const used=d.chars||0,lim=d.limit||1;
const pct=Math.min(100,Math.round(used/lim*100));
html+=`<div class="mem-doc"><div class="label">${escHtml(d.kind||'doc')} <b>${used} / ${lim}</b></div><div class="bar"><span style="width:${pct}%"></span></div></div>`;
});
html+=`<div class="mem-doc"><div class="label">Facts <b>${facts.length}</b></div></div>`;
if(facts.length){
html+='<div style="margin-top:6px">'+facts.slice(0,8).map(f=>`<div class="drw-row"><div class="ttl">${escHtml(f.key)}</div><div class="meta"><span>${escHtml((f.value||'').slice(0,80))}</span></div></div>`).join('')+'</div>';
}
host.innerHTML=html||'<div class="drw-empty">_none yet_ · MEMORY.md is empty, Sparrow will learn as you go.</div>';
}catch(_){host.innerHTML='<div class="drw-empty">_none yet_ · unable to load /memory</div>';}
}
async function loadPluginsPanel(){
const host=document.getElementById('drw-plugins-list');
try{
const r=await fetch('/plugins');const j=await r.json();
const ps=Array.isArray(j.plugins)?j.plugins:[];
document.getElementById('drw-plugins-count').textContent=ps.length;
if(!ps.length){host.innerHTML='<div class="drw-empty">_none yet_ · try <code>sparrow plugins install</code></div>';return;}
host.innerHTML=ps.map(p=>`<div class="drw-row"><div class="ttl">${escHtml(p.name)}</div><div class="meta"><span>${escHtml(p.version||'')}</span><span>${p.commands||0} cmds</span><span>${p.skills||0} skills</span></div></div>`).join('');
}catch(_){host.innerHTML='<div class="drw-empty">_none yet_ · unable to load /plugins</div>';}
}
async function loadToolsPanel(){
const host=document.getElementById('drw-tools-list');
try{
const r=await fetch('/tools');const j=await r.json();
const ts=Array.isArray(j.tools)?j.tools:[];
document.getElementById('drw-tools-count').textContent=ts.length;
if(!ts.length){host.innerHTML='<div class="drw-empty">_none yet_ · no tools metadata loaded</div>';return;}
host.innerHTML=ts.map(t=>`<div class="drw-row" title="${escHtml(t.toolset||'')}"><div class="ttl">${escHtml(t.name)}</div><div class="meta"><span>${escHtml(t.toolset||'')}</span>${t.exec?'<span style="color:var(--rem)">exec</span>':''}${t.mutates_files?'<span style="color:var(--verifier)">mutates</span>':''}${t.network?'<span>net</span>':''}</div></div>`).join('');
}catch(_){host.innerHTML='<div class="drw-empty">_none yet_ · unable to load /tools</div>';}
}
async function loadSkillsPanel(){
const host=document.getElementById('drw-skills-list');
try{
const r=await fetch('/skills');
if(!r.ok){host.innerHTML='<div class="drw-empty">_none yet_ · skills library is empty.</div>';document.getElementById('drw-skills-count').textContent='0';return;}
const j=await r.json();
const sk=Array.isArray(j.skills)?j.skills:(Array.isArray(j.items)?j.items:[]);
document.getElementById('drw-skills-count').textContent=sk.length;
if(!sk.length){host.innerHTML='<div class="drw-empty">_none yet_ · skills library is empty. Sparrow will learn as you go.</div>';return;}
host.innerHTML=sk.map(s=>`<div class="drw-row" title="${escHtml(s.description||s.summary||'')}"><div class="ttl">${escHtml(s.name||s.id||'skill')}</div><div class="meta"><span>${escHtml((s.description||s.summary||'').slice(0,90))}</span>${s.uses?`<span>×${s.uses}</span>`:''}</div></div>`).join('');
}catch(_){host.innerHTML='<div class="drw-empty">_none yet_ · unable to load /skills</div>';document.getElementById('drw-skills-count').textContent='0';}
}
async function loadPermissionsPanel(){
const host=document.getElementById('drw-permissions-body');
try{
const r=await fetch('/permissions');const j=await r.json();
const p=j.permissions||{};
const tools=p.tools||{};
const paths=p.paths||{};
host.innerHTML=
`<div class="mem-doc"><div class="label">Mode <b>${escHtml(p.mode||'unknown')}</b></div></div>`+
`<div class="mem-doc"><div class="label">Tools allow <b>${(tools.allow||[]).length}</b></div></div>`+
`<div class="mem-doc"><div class="label">Tools deny <b>${(tools.deny||[]).length}</b></div></div>`+
`<div class="mem-doc"><div class="label">Paths deny <b>${(paths.deny||[]).length}</b></div></div>`;
}catch(_){host.innerHTML='<div class="drw-empty">_none yet_ · unable to load /permissions</div>';}
}
async function loadSecurityPanel(){
const host=document.getElementById('drw-security-body');
try{
const r=await fetch('/security');const j=await r.json();
const a=j.audit||{};const findings=Array.isArray(a.findings)?a.findings:[];
const score=a.score??100;
const klass=score>=80?'':score>=50?'warn':'crit';
let html=`<div class="sec-score"><div class="num ${klass}">${score}</div><div style="font-size:11px;color:var(--dim)">/ 100<div style="color:var(--dimmer);font-size:9.5px;letter-spacing:.5px">${findings.filter(f=>(f.severity==='Critical')).length} crit · ${findings.filter(f=>(f.severity==='Warning')).length} warn</div></div></div>`;
html+=findings.slice(0,12).map(f=>{
const kl=f.severity==='Critical'?'crit':f.severity==='Warning'?'warn':'';
return `<div class="sec-finding ${kl}"><div class="cat">${escHtml(f.category||'')}</div><div class="msg">${escHtml(f.message||'')}</div></div>`;
}).join('');
host.innerHTML=html;
}catch(_){host.innerHTML='<div class="drw-empty">_none yet_ · unable to load /security</div>';}
}
window.SPARROW_WRITTEN=window.SPARROW_WRITTEN||new Map();
function recordWrittenFile(file){
if(!file)return;
const prev=window.SPARROW_WRITTEN.get(file)||{count:0};
window.SPARROW_WRITTEN.set(file,{count:prev.count+1,ts:Date.now()});
const badge=document.getElementById('drw-artifacts-count');
if(badge){
const uploads=parseInt(badge.dataset.uploads||'0',10);
badge.textContent=String(uploads+window.SPARROW_WRITTEN.size);
}
}
async function loadArtifactsPanel(){
const host=document.getElementById('drw-artifacts-list');
try{
const r=await fetch('/artifacts');const j=await r.json();
const items=Array.isArray(j.items)?j.items:[];
const generated=items.filter(it=>it.source==='generated');
const uploads=items.filter(it=>it.source!=='generated');
const written=[...window.SPARROW_WRITTEN.entries()].sort((a,b)=>b[1].ts-a[1].ts);
const badge=document.getElementById('drw-artifacts-count');
badge.dataset.uploads=String(items.length);
badge.textContent=String(items.length+written.length);
if(!items.length && !written.length){host.innerHTML='<div class="drw-empty">_none yet_ · deliverables land in <b>./artifacts/</b>; drag a file onto the page to upload.</div>';return;}
let html='';
const fileRow=(name,path,meta)=>`<div class="drw-row" title="${escHtml(path||name)}"><div class="ttl">${escHtml(name)}</div><div class="meta"><span>${escHtml(meta||'')}</span></div></div>`;
if(generated.length){
html+='<div style="font-size:10px;color:var(--add);letter-spacing:1px;text-transform:uppercase;margin:4px 6px">deliverables · ./artifacts/</div>';
html+=generated.map(it=>fileRow(it.name,it.path,`${it.kind||'file'} · ${Math.round((it.size||0)/1024)} KB`)).join('');
}
if(written.length){
html+='<div style="font-size:10px;color:var(--dim);letter-spacing:1px;text-transform:uppercase;margin:8px 6px 4px">edited by sparrow</div>';
html+=written.map(([file,meta])=>fileRow(file.split(/[\\/]/).pop()||file,file,`${file} · ×${meta.count}`)).join('');
}
if(uploads.length){
html+='<div style="font-size:10px;color:var(--dim);letter-spacing:1px;text-transform:uppercase;margin:8px 6px 4px">uploaded</div>';
html+=uploads.map(it=>fileRow(it.name,it.path,`${it.kind||'file'} · ${Math.round((it.size||0)/1024)} KB`)).join('');
}
host.innerHTML=html;
}catch(_){host.innerHTML='<div class="drw-empty">_none yet_ · unable to load /artifacts</div>';}
}
const SWARM_TRIAD=new Set(['planner','coder','verifier']);
const SWARM_MAX_VISIBLE=8;
async function loadSwarmAgents(){
try{
const r=await fetch('/agents');
if(!r.ok)return;
const j=await r.json();
renderSwarmExtras(Array.isArray(j.agents)?j.agents:[]);
}catch(_){}
}
function renderSwarmExtras(agents){
const anchor=document.getElementById('swarm-extras-anchor');
const more=document.getElementById('swarm-more');
if(!anchor||!more)return;
let n=anchor.nextElementSibling;
while(n&&n!==more){const next=n.nextElementSibling;if(n.id&&n.id.startsWith('swarm-extra-'))n.remove();n=next;}
const extras=agents.filter(a=>!SWARM_TRIAD.has(a.name.toLowerCase())&&!SWARM_TRIAD.has((a.role||'').toLowerCase()));
const visible=extras.slice(0,SWARM_MAX_VISIBLE);
const overflow=extras.length-visible.length;
visible.forEach(a=>{
const lane=document.createElement('div');
const colorKey=a.color_key||'steel';
lane.className=`lane ${colorKey} ${a.status==='working'?'working':a.status==='done'?'done':a.status==='error'?'error':'idle idle-card'}`;
lane.id=`swarm-extra-${cssSlug(a.name)}`;
lane.title=`${a.name} — ${a.role||'agent'}${a.description?` · ${a.description}`:''}`;
const head=document.createElement('div');head.className='lane-head';
const who=document.createElement('span');who.className='who';who.textContent=a.name;
const st=document.createElement('span');st.className='st';
const led=document.createElement('span');led.className='led-mini';
const txt=document.createElement('span');txt.setAttribute('data-role-state','');txt.textContent=a.status||'idle';
st.append(led,txt);
head.append(who,st);
const msg=document.createElement('span');msg.className='msg';msg.setAttribute('data-role-msg','');
msg.textContent=(a.msg&&a.msg.length)?a.msg:(a.description||'ready');
lane.append(head,msg);
anchor.parentNode.insertBefore(lane,more);
});
if(overflow>0){
more.hidden=false;
more.textContent=`+${overflow} agent${overflow>1?'s':''}…`;
more.title=extras.slice(SWARM_MAX_VISIBLE).map(a=>a.name).join(', ');
}else{
more.hidden=true;
more.textContent='';
}
}
function cssSlug(s){return String(s).toLowerCase().replace(/[^a-z0-9-]+/g,'-').replace(/^-+|-+$/g,'')||'agent';}
let PALETTE_CMDS=[]; let PALETTE_AGENTS=[]; let PALETTE_FILTERED=[];
let PALETTE_SEL=0;
let PALETTE_LOADING=null;
let PALETTE_ERROR='';
async function loadCommandsCache(){
try{
const r=await fetch('/commands');const j=await r.json();
PALETTE_CMDS=Array.isArray(j.commands)?j.commands:[];
}catch(_){
PALETTE_ERROR='commands unavailable';
}
}
async function loadAgentsCache(){
try{
const r=await fetch('/agents');const j=await r.json();
PALETTE_AGENTS=Array.isArray(j.agents)?j.agents:[];
}catch(_){
PALETTE_AGENTS=[
{name:'planner',role:'planner',description:'decomposes the task',status:'idle',msg:'',color_key:'planner'},
{name:'coder',role:'coder',description:'edits files',status:'idle',msg:'',color_key:'coder'},
{name:'verifier',role:'verifier',description:'adversarial review',status:'idle',msg:'',color_key:'verifier'},
];
}
}
async function ensurePaletteData(){
if(PALETTE_CMDS.length&&PALETTE_AGENTS.length)return;
if(!PALETTE_LOADING){
PALETTE_ERROR='';
PALETTE_LOADING=Promise.all([loadCommandsCache(),loadAgentsCache()]).finally(()=>{PALETTE_LOADING=null;});
}
await PALETTE_LOADING;
}
async function loadHistoryCache(){
try{
const r=await fetch('/history?limit=80');
const j=await r.json();
HISTORY_INPUTS=Array.isArray(j.inputs)?j.inputs:[];
}catch(_){HISTORY_INPUTS=[]}
}
let selectedModel=localStorage.getItem('sparrow-model')||null;
let modelsCache=null;
let modelsCacheTs=0;
const MODELS_CACHE_TTL_MS=30_000;
function fmtCtxShort(n){if(n>=1000000)return(n/1e6).toFixed(0)+'M';if(n>=1000)return Math.round(n/1000)+'k';return String(n);}
function fmtCost(c){return c===0?'free':'$'+(c).toFixed(2)+'/Mtok';}
function applyModelPickerLabel(){
const lbl=document.getElementById('modelPickerLabel');
if(!lbl)return;
lbl.textContent=selectedModel?selectedModel.replace(/^[^:]+:/,'').slice(0,22):
(document.getElementById('modelPickerLabel').dataset.live||'auto');
}
async function loadModels(force){
const fresh=modelsCache&&(Date.now()-modelsCacheTs<MODELS_CACHE_TTL_MS);
if(!force&&fresh)return modelsCache;
try{const r=await fetch('/models');modelsCache=await r.json();modelsCacheTs=Date.now();}
catch{modelsCache={providers:[]};modelsCacheTs=Date.now();}
return modelsCache;
}
function invalidateModelsCache(){modelsCache=null;modelsCacheTs=0;}
async function toggleModelPicker(e){
e.stopPropagation();
const drop=document.getElementById('modelPickerDrop');
if(drop.style.display!=='none'){drop.style.display='none';return;}
drop.style.display='block';
const data=await loadModels();
const list=document.getElementById('modelPickerList');
const providers=(data.providers||[]).filter(p=>p.models&&p.models.length);
if(!providers.length){list.innerHTML='<div style="padding:12px 14px;color:var(--dimmer);font-size:11px">no providers configured</div>';return;}
const configured=window._configuredProviders||new Set();
list.innerHTML=providers.map(p=>{
const hasKey=configured.has(p.id);
const dot=`<span style="width:6px;height:6px;border-radius:50%;flex:0 0 auto;background:${hasKey?'var(--add)':'var(--dimmer)'};box-shadow:${hasKey?'0 0 5px var(--add)':''}" title="${hasKey?'configured':'no API key'}"></span>`;
return `<div class="mp-section">
<div class="mp-provider" style="display:flex;align-items:center;gap:6px">${dot}${escHtml(p.label)}</div>
${p.models.map(m=>{
const id=p.id+':'+m.name;const sel=selectedModel===id;
return `<div class="mp-item${sel?' selected':''}" data-ctx-tokens="${m.context_window||0}" onclick="setModelOverride('${escHtml(id)}','${escHtml(fmtCtxShort(m.context_window))}',${m.context_window||0})">
<span class="mp-name">${escHtml(m.label||m.name)}</span>
<span class="mp-ctx">${escHtml(fmtCtxShort(m.context_window))}</span>
<span class="mp-cost">${escHtml(m.cost_in===0?'free':'$'+m.cost_in+'/M')}</span>
${m.recommended?'<span class="mp-rec">★</span>':''}
</div>`;
}).join('')}
</div>`;
}).join('');
}
function setModelOverride(id,ctxLabel,ctxTokens){
selectedModel=id;
if(id)localStorage.setItem('sparrow-model',id);
else localStorage.removeItem('sparrow-model');
document.getElementById('modelPickerDrop').style.display='none';
const lbl=document.getElementById('modelPickerLabel');
if(lbl)lbl.textContent=id?id.replace(/^[^:]+:/,'').slice(0,22):'auto';
if(id){
let limit=0;
if(typeof ctxTokens==='number' && ctxTokens>0)limit=ctxTokens;
else if(ctxLabel){
const m=String(ctxLabel).match(/([\d.]+)\s*([kKmM])?/);
if(m){
const n=parseFloat(m[1]);
const u=(m[2]||'').toLowerCase();
limit=u==='m'?n*1_000_000:u==='k'?n*1_000:n;
}
}
if(limit>0){contextLimit=Math.round(limit);updateContextMeter();}
}
}
document.addEventListener('click',e=>{
if(!e.target.closest('#modelPickerBtn')&&!e.target.closest('#modelPickerDrop')){
const d=document.getElementById('modelPickerDrop');if(d)d.style.display='none';
}
});
async function paletteOpen(){
document.getElementById('palette').classList.add('open');
const inp=document.getElementById('paletteInput');
const source=document.getElementById('taskInput')?.value||'';
inp.value=source.startsWith('/')?source:'';
PALETTE_SEL=0;
const host=document.getElementById('paletteResults');
if(host)host.innerHTML='<div class="pempty">Loading commands…</div>';
await ensurePaletteData();
paletteFilter(inp.value);
setTimeout(()=>inp.focus(),20);
}
function paletteClose(){document.getElementById('palette').classList.remove('open')}
function paletteFilter(q){
const norm=q.toLowerCase().replace(/^\//,'');
const items=[];
PALETTE_SEL=0;
PALETTE_CMDS.forEach(c=>{
const display=String(c.name||'').startsWith('/')?String(c.name||''):'/'+String(c.name||'');
const raw=display.replace(/^\//,'');
const name=raw.toLowerCase();
const desc=(c.description||'').toLowerCase();
const usage=(c.usage||'').toLowerCase();
const source=(c.source||'').toLowerCase();
if(!norm||name.includes(norm)||desc.includes(norm)||usage.includes(norm)||source.includes(norm)){
const score=!norm?0:(name.startsWith(norm)?0:name.includes(norm)?1:desc.includes(norm)?2:usage.includes(norm)?3:4);
items.push({kind:c.source||'builtin',label:display,desc:c.description||'',usage:c.usage||'',insert:display+' ',score});
}
});
PALETTE_AGENTS.forEach(a=>{
const name=(a.name||'').toLowerCase();
if(!norm||name.includes(norm)||(a.role||'').toLowerCase().includes(norm)){
const score=!norm?4:(name.startsWith(norm)?4:name.includes(norm)?5:6);
items.push({kind:'agent',label:'@'+(a.name||''),desc:a.description||a.role||'',usage:'Mention this agent in the next task.',insert:'@'+(a.name||'')+' ',score});
}
});
PALETTE_FILTERED=items.sort((a,b)=>(a.score??9)-(b.score??9)||a.label.localeCompare(b.label)).slice(0,40);
PALETTE_SEL=Math.min(PALETTE_SEL,Math.max(0,PALETTE_FILTERED.length-1));
paletteRender();
}
function paletteRender(){
const host=document.getElementById('paletteResults');
if(PALETTE_LOADING){host.innerHTML='<div class="pempty">Loading commands…</div>';return;}
if(PALETTE_ERROR&&!PALETTE_CMDS.length&&!PALETTE_AGENTS.length){host.innerHTML=`<div class="pempty">${escHtml(PALETTE_ERROR)}</div>`;return;}
if(!PALETTE_FILTERED.length){host.innerHTML='<div class="pempty">No matches.</div>';return;}
host.innerHTML=PALETTE_FILTERED.map((it,i)=>{
const bucket=paletteKindBucket(it.kind);
const usage=it.usage?`<span class="usage">${escHtml(it.usage)}</span>`:'';
return `<div class="res ${bucket} ${i===PALETTE_SEL?'sel':''}" data-i="${i}"><div class="nm">${escHtml(it.label)}</div><div class="src">${escHtml(paletteSourceLabel(it.kind))}</div><div class="desc"><b>${escHtml(it.desc||'Run Sparrow command')}</b>${usage}</div></div>`;
}).join('');
const sel=host.querySelector('.res.sel');
if(sel)sel.scrollIntoView({block:'nearest'});
}
function paletteKindBucket(kind){
const k=String(kind||'builtin').split(':')[0];
return ({builtin:'builtin',user:'builtin',project:'builtin',skill:'skill',plugin:'plugin',agent:'plugin'})[k]||'';
}
function paletteSourceLabel(kind){
const text=String(kind||'builtin');
if(text.startsWith('project:'))return 'project';
if(text.startsWith('user:'))return 'user';
if(text.startsWith('skill:'))return 'skill';
if(text.startsWith('plugin:'))return 'plugin';
return text;
}
function paletteAccept(submit){
const it=PALETTE_FILTERED[PALETTE_SEL];if(!it)return;
const ti=document.getElementById('taskInput');
if(!ti.value||/^[/@]\S*$/.test(ti.value.trim()))ti.value=it.insert;
else ti.value=(ti.value.trimEnd()+' '+it.insert).trimStart();
paletteClose();
ti.focus();
composerInput();
if(submit){const btn=document.getElementById('runBtn');if(btn)btn.click();}
}
document.getElementById('paletteInput').addEventListener('input',e=>{paletteFilter(e.target.value)});
document.getElementById('paletteInput').addEventListener('keydown',e=>{
if(e.key==='ArrowDown'){e.preventDefault();PALETTE_SEL=Math.min(PALETTE_FILTERED.length-1,PALETTE_SEL+1);paletteRender();}
else if(e.key==='ArrowUp'){e.preventDefault();PALETTE_SEL=Math.max(0,PALETTE_SEL-1);paletteRender();}
else if(e.key==='Enter'){e.preventDefault();paletteAccept(e.shiftKey);}
else if(e.key==='Escape'){e.preventDefault();paletteClose();}
});
document.getElementById('paletteResults').addEventListener('click',e=>{
const row=e.target.closest('.res');if(!row)return;
PALETTE_SEL=parseInt(row.dataset.i,10);paletteAccept(e.shiftKey);
});
document.getElementById('palette').addEventListener('click',e=>{if(e.target.id==='palette')paletteClose()});
document.addEventListener('keydown',e=>{
if(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA')return;
if((e.metaKey||e.ctrlKey)&&e.key.toLowerCase()==='k'){e.preventDefault();paletteOpen();return;}
if(e.key.toLowerCase()==='d'&&!e.metaKey&&!e.ctrlKey&&!e.altKey){
const dp=document.getElementById('diffPanel');
if(dp){dp.classList.toggle('open');return;}
}
if(e.key.toLowerCase()==='p'&&!e.metaKey&&!e.ctrlKey&&!e.altKey){
const btn=document.getElementById('modelPickerBtn');
if(btn)btn.click();
}
});
function agentPickerState(input){
const value=input.value;const caret=input.selectionStart??value.length;
const upto=value.slice(0,caret);
const at=upto.lastIndexOf('@');
if(at<0)return null;
if(at>0){
const prev=upto[at-1];if(prev&&!/\s/.test(prev))return null;
}
const frag=upto.slice(at+1);
if(/\s/.test(frag))return null;
return {at,frag};
}
function agentPickerMatches(frag){
const f=frag.toLowerCase();
return PALETTE_AGENTS.filter(a=>(a.name||'').toLowerCase().startsWith(f)).slice(0,6);
}
let PICKER_SEL=0;
function agentPickerShow(){
const inp=document.getElementById('taskInput');
const state=agentPickerState(inp);
const picker=document.getElementById('agentPicker');
if(!state){picker.classList.add('hidden');return;}
const matches=agentPickerMatches(state.frag);
if(!matches.length){picker.classList.add('hidden');return;}
PICKER_SEL=Math.min(PICKER_SEL,matches.length-1);
picker.dataset.matches=JSON.stringify(matches);
picker.dataset.at=String(state.at);
picker.innerHTML='<div class="head">agent picker · '+matches.length+' match'+(matches.length>1?'es':'')+'</div>'+
matches.map((a,i)=>`<div class="opt ${i===PICKER_SEL?'sel':''}" data-i="${i}"><span class="at">@</span><span class="nm">${escHtml(a.name)}</span><span class="role">${escHtml(a.role||'agent')}</span></div>`).join('');
const r=inp.getBoundingClientRect();
picker.style.left=(r.left)+'px';
picker.style.bottom=(window.innerHeight-r.top+6)+'px';
picker.classList.remove('hidden');
}
function agentPickerAccept(){
const picker=document.getElementById('agentPicker');
if(picker.classList.contains('hidden'))return false;
const matches=JSON.parse(picker.dataset.matches||'[]');
const at=parseInt(picker.dataset.at||'-1',10);
const m=matches[PICKER_SEL];if(!m||at<0)return false;
const inp=document.getElementById('taskInput');
const before=inp.value.slice(0,at);
const after=inp.value.slice(inp.selectionStart??inp.value.length);
const tail=after.startsWith(' ')?after:' '+after;
inp.value=before+'@'+m.name+tail;
const newCaret=(before+'@'+m.name+' ').length;
inp.selectionStart=inp.selectionEnd=newCaret;
picker.classList.add('hidden');
inp.focus();
return true;
}
document.getElementById('taskInput').addEventListener('input',()=>{PICKER_SEL=0;agentPickerShow()});
document.getElementById('taskInput').addEventListener('click',()=>agentPickerShow());
document.getElementById('taskInput').addEventListener('keydown',e=>{
const picker=document.getElementById('agentPicker');
const open=!picker.classList.contains('hidden');
if(!open)return;
if(e.key==='ArrowDown'){const m=JSON.parse(picker.dataset.matches||'[]');if(m.length){e.preventDefault();PICKER_SEL=Math.min(m.length-1,PICKER_SEL+1);agentPickerShow();}}
else if(e.key==='ArrowUp'){const m=JSON.parse(picker.dataset.matches||'[]');if(m.length){e.preventDefault();PICKER_SEL=Math.max(0,PICKER_SEL-1);agentPickerShow();}}
else if(e.key==='Enter'||e.key==='Tab'){if(agentPickerAccept())e.preventDefault();}
else if(e.key==='Escape'){picker.classList.add('hidden');}
});
document.getElementById('agentPicker').addEventListener('click',e=>{
const opt=e.target.closest('.opt');if(!opt)return;
PICKER_SEL=parseInt(opt.dataset.i,10);agentPickerAccept();
});
document.addEventListener('click',e=>{
if(!e.target.closest('#taskInput')&&!e.target.closest('#agentPicker'))document.getElementById('agentPicker').classList.add('hidden');
});
let GHOST_HISTORY=[];
let GHOST_REST='';
async function ghostLoadHistory(){
try{
const j=await fetch('/history?limit=200').then(r=>r.json());
if(j.ok&&Array.isArray(j.inputs))GHOST_HISTORY=j.inputs.filter(s=>s&&s.length>3);
}catch(_){}
}
function ghostUpdate(){
const inp=document.getElementById('taskInput');
const ghost=document.getElementById('composerGhost');
if(!inp||!ghost)return;
const v=inp.value;
GHOST_REST='';
if(v.length>=3&&!v.includes('\n')&&inp.selectionStart===v.length){
const low=v.toLowerCase();
const hit=GHOST_HISTORY.find(h=>h.toLowerCase().startsWith(low)&&h.length>v.length);
if(hit)GHOST_REST=hit.slice(v.length);
}
ghost.innerHTML=GHOST_REST?`<span class="g-typed">${esc(v)}</span><span class="g-rest">${esc(GHOST_REST)}</span>`:'';
ghost.scrollTop=inp.scrollTop;
}
{
const inp=document.getElementById('taskInput');
if(inp){
inp.addEventListener('input',ghostUpdate);
inp.addEventListener('scroll',()=>{const g=document.getElementById('composerGhost');if(g)g.scrollTop=inp.scrollTop;});
inp.addEventListener('keydown',e=>{
if(e.defaultPrevented)return; const pickerOpen=!document.getElementById('agentPicker').classList.contains('hidden');
if(e.key==='Tab'&&GHOST_REST&&!pickerOpen&&!e.shiftKey){
e.preventDefault();
inp.value+=GHOST_REST;
GHOST_REST='';
document.getElementById('composerGhost').innerHTML='';
if(typeof autoResizeComposer==='function')autoResizeComposer();
}else if(e.key==='Escape'&&GHOST_REST){
GHOST_REST='';document.getElementById('composerGhost').innerHTML='';
}
});
inp.addEventListener('blur',()=>{GHOST_REST='';const g=document.getElementById('composerGhost');if(g)g.innerHTML='';});
}
ghostLoadHistory();
}
const KBD_HELP=[
['Tools panel',[
['Toggle tools panel',['Mod','Shift','S']],
['Preview',['Mod','P']],
['Diff',['Mod','Shift','D']],
['Terminal',['Ctrl','`']],
['Files',['Mod','Shift','F']],
['Plan',['Mod','Shift','P']],
]],
['Composer',[
['Accept ghost suggestion',['Tab']],
['New line',['Shift','↵']],
['Command palette',['Mod','K']],
['Mention an agent',['@']],
['Run / send',['↵']],
]],
['View & display',[
['Focus / Cockpit',['Alt','F']],
['Cycle theme',['Mod','Shift','L']],
['Mute sounds',['Mod','M']],
['Bigger / smaller text',['Mod','+ / −']],
['This cheat-sheet',['Mod','/']],
]],
['Focus actions',[
["D'accord (approve)",['Alt','A']],
['Annule (undo)',['Alt','U']],
['Explique',['Alt','E']],
]],
];
function _kbdKeys(keys){return keys.map(k=>`<kbd>${esc(k==='Mod'?(RB_IS_MAC?'⌘':'Ctrl'):k)}</kbd>`).join(' ');}
function renderKbdHelp(){
const grid=document.getElementById('kbdHelpGrid');if(!grid)return;
grid.innerHTML=KBD_HELP.map(([sec,rows])=>
`<div class="kh-sec">${esc(sec)}</div>`+rows.map(([desc,keys])=>
`<div class="kh-row"><span class="desc">${esc(desc)}</span>${_kbdKeys(keys)}</div>`).join('')
).join('');
}
function toggleKbdHelp(force){
const el=document.getElementById('kbdHelp');if(!el)return;
const show=force===undefined?!el.classList.contains('show'):force;
if(show){renderKbdHelp();el.classList.add('show');}else el.classList.remove('show');
}
let ws;
function connect(){
ws=new WebSocket(`ws://${location.host}/ws`);
ws.onopen=()=>{
const conn=$('conn');if(conn)conn.textContent='live';
const tag=document.getElementById('conn-tag');if(tag){tag.textContent='live';tag.classList.remove('offline');}
$('status').className='status on';$('status').textContent='● live';
loadConfig();
if(!isFastStart()){
scheduleIdle(()=>loadSwarmAgents().catch(()=>{}));
scheduleIdle(()=>loadAgentsCache().catch(()=>{}));
scheduleIdle(()=>loadCommandsCache().catch(()=>{}));
scheduleIdle(()=>loadHistoryCache().catch(()=>{}));
}
injectHero();hydrateHero();
restoreActivePanel();
runBootAnimation();
};
ws.onclose=()=>{const conn=$('conn');if(conn)conn.textContent='reconnecting…';const tag=document.getElementById('conn-tag');if(tag){tag.textContent='offline';tag.classList.add('offline');}$('status').className='status off';$('status').textContent='● disconnected';setTimeout(connect,2000)};
ws.onmessage=(e)=>{try{handleEvent(JSON.parse(e.data))}catch(err){}};
}
injectHero();
restoreActivePanel();
connect();
const TOOL_CARDS=new Map(); function openToolCard(ev){
const id=ev.id||ev.tool_use_id||ev.run||cryptoUid();
const argsStr=ev.args?(typeof ev.args==='string'?ev.args:JSON.stringify(ev.args,null,2)):'';
const meaningfulArgs=argsStr && argsStr!=='{}' && argsStr.trim().length>2;
let d=TOOL_CARDS.get(id);
if(d){
if(meaningfulArgs){
let det=d.querySelector('.det.tc-args');
if(!det){
det=document.createElement('div');det.className='det tc-args';
det.innerHTML='<div class="lbl">arguments</div><code></code>';
d.appendChild(det);
}
det.querySelector('code').textContent=argsStr;
if(ev.name && d.querySelector('.nm'))d.querySelector('.nm').textContent=ev.name;
if(typeof VERBOSE!=='undefined' && VERBOSE)d.open=true;
}
return;
}
d=document.createElement('details');
d.className='tool-card running';
d.open=!!(typeof VERBOSE!=='undefined' && VERBOSE);
d.dataset.tokBaseIn=actualInV+estInV;
d.dataset.tokBaseOut=actualOutV+estOutV;
const ico=(typeof toolIcon==='function')?toolIcon(ev.name||''):'⚒';
d.innerHTML='<summary><span class="chev">▸</span><span class="tool-ico">'+ico+'</span><span class="nm">'+esc(ev.name||'tool')+'</span><span class="ok" data-status>running…</span></summary>'+
(meaningfulArgs?'<div class="det tc-args"><div class="lbl">arguments</div><code>'+esc(argsStr)+'</code></div>':'');
term.appendChild(d);TOOL_CARDS.set(id,d);
term.scrollTop=term.scrollHeight;
}
function markToolAwaitingApproval(id){
if(!id)return;
const d=TOOL_CARDS.get(id);
if(!d)return;
d.classList.remove('running');
d.classList.add('awaiting-approval');
const st=d.querySelector('[data-status]');
if(st){
st.textContent='en attente';
st.style.color='var(--gold)';
}
}
function inferToolSummary(name,output){
if(!output)return '';
const s=typeof output==='string'?output:JSON.stringify(output);
const kb=Math.round(s.length/1024);
const sizeStr=kb>=1?`${kb} KB`:`${s.length} B`;
if(/^fs_read|^read|^fs_list|^glob/i.test(name||'')){
const fileCount=(s.match(/^[─#├└│]|^\s*[a-zA-Z0-9_.\-/\\]+\.\w+/gm)||[]).length;
return fileCount?`${fileCount} file${fileCount>1?'s':''} · ${sizeStr}`:sizeStr;
}
if(/^search|^web/i.test(name||'')){
const lines=s.split('\n').length;return `${lines} hits · ${sizeStr}`;
}
return sizeStr;
}
function closeToolCard(ev){
const id=ev.id||ev.tool_use_id||ev.run||'';
let d=TOOL_CARDS.get(id);
if(!d){const cards=term.querySelectorAll('.tool-card');d=cards[cards.length-1];}
if(!d)return;
const isErr=ev.is_error||ev.error;
d.classList.remove('running');d.classList.add(isErr?'error':'done');
const st=d.querySelector('[data-status]');
let outRaw='';
if(ev.output!==undefined)outRaw=ev.output;
else if(ev.text)outRaw=ev.text;
else if(Array.isArray(ev.blocks)){
outRaw=ev.blocks.map(b=>{
if(typeof b==='string')return b;
if(b&&typeof b==='object'){
if(typeof b.Text==='string')return b.Text;
if(typeof b.text==='string')return b.text;
if(b.Json!==undefined)return JSON.stringify(b.Json,null,2);
if(b.Diff)return `[diff ${b.Diff.file||''}]\n${b.Diff.patch||''}`;
if(b.Image)return '[image]';
return JSON.stringify(b);
}
return String(b);
}).filter(Boolean).join('\n');
}
const summary=isErr?'✗ error':(inferToolSummary(d.querySelector('.nm')?.textContent,outRaw)||'✓ done');
if(st){
const baseIn=parseInt(d.dataset.tokBaseIn||'0',10);
const baseOut=parseInt(d.dataset.tokBaseOut||'0',10);
const dIn=actualInV+estInV-baseIn;
const dOut=actualOutV+estOutV-baseOut;
const tokHtml=(dIn>0||dOut>0)
?'<span class="inline-tok">'+(dIn>0?'<span class="up-c">↑'+dIn.toLocaleString('en-US')+'</span>':'')+(dOut>0?'<span class="dn-c">↓'+dOut.toLocaleString('en-US')+'</span>':'')+'</span>'
:'';
st.innerHTML=summary+tokHtml;st.style.color=isErr?'var(--rem)':'var(--add)';
}
const out=outRaw;
if(out){
let det=d.querySelector('.det.tc-result');
if(!det){
det=document.createElement('div');det.className='det tc-result';
d.appendChild(det);
}
const outStr=typeof out==='string'?out:JSON.stringify(out,null,2);
const truncated=outStr.length>1200?outStr.slice(0,1200)+'\n… ['+ (outStr.length-1200) +' more chars]':outStr;
det.innerHTML='<div class="lbl" style="margin-top:8px">result</div><code>'+esc(truncated)+'</code>';
}
TOOL_CARDS.delete(id);
}
function parseDiffHunks(patch){
const lines=String(patch||'').split(/\r?\n/);
const hunks=[];let current=null;
lines.forEach(line=>{
if(/^@@/.test(line)){
current={header:line,lines:[]};
hunks.push(current);
return;
}
if(!current){
current={header:'@@',lines:[]};
hunks.push(current);
}
current.lines.push(line);
});
return hunks.length?hunks:[{header:'@@',lines:[]}];
}
function diffLineHtml(line,lineState){
let kls='ctx';
if(/^\+/.test(line)){kls='pls';lineState.lineNum++;}
else if(/^-/.test(line)){kls='mns';}
else if(line.length){lineState.lineNum++;}
return '<span class="dl '+kls+'"><span class="num">'+(kls==='mns'?'':lineState.lineNum)+'</span>'+esc(line)+'</span>';
}
function renderHunkedDiff(patch,opts){
const options=opts||{};
const hunkControls=options.controls!==false;
const lineState={lineNum:0};
return parseDiffHunks(patch).map((hunk,idx)=>{
const lines=hunk.lines.map(line=>diffLineHtml(line,lineState)).join('\n');
const controls=hunkControls
? `<span class="hunk-actions"><button class="accept" data-hunk-action="accept" data-hunk="${idx}">accept</button><button class="reject" data-hunk-action="reject" data-hunk="${idx}">reject</button></span>`
: '';
return `<div class="hunk" data-hunk="${idx}" data-state="pending"><div class="hunk-head"><span class="meta">${esc(hunk.header||'@@')}</span>${controls}</div>${lines}</div>`;
}).join('');
}
function bindHunkControls(root){
root.querySelectorAll('[data-hunk-action]').forEach(btn=>{
btn.addEventListener('click',()=>{
const hunk=btn.closest('.hunk');
if(!hunk)return;
const action=btn.dataset.hunkAction;
hunk.dataset.state=action==='accept'?'accepted':'rejected';
btn.closest('.hunk-actions')?.querySelectorAll('button').forEach(b=>b.disabled=true);
});
});
}
function openDiffPanel(ev){
const path=ev.file||ev.path||'<unknown>';
const patch=ev.patch||ev.diff||'';
let pl=0,mn=0;
patch.split(/\r?\n/).forEach(l=>{if(/^\+[^+]/.test(l))pl++;else if(/^-[^-]/.test(l))mn++;});
const dpPath=document.getElementById('dpPath');
dpPath.textContent=path;
dpPath.style.cursor='pointer';dpPath.title='Click to view full file';
dpPath.onclick=()=>loadFileInPanel(path);
document.getElementById('dpStats').textContent=`+${pl} −${mn}`;
const body=document.getElementById('dpBody');
body.innerHTML=`<div class="diff-card" style="border:0;border-radius:0;margin:0;background:transparent"><pre>${renderHunkedDiff(patch,{controls:true})}</pre></div>`;
bindHunkControls(body);
document.getElementById('diffPanel').classList.add('open');
}
function closeDiffPanel(){document.getElementById('diffPanel').classList.remove('open');}
async function loadFileInPanel(path){
const body=document.getElementById('dpBody');
const stats=document.getElementById('dpStats');
body.innerHTML='<div style="padding:14px;color:var(--dimmer)">loading…</div>';
stats.textContent='';
try{
const r=await fetch('/file?path='+encodeURIComponent(path));
if(!r.ok){body.innerHTML='<div style="padding:14px;color:var(--rem)">cannot read file (outside workdir or not found)</div>';return;}
const j=await r.json();
const lines=j.content.split('\n');
stats.textContent=`${j.lines} lines · ${j.lang}`;
body.innerHTML='<div style="font-family:inherit">'+
lines.map((l,i)=>`<span style="display:flex;min-height:1.6em"><span style="display:inline-block;width:38px;padding:0 8px;color:var(--dimmer);user-select:none;font-size:10px;text-align:right;border-right:1px solid var(--line);flex:0 0 auto">${i+1}</span><span style="padding:0 10px;color:var(--dim);white-space:pre;overflow-x:auto">${esc(l)}</span></span>`).join('')+
'</div>';
document.getElementById('diffPanel').classList.add('open');
}catch(e){body.innerHTML='<div style="padding:14px;color:var(--rem)">fetch failed: '+esc(String(e))+'</div>';}
}
function renderDiffCard(ev){
const path=ev.file||ev.path||'<unknown>';
const patch=ev.patch||ev.diff||'';
const applied=(ev.type==='DiffApplied');
if(applied&&!patch.trim()){
const card=document.createElement('div');card.className='diff-card';
card.innerHTML='<div class="h">◇ <span class="p">'+esc(path)+'</span><span class="m"><span style="color:var(--add)">applied</span></span></div>';
card.querySelector('.p').style.cursor='pointer';
card.querySelector('.p').title='Click to view full file';
card.querySelector('.p').onclick=()=>loadFileInPanel(path);
term.appendChild(card);
term.scrollTop=term.scrollHeight;
chirp(1500,.06);
return;
}
let pl=0,mn=0;patch.split(/\r?\n/).forEach(l=>{if(/^\+[^+]/.test(l)||l.startsWith('+ '))pl++;else if(/^-[^-]/.test(l)||l.startsWith('- '))mn++;});
const card=document.createElement('div');card.className='diff-card';
card.innerHTML='<div class="h">◇ <span class="p">'+esc(path)+'</span><span class="m"><span class="pl">+'+pl+'</span> <span class="mn">−'+mn+'</span> · '+(applied?'<span style="color:var(--add)">applied</span>':'proposed')+'</span></div><pre>'+renderHunkedDiff(patch,{controls:true})+'</pre>';
bindHunkControls(card);
term.appendChild(card);
term.scrollTop=term.scrollHeight;
if(applied)chirp(1500,.06);
}
function renderCompactBanner(ev){
const before=ev.before_chars||0,after=ev.after_chars||0;
const path=ev.handoff_path||'';
const banner=document.createElement('div');banner.className='compact-banner';
banner.innerHTML='<span class="g">⟳</span><span>context compacted · <b>'+before.toLocaleString('fr-FR').replace(/,/g,' ')+' → '+after.toLocaleString('fr-FR').replace(/,/g,' ')+' chars</b> · handoff written</span>'+
(path?'<a href="#" title="'+escAttr(path)+'">'+esc(path.split(/[/\\\\]/).pop()||'handoff')+' →</a>':'');
term.appendChild(banner);term.scrollTop=term.scrollHeight;
chirp(1200,.07);setTimeout(()=>chirp(800,.06),100);
}
let CHECKPOINT_TIMELINE=null;
let runCheckpointIds=[];
function addCheckpointNode(label,id){
if(!CHECKPOINT_TIMELINE||!CHECKPOINT_TIMELINE.isConnected){
CHECKPOINT_TIMELINE=document.createElement('div');
CHECKPOINT_TIMELINE.className='checkpoint-timeline';
const hint=document.createElement('span');hint.style.cssText='font-size:9px;color:var(--dimmer);letter-spacing:.7px;text-transform:uppercase;margin-right:4px';hint.textContent='checkpoints';
CHECKPOINT_TIMELINE.prepend(hint);
term.appendChild(CHECKPOINT_TIMELINE);
}
if(id)runCheckpointIds.push(id);
const node=document.createElement('span');
node.className='checkpoint-node';
node.title=(label||'checkpoint')+(id?` · click to rewind (${id.slice(0,8)}…)`:'');
node.style.cursor=id?'pointer':'default';
if(id){
node.addEventListener('click',()=>{
if(confirm(`Rewind to checkpoint: ${label||id}?`)){
const input=document.getElementById('taskInput');
if(input){input.value=`/rewind ${id}`;document.getElementById('runBtn')?.click();}
}
});
}
CHECKPOINT_TIMELINE.appendChild(node);
term.scrollTop=term.scrollHeight;
}
function showError(title,cause,fix,doc){
const banner=document.createElement('div');
banner.className='error-banner';
banner.innerHTML='<div class="title">'+esc(title||'Error')+'</div>'+
'<div>'+esc(cause||'Something went wrong.')+'</div>'+
(fix?'<div class="fix">'+esc(fix)+(doc?' · <a href="'+escAttr(doc)+'">docs</a>':'')+'</div>':'');
term.appendChild(banner);
term.scrollTop=term.scrollHeight;
}
let STREAM_BUF=null;
let STREAM_STATE=null;
function _cleanStreamChunk(text){
if(!text)return text;
if(text.indexOf('<tool_call')!==-1||text.indexOf('<function')!==-1
||text.indexOf('\x1b[')!==-1||text.indexOf('\\033[')!==-1){
return null; }
return text;
}
function ensureStreamRoot(){
if(STREAM_BUF&&STREAM_BUF.isConnected)return;
STREAM_BUF=document.createElement('div');
STREAM_BUF.className='ln streaming-line';
const tag=document.createElement('span');
tag.className='acc';
tag.style.cssText='font-size:10.5px;letter-spacing:1.2px;text-transform:uppercase;color:var(--brand);margin-right:8px';
tag.textContent='sparrow ›';
STREAM_BUF.appendChild(tag);
term.appendChild(STREAM_BUF);
STREAM_STATE={mode:'prose',el:null,cardEl:null,lang:'',codeStarted:false,pending:''};
openProseSpan();
}
function openProseSpan(){
const wrap=document.createElement('span');wrap.className='prose-wrap';
wrap.style.cssText='display:block;position:relative;width:100%;margin:0';
const span=document.createElement('span');
span.className='cmd streaming md-prose';
span.style.whiteSpace='pre-wrap';
wrap.appendChild(span);
const copyBtn=document.createElement('button');
copyBtn.className='prose-copy';
copyBtn.type='button';
copyBtn.title='Copy this response';
copyBtn.textContent='⧉ copy';
copyBtn.style.cssText='position:absolute;top:-2px;right:-2px;font-family:inherit;font-size:9.5px;padding:1px 7px;border-radius:5px;cursor:pointer;color:var(--dimmer);background:color-mix(in srgb,var(--panel) 88%,transparent);border:1px solid var(--line);opacity:0;transition:.15s';
copyBtn.addEventListener('click',()=>{
navigator.clipboard.writeText(STREAM_STATE?.proseRaw||span.textContent||'');
copyBtn.textContent='copied ✓';copyBtn.style.color='var(--add)';
setTimeout(()=>{copyBtn.textContent='⧉ copy';copyBtn.style.color='';},1400);
});
wrap.appendChild(copyBtn);
wrap.addEventListener('mouseenter',()=>{copyBtn.style.opacity='1';});
wrap.addEventListener('mouseleave',()=>{copyBtn.style.opacity='0';});
STREAM_BUF.appendChild(wrap);
STREAM_STATE.el=span;
STREAM_STATE.cardEl=null;
STREAM_STATE.mode='prose';
STREAM_STATE.proseRaw='';
}
const _EMOJI_MAP={
':tada:':'🎉',':bug:':'🐛',':check:':'✅',':white_check_mark:':'✅',
':x:':'❌',':warning:':'⚠️',':rocket:':'🚀',':zap:':'⚡',':fire:':'🔥',
':sparkles:':'✨',':bulb:':'💡',':wrench:':'🔧',':hammer:':'🔨',
':lock:':'🔒',':unlock:':'🔓',':key:':'🔑',':book:':'📖',':pencil:':'✏️',
':star:':'⭐',':heart:':'❤️',':100:':'💯',':eyes:':'👀',':rotating_light:':'🚨'
};
function _markdownify(src){
if(!src)return '';
let s=String(src);
s=s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
s=s.replace(/:([a-z_0-9]+):/gi,(m,k)=>_EMOJI_MAP[m.toLowerCase()]||m);
const lines=s.split('\n');
const out=[];
let i=0;
while(i<lines.length){
const ln=lines[i];
if(/^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/.test(ln)){
out.push('<hr class="md-hr">');i++;continue;
}
const h=ln.match(/^(#{1,6})\s+(.*)$/);
if(h){
const lvl=h[1].length;
out.push(`<div class="md-h md-h${lvl}">${_inlineMd(h[2])}</div>`);
i++;continue;
}
if(/^\s*>\s?/.test(ln)){
const bq=[];while(i<lines.length && /^\s*>\s?/.test(lines[i])){bq.push(lines[i].replace(/^\s*>\s?/,''));i++;}
out.push(`<blockquote class="md-bq">${_inlineMd(bq.join('\n'))}</blockquote>`);
continue;
}
if(/\|/.test(ln) && i+1<lines.length && /^\s*\|?\s*:?-{2,}/.test(lines[i+1])){
const headers=ln.replace(/^\s*\||\|\s*$/g,'').split('|').map(c=>c.trim());
i+=2;
const rows=[];
while(i<lines.length && /\|/.test(lines[i])){
rows.push(lines[i].replace(/^\s*\||\|\s*$/g,'').split('|').map(c=>c.trim()));
i++;
}
let t='<table class="md-tbl"><thead><tr>';
for(const h of headers)t+=`<th>${_inlineMd(h)}</th>`;
t+='</tr></thead><tbody>';
for(const r of rows){
t+='<tr>';
for(const c of r)t+=`<td>${_inlineMd(c)}</td>`;
t+='</tr>';
}
t+='</tbody></table>';
out.push(t);
continue;
}
if(/^\s*[-*+]\s+/.test(ln)){
const items=[];
while(i<lines.length && /^\s*[-*+]\s+/.test(lines[i])){
items.push(lines[i].replace(/^\s*[-*+]\s+/,''));i++;
}
out.push(`<ul class="md-ul">${items.map(x=>`<li>${_inlineMd(x)}</li>`).join('')}</ul>`);
continue;
}
if(/^\s*\d+\.\s+/.test(ln)){
const items=[];
while(i<lines.length && /^\s*\d+\.\s+/.test(lines[i])){
items.push(lines[i].replace(/^\s*\d+\.\s+/,''));i++;
}
out.push(`<ol class="md-ol">${items.map(x=>`<li>${_inlineMd(x)}</li>`).join('')}</ol>`);
continue;
}
out.push(_inlineMd(ln));
i++;
}
return out.join('\n');
}
const _FILE_PATH_RE=/((?:\.{1,2}[\/\\])?(?:[\w.\-]+[\/\\])+[\w.\-]+\.[A-Za-z0-9]{1,8})/g;
function _linkifyFilePaths(html){
let inCode=0,inA=0;
return html.replace(/(<[^>]+>)|([^<]+)/g,(m,tag,txt)=>{
if(tag){
if(/^<code\b/i.test(tag))inCode++;
else if(/^<\/code>/i.test(tag))inCode=Math.max(0,inCode-1);
else if(/^<a\b/i.test(tag))inA++;
else if(/^<\/a>/i.test(tag))inA=Math.max(0,inA-1);
return tag;
}
if(inCode>0||inA>0)return txt;
return txt.replace(_FILE_PATH_RE,p=>
`<a href="#" class="file-link" data-file-preview="${p}" title="Aperçu de ${p}">${p}</a>`);
});
}
function _inlineMd(s){
if(!s)return '';
s=s.replace(/`([^`\n]+)`/g,'<code class="md-ic">$1</code>');
s=s.replace(/\*\*\*([^*\n]+)\*\*\*/g,'<strong><em>$1</em></strong>');
s=s.replace(/\*\*([^*\n]+)\*\*/g,'<strong>$1</strong>');
s=s.replace(/__([^_\n]+)__/g,'<strong>$1</strong>');
s=s.replace(/(^|\W)\*([^*\n]+)\*/g,'$1<em>$2</em>');
s=s.replace(/(^|\W)_([^_\n]+)_/g,'$1<em>$2</em>');
s=s.replace(/~~([^~\n]+)~~/g,'<del>$1</del>');
s=s.replace(/\[([^\]\n]+)\]\((https?:[^)\s]+)\)/g,'<a href="$2" target="_blank" rel="noreferrer">$1</a>');
s=s.replace(/\[ \]/g,'<span class="md-cb">☐</span>');
s=s.replace(/\[x\]/gi,'<span class="md-cb done">☑</span>');
s=_linkifyFilePaths(s);
return s;
}
function _flushProseSync(){
if(STREAM_STATE && STREAM_STATE.mode==='prose' && STREAM_STATE.el){
const raw=STREAM_STATE.proseRaw||'';
if(!raw.trim()){
const wrap=STREAM_STATE.el.closest?.('.prose-wrap')||STREAM_STATE.el.parentElement;
if(wrap&&wrap.parentElement)wrap.remove();
return;
}
STREAM_STATE.el.innerHTML=_markdownify(raw);
}
}
function normalizeCodeLang(lang){
const l=String(lang||'').trim().toLowerCase();
const aliases={rs:'rust',py:'python',js:'javascript',jsx:'javascript',ts:'typescript',tsx:'typescript',sh:'bash',shell:'bash',zsh:'bash',ps1:'powershell',yml:'yaml',htm:'html'};
return aliases[l]||l||'text';
}
function spanToken(cls,text){return `<span class="${cls}">${text}</span>`;}
function highlightCode(raw,lang){
const normalized=normalizeCodeLang(lang);
const source=String(raw||'');
const placeholders=[];
const stash=(cls,value)=>{
const id=`\u0000${placeholders.length}\u0000`;
placeholders.push(spanToken(cls,esc(value)));
return id;
};
let s=source;
if(['rust','javascript','typescript','css'].includes(normalized)){
s=s.replace(/\/\*[\s\S]*?\*\//g,m=>stash('syn-com',m));
s=s.replace(/\/\/[^\n]*/g,m=>stash('syn-com',m));
}else if(['python','bash','powershell','yaml'].includes(normalized)){
s=s.replace(/(^|[\s;])#[^\n]*/g,m=>stash('syn-com',m));
}else if(normalized==='html'){
s=s.replace(/<!--[\s\S]*?-->/g,m=>stash('syn-com',m));
}
s=s.replace(/`(?:\\.|[^`\\])*`|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g,m=>stash('syn-str',m));
s=esc(s);
const mark=(cls,value)=>{
const id=`\u0000${placeholders.length}\u0000`;
placeholders.push(spanToken(cls,value));
return id;
};
s=s.replace(/\b(0x[\da-fA-F]+|\d+(?:\.\d+)?)\b/g,m=>mark('syn-num',m));
if(normalized==='rust'){
s=s.replace(/\b([a-zA-Z_]\w*)\b/g,(m,word,off,str)=>{
if(/\s*\(/.test(str.slice(off+m.length,off+m.length+3)))return mark('syn-fn',m);
if(/^(fn|let|mut|pub|crate|mod|impl|trait|struct|enum|use|where|async|await|match|if|else|for|while|loop|return|Self|self|super|const|static|move|ref|type|unsafe|dyn)$/.test(word))return mark('syn-key',m);
if(/^(String|Vec|Option|Result|HashMap|PathBuf|Arc|Box|usize|u64|i64|bool|str)$/.test(word))return mark('syn-type',m);
return m;
});
}else if(['javascript','typescript'].includes(normalized)){
s=s.replace(/\.([a-zA-Z_$][\w$]*)/g,(_,p)=>'.'+mark('syn-prop',p));
s=s.replace(/\b([a-zA-Z_$][\w$]*)\b/g,(m,word,off,str)=>{
if(/\s*\(/.test(str.slice(off+m.length,off+m.length+3)))return mark('syn-fn',m);
if(/^(const|let|var|function|return|async|await|import|export|from|class|extends|new|if|else|for|while|switch|case|break|try|catch|throw|typeof|interface|type|public|private)$/.test(word))return mark('syn-key',m);
return m;
});
}else if(normalized==='python'){
s=s.replace(/\b([a-zA-Z_]\w*)\b/g,(m,word,off,str)=>{
if(/\s*\(/.test(str.slice(off+m.length,off+m.length+3)))return mark('syn-fn',m);
if(/^(def|class|return|import|from|as|async|await|if|elif|else|for|while|try|except|finally|with|lambda|yield|True|False|None|self|pass|break|continue|raise|in|is|and|or|not)$/.test(word))return mark('syn-key',m);
return m;
});
}else if(['bash','powershell'].includes(normalized)){
s=s.replace(/(\$[A-Za-z_][\w]*)/g,m=>mark('syn-prop',m));
s=s.replace(/\b(if|then|else|elif|fi|for|while|do|done|case|esac|function|return|export|set|param|foreach|where|select)\b/g,m=>mark('syn-key',m));
}else if(normalized==='yaml'){
s=s.replace(/^(\s*[\w.-]+)(:)/gm,(_,k,op)=>mark('syn-prop',k)+mark('syn-op',op));
}else if(normalized==='json'){
s=s.replace(/\b(true|false|null)\b/g,m=>mark('syn-key',m));
}else if(normalized==='html'){
s=s.replace(/(<\/?)([a-zA-Z][\w:-]*)/g,(_,p,t)=>p+mark('syn-key',t));
s=s.replace(/\s([a-zA-Z_:][\w:.-]*)(=)/g,(_,a,op)=>' '+mark('syn-prop',a)+mark('syn-op',op));
}else if(normalized==='css'){
s=s.replace(/([.#]?[a-zA-Z_-][\w-]*)(\s*\{)/g,(_,sel,op)=>mark('syn-prop',sel)+op);
s=s.replace(/([a-zA-Z-]+)(\s*:)/g,(_,p,op)=>mark('syn-key',p)+mark('syn-op',op));
}
return s.replace(/\u0000(\d+)\u0000/g,(_,i)=>placeholders[Number(i)]||'');
}
function applyCodeHighlight(card){
const codeEl=card?.querySelector('code');
if(!codeEl)return;
const raw=codeEl.textContent||'';
const lang=card.dataset.lang||card.querySelector('.cc-lang')?.textContent||'text';
codeEl.innerHTML=highlightCode(raw,lang);
codeEl.className=`language-${normalizeCodeLang(lang)}`;
}
function openCodeCard(){
_flushProseSync();
const d=document.createElement('details');
d.className='code-card streaming-code';
d.open=true; d.innerHTML='<summary><span class="cc-icon">▸</span>'+
'<span class="cc-lang">code</span>'+
'<span class="cc-meta live" style="color:var(--brand)">streaming…</span>'+
'<button class="cc-copy" onclick="navigator.clipboard.writeText(this.closest(\'.code-card\').querySelector(\'code\').textContent);this.textContent=\'copied ✓\'">copy</button>'+
'</summary><pre><code></code></pre>';
STREAM_BUF.appendChild(d);
STREAM_STATE.el=d.querySelector('code');
STREAM_STATE.cardEl=d;
STREAM_STATE.lang='';
STREAM_STATE.codeStarted=false; STREAM_STATE.mode='code';
}
function closeCodeCard(){
const card=STREAM_STATE.cardEl;
if(card){
card.classList.remove('streaming-code');
applyCodeHighlight(card);
const meta=card.querySelector('.cc-meta');
if(meta){
const codeEl=card.querySelector('code');
const lc=(codeEl?codeEl.textContent:'').split('\n').filter(l=>l.length).length;
meta.textContent=`${lc} lines`;
meta.style.color='';
}
card.open=false; }
openProseSpan();
}
function appendToActive(text){
if(!text)return;
if(STREAM_STATE.mode==='code' && !STREAM_STATE.codeStarted){
const nl=text.indexOf('\n');
if(nl===-1){
STREAM_STATE.lang+=text;
return;
}
STREAM_STATE.lang+=text.slice(0,nl);
const normalizedLang=normalizeCodeLang(STREAM_STATE.lang);
const langEl=STREAM_STATE.cardEl?.querySelector('.cc-lang');
if(langEl && STREAM_STATE.lang.trim())langEl.textContent=normalizedLang;
if(STREAM_STATE.cardEl)STREAM_STATE.cardEl.dataset.lang=normalizedLang;
STREAM_STATE.codeStarted=true;
text=text.slice(nl+1);
if(!text)return;
}
if(STREAM_STATE.mode==='code'){
STREAM_STATE.el.textContent+=text;
return;
}
STREAM_STATE.proseRaw=(STREAM_STATE.proseRaw||'')+text;
if(!STREAM_STATE._scheduled){
STREAM_STATE._scheduled=true;
requestAnimationFrame(()=>{
if(!STREAM_STATE)return;
STREAM_STATE._scheduled=false;
if(STREAM_STATE.mode!=='prose'||!STREAM_STATE.el)return;
STREAM_STATE.el.innerHTML=_markdownify(STREAM_STATE.proseRaw||'');
});
}
}
function streamDelta(text){
const cleaned=_cleanStreamChunk(String(text||''));
if(cleaned===null){
ensureStreamRoot();
if(STREAM_STATE && STREAM_STATE.el && !STREAM_STATE.el.querySelector('.tk-stall')){
const s=document.createElement('span');s.className='tk-stall';s.textContent='⏳';s.style.opacity='.55';
STREAM_STATE.el.appendChild(s);
}
term.scrollTop=term.scrollHeight;
return;
}
text=cleaned;
ensureStreamRoot();
let buf=STREAM_STATE.pending+text;
STREAM_STATE.pending='';
while(buf.length>0){
const idx=buf.indexOf('```');
if(idx===-1){
const tail=buf.match(/(`{1,2})$/);
if(tail){
STREAM_STATE.pending=tail[1];
buf=buf.slice(0,buf.length-tail[1].length);
}
appendToActive(buf);
break;
}
if(idx>0)appendToActive(buf.slice(0,idx));
buf=buf.slice(idx+3); if(STREAM_STATE.mode==='prose')openCodeCard();
else closeCodeCard();
}
term.scrollTop=term.scrollHeight;
}
function cryptoUid(){return Math.random().toString(36).slice(2,10)}
function finalizeStreamBlock(buf){
if(!buf)return;
_flushProseSync();
buf.querySelectorAll('.streaming').forEach(s=>s.classList.remove('streaming'));
const card=buf.querySelector('.streaming-code');
if(card){
card.classList.remove('streaming-code');
const meta=card.querySelector('.cc-meta');
if(meta){
const codeEl=card.querySelector('code');
const lc=(codeEl?codeEl.textContent:'').split('\n').filter(l=>l.length).length;
meta.textContent=`${lc} lines · partial`;
meta.style.color='';
}
}
STREAM_STATE=null;
}
const STREAM_TRANSPARENT = new Set([
'ThinkingDelta',
'CostUpdate',
'TokenUsage',
'TokenUsageEstimated',
'AgentStatus',
'RouteSelected',
'Message',
]);
let VERBOSE=localStorage.getItem('sparrow-verbose')==='1';
function setVerbose(on){
VERBOSE=!!on;
localStorage.setItem('sparrow-verbose',VERBOSE?'1':'0');
const btn=document.getElementById('verboseBtn');
if(btn){
btn.classList.toggle('solid',VERBOSE);
btn.setAttribute('aria-pressed',VERBOSE?'true':'false');
btn.style.color=VERBOSE?'#fff':'';
btn.style.background=VERBOSE?'linear-gradient(180deg,var(--brand),var(--coral))':'';
btn.style.borderColor=VERBOSE?'var(--brand)':'';
btn.style.boxShadow=VERBOSE?'0 0 12px color-mix(in srgb,var(--brand) 45%,transparent)':'none';
btn.textContent=VERBOSE?'⊕ verbose ON':'⊕ verbose';
}
document.body.classList.toggle('verbose-on',VERBOSE);
if(VERBOSE && typeof toast==='function')toast('verbose mode ON · tool args + token ticks visible','ok');
else if(on===false && document.getElementById('verboseBtn') && typeof toast==='function')toast('verbose mode OFF','info');
}
{
const btn=document.getElementById('verboseBtn');
if(btn){
btn.addEventListener('click',()=>setVerbose(!VERBOSE));
if(VERBOSE){
btn.classList.add('solid');
btn.setAttribute('aria-pressed','true');
btn.style.color='#fff';
btn.style.background='linear-gradient(180deg,var(--brand),var(--coral))';
btn.style.borderColor='var(--brand)';
btn.style.boxShadow='0 0 12px color-mix(in srgb,var(--brand) 45%,transparent)';
btn.textContent='⊕ verbose ON';
document.body.classList.add('verbose-on');
}
}
}
function verboseLine(text,cls){
if(!VERBOSE)return;
line(`<span class="muted verbose" style="font-size:11px;line-height:1.35;opacity:.78">${cls?`<span class="${cls}">`:''}∙ ${esc(text)}${cls?'</span>':''}</span>`);
}
let VERBOSE_TICK_ROW=null;
let VERBOSE_TICK_TOTAL_IN=0;
let VERBOSE_TICK_TOTAL_OUT=0;
let VERBOSE_TICK_RAF=null;
function pushVerboseTick(input,output){
if(!VERBOSE)return;
VERBOSE_TICK_TOTAL_IN+=input||0;
VERBOSE_TICK_TOTAL_OUT+=output||0;
if(VERBOSE_TICK_RAF)return;
VERBOSE_TICK_RAF=requestAnimationFrame(()=>{
VERBOSE_TICK_RAF=null;
if(!VERBOSE_TICK_ROW||!VERBOSE_TICK_ROW.isConnected){
VERBOSE_TICK_ROW=document.createElement('div');
VERBOSE_TICK_ROW.className='ln verbose-tick';
term.appendChild(VERBOSE_TICK_ROW);
}
VERBOSE_TICK_ROW.innerHTML=`∙ streamed estimate · <b>${VERBOSE_TICK_TOTAL_IN}</b> in · <b>${VERBOSE_TICK_TOTAL_OUT}</b> out`;
term.scrollTop=term.scrollHeight;
});
}
let LAST_AGENT_LINE=null;
let LAST_ACTIVITY=null;
function stripActiveDecorations(){
if(LAST_AGENT_LINE){
LAST_AGENT_LINE.querySelectorAll('.caret,.agent-activity,.sp').forEach(e=>e.remove());
LAST_AGENT_LINE=null;
}
if(LAST_ACTIVITY){LAST_ACTIVITY.remove();LAST_ACTIVITY=null;}
}
const RB_IS_MAC=/mac/i.test(navigator.platform||'');
function rbKey(sc){return sc.replace('Mod',RB_IS_MAC?'⌘':'Ctrl')}
const RB_TABS={
preview:{label:'Preview',ico:'◫',sc:'Mod+P'},
timeline:{label:'Timeline',ico:'↧',sc:''},
costs:{label:'Costs',ico:'¢',sc:''},
diff:{label:'Diff',ico:'◇',sc:'Mod+Shift+D'},
terminal:{label:'Terminal',ico:'❯',sc:'Ctrl+`'},
files:{label:'Files',ico:'▤',sc:'Mod+Shift+F'},
tasks:{label:'Background tasks',ico:'◷',sc:''},
plan:{label:'Plan',ico:'☰',sc:'Mod+Shift+P'},
roadmap:{label:'Roadmap',ico:'◎',sc:''},
releases:{label:'Watched releases',ico:'⬡',sc:''},
runs:{label:'Autonomous tasks',ico:'⟳',sc:''},
};
const RB={
isOpen:false,
activeTab:null, autoOpenEnabled:localStorage.getItem('sparrow-rightbar-autoopen')!=='0',
wasManuallyClosed:false,
lastAutoOpenReason:null,
};
const RB_DIFFS=new Map(); const RB_TASKS=new Map(); const RB_TERM=[]; const RB_PREVIEW=new Map(); const RB_TOOL_BY_ID=new Map(); const RB_EXEC_NAMES=new Set(['exec','bash','shell','code_exec','run_command','terminal']);
const RB_TIMELINE=[]; const RB_COST_HISTORY=[]; let RB_INTEL_BACKLOG=null; let RB_INTEL_DIGESTS=null; let RB_RUNS=null; let RB_OBJECTIVE='';
let RB_PLAN_TODOS=null;
function rbIsExec(name,args){
return RB_EXEC_NAMES.has(String(name||'').toLowerCase())||!!(args&&typeof args==='object'&&args.command);
}
function rbNow(){return new Date().toLocaleTimeString('en-US',{hour12:false})}
function rbShortText(value,max=120){
let s=String(value||'').replace(/\s+/g,' ').trim();
if(!s)return '';
s=s.replace(/([A-Z0-9_]*(?:API|TOKEN|SECRET|KEY|PASSWORD|PASS|AUTH|BEARER)[A-Z0-9_]*\s*[:=]\s*)[^\s,;]+/gi,'$1[redacted]');
s=s.replace(/\b(?:sk|pk|ghp|gho|github_pat|xox[baprs])-[-_A-Za-z0-9]{12,}\b/g,'[redacted]');
s=s.replace(/\bBearer\s+[-._~+/A-Za-z0-9]+=*\b/gi,'Bearer [redacted]');
return s.length>max?s.slice(0,max-1)+'…':s;
}
function openRightSidebar(tab,reason){
if(typeof window.__sparrowDismissFocusTour==='function')window.__sparrowDismissFocusTour();
if(tab&&!RB_TABS[tab])tab=null;
RB.isOpen=true;
if(tab!==undefined)RB.activeTab=tab;
if(reason)RB.lastAutoOpenReason=reason;
if(reason==='manual'||reason==='shortcut'||reason==='chat-request')RB.wasManuallyClosed=false;
document.body.classList.add('rightbar-open');
localStorage.setItem('sparrow-rightbar-open','1');
if(RB.activeTab)localStorage.setItem('sparrow-rightbar-tab',RB.activeTab);
rbSyncChrome();rbRender();
}
function closeRightSidebar(manual){
RB.isOpen=false;
if(manual!==false)RB.wasManuallyClosed=true;
document.body.classList.remove('rightbar-open');
localStorage.setItem('sparrow-rightbar-open','0');
rbSyncChrome();
}
function toggleRightSidebar(){
if(RB.isOpen)closeRightSidebar(true);else openRightSidebar(RB.activeTab,'manual');
}
function autoOpenRightSidebar(tab,reason,priority){
priority=priority||'medium';
rbBadges();
if(priority==='low')return;
if(priority==='medium'&&(!RB.autoOpenEnabled||RB.wasManuallyClosed))return;
if(RB.isOpen){
if(priority==='high'&&RB.activeTab!==tab){RB.activeTab=tab;rbRender();}
return;
}
RB.isOpen=true;RB.activeTab=tab;RB.lastAutoOpenReason=reason;
document.body.classList.add('rightbar-open');
rbSyncChrome();rbRender();
}
function rbSetAutoOpen(on){
RB.autoOpenEnabled=!!on;
localStorage.setItem('sparrow-rightbar-autoopen',on?'1':'0');
const pin=document.getElementById('rbPin');
if(pin){pin.classList.toggle('on',RB.autoOpenEnabled);pin.textContent=RB.autoOpenEnabled?'◉':'○';
pin.title='Auto-open: '+(RB.autoOpenEnabled?'on':'off');pin.setAttribute('aria-pressed',String(RB.autoOpenEnabled));}
const cb=document.getElementById('cfgRightbarAuto');
if(cb&&cb.checked!==RB.autoOpenEnabled)cb.checked=RB.autoOpenEnabled;
}
function rbSyncChrome(){
const btn=document.getElementById('rightbarBtn');
if(btn){
btn.title=RB.isOpen?'Close tools panel':'Open tools panel';
btn.setAttribute('aria-label',btn.title);
btn.setAttribute('aria-pressed',String(RB.isOpen));
btn.classList.toggle('solid',RB.isOpen);
}
const bar=document.getElementById('rightbar');
if(bar){
bar.setAttribute('aria-hidden',String(!RB.isOpen));
try{bar.inert=!RB.isOpen;}catch(_){}
}
}
function rbTouch(){const t=document.getElementById('rbFootTime');if(t)t.textContent='updated '+rbNow();}
function rbFootState(state,running){
const el=document.getElementById('rbFootState'),foot=document.getElementById('rbFoot');
if(el)el.textContent=state;
if(foot)foot.classList.toggle('running',!!running);
}
function rbBadges(){
if(RB.isOpen&&RB.activeTab===null)rbRender();
else if(RB.isOpen)rbRenderIfActive();
}
let _rbRenderPending=false;
function rbRenderIfActive(){
if(!RB.isOpen||_rbRenderPending)return;
_rbRenderPending=true;
setTimeout(()=>{_rbRenderPending=false;if(RB.isOpen)rbRender();},150);
}
function rbRender(){
const body=document.getElementById('rbBody');
const title=document.getElementById('rbTitle');
const back=document.getElementById('rbBack');
if(!body)return;
const tab=RB.activeTab;
if(back)back.style.display=tab?'':'none';
if(title)title.textContent=tab?RB_TABS[tab].label:'Tools';
if(!tab){body.innerHTML=rbMenuHtml();}
else if(tab==='preview')rbRenderPreview(body);
else if(tab==='timeline')rbRenderTimeline(body);
else if(tab==='costs')rbRenderCosts(body);
else if(tab==='diff')rbRenderDiff(body);
else if(tab==='terminal')rbRenderTerminal(body);
else if(tab==='files')rbRenderFiles(body);
else if(tab==='tasks')rbRenderTasks(body);
else if(tab==='plan')rbRenderPlan(body);
else if(tab==='roadmap')rbRenderRoadmap(body);
else if(tab==='releases')rbRenderReleases(body);
else if(tab==='runs')rbRenderRuns(body);
rbTouch();
}
function rbMenuHtml(){
const running=[...RB_TASKS.values()].filter(t=>t.status==='running'||t.status==='waiting').length;
const failed=[...RB_TASKS.values()].some(t=>t.status==='failed');
const counts={
preview:RB_PREVIEW.size,
timeline:RB_TIMELINE.length,
costs:RB_COST_HISTORY.length,
diff:RB_DIFFS.size,
terminal:RB_TERM.length,
files:(window.SPARROW_WRITTEN?window.SPARROW_WRITTEN.size:0),
tasks:running,
plan:(RB_PLAN_TODOS||[]).filter(t=>t.status!=='completed'&&t.status!=='cancelled').length,
roadmap:(RB_INTEL_BACKLOG||[]).length,
releases:(RB_INTEL_DIGESTS||[]).length,
runs:(RB_RUNS||[]).length,
};
return Object.entries(RB_TABS).map(([k,t])=>{
const n=counts[k]||0;
const badge=`<span class="mi-badge${n?' show':''}${k==='tasks'&&failed?' err':''}">${n}</span>`;
const kbd=t.sc?`<kbd>${rbKey(t.sc)}</kbd>`:'';
return `<button class="rb-menu-item" data-rb-tab="${k}" role="menuitem">
<span class="mi-ico" aria-hidden="true">${t.ico}</span>
<span class="mi-label">${t.label}</span>${badge}${kbd}
</button>`;
}).join('');
}
function rbRenderTimeline(body){
if(!RB_TIMELINE.length){
body.innerHTML='<div class="rb-empty">No run events yet.<br><span style="font-size:10.5px">RunStarted, tools, diffs, checkpoints, errors and finish events appear here live.</span></div>';
return;
}
body.innerHTML=RB_TIMELINE.slice(-80).reverse().map(e=>
`<div class="rb-row">
<div class="r-top"><span class="r-name">${esc(e.type)}</span><span class="r-meta">${esc(e.time)}</span></div>
<div class="r-meta"><span>${esc(e.summary)}</span></div>
</div>`).join('');
}
function rbRenderCosts(body){
const latest=RB_COST_HISTORY[RB_COST_HISTORY.length-1];
let html=`<div class="rb-objective"><div class="ob-label">current run cost</div>${esc((typeof costV==='number'?costV:0).toFixed(4))} USD</div>`;
html+=`<div class="rb-row"><div class="r-top"><span class="r-name">Tokens</span><span class="rb-st completed">${esc(String(typeof tokV==='number'?tokV:0))}</span></div><div class="r-meta"><span>live meter from CostUpdate/TokenUsage events</span></div></div>`;
if(!latest){
body.innerHTML=html+'<div class="rb-empty" style="padding:10px">No CostUpdate event yet.</div>';
return;
}
html+='<div class="rb-sec">recent cost updates</div>'+RB_COST_HISTORY.slice(-24).reverse().map(c=>
`<div class="rb-row"><div class="r-top"><span class="r-name">$${esc(c.usd.toFixed(4))}</span><span class="r-meta">${esc(c.time)}</span></div></div>`).join('');
body.innerHTML=html;
}
function rbRenderPreview(body){
const draw=()=>{
const items=[...RB_PREVIEW.entries()].sort((a,b)=>b[1]-a[1]);
let html=`<div class="r-meta" style="gap:6px;margin-bottom:10px;display:flex;align-items:center">
<input id="rbPreviewUrl" type="text" placeholder="http://localhost:3000" spellcheck="false"
style="flex:1;min-width:0;padding:5px 8px;border-radius:6px;border:1px solid var(--line);background:var(--panel2);color:var(--fg);font-family:inherit;font-size:11px">
<button class="rb-btn" id="rbPreviewGo">embed</button>
<button class="rb-btn" id="rbPreviewRescan" title="Probe common dev ports on localhost">⟳ scan</button>
</div>`;
if(!items.length){
html+='<div class="rb-empty">No local server detected.<br><span style="font-size:10.5px">Servers are found by probing common dev ports (3000, 5173, 8080…) and by watching command output — start one and hit <b>⟳ scan</b>.</span></div>';
}else{
html+='<div class="rb-sec">live local servers</div>'+items.map(([url])=>
`<div class="rb-row"><div class="r-top"><span class="r-name" title="${escAttr(url)}">${esc(url)}</span></div>
<div class="r-meta" style="gap:6px;margin-top:3px">
<button class="rb-btn" data-rb-embed="${escAttr(url)}">embed</button>
<button class="rb-btn" data-rb-open="${escAttr(url)}">open ↗</button>
</div></div>`).join('');
}
html+='<div id="rbPreviewFrameHost"></div>';
body.innerHTML=html;
const embed=url=>{
const host=document.getElementById('rbPreviewFrameHost');
if(host&&url)host.innerHTML=`<iframe class="rb-preview-frame" src="${escAttr(url)}" title="App preview" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>`;
};
body.querySelectorAll('[data-rb-open]').forEach(b=>b.addEventListener('click',()=>window.open(b.dataset.rbOpen,'_blank')));
body.querySelectorAll('[data-rb-embed]').forEach(b=>b.addEventListener('click',()=>embed(b.dataset.rbEmbed)));
const urlInput=document.getElementById('rbPreviewUrl');
const go=()=>{let u=(urlInput.value||'').trim();if(!u)return;if(!/^https?:\/\//.test(u))u='http://'+u;embed(u);};
document.getElementById('rbPreviewGo')?.addEventListener('click',go);
urlInput?.addEventListener('keydown',e=>{if(e.key==='Enter')go();});
document.getElementById('rbPreviewRescan')?.addEventListener('click',()=>rbScanPreviewServers(true));
};
draw();
rbScanPreviewServers(false);
}
let _rbScanInflight=false;
async function rbScanPreviewServers(force){
if(_rbScanInflight)return;_rbScanInflight=true;
try{
const j=await fetch('/preview/scan').then(r=>r.json());
let found=false;
(j.servers||[]).forEach(s=>{if(s.url&&!RB_PREVIEW.has(s.url)){RB_PREVIEW.set(s.url,Date.now());found=true;}});
if((found||force)&&RB.isOpen&&RB.activeTab==='preview')rbRender();
if(found)rbBadges();
}catch(_){}
finally{_rbScanInflight=false;}
}
async function rbFetchIntelBacklog(){
try{const j=await fetch('/intel/backlog').then(r=>r.json());RB_INTEL_BACKLOG=Array.isArray(j.tickets)?j.tickets:[];}
catch(_){RB_INTEL_BACKLOG=RB_INTEL_BACKLOG||[];}
}
async function rbFetchIntelDigests(){
try{const j=await fetch('/intel/digests').then(r=>r.json());RB_INTEL_DIGESTS=Array.isArray(j.digests)?j.digests:[];}
catch(_){RB_INTEL_DIGESTS=RB_INTEL_DIGESTS||[];}
}
async function rbFetchRuns(){
try{
const j=await fetch('/runs').then(r=>r.json());
RB_RUNS=(Array.isArray(j.runs)?j.runs:[]).map(r=>Object.assign({},r,{task:rbShortText(r.task||r.run_id||'run',90)}));
}
catch(_){RB_RUNS=RB_RUNS||[];}
}
function rbRenderRoadmap(body){
const draw=()=>{
const items=RB_INTEL_BACKLOG||[];
if(!items.length){body.innerHTML='<div class="rb-empty">No scored intel backlog yet.<br><span style="font-size:10.5px">Run `sparrow intel scan` with opt-in public sources, then reopen this panel.</span></div>';return;}
body.innerHTML=items.map(t=>
`<div class="rb-row"><div class="r-top"><span class="r-name" title="${escAttr(t.title||'ticket')}">${esc(rbShortText(t.title||'ticket',78))}</span><span class="rb-st waiting">${esc(String(t.score||0))}</span></div>
<div class="r-meta"><span>${esc(rbShortText(t.reason||'',150))}</span></div>
<div class="r-meta"><span>${esc(t.source||'')}</span><span>${esc(rbShortText(t.url||'',100))}</span></div></div>`).join('');
};
if(RB_INTEL_BACKLOG===null){body.innerHTML='<div class="rb-empty">loading roadmap…</div>';rbFetchIntelBacklog().then(draw);}else draw();
}
function rbRenderReleases(body){
const draw=()=>{
const items=RB_INTEL_DIGESTS||[];
if(!items.length){body.innerHTML='<div class="rb-empty">No watched releases cached yet.<br><span style="font-size:10.5px">The WebView reads the local intel cache only; scanning stays opt-in.</span></div>';return;}
body.innerHTML=items.map(d=>
`<div class="rb-row"><div class="r-top"><span class="r-name">${esc(d.source||'source')} ${esc(d.version||'')}</span><span class="r-meta">${esc((d.date||'').slice(0,10))}</span></div>
<div class="r-meta"><span>${esc(rbShortText(d.title||'',120))}</span></div>
<div style="font-size:10.5px;color:var(--dim);line-height:1.45;margin-top:4px">${esc(rbShortText(d.summary||'',220))}</div></div>`).join('');
};
if(RB_INTEL_DIGESTS===null){body.innerHTML='<div class="rb-empty">loading releases…</div>';rbFetchIntelDigests().then(draw);}else draw();
}
function rbRenderRuns(body){
const draw=()=>{
const persisted=RB_RUNS||[];
const active=[...RB_TASKS.values()].filter(t=>t.status==='running'||t.status==='waiting');
if(!persisted.length&&!active.length){body.innerHTML='<div class="rb-empty">No autonomous tasks recorded yet.</div>';return;}
let html='';
if(active.length){
html+='<div class="rb-sec">active</div>'+active.map(t=>`<div class="rb-row"><div class="r-top"><span class="r-name">${esc(t.label)}</span><span class="rb-st ${t.status}">${esc(t.status)}</span></div><div class="r-meta"><span>${esc(t.kind)}</span></div></div>`).join('');
}
if(persisted.length){
html+='<div class="rb-sec">recorded</div>'+persisted.slice(0,40).map(r=>
`<div class="rb-row"><div class="r-top"><span class="r-name">${esc(r.task||r.run_id||'run')}</span><span class="rb-st completed">${esc(r.status||'recorded')}</span></div>
<div class="r-meta"><span>${esc(r.run_id||'')}</span><span>${esc(r.created_at||'')}</span></div></div>`).join('');
}
body.innerHTML=html;
};
if(RB_RUNS===null){
body.innerHTML='<div class="rb-empty">loading runs…</div>';
rbFetchRuns().then(draw).catch(()=>{body.innerHTML='<div class="rb-empty">Runs unavailable.</div>';});
}else draw();
}
function rbRenderDiff(body){
if(!RB_DIFFS.size){body.innerHTML='<div class="rb-empty">No changes yet.</div>';return;}
const items=[...RB_DIFFS.entries()].sort((a,b)=>b[1].ts-a[1].ts);
body.innerHTML=items.map(([file,d])=>
`<div class="rb-row click" data-rb-diff="${escAttr(file)}" title="Open diff viewer">
<div class="r-top"><span class="r-name">${esc(file.split(/[\\/]/).pop()||file)}</span>
<span class="rb-st ${d.applied?'completed':'waiting'}">${d.applied?'applied':'proposed'}</span></div>
<div class="r-meta"><span>${esc(file)}</span><span><b style="color:var(--add)">+${d.plus||0}</b> <b style="color:var(--rem)">−${d.minus||0}</b></span></div>
</div>`).join('');
body.querySelectorAll('[data-rb-diff]').forEach(r=>r.addEventListener('click',()=>{
const d=RB_DIFFS.get(r.dataset.rbDiff);
if(d&&d.patch)openDiffPanel({file:r.dataset.rbDiff,patch:d.patch});
else loadFileInPanel(r.dataset.rbDiff);
}));
}
function rbRenderTerminal(body){
if(!RB_TERM.length){
body.innerHTML='<div class="rb-empty">No commands run yet.<br><span style="font-size:10.5px">Shell commands executed by the agent appear here with their output.</span></div>'
+'<div style="text-align:center"><button class="rb-btn" onclick="document.getElementById(\'taskInput\')?.focus()">focus composer ❯</button></div>';
return;
}
body.innerHTML=RB_TERM.slice(-30).reverse().map(t=>
`<div class="rb-term-line${t.err?' err':''}"><span class="cmdline">❯ ${esc(t.cmd)}</span>${t.err?'<span style="color:var(--rem);font-size:9px;margin-left:6px;letter-spacing:.6px">FAILED</span>':''}${t.output?`<span class="out">${esc(t.output)}</span>`:''}</div>`).join('');
}
function rbRenderFiles(body){
const written=window.SPARROW_WRITTEN?[...window.SPARROW_WRITTEN.entries()].sort((a,b)=>b[1].ts-a[1].ts):[];
let html='';
if(written.length){
html+='<div class="rb-sec">modified by sparrow</div>'+written.map(([file,meta])=>
`<div class="rb-row click" data-rb-file="${escAttr(file)}" title="View file">
<div class="r-top"><span class="r-name">${esc(file.split(/[\\/]/).pop()||file)}</span><span class="r-meta">×${meta.count}</span></div>
<div class="r-meta"><span>${esc(file)}</span></div>
</div>`).join('');
}
html+='<div class="rb-sec">attachments and artifacts</div><input id="rbFilesFilter" type="search" placeholder="filter files" spellcheck="false" style="width:100%;margin:0 0 8px;padding:6px 8px;border-radius:6px;border:1px solid var(--line);background:var(--panel2);color:var(--fg);font-family:inherit;font-size:11px"><div id="rbFilesUploads"><div class="rb-empty" style="padding:8px">loading…</div></div>';
if(!written.length&&!html)html='<div class="rb-empty">No files yet.</div>';
body.innerHTML=html||'<div class="rb-empty">No files yet.</div>';
body.querySelectorAll('[data-rb-file]').forEach(r=>r.addEventListener('click',()=>loadFileInPanel(r.dataset.rbFile)));
const renderArtifacts=(items,filter='')=>{
const host=document.getElementById('rbFilesUploads');if(!host)return;
const q=filter.trim().toLowerCase();
const filtered=items.filter(it=>!q||String(it.name||'').toLowerCase().includes(q)||String(it.kind||'').toLowerCase().includes(q));
const limited=filtered.slice(0,60);
host.innerHTML=limited.length?limited.map(it=>
`<div class="rb-row"><div class="r-top"><span class="r-name" title="${escAttr(it.name)}">${esc(rbShortText(it.name,70))}</span></div>
<div class="r-meta"><span>${esc(it.kind||'file')}</span><span>${Math.round((it.size||0)/1024)} KB</span></div></div>`).join('')+
(filtered.length>limited.length?`<div class="rb-empty" style="padding:8px">${filtered.length-limited.length} more hidden by panel limit.</div>`:'')
:'<div class="rb-empty" style="padding:8px">no matching files</div>';
};
fetch('/artifacts').then(r=>r.json()).then(j=>{
const items=Array.isArray(j.items)?j.items:[];
renderArtifacts(items);
document.getElementById('rbFilesFilter')?.addEventListener('input',e=>renderArtifacts(items,e.target.value));
}).catch(()=>{
const host=document.getElementById('rbFilesUploads');
if(host)host.innerHTML='<div class="rb-empty" style="padding:8px">attachments unavailable</div>';
});
}
function rbRenderTasks(body){
if(!RB_TASKS.size){body.innerHTML='<div class="rb-empty">No background tasks running.</div>';return;}
const items=[...RB_TASKS.values()].sort((a,b)=>b.started-a.started).slice(0,40);
body.innerHTML=items.map(t=>{
const dur=t.ended?Math.max(0,Math.round((t.ended-t.started)/1000))+'s':'';
return `<div class="rb-row">
<div class="r-top"><span class="r-name" title="${escAttr(t.label)}">${esc(t.label)}</span>
<span class="rb-st ${t.status}">${t.status}</span></div>
<div class="r-meta"><span>${esc(t.kind)}</span>${dur?`<span>${dur}</span>`:''}${t.detail?`<span>${esc(t.detail)}</span>`:''}</div>
</div>`;
}).join('');
}
async function rbFetchTodos(){
try{
const j=await fetch('/todos').then(r=>r.json());
RB_PLAN_TODOS=Array.isArray(j.todos)?j.todos:[];
}catch(_){RB_PLAN_TODOS=RB_PLAN_TODOS||[];}
}
function rbRenderPlan(body){
const draw=()=>{
const todos=RB_PLAN_TODOS||[];
const open=todos.filter(t=>t.status==='pending'||t.status==='in_progress');
const done=todos.filter(t=>t.status==='completed');
let html='';
if(RB_OBJECTIVE)html+=`<div class="rb-objective"><div class="ob-label">current objective</div>${esc(RB_OBJECTIVE)}</div>`;
if(!todos.length&&!RB_OBJECTIVE){
body.innerHTML='<div class="rb-empty">No plan yet.<br><span style="font-size:10.5px">When the agent builds a task list, its steps and status appear here.</span></div>';
return;
}
const mark=s=>s==='completed'?'✓':s==='in_progress'?'●':s==='cancelled'?'✕':'○';
if(open.length){
html+='<div class="rb-sec">steps</div>'+open.map(t=>
`<div class="rb-plan-step ${esc(t.status)}"><span class="ps-mark">${mark(t.status)}</span><span class="ps-txt">${esc(t.content)}</span></div>`).join('');
const next=open.find(t=>t.status==='in_progress')||open[0];
if(next)html+=`<div class="r-meta" style="margin:2px 4px 8px;font-size:10px;color:var(--dim)">next: <b>${esc(next.content)}</b></div>`;
}
if(done.length){
html+='<div class="rb-sec">completed</div>'+done.slice(-10).map(t=>
`<div class="rb-plan-step completed"><span class="ps-mark">✓</span><span class="ps-txt">${esc(t.content)}</span></div>`).join('');
}
if(!open.length&&!done.length&&RB_OBJECTIVE)html+='<div class="rb-empty" style="padding:10px">no detailed steps recorded for this objective</div>';
body.innerHTML=html;
};
if(RB_PLAN_TODOS===null){
body.innerHTML='<div class="rb-empty">loading plan…</div>';
rbFetchTodos().then(draw);
}else draw();
}
let _rbLongRunTimers=new Map();
function rbTask(key,patch){
const cur=RB_TASKS.get(key)||{label:'task',kind:'task',status:'waiting',started:Date.now(),ended:null,detail:''};
RB_TASKS.set(key,Object.assign(cur,patch));
}
function rbScanPreviewUrls(text){
if(!text)return;
const re=/https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(?::\d+)?(?:\/[^\s"'<>)\]]*)?/g;
let m,found=false;
while((m=re.exec(text)))if(!RB_PREVIEW.has(m[0])){RB_PREVIEW.set(m[0],Date.now());found=true;}
if(found)autoOpenRightSidebar('preview','preview-detected','low');
}
const RB_INTENTS=[
['preview',/\b(preview|aper[cç]u|pr[ée]visualis\w*)\b/i],
['diff',/\b(diff|diffs|changements|modifs?|modifications)\b/i],
['terminal',/\b(terminal|console shell|shell)\b/i],
['files',/\b(files?|fichiers?)\b/i],
['tasks',/\b(background tasks?|t[aâ]ches de fond|jobs en cours)\b/i],
['plan',/\b(plan|todo ?list|liste des [ée]tapes)\b/i],
];
function rbDetectIntent(task){
if(!task)return;
if(!/\b(montre|montrer|affiche|afficher|ouvre|ouvrir|voir|show|open|display|view)\b/i.test(task))return;
for(const[tab,re]of RB_INTENTS){
if(re.test(task)){openRightSidebar(tab,'chat-request');break;}
}
}
function rbOnEvent(ev){
rbRecordTimeline(ev);
switch(ev.type){
case 'RunStarted':
RB_OBJECTIVE=ev.task||'';
rbTask('run:'+(ev.run||''),{label:(ev.task||'task').slice(0,90),kind:'run',status:'running',started:Date.now(),ended:null});
rbFootState('run in progress',true);
break;
case 'ToolUseProposed':{
RB_TOOL_BY_ID.set(ev.id,{name:ev.name,args:ev.args});
if(rbIsExec(ev.name,ev.args)){
const cmd=(ev.args&&(ev.args.command||ev.args.cmd))||ev.name;
rbTask('tool:'+ev.id,{label:String(cmd).slice(0,90),kind:'command',status:'waiting',started:Date.now()});
}
if(String(ev.name||'').toLowerCase()==='todo'){
const action=ev.args&&ev.args.action;
if(action&&action!=='list'){
RB_PLAN_TODOS=null; autoOpenRightSidebar('plan','plan-updated','medium');
}
}
break;
}
case 'ToolUseStarted':{
const meta=RB_TOOL_BY_ID.get(ev.id);
if(meta&&rbIsExec(meta.name,meta.args)){
rbTask('tool:'+ev.id,{status:'running'});
autoOpenRightSidebar('tasks','task-started','low');
_rbLongRunTimers.set(ev.id,setTimeout(()=>{
const t=RB_TASKS.get('tool:'+ev.id);
if(t&&t.status==='running')autoOpenRightSidebar('tasks','long-running-task','medium');
},5000));
}
break;
}
case 'ToolOutput':{
const meta=RB_TOOL_BY_ID.get(ev.id);
const text=(ev.blocks||[]).map(b=>typeof b==='string'?b:(b&&b.Text)||'').filter(Boolean).join('\n');
const failed=!!ev.is_error; if(meta&&rbIsExec(meta.name,meta.args)){
if(RB_TASKS.has('tool:'+ev.id))rbTask('tool:'+ev.id,{status:failed?'failed':'completed',ended:Date.now(),
detail:failed?text.split(/\r?\n/)[0].slice(0,80):''});
const tm=_rbLongRunTimers.get(ev.id);if(tm){clearTimeout(tm);_rbLongRunTimers.delete(ev.id);}
const cmd=(meta.args&&(meta.args.command||meta.args.cmd))||meta.name;
RB_TERM.push({cmd:String(cmd),output:text.slice(0,4000),ts:Date.now(),err:failed});
if(RB_TERM.length>60)RB_TERM.shift();
if(failed)autoOpenRightSidebar('tasks','task-failed','high');
}
if(meta&&String(meta.name||'').toLowerCase()==='todo')RB_PLAN_TODOS=null;
rbScanPreviewUrls(text);
break;
}
case 'DiffProposed':
RB_DIFFS.set(ev.file,{patch:ev.patch||'',plus:ev.plus||0,minus:ev.minus||0,applied:false,ts:Date.now()});
autoOpenRightSidebar('diff','diff-available','medium');
break;
case 'DiffApplied':{
const prev=RB_DIFFS.get(ev.file);
RB_DIFFS.set(ev.file,Object.assign(prev||{patch:ev.patch||'',plus:0,minus:0},{applied:true,ts:Date.now()}));
autoOpenRightSidebar('diff','diff-applied','low');
break;
}
case 'TestResult':{
const failed=(ev.failed||0)>0;
rbTask('tests:'+Date.now(),{label:failed?`tests · ${ev.failed} failed / ${ev.passed} passed`:`tests · ${ev.passed} passed`,
kind:'tests',status:failed?'failed':'completed',started:Date.now(),ended:Date.now(),detail:(ev.detail||'').slice(0,80)});
autoOpenRightSidebar('tasks',failed?'task-failed':'tests-passed',failed?'high':'low');
break;
}
case 'Error':
RB_TASKS.forEach((t,k)=>{if(t.status==='running'||t.status==='waiting')rbTask(k,{status:'failed',ended:Date.now(),detail:(ev.message||'').slice(0,80)});});
rbFootState('error',false);
autoOpenRightSidebar('tasks','run-error','high');
break;
case 'RunFinished':
RB_TASKS.forEach((t,k)=>{if(t.status==='running'||t.status==='waiting')rbTask(k,{status:'completed',ended:Date.now()});});
RB_PLAN_TODOS=null; rbFootState('idle',false);
if(typeof ghostLoadHistory==='function')ghostLoadHistory(); rbBadges();
break;
default:return;
}
rbBadges();
}
(function initRightbar(){
const bar=document.getElementById('rightbar');if(!bar)return;
document.getElementById('rightbarBtn')?.addEventListener('click',toggleRightSidebar);
document.getElementById('rbClose')?.addEventListener('click',()=>closeRightSidebar(true));
document.getElementById('rbBack')?.addEventListener('click',()=>{RB.activeTab=null;localStorage.removeItem('sparrow-rightbar-tab');rbRender();});
document.getElementById('rbPin')?.addEventListener('click',()=>rbSetAutoOpen(!RB.autoOpenEnabled));
document.getElementById('rbBody')?.addEventListener('click',e=>{
const item=e.target.closest('[data-rb-tab]');
if(item)openRightSidebar(item.dataset.rbTab,'manual');
});
rbSetAutoOpen(RB.autoOpenEnabled);
if(localStorage.getItem('sparrow-rightbar-open')==='1'){
const tab=localStorage.getItem('sparrow-rightbar-tab');
openRightSidebar(tab&&RB_TABS[tab]?tab:null,'restore');
}else rbSyncChrome();
})();
function handleEvent(ev){
try{rbOnEvent(ev)}catch(_){}
if(STREAM_BUF&&!STREAM_TRANSPARENT.has(ev.type)){
const s=STREAM_BUF.querySelector('.streaming');if(s)s.classList.remove('streaming');
finalizeStreamBlock(STREAM_BUF);
STREAM_BUF=null;
}
switch(ev.type){
case 'RunStarted':{
const turnCount=parseInt(localStorage.getItem('sparrow-turn-count')||'0',10)+1;
localStorage.setItem('sparrow-turn-count',String(turnCount));
if(VERBOSE)verboseLine(`started${turnCount>1?` · context retained · turn #${turnCount}`:''}`,'planner');
resetRunMetrics();resetSwarm();_verifierRan=false;runStartMs=Date.now();runDiffsApplied=0;runCheckpoints=0;runCheckpointIds=[];CHECKPOINT_TIMELINE=null;
VERBOSE_TICK_ROW=null; VERBOSE_TICK_TOTAL_IN=0; VERBOSE_TICK_TOTAL_OUT=0;
LAST_AGENT_LINE=null;
setSwarm('planner','thinking','classifying request and selecting route');ensureTokenMeter();break;
}
case 'RouteSelected':{
const chain=summarizeChain(ev.chain);
const compact=compactChain(ev.chain);
setRoute(chain);
setSwarm('planner','done','route selected · '+compact);
if(VERBOSE)verboseLine(`route selected · ${chain}`,'planner');
if(ev.context_window&&ev.context_window>0){contextLimit=ev.context_window;updateContextMeter();}
const primary=Array.isArray(ev.chain)&&ev.chain[0]?ev.chain[0]:'auto';
const lbl=$('modelPickerLabel');if(lbl)lbl.textContent=primary.length>22?primary.slice(0,20)+'…':primary;
updateRoutePanel(ev.chain,ev.context_window);
break;
}
case 'ModelSwitched':
if(ev.simple&&ev.human){line(esc(ev.human),'muted');}
else{line(`<span class="warn">${routeSwitchLabel(ev.from,ev.to,ev.reason)}</span> <span style="color:var(--add);font-size:10px;margin-left:6px">● context preserved</span>`);}
break;
case 'Message':
if(ev.role==='router'){
if(VERBOSE&&ev.text&&!ev.text.startsWith('requete:')){
verboseLine(ev.text,'planner');
}
}
else if(VERBOSE){const sender=ev.role==='user'?'you':ev.role;line('<span class="muted">'+esc(sender)+':</span> <span class="cmd">'+esc(ev.text)+'</span>')}
break;
case 'ThinkingDelta':streamDelta(ev.text);break;
case 'ToolUseProposed':
setSwarm('coder','working','using tool · '+ev.name);
openToolCard(ev);
if(VERBOSE){
const card=TOOL_CARDS.get(ev.id||ev.tool_use_id);
if(card)card.open=true;
verboseLine(`tool proposed · ${ev.name||'(unknown)'} · risk ${ev.risk||'n/a'}`);
}
break;
case 'ToolUseStarted':
openToolCard(ev);
if(VERBOSE)verboseLine(`tool started · id=${ev.id||'?'}`);
break;
case 'ToolOutput':
closeToolCard(ev);
if(VERBOSE){
const card=TOOL_CARDS.get(ev.id||ev.tool_use_id);
if(card)card.open=true;
const blocks=ev.blocks||[];
verboseLine(`tool output · ${blocks.length} block${blocks.length===1?'':'s'}`);
}
break;
case 'DiffProposed':renderDiffCard(ev);break;
case 'DiffApplied':renderDiffCard(ev);recordWrittenFile(ev.file);runDiffsApplied++;break;
case 'Compacted':renderCompactBanner(ev);break;
case 'ApprovalRequested':{
markToolAwaitingApproval(ev.id);
const id=escAttr(ev.id||'');
const tool=esc(ev.tool||'tool');
const risk=esc(ev.risk||'unknown');
const summary=esc(ev.summary||'');
window.SPARROW_SESSION_ALLOW=window.SPARROW_SESSION_ALLOW||new Set();
if(window.SPARROW_SESSION_ALLOW.has(ev.tool||'')){
line(`<span class="muted" style="font-size:11px">↳ <span class="ok">${tool}</span> auto-approved (session)</span>`);
fetch('/approval',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:ev.id,decision:'allow_once'})});
break;
}
const card=line(`<details open class="approval-card-inline" data-approval-id="${id}" data-tool="${esc(ev.tool||'')}" style="margin:4px 0 6px;padding:6px 10px;border:1px solid var(--line-hot);border-radius:8px;background:color-mix(in srgb,var(--panel) 75%,transparent);font-size:11.5px">
<summary style="cursor:pointer;display:flex;align-items:center;gap:8px;list-style:none">
<span style="color:var(--brand);letter-spacing:1.4px;text-transform:uppercase;font-size:10px">◌ approve</span>
<span style="color:var(--fg);font-weight:600">${tool}</span>
<span style="color:var(--gold);font-size:10px">risk: ${risk}</span>
<span class="approval-actions" style="margin-left:auto;margin-top:0">
<button class="approve" title="Allow once" style="padding:3px 8px;font-size:11px" onclick="resolveApproval('${id}','allow_once',this)">once</button>
<button class="approve" title="Allow for the rest of this session" style="padding:3px 8px;font-size:11px;background:rgba(116,194,88,.18)" onclick="resolveApproval('${id}','allow_session',this)">session</button>
<button class="approve" title="Allow always (persists)" style="padding:3px 8px;font-size:11px;background:rgba(111,166,230,.18);border-color:rgba(111,166,230,.4);color:var(--planner)" onclick="resolveApproval('${id}','allow_always',this)">always</button>
<button class="deny" style="padding:3px 8px;font-size:11px" onclick="resolveApproval('${id}','deny',this)">deny</button>
</span>
</summary>
<div style="font-size:11px;color:var(--dim);line-height:1.45;margin-top:6px;overflow-wrap:anywhere">${summary}</div>
</details>`);
break;
}
case 'ApprovalResolved':if(VERBOSE)verboseLine(`approval · ${ev.decision}`,'planner');break;
case 'AgentSpawned':
setSwarm(ev.role,'working',`spawned · ${ev.model}`);
updateCrewLiveStatus(ev.role||ev.name,'working',`spawned · ${ev.model}`);
if(VERBOSE)verboseLine(`◆ ${ev.role} spawned (${ev.model})`);
break;
case 'AgentStatus':{
const isThinking=ev.status==='Thinking'||ev.status==='Working';
const note=ev.note||'';
if(ev.role==='verifier'&&isThinking)_verifierRan=true;
const verbedNote=isThinking&&!note.includes('·')?FLIGHT_VERBS[(_vi++)%FLIGHT_VERBS.length]+' · '+note:note;
setSwarm(ev.role,ev.status,verbedNote);
updateCrewLiveStatus(ev.role||ev.name,ev.status,verbedNote);
stripActiveDecorations();
if(VERBOSE){
const dot=icon(ev.status);
const roleCl=roleCls(ev.role);
const newLine=line('<span class="'+roleCl+'">'+dot+'</span> <span class="'+roleCl+'" style="font-weight:500">'+esc(ev.role)+'</span>'+(isThinking?' <span class="sp">◌</span>':'')+' <span class="muted">'+esc(verbedNote)+'</span>'+(isThinking?'<span class="caret"></span>':'')+(isThinking?'<span class="agent-activity">'+esc(note||'working')+'</span>':''));
if(isThinking){
LAST_AGENT_LINE=newLine;
LAST_ACTIVITY=newLine.querySelector('.agent-activity');
}
}
break;
}
case 'CheckpointCreated':if(VERBOSE)verboseLine(`checkpoint · ${ev.label||ev.id||''}`,'planner');addCheckpointNode(ev.label,ev.id);runCheckpoints++;chirp(1400,.07);break;
case 'SkillLearned':if(VERBOSE)line(`<span class="skill-pop">✦ skill learned · ${esc(ev.name)}</span>`);chirp(1100,.05);setTimeout(()=>chirp(1700,.07),80);break;
case 'CostUpdate':
RB_COST_HISTORY.push({usd:Number(ev.usd||0),time:rbNow()});
if(RB_COST_HISTORY.length>200)RB_COST_HISTORY.shift();
autoOpenRightSidebar('costs','cost-updated','low');
setCost(ev.usd);
break;
case 'TokenUsageEstimated':
addTokEstimate(ev.input,ev.output);
pushVerboseTick(ev.input,ev.output);
break;
case 'TokenUsage':
addTokActual(ev.input,ev.output);
if(VERBOSE)verboseLine(`tokens actual · ${ev.input||0} in · ${ev.output||0} out`);
break;
case 'AutonomyChanged':setAuto(ev.level.toLowerCase());break;
case 'TestResult':
setSwarm('verifier',ev.failed>0?'error':'done',ev.failed>0?`${ev.failed} failing tests`:`${ev.passed} tests passed`);
if(ev.failed>0){line(`<span class="warn">⚠ tests ${ev.passed} passed · ${ev.failed} failed</span>`)}
else{line(`<span class="ok">✓ tests ${ev.passed} passed</span>`)}break;
case 'RunFinished':{
stripActiveDecorations();
setSwarm('coder','done','response completed');
if(_verifierRan)setSwarm('verifier','done','verification complete');
const durMs=ev.outcome?.duration_ms??(runStartMs?Date.now()-runStartMs:0);
const durStr=durMs>60000?Math.floor(durMs/60000)+'m '+Math.round((durMs%60000)/1000)+'s':durMs>1000?(durMs/1000).toFixed(1)+'s':durMs+'ms';
const finalCost=ev.outcome?.cost_usd??costV;
const finalTok=ev.outcome?.tokens?.input+ev.outcome?.tokens?.output||tokV;
const status=esc(ev.outcome?.status||'done');
if(ev.simple){
if(ev.human)line('<span class="ok">✓</span> '+esc(ev.human));
let costLine;
if(finalCost<=0)costLine="C'était gratuit.";
else if(finalCost<0.01)costLine="Coût : moins d'un centime.";
else costLine='Coût : environ '+Math.round(finalCost*100)+' centimes.';
line('<span class="muted">'+esc(costLine)+'</span>');
}else{
const meta=document.createElement('div');meta.className='ln run-meta';
meta.innerHTML='<span class="ok">✓</span> <span class="muted">'+status+'</span>'+
'<span style="color:var(--brand);font-size:10px;margin-left:8px">$'+finalCost.toFixed(4)+'</span>'+
'<span style="color:var(--agent);font-size:10px;margin-left:8px">'+(finalTok||tokV).toLocaleString('en-US')+' tok</span>'+
'<span class="muted" style="font-size:10px;margin-left:8px">'+durStr+'</span>'+
(runDiffsApplied?'<span class="muted" style="font-size:10px;margin-left:8px">'+runDiffsApplied+' file'+(runDiffsApplied===1?'':'s')+'</span>':'');
term.appendChild(meta);term.scrollTop=term.scrollHeight;
}
chirp(900,.05);setTimeout(function(){chirp(1400,.07);},100);
setRunActive(false);
break;
}
case 'Error':
if(!String(ev.message||'').includes('Ollama API error 404')){
showError('Runtime error',ev.message,'Check config, provider availability, or retry after fixing the reported cause.','docs/troubleshooting.md')
}
break;
default:line(`<span class="dimd">${esc(JSON.stringify(ev))}</span>`);break;
}
}
function rbRecordTimeline(ev){
if(!ev||['ThinkingDelta','ReasoningDelta','TokenUsageEstimated'].includes(ev.type))return;
const summary=ev.task||ev.name||ev.file||ev.message||ev.role||ev.id||ev.outcome?.status||'';
RB_TIMELINE.push({idx:RB_TIMELINE.length+1,type:ev.type,time:rbNow(),summary:String(summary).slice(0,140)});
if(RB_TIMELINE.length>300)RB_TIMELINE.shift();
}
function esc(s){return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
function escAttr(s){return esc(s).replace(/"/g,'"');}
function roleCls(r){return r==='planner'?'planner':r==='coder'?'coder':r==='verifier'?'verifier':'muted';}
function icon(s){const m={Done:'✓',Working:'●',Thinking:'○',Error:'✗'};return m[s]||'◌';}
async function bootIntro(){
if(term.childElementCount>0)return;
let providerSummary='loading…';
let activeCount=0,totalProviders=0,totalModels=0;
try{
const [mr,cr]=await Promise.all([fetch('/models'),fetch('/config')]);
const md=await mr.json();const cfg=await cr.json();
const ps=md.providers||[];
totalProviders=ps.length;
totalModels=ps.reduce((s,p)=>s+p.models.length,0);
activeCount=(cfg.providers||[]).filter(p=>p.has_credential||p.configured).length;
providerSummary=`${totalProviders} providers · ${totalModels} models · <b style="color:var(--add)">${activeCount}</b> with key`;
}catch{providerSummary='provider catalog unavailable';}
const bootTime=new Date().toLocaleTimeString('en-US',{hour12:false});
const home=document.createElement('div');
home.className='home-panel';
home.innerHTML=`
<section class="home-left">
<svg class="hero-logo" viewBox="0 0 240 240" aria-label="Sparrow logo"><use href="#sparrow"/></svg>
<div>
<div class="home-title">Welcome back, <b style="color:var(--brand)">${escHtml(navigator.userAgent.includes('Windows')?'Captain':'friend')}</b></div>
<div class="home-sub"><b>SPARROW</b> · one cli · grows with you · booted at <span style="color:var(--fg)">${bootTime}</span></div>
<div class="home-path" id="homePath">—</div>
</div>
</section>
<section class="home-right">
<div class="home-block">
<h3>Route cockpit</h3>
<p>${providerSummary} · automatic fallback · cost and token tracking live.</p>
</div>
<div class="home-block">
<h3>Model picker</h3>
<p>Click <b>⬡ auto</b> in the bar above to fix a specific model. Auto-route selects the cheapest capable brain per task.</p>
</div>
<div class="home-block">
<h3>Memory & crew</h3>
<p id="memorySummary">Loading memory and agent index…</p>
</div>
<div class="home-block">
<h3>Start now</h3>
<div class="qa-chips">
<button data-qa="Répare ce projet : décris le problème ou colle l'erreur ici">🔧 Répare</button>
<button data-qa="Explique-moi simplement ce que fait ce projet et par où commencer">💡 Explique</button>
<button data-qa="Fais un plan détaillé pour : ">📋 Plan</button>
<button data-qa="__ideas__">✨ Idées</button>
<button data-qa="__help__">⌨ Raccourcis</button>
</div>
</div>
</section>`;
term.appendChild(home);
home.querySelectorAll('.qa-chips button').forEach(b=>b.addEventListener('click',()=>{
const v=b.dataset.qa;
if(v==='__help__'){toggleKbdHelp(true);return;}
const inp=document.getElementById('taskInput');if(!inp)return;
inp.value=(v==='__ideas__')?'/idees':v;
if(typeof autoResizeComposer==='function')autoResizeComposer();
inp.focus();
if(v==='__ideas__')document.getElementById('runBtn')?.click();
else{inp.selectionStart=inp.selectionEnd=inp.value.length;ghostUpdate();}
}));
const boot=line('<span class="sec">— cockpit online —</span>');
const bootParent=term;
[
'<span class="planner">○ router</span> <span class="muted">provider catalog loaded · config ready</span>',
'<span class="coder">○ tools</span> <span class="muted">fs · edit · search · exec · git · web · todo</span><span class="caret"></span>',
'<span class="verifier">○ audit</span> <span class="muted">tokens, cost, routing and checkpoints stream live</span>',
'<span id="learn-toast" class="coder">✦ skill learned · <b>write-and-fix-tests</b></span>'
].forEach((html,index)=>addBootLine(bootParent,html,index));
pulseLearnToast('learn-toast');
startTokenCounter('tok');
updateContextMeter();
loadMemory();
typeCmd('status',term);
}
let runActive=false;
function setRunActive(active){
runActive=active;
const stop=$('stopBtn'),run=$('runBtn'),ta=$('taskInput'),chromeAbort=$('abortChromeBtn');
if(stop){
stop.style.display=active?'inline-flex':'none';
stop.style.boxShadow=active?'0 0 0 0 color-mix(in srgb,var(--rem) 60%,transparent)':'none';
stop.style.animation=active?'abort-pulse 1.4s ease-in-out infinite':'none';
}
if(chromeAbort){
chromeAbort.style.display=active?'inline-flex':'none';
chromeAbort.style.animation=active?'abort-pulse 1.4s ease-in-out infinite':'none';
}
if(run){
run.textContent=active?'inject':'run';
run.style.opacity=active?'0.85':'1';
}
if(ta)ta.placeholder=active?'type to inject into the running task · Enter to send':'Type a task and press Enter…';
let badge=document.getElementById('abortBadge');
if(active){
if(!badge){
badge=document.createElement('button');
badge.id='abortBadge';
badge.type='button';
badge.title='Abort the running task';
badge.innerHTML='◼ abort run';
badge.style.cssText='position:fixed;left:64px;bottom:64px;z-index:9999;font-family:inherit;'+
'font-size:12px;font-weight:600;letter-spacing:.6px;padding:8px 16px;border-radius:999px;cursor:pointer;'+
'color:#fff;background:linear-gradient(180deg,#d44,#a22);'+
'border:1px solid #f55;'+
'box-shadow:0 0 18px rgba(212,68,68,.55), 0 2px 8px rgba(0,0,0,.35);'+
'animation:abort-pulse 1.4s ease-in-out infinite';
badge.addEventListener('click',stopRun);
document.body.appendChild(badge);
}
badge.style.display='inline-flex';
}else if(badge){
badge.style.display='none';
}
}
document.addEventListener('DOMContentLoaded',()=>{
const cab=document.getElementById('abortChromeBtn');
if(cab)cab.addEventListener('click',stopRun);
fetch('/status').then(r=>r.json()).then(s=>{
const el=document.getElementById('appVersion');
if(el && s && s.version) el.textContent='v'+s.version;
}).catch(()=>{});
});
{
const cab=document.getElementById('abortChromeBtn');
if(cab)cab.addEventListener('click',stopRun);
}
(function injectAbortPulse(){
if(document.getElementById('abort-pulse-kf'))return;
const st=document.createElement('style');st.id='abort-pulse-kf';
st.textContent=`
@keyframes abort-pulse{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--rem) 55%,transparent)}50%{box-shadow:0 0 0 6px color-mix(in srgb,var(--rem) 0%,transparent)}}
/* Inline markdown rendering — applied to streamed prose */
.md-prose{font-size:15px;line-height:1.62;letter-spacing:0}
.md-prose .md-h{font-weight:700;letter-spacing:0;margin:18px 0 10px;line-height:1.35}
.md-prose .md-h1{font-size:16px;color:var(--brand)}
.md-prose .md-h2{font-size:15px;color:var(--coral)}
.md-prose .md-h3{font-size:15px;color:var(--planner)}
.md-prose .md-h4,.md-prose .md-h5,.md-prose .md-h6{font-size:14px;color:var(--agent)}
.md-prose strong{color:var(--fg);font-weight:700}
.md-prose em{color:var(--coral);font-style:italic}
.md-prose del{color:var(--dimmer);text-decoration:line-through}
.md-prose .md-ic{display:inline;font-family:var(--mono-font);background:color-mix(in srgb,var(--fg) 6%,transparent);color:var(--fg);padding:1px 5px;border-radius:5px;font-size:13px;line-height:1.25;border:1px solid color-mix(in srgb,var(--fg) 16%,transparent);letter-spacing:0}
.md-prose a{color:var(--planner);text-decoration:underline dotted;text-underline-offset:2px}
/* Clickable file/artifact paths in chat → open the right-side viewer. */
.file-link{color:var(--add);text-decoration:underline dotted;text-underline-offset:2px;cursor:pointer;font-family:var(--mono-font);font-size:.95em;border-radius:4px;padding:0 2px}
.file-link:hover{background:color-mix(in srgb,var(--add) 14%,transparent);text-decoration-style:solid}
.file-link::before{content:"◇ ";opacity:.6;font-size:.85em}
.md-prose a:hover{color:var(--coral)}
.md-prose .md-hr{border:0;height:1px;background:linear-gradient(90deg,transparent,var(--line-hot),transparent);margin:14px 0}
.md-prose .md-bq{margin:12px 0;padding:9px 12px;border-left:3px solid var(--brand);background:color-mix(in srgb,var(--brand) 5%,transparent);color:var(--dim);font-style:italic;border-radius:0 7px 7px 0}
.md-prose .md-ul,.md-prose .md-ol{margin:8px 0 10px 20px;padding:0}
.md-prose .md-ul li,.md-prose .md-ol li{margin:4px 0;color:var(--fg);line-height:1.55}
.md-prose .md-ul li::marker{color:var(--brand)}
.md-prose .md-ol li::marker{color:var(--coral);font-weight:600}
.md-prose .md-cb{color:var(--brand);font-size:1.1em;margin-right:4px}
.md-prose .md-cb.done{color:var(--add)}
.md-prose .md-tbl{width:100%;max-width:900px;border-collapse:collapse;margin:10px 0 12px;font-size:13px;border:1px solid var(--line-hot);border-radius:9px;overflow:hidden}
.md-prose .md-tbl th{background:color-mix(in srgb,var(--brand) 10%,var(--panel));color:var(--brand);font-weight:700;text-align:left;padding:9px 11px;border-bottom:1px solid var(--line-hot);font-size:10.5px;letter-spacing:.02em;text-transform:uppercase}
.md-prose .md-tbl td{padding:9px 11px;border-top:1px solid var(--line);color:var(--fg);line-height:1.45}
.md-prose .md-tbl tr:nth-child(odd) td{background:color-mix(in srgb,var(--panel2) 70%,transparent)}
/* Verbose flood: collapse repeated streamed-output-estimate ticks into 1 live row */
.verbose-tick{font-size:11px;line-height:1.35;color:var(--dimmer);opacity:.72;letter-spacing:0;padding:2px 8px;border-left:2px solid var(--line);background:color-mix(in srgb,var(--planner) 4%,transparent)}
.verbose-tick b{color:var(--brand);font-variant-numeric:tabular-nums}
/* Live activity line near agent spinner */
.agent-activity{display:inline-block;margin-left:7px;padding:1px 7px;border-radius:5px;background:color-mix(in srgb,var(--brand) 10%,transparent);color:var(--brand);font-size:11px;line-height:1.35;font-style:italic;animation:pulse-act 1.6s ease-in-out infinite}
@keyframes pulse-act{0%,100%{opacity:.85}50%{opacity:.45}}
/* Caret animation (spinner dot) — only on currently active line */
.caret{display:inline-block;margin-left:6px;animation:caret-bl 1s steps(1) infinite;color:var(--brand)}
.caret::after{content:"●"}
@keyframes caret-bl{0%,49%{opacity:1}50%,100%{opacity:0}}`;
document.head.appendChild(st);
})();
async function stopRun(){
try{await fetch('/stop',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}'});}
catch(e){}
setRunActive(false);
if(typeof toast==='function')toast('run aborted','ok');
}
$('runBtn').addEventListener('click',runTask);
const _stopBtn=$('stopBtn');if(_stopBtn)_stopBtn.addEventListener('click',stopRun);
const _focusOk=$('focusOkBtn');if(_focusOk)_focusOk.addEventListener('click',focusAccept);
const _focusUndo=$('focusUndoBtn');if(_focusUndo)_focusUndo.addEventListener('click',focusUndo);
const _focusExplain=$('focusExplainBtn');if(_focusExplain)_focusExplain.addEventListener('click',focusExplain);
const _micBtn=$('micBtn');if(_micBtn)_micBtn.addEventListener('click',toggleMicDictation);
$('taskInput').addEventListener('keydown',composerKeydown);
$('taskInput').addEventListener('input',composerInput);
$('taskInput').addEventListener('paste',composerPaste);
$('fileInput').addEventListener('change',handleFiles);
restoreDraft();
autoResizeComposer();
initFocusTour();
function composerInput(){
HISTORY_IDX=null;
const value=$('taskInput').value;
localStorage.setItem('sparrow-composer-draft',value);
autoResizeComposer();
updateContextMeter();
const palette=document.getElementById('palette');
if(value.startsWith('/')&&!/\s/.test(value.trim())&&document.activeElement===$('taskInput')){
if(!palette.classList.contains('open')){
palette.classList.add('open');
const pinput=document.getElementById('paletteInput');
pinput.value=value;
PALETTE_SEL=0;
paletteFilter(value);
setTimeout(()=>pinput.focus(),0);
}
}else if(palette.classList.contains('open')&&document.activeElement===$('taskInput')){
paletteClose();
}
}
function restoreDraft(){
const draft=localStorage.getItem('sparrow-composer-draft')||'';
if(draft&&!$('taskInput').value)$('taskInput').value=draft;
}
function autoResizeComposer(){
const input=$('taskInput');
input.style.height='auto';
const max=parseFloat(getComputedStyle(input).lineHeight||'18')*12+18;
input.style.height=Math.min(input.scrollHeight,max)+'px';
input.style.overflowY=input.scrollHeight>max?'auto':'hidden';
}
function composerKeydown(e){
if(e.defaultPrevented)return;
if(e.key==='Enter'&&!e.shiftKey){
e.preventDefault();
runTask();
return;
}
if(e.key==='ArrowUp'&&!attachedFiles.length&&composerAtFirstLine()){
if(navigateHistory(1))e.preventDefault();
return;
}
if(e.key==='ArrowDown'&&HISTORY_IDX!==null&&composerAtLastLine()){
if(navigateHistory(-1))e.preventDefault();
return;
}
if(e.key==='Escape'&&HISTORY_IDX!==null){
e.preventDefault();
HISTORY_IDX=null;
$('taskInput').value=HISTORY_DRAFT;
composerInput();
}
}
function composerAtFirstLine(){
const input=$('taskInput');
return input.value.slice(0,input.selectionStart||0).indexOf('\n')<0;
}
function composerAtLastLine(){
const input=$('taskInput');
return input.value.slice(input.selectionStart||0).indexOf('\n')<0;
}
function navigateHistory(direction){
if(!HISTORY_INPUTS.length)return false;
if(HISTORY_IDX===null){
HISTORY_DRAFT=$('taskInput').value;
HISTORY_IDX=direction>0?0:null;
}else{
HISTORY_IDX+=direction>0?1:-1;
}
if(HISTORY_IDX===null||HISTORY_IDX<0){
HISTORY_IDX=null;
$('taskInput').value=HISTORY_DRAFT;
}else if(HISTORY_IDX>=HISTORY_INPUTS.length){
HISTORY_IDX=HISTORY_INPUTS.length-1;
}
if(HISTORY_IDX!==null)$('taskInput').value=HISTORY_INPUTS[HISTORY_IDX];
localStorage.setItem('sparrow-composer-draft',$('taskInput').value);
autoResizeComposer();
updateContextMeter();
const input=$('taskInput');
input.selectionStart=input.selectionEnd=input.value.length;
return true;
}
function rememberSubmitted(text){
const clean=text.trim();
if(!clean)return;
HISTORY_INPUTS=HISTORY_INPUTS.filter(v=>v!==clean);
HISTORY_INPUTS.unshift(clean);
HISTORY_INPUTS=HISTORY_INPUTS.slice(0,80);
HISTORY_IDX=null;
HISTORY_DRAFT='';
localStorage.removeItem('sparrow-composer-draft');
}
async function composerPaste(e){
const files=Array.from(e.clipboardData?.files||[]);
if(files.length){
e.preventDefault();
await attachFiles(files);
insertComposerText(files.map(f=>`[attachment: ${f.name}]`).join(' '));
return;
}
const text=e.clipboardData?.getData('text')||'';
if(text.split(/\r?\n/).length>5){
setTimeout(autoResizeComposer,0);
}
}
let dragDepth=0;
window.addEventListener('dragenter',e=>{
if(!Array.from(e.dataTransfer?.types||[]).includes('Files'))return;
dragDepth++;
e.preventDefault();
$('dropZone').classList.add('show');
});
window.addEventListener('dragover',e=>{
if(Array.from(e.dataTransfer?.types||[]).includes('Files'))e.preventDefault();
});
window.addEventListener('dragleave',e=>{
if(!Array.from(e.dataTransfer?.types||[]).includes('Files'))return;
dragDepth=Math.max(0,dragDepth-1);
if(dragDepth===0)$('dropZone').classList.remove('show');
});
window.addEventListener('drop',async e=>{
const files=Array.from(e.dataTransfer?.files||[]);
if(!files.length)return;
e.preventDefault();
dragDepth=0;
$('dropZone').classList.remove('show');
await attachFiles(files);
insertComposerText(files.map(f=>`[attachment: ${f.name}]`).join(' '));
});
function renderAttachments(){
const box=$('attachments');
if(!attachedFiles.length){box.classList.remove('show');box.innerHTML='';updateContextMeter();return;}
box.classList.add('show');
box.innerHTML=attachedFiles.map((f,i)=>`
<span class="file-chip" title="${escAttr(f.name)}">
<span>${esc(f.name)} · ${f.tokens.toLocaleString('en-US')} tok</span>
<button type="button" aria-label="remove ${escAttr(f.name)}" data-remove-file="${i}">×</button>
</span>`).join('');
box.querySelectorAll('[data-remove-file]').forEach(btn=>btn.addEventListener('click',()=>{
attachedFiles.splice(Number(btn.dataset.removeFile),1);
renderAttachments();
}));
updateContextMeter();
}
async function handleFiles(e){
const files=Array.from(e.target.files||[]);
await attachFiles(files);
e.target.value='';
}
async function attachFiles(files){
let usedChars=attachedFiles.reduce((sum,f)=>sum+(f.content||'').length,0);
for(const file of files){
if(file.size>MAX_ATTACHMENT_BYTES){
showError('Upload rejected',`${file.name} too large: ${file.size} bytes > limit ${MAX_ATTACHMENT_BYTES}`,'Attach a smaller file or compress it before retrying.','docs/media.md');
continue;
}
const isText=file.type.startsWith('text/')||/\.(md|txt|json|rs|js|ts|tsx|jsx|py|toml|yaml|yml|html|css|csv)$/i.test(file.name);
const uploaded=await uploadFile(file);
if(!isText){
attachedFiles.push({name:file.name,content:'',note:uploaded?.path?`[uploaded: ${uploaded.path}]`:`[binary file omitted: ${file.name}, ${file.size} bytes]`,tokens:estimateTokens(file.name)});
continue;
}
const raw=await file.text();
const remaining=Math.max(0,ATTACHMENT_CHAR_LIMIT-usedChars);
const content=raw.slice(0,remaining);
usedChars+=content.length;
const truncated=raw.length>content.length;
attachedFiles.push({
name:file.name,
content,
note:(uploaded?.path?`uploaded: ${uploaded.path}`:'')+(truncated?'\ntruncated to fit console attachment budget':''),
tokens:estimateTokens(content)
});
if(usedChars>=ATTACHMENT_CHAR_LIMIT)break;
}
renderAttachments();
}
async function uploadFile(file){
const form=new FormData();
form.append('file',file,file.name);
try{
const r=await fetch('/upload',{method:'POST',body:form});
const j=await r.json();
const rejected=Array.isArray(j.rejected)?j.rejected:[];
rejected.forEach(x=>showError('Upload rejected',`${x.name||file.name} ${x.reason||''}`,'Attach a smaller or supported file and retry.','docs/media.md'));
const accepted=Array.isArray(j.accepted)?j.accepted:[];
if(accepted[0]){
loadArtifacts();
return accepted[0];
}
}catch(e){
showError('Upload failed',e.message,'Check that the WebView server is still running.');
}
return null;
}
function insertComposerText(text){
const input=$('taskInput');
const start=input.selectionStart??input.value.length;
const end=input.selectionEnd??input.value.length;
const prefix=input.value&&start>0&&!/\s$/.test(input.value.slice(0,start))?' ':'';
const suffix=end<input.value.length&&!/^\s/.test(input.value.slice(end))?' ':'';
input.value=input.value.slice(0,start)+prefix+text+suffix+input.value.slice(end);
const caret=start+prefix.length+text.length+suffix.length;
input.selectionStart=input.selectionEnd=caret;
composerInput();
input.focus();
}
function attachmentPrompt(){
if(!attachedFiles.length)return '';
const blocks=attachedFiles.map(f=>{
if(!f.content)return `### file: ${f.name}\n${f.note}`;
const note=f.note?`\n${f.note}`:'';
return `### file: ${f.name}${note}\n\`\`\`text\n${f.content}\n\`\`\``;
});
return `\n\n[Attached files]\n${blocks.join('\n\n')}`;
}
async function runTask(){
const task=$('taskInput').value.trim();if(!task&&!attachedFiles.length)return;
if(task.startsWith('/')){
const handled=await handleSlashCommand(task);
if(handled)return;
}
const payload=task+attachmentPrompt();
rbDetectIntent(task); $('taskInput').value='';
localStorage.removeItem('sparrow-composer-draft');
autoResizeComposer();
const runBtn=$('runBtn');if(runBtn){runBtn.style.transform='scale(0.92)';setTimeout(function(){runBtn.style.transform='';},120);}
updateContextMeter();
const attachNote=attachedFiles.length?` <span class="muted">+ ${attachedFiles.length} file${attachedFiles.length>1?'s':''}</span>`:'';
line(`<span class="prompt">you ›</span> <span class="cmd">${esc(task||'analyse attached files')}</span>${attachNote}`);
try{
const runBody={task:payload};
if(selectedModel)runBody.model_override=selectedModel;
if(ACTIVE_AGENT)runBody.agent_name=ACTIVE_AGENT;
const r=await fetch('/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(runBody)});
const j=await r.json();
if(!j.ok)showError('Run failed',j.message,'Check the command channel and retry.');
else{rememberSubmitted(task||'analyse attached files');attachedFiles=[];renderAttachments();updateContextMeter();}
}catch(e){showError('Run failed',e.message,'Check that the WebView server is still connected.')}
}
async function handleSlashCommand(task){
const lower=task.toLowerCase();
const cmd=(lower.match(/^\/([a-z0-9_-]+)/)||[])[1]||'';
if(cmd==='help'||cmd==='commands'){await showCommands();clearComposerAfterCommand();return true;}
if(cmd==='plan'){await planTask(task.replace(/^\/plan\s*/i,'').trim());return true;}
if(cmd==='run'){
const prompt=task.replace(/^\/run\s*/i,'').trim();
if(prompt){
$('taskInput').value=prompt;
await runTask();
}else{
line('<span class="warn">Usage: /run <task></span>');
clearComposerAfterCommand();
}
return true;
}
if(cmd==='clear'){
term.innerHTML='';
clearComposerAfterCommand();
return true;
}
if(cmd==='reset'){
clearComposerAfterCommand();
await resetConversationFromSlash();
return true;
}
if(cmd==='stop'){
clearComposerAfterCommand();
await stopRun();
return true;
}
if(cmd==='config'){
clearComposerAfterCommand();
openConfig();
line('<span class="acc">/config</span> <span class="muted">configuration panel opened</span>');
return true;
}
if(cmd==='upload'){
clearComposerAfterCommand();
$('fileInput')?.click();
line('<span class="acc">/upload</span> <span class="muted">choose files, or drag them into Sparrow</span>');
return true;
}
const localEndpoints={
status:'/status',
memory:'/memory',
tools:'/tools',
plugins:'/plugins',
security:'/security',
sessions:'/sessions',
agents:'/agents',
routing:'/routing',
permissions:'/permissions',
models:'/models'
};
if(localEndpoints[cmd] && task.trim()===`/${cmd}`){
await showEndpointCommand(task,localEndpoints[cmd]);
clearComposerAfterCommand();
return true;
}
await runWebviewCliCommand(task);
clearComposerAfterCommand();
return true;
}
function clearComposerAfterCommand(){
$('taskInput').value='';
localStorage.removeItem('sparrow-composer-draft');
autoResizeComposer();
updateContextMeter();
}
async function resetConversationFromSlash(){
try{
const r=await fetch('/conversation/reset',{method:'POST'});
const j=await r.json();
line(`<span class="${j.ok?'acc':'warn'}">/reset</span> <span class="muted">${esc(j.message||'conversation reset')}</span>`);
}catch(e){showError('Reset failed',e.message,'Reload the WebView and retry.')}
}
async function runWebviewCliCommand(task){
line(`<span class="prompt">sparrow ›</span> <span class="cmd">${esc(task)}</span>`);
try{
const r=await fetch('/cli',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({command:task})});
const j=await r.json();
const label=j.ok?'completed':(j.message||'failed');
line(`<span class="${j.ok?'acc':'warn'}">↳ command ${esc(label)}</span>`);
if(j.stdout)renderCommandOutput('stdout',j.stdout);
if(j.stderr)renderCommandOutput('stderr',j.stderr);
if(!j.stdout&&!j.stderr&&!j.ok)line(`<span class="warn">${esc(j.message||'command failed')}</span>`);
if(j.ok)rememberSubmitted(task);
}catch(e){showError('Command failed',e.message,'Check that the WebView server is still connected.')}
}
async function showEndpointCommand(task,url){
line(`<span class="prompt">sparrow ›</span> <span class="cmd">${esc(task)}</span>`);
try{
const r=await fetch(url);
const j=await r.json();
renderCommandOutput(url.replace('/','')||'json',JSON.stringify(j,null,2),'json');
rememberSubmitted(task);
}catch(e){showError('Command failed',e.message,`Unable to read ${url}.`)}
}
function renderCommandOutput(label,text,lang='text'){
const d=document.createElement('details');
d.className='code-card';
d.open=true;
const lines=String(text).split(/\r?\n/).filter(Boolean).length||1;
d.dataset.lang=lang;
d.innerHTML='<summary><span class="cc-icon">▸</span>'+
`<span class="cc-lang">${escHtml(label)}</span>`+
`<span class="cc-meta">${lines} lines</span>`+
'<button class="cc-copy" onclick="navigator.clipboard.writeText(this.closest(\'.code-card\').querySelector(\'code\').textContent);this.textContent=\'copied ✓\'">copy</button>'+
'</summary><pre><code></code></pre>';
d.querySelector('code').textContent=text;
term.appendChild(d);
term.scrollTop=term.scrollHeight;
}
async function showCommands(){
try{
const r=await fetch('/commands');
const j=await r.json();
if(!j.ok){showError('Commands failed',j.message,'Reload the WebView or run `sparrow commands` in the terminal.');return;}
line('<span class="acc">/help</span> <span class="muted">available slash commands</span>');
(j.commands||[]).forEach(cmd=>{
line(`<span class="planner">${esc(cmd.name)}</span> <span class="cmd">— ${esc(cmd.description||'Run Sparrow command')}</span> <span class="muted">· ${esc(cmd.usage||'Usage: '+cmd.name)} · ${esc(cmd.source)}</span>`);
});
}catch(e){showError('Commands failed',e.message,'Reload the WebView or run `sparrow commands` in the terminal.')}
}
async function loadMemory(){
try{
const r=await fetch('/memory');
const j=await r.json();
if(!j.ok||!j.stats)return;
const s=j.stats;
const text=`${s.facts} facts · MEMORY.md ${s.memory_chars}/${s.memory_limit} · USER.md ${s.user_chars}/${s.user_limit}`;
const target=$('memorySummary');
if(target)target.textContent=text;
}catch(_e){
const target=$('memorySummary');
if(target)target.textContent='Memory status unavailable.';
}
}
async function planTask(task){
if(!task&&attachedFiles.length){task='analyse attached files'}
if(!task){line('<span class="warn">Usage: /plan <task></span>');return;}
const payload=task+attachmentPrompt();
$('taskInput').value='';
updateContextMeter();
line(`<span class="prompt">sparrow ›</span> <span class="cmd">/plan ${esc(task)}</span>`);
try{
const r=await fetch('/plan',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({task:payload,agent_name:ACTIVE_AGENT||undefined})});
const j=await r.json();
if(!j.ok||!j.plan){showError('Plan failed',j.message,'Try a more specific task or check provider configuration.');return;}
renderPlan(j.plan,task);
}catch(e){showError('Plan failed',e.message,'Check that the WebView server is still connected.')}
}
function renderPlan(plan,task){
line(`<span class="acc">◆ read-only plan</span> <span class="muted">tier ${esc(plan.estimated_tier)} · no tools or edits executed</span>`);
line(`<span class="muted">${esc(plan.summary)}</span>`);
(plan.steps||[]).forEach((step,i)=>line(`<span class="planner">${i+1}.</span> <span class="cmd">${esc(step)}</span>`));
if((plan.risks||[]).length){
line('<span class="warn">Risks</span>');
plan.risks.forEach(r=>line(`<span class="muted">- ${esc(r)}</span>`));
}
const actions=line(`<span class="approval-actions" data-plan-task="${escAttr(task)}"><button class="approve" data-plan-action="run">run plan</button><button data-plan-action="edit">edit</button><button class="deny" data-plan-action="reject">reject</button></span>`);
actions.querySelector('[data-plan-action="run"]').addEventListener('click',()=>acceptPlan(task));
actions.querySelector('[data-plan-action="edit"]').addEventListener('click',()=>editPlan(task));
actions.querySelector('[data-plan-action="reject"]').addEventListener('click',()=>rejectPlan(task));
}
function acceptPlan(task){
$('taskInput').value=task;
runTask();
}
function editPlan(task){
$('taskInput').value=task;
$('taskInput').focus();
updateContextMeter();
}
function rejectPlan(task){
line(`<span class="muted">↳ plan rejected · ${esc(task||'no task')}</span>`);
}
function showApprovalModal(ev){
const modal=$('approvalModal');
modal.dataset.id=ev.id||'';
$('approvalSummary').textContent=ev.summary||'Sparrow is asking before it acts.';
$('approvalRisk').textContent='risk: '+(ev.risk||'unknown');
modal.classList.add('show');
setTimeout(()=>$('approvalApprove').focus(),40);
}
function closeApprovalModal(){
const modal=$('approvalModal');
modal.classList.remove('show');
modal.dataset.id='';
}
async function resolveApprovalFromModal(decision){
const modal=$('approvalModal');
const id=modal.dataset.id;
if(!id)return;
[...modal.querySelectorAll('button')].forEach(b=>b.disabled=true);
try{
const r=await fetch('/approval',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,decision})});
const j=await r.json();
if(j.ok){
closeApprovalModal();
line(`<span class="${decision==='approve'?'ok':'warn'}">approval ${decision==='approve'?'approved':'denied'}</span>`);
}else{
showError('Approval failed',j.message,'The request may have expired; retry the action if it is still needed.');
}
}catch(e){
showError('Approval failed',e.message,'Check that the WebView server is still connected.');
}finally{
[...modal.querySelectorAll('button')].forEach(b=>b.disabled=false);
}
}
$('approvalApprove').addEventListener('click',()=>resolveApprovalFromModal('approve'));
$('approvalDeny').addEventListener('click',()=>resolveApprovalFromModal('deny'));
$('approvalModal').addEventListener('click',e=>{if(e.target.id==='approvalModal')closeApprovalModal()});
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&$('approvalModal').classList.contains('show'))closeApprovalModal()});
async function resolveApproval(id,decision,btn){
const card=btn.closest('.approval-card-inline');
const wrap=btn.closest('.approval-actions');
if(wrap)[...wrap.querySelectorAll('button')].forEach(b=>b.disabled=true);
const tool=card?.dataset?.tool||'';
if(decision==='allow_session' && tool){
window.SPARROW_SESSION_ALLOW=window.SPARROW_SESSION_ALLOW||new Set();
window.SPARROW_SESSION_ALLOW.add(tool);
}
if(decision==='allow_always' && tool){
try{
const pr=await fetch('/permissions');const pj=await pr.json();
const allow=Array.isArray(pj.allow)?pj.allow.slice():[];
if(!allow.includes(tool))allow.push(tool);
await fetch('/permissions',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({allow})});
}catch(_){}
}
try{
const r=await fetch('/approval',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,decision})});
const j=await r.json();
if(j.ok){
const label={allow_once:'allowed (once)',allow_session:'allowed (session)',allow_always:'allowed (always)',approve:'allowed',deny:'denied'}[decision]||decision;
const cls=decision==='deny'?'warn':'ok';
if(card){
card.open=false;
const summaryEl=card.querySelector('summary');
if(summaryEl){
summaryEl.innerHTML=`<span style="color:var(--dim);font-size:10px;letter-spacing:1px">↳ ${esc(tool||'tool')}</span> <span class="${cls}" style="margin-left:8px;font-size:11px">${label}</span>`;
}
card.style.borderColor='var(--line)';
card.style.background='transparent';
} else if(wrap){
wrap.innerHTML=`<span class="${cls}">${label}</span>`;
}
}else{
if(wrap)wrap.innerHTML=`<span class="err">${esc(j.message)}</span>`;
}
}catch(e){if(wrap)wrap.innerHTML=`<span class="err">${esc(e.message)}</span>`;}
}
function openAgentCreator(){
const bd=document.getElementById('agentCreatorBackdrop');
if(!bd)return;
bd.style.display='flex';
setTimeout(()=>document.getElementById('ag-name')?.focus(),50);
}
function closeAgentCreator(){
const bd=document.getElementById('agentCreatorBackdrop');
if(!bd)return;
bd.style.display='none';
const fb=document.getElementById('ag-feedback');if(fb)fb.textContent='';
}
async function saveNewAgent(){
const name=document.getElementById('ag-name').value.trim();
const fb=document.getElementById('ag-feedback');
if(!name){fb.style.color='var(--rem)';fb.textContent='name is required';return;}
if(!/^[A-Za-z0-9_-]+$/.test(name)){fb.style.color='var(--rem)';fb.textContent='name must be ascii letters/digits/_/-';return;}
const body={
name,
role:document.getElementById('ag-role').value.trim()||undefined,
description:document.getElementById('ag-desc').value.trim()||undefined,
model:document.getElementById('ag-model').value.trim()||undefined,
color_key:document.getElementById('ag-color').value||undefined,
allowed_tools:document.getElementById('ag-tools').value.split(',').map(s=>s.trim()).filter(Boolean),
soul:document.getElementById('ag-soul').value.trim()||undefined,
agent_md:document.getElementById('ag-md').value.trim()||undefined,
};
if(!body.allowed_tools.length)delete body.allowed_tools;
const btn=document.getElementById('ag-save');btn.disabled=true;
fb.style.color='var(--dim)';fb.textContent='creating…';
try{
const r=await fetch('/agents',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
const j=await r.json();
if(j.ok){
fb.style.color='var(--add)';fb.textContent='created · '+j.soul_path;
loadSwarmAgents().catch(()=>{});
loadCrewPanel?.().catch(()=>{});
setTimeout(closeAgentCreator,900);
} else {
fb.style.color='var(--rem)';fb.textContent=j.message||'failed';
}
}catch(e){fb.style.color='var(--rem)';fb.textContent=e.message;}
btn.disabled=false;
}
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&document.getElementById('agentCreatorBackdrop')?.style.display==='flex')closeAgentCreator();});
function openConfig(){$('overlay').classList.add('show');$('configPanel').classList.add('show');(async()=>{await loadConfig();await loadRouting();})();setTimeout(()=>$('provSelect').focus(),60)}
function closeConfig(){$('overlay').classList.remove('show');$('configPanel').classList.remove('show');$('taskInput').focus()}
$('cfgBtn').addEventListener('click',openConfig);
$('closeConfig').addEventListener('click',closeConfig);
$('overlay').addEventListener('click',closeConfig);
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&$('configPanel').classList.contains('show'))closeConfig()});
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&document.getElementById('diffPanel').classList.contains('open'))closeDiffPanel();});
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&document.getElementById('modelPickerDrop').style.display!=='none')document.getElementById('modelPickerDrop').style.display='none';});
let providers=[];
let _modelsRegistry={};
async function loadConfig(){
try{
const [cr,mr]=await Promise.all([fetch('/config'),fetch('/models')]);
const j=await cr.json();const md=await mr.json();
if(!j.ok)return;
providers=j.providers||[];
_modelsRegistry={};
(md.providers||[]).forEach(p=>{_modelsRegistry[p.id]={};p.models.forEach(m=>{_modelsRegistry[p.id][m.name]=m;});});
if(j.autonomy){$('autonomySelect').value=j.autonomy.toLowerCase();}
if(j.sandbox){$('sandboxSelect').value=j.sandbox;}
await loadPermissions();
setAuto(($('autonomySelect').value||'trusted').toLowerCase());
const sel=$('provSelect');if(sel)sel.innerHTML='<option value="">— choose provider —</option>'+providers.map(p=>`<option value="${escAttr(p.name)}">${esc(p.label||p.name)}</option>`).join('');
const badge=$('credBadge');
const keyCount=providers.filter(p=>p.has_credential).length;
if(badge){badge.textContent=keyCount+'/'+providers.length+' keys';badge.className='badge '+(keyCount>0?'found':'missing');}
renderProviderList();
const totalModels=providers.reduce((s,p)=>s+(p.models||[]).length,0);
if($('cfgProvCount'))$('cfgProvCount').textContent=providers.length;
if($('cfgKeyCount'))$('cfgKeyCount').textContent=keyCount;
if($('cfgModelCount'))$('cfgModelCount').textContent=totalModels;
const drs=$('defaultRouteSelect');
if(drs){
drs.innerHTML='<option value="">— auto (cheapest capable) —</option>'+
providers.flatMap(p=>(p.models||[]).map(m=>`<option value="${escAttr(p.name+':'+m)}">${esc(p.label||p.name)} · ${esc(m)}${p.has_credential?'':' ⚠ no key'}</option>`)).join('');
if(selectedModel)drs.value=selectedModel;
}
if($('cfgCurrentModel'))$('cfgCurrentModel').textContent=selectedModel||'auto-route';
}catch(e){console.error('loadConfig failed',e);}
}
function renderProviderList(){
const host=document.getElementById('providerList');if(!host)return;
host.innerHTML=providers.map(p=>{
const reg=_modelsRegistry[p.name]||{};
const models=(p.models||[]).map(m=>{
const info=reg[m]||{};
const isDefault=selectedModel===p.name+':'+m;
const star=info.recommended?'<span class="mstar" title="recommended">★</span>':'';
const ctx=info.context_window?fmtCtxShort(info.context_window):'';
const cost=info.cost_in!==undefined?(info.cost_in===0?'free':'$'+info.cost_in+'/M'):'';
return `<div class="mrow${isDefault?' is-default':''}">
<span class="mname">${escHtml(m)}</span>
${star}
${ctx?'<span class="mctx">'+escHtml(ctx)+'</span>':''}
${cost?'<span class="mcost">'+escHtml(cost)+'</span>':''}
<button class="mset" onclick="setModelDefault('${escHtml(p.name+':'+m)}')">${isDefault?'✓ default':'set default'}</button>
</div>`;
}).join('');
const envHint=p.api_key_env?`<span class="envhint">env: <b>${escHtml(p.api_key_env)}</b></span>`:'';
return `<details class="cfg-provider${p.has_credential?' has-key':''}" data-prov="${escHtml(p.name)}">
<summary>
<span class="led"></span>
<span class="lbl">${escHtml(p.label||p.name)}</span>
<span class="adp">${escHtml(p.adapter||'')}</span>
<span class="count">${(p.models||[]).length} model${(p.models||[]).length!==1?'s':''}${p.has_credential?' · key ✓':''}</span>
<span class="chev">▸</span>
</summary>
<div class="body">
${p.notes?'<div class="notes">'+escHtml(p.notes)+'</div>':''}
<div class="models">${models||'<div style="color:var(--dimmer);font-size:10.5px;padding:6px">no models declared</div>'}</div>
<div class="keyrow">
${envHint}
<input type="password" autocomplete="off" id="key-${escHtml(p.name)}" placeholder="${p.has_credential?'key configured · paste to override':'paste API key'}">
<button onclick="saveProviderKey('${escHtml(p.name)}')">save key</button>
<button class="btn sm" onclick="scanProviderModels('${escHtml(p.name)}',this)" title="Fetch all available models from this provider API">🔍 scan models</button>
</div>
<div class="scan-result" id="scan-${escHtml(p.name)}" style="font-size:10.5px;color:var(--dim);margin-top:4px;display:none"></div>
</div>
</details>`;
}).join('');
}
function filterProviders(q){
const norm=q.toLowerCase().trim();
document.querySelectorAll('.cfg-provider').forEach(d=>{
const txt=d.textContent.toLowerCase();
d.style.display=(!norm||txt.includes(norm))?'':'none';
if(norm&&txt.includes(norm))d.setAttribute('open','');
});
}
function setModelDefault(id){
setModelOverride(id,_modelsRegistry[id.split(':')[0]]?.[id.split(':').slice(1).join(':')]?.context_window?fmtCtxShort(_modelsRegistry[id.split(':')[0]][id.split(':').slice(1).join(':')].context_window):'');
renderProviderList();
if($('cfgCurrentModel'))$('cfgCurrentModel').textContent=id;
const drs=$('defaultRouteSelect');if(drs)drs.value=id;
}
async function saveProviderKey(name){
const p=providers.find(p=>p.name===name);if(!p)return;
const inp=document.getElementById('key-'+name);const key=inp.value.trim();
if(!key){toast('paste a key first','error');return;}
const body={
name,adapter:p.adapter||'openai-compatible',base_url:p.base_url||null,
models:p.models||[],
api_key_env:p.api_key_env||null,
api_key:key,
autonomy:$('autonomySelect').value,
sandbox:$('sandboxSelect').value,
};
try{
const r=await fetch('/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
const j=await r.json();
if(j.ok){inp.value='';await loadConfig();toast('key saved','ok');}
else toast('save failed: '+j.message,'error');
}catch(e){toast('save failed: '+e.message,'error');}
}
async function scanProviderModels(name, btn){
const result=document.getElementById('scan-'+name);
if(!result)return;
btn.disabled=true;btn.textContent='scanning…';
result.style.display='block';result.textContent='⏳ fetching models…';
try{
const r=await fetch('/providers/scan',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({provider:name})});
const j=await r.json();
if(j.ok){
result.style.color='var(--add)';result.textContent='✓ '+j.message+' — '+j.models.join(', ');
const prov=document.querySelector('.cfg-provider[data-prov="'+name+'"] .models');
if(prov&&j.models.length){
const existing=new Set();prov.querySelectorAll('.mname').forEach(function(m){existing.add(m.textContent);});
var added=0;
j.models.forEach(function(m){
if(!existing.has(m)){
var d=document.createElement('div');d.className='mrow';
var reg=_modelsRegistry[name]||{};var info=reg[m]||{};
var ctx=info.context_window?fmtCtxShort(info.context_window):'';
var cost=info.cost_in!==undefined?(info.cost_in===0?'free':'$'+info.cost_in+'/M'):'';
d.innerHTML='<span class="mname" style="color:var(--add)">'+escHtml(m)+'</span>'+
(ctx?'<span class="mctx">'+escHtml(ctx)+'</span>':'')+
(cost?'<span class="mcost">'+escHtml(cost)+'</span>':'');
var btn=document.createElement('button');btn.className='mset';
var mid=name+':'+m;
btn.textContent='set default';
btn.onclick=function(){setModelDefault(mid);};
d.appendChild(btn);
prov.appendChild(d);added++;
}
});
if(added)result.textContent+=' · '+added+' new model'+(added>1?'s':'')+' added ↑';
}
}else{
result.style.color='var(--rem)';result.textContent='✗ '+j.message;
}
}catch(e){
result.style.color='var(--rem)';result.textContent='✗ '+e.message;
}finally{btn.disabled=false;btn.textContent='🔍 scan models';}
}
async function loadRouting(){
try{
const r=await fetch('/routing');const j=await r.json();
const sel=document.getElementById('preferredProviderSelect');
if(sel){
const cur=j.preferred_provider||'';
sel.innerHTML='<option value="">-- auto (smart routing per tier) --</option>'+
(j.all_providers||[]).map(function(id){return '<option value="'+id+'"'+(id===cur?' selected':'')+'>'+id+'</option>';}).join('');
}
const modelSel=document.getElementById('preferredModelSelect');
if(modelSel){
modelSel.value=j.preferred_model||'';
}
const modeSel=document.getElementById('routingModeSelect');
if(modeSel){
modeSel.value=j.routing_mode||'auto';
}
const tog=document.getElementById('autoDiscoverToggle');
if(tog)tog.checked=j.auto_discover!==false;
}catch(e){console.warn('loadRouting failed',e);}
}
async function saveRouting(){
const sel=document.getElementById('preferredProviderSelect');
const modelSel=document.getElementById('preferredModelSelect');
const modeSel=document.getElementById('routingModeSelect');
const tog=document.getElementById('autoDiscoverToggle');
const body={
preferred_provider:sel&&sel.value?sel.value:null,
preferred_model:modelSel&&modelSel.value?modelSel.value:null,
routing_mode:modeSel?modeSel.value:'auto',
auto_discover:tog?tog.checked:true
};
try{
const r=await fetch('/routing',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
const j=await r.json();
const st=document.getElementById('routingSaveStatus');
if(j.ok&&st){st.style.display='inline';setTimeout(function(){st.style.display='none';},2500);}
else if(!j.ok)alert('save routing failed: '+j.message);
}catch(e){alert('save routing failed: '+e.message);}
}
document.addEventListener('click',e=>{
const tab=e.target.closest('.cfg-tab');if(!tab)return;
const name=tab.dataset.cfgTab;
document.querySelectorAll('.cfg-tab').forEach(t=>t.classList.toggle('active',t===tab));
document.querySelectorAll('[data-cfg-pane]').forEach(p=>{p.style.display=p.dataset.cfgPane===name?'':'none';});
});
async function loadPermissions(){
try{
const r=await fetch('/permissions');const j=await r.json();
const mode=j.permissions?.mode||'supervised';
if($('permissionSelect'))$('permissionSelect').value=mode;
}catch(e){}
}
function selectProvider(name){
const p=providers.find(p=>p.name===name);
if(!p){$('modelSelect').innerHTML='<option value="">— select provider first —</option>';$('modelTags').innerHTML='';$('apiKey').value='';$('credBadge').className='badge missing';$('credBadge').textContent='no key';return;}
$('modelSelect').innerHTML=p.models.map(m=>`<option value="${escAttr(m)}">${esc(m)}</option>`).join('');
$('apiKey').value='';
$('apiKey').placeholder=p.api_key_env?'env: '+p.api_key_env:'paste key here';
$('credBadge').className='badge '+(p.has_credential?'found':'missing');
$('credBadge').textContent=p.has_credential?'key found':(p.configured?'configured · no key':'not configured');
const tags=(p.tags||[]).map(t=>`<span class="tag">${esc(t)}</span>`).join('');
$('modelTags').innerHTML=tags+(p.notes?`<span class="tag">${esc(p.notes)}</span>`:'');
}
$('saveConfig').addEventListener('click',async()=>{
const name=$('provSelect').value;
const p=providers.find(p=>p.name===name);
const body={
name,adapter:p?.adapter||'openai-compatible',base_url:p?.base_url||null,
models:[$('modelSelect').value].filter(m=>m),
api_key_env:$('apiKey').placeholder.startsWith('env:')?$('apiKey').placeholder.slice(5):null,
api_key:$('apiKey').value.trim()||null,
autonomy:$('autonomySelect').value,
sandbox:$('sandboxSelect').value,
};
try{
const r=await fetch('/config',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
const j=await r.json();
$('saveMsg').textContent=j.ok?'saved ✓':'error: '+j.message;
$('saveMsg').style.color=j.ok?'var(--add)':'var(--rem)';
if(j.ok){
await savePermissions();
$('apiKey').value='';
await loadConfig();
if(name)selectProvider(name);
setAuto(($('autonomySelect').value||'trusted').toLowerCase());
setTimeout(closeConfig,450)
}
}catch(e){$('saveMsg').textContent='error: '+e.message;$('saveMsg').style.color='var(--rem)'}
});
async function savePermissions(){
if(!$('permissionSelect'))return;
const r=await fetch('/permissions',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({mode:$('permissionSelect').value})});
const j=await r.json();
if(!j.ok)throw new Error(j.message||'permissions save failed');
}
document.querySelectorAll('.preset').forEach(el=>{
el.addEventListener('click',()=>{
document.querySelectorAll('.preset').forEach(e=>e.classList.remove('sel'));
el.classList.add('sel');
const p=el.dataset.preset;
const sel=$('provSelect');
if(p==='local'){sel.value='ollama';selectProvider('ollama')}
else if(p==='cloud'){sel.value='nvidia';selectProvider('nvidia')}
else if(p==='strong'){sel.value='anthropic';selectProvider('anthropic')}
});
});
for(let i=0;i<14;i++){const e=document.createElement('div');e.className='ember';
e.style.left=Math.random()*100+'vw';e.style.background=Math.random()>.5?'#f2a93c':'#f0674a';
e.style.animationDuration=(8+Math.random()*7)+'s';e.style.animationDelay=(Math.random()*7)+'s';document.body.appendChild(e);}
(async function checkUpdate(){
try{
const r=await fetch('/update/check');
const j=await r.json();
if(j.update_available){
const banner=document.createElement('div');
banner.style.cssText='position:fixed;top:0;left:0;right:0;z-index:9999;'
+'background:linear-gradient(135deg,#f2a93c,#d4891a);color:#0e0b08;'
+'padding:10px 20px;font-family:Inter,system-ui,sans-serif;font-size:13px;'
+'display:flex;align-items:center;justify-content:center;gap:12px;'
+'box-shadow:0 2px 12px rgba(242,169,60,0.3);cursor:default';
banner.innerHTML='📦 <b>Sparrow v'+j.latest+'</b> available (current: v'+j.current+') '
+'<button id="update-copy-btn" style="background:#0e0b08;color:#f2a93c;border:none;padding:4px 12px;border-radius:4px;cursor:pointer;font-size:12px;font-weight:600">copy cmd</button> '
+'<a href="'+j.release_url+'" target="_blank" '
+'style="color:#0e0b08;font-weight:600;font-size:12px">release notes →</a> '
+'<span onclick="this.parentElement.remove()" style="cursor:pointer;margin-left:8px;opacity:.6;font-size:16px">✕</span>';
document.body.appendChild(banner);
setTimeout(()=>{
const btn=document.getElementById('update-copy-btn');
if(btn) btn.onclick=()=>{
navigator.clipboard.writeText(j.install_cmd);
btn.textContent='copied!';
setTimeout(()=>{btn.textContent='copy cmd'},2000);
};
},100);
}
}catch(_){}
})();
bootIntro();
</script>
</body>
</html>