pi-straw-boss 0.6.10 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/gui/assets/ask-CVw44ZU3.js +1 -0
- package/dist/gui/assets/{chat-BO71QXXf.js → chat-DAQHdiSd.js} +1 -1
- package/dist/gui/assets/{index-D8pxo5hc.css → index-CHtZ727l.css} +1 -1
- package/dist/gui/assets/{index-5qCqN1sA.js → index-CWA0eWOk.js} +1 -1
- package/dist/gui/assets/layer-DkIPqu42.js +2 -0
- package/dist/gui/assets/views-CItUTayr.js +2 -0
- package/dist/gui/index.html +2 -2
- package/package.json +1 -1
- package/src/ask.ts +5 -3
- package/src/bridge.ts +21 -14
- package/src/control.ts +18 -14
- package/src/dialogs.ts +30 -6
- package/src/gui.ts +13 -4
- package/src/keys.ts +80 -0
- package/src/mirror.ts +131 -0
- package/src/probe.ts +31 -0
- package/dist/gui/assets/ask-Dpo9zn5X.js +0 -1
- package/dist/gui/assets/layer-DhF1EBxx.js +0 -2
- package/dist/gui/assets/views-U9D50TDD.js +0 -2
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ The tower only routes and coordinates, so it can run a lighter model: set `tower
|
|
|
48
48
|
| `boss_broadcast` | Message a tab, a workspace, all towers, or every session. |
|
|
49
49
|
| `/boss` | Take over as this tab's main agent. |
|
|
50
50
|
| `/boss status`, `/boss tower` | Show the status; open and focus the tower. |
|
|
51
|
-
| `/boss gui`, `/boss gui stop` | Open the net in the browser (any session, joins nothing); stop its server. Type into a session from its comms view; answer a waiting `robot_hand` selector or `ask_user_question` questionnaire there or in its alert (other dialogs show read-only, to answer in the terminal; `secret_drop` stays terminal-only). |
|
|
51
|
+
| `/boss gui`, `/boss gui stop` | Open the net in the browser (any session, joins nothing); stop its server. Type into a session from its comms view, or work its terminal with the key pad under the terminal tile; answer a waiting `robot_hand` selector or `ask_user_question` questionnaire there or in its alert (other dialogs show read-only, to answer in the terminal; `secret_drop` stays terminal-only). |
|
|
52
52
|
| `/boss gui remote`, `/boss gui remote off` | Reach the GUI from your phone over your tailnet (`tailscale serve`, never Funnel): pair by QR code; off removes only its own serve entry. |
|
|
53
53
|
|
|
54
54
|
Subagents and sessions in Herdr worktree workspaces never join: the session that launched them speaks for them. Outside Herdr the extension does nothing.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{n as e}from"./post-B39ywXKd.js";var t=e=>e.multiple?`It takes several choices.`:e.freeform&&e.options.length?`It also takes your own words.`:``,n=(n,r)=>e(`div`,{class:`ask-one`},n.header&&e(`b`,{class:`ask-head`},n.header),e(`p`,{class:`ask-q`},n.question),n.context&&e(`div`,{class:`ask-ctx`},n.context),n.options.length>0&&e(`ol`,{class:`ask-opts`,"aria-label":`Options`},...n.options.map(t=>e(`li`,{class:`ask-opt`},e(`b`,null,t.title),t.description&&e(`small`,null,t.description)))),r&&t(n)&&e(`small`,{class:`ask-how`},t(n))),r=e=>e.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g,`�`),i=(e,t)=>(e.select?.choices??[]).map((e,t)=>({...e,i:t})).filter(e=>!(t&&e.localOnly)),a=(e,t,n)=>({...e.size?{choices:[...e].sort((e,t)=>e-t)}:{},...t.trim()?{text:t}:{},...n.trim()?{note:n}:{}}),o=(e,t,n)=>({dialog:e.dialog,digest:e.digest,answerId:n,...t}),s={digest:`The question changed; read it again.`,"not-allowed":`That answer does not fit the question.`,malformed:`The session could not read that answer.`};function c(e){let{outcome:t,reason:n,by:r}=e.body;return t===`accepted`?[`ok`,`Answered.`]:t===`stale`?[`error`,r===`local`?`It was already answered in the terminal.`:r===`cancel`?`It was dismissed in the terminal.`:`It is no longer open.`]:t===`rejected`?[`error`,s[String(n)]??`The session refused that answer.`]:t===`busy`?[`error`,`Another answer is on its way; check the terminal.`]:t===`unknown`||e.status===504?[`error`,`Not confirmed. Check the terminal.`,!0]:[`error`,n??`Refused (${e.status}).`]}var l=e=>[`accepted`,`stale`,`busy`,`unknown`].includes(String(e.body.outcome))||e.status===504,u=e=>e.answer?`d\n${e.answer.dialog}\n${e.answer.digest}`:`c\n${e.callId}\n${e.why??``}`;function d(s,d,f=location.hostname!==`127.0.0.1`,p){let m,h,g,_=!1,v=!1,y;for(let e of[`focusin`,`pointerdown`])document.addEventListener(e,e=>h&&!h.el.contains(e.target)&&(h.lost=void 0));let b=(e,t,n,...r)=>(e.dataset.tone=t,e.replaceChildren(n,...r)),x=()=>{if(!h)return;let e=document.activeElement;e&&e!==document.body&&e!==h.lost&&(h.lost=void 0),h.go.disabled=!_||v,h.controls.forEach(e=>e.disabled=!_||v||h.done),h.send&&(h.send.disabled=!_||v||h.done||!h.pick()),e&&h.el.contains(e)&&e.disabled&&(h.lost=e);let t=h.lost;t&&!t.disabled&&h.el.contains(t)&&(!document.activeElement||document.activeElement===document.body)&&(t.focus(),h.lost=void 0)};async function S(t,n){[t.sending,v,t.spell]=[!0,!0,!1],x(),b(t.note,``,`Sending…`);let r=await d(`answer`,n).catch(()=>void 0),i=e(`button`,{type:`button`,class:`cmp-link`},`Check again`);i.onclick=()=>void S(t,n);let[a,o,s]=r?c(r):[`error`,`Not confirmed. Check the terminal.`,!0];[t.done,t.sending,v,t.busy]=[!r||l(r),!1,!1,r?.body.outcome===`busy`],b(t.note,a,o,...s?[i]:[]),x(),t.note.scrollIntoView?.({block:`nearest`})}let C=(t,n,r,i,a)=>{let o=e(`button`,{type:`button`,class:`ask-opt`,role:t,"aria-checked":`false`},e(`b`,a?{"data-n":String(a)}:null,n),...r.filter(Boolean).map(t=>e(`small`,null,t)));return o.onclick=i,o};function w(t,n){let a,o=i(n,f).map(e=>C(`radio`,e.label,[e.note],()=>(a=e.i,o.forEach((t,r)=>t.setAttribute(`aria-checked`,String(i(n,f)[r].i===e.i))),x()),e.i+1)),s=n.select?.copy!==void 0&&e(`button`,{type:`button`,class:`cmp-link`},`Copy command`);return s&&(s.disabled=!navigator.clipboard,s.onclick=()=>void navigator.clipboard.writeText(n.select.copy).then(()=>b(t.note,`ok`,`Copied to this device's clipboard.`),()=>b(t.note,`error`,`The browser refused the clipboard.`))),t.pick=()=>a===void 0?void 0:{choice:a},t.controls=o,[e(`pre`,{class:`ask-title`},r(n.select.title)),e(`div`,{class:`ask-opts`,role:`radiogroup`,"aria-label":`Choices`},...o),e(`div`,{class:`ask-acts`},t.send,s,t.go)]}function T(t,n){let r=n.questions.map(()=>({picks:new Set,text:e(`input`,{type:`text`,maxlength:`8192`,class:`ask-own`,placeholder:`Or type your own answer`,"aria-label":`Your own answer`}),note:e(`input`,{type:`text`,maxlength:`2048`,class:`ask-own ask-memo`,placeholder:`Note (optional)`,"aria-label":`Note`})})),i=n.questions.map((n,i)=>{let a=r[i],o=n.multiple?`checkbox`:`radio`,s=n.options.map((e,t)=>C(o,e.title,[e.description,e.preview&&`It has a preview; see it in the terminal.`],()=>{n.multiple||(a.picks.clear(),a.text.value=``),a.picks.has(t)?a.picks.delete(t):a.picks.add(t),s.forEach((e,t)=>e.setAttribute(`aria-checked`,String(a.picks.has(t)))),x()}));return a.text.oninput=()=>(n.multiple||(a.picks.clear(),s.forEach(e=>e.setAttribute(`aria-checked`,`false`))),x()),t.controls.push(...s,a.text,a.note),e(`div`,{class:`ask-one`},n.header&&e(`b`,{class:`ask-head`},n.header),e(`p`,{class:`ask-q`},n.question),e(`div`,{class:`ask-opts`,role:n.multiple?`group`:`radiogroup`,"aria-label":`Options`},...s),a.text,a.note)});return t.pick=()=>r.every(e=>e.picks.size||e.text.value.trim())?{answers:r.map(e=>a(e.picks,e.text.value,e.note.value))}:void 0,[...i,e(`div`,{class:`ask-acts`},t.send,t.go)]}function E(r){let i=r.asks??(r.ask?[r.ask]:[]),a=i.length>1,s=r.answer,c={key:u(r),el:e(`section`,{class:`ask`}),note:e(`p`,{class:`ask-note`,role:`status`,"aria-live":`polite`}),go:e(`button`,{type:`button`,class:`cmp-link`},`Focus terminal`),controls:[],pick:()=>void 0,done:!1,sending:!1};c.go.onclick=async()=>{v=!0,x();let e=await d(`focus`,{}).catch(()=>void 0);b(c.note,e?.ok?``:`error`,e?.ok?``:e?.body.reason??`Herdr could not be reached.`),v=!1,x()},s&&((c.send=e(`button`,{type:`button`,class:`ask-send`},`Send answer`)).onclick=()=>void(c.pick()&&S(c,o(s,c.pick(),crypto.randomUUID()))));let l=e(`p`,{class:`ask-term`},r.why&&i.length?r.why:`${a?`Answer them`:`Answer it`} in the terminal. ${a||!i[0]?``:t(i[0])}`.trim()),f=p&&r.tool!==`secret_drop`&&e(`button`,{type:`button`,class:`cmp-link`},`Show terminal`);return f&&(f.onclick=p),c.el.append(...s?s.select?w(c,s):T(c,s):[...i.length?i.map(e=>n(e,a)):[e(`p`,{class:`ask-why`},r.why??`It waits on a dialog.`)],e(`div`,{class:`ask-acts`},l,f,c.go)],c.note),c}let D=(e,t)=>{let n=document.activeElement;h&&h!==t&&(h.focus=h.el.contains(n)?n:void 0),[m,h,g]=[e,t,t?.send?t:g],s.replaceChildren(...t?[t.el]:[]),x(),t?.focus&&(!document.activeElement||document.activeElement===document.body)&&t.focus.focus()};return{update(e,t){_=t,e&&(y=void clearTimeout(y)),!e&&(h?.done||h?.sending)?y??=setTimeout(()=>(y=void 0,D()),6e3):e?h&&(u(e)===h.key||!e.answer&&(h.done||h.sending)&&e.callId===m?.callId)?((h.busy||h.sending)&&!e.answer?h.spell=!0:h.busy&&h.spell&&e.answer&&([h.done,h.busy]=[!1,!1],b(h.note,``,`The other answer was not taken; this one can go.`)),m=e):D(e,g?.key===u(e)&&!g.done?g:E(e)):D(),x()}}}export{d as t};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{l as e,n as t}from"./index-
|
|
1
|
+
import{l as e,n as t}from"./index-CWA0eWOk.js";import{n}from"./post-B39ywXKd.js";function r(){let e=!0,t=0;return{get pinned(){return e},scrolled(n){e=n,n&&(t=0)},hold:()=>void(e=!1),arrived(n){return e||(t+=n),e},jump:()=>void(e=!0,t=0),button:n=>e&&!n?void 0:t&&!n?`${t} new`:`jump to latest`}}async function i(e,t,n,r){let i=()=>e.items.some(e=>e.id===n);for(;!i()&&e.before;)r(e.prepend(await t.older(e.before)));return i()?!0:(r(e.reset(await t.latest())),!1)}var a=(e,t,n)=>({tag:e,kids:t,attrs:n}),o=/^(?:https?:\/\/|mailto:)[^\s\u0000-\u001f]+$/i,s=(e,t)=>a(`a`,t,{href:e,rel:`noopener noreferrer`,target:`_blank`}),c=/(`+)([^`]|[^`][^]{0,1000}?[^`])\1(?!`)|\*\*([^*][^]{0,1000}?)\*\*|~~([^~][^]{0,1000}?)~~|\*([^\s*][^*]{0,500}?)\*|(?<![\w])_([^\s_][^_]{0,500}?)_(?![\w])|\[([^\]]{1,500})\]\(([^)\s]{1,2000})(?:\s+"[^"]*")?\)|(https?:\/\/[^\s<>)\]]+)/g;function l(e){let t=[],n=0;for(let r of e.matchAll(c))r.index>n&&t.push(e.slice(n,r.index)),n=r.index+r[0].length,r[2]===void 0?r[3]===void 0?r[4]===void 0?r[5]!==void 0||r[6]!==void 0?t.push(a(`em`,l(r[5]??r[6]))):r[7]===void 0?t.push(s(r[9],[r[9]])):t.push(o.test(r[8])?s(r[8],l(r[7])):r[0]):t.push(a(`del`,l(r[4]))):t.push(a(`strong`,l(r[3]))):t.push(a(`code`,[r[2]]));return n<e.length&&t.push(e.slice(n)),t}var u=/^(\s*)([-*+]|\d+[.)])\s+(.*)$/,d=/^\s*([-*_])(\s*\1){2,}\s*$/,f=/^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/,p=e=>e.trim().replace(/^\|/,``).replace(/\|$/,``).split(`|`).map(e=>e.trim()),m=(e,t)=>/^\s*(`{3,}|~{3,}|#{1,6}\s|>)/.test(e)||d.test(e)||u.test(e)||e.includes(`|`)&&t!==void 0&&f.test(t)&&t.includes(`-`);function h(e,t=0){if(t>=16)return[a(`p`,[e])];let n=e.replace(/\r\n?/g,`
|
|
2
2
|
`).split(`
|
|
3
3
|
`),r=[];for(let e=0;e<n.length;){let i=n[e],o=n[e+1],s=i.match(/^\s*(`{3,}|~{3,})\s*([\w+-]*)/),c=i.match(/^\s*(#{1,6})\s+(.*?)\s*#*\s*$/),g=i.match(u);if(!i.trim())e++;else if(s){let t=[];for(e++;e<n.length&&!n[e].trim().startsWith(s[1]);e++)t.push(n[e]);e++,r.push(a(`pre`,[a(`code`,[t.join(`
|
|
4
4
|
`)],s[2]?{"data-lang":s[2]}:void 0)]))}else if(c)r.push(a(`h${c[1].length}`,l(c[2]))),e++;else if(d.test(i))r.push(a(`hr`,[])),e++;else if(/^\s*>/.test(i)){let i=[];for(;e<n.length&&/^\s*>/.test(n[e]);e++)i.push(n[e].replace(/^\s*> ?/,``));r.push(a(`blockquote`,h(i.join(`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
:root{--bg:#060a14;--panel:#0e1628e0;--line:#7a98e033;--text:#dbe6ff;--dim:#8b99ba;--gold:#ffd166;--grid:linear-gradient(#82a0ff0d 1px, transparent 1px) 0 0 / 44px 44px, linear-gradient(90deg, #82a0ff0d 1px, transparent 1px) 0 0 / 44px 44px;--mono:ui-monospace, "SF Mono", Menlo, Consolas, monospace;--sans:ui-rounded, "Avenir Next", "Trebuchet MS", system-ui, -apple-system, "Segoe UI", sans-serif}*{box-sizing:border-box;margin:0}html{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}body{min-height:100vh;color:var(--text);font:14px/1.4 var(--sans);background:radial-gradient(1100px 640px at 12% -10%, #38e0ff1c, transparent 62%), radial-gradient(900px 600px at 98% 8%, #966eff1c, transparent 60%), linear-gradient(var(--bg), #0a1020);background-attachment:fixed}svg{display:block}[data-mark=working]{--c:#38e0ff}[data-mark=waiting]{--c:#ffb02e}[data-mark=idle]{--c:#7184ad}[data-mark=done]{--c:#3ddc97}[data-mark=error]{--c:#ff5470}[data-mark=unknown]{--c:#4f5c7c}[data-mark]{--c1:color-mix(in srgb, var(--c) 13%, transparent);--c4:color-mix(in srgb, var(--c) 45%, transparent)}:focus-visible{outline-offset:2px;outline:2px solid #38e0ff}.shell{max-width:1480px;margin:0 auto;padding:18px 22px 40px}.hud{border:1px solid var(--line);background:var(--panel);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);z-index:5;border-radius:18px;align-items:center;gap:24px;padding:12px 18px;display:flex;position:sticky;top:12px}.brand{align-items:center;gap:12px;margin-right:auto;display:flex}.logo{color:#38e0ff;border:1px solid var(--line);background:radial-gradient(circle at 30% 25%,#38e0ff4d,#0a1020e6);border-radius:14px;place-items:center;width:44px;height:44px;display:grid}.logo svg{filter:drop-shadow(0 0 6px #38e0ff);width:28px;height:28px}.brand h1{letter-spacing:.14em;text-transform:uppercase;font-size:20px}.brand p{color:var(--dim);font-size:12px}.stats{gap:10px;display:flex}.stats div,.tally div,.alerts{background:#ffffff0a;border:1px solid #ffffff0f;border-radius:12px;min-width:78px;padding:6px 12px}.stats dt,.tally dt{color:var(--dim);letter-spacing:.08em;text-transform:uppercase;font-size:11px}.stats dd,.tally dd{font:600 20px/1.1 var(--mono)}.stats [data-mark] dd,.tally [data-mark] dd,.count b{color:var(--c)}.pair-btn{border:1px solid var(--line);color:var(--dim);font:600 12px var(--sans);letter-spacing:.08em;text-transform:uppercase;cursor:pointer;background:0 0;border-radius:12px;padding:6px 12px}.pair{text-align:center;color:var(--dim);justify-items:center;gap:12px;font-size:13px;display:grid}.qr{shape-rendering:crispedges;border-radius:12px;width:min(100%,300px)}.link{color:var(--dim);text-transform:uppercase;letter-spacing:.08em;align-items:center;gap:8px;font-size:12px;display:flex}.link i{background:#4f5c7c;border-radius:50%;width:9px;height:9px}.link[data-state=live] i{background:#3ddc97;animation:2.4s ease-in-out infinite blink}.link[data-state=live]{color:#3ddc97}.link[data-state=reconnecting] i,.link[data-state=connecting] i{background:#ffb02e;animation:1s ease-in-out infinite blink}.link[data-state=offline] i{background:#ff5470}.banner{color:#c9d6f5;background:#7184ad29;border-radius:14px;margin:14px 0 0;padding:10px 16px;font-size:13px}.banner[hidden]{display:none}.banner b{font-family:var(--mono);font-weight:600}.stage{grid-template-columns:minmax(0,1fr) 300px;align-items:start;gap:20px;margin-top:18px;transition:opacity .4s;display:grid}.stage[data-stale=true] .map,body[data-stale=true] .stats{opacity:.4}.stage[data-stale=true] *{animation-play-state:paused!important}.map{border:1px solid var(--line);background:var(--grid), #080d1a8c;border-radius:22px;min-height:420px;padding:26px;transition:opacity .4s;position:relative}.links{pointer-events:none;width:100%;height:100%;position:absolute;inset:0}.wire{fill:none;stroke:#38e0ff4d;stroke-width:1.5px;stroke-dasharray:3 7;stroke-linecap:round}.packet{fill:#38e0ff;opacity:.9}.bases{grid-template-columns:repeat(auto-fill,minmax(min(100%,300px),1fr));align-items:start;gap:30px;display:grid;position:relative}.empty{color:var(--dim);text-align:center;padding:120px 0;position:relative}.base{border:1px solid var(--line);background:linear-gradient(#16213c,#0c1324);border-radius:18px;padding:16px;position:relative;box-shadow:0 18px 40px #00000059}.base.head{background:linear-gradient(#ffd1661a,#0000 38%),linear-gradient(#16213c,#0c1324)}.base[data-mark=waiting]{box-shadow:0 18px 40px #00000059,inset 0 0 0 999px #ffb02e0b}.base>header{align-items:center;gap:14px;margin-bottom:14px;display:flex}.title{justify-items:start;min-width:0;margin-right:auto;display:grid}.title h2{white-space:nowrap;text-overflow:ellipsis;max-width:100%;font-size:17px;overflow:hidden}.title p{max-width:100%;color:var(--dim);white-space:nowrap;text-overflow:ellipsis;font-size:12px;overflow:hidden}.crown{letter-spacing:.12em;text-transform:uppercase;color:var(--gold);background:#ffd1661f;border-radius:999px;align-items:center;gap:5px;margin-bottom:3px;padding:2px 9px 2px 6px;font-size:10px;display:inline-flex}.crown svg{width:14px;height:14px}.base:not(.head) .crown{display:none}.tower{width:80px;height:80px;color:var(--c);flex:none;place-items:center;display:grid;position:relative}.tower:before{content:"";background:radial-gradient(circle at 35% 30%, color-mix(in srgb, var(--c) 24%, #101a2e), #0b1222 70%);border:1px solid color-mix(in srgb, var(--c) 45%, transparent);border-radius:50%;position:absolute;inset:0}.tower svg{width:48px;height:48px;filter:drop-shadow(0 0 6px color-mix(in srgb, var(--c) 70%, transparent));position:relative}.tower.ghost{opacity:.45}.tower.ghost:before{border-style:dashed}.sweep,.pulse{opacity:0;pointer-events:none;border-radius:50%;position:absolute;inset:0}.sweep{background:conic-gradient(from 0deg, transparent 0 70%, color-mix(in srgb, var(--c) 55%, transparent) 100%)}.pulse{border:2px solid var(--c)}.tower[data-mark=working] .sweep{opacity:1;animation:3.2s linear infinite spin}.tower[data-mark=waiting] .pulse{animation:1.5s ease-out infinite ping}.tower>.badge{--s:40px;position:absolute;bottom:-2px;right:-2px}.tabs{gap:12px;display:grid}.tab{background:#ffffff06;border:1px solid #ffffff0d;border-radius:14px;padding:10px}.tab h3{color:var(--dim);letter-spacing:.12em;text-transform:uppercase;margin-bottom:8px;font-size:11px;font-weight:600}.units,.subs{gap:8px;display:grid}.outposts{gap:8px;margin-top:12px;display:grid}.outposts:empty{display:none}.outposts:before{content:"Outposts";color:var(--dim);letter-spacing:.12em;text-transform:uppercase;font-size:11px}.card{background:#ffffff09;border:1px solid #ffffff0f;border-radius:12px;align-items:center;gap:10px;padding:8px 10px;transition:background-color .4s;display:flex;position:relative}.unit[data-mark=waiting]>.card{background:#ffb02e1c}.unit[data-mark=error]>.card{background:#ff54701c}.unit[data-mark=working]>.card{background:#38e0ff12}.plate{width:var(--s,40px);height:var(--s,40px);color:var(--c);background:radial-gradient(circle at 35% 30%, color-mix(in srgb, var(--c) 26%, #101a2e), #0b1222 72%);border-radius:50%;flex:none;place-items:center;transition:color .4s;display:grid;position:relative}.plate svg{width:calc(var(--s,40px) * .55);height:calc(var(--s,40px) * .55)}.ring,.plate:after{pointer-events:none;border-radius:50%;position:absolute;inset:-3px}.ring{opacity:0;background:conic-gradient(from 0deg, transparent 0 55%, var(--c));-webkit-mask:radial-gradient(farthest-side,#0000 calc(100% - 3px),#000 calc(100% - 2px));mask:radial-gradient(farthest-side,#0000 calc(100% - 3px),#000 calc(100% - 2px))}.plate[data-mark=working]>.ring{opacity:1;animation:1.1s linear infinite spin}.plate:after{content:"";border:2px solid var(--c);opacity:0}.plate[data-mark=waiting]:after{animation:1.5s ease-out infinite ping}.badge{width:clamp(16px, calc(var(--s,40px) * .3), 24px);height:clamp(16px, calc(var(--s,40px) * .3), 24px);font:700 10px/1 var(--mono);color:#06101e;background:var(--c);border-radius:50%;flex:none;place-items:center;transition:background-color .4s;display:grid}.plate>.badge{position:absolute;bottom:-4px;right:-4px}[data-mark=waiting]>.badge:after{content:"?"}[data-mark=idle]>.badge:after{content:"z"}[data-mark=done]>.badge:after{content:"✓"}[data-mark=error]>.badge:after{content:"!"}[data-mark=unknown]>.badge:after{content:"·"}[data-mark=working]>.badge:after{content:"";border:2px solid #06101e;border-right-color:#0000;border-radius:50%;width:8px;height:8px;animation:.8s linear infinite spin}[data-mark=waiting]>.badge{animation:1.2s ease-in-out infinite hop}.meta{min-width:0;display:grid}.name{white-space:nowrap;text-overflow:ellipsis;font-weight:600;overflow:hidden}.line{color:var(--dim);align-items:baseline;gap:8px;min-width:0;font-size:12px;display:flex}.role{letter-spacing:.1em;text-transform:uppercase;color:color-mix(in srgb, var(--c) 70%, var(--dim));font-size:10px;font-style:normal}.note{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.unit[data-mark=idle]>.card{opacity:.85}.subs{margin:6px 0 0 22px;padding-left:18px;position:relative}.subs:empty{display:none}.subs:before{content:"";background:repeating-linear-gradient(#7a98e080 0 4px,#0000 4px 8px);width:1px;position:absolute;top:-4px;bottom:22px;left:3px}.subs>.unit>.card:before{content:"";background:repeating-linear-gradient(90deg,#7a98e080 0 4px,#0000 4px 8px);width:15px;height:1px;position:absolute;top:50%;left:-15px}.subs>.unit{position:relative}.subs .plate{--s:30px}.subs .name{font-size:13px}.side{gap:16px;display:grid;position:sticky;top:96px}.panel{border:1px solid var(--line);background:var(--panel);border-radius:18px;padding:14px 16px}.panel h2{color:var(--dim);letter-spacing:.14em;text-transform:uppercase;margin-bottom:10px;font-size:11px}.net-log summary{cursor:pointer;justify-content:space-between;align-items:center;list-style:none;display:flex}.net-log summary::-webkit-details-marker{display:none}.net-log summary:after{content:"▾";color:var(--dim);transition:transform .2s}.net-log:not([open]) summary:after{transform:rotate(-90deg)}.net-log:not([open]) h2{margin-bottom:0}.log{gap:7px;max-height:360px;padding:0;list-style:none;display:grid;overflow:hidden}.log:empty:before{content:"Waiting for changes…";color:var(--dim);font-size:12px}.log li{grid-template-columns:auto 1fr;gap:2px 8px;font-size:12px;animation:.4s ease-out both slide;display:grid}.log li:before{content:"";background:var(--c);border-radius:50%;width:8px;height:8px;margin-top:5px}.log time{font:11px var(--mono);color:var(--dim);grid-column:2}.credit{color:var(--dim);padding:0 6px;font-size:11px}.legend{gap:8px;padding:0;font-size:12px;list-style:none;display:grid}.legend li{align-items:center;gap:9px;display:flex}.enter{animation:.5s cubic-bezier(.2,.9,.3,1.15) both pop}.leaving{pointer-events:none;animation:.35s ease-in both out}.flash>.card>.plate,.flash.tower{animation:.6s ease-out flash}@keyframes spin{to{transform:rotate(360deg)}}@keyframes ping{0%{opacity:.6;transform:scale(1)}to{opacity:0;transform:scale(1.7)}}@keyframes hop{50%{transform:translateY(-3px)}}@keyframes blink{50%{opacity:.35}}@keyframes pop{0%{opacity:0;transform:scale(.92)translateY(8px)}}@keyframes out{to{opacity:0;transform:scale(.94)}}@keyframes flash{40%{transform:scale(1.12)}}@keyframes slide{0%{opacity:0;transform:translate(10px)}}@keyframes zoom{0%{opacity:0;transform:scale(.45)}}[hidden]{display:none!important}a{color:inherit}.crumbs,.tally dt,.comms-head small,.tx.you>header,.notice em,.rule summary,.dossier-in>.tag,.d-drones h4,.specs dt,.stop,.dialog .tag,.intel h4,.pending b{color:var(--dim);letter-spacing:.14em;text-transform:uppercase;font-size:11px;font-style:normal}.token .name,.drone .note,.count small,.sum,.where,.tile b{white-space:nowrap;text-overflow:ellipsis;max-width:100%;overflow:hidden}.md a,.bubble a,.notice a,.d-note a,.specs a,.crumbs a{color:#38e0ff}.crumbs{gap:10px;margin:14px 6px 0;display:flex}.crumbs>*+:before{content:"›";opacity:.5;margin-right:10px}.crumbs a,.brand{text-decoration:none}.work{align-items:flex-start;gap:20px;min-width:0;display:flex}.view{flex:1 1 0;min-width:0}body[data-stale=true] .work{opacity:.45}.enter-zoom{transform-origin:var(--ox,50%) var(--oy,0);animation:.5s ease-out both zoom}.enter-slide,.dossier{animation:.4s ease-out both slide}.enter-back{animation:.4s ease-out both pop}.hit,.enter-base:after{z-index:1;border-radius:inherit;position:absolute;inset:0}.enter-base:after{content:"";z-index:0}.hit:hover{box-shadow:inset 0 0 0 1px var(--c)}.base>header{position:relative}.enter-base{text-decoration:none}.base>header:hover h2{color:#38e0ff}.alerts{width:230px;color:inherit;font:inherit;text-align:left;cursor:pointer;align-items:center;gap:10px;display:flex;position:relative}.alerts:disabled{cursor:default;opacity:.6}.alerts:not(:disabled){border-color:var(--c4);background:var(--c1)}.faces{flex:none;padding-left:10px;display:flex}.faces .plate{--s:28px;margin-left:-10px;box-shadow:0 0 0 2px #0e1628}.faces .badge{display:none}.count{min-width:0;display:grid}.count b{font:600 18px/1.1 var(--mono)}.count small,.comms-head p,.cmp-note,.where,.queued,.d-line,.who p{color:var(--dim);font-size:12px}.token{border:1px solid #0000;border-radius:16px;justify-items:center;gap:3px;width:100%;padding:10px 8px 9px;text-decoration:none;display:grid;position:relative}a.token:hover{border-color:var(--c4)}.token[data-mark=waiting]{background:#ffb02e1c}.token[data-mark=error]{background:#ff54701c}.token .plate{--s:64px;margin-bottom:4px}.token .name{font-weight:600}.token .line{justify-items:center;max-width:100%;display:grid}.drone{width:94px;padding:6px;font-size:12px}.drone .plate{--s:44px}.drone .role{display:none}.flash.token>.plate{animation:.6s ease-out flash}.zoom-in,.comms-in{border:1px solid var(--line);background:var(--grid), #080d1a8c;border-radius:22px;position:relative}.zoom-in{min-height:420px;padding:24px}.zoom-in.void>:not(.lost){display:none}.lost,.chat-empty{color:var(--dim);text-align:center;padding:60px 0}.keep{align-items:center;gap:20px;padding:18px 22px;display:flex}.keep h2{font-size:24px}.tower.big{width:120px;height:120px}.tower.big svg{width:72px;height:72px}.tower.big>.badge{--s:96px;bottom:4px;right:4px}a.tower:hover:before{border-color:var(--c)}.tally{gap:10px;display:flex}.squads,.camps{grid-template-columns:repeat(auto-fit,minmax(min(100%,430px),1fr));align-items:start;gap:18px;margin-top:20px;display:grid}.squad{background:#0c1324c7;padding:14px 16px 18px}.squad[data-mark=waiting]{background:#262121d9}.squad h3{align-items:center;gap:10px;margin-bottom:14px;display:flex}.squad h3 b,.crumbs b{color:var(--text)}.squad h3 small{margin-left:auto}.squad h3 svg{width:18px;height:18px}.flag{background:var(--c);width:10px;height:10px;box-shadow:0 0 8px var(--c);border-radius:2px;transform:rotate(45deg)}.formation{flex-wrap:wrap;align-items:flex-start;gap:16px 20px;display:flex}.col{justify-items:center;gap:8px;width:196px;display:grid}.say{width:100%;color:var(--dim);background:#111a2f;border-radius:12px;margin-top:4px;padding:7px 10px;font-size:12px;line-height:1.35}.say p{overflow-wrap:anywhere;max-height:4.05em;overflow:hidden}.drones{flex-wrap:wrap;justify-content:center;gap:8px;width:100%;padding-top:16px;display:flex;position:relative}.drones:empty{display:none}.drones:before{content:"";background:repeating-linear-gradient(#7a98e099 0 4px,#0000 4px 8px);width:1px;height:14px;position:absolute;top:0;left:50%}.camp .token{width:156px}[data-group=outside] .plate{filter:saturate(.3);outline-offset:4px;outline:1px dashed #8b99ba8c}.comms-in,.tile{height:calc(var(--vvh,100vh) - 190px);grid-template-rows:auto minmax(0,1fr) auto;min-height:380px;display:grid;overflow:hidden}.comms-head,.tile>header{background:var(--panel);align-items:center;gap:12px;padding:12px 18px;display:flex}.ava{color:#38e0ff;background:radial-gradient(circle at 35% 30%,#38e0ff47,#0b1222 72%);border-radius:50%;flex:none;place-items:center;width:30px;height:30px;display:grid}.ava svg{width:58%;height:58%}.comms-head h2{font-size:16px}.chan{margin-left:auto}.chat-root{min-height:0;display:grid;position:relative}.comms-in .ask{max-height:calc((var(--vvh,100vh) - 190px) * .55);overscroll-behavior:contain;overflow-y:auto}.chat-scroll{overflow-anchor:none;padding:16px 20px;overflow-y:auto}.chat-list{gap:14px;display:grid}.chat-top{height:1px}.chat-live{color:var(--c);align-items:center;gap:10px;margin-top:14px;padding-left:44px;font-size:12px;display:flex}.chat-pill{--c:#38e0ff;border:0;position:absolute;bottom:14px;left:50%;transform:translate(-50%)}.lit:after{content:"";background:#38e0ff2e;border-radius:14px;animation:1s 1s forwards out;position:absolute;inset:0}.composer{border-top:1px solid var(--line);background:var(--panel);gap:8px;padding:12px 18px 14px;display:grid}.cmp-row,.cmp-acts,.cmp-foot,.cmp-note{align-items:center;gap:8px;display:flex}.cmp-row{align-items:flex-end;gap:10px}.composer textarea{resize:none;border:1px solid var(--line);min-width:0;color:var(--text);font:14px/1.4 var(--sans);background:#070c18;border-radius:12px;flex:1;padding:9px 12px}.composer textarea:focus{border-color:#38e0ff80;outline:none;box-shadow:0 0 0 3px #38e0ff1f}.composer button:disabled{opacity:.45;cursor:default}.cmp-note{flex-wrap:wrap;flex:1;min-height:18px}.cmp-note[data-tone=error],.cmp-alt.cmp-stop{color:#ff8a9c}.cmp-note[data-tone=ok]{color:#3ddc97}.cmp-note[data-tone=ask]{color:var(--gold)}.cmp-link{color:#38e0ff;font:12px var(--sans);cursor:pointer;background:0 0;border:0;padding:0}.cmp-yes{background:var(--gold);color:#06101e;font:600 12px var(--sans);cursor:pointer;border:0;border-radius:8px;padding:3px 10px}.tx{content-visibility:auto;contain-intrinsic-size:auto 90px;min-width:0}.tx time,.tx.note{color:var(--dim);font:11px var(--mono)}.tx.you{justify-items:end;gap:5px;display:grid}.tx.you>header{color:var(--gold);align-items:center;gap:8px;display:flex}.tx.you .ava{color:var(--gold)}.bubble{white-space:pre-wrap;overflow-wrap:anywhere;background:#ffd16614;border:1px solid #ffd16638;border-radius:14px;max-width:80%;padding:9px 13px}.tx.in{grid-template-columns:34px minmax(0,1fr);align-items:start;gap:10px;display:grid}.tx-body{border:1px solid var(--line);background:#0e1628d9;border-radius:14px;gap:8px;padding:10px 14px;display:grid}.tx-body,.tx-body>*{min-width:0}.tx-body>header{color:var(--dim);font:11px var(--mono);align-items:baseline;gap:10px;display:flex}.tx-body>header b{color:#38e0ff;font:600 13px var(--sans)}.stop,.dialog .tag{background:var(--c1);color:var(--c);border-radius:999px;padding:1px 8px;font-size:10px}.fault{color:#ff8a9c;font-size:13px}.thought,.more,.tab-btn{border:1px solid var(--line);color:var(--dim);font:11px var(--sans);cursor:pointer;background:0 0;border-radius:999px;justify-self:start;padding:2px 10px}.thought-text:empty{display:none}.thought-text,.out,.args,.notice pre,.rule pre{white-space:pre-wrap;overflow-wrap:anywhere;color:var(--dim);font:12px/1.4 var(--mono)}.act,.notice{background:#ffffff08;border:1px solid #ffffff12;border-radius:10px}.act>summary,.notice>summary,.rule summary{cursor:pointer;align-items:center;gap:9px;min-width:0;padding:6px 10px;font-size:12.5px;list-style:none;display:flex}summary::-webkit-details-marker{display:none}.act code{color:var(--c);font:600 11px var(--mono);letter-spacing:.06em;text-transform:uppercase}.sum{min-width:0;color:var(--dim)}.act[open] .sum{white-space:normal}.open{flex:none;margin-left:auto}.act>pre,.result{margin:0 10px 8px}.result{gap:6px;display:grid}.tx.sys{padding-left:44px}.notice{background:#966eff12;border-color:#966eff33}.notice .ava{width:22px;height:22px}.notice pre,.rule pre{padding:0 10px 8px}.rule summary{justify-content:center}.rule summary:before,.rule summary:after{content:"";background:var(--line);flex:1;height:1px}.tx.note{text-align:center}.dossier-in{text-align:center;justify-items:center;gap:6px;display:grid}.dossier-in>.tag{text-align:left;justify-self:stretch}.portrait{background:radial-gradient(var(--c1), transparent 70%);border-radius:50%;padding:8px}.portrait .plate{--s:96px}.dossier-in h3{overflow-wrap:anywhere;margin-top:4px;font-size:18px}.d-line,.who p{align-items:baseline;gap:8px;display:flex}.d-note{background:#ffb02e1f;border-radius:10px;padding:8px 10px;font-size:12px}.specs{text-align:left;justify-self:stretch;gap:2px;margin-top:10px;display:grid}.specs div{background:#ffffff09;grid-template-columns:76px minmax(0,1fr);gap:8px;padding:7px 10px;display:grid}.specs dt{font-size:10px}.specs dd{overflow-wrap:anywhere;font-size:12.5px}.d-drones{margin-top:8px}.d-drones .formation{justify-content:center;gap:6px}.md{overflow-wrap:anywhere;line-height:1.5}.md p,.md ul,.md ol,.md pre,.md table,.md blockquote{margin:0 0 10px}.md>:last-child{margin-bottom:0}.md :is(h1,h2,h3,h4,h5,h6){margin:14px 0 6px;font-size:15px}.md>:first-child{margin-top:0}.md h1{font-size:19px}.md ul,.md ol{padding-left:22px}.md li>p{margin:0}.md li.task{margin-left:-18px;list-style:none}.check{vertical-align:-2px;text-align:center;width:14px;height:14px;font:700 10px/12px var(--mono);border:1px solid;border-radius:4px;margin-right:7px;display:inline-block}.check[data-done=true]{color:#06101e;background:#3ddc97}.md code{font:12.5px var(--mono);background:#ffffff14;border-radius:5px;padding:1px 5px}.md pre{border:1px solid var(--line);background:#070c18;border-radius:10px;padding:10px 12px;overflow-x:auto}.md pre code{background:0 0;padding:0}.md blockquote{color:var(--dim);background:#ffffff0a;padding:6px 12px}.md table{border-collapse:collapse;display:block;overflow-x:auto}.md th,.md td{border:1px solid var(--line);padding:4px 10px}.tiles{flex:1 1 0;gap:20px;min-width:0;display:flex}.tile-tabs{display:none}.tile{border:1px solid var(--line);background:var(--panel);border-radius:22px;flex:1 1 0;min-width:0;position:sticky;top:96px}.tile>header svg{width:22px;height:22px;color:var(--gold)}.tile-body{padding:16px 20px;overflow:auto}.tiles.tabbed{flex-direction:column;gap:8px}.tiles.tabbed .tile-tabs{gap:6px;display:flex}.tiles.tabbed .tile:not(.active){display:none}.tab-btn.active,.open{color:var(--gold);border-color:#ffd16666}.dock{z-index:20;pointer-events:none;width:min(300px,100vw - 44px);display:grid;position:fixed;top:108px;right:max(22px,50vw - 718px)}.dialog{pointer-events:auto;border:1px solid var(--c4);background:linear-gradient(var(--c1), #0c1324 70%), #121a30;transform-origin:top;border-radius:18px;grid-area:1/1;padding:14px 16px 16px;transition:transform .3s,opacity .3s;position:relative;box-shadow:0 24px 60px #000c}.dialog:focus{outline:none}.dialog{overscroll-behavior:contain;max-height:calc(100dvh - 128px);overflow-y:auto}.dialog>header{align-items:center;gap:8px;min-width:0;margin-bottom:12px;display:flex}.where{min-width:0}.x{color:var(--dim);cursor:pointer;background:0 0;border:0;margin-left:auto;font-size:20px}.who{align-items:center;gap:14px;display:flex}.who .plate{--s:56px}.who h3{font-size:17px}.intel{background:#060a148c;border-radius:12px;margin-top:12px;padding:10px 12px}.intel h4{margin-bottom:4px}.said{overflow-wrap:anywhere;font-size:13px;line-height:1.45}.pending{color:var(--c);font:12px var(--mono);overflow-wrap:anywhere;margin-top:8px}.dialog footer{flex-wrap:wrap;align-items:center;gap:10px;margin-top:14px;display:flex}.ask{background:#ffb02e0f;border:1px solid #ffb02e59;border-radius:14px;gap:8px;margin:10px 18px 0;padding:12px 14px;display:grid}.dialog .ask{margin:10px 0 0}.ask-one{gap:8px;display:grid}.ask-one+.ask-one{margin-top:8px}.ask-head{color:var(--gold);letter-spacing:.06em;text-transform:uppercase;background:#ffb02e24;border-radius:999px;justify-self:start;padding:1px 9px;font-size:11px}.ask-how{color:var(--dim);font-size:12px}.ask-q{white-space:pre-wrap;overflow-wrap:anywhere;font-weight:600}.ask-ctx,.ask-why{color:var(--dim);white-space:pre-wrap;overflow-wrap:anywhere;max-height:9em;font-size:13px;overflow:auto}.ask-opts{counter-reset:opt;gap:6px;max-height:16em;margin:0;padding:0;list-style:none;display:grid;overflow:auto}.ask-opt{border:1px solid var(--line);overflow-wrap:anywhere;counter-increment:opt;background:#070c18;border-radius:10px;gap:2px;padding:7px 12px;font-size:13px;display:grid}.ask-opt b:before{content:counter(opt) ". ";color:var(--dim)}.ask-opt b[data-n]:before{content:attr(data-n) ". "}.ask-opt small{color:var(--dim);font-size:12px}.ask-acts{flex-wrap:wrap;align-items:center;gap:6px 14px;display:flex}.ask-acts>:last-child{margin-left:auto}.ask-term{color:var(--gold);font-size:12px}.ask button:disabled{opacity:.45;cursor:default}.ask-note:empty{display:none}.ask-note{color:var(--dim);flex-wrap:wrap;align-items:center;gap:4px 10px;font-size:12px;display:flex}.ask-note[data-tone=error]{color:#ff8a9c}.ask-note[data-tone=ok]{color:#3ddc97}.ask-title{font:12.5px/1.45 var(--mono);white-space:pre-wrap;overflow-wrap:anywhere;max-height:12em;margin:0;overflow:auto}div.ask-opts{max-height:none}button.ask-opt{text-align:left;color:var(--text);font:13px var(--sans);cursor:pointer}.ask-opt[aria-checked=true]{background:#ffb02e1f;border-color:#ffb02e}.ask-own{border:1px solid var(--line);min-width:0;color:var(--text);font:13px var(--sans);background:#070c18;border-radius:10px;padding:7px 12px}.ask-memo{font-size:12px}.ask input:disabled{opacity:.45}.ask-send{color:#06101e;font:600 13px var(--sans);cursor:pointer;background:#ffb02e;border:0;border-radius:10px;padding:7px 13px}.go,.later,.chat-pill,.cmp-send,.cmp-alt{font:600 13px var(--sans);cursor:pointer;border-radius:10px;padding:7px 13px;text-decoration:none}.go,.chat-pill{background:var(--c);color:#06101e}.cmp-send{color:#06101e;background:#38e0ff;border:0}.later,.cmp-alt{border:1px solid var(--line);color:var(--text);background:0 0}.queued{color:var(--dim);flex:1 0 100%;font-size:12px}.queued:empty{display:none}html.paused *,html.paused :before,html.paused :after{animation-play-state:paused!important}@media (width<=1100px){.stage{grid-template-columns:1fr}.side{position:static}.stats{display:none}}@media (width<=900px){.work{flex-direction:column;align-items:stretch}.view,.tiles{flex:none}.tile{position:static}.keep{flex-wrap:wrap}}@media (width<=640px){.shell{padding:max(8px, env(safe-area-inset-top)) max(8px, env(safe-area-inset-right)) calc(24px + env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left))}.hud{flex-wrap:wrap;gap:10px;padding:10px 12px;position:static}.brand p,.link span{display:none}.alerts{flex:100%;order:3;width:auto}.map,.zoom-in{padding:14px}.bases{gap:18px}.keep{gap:12px;padding:6px}.tower.big{width:84px;height:84px}.tally{flex-wrap:wrap}.col{width:100%}.comms-in,.tile{height:calc(var(--vvh,100dvh) - 16px);border-radius:16px;min-height:300px}.chat-scroll{padding:12px}.bubble{max-width:92%}.composer{padding:10px 12px calc(10px + env(safe-area-inset-bottom))}.cmp-row{flex-wrap:wrap}.composer textarea{flex:100%;min-height:64px}.cmp-acts{margin-left:auto}.log{max-height:none}.log li:nth-child(n+3){display:none}.dock{top:auto;right:10px;bottom:calc(10px + env(safe-area-inset-bottom));width:calc(100vw - 20px)}}@media (pointer:coarse){.composer textarea,.ask input{font-size:16px}.composer button,.go,.later,.x,.chat-pill,.pair-btn,.alerts,.ask button{min-height:40px}}@media (prefers-reduced-motion:reduce){*,:before,:after{transition:none!important;animation:none!important}}
|
|
1
|
+
:root{--bg:#060a14;--panel:#0e1628e0;--line:#7a98e033;--text:#dbe6ff;--dim:#8b99ba;--gold:#ffd166;--grid:linear-gradient(#82a0ff0d 1px, transparent 1px) 0 0 / 44px 44px, linear-gradient(90deg, #82a0ff0d 1px, transparent 1px) 0 0 / 44px 44px;--mono:ui-monospace, "SF Mono", Menlo, Consolas, monospace;--sans:ui-rounded, "Avenir Next", "Trebuchet MS", system-ui, -apple-system, "Segoe UI", sans-serif}*{box-sizing:border-box;margin:0}html{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}body{min-height:100vh;color:var(--text);font:14px/1.4 var(--sans);background:radial-gradient(1100px 640px at 12% -10%, #38e0ff1c, transparent 62%), radial-gradient(900px 600px at 98% 8%, #966eff1c, transparent 60%), linear-gradient(var(--bg), #0a1020);background-attachment:fixed}svg{display:block}[data-mark=working]{--c:#38e0ff}[data-mark=waiting]{--c:#ffb02e}[data-mark=idle]{--c:#7184ad}[data-mark=done]{--c:#3ddc97}[data-mark=error]{--c:#ff5470}[data-mark=unknown]{--c:#4f5c7c}[data-mark]{--c1:color-mix(in srgb, var(--c) 13%, transparent);--c4:color-mix(in srgb, var(--c) 45%, transparent)}:focus-visible{outline-offset:2px;outline:2px solid #38e0ff}.shell{max-width:1480px;margin:0 auto;padding:18px 22px 40px}.hud{border:1px solid var(--line);background:var(--panel);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);z-index:5;border-radius:18px;align-items:center;gap:24px;padding:12px 18px;display:flex;position:sticky;top:12px}.brand{align-items:center;gap:12px;margin-right:auto;display:flex}.logo{color:#38e0ff;border:1px solid var(--line);background:radial-gradient(circle at 30% 25%,#38e0ff4d,#0a1020e6);border-radius:14px;place-items:center;width:44px;height:44px;display:grid}.logo svg{filter:drop-shadow(0 0 6px #38e0ff);width:28px;height:28px}.brand h1{letter-spacing:.14em;text-transform:uppercase;font-size:20px}.brand p{color:var(--dim);font-size:12px}.stats{gap:10px;display:flex}.stats div,.tally div,.alerts{background:#ffffff0a;border:1px solid #ffffff0f;border-radius:12px;min-width:78px;padding:6px 12px}.stats dt,.tally dt{color:var(--dim);letter-spacing:.08em;text-transform:uppercase;font-size:11px}.stats dd,.tally dd{font:600 20px/1.1 var(--mono)}.stats [data-mark] dd,.tally [data-mark] dd,.count b{color:var(--c)}.pair-btn{border:1px solid var(--line);color:var(--dim);font:600 12px var(--sans);letter-spacing:.08em;text-transform:uppercase;cursor:pointer;background:0 0;border-radius:12px;padding:6px 12px}.pair{text-align:center;color:var(--dim);justify-items:center;gap:12px;font-size:13px;display:grid}.qr{shape-rendering:crispedges;border-radius:12px;width:min(100%,300px)}.link{color:var(--dim);text-transform:uppercase;letter-spacing:.08em;align-items:center;gap:8px;font-size:12px;display:flex}.link i{background:#4f5c7c;border-radius:50%;width:9px;height:9px}.link[data-state=live] i{background:#3ddc97;animation:2.4s ease-in-out infinite blink}.link[data-state=live]{color:#3ddc97}.link[data-state=reconnecting] i,.link[data-state=connecting] i{background:#ffb02e;animation:1s ease-in-out infinite blink}.link[data-state=offline] i{background:#ff5470}.banner{color:#c9d6f5;background:#7184ad29;border-radius:14px;margin:14px 0 0;padding:10px 16px;font-size:13px}.banner[hidden]{display:none}.banner b{font-family:var(--mono);font-weight:600}.stage{grid-template-columns:minmax(0,1fr) 300px;align-items:start;gap:20px;margin-top:18px;transition:opacity .4s;display:grid}.stage[data-stale=true] .map,body[data-stale=true] .stats{opacity:.4}.stage[data-stale=true] *{animation-play-state:paused!important}.map{border:1px solid var(--line);background:var(--grid), #080d1a8c;border-radius:22px;min-height:420px;padding:26px;transition:opacity .4s;position:relative}.links{pointer-events:none;width:100%;height:100%;position:absolute;inset:0}.wire{fill:none;stroke:#38e0ff4d;stroke-width:1.5px;stroke-dasharray:3 7;stroke-linecap:round}.packet{fill:#38e0ff;opacity:.9}.bases{grid-template-columns:repeat(auto-fill,minmax(min(100%,300px),1fr));align-items:start;gap:30px;display:grid;position:relative}.empty{color:var(--dim);text-align:center;padding:120px 0;position:relative}.base{border:1px solid var(--line);background:linear-gradient(#16213c,#0c1324);border-radius:18px;padding:16px;position:relative;box-shadow:0 18px 40px #00000059}.base.head{background:linear-gradient(#ffd1661a,#0000 38%),linear-gradient(#16213c,#0c1324)}.base[data-mark=waiting]{box-shadow:0 18px 40px #00000059,inset 0 0 0 999px #ffb02e0b}.base>header{align-items:center;gap:14px;margin-bottom:14px;display:flex}.title{justify-items:start;min-width:0;margin-right:auto;display:grid}.title h2{white-space:nowrap;text-overflow:ellipsis;max-width:100%;font-size:17px;overflow:hidden}.title p{max-width:100%;color:var(--dim);white-space:nowrap;text-overflow:ellipsis;font-size:12px;overflow:hidden}.crown{letter-spacing:.12em;text-transform:uppercase;color:var(--gold);background:#ffd1661f;border-radius:999px;align-items:center;gap:5px;margin-bottom:3px;padding:2px 9px 2px 6px;font-size:10px;display:inline-flex}.crown svg{width:14px;height:14px}.base:not(.head) .crown{display:none}.tower{width:80px;height:80px;color:var(--c);flex:none;place-items:center;display:grid;position:relative}.tower:before{content:"";background:radial-gradient(circle at 35% 30%, color-mix(in srgb, var(--c) 24%, #101a2e), #0b1222 70%);border:1px solid color-mix(in srgb, var(--c) 45%, transparent);border-radius:50%;position:absolute;inset:0}.tower svg{width:48px;height:48px;filter:drop-shadow(0 0 6px color-mix(in srgb, var(--c) 70%, transparent));position:relative}.tower.ghost{opacity:.45}.tower.ghost:before{border-style:dashed}.sweep,.pulse{opacity:0;pointer-events:none;border-radius:50%;position:absolute;inset:0}.sweep{background:conic-gradient(from 0deg, transparent 0 70%, color-mix(in srgb, var(--c) 55%, transparent) 100%)}.pulse{border:2px solid var(--c)}.tower[data-mark=working] .sweep{opacity:1;animation:3.2s linear infinite spin}.tower[data-mark=waiting] .pulse{animation:1.5s ease-out infinite ping}.tower>.badge{--s:40px;position:absolute;bottom:-2px;right:-2px}.tabs{gap:12px;display:grid}.tab{background:#ffffff06;border:1px solid #ffffff0d;border-radius:14px;padding:10px}.tab h3{color:var(--dim);letter-spacing:.12em;text-transform:uppercase;margin-bottom:8px;font-size:11px;font-weight:600}.units,.subs{gap:8px;display:grid}.outposts{gap:8px;margin-top:12px;display:grid}.outposts:empty{display:none}.outposts:before{content:"Outposts";color:var(--dim);letter-spacing:.12em;text-transform:uppercase;font-size:11px}.card{background:#ffffff09;border:1px solid #ffffff0f;border-radius:12px;align-items:center;gap:10px;padding:8px 10px;transition:background-color .4s;display:flex;position:relative}.unit[data-mark=waiting]>.card{background:#ffb02e1c}.unit[data-mark=error]>.card{background:#ff54701c}.unit[data-mark=working]>.card{background:#38e0ff12}.plate{width:var(--s,40px);height:var(--s,40px);color:var(--c);background:radial-gradient(circle at 35% 30%, color-mix(in srgb, var(--c) 26%, #101a2e), #0b1222 72%);border-radius:50%;flex:none;place-items:center;transition:color .4s;display:grid;position:relative}.plate svg{width:calc(var(--s,40px) * .55);height:calc(var(--s,40px) * .55)}.ring,.plate:after{pointer-events:none;border-radius:50%;position:absolute;inset:-3px}.ring{opacity:0;background:conic-gradient(from 0deg, transparent 0 55%, var(--c));-webkit-mask:radial-gradient(farthest-side,#0000 calc(100% - 3px),#000 calc(100% - 2px));mask:radial-gradient(farthest-side,#0000 calc(100% - 3px),#000 calc(100% - 2px))}.plate[data-mark=working]>.ring{opacity:1;animation:1.1s linear infinite spin}.plate:after{content:"";border:2px solid var(--c);opacity:0}.plate[data-mark=waiting]:after{animation:1.5s ease-out infinite ping}.badge{width:clamp(16px, calc(var(--s,40px) * .3), 24px);height:clamp(16px, calc(var(--s,40px) * .3), 24px);font:700 10px/1 var(--mono);color:#06101e;background:var(--c);border-radius:50%;flex:none;place-items:center;transition:background-color .4s;display:grid}.plate>.badge{position:absolute;bottom:-4px;right:-4px}[data-mark=waiting]>.badge:after{content:"?"}[data-mark=idle]>.badge:after{content:"z"}[data-mark=done]>.badge:after{content:"✓"}[data-mark=error]>.badge:after{content:"!"}[data-mark=unknown]>.badge:after{content:"·"}[data-mark=working]>.badge:after{content:"";border:2px solid #06101e;border-right-color:#0000;border-radius:50%;width:8px;height:8px;animation:.8s linear infinite spin}[data-mark=waiting]>.badge{animation:1.2s ease-in-out infinite hop}.meta{min-width:0;display:grid}.name{white-space:nowrap;text-overflow:ellipsis;font-weight:600;overflow:hidden}.line{color:var(--dim);align-items:baseline;gap:8px;min-width:0;font-size:12px;display:flex}.role{letter-spacing:.1em;text-transform:uppercase;color:color-mix(in srgb, var(--c) 70%, var(--dim));font-size:10px;font-style:normal}.note{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.unit[data-mark=idle]>.card{opacity:.85}.subs{margin:6px 0 0 22px;padding-left:18px;position:relative}.subs:empty{display:none}.subs:before{content:"";background:repeating-linear-gradient(#7a98e080 0 4px,#0000 4px 8px);width:1px;position:absolute;top:-4px;bottom:22px;left:3px}.subs>.unit>.card:before{content:"";background:repeating-linear-gradient(90deg,#7a98e080 0 4px,#0000 4px 8px);width:15px;height:1px;position:absolute;top:50%;left:-15px}.subs>.unit{position:relative}.subs .plate{--s:30px}.subs .name{font-size:13px}.side{gap:16px;display:grid;position:sticky;top:96px}.panel{border:1px solid var(--line);background:var(--panel);border-radius:18px;padding:14px 16px}.panel h2{color:var(--dim);letter-spacing:.14em;text-transform:uppercase;margin-bottom:10px;font-size:11px}.net-log summary{cursor:pointer;justify-content:space-between;align-items:center;list-style:none;display:flex}.net-log summary::-webkit-details-marker{display:none}.net-log summary:after{content:"▾";color:var(--dim);transition:transform .2s}.net-log:not([open]) summary:after{transform:rotate(-90deg)}.net-log:not([open]) h2{margin-bottom:0}.log{gap:7px;max-height:360px;padding:0;list-style:none;display:grid;overflow:hidden}.log:empty:before{content:"Waiting for changes…";color:var(--dim);font-size:12px}.log li{grid-template-columns:auto 1fr;gap:2px 8px;font-size:12px;animation:.4s ease-out both slide;display:grid}.log li:before{content:"";background:var(--c);border-radius:50%;width:8px;height:8px;margin-top:5px}.log time{font:11px var(--mono);color:var(--dim);grid-column:2}.credit{color:var(--dim);padding:0 6px;font-size:11px}.legend{gap:8px;padding:0;font-size:12px;list-style:none;display:grid}.legend li{align-items:center;gap:9px;display:flex}.enter{animation:.5s cubic-bezier(.2,.9,.3,1.15) both pop}.leaving{pointer-events:none;animation:.35s ease-in both out}.flash>.card>.plate,.flash.tower{animation:.6s ease-out flash}@keyframes spin{to{transform:rotate(360deg)}}@keyframes ping{0%{opacity:.6;transform:scale(1)}to{opacity:0;transform:scale(1.7)}}@keyframes hop{50%{transform:translateY(-3px)}}@keyframes blink{50%{opacity:.35}}@keyframes pop{0%{opacity:0;transform:scale(.92)translateY(8px)}}@keyframes out{to{opacity:0;transform:scale(.94)}}@keyframes flash{40%{transform:scale(1.12)}}@keyframes slide{0%{opacity:0;transform:translate(10px)}}@keyframes zoom{0%{opacity:0;transform:scale(.45)}}[hidden]{display:none!important}a{color:inherit}.crumbs,.tally dt,.comms-head small,.tx.you>header,.notice em,.rule summary,.dossier-in>.tag,.d-drones h4,.specs dt,.stop,.dialog .tag,.intel h4,.pending b{color:var(--dim);letter-spacing:.14em;text-transform:uppercase;font-size:11px;font-style:normal}.token .name,.drone .note,.count small,.sum,.where,.tile b{white-space:nowrap;text-overflow:ellipsis;max-width:100%;overflow:hidden}.md a,.bubble a,.notice a,.d-note a,.specs a,.crumbs a{color:#38e0ff}.crumbs{gap:10px;margin:14px 6px 0;display:flex}.crumbs>*+:before{content:"›";opacity:.5;margin-right:10px}.crumbs a,.brand{text-decoration:none}.work{align-items:flex-start;gap:20px;min-width:0;display:flex}.view{flex:1 1 0;min-width:0}body[data-stale=true] .work{opacity:.45}.enter-zoom{transform-origin:var(--ox,50%) var(--oy,0);animation:.5s ease-out both zoom}.enter-slide,.dossier{animation:.4s ease-out both slide}.enter-back{animation:.4s ease-out both pop}.hit,.enter-base:after{z-index:1;border-radius:inherit;position:absolute;inset:0}.enter-base:after{content:"";z-index:0}.hit:hover{box-shadow:inset 0 0 0 1px var(--c)}.base>header{position:relative}.enter-base{text-decoration:none}.base>header:hover h2{color:#38e0ff}.alerts{width:230px;color:inherit;font:inherit;text-align:left;cursor:pointer;align-items:center;gap:10px;display:flex;position:relative}.alerts:disabled{cursor:default;opacity:.6}.alerts:not(:disabled){border-color:var(--c4);background:var(--c1)}.faces{flex:none;padding-left:10px;display:flex}.faces .plate{--s:28px;margin-left:-10px;box-shadow:0 0 0 2px #0e1628}.faces .badge{display:none}.count{min-width:0;display:grid}.count b{font:600 18px/1.1 var(--mono)}.count small,.comms-head p,.cmp-note,.where,.queued,.d-line,.who p{color:var(--dim);font-size:12px}.token{border:1px solid #0000;border-radius:16px;justify-items:center;gap:3px;width:100%;padding:10px 8px 9px;text-decoration:none;display:grid;position:relative}a.token:hover{border-color:var(--c4)}.token[data-mark=waiting]{background:#ffb02e1c}.token[data-mark=error]{background:#ff54701c}.token .plate{--s:64px;margin-bottom:4px}.token .name{font-weight:600}.token .line{justify-items:center;max-width:100%;display:grid}.drone{width:94px;padding:6px;font-size:12px}.drone .plate{--s:44px}.drone .role{display:none}.flash.token>.plate{animation:.6s ease-out flash}.zoom-in,.comms-in{border:1px solid var(--line);background:var(--grid), #080d1a8c;border-radius:22px;position:relative}.zoom-in{min-height:420px;padding:24px}.zoom-in.void>:not(.lost){display:none}.lost,.chat-empty{color:var(--dim);text-align:center;padding:60px 0}.keep{align-items:center;gap:20px;padding:18px 22px;display:flex}.keep h2{font-size:24px}.tower.big{width:120px;height:120px}.tower.big svg{width:72px;height:72px}.tower.big>.badge{--s:96px;bottom:4px;right:4px}a.tower:hover:before{border-color:var(--c)}.tally{gap:10px;display:flex}.squads,.camps{grid-template-columns:repeat(auto-fit,minmax(min(100%,430px),1fr));align-items:start;gap:18px;margin-top:20px;display:grid}.squad{background:#0c1324c7;padding:14px 16px 18px}.squad[data-mark=waiting]{background:#262121d9}.squad h3{align-items:center;gap:10px;margin-bottom:14px;display:flex}.squad h3 b,.crumbs b{color:var(--text)}.squad h3 small{margin-left:auto}.squad h3 svg{width:18px;height:18px}.flag{background:var(--c);width:10px;height:10px;box-shadow:0 0 8px var(--c);border-radius:2px;transform:rotate(45deg)}.formation{flex-wrap:wrap;align-items:flex-start;gap:16px 20px;display:flex}.col{justify-items:center;gap:8px;width:196px;display:grid}.say{width:100%;color:var(--dim);background:#111a2f;border-radius:12px;margin-top:4px;padding:7px 10px;font-size:12px;line-height:1.35}.say p{overflow-wrap:anywhere;max-height:4.05em;overflow:hidden}.drones{flex-wrap:wrap;justify-content:center;gap:8px;width:100%;padding-top:16px;display:flex;position:relative}.drones:empty{display:none}.drones:before{content:"";background:repeating-linear-gradient(#7a98e099 0 4px,#0000 4px 8px);width:1px;height:14px;position:absolute;top:0;left:50%}.camp .token{width:156px}[data-group=outside] .plate{filter:saturate(.3);outline-offset:4px;outline:1px dashed #8b99ba8c}.comms-in,.tile{height:calc(var(--vvh,100vh) - 190px);grid-template-rows:auto minmax(0,1fr) auto;min-height:380px;display:grid;overflow:hidden}.comms-head,.tile>header{background:var(--panel);align-items:center;gap:12px;padding:12px 18px;display:flex}.ava{color:#38e0ff;background:radial-gradient(circle at 35% 30%,#38e0ff47,#0b1222 72%);border-radius:50%;flex:none;place-items:center;width:30px;height:30px;display:grid}.ava svg{width:58%;height:58%}.comms-head h2{font-size:16px}.chan{margin-left:auto}.chat-root{min-height:0;display:grid;position:relative}.comms-in .ask{max-height:calc((var(--vvh,100vh) - 190px) * .55);overscroll-behavior:contain;overflow-y:auto}.chat-scroll{overflow-anchor:none;padding:16px 20px;overflow-y:auto}.chat-list{gap:14px;display:grid}.chat-top{height:1px}.chat-live{color:var(--c);align-items:center;gap:10px;margin-top:14px;padding-left:44px;font-size:12px;display:flex}.chat-pill{--c:#38e0ff;border:0;position:absolute;bottom:14px;left:50%;transform:translate(-50%)}.lit:after{content:"";background:#38e0ff2e;border-radius:14px;animation:1s 1s forwards out;position:absolute;inset:0}.composer{border-top:1px solid var(--line);background:var(--panel);gap:8px;padding:12px 18px 14px;display:grid}.cmp-row,.cmp-acts,.cmp-foot,.cmp-note{align-items:center;gap:8px;display:flex}.cmp-row{align-items:flex-end;gap:10px}.composer textarea{resize:none;border:1px solid var(--line);min-width:0;color:var(--text);font:14px/1.4 var(--sans);background:#070c18;border-radius:12px;flex:1;padding:9px 12px}.composer textarea:focus{border-color:#38e0ff80;outline:none;box-shadow:0 0 0 3px #38e0ff1f}.composer button:disabled{opacity:.45;cursor:default}.cmp-note{flex-wrap:wrap;flex:1;min-height:18px}.cmp-note[data-tone=error],.cmp-alt.cmp-stop{color:#ff8a9c}.cmp-note[data-tone=ok]{color:#3ddc97}.cmp-note[data-tone=ask]{color:var(--gold)}.cmp-link{color:#38e0ff;font:12px var(--sans);cursor:pointer;background:0 0;border:0;padding:0}.cmp-yes{background:var(--gold);color:#06101e;font:600 12px var(--sans);cursor:pointer;border:0;border-radius:8px;padding:3px 10px}.tx{content-visibility:auto;contain-intrinsic-size:auto 90px;min-width:0}.tx time,.tx.note{color:var(--dim);font:11px var(--mono)}.tx.you{justify-items:end;gap:5px;display:grid}.tx.you>header{color:var(--gold);align-items:center;gap:8px;display:flex}.tx.you .ava{color:var(--gold)}.bubble{white-space:pre-wrap;overflow-wrap:anywhere;background:#ffd16614;border:1px solid #ffd16638;border-radius:14px;max-width:80%;padding:9px 13px}.tx.in{grid-template-columns:34px minmax(0,1fr);align-items:start;gap:10px;display:grid}.tx-body{border:1px solid var(--line);background:#0e1628d9;border-radius:14px;gap:8px;padding:10px 14px;display:grid}.tx-body,.tx-body>*{min-width:0}.tx-body>header{color:var(--dim);font:11px var(--mono);align-items:baseline;gap:10px;display:flex}.tx-body>header b{color:#38e0ff;font:600 13px var(--sans)}.stop,.dialog .tag{background:var(--c1);color:var(--c);border-radius:999px;padding:1px 8px;font-size:10px}.fault{color:#ff8a9c;font-size:13px}.thought,.more,.tab-btn{border:1px solid var(--line);color:var(--dim);font:11px var(--sans);cursor:pointer;background:0 0;border-radius:999px;justify-self:start;padding:2px 10px}.thought-text:empty{display:none}.thought-text,.out,.args,.notice pre,.rule pre{white-space:pre-wrap;overflow-wrap:anywhere;color:var(--dim);font:12px/1.4 var(--mono)}.act,.notice{background:#ffffff08;border:1px solid #ffffff12;border-radius:10px}.act>summary,.notice>summary,.rule summary{cursor:pointer;align-items:center;gap:9px;min-width:0;padding:6px 10px;font-size:12.5px;list-style:none;display:flex}summary::-webkit-details-marker{display:none}.act code{color:var(--c);font:600 11px var(--mono);letter-spacing:.06em;text-transform:uppercase}.sum{min-width:0;color:var(--dim)}.act[open] .sum{white-space:normal}.open{flex:none;margin-left:auto}.act>pre,.result{margin:0 10px 8px}.result{gap:6px;display:grid}.tx.sys{padding-left:44px}.notice{background:#966eff12;border-color:#966eff33}.notice .ava{width:22px;height:22px}.notice pre,.rule pre{padding:0 10px 8px}.rule summary{justify-content:center}.rule summary:before,.rule summary:after{content:"";background:var(--line);flex:1;height:1px}.tx.note{text-align:center}.dossier-in{text-align:center;justify-items:center;gap:6px;display:grid}.dossier-in>.tag{text-align:left;justify-self:stretch}.portrait{background:radial-gradient(var(--c1), transparent 70%);border-radius:50%;padding:8px}.portrait .plate{--s:96px}.dossier-in h3{overflow-wrap:anywhere;margin-top:4px;font-size:18px}.d-line,.who p{align-items:baseline;gap:8px;display:flex}.d-note{background:#ffb02e1f;border-radius:10px;padding:8px 10px;font-size:12px}.specs{text-align:left;justify-self:stretch;gap:2px;margin-top:10px;display:grid}.specs div{background:#ffffff09;grid-template-columns:76px minmax(0,1fr);gap:8px;padding:7px 10px;display:grid}.specs dt{font-size:10px}.specs dd{overflow-wrap:anywhere;font-size:12.5px}.d-drones{margin-top:8px}.d-drones .formation{justify-content:center;gap:6px}.md{overflow-wrap:anywhere;line-height:1.5}.md p,.md ul,.md ol,.md pre,.md table,.md blockquote{margin:0 0 10px}.md>:last-child{margin-bottom:0}.md :is(h1,h2,h3,h4,h5,h6){margin:14px 0 6px;font-size:15px}.md>:first-child{margin-top:0}.md h1{font-size:19px}.md ul,.md ol{padding-left:22px}.md li>p{margin:0}.md li.task{margin-left:-18px;list-style:none}.check{vertical-align:-2px;text-align:center;width:14px;height:14px;font:700 10px/12px var(--mono);border:1px solid;border-radius:4px;margin-right:7px;display:inline-block}.check[data-done=true]{color:#06101e;background:#3ddc97}.md code{font:12.5px var(--mono);background:#ffffff14;border-radius:5px;padding:1px 5px}.md pre{border:1px solid var(--line);background:#070c18;border-radius:10px;padding:10px 12px;overflow-x:auto}.md pre code{background:0 0;padding:0}.md blockquote{color:var(--dim);background:#ffffff0a;padding:6px 12px}.md table{border-collapse:collapse;display:block;overflow-x:auto}.md th,.md td{border:1px solid var(--line);padding:4px 10px}.tiles{flex:1 1 0;gap:20px;min-width:0;display:flex}.tile-tabs{display:none}.tile{border:1px solid var(--line);background:var(--panel);border-radius:22px;flex:1 1 0;min-width:0;position:sticky;top:96px}.tile>header svg{width:22px;height:22px;color:var(--gold)}.tile{grid-template-columns:minmax(0,1fr)}.tile>header b{flex:1;min-width:0}.tile-body{padding:16px 20px;overflow:auto}.tiles.tabbed{flex-direction:column;gap:8px}.tiles.tabbed .tile-tabs{gap:6px;display:flex}.tiles.tabbed .tile:not(.active){display:none}.tab-btn.active,.open{color:var(--gold);border-color:#ffd16666}.dock{z-index:20;pointer-events:none;width:min(300px,100vw - 44px);display:grid;position:fixed;top:108px;right:max(22px,50vw - 718px)}.dialog{pointer-events:auto;border:1px solid var(--c4);background:linear-gradient(var(--c1), #0c1324 70%), #121a30;transform-origin:top;border-radius:18px;grid-area:1/1;padding:14px 16px 16px;transition:transform .3s,opacity .3s;position:relative;box-shadow:0 24px 60px #000c}.dialog:focus{outline:none}.dialog{overscroll-behavior:contain;max-height:calc(100dvh - 128px);overflow-y:auto}.dialog>header{align-items:center;gap:8px;min-width:0;margin-bottom:12px;display:flex}.where{min-width:0}.x{color:var(--dim);cursor:pointer;background:0 0;border:0;margin-left:auto;font-size:20px}.who{align-items:center;gap:14px;display:flex}.who .plate{--s:56px}.who h3{font-size:17px}.intel{background:#060a148c;border-radius:12px;margin-top:12px;padding:10px 12px}.intel h4{margin-bottom:4px}.said{overflow-wrap:anywhere;font-size:13px;line-height:1.45}.pending{color:var(--c);font:12px var(--mono);overflow-wrap:anywhere;margin-top:8px}.dialog footer{flex-wrap:wrap;align-items:center;gap:10px;margin-top:14px;display:flex}.ask{background:#ffb02e0f;border:1px solid #ffb02e59;border-radius:14px;gap:8px;margin:10px 18px 0;padding:12px 14px;display:grid}.dialog .ask{margin:10px 0 0}.ask-one{gap:8px;display:grid}.ask-one+.ask-one{margin-top:8px}.ask-head{color:var(--gold);letter-spacing:.06em;text-transform:uppercase;background:#ffb02e24;border-radius:999px;justify-self:start;padding:1px 9px;font-size:11px}.ask-how{color:var(--dim);font-size:12px}.ask-q{white-space:pre-wrap;overflow-wrap:anywhere;font-weight:600}.ask-ctx,.ask-why{color:var(--dim);white-space:pre-wrap;overflow-wrap:anywhere;max-height:9em;font-size:13px;overflow:auto}.ask-opts{counter-reset:opt;gap:6px;max-height:16em;margin:0;padding:0;list-style:none;display:grid;overflow:auto}.ask-opt{border:1px solid var(--line);overflow-wrap:anywhere;counter-increment:opt;background:#070c18;border-radius:10px;gap:2px;padding:7px 12px;font-size:13px;display:grid}.ask-opt b:before{content:counter(opt) ". ";color:var(--dim)}.ask-opt b[data-n]:before{content:attr(data-n) ". "}.ask-opt small{color:var(--dim);font-size:12px}.ask-acts{flex-wrap:wrap;align-items:center;gap:6px 14px;display:flex}.ask-acts>:last-child{margin-left:auto}.ask-term{color:var(--gold);font-size:12px}.ask button:disabled{opacity:.45;cursor:default}.ask-note:empty{display:none}.ask-note{color:var(--dim);flex-wrap:wrap;align-items:center;gap:4px 10px;font-size:12px;display:flex}.ask-note[data-tone=error]{color:#ff8a9c}.ask-note[data-tone=ok]{color:#3ddc97}.ask-title{font:12.5px/1.45 var(--mono);white-space:pre-wrap;overflow-wrap:anywhere;max-height:12em;margin:0;overflow:auto}div.ask-opts{max-height:none}button.ask-opt{text-align:left;color:var(--text);font:13px var(--sans);cursor:pointer}.ask-opt[aria-checked=true]{background:#ffb02e1f;border-color:#ffb02e}.ask-own{border:1px solid var(--line);min-width:0;color:var(--text);font:13px var(--sans);background:#070c18;border-radius:10px;padding:7px 12px}.ask-memo{font-size:12px}.ask input:disabled{opacity:.45}.ask-send{color:#06101e;font:600 13px var(--sans);cursor:pointer;background:#ffb02e;border:0;border-radius:10px;padding:7px 13px}.go,.later,.chat-pill,.cmp-send,.cmp-alt{font:600 13px var(--sans);cursor:pointer;border-radius:10px;padding:7px 13px;text-decoration:none}.go,.chat-pill{background:var(--c);color:#06101e}.cmp-send{color:#06101e;background:#38e0ff;border:0}.later,.cmp-alt{border:1px solid var(--line);color:var(--text);background:0 0}.queued{color:var(--dim);flex:1 0 100%;font-size:12px}.queued:empty{display:none}html.paused *,html.paused :before,html.paused :after{animation-play-state:paused!important}@media (width<=1100px){.stage{grid-template-columns:1fr}.side{position:static}.stats{display:none}}.term-tile{grid-template-rows:auto minmax(0,1fr) auto;gap:10px;height:100%;display:grid}.tile-body:has(.term-tile){padding:12px 14px;overflow:hidden}.term-bar{justify-content:space-between;align-items:center;gap:12px;font-size:12px;display:flex}.term-state{color:var(--dim)}.term-state[data-tone=ok]{color:#3ddc97}.term-state[data-tone=error]{color:#ff8a9c}.term-scroll{overscroll-behavior:contain;border:1px solid var(--line);background:#090d18;border-radius:14px;padding:8px 10px;overflow:auto}.term{color:#cdd6f4;width:max-content;min-width:100%;font:12px/1.3 var(--mono);white-space:pre;transition:opacity .2s}.term .tr{min-height:1.3em}.term.stale{opacity:.5}.term.hidden{display:none}.d-term{margin-top:12px;font-size:13px}.pad{gap:6px;display:grid}.pad-row{grid-template-columns:repeat(auto-fit,minmax(48px,1fr));gap:6px;display:grid}.pad-key{border:1px solid var(--line);min-height:34px;color:var(--text);font:600 12px var(--mono);cursor:pointer;background:#0b1224;border-bottom-width:3px;border-radius:9px;padding:4px 6px}.pad-key:active:not(:disabled){background:#13203e;border-bottom-width:1px;transform:translateY(2px)}.pad-key:disabled{opacity:.35;cursor:default}.pad-key[data-key=enter]{color:#38e0ff}.pad-key[data-key=esc]{color:var(--gold)}.pad-key:is([data-key=up],[data-key=down],[data-key=left],[data-key=right],[data-key=backspace]){font-size:16px}.pad-digits summary{color:var(--dim);cursor:pointer;padding:2px 0;font-size:12px}.pad-note:empty{display:none}.pad-note{color:var(--dim);flex-wrap:wrap;align-items:center;gap:6px 12px;margin:0;font-size:12px;display:flex}.pad-note[data-tone=error]{color:#ff8a9c}.pad-note[data-tone=ask]{color:var(--text);background:#ffb02e14;border:1px solid #ffb02e66;border-radius:10px;padding:8px 10px}@media (width<=640px){.pad-key,.pad-yes{min-height:44px}.pad-digits summary{padding:14px 0}}.pad-yes{color:#06101e;font:600 12px var(--sans);cursor:pointer;background:#ffb02e;border:0;border-radius:9px;padding:5px 11px}.c0{color:#45475a}.cz{color:#090d18}.c1{color:#f38ba8}.c2{color:#a6e3a1}.c3{color:#f9e2af}.c4{color:#89b4fa}.c5{color:#f5c2e7}.c6{color:#94e2d5}.c7{color:#bac2de}.c8{color:#585b70}.c9{color:#f37799}.c10{color:#89d88b}.c11{color:#ebd391}.c12{color:#74a8fc}.c13{color:#f2aede}.c14{color:#6bd7ca}.c15{color:#a6adc8}.k0{background:#090d18}.k1{background:#f38ba8}.k2{background:#a6e3a1}.k3{background:#f9e2af}.k4{background:#89b4fa}.k5{background:#f5c2e7}.k6{background:#94e2d5}.k7,.kz{background:#bac2de}.k8{background:#585b70}.k9{background:#f37799}.k10{background:#89d88b}.k11{background:#ebd391}.k12{background:#74a8fc}.k13{background:#f2aede}.k14{background:#6bd7ca}.k15{background:#a6adc8}.tb{font-weight:700}.td{opacity:.6}.ti{font-style:italic}.tu{text-decoration:underline}.ts{text-decoration:line-through}.tu.ts{text-decoration:underline line-through}@media (width<=900px){.work{flex-direction:column;align-items:stretch}.view,.tiles{flex:none}.tile{position:static}.keep{flex-wrap:wrap}}@media (width<=640px){.shell{padding:max(8px, env(safe-area-inset-top)) max(8px, env(safe-area-inset-right)) calc(24px + env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left))}.hud{flex-wrap:wrap;gap:10px;padding:10px 12px;position:static}.brand p,.link span{display:none}.alerts{flex:100%;order:3;width:auto}.map,.zoom-in{padding:14px}.bases{gap:18px}.keep{gap:12px;padding:6px}.tower.big{width:84px;height:84px}.tally{flex-wrap:wrap}.col{width:100%}.comms-in,.tile{height:calc(var(--vvh,100dvh) - 16px);border-radius:16px;min-height:300px}.chat-scroll{padding:12px}.bubble{max-width:92%}.composer{padding:10px 12px calc(10px + env(safe-area-inset-bottom))}.cmp-row{flex-wrap:wrap}.composer textarea{flex:100%;min-height:64px}.cmp-acts{margin-left:auto}.log{max-height:none}.log li:nth-child(n+3){display:none}.dock{top:auto;right:10px;bottom:calc(10px + env(safe-area-inset-bottom));width:calc(100vw - 20px)}}@media (pointer:coarse){.composer textarea,.ask input{font-size:16px}.composer button,.go,.later,.x,.chat-pill,.pair-btn,.alerts,.ask button,.term-bar button,.d-term{min-height:40px}.pad-key,.pad-yes{min-height:44px}.pad-digits summary{padding:14px 0}}@media (prefers-reduced-motion:reduce){*,:before,:after{transition:none!important;animation:none!important}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var e={working:`started working`,waiting:`is waiting on a question`,idle:`went idle`,done:`finished`,error:`hit an error`,unknown:`lost its status`};function t(e){let t=new Map;for(let n of e.bases){n.tower&&t.set(`tower:${n.id}`,{name:`${n.label} tower`,mark:n.tower.mark});for(let e of n.tabs.flatMap(e=>e.units))for(let n of[e,...e.subs])t.set(n.id,{name:n.name,mark:n.mark});for(let e of n.outposts)t.set(e.id,{name:e.label,mark:e.mark})}return t}function n(n,r){if(!n)return[];let[i,a]=[t(n),t(r)],o=[];for(let[t,n]of a){let r=i.get(t);r?r.mark!==n.mark&&o.push({text:`${n.name} ${e[n.mark]}`,mark:n.mark}):o.push({text:`${n.name} joined`,mark:n.mark})}for(let[e,t]of i)a.has(e)||o.push({text:`${t.name} left`,mark:`idle`});return o}var r=e=>e===`waiting`||e===`error`?0:1;function i(e,t){let n=new Map;for(let r of e.bases)for(let e of[...r.tower?[{...r.tower,role:`tower`}]:[],...[...r.tabs.flatMap(e=>e.units),...r.outside].flatMap(e=>[e,...e.subs])])n.set(e.key,{key:e.key,ws:r.id,name:e.name,role:e.role,mark:e.mark,note:e.note,at:t,pending:e.pending});return n}function a(e,t){return[...t.values()].filter(t=>e?.get(t.key)?.mark!==t.mark&&(r(t.mark)===0||e!==void 0&&t.mark===`done`))}var o=e=>{try{return decodeURIComponent(e)}catch{return e}},s=e=>/^\.+$/.test(e)?e.replaceAll(`.`,`%2E`):encodeURIComponent(e);function c(e){let[t,n]=e.split(`#`)[0].split(`?`),[r,i,a,s]=t.replace(/^\/+|\/+$/g,``).split(`/`).map(o);if(r!==`w`||!i)return{view:`map`};if(a!==`s`||!s)return{view:`workspace`,ws:i};let c=new URLSearchParams(n).get(`at`);return c?{view:`session`,ws:i,key:s,at:c}:{view:`session`,ws:i,key:s}}var l=e=>e.view===`map`?``:e.view===`workspace`?`w/${s(e.ws)}`:`w/${s(e.ws)}/s/${s(e.key)}${e.at?`?at=${encodeURIComponent(e.at)}`:``}`,u=e=>e.view===`session`?{view:`workspace`,ws:e.ws}:{view:`map`},d=e=>e.view===`session`?e.key:e.view===`workspace`?`w:${e.ws}`:null;function ee(e,t=new URL(document.baseURI).pathname){let n=()=>c(location.pathname.slice(t.length)+location.search),r={route:n,href:e=>l(e)||`./`,go(n){let r=t+l(n);location.pathname+location.search!==r&&history.pushState(null,``,r),e(n)}};return addEventListener(`popstate`,()=>e(n())),document.addEventListener(`click`,e=>{let t=e.target?.closest?.(`a[data-route]`);!t||e.defaultPrevented||e.button!==0||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||(e.preventDefault(),r.go(c(t.getAttribute(`data-route`))))}),r}var f={main:`main`,member:`member`,subagent:`subagent`,outpost:`outpost`,outside:`outside`,tower:`tower`},p={working:`working`,waiting:`waiting on a question`,idle:`idle`,done:`finished`,error:`error`,unknown:`no status`},m=e=>e===`waiting`||e===`error`,h=e=>{let t=new DOMParser().parseFromString(e.trim(),`text/html`).body.firstElementChild;if(!(t instanceof HTMLElement))throw Error(`bad template`);return document.importNode(t,!0)},g=(e,t)=>{let n=document.getElementById(e);if(!(n instanceof t))throw Error(`#${e} is missing from the page`);return n},_=(e,t,n)=>{let r=e.querySelector(t);r.textContent!==n&&(r.textContent=n)},v=(e,t)=>{if(e.dataset.mark===t)return;let n=e.dataset.mark!==void 0;e.dataset.mark=t,n&&(e.classList.remove(`flash`),e.offsetWidth,e.classList.add(`flash`))},y=(e,t)=>e?e.replace(/^tool:/,`using `):p[t],b=(e,t)=>{e.toggleAttribute(`hidden`,!t),t&&(e.setAttribute(`href`,l(t)||`./`),e.setAttribute(`data-route`,l(t)))},x=`<div class="plate"><i class="ring"></i><svg><use href="#i-main"/></svg><b class="badge"></b></div>`;function S(e,t,n){v(e,n);let r=e.querySelector(`use`),i=`#i-${f[t]??`member`}`;r.getAttribute(`href`)!==i&&r.setAttribute(`href`,i)}function C(e,t,n,r){let i=new Map;for(let t of e.children)!t.classList.contains(`leaving`)&&t.dataset.key&&i.set(t.dataset.key,t);let a=e.firstElementChild;for(let o of t){let t=i.get(o.id);for(t||(t=n(o),t.dataset.key=o.id,t.classList.add(`enter`),setTimeout(()=>t.classList.remove(`enter`),600)),i.delete(o.id);a?.classList.contains(`leaving`);)a=a.nextElementSibling;t===a?a=a.nextElementSibling:e.insertBefore(t,a),r(t,o)}for(let e of i.values())e.classList.add(`leaving`),setTimeout(()=>e.remove(),400)}var w=()=>h(`<div class="unit"><div class="card"><a class="hit"></a>${x}<div class="meta"><span class="name"></span><span class="line"><em class="role"></em><span class="note"></span></span></div></div><div class="subs"></div></div>`);function T(e,t){let n=e.closest(`.base`)?.dataset.key,r=e.querySelector(`.hit`);b(r,t.key&&n?{view:`session`,ws:n,key:t.key}:void 0),r.setAttribute(`aria-label`,`${t.name}: ${y(t.note,t.mark)}`),r.toggleAttribute(`data-alert`,m(t.mark)),v(e,t.mark),S(e.querySelector(`.plate`),t.role,t.mark),_(e,`.name`,t.name),_(e,`.role`,t.role),_(e,`.note`,y(t.note,t.mark)),C(e.querySelector(`.subs`),t.subs??[],w,T)}var te=()=>h(`<section class="base"><header><div class="tower"><i class="sweep"></i><i class="pulse"></i><svg><use href="#i-tower"/></svg><b class="badge"></b></div><div class="title"><span class="crown"><svg><use href="#i-crown"/></svg>head tower</span><h2><a class="enter-base"></a></h2><p></p></div></header><div class="tabs"></div><div class="outposts"></div></section>`);function ne(e,t){v(e,t.mark),e.classList.toggle(`head`,t.head);let n=e.querySelector(`.tower`);n.classList.toggle(`ghost`,!t.tower),v(n,t.tower?.mark??`unknown`),_(e,`h2 a`,t.label),b(e.querySelector(`h2 a`),{view:`workspace`,ws:t.id}),_(e,`.title p`,t.tower?`control tower · ${p[t.tower.mark]}`:`no tower yet`),C(e.querySelector(`.tabs`),t.tabs,()=>h(`<div class="tab"><h3></h3><div class="units"></div></div>`),(e,t)=>{_(e,`h3`,`tab ${t.label}`),C(e.querySelector(`.units`),t.units,w,T)}),C(e.querySelector(`.outposts`),t.outposts.map(e=>({id:e.id,name:e.label,role:`outpost`,mark:e.mark,note:e.mark===`unknown`?`no agent running`:void 0})),w,T)}function re(e,t,n){let r=[...e.querySelectorAll(`.base:not(.leaving) .tower`)],i=e.querySelector(`.base.head:not(.leaving) .tower`),a=e.getBoundingClientRect(),o=e=>{let t=e.getBoundingClientRect();return[Math.round(t.x+t.width/2-a.x),Math.round(t.y+t.height/2-a.y)]},s=i?r.filter(e=>e!==i).map(e=>{let[[t,n],[r,a]]=[o(i),o(e)],s=Math.round((t+r)/2);return`M${t} ${n}C${s} ${n} ${s} ${a} ${r} ${a}`}):[],c=s.join(`|`);if(t.dataset.sig===c)return;t.dataset.sig=c;let l=(e,t,...n)=>{let r=document.createElementNS(`http://www.w3.org/2000/svg`,e);for(let[e,n]of Object.entries(t))r.setAttribute(e,n);return r.append(...n),r};t.replaceChildren(...s.flatMap(e=>[l(`path`,{class:`wire`,d:e}),...n?[]:[l(`circle`,{class:`packet`,r:`3`},l(`animateMotion`,{dur:`5s`,repeatCount:`indefinite`,path:e}))]]))}function ie(){let[e,t,n,r]=[g(`map`,HTMLElement),g(`bases`,HTMLElement),g(`links`,SVGSVGElement),g(`log`,HTMLElement)],i=matchMedia(`(prefers-reduced-motion: reduce)`).matches,a=()=>!e.hidden&&re(e,n,i);return new ResizeObserver(a).observe(e),{layout:a,stats(e){let t=e.bases.flatMap(e=>e.tabs.flatMap(e=>e.units.flatMap(e=>[e,...e.subs]))),n=e=>String(t.filter(t=>!e||t.mark===e).length);for(let[t,r]of[[`s-bases`,String(e.bases.length)],[`s-agents`,n()],[`s-working`,n(`working`)],[`s-waiting`,n(`waiting`)]])g(t,HTMLElement).textContent=r},show(e){C(t,e.bases,te,ne),g(`empty`,HTMLElement).hidden=e.bases.length>0,requestAnimationFrame(a)},log(e){let t=new Date().toLocaleTimeString([],{hour12:!1});for(let n of e){let e=h(`<li><span></span><time></time></li>`);e.dataset.mark=n.mark,_(e,`span`,n.text),_(e,`time`,t),r.prepend(e)}for(;r.children.length>12;)r.lastElementChild.remove()},pause(e){document.documentElement.classList.toggle(`paused`,e)},still(e){e?n.pauseAnimations():n.unpauseAnimations()}}}var E=45e3,ae=1e3,oe=3e4,se=(e,t)=>Math.min(oe,ae*2**(e-1))*(.8+.4*t);function D(e){try{return JSON.parse(e)}catch{return}}function ce(e){let t,n,r,i=0,a={state:`connecting`,attempt:0},o=t=>e.onStatus(a={...a,...t}),s=()=>(e.clearTimeout(n),e.clearTimeout(r),n=r=void 0),c=()=>{let e=t;t=void 0,e?.close()},l=()=>{e.clearTimeout(r),r=e.setTimeout(()=>u(),E),o({state:`live`,attempt:0,lastContact:e.now()})},u=()=>{c(),s();let t=a.attempt+1;if(t>8)return o({state:`offline`,attempt:t});o({state:`reconnecting`,attempt:t}),e.hidden()||(n=e.setTimeout(d,se(t,e.random())))},d=()=>{c(),s(),i=e.now();let n=t=e.open();n.onError(()=>t===n&&u()),n.listen(`message`,r=>{if(t!==n)return;let i=D(r);i&&e.onNet(i),o({problem:i?void 0:`the server sent a snapshot the page cannot read`}),l()}),n.listen(`ping`,()=>t===n&&l()),n.listen(`problem`,e=>{t===n&&(o({problem:D(e)??`the server reported a problem the page cannot read`}),l())}),r=e.setTimeout(()=>u(),E)};return{start(){o({state:`connecting`,attempt:0}),d()},resume(){let n=e.now()-i<2e3||a.lastContact!==void 0&&e.now()-a.lastContact<=2e4;t&&n||(o({attempt:0,state:a.state===`live`?`reconnecting`:a.state}),d())},restart:d,hide(){e.clearTimeout(n),n=void 0},stop(){c(),s()}}}var le=(function(){let e=typeof document<`u`&&document.createElement(`link`).relList;return e&&e.supports&&e.supports(`modulepreload`)?`modulepreload`:`preload`})(),ue=function(e,t){return new URL(e,t).href},de={},fe=function(e){return e.pathname.endsWith(`.css`)},pe=function(e,t,n){if(t in e)return e[t];let r=n();if(!r){e[t]=void 0;return}let i=r.then(()=>{e[t]=void 0},n=>{throw e[t]=void 0,n});return e[t]=i,i},O=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e,i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?new URL(import.meta.resolve(e)):new URL(e,import.meta.url)}r=o(t.map(t=>{t=ue(t,n);let r=s(t),i=fe(r);return pe(de,r.href,()=>{if(e===void 0){e={all:new Set,styles:new Set};let t=document.getElementsByTagName(`link`);for(let n=t.length-1;n>=0;n--){let r=t[n];e.all.add(r.href),r.rel===`stylesheet`&&e.styles.add(r.href)}}if((i?e.styles:e.all).has(r.href))return;let t=document.createElement(`link`);if(t.rel=i?`stylesheet`:le,i||(t.as=`script`),t.crossOrigin=``,t.href=r.href,a&&t.setAttribute(`nonce`,a),document.head.appendChild(t),i)return new Promise((e,n)=>{t.addEventListener(`load`,e),t.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${r}`)))})})}).filter(e=>e!==void 0))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},k=ie(),A=e=>document.getElementById(e),me=e=>e?new Date(e).toLocaleTimeString([],{hour12:!1}):`never`,j,M,N,P,F={state:`connecting`,attempt:0},I={view:`map`},L=!1,R,z,he,B=e=>z?e(z):void he?.then(e);function ge(){he??=O(async()=>{let{mountFocus:e}=await import(`./views-U9D50TDD.js`);return{mountFocus:e}},[],import.meta.url).then(({mountFocus:e})=>(z=e({data:ve,restart:()=>Q.restart(),openTile:W,online:()=>F.state===`live`}),z.show(I,I),N&&z.net(N),z))}var V,H,U=e=>{if(V)return e(V);H??=O(async()=>{let{mountPanels:e}=await import(`./layer-DhF1EBxx.js`);return{mountPanels:e}},[],import.meta.url).then(({mountPanels:e})=>V=e({label:e=>N?.bases.find(t=>t.id===e)?.label??e,recent:async e=>(await(Z?Z.data(e).latest():X(e,`?limit=12`))).items,post:e=>Z&&((t,n)=>Z.data(e).post(t,n))})),H.then(e)},W=(e,t)=>U(n=>n.tiles.open(e,t));function G(e){if(N=P=e,document.hidden)return;k.stats(e);let t=i(e,Date.now()),r=a(R,t);(r.length||V)&&U(e=>e.alerts(r,t)),R=t,k.log(n(M,e)),M=e,B(t=>t.net(e)),I.view===`map`&&(k.show(e),j=e,P=void 0)}function K(e){let t=I,n=e.view===`workspace`&&t.view===`map`?document.querySelector(`#bases [data-key="${CSS.escape(e.ws)}"]`)?.getBoundingClientRect():void 0;I=e,e.view!==`map`&&ge(),A(`map`).hidden=e.view!==`map`,A(`legend`).hidden=e.view===`session`,k.still(e.view!==`map`),B(r=>r.show(e,t,n)),e.view===`map`&&N&&N!==j&&(G(N),requestAnimationFrame(k.layout)),L&&d(e)!==d(t)&&Q.restart()}function q(e){F=e;let t=e.state===`reconnecting`||e.state===`offline`;document.body.dataset.stale=A(`stage`).dataset.stale=String(t),A(`link`).dataset.state=e.state,A(`link-text`).textContent={connecting:`connecting`,live:`live`,reconnecting:`reconnecting`,offline:`offline`}[e.state],z&&N&&z.net(N),V?.stack.redraw();let n=A(`banner`);n.hidden=!t&&!e.problem;let r=e.state===`offline`?`Disconnected. Gave up after ${e.attempt-1} tries; it reconnects when you return to this tab.`:e.state===`reconnecting`?`Disconnected from the straw net. Reconnecting${e.attempt?` (try ${e.attempt})`:``}… The view shows the last known state.`:`The server is up but cannot read the net: ${e.problem}`,i=document.createElement(`b`);i.textContent=me(e.lastContact),n.replaceChildren(`${r} Last update `,i,`.`)}function J(e,t){try{let n=JSON.parse(t);B(t=>e===`history`?t.history(n):t.cards(n))}catch{}}function _e(e){let t=new EventSource(e?`events?watch=${encodeURIComponent(e)}`:`events`);return{onError:e=>t.onerror=e,listen:(e,n)=>t.addEventListener(e,e=>n(e.data)),close:()=>t.close()}}var Y=e=>fetch(`api/${e}`,{cache:`no-store`}).then(e=>e.ok?e.json():Promise.reject(Error(String(e.status)))),X=(e,t=``)=>Y(`history/${encodeURIComponent(e)}${t}`),Z=void 0,ve=e=>({older:t=>X(e,`?before=${encodeURIComponent(t)}`),latest:()=>X(e),entry:t=>Y(`entry/${encodeURIComponent(e)}/${encodeURIComponent(t)}`).catch(()=>void 0)}),ye=()=>{let e=_e(d(I));return e.listen(`history`,e=>J(`history`,e)),e.listen(`cards`,e=>J(`cards`,e)),e};document.addEventListener(`click`,e=>{let t=e.target?.closest?.(`a[data-alert]`),n=t&&c(t.getAttribute(`data-route`)??``);if(!n||n.view!==`session`||e.button!==0||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey)return;let r=R?.get(n.key);r&&(e.preventDefault(),U(e=>e.stack.show(r)))},!0);var be=ee(e=>K(e)),Q=ce({open:ye,now:Date.now,setTimeout:(e,t)=>window.setTimeout(e,t),clearTimeout:e=>window.clearTimeout(e),random:Math.random,hidden:()=>document.hidden,onNet:G,onStatus:q}),$=()=>Q.resume();document.addEventListener(`visibilitychange`,()=>{if(k.pause(document.hidden),document.hidden)return Q.hide();P&&G(P),B(e=>e.wake()),$()}),addEventListener(`keydown`,e=>{e.key!==`Escape`||e.defaultPrevented||(e.preventDefault(),!V?.stack.close()&&I.view!==`map`&&be.go(u(I)))}),A(`pair`).addEventListener(`click`,()=>void O(async()=>{let{pairTile:e}=await import(`./pair-DDTg3MTR.js`);return{pairTile:e}},[],import.meta.url).then(({pairTile:e})=>W(e(),!0)));for(let e of[`focus`,`pageshow`,`online`])window.addEventListener(e,()=>!document.hidden&&$());q(F),K(be.route()),Q.start(),L=!0;export{b as a,S as c,C as d,_ as f,x as i,h as l,r as m,f as n,g as o,m as p,p as r,y as s,O as t,v as u};
|
|
1
|
+
var e={working:`started working`,waiting:`is waiting on a question`,idle:`went idle`,done:`finished`,error:`hit an error`,unknown:`lost its status`};function t(e){let t=new Map;for(let n of e.bases){n.tower&&t.set(`tower:${n.id}`,{name:`${n.label} tower`,mark:n.tower.mark});for(let e of n.tabs.flatMap(e=>e.units))for(let n of[e,...e.subs])t.set(n.id,{name:n.name,mark:n.mark});for(let e of n.outposts)t.set(e.id,{name:e.label,mark:e.mark})}return t}function n(n,r){if(!n)return[];let[i,a]=[t(n),t(r)],o=[];for(let[t,n]of a){let r=i.get(t);r?r.mark!==n.mark&&o.push({text:`${n.name} ${e[n.mark]}`,mark:n.mark}):o.push({text:`${n.name} joined`,mark:n.mark})}for(let[e,t]of i)a.has(e)||o.push({text:`${t.name} left`,mark:`idle`});return o}var r=e=>e===`waiting`||e===`error`?0:1;function i(e,t){let n=new Map;for(let r of e.bases)for(let e of[...r.tower?[{...r.tower,role:`tower`}]:[],...[...r.tabs.flatMap(e=>e.units),...r.outside].flatMap(e=>[e,...e.subs])])n.set(e.key,{key:e.key,ws:r.id,name:e.name,role:e.role,mark:e.mark,note:e.note,at:t,pending:e.pending});return n}function a(e,t){return[...t.values()].filter(t=>e?.get(t.key)?.mark!==t.mark&&(r(t.mark)===0||e!==void 0&&t.mark===`done`))}var o=e=>{try{return decodeURIComponent(e)}catch{return e}},s=e=>/^\.+$/.test(e)?e.replaceAll(`.`,`%2E`):encodeURIComponent(e);function c(e){let[t,n]=e.split(`#`)[0].split(`?`),[r,i,a,s]=t.replace(/^\/+|\/+$/g,``).split(`/`).map(o);if(r!==`w`||!i)return{view:`map`};if(a!==`s`||!s)return{view:`workspace`,ws:i};let c=new URLSearchParams(n).get(`at`);return c?{view:`session`,ws:i,key:s,at:c}:{view:`session`,ws:i,key:s}}var l=e=>e.view===`map`?``:e.view===`workspace`?`w/${s(e.ws)}`:`w/${s(e.ws)}/s/${s(e.key)}${e.at?`?at=${encodeURIComponent(e.at)}`:``}`,u=e=>e.view===`session`?{view:`workspace`,ws:e.ws}:{view:`map`},d=e=>e.view===`session`?e.key:e.view===`workspace`?`w:${e.ws}`:null;function ee(e,t=new URL(document.baseURI).pathname){let n=()=>c(location.pathname.slice(t.length)+location.search),r={route:n,href:e=>l(e)||`./`,go(n){let r=t+l(n);location.pathname+location.search!==r&&history.pushState(null,``,r),e(n)}};return addEventListener(`popstate`,()=>e(n())),document.addEventListener(`click`,e=>{let t=e.target?.closest?.(`a[data-route]`);!t||e.defaultPrevented||e.button!==0||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||(e.preventDefault(),r.go(c(t.getAttribute(`data-route`))))}),r}var f={main:`main`,member:`member`,subagent:`subagent`,outpost:`outpost`,outside:`outside`,tower:`tower`},p={working:`working`,waiting:`waiting on a question`,idle:`idle`,done:`finished`,error:`error`,unknown:`no status`},m=e=>e===`waiting`||e===`error`,h=e=>{let t=new DOMParser().parseFromString(e.trim(),`text/html`).body.firstElementChild;if(!(t instanceof HTMLElement))throw Error(`bad template`);return document.importNode(t,!0)},g=(e,t)=>{let n=document.getElementById(e);if(!(n instanceof t))throw Error(`#${e} is missing from the page`);return n},_=(e,t,n)=>{let r=e.querySelector(t);r.textContent!==n&&(r.textContent=n)},v=(e,t)=>{if(e.dataset.mark===t)return;let n=e.dataset.mark!==void 0;e.dataset.mark=t,n&&(e.classList.remove(`flash`),e.offsetWidth,e.classList.add(`flash`))},y=(e,t)=>e?e.replace(/^tool:/,`using `):p[t],b=(e,t)=>{e.toggleAttribute(`hidden`,!t),t&&(e.setAttribute(`href`,l(t)||`./`),e.setAttribute(`data-route`,l(t)))},x=`<div class="plate"><i class="ring"></i><svg><use href="#i-main"/></svg><b class="badge"></b></div>`;function S(e,t,n){v(e,n);let r=e.querySelector(`use`),i=`#i-${f[t]??`member`}`;r.getAttribute(`href`)!==i&&r.setAttribute(`href`,i)}function C(e,t,n,r){let i=new Map;for(let t of e.children)!t.classList.contains(`leaving`)&&t.dataset.key&&i.set(t.dataset.key,t);let a=e.firstElementChild;for(let o of t){let t=i.get(o.id);for(t||(t=n(o),t.dataset.key=o.id,t.classList.add(`enter`),setTimeout(()=>t.classList.remove(`enter`),600)),i.delete(o.id);a?.classList.contains(`leaving`);)a=a.nextElementSibling;t===a?a=a.nextElementSibling:e.insertBefore(t,a),r(t,o)}for(let e of i.values())e.classList.add(`leaving`),setTimeout(()=>e.remove(),400)}var w=()=>h(`<div class="unit"><div class="card"><a class="hit"></a>${x}<div class="meta"><span class="name"></span><span class="line"><em class="role"></em><span class="note"></span></span></div></div><div class="subs"></div></div>`);function T(e,t){let n=e.closest(`.base`)?.dataset.key,r=e.querySelector(`.hit`);b(r,t.key&&n?{view:`session`,ws:n,key:t.key}:void 0),r.setAttribute(`aria-label`,`${t.name}: ${y(t.note,t.mark)}`),r.toggleAttribute(`data-alert`,m(t.mark)),v(e,t.mark),S(e.querySelector(`.plate`),t.role,t.mark),_(e,`.name`,t.name),_(e,`.role`,t.role),_(e,`.note`,y(t.note,t.mark)),C(e.querySelector(`.subs`),t.subs??[],w,T)}var te=()=>h(`<section class="base"><header><div class="tower"><i class="sweep"></i><i class="pulse"></i><svg><use href="#i-tower"/></svg><b class="badge"></b></div><div class="title"><span class="crown"><svg><use href="#i-crown"/></svg>head tower</span><h2><a class="enter-base"></a></h2><p></p></div></header><div class="tabs"></div><div class="outposts"></div></section>`);function ne(e,t){v(e,t.mark),e.classList.toggle(`head`,t.head);let n=e.querySelector(`.tower`);n.classList.toggle(`ghost`,!t.tower),v(n,t.tower?.mark??`unknown`),_(e,`h2 a`,t.label),b(e.querySelector(`h2 a`),{view:`workspace`,ws:t.id}),_(e,`.title p`,t.tower?`control tower · ${p[t.tower.mark]}`:`no tower yet`),C(e.querySelector(`.tabs`),t.tabs,()=>h(`<div class="tab"><h3></h3><div class="units"></div></div>`),(e,t)=>{_(e,`h3`,`tab ${t.label}`),C(e.querySelector(`.units`),t.units,w,T)}),C(e.querySelector(`.outposts`),t.outposts.map(e=>({id:e.id,name:e.label,role:`outpost`,mark:e.mark,note:e.mark===`unknown`?`no agent running`:void 0})),w,T)}function re(e,t,n){let r=[...e.querySelectorAll(`.base:not(.leaving) .tower`)],i=e.querySelector(`.base.head:not(.leaving) .tower`),a=e.getBoundingClientRect(),o=e=>{let t=e.getBoundingClientRect();return[Math.round(t.x+t.width/2-a.x),Math.round(t.y+t.height/2-a.y)]},s=i?r.filter(e=>e!==i).map(e=>{let[[t,n],[r,a]]=[o(i),o(e)],s=Math.round((t+r)/2);return`M${t} ${n}C${s} ${n} ${s} ${a} ${r} ${a}`}):[],c=s.join(`|`);if(t.dataset.sig===c)return;t.dataset.sig=c;let l=(e,t,...n)=>{let r=document.createElementNS(`http://www.w3.org/2000/svg`,e);for(let[e,n]of Object.entries(t))r.setAttribute(e,n);return r.append(...n),r};t.replaceChildren(...s.flatMap(e=>[l(`path`,{class:`wire`,d:e}),...n?[]:[l(`circle`,{class:`packet`,r:`3`},l(`animateMotion`,{dur:`5s`,repeatCount:`indefinite`,path:e}))]]))}function ie(){let[e,t,n,r]=[g(`map`,HTMLElement),g(`bases`,HTMLElement),g(`links`,SVGSVGElement),g(`log`,HTMLElement)],i=matchMedia(`(prefers-reduced-motion: reduce)`).matches,a=()=>!e.hidden&&re(e,n,i);return new ResizeObserver(a).observe(e),{layout:a,stats(e){let t=e.bases.flatMap(e=>e.tabs.flatMap(e=>e.units.flatMap(e=>[e,...e.subs]))),n=e=>String(t.filter(t=>!e||t.mark===e).length);for(let[t,r]of[[`s-bases`,String(e.bases.length)],[`s-agents`,n()],[`s-working`,n(`working`)],[`s-waiting`,n(`waiting`)]])g(t,HTMLElement).textContent=r},show(e){C(t,e.bases,te,ne),g(`empty`,HTMLElement).hidden=e.bases.length>0,requestAnimationFrame(a)},log(e){let t=new Date().toLocaleTimeString([],{hour12:!1});for(let n of e){let e=h(`<li><span></span><time></time></li>`);e.dataset.mark=n.mark,_(e,`span`,n.text),_(e,`time`,t),r.prepend(e)}for(;r.children.length>12;)r.lastElementChild.remove()},pause(e){document.documentElement.classList.toggle(`paused`,e)},still(e){e?n.pauseAnimations():n.unpauseAnimations()}}}var ae=45e3,oe=1e3,se=3e4,ce=(e,t)=>Math.min(se,oe*2**(e-1))*(.8+.4*t);function E(e){try{return JSON.parse(e)}catch{return}}function le(e){let t,n,r,i=0,a={state:`connecting`,attempt:0},o=t=>e.onStatus(a={...a,...t}),s=()=>(e.clearTimeout(n),e.clearTimeout(r),n=r=void 0),c=()=>{let e=t;t=void 0,e?.close()},l=()=>{e.clearTimeout(r),r=e.setTimeout(()=>u(),ae),o({state:`live`,attempt:0,lastContact:e.now()})},u=()=>{c(),s();let t=a.attempt+1;if(t>8)return o({state:`offline`,attempt:t});o({state:`reconnecting`,attempt:t}),e.hidden()||(n=e.setTimeout(d,ce(t,e.random())))},d=()=>{c(),s(),i=e.now();let n=t=e.open();n.onError(()=>t===n&&u()),n.listen(`message`,r=>{if(t!==n)return;let i=E(r);i&&e.onNet(i),o({problem:i?void 0:`the server sent a snapshot the page cannot read`}),l()}),n.listen(`ping`,()=>t===n&&l()),n.listen(`problem`,e=>{t===n&&(o({problem:E(e)??`the server reported a problem the page cannot read`}),l())}),r=e.setTimeout(()=>u(),ae)};return{start(){o({state:`connecting`,attempt:0}),d()},resume(){let n=e.now()-i<2e3||a.lastContact!==void 0&&e.now()-a.lastContact<=2e4;t&&n||(o({attempt:0,state:a.state===`live`?`reconnecting`:a.state}),d())},restart:d,hide(){e.clearTimeout(n),n=void 0},stop(){c(),s()}}}var ue=(function(){let e=typeof document<`u`&&document.createElement(`link`).relList;return e&&e.supports&&e.supports(`modulepreload`)?`modulepreload`:`preload`})(),de=function(e,t){return new URL(e,t).href},fe={},pe=function(e){return e.pathname.endsWith(`.css`)},me=function(e,t,n){if(t in e)return e[t];let r=n();if(!r){e[t]=void 0;return}let i=r.then(()=>{e[t]=void 0},n=>{throw e[t]=void 0,n});return e[t]=i,i},D=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e,i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?new URL(import.meta.resolve(e)):new URL(e,import.meta.url)}r=o(t.map(t=>{t=de(t,n);let r=s(t),i=pe(r);return me(fe,r.href,()=>{if(e===void 0){e={all:new Set,styles:new Set};let t=document.getElementsByTagName(`link`);for(let n=t.length-1;n>=0;n--){let r=t[n];e.all.add(r.href),r.rel===`stylesheet`&&e.styles.add(r.href)}}if((i?e.styles:e.all).has(r.href))return;let t=document.createElement(`link`);if(t.rel=i?`stylesheet`:ue,i||(t.as=`script`),t.crossOrigin=``,t.href=r.href,a&&t.setAttribute(`nonce`,a),document.head.appendChild(t),i)return new Promise((e,n)=>{t.addEventListener(`load`,e),t.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${r}`)))})})}).filter(e=>e!==void 0))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},O=ie(),k=e=>document.getElementById(e),he=e=>e?new Date(e).toLocaleTimeString([],{hour12:!1}):`never`,A,j,M,N,P={state:`connecting`,attempt:0},F={view:`map`},I=!1,L,R=``,z,B,V=e=>z?e(z):void B?.then(e);function ge(){B??=D(async()=>{let{mountFocus:e}=await import(`./views-CItUTayr.js`);return{mountFocus:e}},[],import.meta.url).then(({mountFocus:e})=>(z=e({data:ye,restart:()=>Q.restart(),openTile:G,openTerminal:(e,t)=>W(n=>n.terminal.open(e,t)),online:()=>P.state===`live`}),z.show(F,F),M&&z.net(M),z))}var H,U,W=e=>{if(H)return e(H);U??=D(async()=>{let{mountPanels:e}=await import(`./layer-DkIPqu42.js`);return{mountPanels:e}},[],import.meta.url).then(({mountPanels:e})=>H=e({label:e=>M?.bases.find(t=>t.id===e)?.label??e,recent:async e=>(await(Z?Z.data(e).latest():X(e,`?limit=12`))).items,post:e=>Z&&((t,n)=>Z.data(e).post(t,n)),stream:()=>R})),U.then(e)},G=(e,t)=>W(n=>n.tiles.open(e,t));function K(e){if(M=N=e,document.hidden)return;O.stats(e);let t=i(e,Date.now()),r=a(L,t);(r.length||H)&&W(e=>e.alerts(r,t)),L=t,O.log(n(j,e)),j=e,V(t=>t.net(e)),F.view===`map`&&(O.show(e),A=e,N=void 0)}function q(e){let t=F,n=e.view===`workspace`&&t.view===`map`?document.querySelector(`#bases [data-key="${CSS.escape(e.ws)}"]`)?.getBoundingClientRect():void 0;F=e,e.view!==`map`&&ge(),k(`map`).hidden=e.view!==`map`,k(`legend`).hidden=e.view===`session`,O.still(e.view!==`map`),V(r=>r.show(e,t,n)),e.view===`map`&&M&&M!==A&&(K(M),requestAnimationFrame(O.layout)),I&&d(e)!==d(t)&&Q.restart()}function J(e){P=e;let t=e.state===`reconnecting`||e.state===`offline`;document.body.dataset.stale=k(`stage`).dataset.stale=String(t),k(`link`).dataset.state=e.state,k(`link-text`).textContent={connecting:`connecting`,live:`live`,reconnecting:`reconnecting`,offline:`offline`}[e.state],z&&M&&z.net(M),H?.stack.redraw();let n=k(`banner`);n.hidden=!t&&!e.problem;let r=e.state===`offline`?`Disconnected. Gave up after ${e.attempt-1} tries; it reconnects when you return to this tab.`:e.state===`reconnecting`?`Disconnected from the straw net. Reconnecting${e.attempt?` (try ${e.attempt})`:``}… The view shows the last known state.`:`The server is up but cannot read the net: ${e.problem}`,i=document.createElement(`b`);i.textContent=he(e.lastContact),n.replaceChildren(`${r} Last update `,i,`.`)}function Y(e,t){try{let n=JSON.parse(t);V(t=>e===`history`?t.history(n):t.cards(n))}catch{}}function _e(e){let t=new EventSource(e?`events?watch=${encodeURIComponent(e)}`:`events`);return{onError:e=>t.onerror=e,listen:(e,n)=>t.addEventListener(e,e=>n(e.data)),close:()=>t.close()}}var ve=e=>fetch(`api/${e}`,{cache:`no-store`}).then(e=>e.ok?e.json():Promise.reject(Error(String(e.status)))),X=(e,t=``)=>ve(`history/${encodeURIComponent(e)}${t}`),Z=void 0,ye=e=>({older:t=>X(e,`?before=${encodeURIComponent(t)}`),latest:()=>X(e),entry:t=>ve(`entry/${encodeURIComponent(e)}/${encodeURIComponent(t)}`).catch(()=>void 0)}),be=()=>{let e=_e(d(F));return e.listen(`history`,e=>Y(`history`,e)),e.listen(`cards`,e=>Y(`cards`,e)),e.listen(`hello`,e=>(R=e,H?.terminal.hello())),e.listen(`mirror`,e=>H?.terminal.frame(e)),e};document.addEventListener(`click`,e=>{let t=e.target?.closest?.(`a[data-alert]`),n=t&&c(t.getAttribute(`data-route`)??``);if(!n||n.view!==`session`||e.button!==0||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey)return;let r=L?.get(n.key);r&&(e.preventDefault(),W(e=>e.stack.show(r)))},!0);var xe=ee(e=>q(e)),Q=le({open:be,now:Date.now,setTimeout:(e,t)=>window.setTimeout(e,t),clearTimeout:e=>window.clearTimeout(e),random:Math.random,hidden:()=>document.hidden,onNet:K,onStatus:J}),$=()=>Q.resume();document.addEventListener(`visibilitychange`,()=>{if(O.pause(document.hidden),document.hidden)return Q.hide();N&&K(N),V(e=>e.wake()),$()}),addEventListener(`keydown`,e=>{e.key!==`Escape`||e.defaultPrevented||(e.preventDefault(),!H?.stack.close()&&F.view!==`map`&&xe.go(u(F)))}),k(`pair`).addEventListener(`click`,()=>void D(async()=>{let{pairTile:e}=await import(`./pair-DDTg3MTR.js`);return{pairTile:e}},[],import.meta.url).then(({pairTile:e})=>G(e(),!0)));for(let e of[`focus`,`pageshow`,`online`])window.addEventListener(e,()=>!document.hidden&&$());J(P),q(xe.route()),Q.start(),I=!0;export{b as a,S as c,C as d,_ as f,x as i,h as l,r as m,f as n,g as o,m as p,p as r,y as s,D as t,v as u};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{a as e,c as t,d as n,f as r,i,l as a,m as o,o as s,s as c,u as l}from"./index-CWA0eWOk.js";import{n as u,t as d}from"./post-B39ywXKd.js";import{t as f}from"./ask-CVw44ZU3.js";var p=[[[`esc`,`esc`,`Escape`],[`tab`,`tab`,`Tab`],[`up`,`↑`,`Up`],[`enter`,`enter`,`Enter`]],[[`shift+tab`,`⇧tab`,`Shift+Tab`],[`left`,`←`,`Left`],[`down`,`↓`,`Down`],[`right`,`→`,`Right`]],[[`space`,`space`,`Space`],[`backspace`,`⌫`,`Backspace`],[`home`,`home`,`Home`],[`end`,`end`,`End`],[`pageup`,`pgup`,`Page up`],[`pagedown`,`pgdn`,`Page down`]]],m=`123456789`.split(``);function h({name:e,state:t,post:n,stream:r}){let i=[],a=u(`p`,{class:`pad-note`,role:`status`}),o=!1,s=([e,t,n])=>{let r=u(`button`,{type:`button`,class:`pad-key`,"data-key":e,"aria-label":n},t);return r.onclick=t=>t.isTrusted&&!o&&void h(e),i.push(r),r},c=u(`details`,{class:`pad-digits`},u(`summary`,null,`1–9`),u(`div`,{class:`pad-row`},...m.map(e=>s([e,e,e])))),l=u(`div`,{class:`pad`,role:`group`,"aria-label":`Keys for ${e}`},...p.map(e=>u(`div`,{class:`pad-row`},...e.map(s))),c,a),d=(e,t=``)=>(a.dataset.tone=t,a.replaceChildren(e)),f=(e,t,n)=>{let r=u(`button`,{type:`button`,class:`pad-yes`},e===`enter`?`Send Enter`:`Send Escape`),i=u(`button`,{type:`button`,class:`cmp-link`},`Cancel`);r.onclick=t=>t.isTrusted&&!o&&void h(e,n),i.onclick=()=>d(``),a.dataset.tone=`ask`,a.replaceChildren(u(`span`,null,t),r,i),i.focus()};async function h(e,t){o=!0,g(),d(``);try{let i=await n(`keys`,{key:e,stream:r(),...t?{confirmed:t}:{}});i.status===409&&i.body.error===`confirm`&&typeof i.body.confirm==`string`?f(e,String(i.body.reason??``),i.body.confirm):i.ok||d(i.body.reason??`Refused (${i.status}).`,`error`)}catch{d(`The server could not be reached.`,`error`)}finally{o=!1,g()}}function g(){let e=t();for(let t of i)t.disabled=o||e===`off`||e===`hidden`&&t.dataset.key!==`esc`}return g(),{el:l,update:g}}var g=[0,95,135,175,215,255],_=120,v=5e3,y=3e3,b=e=>Number.isInteger(e)&&e>=0&&e<=255,x=e=>b(e)?e<16?e:e<232?`rgb(${g[Math.floor((e-16)/36)]},${g[Math.floor((e-16)/6)%6]},${g[(e-16)%6]})`:`rgb(${8+10*(e-232)},${8+10*(e-232)},${8+10*(e-232)})`:void 0,S=([e,t,n])=>[e,t,n].every(e=>b(e))?`rgb(${e},${t},${n})`:void 0;function C(e,t){let n={...e},r=t.split(`;`);for(let e=0;e<r.length;e++){let t=r[e].split(`:`).map(e=>e===``?0:Number(e)),i=t[0];if(i===38||i===48||i===58){let a=t.length>1?t[1]:Number(r[e+1]),o=t.length>1?t.slice(2):r.slice(e+2,e+(a===5?3:5)).map(Number);t.length===1&&(e+=a===5?2:a===2?4:1);let s=a===5?x(o[0]):a===2?S(o.slice(-3)):void 0;s!==void 0&&i!==58&&(n[i===38?`fg`:`bg`]=s)}else if(i===0)for(let e of Object.keys(n))delete n[e];else i===1||i===2?n[i===1?`b`:`d`]=!0:i===22?n.b=n.d=!1:i===3||i===23?n.i=i===3:i===4||i===21||i===24?n.u=i!==24&&t[1]!==0:i===7||i===27?n.v=i===7:i===9||i===29?n.s=i===9:i>=30&&i<=37||i>=90&&i<=97?n.fg=i-(i<90?30:82):i>=40&&i<=47||i>=100&&i<=107?n.bg=i-(i<100?40:92):(i===39||i===49)&&delete n[i===39?`fg`:`bg`]}return n}function w(e){let t=[],n={},r=0,i=e=>{let r=e.replace(/\x1b(\[[0-?]*[ -/]*[@-~]?)?|[\u0000-\u001f\u007f-\u009f]/g,``);if(!r)return;let[i,a]=n.v?[n.bg??`z`,n.fg??`z`]:[n.fg,n.bg],o=[typeof i==`number`||i===`z`?`c${i}`:``,typeof a==`number`||a===`z`?`k${a}`:``,...[`b`,`d`,`i`,`u`,`s`].filter(e=>n[e]).map(e=>`t${e}`)].filter(Boolean).join(` `);t.push({text:r,cls:o,...typeof i==`string`&&i!==`z`&&{fg:i},...typeof a==`string`&&a!==`z`&&{bg:a}})};for(let t of e.matchAll(/\x1b\[([0-9;:]{0,64})m/g))i(e.slice(r,t.index)),n=C(n,t[1]),r=t.index+t[0].length;return i(e.slice(r)),t}function T(e,t){let n=Math.min(Math.max(0,Number(t.rows)||0),_);for(;e.children.length<n;)e.append(u(`div`,{class:`tr`}));for(;e.children.length>n;)e.lastElementChild.remove();for(let[r,i]of Array.isArray(t.set)?t.set:[])!Number.isInteger(r)||r<0||r>=n||typeof i!=`string`||e.children[r].replaceChildren(...w(i).map(e=>{if(!e.cls&&!e.fg&&!e.bg)return e.text;let t=u(`span`,e.cls?{class:e.cls}:null,e.text);return e.fg&&(t.style.color=e.fg),e.bg&&(t.style.backgroundColor=e.bg),t}))}function E({tiles:e,post:t,stream:n,pad:r}){let i=new Map,a,o=()=>{try{return String(JSON.parse(n()).stream??``)}catch{return``}},s=(e,t,n=``)=>(e.pad?.update(),(e.state.textContent!==t||e.state.dataset.tone!==n)&&(e.state.dataset.tone=n,e.state.textContent=t)),c=(e,t)=>(e.ended=!0,s(e,`${t} Open the terminal again to retry.`,`error`)),l=(e,t,n=o())=>n&&void(e.queue=e.queue.then(()=>e.post(`mirror`,{stream:n,on:t}).then(n=>t&&!n.ok&&c(e,n.body.reason??`Refused (${n.status}).`),()=>t&&c(e,`The server could not be reached.`)))),d=(e,t=Math.max(e.cols,...[...e.box.children].map(e=>e.textContent?.length??0)))=>e.box.style.fontSize=e.fit.getAttribute(`aria-pressed`)===`true`&&t?`${Math.max(6,Math.floor((e.scroll.clientWidth-20)/(t*.6)))}px`:``,f=e=>Date.now()-e.seen>y,p=()=>(clearInterval(a),a=i.size&&!document.hidden?setInterval(()=>i.forEach(e=>(e.box.classList.toggle(`stale`,f(e)),e.pad?.update())),1e3):void 0);document.addEventListener(`visibilitychange`,()=>(p(),i.forEach(e=>{clearTimeout(e.away),document.hidden?e.away=setTimeout(()=>(e.parked=!0,l(e,!1),s(e,`Paused while this tab is hidden.`)),v):e.parked&&(e.parked=!1,l(e,!0))})));let m=(e,n)=>{let a={key:e,box:u(`div`,{class:`term`,role:`group`,"aria-label":`Terminal of ${n}, read only`}),scroll:u(`div`,{class:`term-scroll`,tabindex:`0`}),state:u(`p`,{class:`term-state`,role:`status`},`Connecting…`),fit:u(`button`,{type:`button`,class:`cmp-link`,"aria-pressed":`false`},`Fit width`),post:t(e),cols:0,seen:0,parked:!1,first:!0,ended:!1,queue:Promise.resolve()};return a.fit.onclick=()=>(a.fit.setAttribute(`aria-pressed`,String(a.fit.getAttribute(`aria-pressed`)!==`true`)),d(a)),a.scroll.append(a.box),new ResizeObserver(()=>d(a)).observe(a.scroll),i.set(e,a),l(a,!0),p(),a.pad=r?.({name:n,state:()=>a.parked||a.ended||f(a)?`off`:a.box.classList.contains(`hidden`)?`hidden`:`live`,post:a.post,stream:o}),a.tile={id:`term:${e}`,title:`Terminal · ${n}`,icon:`i-comms`,body:()=>u(`div`,{class:`term-tile`},u(`div`,{class:`term-bar`},a.state,a.fit),a.scroll,a.pad?.el),closed:()=>(clearTimeout(a.away),l(a,!1),i.delete(e),p())}};return{open(t,n){let r=i.get(t);r?.ended&&(r.ended=!1,s(r,`Connecting…`),l(r,!0)),e.open(r?.tile??m(t,n),!0)},hello:()=>i.forEach(e=>!e.parked&&!e.ended&&l(e,!0)),frame(e){let t;try{t=JSON.parse(e)}catch{return}let n=i.get(t.key);if(n){if(n.box.classList.toggle(`hidden`,!!(t.hidden||t.ended)),t.ended)return c(n,`The mirror ended: ${t.ended}`);if([n.seen,n.ended]=[Date.now(),!1],n.box.classList.remove(`stale`),t.hidden)return T(n.box,{key:n.key,rows:0}),s(n,`Hidden while it waits for a secret; type it in the terminal.`);T(n.box,t),(t.whole||t.cols&&t.cols!==n.cols)&&(n.cols=t.cols||n.cols,d(n)),s(n,`Live · ${n.cols}×${n.box.children.length}`,`ok`),n.first&&t.whole&&(n.first=!1,requestAnimationFrame(()=>n.scroll.scrollTop=n.scroll.scrollHeight))}}}}var D=(e,t)=>o(e.mark)-o(t.mark)||e.at-t.at,O=e=>{let t=e.filter(e=>o(e.mark)===0).length,n=e.length-t;return[t&&`${t} need${t===1?`s`:``} you`,n&&`${n} finished`].filter(Boolean).join(` · `)};function k(e){let t=[],n=[],r=()=>e.render(t.map(e=>e.alert),n),i=(i,a,o=!0)=>{let s=t.findIndex(e=>e.alert.key===i.key);if(s>=0&&!o)return;let c=s>=0?t.splice(s,1)[0]:{alert:i,back:a,raised:i.mark};!o&&t.length?t.splice(t.length-1,0,c):t.push(c),n=n.filter(e=>e.key!==i.key),r(),o&&e.enter(c.alert)};return{get open(){return t.map(e=>e.alert)},get queue(){return n},arrive(e){let i=t.find(t=>t.alert.key===e.key);if(i)[i.alert,i.raised]=[{...e,at:i.alert.at},e.mark];else{let t=n.find(t=>t.key===e.key);n=[...n.filter(t=>t.key!==e.key),t&&t.mark===e.mark?t:e].sort(D)}r()},prune(e){n=n.filter(t=>e(t.key)?.mark===t.mark);for(let n of t){let t=e(n.alert.key);n.alert=t?{...n.alert,...t,gone:void 0,at:n.alert.at}:{...n.alert,mark:`unknown`,note:void 0,pending:void 0,gone:!0}}r()},redraw:r,show:(t,n=!0)=>i(t,e.focused(),n),raise(n){let r=t.find(e=>e.alert.key===n);r&&r!==t.at(-1)&&i(r.alert,e.focused())},next(){n[0]&&i(n[0],e.focused())},close(a=`close`){let s=t.pop();if(!s)return!1;let c=t.at(-1),l=a===`close`?n.find(e=>o(e.mark)===0):void 0;return a===`later`&&s.alert.mark===s.raised&&(n=[...n,s.alert].sort(D)),!c&&l?(i(l,s.back),!0):(r(),!e.restore(s.back)&&c&&e.enter(c.alert),!0)}}}var A=`a, button, input, textarea, select, summary, label, [role=button]`;function j(e){let t=[],n,r=()=>e.render(t,n);return{get tiles(){return t},get active(){return n},open(i,a=!0){t.some(e=>e.id===i.id)||(t=[...t,i]).slice(0,-4).forEach(e=>e.closed?.()),t=t.slice(-4),(a||!t.some(e=>e.id===n))&&(n=i.id),r(),a&&e.enter(t.find(e=>e.id===i.id))},close(e){let i=t.findIndex(t=>t.id===e);i<0||(t[i].closed?.(),t=t.filter(t=>t.id!==e),n===e&&(n=(t[i]??t[i-1])?.id),r())},activate(e){n=e,r()}}}function M(e){let t=new Set(e.flatMap(e=>e.kind===`result`&&e.callId?[e.callId]:[])),n=e.filter(e=>e.kind===`assistant`),r=n.at(-1),i=r?.tools?.find(e=>!t.has(e.id));return{said:(n.findLast(e=>e.text?.trim())?.text?.trim().split(`
|
|
2
|
+
`).filter(Boolean).at(-1))?.slice(0,280),pending:i&&`${i.name} · ${i.summary}`.slice(0,200),at:(r??e.findLast(e=>e.kind!==`result`))?.id}}var N=()=>a(`<section class="dialog" role="dialog" aria-modal="false" tabindex="-1"><header><b class="tag">alert</b><span class="where"></span><button class="x" type="button" aria-label="Close">×</button></header><div class="who">${i}<div><h3></h3><p><em class="role"></em> <span class="state"></span></p></div></div><div class="intel"><h4>Last transmission</h4><p class="said">…</p><p class="pending" hidden><b>waiting on</b> <span></span></p></div><div class="ask-root"></div><footer><a class="go">Open comms</a><button class="later" type="button">Later</button><span class="queued"></span></footer></section>`),P=()=>a(`<section class="tile" tabindex="-1"><header><svg><use href="#i-doc"/></svg><b></b><button class="x" type="button" aria-label="Close document">×</button></header><div class="tile-body"></div></section>`);function F(o){let[u,p,m,g,_]=[s(`dock`,HTMLElement),s(`work`,HTMLElement),s(`tiles`,HTMLElement),s(`tile-tabs`,HTMLElement),s(`alerts`,HTMLButtonElement)],v=new Map,y=(t,n,i=v.get(n.key))=>{let a=t.dataset.gone===`true`;r(t,`.said`,i?.failed?`History unavailable.`:i?.said??`No messages yet.`),t.querySelector(`.pending`).hidden=!i?.pending||a,r(t,`.pending span`,i?.pending??``),e(t.querySelector(`.go`),a?void 0:{view:`session`,ws:n.ws,key:n.key,...i?.at&&{at:i.at}})},b=(e,t={said:`…`})=>{v.set(e.key,t),o.recent(e.key).then(M,()=>({failed:!0})).then(n=>{let r=v.get(e.key)===t&&(v.set(e.key,n),u.querySelector(`.dialog[data-key="${CSS.escape(e.key)}"]`));r&&y(r,e)})},x=(e,n,i,a)=>{l(e,n.mark),e.dataset.gone=String(!!n.gone),e.style.transform=i?`translateY(${-12*i}px) scale(${1-.06*i})`:``,e.style.opacity=String(Math.max(0,1-.3*i)),e.toggleAttribute(`inert`,i>0),e.setAttribute(`aria-label`,`${n.name}: ${c(n.note,n.mark)}`),t(e.querySelector(`.plate`),n.role,n.mark),r(e,`h3`,n.name),r(e,`.role`,n.role===`outside`?`unaligned`:n.role),r(e,`.state`,n.gone?`left the net`:c(n.note,n.mark)),r(e,`.where`,o.label(n.ws)),r(e,`.tag`,n.gone?`gone`:{waiting:`question`,error:`error`,done:`finished`}[n.mark]??`update`),r(e,`.queued`,a.length?`Also queued: ${O(a)}`:``),v.has(n.key)||b(n),y(e,n);let s=S.get(e);s||S.set(e,s=f(e.querySelector(`.ask-root`),C(n.key),void 0,()=>I.open(n.key,n.name))),s.update(n.pending,n.mark===`waiting`&&document.body.dataset.stale!==`true`)},S=new WeakMap,C=e=>o.post?.(e)??((t,n)=>d(`sessions/${encodeURIComponent(e)}/${t}`,n)),w=k({render(e,o){n(u,e.map(e=>({...e,id:e.key})),N,(t,n)=>x(t,n,e.length-1-e.findIndex(e=>e.key===n.key),o));let s=o[0];_.disabled=!s,_.dataset.mark=s?.mark??`unknown`,r(_,`#alert-count`,String(o.length)),_.title=o.length?`Alerts: ${O(o)}`:``,r(_,`#alert-text`,s?`${s.name} · ${c(s.note,s.mark)}`:`no alerts`),n(_.querySelector(`.faces`),o.slice(0,3).map(e=>({...e,id:e.key})),()=>a(i),(e,n)=>t(e,n.role,n.mark))},enter:e=>u.querySelector(`[data-key="${CSS.escape(e.key)}"] .go`)?.focus(),focused:()=>document.activeElement instanceof HTMLElement&&document.activeElement!==document.body?document.activeElement:void 0,restore:e=>!!(e?.isConnected&&!e.closest(`[inert]`)&&(e.focus(),document.activeElement===e))});_.addEventListener(`click`,()=>w.next()),document.addEventListener(`click`,e=>{let t=t=>{let n=t.getBoundingClientRect();return e.clientX>=n.left&&e.clientX<=n.right&&e.clientY>=n.top&&e.clientY<=n.bottom},n=[...u.querySelectorAll(`.dialog:not(.leaving)`)],r=n.slice(0,-1).reverse().find(t);e.defaultPrevented||!r||!n.length||t(n.at(-1))||e.target?.closest?.(A)||(e.preventDefault(),e.stopPropagation(),w.raise(r.dataset.key))},!0),u.addEventListener(`click`,e=>{let t=e.target;t.closest(`.x`)?w.close():t.closest(`.later`)?w.close(`later`):t.closest(`.go`)&&queueMicrotask(()=>void w.close(`go`))});let T=0,D=()=>m.classList.toggle(`tabbed`,F.tiles.length>1&&p.clientWidth<460+F.tiles.length*400),F=j({render(e,t){m.hidden=!e.length,n(m,e,e=>{let t=P();return t.querySelector(`use`).setAttribute(`href`,`#${e.icon??`i-doc`}`),t.querySelector(`.x`).setAttribute(`aria-label`,`Close ${e.title}`),t.querySelector(`.tile-body`).append(e.body()),t.querySelector(`.x`).addEventListener(`click`,()=>F.close(e.id)),t},(e,n)=>{r(e,`header b`,n.title),e.classList.toggle(`active`,n.id===t)}),n(g,e,()=>a(`<button type="button" class="tab-btn"></button>`),(e,n)=>{e.textContent=n.title,e.classList.toggle(`active`,n.id===t),e.onclick=()=>F.activate(n.id)}),m.prepend(g),e.length!==T&&(T=e.length,D())},enter:e=>requestAnimationFrame(()=>m.querySelector(`.tile[data-key="${CSS.escape(e.id)}"]`)?.focus())});new ResizeObserver(D).observe(p);let I=E({tiles:F,post:C,stream:o.stream,pad:h});return{tiles:F,stack:w,terminal:I,alerts(e,t){for(let t of e)b(t),w.arrive(t);w.prune(e=>t.get(e))}}}export{F as mountPanels};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./index-CHtZ727l.css"])))=>i.map(i=>d[i]);
|
|
2
|
+
import{a as e,c as t,d as n,f as r,i,l as a,p as o,r as s,s as c,t as l,u}from"./index-CWA0eWOk.js";import{n as d,t as f}from"./post-B39ywXKd.js";import{t as p}from"./ask-CVw44ZU3.js";var m=16384,h=`Not confirmed. Check the terminal before sending again.`,g=`The original already went to the session; what is here is your edited copy, unsent.`,_=/^\s*\/(quit|exit|new|resume|fork|clone|import|reload)(?=\s|$)/,v=new Map,y=e=>(t,n)=>f(`sessions/${encodeURIComponent(e)}/${t}`,n);function b(e,t,n){if(!e.ok)return[`error`,e.status===504?h:e.body.reason??`Refused (${e.status}).`];if(t&&!t.startsWith(n))return[`ok`,g]}var x=e=>{try{return e?.startsWith(`[`)?JSON.parse(e):e?[`error`,e]:void 0}catch{return[`error`,e]}},S=(e,t)=>e!==``&&!e.startsWith(t)?`Sent as it was; your edits stayed here, unsent.`:`Sent. It shows in the log once pi writes it.`,C=(e,t)=>({hidden:!e.live||!e.control||!!e.asking,disabled:t||!e.online}),w=(e,t)=>e.startsWith(t)?e.slice(t.length).replace(/^\n/,``):e;function T(e){if(!e.online)return`The stream is down; sending waits for it to come back.`;if(!e.live)return`This session is no longer live; its history stays readable.`;if(!e.control)return`No live pi pane backs this unit, so there is nothing to type into.`;if(e.mark===`waiting`)return e.answerable?`It waits on a dialog: answer it above, or in its terminal.`:`It waits on a dialog; answer it in its terminal.`}function E(e,t,n){let r=`sb-draft:${t}`,i=matchMedia(`(pointer: coarse)`).matches,a=d(`textarea`,{rows:`1`,maxlength:String(m),"aria-label":`Message`,enterkeyhint:i?`enter`:`send`}),o=d(`button`,{type:`submit`,class:`cmp-send`},`Send`),s=d(`button`,{type:`button`,class:`cmp-alt`,title:`Alt+Enter`},`Follow-up`),c=d(`button`,{type:`button`,class:`cmp-alt cmp-stop`},`Stop`),l=d(`p`,{class:`cmp-note`,role:`status`,"aria-live":`polite`}),u=d(`button`,{type:`button`,class:`cmp-link`},`Focus terminal`),f=d(`form`,{class:`composer`},d(`div`,{class:`cmp-row`},a,d(`div`,{class:`cmp-acts`},o,s,c)),d(`div`,{class:`cmp-foot`},l,u));e.replaceChildren(f);let p={name:``,control:!1,live:!1,mark:`unknown`,online:!1},g=!1,y=!1,E=e=>{try{e()}catch{}};E(()=>a.value=sessionStorage.getItem(r)??``);let D=()=>E(()=>a.value?sessionStorage.setItem(r,a.value):sessionStorage.removeItem(r)),O=()=>(a.style.height=`auto`,a.style.height=`${Math.min(a.scrollHeight,220)}px`),k=(e,t=``,...n)=>(l.dataset.tone=t,l.replaceChildren(e,...n)),A=(e,t,n)=>{let r=d(`button`,{type:`button`,class:`cmp-yes`},t),i=d(`button`,{type:`button`,class:`cmp-link`},`Cancel`);y=!0,r.onclick=()=>(y=!1,n()),i.onclick=()=>(y=!1,k(``),a.focus()),k(e,`ask`,r,i),r.focus()},[j,M]=[`${r}:note`,()=>{try{return sessionStorage.getItem(r)??``}catch{return}}],N=e=>E(()=>e?sessionStorage.setItem(j,JSON.stringify(e)):sessionStorage.removeItem(j));function P(e,i,a){let o={sent:a,answer:n(e,i)};return a!==void 0&&(N([`error`,h]),v.set(t,o),o.answer.then(e=>{let t=M();e.ok&&t!==void 0&&E(()=>sessionStorage.setItem(r,w(t,a))),N(b(e,t,a))},()=>N([`error`,h]))),F(o)}async function F(e,{sent:n}=e){g=!0,y=!1,L(),k(n===void 0?``:`Sending…`);try{let t=await e.answer;if(!f.isConnected)return;if(!t.ok)return t.status===504||N(),k(t.body.reason??`Refused (${t.status}).`,`error`);if(n===void 0)return k(``);N();let r=S(a.value,n);a.value=w(a.value,n),D(),O(),k(r,`ok`)}catch{k(h,`error`)}finally{v.get(t)===e&&v.delete(t),g=!1,L()}}function I(e,t){let n=a.value,r=T(p),i=t===n;if(g||y||!n.trim())return;if(r)return k(r,`error`);if(new TextEncoder().encode(n).length>16384)return k(`The text is longer than 16 KB; shorten it.`,`error`);let o=!i&&n.match(_)?.[0].trim();if(o)return A(`${o} ends this session or switches its pane to another one.`,`Send ${o}`,()=>I(e,n));P(`input`,{text:n,as:e,confirmed:i},n)}function L(){let e=T(p),t=p.mark===`working`;a.placeholder=e??`Message ${p.name} · ${i?``:`Enter sends, Shift+Enter adds a line · `}lands after any unsent text in its terminal`,o.disabled=g||!!e,s.hidden=c.hidden=!t||!!e,s.disabled=c.disabled=g,Object.assign(u,C(p,g))}f.addEventListener(`submit`,e=>(e.preventDefault(),I(`send`))),s.onclick=()=>I(`followUp`),c.onclick=()=>A(`Stop the current task? Queued messages go back to its terminal editor.`,`Stop`,()=>void(g||P(`keys`,{keys:[`esc`]}))),u.onclick=()=>void(g||P(`focus`,{})),a.addEventListener(`input`,()=>(D(),O())),f.addEventListener(`keydown`,e=>{e.key===`Escape`&&!e.isComposing&&(y||e.target===a&&a.value)&&(e.preventDefault(),y&&(y=!1,k(``),a.focus()))}),a.addEventListener(`keydown`,e=>{e.isComposing||e.keyCode===229||e.key!==`Enter`||e.shiftKey||i&&!e.altKey||(e.preventDefault(),I(e.altKey?`followUp`:`send`))});let R=window.visualViewport,z=()=>document.documentElement.style.setProperty(`--vvh`,`${R.height}px`);return R&&(R.addEventListener(`resize`,z),z()),O(),v.has(t)?F(v.get(t)):E(()=>{let e=x(sessionStorage.getItem(j));sessionStorage.removeItem(j),e&&k(e[1],e[0])}),{box:a,update:e=>(p=e,L()),ready:()=>!T(p)}}var D=200;function O(e,t){for(let n of e?.bases??[]){if(n.tower?.key===t)return{u:{key:t,name:n.tower.name,role:`tower`,mark:n.tower.mark,control:n.tower.control,pending:n.tower.pending,subs:[]},base:n};for(let[e,r]of[...n.tabs.map(e=>[e.label,e.units]),[void 0,n.outside]])for(let i of r){if(i.key===t)return{u:i,base:n,squad:e};let r=i.subs.find(e=>e.key===t);if(r)return{u:r,base:n,squad:e,launcher:i}}}}var k=()=>a(`<a class="token">${i}<span class="name"></span><span class="line"><em class="role"></em><span class="note"></span></span></a>`);function A(n,i,a){e(n,i.key?{view:`session`,ws:a,key:i.key}:void 0),n.toggleAttribute(`hidden`,!1),n.toggleAttribute(`data-alert`,!!i.key&&o(i.mark)),u(n,i.mark),t(n.querySelector(`.plate`),i.role,i.mark),r(n,`.name`,i.name),r(n,`.role`,i.role===`outside`?`unaligned`:i.role),r(n,`.note`,c(i.note,i.mark))}var j=()=>a(`<div class="col"><a class="token">${i}<span class="name"></span><span class="line"><em class="role"></em><span class="note"></span></span></a><div class="say"><p></p></div><div class="drones"></div></div>`),M=`<div class="zoom-in"><header class="base keep"><a class="tower big"><i class="sweep"></i><i class="pulse"></i><svg><use href="#i-tower"/></svg><b class="badge"></b></a><div class="title"><span class="crown"><svg><use href="#i-crown"/></svg>head tower</span><h2></h2><p></p></div><dl class="tally"><div><dt>Squads</dt><dd></dd></div><div><dt>Units</dt><dd></dd></div><div data-mark="working"><dt>Working</dt><dd></dd></div><div data-mark="waiting"><dt>Waiting</dt><dd></dd></div></dl></header><div class="squads"></div><div class="camps"><section class="tab squad camp" data-group="outposts"><h3><svg><use href="#i-outpost"/></svg><b>Outposts</b><small></small></h3><div class="formation"></div></section><section class="tab squad camp" data-group="outside"><h3><svg><use href="#i-outside"/></svg><b>Outside the net</b><small>unaligned pi panes</small></h3><div class="formation"></div></section></div><p class="lost" hidden></p></div>`,N=`<div class="dossier-in"><span class="tag">unit dossier</span><div class="portrait">${i}</div><h3></h3><p class="d-line"><em class="role"></em> <span class="state"></span></p><p class="d-note" hidden></p><dl class="specs"></dl><button class="cmp-link d-term" type="button" hidden>Terminal</button><div class="d-drones" hidden><h4>Dispatched</h4><div class="formation"></div></div></div>`,P=`<div class="comms-in"><header class="comms-head"><span class="logo"><svg><use href="#i-comms"/></svg></span><div><h2><small>comms</small> <span></span></h2><p></p></div><span class="chan link" data-state="live"><i></i><b>channel open</b></span></header><div class="chat-root"></div><div class="ask-root"></div><div class="composer-root"></div></div>`;function F({data:i,restart:d,openTile:f,openTerminal:m,online:h}){let g=e=>document.getElementById(e),[_,v,b]=[`ws-view`,`session-view`,`dossier`].map(g),x={view:`map`},S,C={},w,T=[],F=!1,I,L,R,z=0,B,V,H=()=>x.view===`map`?``:x.ws,U=(t,n)=>{let r=document.createElement(`a`);return e(r,t),r.textContent=n,r};function W(){let t=(t,n,r)=>{let i=document.createElement(t);return i.textContent=n,r&&e(i,r),i},n=g(`crumbs`),r=x;if(r.view===`map`)return n.replaceChildren(t(`b`,`Straw Net`),t(`span`,`all bases`));let i=S?.bases.find(e=>e.id===r.ws)?.label??r.ws,a=r.view===`session`?O(S,r.key)?.u.name??I?.name??r.key:``;n.replaceChildren(t(`a`,`Straw Net`,{view:`map`}),r.view===`workspace`?t(`b`,i):t(`a`,i,{view:`workspace`,ws:r.ws}),...a?[t(`b`,a)]:[])}function G(e,t,n){let r=e=>[`map`,`workspace`,`session`].indexOf(e.view),i=r(e)<r(t)?`enter-back`:e.view===`workspace`&&n?`enter-zoom`:e.view===`session`&&t.view!==`session`?`enter-slide`:``,a=g({map:`map`,workspace:`ws-view`,session:`session-view`}[e.view]);if(i){if(n){let e=a.getBoundingClientRect();a.style.setProperty(`--ox`,`${Math.round(n.x+n.width/2-e.x)}px`),a.style.setProperty(`--oy`,`${Math.round(n.y+n.height/2-e.y)}px`)}a.classList.remove(`enter-zoom`,`enter-slide`,`enter-back`),a.offsetWidth,a.classList.add(i),setTimeout(()=>a.classList.remove(i),700)}}function K(){_.firstElementChild||_.append(a(M));let t=_.firstElementChild,i=S?.bases.find(e=>e.id===H()),c=t.querySelector(`.lost`);if(c.hidden=!!i,c.textContent=S?`This workspace is not part of the net.`:`Waiting for the net…`,t.classList.toggle(`void`,!i),!i)return;let l=t.querySelector(`.keep`);u(l,i.mark),l.classList.toggle(`head`,i.head);let d=t.querySelector(`.tower`);d.classList.toggle(`ghost`,!i.tower),u(d,i.tower?.mark??`unknown`),e(d,i.tower?{view:`session`,ws:i.id,key:i.tower.key}:void 0),d.toggleAttribute(`hidden`,!1),d.toggleAttribute(`data-alert`,!!(i.tower&&o(i.tower.mark))),r(t,`h2`,i.label),r(t,`.title p`,i.tower?`control tower · ${s[i.tower.mark]}`:`no tower yet`);let f=i.tabs.flatMap(e=>e.units.flatMap(e=>[e,...e.subs]));[i.tabs.length,f.length,f.filter(e=>e.mark===`working`).length,f.filter(e=>e.mark===`waiting`).length].forEach((e,n)=>t.querySelectorAll(`dd`)[n].textContent=String(e));let p=(e,t)=>{u(e,t.mark),A(e.querySelector(`.token`),t,i.id);let a=e.querySelector(`.say`),o=C[t.key]??``;a.hidden=!o,r(a,`p`,o),n(e.querySelector(`.drones`),t.subs,k,(e,t)=>(e.classList.add(`drone`),A(e,t,i.id)))};n(t.querySelector(`.squads`),i.tabs,()=>a(`<section class="tab squad"><h3><i class="flag"></i><b></b><small></small></h3><div class="formation"></div></section>`),(e,t)=>{r(e,`h3 b`,`squad ${t.label}`);let i=t.units.flatMap(e=>[e,...e.subs]);r(e,`h3 small`,`${i.length} unit${i.length===1?``:`s`}`),u(e,[`waiting`,`error`,`working`].find(e=>i.some(t=>t.mark===e))??`idle`),n(e.querySelector(`.formation`),t.units,j,p)});let m=(e,r)=>{let a=t.querySelector(`[data-group="${e}"]`);a.hidden=!r.length,n(a.querySelector(`.formation`),r,k,(e,t)=>A(e,t,i.id))};m(`outposts`,i.outposts.map(e=>({id:e.id,name:e.label,role:`outpost`,mark:e.mark,note:e.mark===`unknown`?`no agent running`:void 0}))),m(`outside`,i.outside)}function q(){if(x.view!==`session`)return;b.firstElementChild||b.append(a(N));let e=b.firstElementChild,i=O(S,x.key),l=i?.u,d=l?.mark??`unknown`;u(e,d),t(e.querySelector(`.plate`),l?.role??I?.role??`member`,d),r(e,`h3`,l?.name??I?.name??x.key),r(e,`.role`,l?.role===`outside`?`unaligned`:l?.role??I?.role??`session`),r(e,`.state`,l?c(l.note,d):`not in the net`);let f=e.querySelector(`.d-note`),p=I?.state;f.hidden=p!==`recent`&&p!==`unknown`,f.replaceChildren(p===`unknown`?`Not in the net (it may have ended more than ten minutes ago).`:`No longer live; its history stays readable for ten minutes.`),I?.successor&&f.append(` Now: `,U({view:`session`,ws:H(),key:I.successor},O(S,I.successor)?.u.name??`the new session`));let g=[[`model`,L],[`context`,R?`${R.toLocaleString()} tokens`:void 0],[`directory`,I?.header?.cwd],[`base`,i&&U({view:`workspace`,ws:i.base.id},i.base.label)],[`squad`,i?.squad],[`launcher`,i?.launcher&&U({view:`session`,ws:i.base.id,key:i.launcher.key},i.launcher.name)],[`unreadable`,I?.unreadable?`${I.unreadable} entr${I.unreadable===1?`y`:`ies`}`:void 0]];e.querySelector(`.specs`).replaceChildren(...g.filter(([,e])=>e).map(([e,t])=>{let n=a(`<div><dt></dt><dd></dd></div>`);return n.firstElementChild.textContent=e,n.lastElementChild.append(t),n}));let _=e.querySelector(`.d-term`),y=x.key,C=l?.name??I?.name??y;[_.hidden,_.onclick]=[!l?.control,()=>m(y,C)];let T=e.querySelector(`.d-drones`);T.hidden=!l?.subs.length,n(T.querySelector(`.formation`),(l?.subs??[]).map(e=>({...e,id:e.key})),k,(e,t)=>(e.classList.add(`drone`),A(e,t,H())));let E=v.querySelector(`.comms-head`);if(E){r(E,`h2 span`,l?.name??I?.name??``),r(E,`p`,[i?.base.label,i?.squad&&`squad ${i.squad}`].filter(Boolean).join(` · `));let e=E.querySelector(`.chan`),t=p===`live`||!p&&l;e.dataset.state=t?`live`:`closed`,r(e,`b`,t?`channel open`:p===`recent`?`channel closed`:`no channel`)}w?.setLive(d===`working`?`${c(l.note,d)}`:o(d)?s[d]:``,d),B?.update({name:l?.name??I?.name??x.key,control:!!l?.control,live:p?p===`live`:!!l,mark:d,online:h(),asking:!!l?.pending,answerable:!!l?.pending?.answer}),V?.update(l?.pending,d===`waiting`&&!!l?.control&&h())}let J=()=>setTimeout(()=>x.view===`session`&&(document.activeElement??document.body)===document.body&&(matchMedia(`(pointer: fine)`).matches&&B?.ready()?B.box:w?.log)?.focus({preventScroll:!0}));async function Y(e){let t=++z,{mountChat:n}=await l(async()=>{let{mountChat:e}=await import(`./chat-DAQHdiSd.js`);return{mountChat:e}},__vite__mapDeps([0]),import.meta.url);if(t!==z||x.view!==`session`)return;v.replaceChildren(a(P));let r=()=>O(S,e)?.u??{key:e,name:I?.name??e,role:I?.role??`member`,mark:`unknown`};w=n(v.querySelector(`.chat-root`),i(e),{who:r,link:t=>{let n=O(S,e)?.u.subs.find(e=>e.name===t)??S?.bases.flatMap(e=>[...e.tabs.flatMap(e=>e.units),...e.outside]).find(e=>e.name===t);return n&&U({view:`session`,ws:O(S,n.key)?.base.id??H(),key:n.key},t)},openTile:f});let o=i(e).post??y(e);[B,V]=[E(v.querySelector(`.composer-root`),e,o),p(v.querySelector(`.ask-root`),o,void 0,()=>m(e,r().name))],w.reveal(x.at),T.splice(0).forEach(X),q(),J()}function X(e){w?.apply(e);for(let t of e.items??[])t.kind===`assistant`&&([L,R]=[t.model??L,t.ctx??R])}let Z=()=>(W(),x.view===`workspace`?K():q());return{locate:e=>O(S,e),show(e,t,n){let[r,i]=x.view===`session`?[x.key,x.at]:[];(e.view!==`workspace`||x.view!==`workspace`||e.ws!==x.ws)&&(C={}),x=e,_.hidden=e.view!==`workspace`,v.hidden=b.hidden=e.view!==`session`,e.view===`session`&&e.key!==r?([w,B,V,I,L,R,T,F]=[void 0,void 0,void 0,void 0,void 0,void 0,[],!1],v.replaceChildren(a(`<p class="lost">Opening the channel…</p>`)),Y(e.key)):e.view===`session`&&e.at!==i?(w?.reveal(e.at),J()):e.view!==`session`&&(z++,w=B=V=void 0),Z(),e!==t&&G(e,t,n)},net(e){S=e,x.view!==`map`&&Z()},cards(e){C={...C,...e},x.view===`workspace`&&K()},history(e){if(x.view===`session`&&e.key===x.key){if(I={...I,...e,items:void 0},e.reset&&([L,R]=[void 0,void 0]),document.hidden)return T.length<D?void T.push(e):void(F=!0);w?X(e):T.push(e),q()}},wake(){if(F)return F=!1,T=[],d();T.splice(0).forEach(e=>w&&X(e)),Z()}}}export{F as mountFocus};
|
package/dist/gui/index.html
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<meta name="referrer" content="no-referrer">
|
|
7
7
|
<title>Straw Net</title>
|
|
8
8
|
<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='8' fill='%230b1220'/%3E%3Cpath d='M16 5l9 7h-4v13h-10V12H7z' fill='%2338e0ff'/%3E%3C/svg%3E">
|
|
9
|
-
<script type="module" crossorigin src="./assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-CWA0eWOk.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="./assets/index-CHtZ727l.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<!-- Icons: game-icons.net, CC BY 3.0 (Lorc, Delapouite); see gui/ICONS.md -->
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-straw-boss",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Straw Boss coordination for pi in Herdr: sessions join a tab's net by choice, the first joiner is its main agent, a workspace tower opens on demand with a live monitoring dashboard, and boss_broadcast reaches the rest",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/ask.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { Answerable, Dialog } from "./owners.ts";
|
|
|
9
9
|
// Two tools are read: pi-ask-user's `ask_user` (one question with a context) and @juicesharp/rpiv-ask-user-question's `ask_user_question` (one to four questions
|
|
10
10
|
// with a header, options with descriptions, and a multi-select flag; no context). Older session files hold the first, new ones the second.
|
|
11
11
|
|
|
12
|
-
const TAIL = 262_144, LIMIT = 16_777_216, CACHE = new Map<string, { sig: string; pending?: Pending }>();
|
|
12
|
+
const TAIL = 262_144, LIMIT = 16_777_216, CACHE = new Map<string, { sig: string; pending?: Pending; secret: boolean }>();
|
|
13
13
|
export interface Option { title: string; description?: string }
|
|
14
14
|
/** `multiple`: it takes several choices; `freeform`: it takes the user's own words; `header`: the short tag ask_user_question gives a question. */
|
|
15
15
|
export interface Ask { question: string; header?: string; context?: string; options: Option[]; multiple: boolean; freeform: boolean }
|
|
@@ -93,13 +93,15 @@ export function questionsOf(a: Json): { ask: Ask } | { asks: Ask[] } | { why: st
|
|
|
93
93
|
export async function pendingOf(file: string): Promise<Pending | undefined> {
|
|
94
94
|
const s = await stat(file).catch(() => undefined), sig = s ? `${s.size}:${s.mtimeMs}` : "", hit = CACHE.get(file);
|
|
95
95
|
if (hit?.sig === sig) return hit.pending;
|
|
96
|
-
const found = s ? await openCalls(file).catch(() =>
|
|
96
|
+
const found = s ? await openCalls(file).catch(() => null) : [], calls = !found || found === "cut" ? [] : found, [c] = calls;
|
|
97
97
|
const shape = c?.name === "ask_user" ? askOf(c.args) : c?.name === "ask_user_question" ? questionsOf(c.args) : { why: c?.name === "secret_drop" ? "It waits for a secret." : "It waits on a dialog." };
|
|
98
98
|
const pending = found === "cut" ? { callId: `cut:${sig}`, tool: "dialog", why: "Its call is too long to read here." } : c && { callId: c.id, tool: c.name, ...(calls.length > 1 ? { why: SEVERAL } : shape) };
|
|
99
|
-
CACHE.set(file, { sig, pending });
|
|
99
|
+
CACHE.set(file, { sig, pending, secret: !found || found === "cut" || calls.some((x) => x.name === "secret_drop") }); // a file it could not read through counts as a secret
|
|
100
100
|
if (CACHE.size > 64) CACHE.delete(CACHE.keys().next().value!);
|
|
101
101
|
return pending;
|
|
102
102
|
}
|
|
103
|
+
/** Whether one of the file's open calls is secret_drop (or the file could not be read through; a missing one, a new session, is not): the terminal mirror hides, and the key pad sends only Escape, while it is. */
|
|
104
|
+
export const waitsForSecret = async (file: string) => (await pendingOf(file), CACHE.get(file)?.secret !== false); // dropped from the cache meanwhile: hidden until the next look
|
|
103
105
|
|
|
104
106
|
/** The dialogs bridges relay, by unit key, and how they join what the file says (dialogs.ts). */
|
|
105
107
|
export interface Relayed { dialogsOf(key: string): Dialog[] | undefined; merge(key: string, file: Pending | undefined, dialogs: Dialog[], readable: boolean): Pending }
|
package/src/bridge.ts
CHANGED
|
@@ -3,14 +3,17 @@ import { readFileSync, statSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { type Answer, type Bus, type Dialog, OWNERS, type OwnerAdapter, type Verdict } from "./owners.ts";
|
|
6
|
+
import { type Editor, probeEditor } from "./probe.ts";
|
|
6
7
|
|
|
7
8
|
// The bridge runs in every pi session inside Herdr, subagents included. It turns the owners' dialog events (owners.ts) into one list of open
|
|
8
9
|
// dialogs, relays it to the one running GUI server (gui.json in the state directory names its session G), and hands the server's answers to
|
|
9
|
-
// the owners.
|
|
10
|
+
// the owners. Of ctx.ui it uses only notices and the editor check (getEditorText), and it never judges an answer's meaning: the owner does. Transport is pi-intercom's extension channel
|
|
10
11
|
// straw-boss/gui/<sha256(G) prefix>, where only G is owner-eligible: dialogs and acks go to the owner, answers come from it (ownerOnly, and
|
|
11
12
|
// checked here against G and the owner's epoch); when G is this session the hub (dialogs.ts) is called in-process instead.
|
|
12
13
|
// Per dialog: open → settling (an answer is with the owner) → open again if refused, or closed by the owner's `closed`, the end of its tool
|
|
13
14
|
// call, or the session's end. An answer whose verdict does not come within 2 s leaves it `unknown`: later answers get `busy` until it closes.
|
|
15
|
+
// The bridge follows gui.json from the session's start (a stat every 2 s), so it is on G's channel whenever a server runs: the key pad's
|
|
16
|
+
// Enter guard asks it what the editor would submit (`probe`, probe.ts), and it answers with a class and at most a command name, never the draft.
|
|
14
17
|
|
|
15
18
|
export const REGISTER = "intercom:extension-register";
|
|
16
19
|
const VERDICT_MS = 2000, POLL_MS = 2000, KEEP_MS = 120_000, KEEP = 16, MAX_GUIS = 8, MAX_MESSAGE = 15 * 1024;
|
|
@@ -20,12 +23,13 @@ export interface Ack { version: 1; type: "ack"; bridgeId: string; id: string; an
|
|
|
20
23
|
/** `closed`: dialogs closed in the last 2 minutes (at most 16), so an answer that comes late hears who closed it, and a resent one that won, that it won. */
|
|
21
24
|
export interface Dialogs { version: 1; type: "dialogs"; bridgeId: string; pane: string; file: string | null; dialogs: Dialog[]; closed: Closed[] }
|
|
22
25
|
export interface Closed { id: string; by: string; answerId?: string }
|
|
23
|
-
export type
|
|
26
|
+
export interface ProbeResult extends Editor { version: 1; type: "probe-result"; bridgeId: string; probeId: string }
|
|
27
|
+
export type ToHub = Dialogs | Ack | ProbeResult;
|
|
24
28
|
export interface AnswerMessage extends Answer { version: 1; type: "answer"; to: string }
|
|
25
29
|
/** The hub when it runs in this session: it takes what a remote bridge would publish, from this session's id. */
|
|
26
30
|
export interface LocalHub { receive(from: string, msg: ToHub): void }
|
|
27
31
|
/** What the hub calls on the bridge of its own session. */
|
|
28
|
-
export interface Bridge { deliver(msg: AnswerMessage, reply: (ack: Ack) => void): void; sync(): void }
|
|
32
|
+
export interface Bridge { deliver(msg: AnswerMessage, reply: (ack: Ack) => void): void; sync(): void; probe(): Editor }
|
|
29
33
|
interface Channel { snapshot(): { connected: boolean; owner?: { sessionId: string; epoch: string } }; publish(payload: unknown, options?: { audience?: "owner" | "capable" }): void }
|
|
30
34
|
interface Rec { adapter: OwnerAdapter; d: Dialog; state: "open" | "settling" | "unknown" | "closed"; answerId?: string; accepted?: boolean; by?: string; at?: number; last?: Ack; waiting: ((ack: Ack) => void)[] }
|
|
31
35
|
type Event = { type: string; fromSessionId?: string; owner?: { sessionId: string; epoch: string }; payload?: Record<string, unknown> };
|
|
@@ -33,9 +37,10 @@ type Event = { type: string; fromSessionId?: string; owner?: { sessionId: string
|
|
|
33
37
|
export function startBridge(pi: ExtensionAPI, { env, dir, local }: { env: NodeJS.ProcessEnv; dir: string; local(): LocalHub | undefined }): Bridge {
|
|
34
38
|
const bus = pi.events as Bus, bridgeId = randomUUID(), recs = new Map<string, Rec>(), calls = new Map<string, { tool: string; signal?: AbortSignal; stop?: () => void }>();
|
|
35
39
|
const chans = new Map<string, Channel>(), me = () => env.PI_INTERCOM_SESSION_ID ?? "";
|
|
36
|
-
let g: string | undefined, seen: number | undefined, poll: ReturnType<typeof setInterval> | undefined, offs: (() => void)[] = [], warned = false;
|
|
37
|
-
let file: () => string | undefined = () => undefined, tell = (_text: string) => {};
|
|
40
|
+
let g: string | undefined, gpid = 0, seen: number | undefined, poll: ReturnType<typeof setInterval> | undefined, offs: (() => void)[] = [], warned = false;
|
|
41
|
+
let file: () => string | undefined = () => undefined, tell = (_text: string) => {}, editor = (): Editor => ({ editor: "unknown" });
|
|
38
42
|
const open = () => [...recs.values()].filter((r) => r.state !== "closed");
|
|
43
|
+
const alive = () => { try { return gpid > 0 && (process.kill(gpid, 0), true); } catch (e) { return (e as NodeJS.ErrnoException).code === "EPERM"; } }; // a server that died without removing gui.json gets no probe answers
|
|
39
44
|
|
|
40
45
|
// Where the dialogs go: the hub in this process while it runs here, else G's channel while G owns it.
|
|
41
46
|
const target = () => (local() ? me() : g);
|
|
@@ -52,13 +57,14 @@ export function startBridge(pi: ExtensionAPI, { env, dir, local }: { env: NodeJS
|
|
|
52
57
|
if (JSON.stringify(msg).length > MAX_MESSAGE) msg = { ...msg, dialogs: dialogs.map(({ select, questions, ...d }) => ({ ...d, big: true as const })) }; // shown read-only: too long to show
|
|
53
58
|
send(target(), msg);
|
|
54
59
|
};
|
|
55
|
-
// gui.json names the server's session; it is read at the
|
|
60
|
+
// gui.json names the server's session; it is read at the session's start and then on every mtime change, checked every 2 s (unref'd).
|
|
56
61
|
const follow = () => {
|
|
57
62
|
const at = statSync(join(dir, "gui.json"), { throwIfNoEntry: false })?.mtimeMs ?? 0;
|
|
58
63
|
if (at === seen) return false;
|
|
59
64
|
seen = at;
|
|
60
65
|
try {
|
|
61
|
-
|
|
66
|
+
const r = JSON.parse(readFileSync(join(dir, "gui.json"), "utf8")) as { pid?: number; ownerSession?: string };
|
|
67
|
+
[g, gpid] = [r.ownerSession || undefined, Number(r.pid) || 0];
|
|
62
68
|
} catch {
|
|
63
69
|
g = undefined;
|
|
64
70
|
}
|
|
@@ -77,19 +83,16 @@ export function startBridge(pi: ExtensionAPI, { env, dir, local }: { env: NodeJS
|
|
|
77
83
|
const onEvent = (from: string, ch: Channel, e: Event) => {
|
|
78
84
|
if (e.type === "owner" || e.type === "connection") return from === g && push();
|
|
79
85
|
const owner = ch.snapshot().owner, p = e.payload;
|
|
80
|
-
|
|
86
|
+
if (p?.type === "probe") follow(); // a probe goes only to the server gui.json names at this moment
|
|
87
|
+
// Only G's own session, as the broker's current owner of this channel and the server gui.json names now, may answer, probe, or ask for a sync.
|
|
81
88
|
if (e.type !== "message" || p?.version !== 1 || e.fromSessionId !== from || owner?.sessionId !== from || e.owner?.epoch !== owner.epoch) return;
|
|
82
89
|
if (p.type === "sync" && from === g) push();
|
|
83
90
|
// SAFETY: deliver checks the fields it uses before it hands anything to an owner.
|
|
84
91
|
if (p.type === "answer" && p.to === bridgeId && from === g) deliver(p as unknown as AnswerMessage, (ack) => send(from, ack));
|
|
85
|
-
|
|
86
|
-
const watch = () => {
|
|
87
|
-
if (open().length && !poll) (poll = setInterval(follow, POLL_MS)).unref?.();
|
|
88
|
-
else if (!open().length && poll) (clearInterval(poll), (poll = undefined));
|
|
92
|
+
if (p.type === "probe" && p.to === bridgeId && from === g && alive() && typeof p.probeId === "string" && p.probeId.length <= 64) send(from, { version: 1, type: "probe-result", bridgeId, probeId: p.probeId, ...editor() });
|
|
89
93
|
};
|
|
90
94
|
const changed = () => {
|
|
91
95
|
for (const [id, r] of recs) if (r.state === "closed" && (Date.now() - r.at! > KEEP_MS || recs.size > KEEP)) recs.delete(id);
|
|
92
|
-
watch();
|
|
93
96
|
if (!follow()) push();
|
|
94
97
|
};
|
|
95
98
|
|
|
@@ -145,7 +148,11 @@ export function startBridge(pi: ExtensionAPI, { env, dir, local }: { env: NodeJS
|
|
|
145
148
|
offs.forEach((off) => off());
|
|
146
149
|
file = () => ctx.sessionManager.getSessionFile();
|
|
147
150
|
tell = (text) => ctx.hasUI && ctx.ui.notify(text, "warning");
|
|
151
|
+
editor = () => probeEditor(ctx);
|
|
148
152
|
offs = OWNERS.map((o) => o.listen(bus, report));
|
|
153
|
+
seen = -1; // a new session file (/new, /resume) is told to the server at once
|
|
154
|
+
follow();
|
|
155
|
+
if (!poll) (poll = setInterval(follow, POLL_MS)).unref?.();
|
|
149
156
|
});
|
|
150
157
|
pi.on("tool_execution_start", (e, ctx) => {
|
|
151
158
|
const signal = ctx.signal, entry: { tool: string; signal?: AbortSignal; stop?: () => void } = { tool: e.toolName, signal };
|
|
@@ -166,5 +173,5 @@ export function startBridge(pi: ExtensionAPI, { env, dir, local }: { env: NodeJS
|
|
|
166
173
|
offs.splice(0).forEach((off) => off());
|
|
167
174
|
(clearInterval(poll), (poll = undefined));
|
|
168
175
|
});
|
|
169
|
-
return { deliver, sync: () => ((seen = -1), follow()) };
|
|
176
|
+
return { deliver, sync: () => ((seen = -1), follow()), probe: () => editor() };
|
|
170
177
|
}
|
package/src/control.ts
CHANGED
|
@@ -2,28 +2,29 @@ import { timingSafeEqual } from "node:crypto";
|
|
|
2
2
|
import type { IncomingMessage } from "node:http";
|
|
3
3
|
import { ANSWER_ID, checkAnswer, CONTROL, type Hub, type Sent, type Target } from "./dialogs.ts";
|
|
4
4
|
import { HerdrError, type HerdrCall, type Pane } from "./herdr.ts";
|
|
5
|
+
import { createKeys } from "./keys.ts";
|
|
6
|
+
import type { createMirrors } from "./mirror.ts";
|
|
5
7
|
import type { Found } from "./net.ts";
|
|
8
|
+
import { ENDING } from "./probe.ts";
|
|
6
9
|
|
|
7
10
|
// Typing into a session from the page, through Herdr's own input path only, as if typed in the terminal:
|
|
8
11
|
// POST /<token>/api/sessions/<key>/input {text, as: "send" | "followUp", confirmed?} the text, then Enter (or Alt+Enter while it works)
|
|
9
|
-
// POST /<token>/api/sessions/<key>/keys {
|
|
12
|
+
// POST /<token>/api/sessions/<key>/keys {key, stream, confirmed?} one key pad key under its mirror (keys.ts), or Stop's {keys: ["esc"]}
|
|
10
13
|
// POST /<token>/api/sessions/<key>/focus brings the session's pane forward in Herdr
|
|
11
14
|
// POST /<token>/api/sessions/<key>/answer {dialog, digest, answerId, choice | answers} answers its dialog through its bridge (dialogs.ts); no keys
|
|
15
|
+
// POST /<token>/api/sessions/<key>/mirror {stream, on} shows the pane's screen on that page's stream (mirror.ts), or stops
|
|
12
16
|
// POST /<token>/api/remote/pair a new one-time pairing URL and its QR, while remote access is on
|
|
13
17
|
// Every write needs the page's own origin (Origin, Sec-Fetch-Site), a JSON body of at most 64 KB, and the CSRF secret the server wrote into
|
|
14
18
|
// the page; the custom header forces a CORS preflight that is never approved. The key is resolved in the registry (paths and panes never come
|
|
15
19
|
// from the browser), and Herdr is asked right then that the pane still runs pi on that session's file, so a reused pane id is refused.
|
|
16
20
|
// One request per session is in flight at a time. Typed text is never logged; a Herdr timeout is not retried: it does not prove nothing arrived.
|
|
17
21
|
|
|
18
|
-
export const MAX_TEXT = 16_384, MAX_BODY = 65_536
|
|
19
|
-
|
|
20
|
-
export const KEYS = new Set(["esc"]);
|
|
21
|
-
/** Commands that end the session or switch its pane to another one: the page asks first, and the server refuses them unconfirmed. */
|
|
22
|
-
export const ENDING = /^\s*\/(quit|exit|new|resume|fork|clone|import|reload)(?=\s|$)/;
|
|
22
|
+
export const MAX_TEXT = 16_384, MAX_BODY = 65_536;
|
|
23
|
+
export { ENDING };
|
|
23
24
|
|
|
24
25
|
export interface Reply { status: number; body: unknown }
|
|
25
26
|
/** `answers.target` reads the net afresh, so the binding and the dialog are checked again for every answer. */
|
|
26
|
-
export interface ControlDeps { lookup(key: string): Found | undefined; call?: HerdrCall; csrf: string; canonical(file?: string): string | undefined; pair?(): { url: string; rows: string[] } | undefined; answers?: Pick<Hub, "replay" | "ended" | "send"> & { target(key: string): Promise<Target | undefined> } }
|
|
27
|
+
export interface ControlDeps { lookup(key: string): Found | undefined; call?: HerdrCall; csrf: string; canonical(file?: string): string | undefined; pair?(): { url: string; rows: string[] } | undefined; mirrors?: Pick<ReturnType<typeof createMirrors>, "toggle" | "fresh">; secret?(file: string): Promise<boolean>; answers?: Pick<Hub, "replay" | "ended" | "send" | "probe"> & { target(key: string): Promise<Target | undefined> } }
|
|
27
28
|
|
|
28
29
|
const no = (status: number, error: string, reason: string): Reply => ({ status, body: { error, reason } });
|
|
29
30
|
const same = (a: string, b: string) => a.length === b.length && timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
@@ -64,8 +65,9 @@ export function checkText(text: unknown): string | Reply {
|
|
|
64
65
|
return typed;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
export function createControl({ lookup, call, csrf, canonical, pair, answers }: ControlDeps) {
|
|
68
|
+
export function createControl({ lookup, call, csrf, canonical, pair, mirrors, secret = async () => true, answers }: ControlDeps) {
|
|
68
69
|
const busy = new Set<string>();
|
|
70
|
+
const pad = createKeys({ fresh: (id, key, pane) => mirrors?.fresh(id, key, pane) ?? false, secret, answerable: async (key) => Boolean(await answers?.target(key)), probe: async (key) => answers?.probe(key) });
|
|
69
71
|
/** The origin checks, then the session checks, then one Herdr call. `parts` is the path after api/: sessions/<key>/<action>, or remote/pair. */
|
|
70
72
|
return async function handle(req: IncomingMessage, origin: string, [scope, key = "", action = ""]: string[]): Promise<Reply> {
|
|
71
73
|
if (req.method !== "POST") return no(405, "method", "Use POST.");
|
|
@@ -79,6 +81,7 @@ export function createControl({ lookup, call, csrf, canonical, pair, answers }:
|
|
|
79
81
|
const paired = key === "pair" && !action ? pair?.() : undefined;
|
|
80
82
|
return paired ? { status: 200, body: paired } : key === "pair" && !action ? no(409, "remote_off", "Remote access is off; turn it on with /boss gui remote.") : no(404, "not_found", "No such action.");
|
|
81
83
|
}
|
|
84
|
+
if (action === "mirror") return mirrors ? mirrors.toggle(key, read.json) : no(503, "unavailable", "This server shows no terminals.");
|
|
82
85
|
if (!["input", "keys", "focus", "answer"].includes(action)) return no(404, "not_found", "No such action.");
|
|
83
86
|
const found = lookup(key);
|
|
84
87
|
if (!found) return no(404, "not_found", "No such session.");
|
|
@@ -100,13 +103,14 @@ export function createControl({ lookup, call, csrf, canonical, pair, answers }:
|
|
|
100
103
|
: call("agent.prompt", { target: pane.pane_id, text }, 15_000);
|
|
101
104
|
};
|
|
102
105
|
} else if (action === "keys") {
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
if (busy.has(key)) return no(429, "busy", "Another input to this session is still on its way.");
|
|
107
|
+
const asked = pad.check(key, json);
|
|
108
|
+
if ("status" in asked) return asked;
|
|
105
109
|
job = async () => {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
const plan = await pad.send(key, found, await live(found), asked);
|
|
111
|
+
if (asked.key === "enter") await live(found); // the editor check took up to a second: still pi, on this file
|
|
112
|
+
for (const [i, [method, params]] of plan.calls.entries()) await (i ? new Promise((r) => setTimeout(r, 60)) : Promise.resolve()).then(() => call(method, params, 15_000));
|
|
113
|
+
pad.sent(key, plan);
|
|
110
114
|
};
|
|
111
115
|
} else if (action === "answer") {
|
|
112
116
|
const { dialog, digest, answerId } = json;
|
package/src/dialogs.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { type Pending, type Relayed, SEVERAL } from "./ask.ts";
|
|
3
4
|
import { type Ack, type AnswerMessage, type Bridge, type Dialogs, guiNamespace, type LocalHub, REGISTER, type ToHub } from "./bridge.ts";
|
|
@@ -5,13 +6,15 @@ import type { Session } from "./herdr.ts";
|
|
|
5
6
|
import { canonical } from "./history.ts";
|
|
6
7
|
import type { Ref } from "./net.ts";
|
|
7
8
|
import { type Answerable, type Dialog, OWNERS, questionsDigest, type Reply, selectDigest } from "./owners.ts";
|
|
9
|
+
import { checkProbe, type Editor } from "./probe.ts";
|
|
8
10
|
|
|
9
11
|
// The hub, in the session that runs the GUI server: it takes the dialog lists bridges relay (bridge.ts), binds each to the unit on the
|
|
10
12
|
// bridge's pane, decides whether the page may answer it, and carries an answer to that bridge. A list counts only from the session intercom
|
|
11
13
|
// places on that pane, for the unit whose session file it names. The page may answer a dialog only when it is the session's one open dialog,
|
|
12
14
|
// with no answer on its way, belonging to the file's one open call (never a secret_drop), with a digest that matches its content.
|
|
15
|
+
// The same binding, for every bridge whether it holds a dialog or not, tells where to ask what a session's editor would submit (`probe`).
|
|
13
16
|
|
|
14
|
-
const ACK_MS = 5000, KEEP_MS = 600_000, KEEP = 256, MAX_TEXT = 8192, MAX_NOTE = 2048, MAX_ALL = 12_288;
|
|
17
|
+
const ACK_MS = 5000, PROBE_MS = 1000, KEEP_MS = 600_000, KEEP = 256, MAX_TEXT = 8192, MAX_NOTE = 2048, MAX_ALL = 12_288;
|
|
15
18
|
export const ANSWER_ID = /^[A-Za-z0-9_-]{16,64}$/, CONTROL = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/;
|
|
16
19
|
interface Channel { snapshot(): { connected: boolean; owner?: { sessionId: string; epoch: string } }; publish(payload: unknown, options?: { audience?: "owner" | "capable"; ownerOnly?: boolean }): void }
|
|
17
20
|
type Event = { type: string; fromSessionId?: string; sessionId?: string; owner?: { sessionId: string; epoch: string }; payload?: unknown };
|
|
@@ -52,8 +55,8 @@ export function checkAnswer(t: Answerable, json: Record<string, unknown>, remote
|
|
|
52
55
|
|
|
53
56
|
export function createHub(pi: ExtensionAPI, me: string, local: Bridge) {
|
|
54
57
|
let channel: Channel | undefined, epoch: string | undefined, active = false, change = () => {};
|
|
55
|
-
let bound = new Map<string, { from: string; b: Dialogs }>(), targets = new Map<string, Target>();
|
|
56
|
-
const bridges = new Map<string, Dialogs>(), waiting = new Map<string, (ack?: Ack) => void>(), done = new Map<string, { key: string; at: number; sent: Sent }>();
|
|
58
|
+
let bound = new Map<string, { from: string; b: Dialogs }>(), targets = new Map<string, Target>(), reach = new Map<string, { from: string; bridgeId: string }>();
|
|
59
|
+
const bridges = new Map<string, Dialogs>(), waiting = new Map<string, (ack?: Ack) => void>(), done = new Map<string, { key: string; at: number; sent: Sent }>(), probes = new Map<string, (e?: Editor) => void>();
|
|
57
60
|
const owner = () => channel?.snapshot().owner?.sessionId === me;
|
|
58
61
|
const sync = () => {
|
|
59
62
|
local.sync();
|
|
@@ -64,6 +67,7 @@ export function createHub(pi: ExtensionAPI, me: string, local: Bridge) {
|
|
|
64
67
|
const closed = msg.type === "dialogs" && Array.isArray(msg.closed) && msg.closed.length <= 16 && msg.closed.every((c) => str(c?.id) && str(c.by) && opt(c.answerId));
|
|
65
68
|
if (closed && str(msg.pane) && (msg.file === null || str(msg.file)) && Array.isArray(msg.dialogs) && msg.dialogs.every(valid)) (bridges.set(from, msg), change());
|
|
66
69
|
if (msg.type === "ack") waiting.get([from, msg.bridgeId, msg.id, msg.answerId].join("\n"))?.(msg);
|
|
70
|
+
if (msg.type === "probe-result") probes.get([from, msg.bridgeId, msg.probeId].join("\n"))?.(checkProbe({ ...msg }));
|
|
67
71
|
};
|
|
68
72
|
const onEvent = (e: Event) => {
|
|
69
73
|
if (e.type === "owner" && e.owner?.sessionId === me && e.owner.epoch !== epoch && (epoch = e.owner.epoch) && active) sync();
|
|
@@ -86,6 +90,7 @@ export function createHub(pi: ExtensionAPI, me: string, local: Bridge) {
|
|
|
86
90
|
const hub: LocalHub & {
|
|
87
91
|
readonly running: boolean; readonly me: string; start(onChange: () => void): void; stop(): void; relayed(sessions: Session[], refs: Map<string, Ref>): Relayed; target(key: string): Target | undefined;
|
|
88
92
|
replay(answerId: string, key: string): Sent | undefined; ended(key: string, dialog: string, answerId: string): Sent | undefined; send(key: string, t: Target, a: Omit<AnswerMessage, "version" | "type" | "to">): Promise<Sent>;
|
|
93
|
+
probe(key: string): Promise<Editor | undefined>;
|
|
89
94
|
} = {
|
|
90
95
|
receive,
|
|
91
96
|
get running() { return active; },
|
|
@@ -103,15 +108,16 @@ export function createHub(pi: ExtensionAPI, me: string, local: Bridge) {
|
|
|
103
108
|
},
|
|
104
109
|
stop() {
|
|
105
110
|
active = false;
|
|
106
|
-
[bridges, bound, targets].forEach((m) => m.clear());
|
|
107
|
-
[...waiting.values()].forEach((w) => w());
|
|
111
|
+
[bridges, bound, targets, reach].forEach((m) => m.clear());
|
|
112
|
+
[...waiting.values(), ...probes.values()].forEach((w) => w());
|
|
108
113
|
},
|
|
109
114
|
/** Binds each relayed list to a unit, from this net reading's sessions and registry. */
|
|
110
115
|
relayed(sessions, refs) {
|
|
111
|
-
[bound, targets] = [new Map(), new Map()];
|
|
116
|
+
[bound, targets, reach] = [new Map(), new Map(), new Map()];
|
|
112
117
|
for (const [from, b] of bridges) {
|
|
113
118
|
const file = sessions.find((s) => s.id === from)?.herdrLocation?.paneId === b.pane ? canonical(b.file ?? undefined) : undefined;
|
|
114
119
|
const ref = file && [...refs.values()].find((r) => r.pane === b.pane && r.file === file);
|
|
120
|
+
if (ref) reach.set(ref.key, { from, bridgeId: b.bridgeId });
|
|
115
121
|
if (ref && (b.dialogs.length || b.closed.length)) bound.set(ref.key, { from, b });
|
|
116
122
|
}
|
|
117
123
|
return { dialogsOf: (key) => bound.get(key)?.b.dialogs, merge };
|
|
@@ -148,6 +154,24 @@ export function createHub(pi: ExtensionAPI, me: string, local: Bridge) {
|
|
|
148
154
|
for (const [id, old] of done) if (done.size > KEEP || Date.now() - old.at > KEEP_MS) done.delete(id);
|
|
149
155
|
return sent;
|
|
150
156
|
},
|
|
157
|
+
/** What the session's editor would submit, from the bridge bound to its pane and file: undefined with no such bridge or no answer within 1 s. */
|
|
158
|
+
async probe(key) {
|
|
159
|
+
const t = reach.get(key);
|
|
160
|
+
if (!t || !active) return undefined;
|
|
161
|
+
if (t.from === me) return checkProbe({ ...local.probe() });
|
|
162
|
+
const probeId = randomUUID(), k = [t.from, t.bridgeId, probeId].join("\n");
|
|
163
|
+
const got = new Promise<Editor | undefined>((resolve) => {
|
|
164
|
+
const timer = setTimeout(() => settle(), PROBE_MS), settle = (e?: Editor) => (clearTimeout(timer), probes.delete(k), resolve(e));
|
|
165
|
+
probes.set(k, settle);
|
|
166
|
+
});
|
|
167
|
+
try {
|
|
168
|
+
if (!owner()) throw new Error("not the owner");
|
|
169
|
+
channel!.publish({ version: 1, type: "probe", to: t.bridgeId, probeId }, { audience: "capable", ownerOnly: true });
|
|
170
|
+
} catch {
|
|
171
|
+
probes.get(k)?.(); // unreachable: the editor stays unknown
|
|
172
|
+
}
|
|
173
|
+
return got;
|
|
174
|
+
},
|
|
151
175
|
};
|
|
152
176
|
return hub;
|
|
153
177
|
}
|
package/src/gui.ts
CHANGED
|
@@ -5,10 +5,12 @@ import { createServer, type IncomingMessage, request, type ServerResponse } from
|
|
|
5
5
|
import type { AddressInfo } from "node:net";
|
|
6
6
|
import { extname, join, resolve, sep } from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { waitsForSecret } from "./ask.ts";
|
|
8
9
|
import { createControl, type Reply } from "./control.ts";
|
|
9
10
|
import type { HerdrCall } from "./herdr.ts";
|
|
10
11
|
import type { Hub } from "./dialogs.ts";
|
|
11
12
|
import { canonical, readers } from "./history.ts";
|
|
13
|
+
import { createMirrors } from "./mirror.ts";
|
|
12
14
|
import type { Found, Net, Ref } from "./net.ts";
|
|
13
15
|
import { qr } from "./qr.ts";
|
|
14
16
|
import { type Access, cookieFor, createAccess } from "./remote.ts";
|
|
@@ -50,7 +52,8 @@ export async function startGui({ read, net, call, hub, dir = BUNDLE, port = 0, t
|
|
|
50
52
|
// Turning remote access off ends the streams of remote pages; their next request finds the cookie void.
|
|
51
53
|
const clients = new Map<ServerResponse, Watch | undefined>(), remotes = new Set<ServerResponse>(), access = createAccess(token, () => remotes.forEach((c) => c.end()));
|
|
52
54
|
const pair = (code = access.pair(), host = access.remote?.host, url = `https://${host}/${token}/pair/${code}`) => (code && host ? { url, rows: qr(url).map((row) => row.map(Number).join("")) } : undefined);
|
|
53
|
-
const
|
|
55
|
+
const mirrors = createMirrors({ call, lookup: (key) => net?.lookup(key), secret: waitsForSecret, canonical });
|
|
56
|
+
const control = createControl({ lookup: (key) => net?.lookup(key), call, csrf, canonical, pair, mirrors, secret: waitsForSecret, answers: hub && { replay: hub.replay, ended: hub.ended, send: hub.send, probe: hub.probe, target: async (key) => (await tick(), hub.target(key)) } });
|
|
54
57
|
let last: string | undefined, problem: string | undefined, inflight: Promise<void> | undefined;
|
|
55
58
|
let poll: ReturnType<typeof setInterval> | undefined, beat: ReturnType<typeof setInterval> | undefined;
|
|
56
59
|
const send = (to: ServerResponse, data: string, event?: string) => to.writableEnded || to.write(`${event ? `event: ${event}\n` : ""}data: ${data}\n\n`); // a reading in flight at stop ends after the streams
|
|
@@ -76,7 +79,10 @@ export async function startGui({ read, net, call, hub, dir = BUNDLE, port = 0, t
|
|
|
76
79
|
const events = (req: IncomingMessage, res: ServerResponse, target: string | null, remote: boolean) => {
|
|
77
80
|
res.writeHead(200, { ...HEADERS, "content-type": "text/event-stream", connection: "keep-alive" });
|
|
78
81
|
if (remote) remotes.add(res);
|
|
79
|
-
|
|
82
|
+
// `hello` names this stream for the terminal mirrors it attaches (mirror.ts), which end with it; before the net's first reading, after it.
|
|
83
|
+
const to = (event: string, data: unknown) => send(res, JSON.stringify(data), event), stream = randomBytes(16).toString("hex"), hello = () => to("hello", { stream });
|
|
84
|
+
mirrors.open(stream, to);
|
|
85
|
+
if (last) hello();
|
|
80
86
|
const watch = target?.startsWith("w:") ? watchWorkspace(files, target.slice(2), to) : target ? watchSession(files, target, to) : undefined;
|
|
81
87
|
clients.set(res, watch);
|
|
82
88
|
if (last) res.write(`data: ${last}\n\n`);
|
|
@@ -84,11 +90,13 @@ export async function startGui({ read, net, call, hub, dir = BUNDLE, port = 0, t
|
|
|
84
90
|
poll = setInterval(() => void tick(), pollMs).unref();
|
|
85
91
|
beat = setInterval(() => push("", "ping"), pingMs).unref();
|
|
86
92
|
}
|
|
87
|
-
// The registry is filled by reading the net, so a watched page starts once a reading exists.
|
|
88
|
-
|
|
93
|
+
// The registry is filled by reading the net, so a watched page starts once a reading exists, and an attach finds its session then.
|
|
94
|
+
const first = !last;
|
|
95
|
+
void (clients.size === 1 || first ? tick() : watch?.refresh())?.catch(() => {}).finally(() => first && hello());
|
|
89
96
|
req.on("close", () => {
|
|
90
97
|
clients.delete(res);
|
|
91
98
|
remotes.delete(res);
|
|
99
|
+
mirrors.close(stream);
|
|
92
100
|
watch?.stop();
|
|
93
101
|
if (!clients.size) (clearInterval(poll), clearInterval(beat));
|
|
94
102
|
});
|
|
@@ -140,6 +148,7 @@ export async function startGui({ read, net, call, hub, dir = BUNDLE, port = 0, t
|
|
|
140
148
|
url: `http://127.0.0.1:${bound}/${token}/`, port: bound, token, csrf, access,
|
|
141
149
|
stop: () => new Promise<void>((done) => {
|
|
142
150
|
hub?.stop();
|
|
151
|
+
mirrors.stop();
|
|
143
152
|
clearInterval(poll);
|
|
144
153
|
clearInterval(beat);
|
|
145
154
|
clients.forEach((_, c) => c.end());
|
package/src/keys.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Found } from "./net.ts";
|
|
2
|
+
import type { Editor } from "./probe.ts";
|
|
3
|
+
|
|
4
|
+
// The key pad's keys (POST …/sessions/<key>/keys {key, stream, confirmed?}), checked in order after the write guards and the registry:
|
|
5
|
+
// the key is on the pad and alone, at most 8 a second per session; the page's stream holds this session's mirror, on its pane, and got a
|
|
6
|
+
// frame in the last 3 s; while the session file's open call is `secret_drop` or the unit has an answerable dialog, only Escape; Enter asks
|
|
7
|
+
// the session's bridge what it would submit (probe.ts), checks the mirror, the secret, and the dialog again after the answer, and needs the
|
|
8
|
+
// confirm the server names for an ending command, another `/` or `!` command, or an editor it could not read. A second Escape within 1 s
|
|
9
|
+
// (from when Herdr took the first) to a session that neither works nor waits runs pi's double-Escape action, so it needs a confirm; once
|
|
10
|
+
// confirmed both Escapes go, as two writes 60 ms apart (one write of two ESC bytes reads as Ctrl+Alt+[ to a terminal without the kitty
|
|
11
|
+
// protocol, and pi pairs Escapes only within 500 ms), or one if the confirm came within 450 ms of the first, which pi pairs with it.
|
|
12
|
+
// Letters, `/`, Ctrl, and Alt are never sendable: not on the pad. Stop (`{keys: ["esc"]}`, the composer's) needs no mirror. Herdr 0.9 has no
|
|
13
|
+
// names for Home, End, Page Up, and Page Down: those go as the fixed sequences pi reads (pane.send_text writes them raw), and only they.
|
|
14
|
+
|
|
15
|
+
/** What the pad can send, as Herdr names the keys. */
|
|
16
|
+
export const PAD = ["esc", "enter", "up", "down", "left", "right", "tab", "shift+tab", "space", "backspace", "pageup", "pagedown", "home", "end", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
|
|
17
|
+
/** The pad keys Herdr cannot name, as the bytes a terminal sends for them. */
|
|
18
|
+
export const SEQUENCES: Record<string, string> = { home: "\x1b[H", end: "\x1b[F", pageup: "\x1b[5~", pagedown: "\x1b[6~" };
|
|
19
|
+
export const RATE = 8, RATE_MS = 1000, DOUBLE_MS = 1000, PAIR_MS = 450;
|
|
20
|
+
export interface Reply { status: number; body: unknown }
|
|
21
|
+
export interface KeyDeps { fresh(stream: string, key: string, pane: string): boolean; secret(file: string): Promise<boolean>; answerable(key: string): Promise<boolean>; probe(key: string): Promise<Editor | undefined>; now?(): number }
|
|
22
|
+
export interface Asked { key: string; stream?: string; confirmed?: unknown }
|
|
23
|
+
type Pane = { pane_id: string; agent_status?: string };
|
|
24
|
+
/** The keys to send, the Herdr calls that send them, and whether they are the confirmed double-Escape. */
|
|
25
|
+
export interface Plan { keys: string[]; calls: [string, Record<string, unknown>][]; double: boolean }
|
|
26
|
+
|
|
27
|
+
const no = (status: number, error: string, reason: string, extra = {}): Reply => ({ status, body: { error, reason, ...extra } });
|
|
28
|
+
const FOCUS = "Enter goes to whatever has focus in the terminal; confirm to send it.";
|
|
29
|
+
const ASK: Record<string, string> = {
|
|
30
|
+
unread: `The session's editor could not be read, so Enter might run a command that ends the session. ${FOCUS}`,
|
|
31
|
+
command: `The editor holds a slash command, which Enter may complete and run. ${FOCUS}`,
|
|
32
|
+
"double-esc": "A second Escape on an idle session runs pi's double-Escape action (tree or fork); confirm to run it.",
|
|
33
|
+
};
|
|
34
|
+
const confirm = (token: string) => no(409, "confirm", ASK[token] ?? `Enter would run /${token} (or the command picked in pi's menu, if it is open), which ends or replaces this session. ${FOCUS}`, { confirm: token });
|
|
35
|
+
|
|
36
|
+
export function createKeys({ fresh, secret, answerable, probe, now = Date.now }: KeyDeps) {
|
|
37
|
+
const sent = new Map<string, number[]>(), escaped = new Map<string, number>();
|
|
38
|
+
const guard = async (session: string, found: Found, pane: Pane, a: Asked) => {
|
|
39
|
+
if (a.stream !== undefined && !fresh(a.stream, session, pane.pane_id)) throw no(409, "no_mirror", "Keys are sent only while this page shows the session's terminal live; open it again.");
|
|
40
|
+
if (a.key !== "esc" && ((await secret(found.file!)) || (await answerable(session)))) throw no(409, "blocked", "While it waits on a secret or a question only Escape is sent from here; answer the question in its panel or the terminal.");
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
/** The body as one pad key (or Stop's Escape), or why not, before Herdr is asked; a request that passes counts toward the rate. */
|
|
44
|
+
check(session: string, json: Record<string, unknown>): Asked | Reply {
|
|
45
|
+
const stop = json.key === undefined && Array.isArray(json.keys) && json.keys.length === 1 && json.keys[0] === "esc";
|
|
46
|
+
if (!stop && (typeof json.key !== "string" || !PAD.includes(json.key) || json.keys !== undefined)) return no(400, "key", "Only one key from the pad can be sent at a time (never letters, /, Ctrl or Alt).");
|
|
47
|
+
if (!stop && (typeof json.stream !== "string" || !json.stream)) return no(400, "bad_request", "Name this page's stream; reload the page if it reconnected.");
|
|
48
|
+
const t = now(), recent = (sent.get(session) ?? []).filter((at) => t - at < RATE_MS);
|
|
49
|
+
if (recent.length >= RATE) return no(429, "rate", `At most ${RATE} keys a second reach a session.`);
|
|
50
|
+
for (const [s, ats] of sent) if (t - ats.at(-1)! >= RATE_MS) sent.delete(s); // sessions gone quiet leave nothing behind
|
|
51
|
+
for (const [s, at] of escaped) if (t - at >= 60_000) escaped.delete(s);
|
|
52
|
+
sent.set(session, [...recent, t]);
|
|
53
|
+
return stop ? { key: "esc" } : { key: json.key as string, stream: json.stream as string, confirmed: json.confirmed };
|
|
54
|
+
},
|
|
55
|
+
/** The guards that need the pane as Herdr has it now, then the Herdr calls that send the key, in order, 60 ms apart. */
|
|
56
|
+
async send(session: string, found: Found, pane: Pane, a: Asked): Promise<Plan> {
|
|
57
|
+
const { keys, double } = await this.keys(session, found, pane, a), text = SEQUENCES[keys[0]!];
|
|
58
|
+
return { keys, double, calls: text ? [["pane.send_text", { pane_id: pane.pane_id, text }]] : keys.map((k) => ["pane.send_input", { pane_id: pane.pane_id, keys: [k] }]) };
|
|
59
|
+
},
|
|
60
|
+
/** The guards, then the keys: one, or Escape twice for a confirmed double-Escape (`double`: this was the double, sent or paired). */
|
|
61
|
+
async keys(session: string, found: Found, pane: Pane, a: Asked): Promise<{ keys: string[]; double: boolean }> {
|
|
62
|
+
await guard(session, found, pane, a);
|
|
63
|
+
if (a.key === "enter") {
|
|
64
|
+
const e = await probe(session), need = !e || e.editor === "unknown" ? "unread" : (e.ending ?? (e.editor === "command" ? "command" : undefined));
|
|
65
|
+
await guard(session, found, pane, a); // the probe took up to a second: a secret or a dialog may have opened meanwhile
|
|
66
|
+
if (need && a.confirmed !== need) throw confirm(need);
|
|
67
|
+
}
|
|
68
|
+
if (a.key !== "esc") return { keys: [a.key], double: false };
|
|
69
|
+
const idle = pane.agent_status !== "working" && pane.agent_status !== "blocked", last = escaped.get(session) ?? -Infinity, t = now();
|
|
70
|
+
if (idle && t - last < DOUBLE_MS && a.confirmed !== "double-esc") throw confirm("double-esc");
|
|
71
|
+
if (!(idle && a.confirmed === "double-esc" && t - last < 60_000)) return { keys: ["esc"], double: false };
|
|
72
|
+
return { keys: t - last < PAIR_MS ? ["esc"] : ["esc", "esc"], double: true }; // confirmed so fast that pi pairs it with the Escape before
|
|
73
|
+
},
|
|
74
|
+
/** Herdr took the keys: an Escape counts from now; the double leaves none, so the next Escape starts afresh. */
|
|
75
|
+
sent(session: string, { keys, double }: Plan) {
|
|
76
|
+
if (double) escaped.delete(session);
|
|
77
|
+
else if (keys[0] === "esc") escaped.set(session, now());
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
package/src/mirror.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { HerdrCall, Pane } from "./herdr.ts";
|
|
2
|
+
import type { Found } from "./net.ts";
|
|
3
|
+
|
|
4
|
+
// The terminal mirror: what a session's pane shows, for the page's terminal tile. Herdr gives no change signal, so one reader per mirrored
|
|
5
|
+
// pane polls `pane.read` (visible, ansi) and sends only the rows that changed, on the streams of the pages that have the tile open: every
|
|
6
|
+
// 250 ms while the screen changed in the last 2 s, else every second, with every row again on attach, after a size change, and every 10 s.
|
|
7
|
+
// A stream asks for a session's mirror with POST …/mirror {stream, on}; the reads are shared by every stream on that session and stop when
|
|
8
|
+
// the last one leaves, when the unit leaves the registry or its pane runs something else, or after 10 failed reads in a row (`ended`).
|
|
9
|
+
// While the session file's open call is secret_drop no row is read, and the streams hear `hidden` instead. Rows are display data only:
|
|
10
|
+
// nothing here reads them for meaning, and the sanitizer keeps SGR (colours and attributes) and drops every other escape and control.
|
|
11
|
+
|
|
12
|
+
export const FAST_MS = 250, SLOW_MS = 1000, CALM_MS = 2000, WHOLE_MS = 10_000, SIZE_MS = 2000, FAILS = 10, MAX_ROWS = 120, MAX_ROW = 4096, PER_STREAM = 2, PANES = 6, FRESH_MS = 3000;
|
|
13
|
+
export type Send = (event: string, data: unknown) => void;
|
|
14
|
+
export type TimerId = ReturnType<typeof setTimeout> | number;
|
|
15
|
+
export interface Timer { now(): number; set(run: () => void, ms: number): TimerId; clear(id: TimerId | undefined): void }
|
|
16
|
+
export interface MirrorDeps { call?: HerdrCall; lookup(key: string): Found | undefined; secret(file: string): Promise<boolean>; canonical(file?: string): string | undefined; timer?: Timer }
|
|
17
|
+
interface Viewer { send: Send; at: number; whole: boolean }
|
|
18
|
+
interface Reader { key: string; pane: string; file: string; viewers: Map<string, Viewer>; rows: string[]; cols: number; seq: number; changed: number; whole: number; sized: number; fails: number; hidden: boolean; timer?: TimerId; gone: boolean }
|
|
19
|
+
|
|
20
|
+
// CSI (kept only as SGR: final m, no intermediates, digits and separators), OSC/DCS/SOS/PM/APC up to BEL or ST, any other escape, C0 and C1.
|
|
21
|
+
const ESCAPES = /\x1b(?:\[([0-?]*)([ -/]*)([@-~]|(?=\x1b)|$)|[\]PX^_][^]*?(?:\x07|\x1b\\|$)|[ -/]*[0-~]?)|[\u0000-\u001f\u007f-\u009f]/g;
|
|
22
|
+
/** One row as the page may draw it: SGR only, at most 4 KB, never ending inside an escape, and without trailing blanks in the terminal's
|
|
23
|
+
* own colours (after a reset or no SGR at all), which Herdr pads every row with: they show nothing and were two thirds of a frame. */
|
|
24
|
+
export function sanitize(row: string): string {
|
|
25
|
+
let kept = row.replace(ESCAPES, (all, params?: string, inter?: string, final?: string) => (final === "m" && !inter && /^[0-9;:]{0,64}$/.test(params!) ? all : ""));
|
|
26
|
+
const blank = / +$/.exec(kept), last = blank && kept.slice(0, blank.index).match(/\x1b\[[0-9;:]*m/g)?.at(-1);
|
|
27
|
+
if (blank && (!last || last === "\x1b[0m" || last === "\x1b[m")) kept = kept.slice(0, blank.index);
|
|
28
|
+
if (Buffer.byteLength(kept) <= MAX_ROW) return kept;
|
|
29
|
+
return Buffer.from(kept).subarray(0, MAX_ROW).toString("utf8").replace(/\ufffd$/, "").replace(/\x1b(\[[0-9;:]*)?$/, "");
|
|
30
|
+
}
|
|
31
|
+
const timers: Timer = { now: Date.now, set: (run, ms) => setTimeout(run, ms).unref(), clear: (id) => clearTimeout(id) };
|
|
32
|
+
const no = (status: number, error: string, reason: string) => ({ status, body: { error, reason } });
|
|
33
|
+
|
|
34
|
+
export function createMirrors({ call, lookup, secret, canonical, timer = timers }: MirrorDeps) {
|
|
35
|
+
const streams = new Map<string, { send: Send; keys: Set<string> }>(), readers = new Map<string, Reader>(), started = new Map<string, number>();
|
|
36
|
+
const frame = (r: Reader, set: [number, string][], whole?: true) => ({ key: r.key, seq: r.seq, rows: r.rows.length, cols: r.cols, ...(whole && { whole }), set });
|
|
37
|
+
const end = (r: Reader, reason: string) => {
|
|
38
|
+
if (r.gone) return;
|
|
39
|
+
r.gone = true;
|
|
40
|
+
timer.clear(r.timer);
|
|
41
|
+
readers.delete(r.key);
|
|
42
|
+
r.viewers.forEach((v, id) => void (streams.get(id)?.keys.delete(r.key), v.send("mirror", { key: r.key, ended: reason })));
|
|
43
|
+
r.viewers.clear(); // a read still on its way delivers to nobody
|
|
44
|
+
};
|
|
45
|
+
const detach = (id: string, key: string) => {
|
|
46
|
+
const r = readers.get(key);
|
|
47
|
+
streams.get(id)?.keys.delete(key);
|
|
48
|
+
if (!r?.viewers.delete(id) || r.viewers.size) return;
|
|
49
|
+
timer.clear(r.timer); // the last viewer left: no more reads
|
|
50
|
+
r.gone = true;
|
|
51
|
+
readers.delete(key);
|
|
52
|
+
};
|
|
53
|
+
/** What each viewer gets after a read: every row when it needs them, the changed ones, or an empty frame once a second so it knows it is current. */
|
|
54
|
+
const deliver = (r: Reader, changed: number[]) => {
|
|
55
|
+
const now = timer.now(), all = now - r.whole >= WHOLE_MS;
|
|
56
|
+
if (all) r.whole = now;
|
|
57
|
+
for (const v of r.viewers.values()) {
|
|
58
|
+
if (r.hidden) v.whole = true;
|
|
59
|
+
const whole = !r.hidden && (v.whole || all), set = whole ? r.rows.map((row, i): [number, string] => [i, row]) : changed.map((i): [number, string] => [i, r.rows[i]!]);
|
|
60
|
+
if (r.hidden ? now - v.at < SLOW_MS : !whole && !set.length && now - v.at < SLOW_MS) continue;
|
|
61
|
+
v.send("mirror", r.hidden ? { key: r.key, hidden: true } : frame(r, set, whole || undefined));
|
|
62
|
+
[v.at, v.whole] = [now, r.hidden];
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
async function read(r: Reader) {
|
|
66
|
+
const found = lookup(r.key);
|
|
67
|
+
if (!found?.live || found.pane !== r.pane || found.file !== r.file) return end(r, "The session is no longer live in this pane.");
|
|
68
|
+
const hidden = await secret(r.file);
|
|
69
|
+
if (r.gone) return; // detached meanwhile: no Herdr call for nobody
|
|
70
|
+
if ((r.hidden = hidden)) return (r.rows = []), deliver(r, []); // a secret is typed into that screen: nothing is read
|
|
71
|
+
if (timer.now() - r.sized >= SIZE_MS) {
|
|
72
|
+
const [{ pane }, { layout }] = (await Promise.all([call!("pane.get", { pane_id: r.pane }), call!("pane.layout", { pane_id: r.pane })])) as [{ pane: Pane }, { layout: { panes: { pane_id: string; rect: { width: number } }[] } }];
|
|
73
|
+
if (pane.agent !== "pi" || canonical(pane.agent_session?.value) !== r.file) return end(r, "The pane runs something else now.");
|
|
74
|
+
const cols = layout.panes.find((p) => p.pane_id === r.pane)?.rect.width ?? r.cols;
|
|
75
|
+
if (cols !== r.cols) [r.cols, r.whole] = [cols, 0];
|
|
76
|
+
r.sized = timer.now();
|
|
77
|
+
}
|
|
78
|
+
if (r.gone) return;
|
|
79
|
+
const { read } = (await call!("pane.read", { pane_id: r.pane, source: "visible", format: "ansi", strip_ansi: false })) as { read: { text: string } };
|
|
80
|
+
const rows = String(read.text).split("\n").slice(-MAX_ROWS).map(sanitize), changed = rows.flatMap((row, i) => (row !== r.rows[i] ? [i] : []));
|
|
81
|
+
if (rows.length !== r.rows.length) r.whole = 0; // a size change sends every row
|
|
82
|
+
if (changed.length) [r.seq, r.changed] = [r.seq + 1, timer.now()];
|
|
83
|
+
r.rows = rows;
|
|
84
|
+
deliver(r, changed);
|
|
85
|
+
}
|
|
86
|
+
const loop = async (r: Reader) => {
|
|
87
|
+
started.set(r.key, timer.now());
|
|
88
|
+
try {
|
|
89
|
+
await read(r);
|
|
90
|
+
r.fails = 0;
|
|
91
|
+
} catch {
|
|
92
|
+
if (++r.fails >= FAILS) end(r, "The pane could not be read.");
|
|
93
|
+
}
|
|
94
|
+
if (!r.gone) r.timer = timer.set(() => void loop(r), timer.now() - r.changed < CALM_MS ? FAST_MS : SLOW_MS);
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
/** A page's stream connected (`hello`), and may now attach mirrors. */
|
|
98
|
+
open: (id: string, send: Send) => void streams.set(id, { send, keys: new Set() }),
|
|
99
|
+
/** The stream closed: everything it held is detached. */
|
|
100
|
+
close(id: string) {
|
|
101
|
+
for (const key of streams.get(id)?.keys ?? []) detach(id, key);
|
|
102
|
+
streams.delete(id);
|
|
103
|
+
},
|
|
104
|
+
/** POST …/sessions/<key>/mirror {stream, on}: attach this stream to the session's mirror, or detach it. */
|
|
105
|
+
toggle(key: string, { stream, on }: Record<string, unknown>) {
|
|
106
|
+
const s = typeof stream === "string" ? streams.get(stream) : undefined;
|
|
107
|
+
if (!s || typeof on !== "boolean") return no(400, "bad_request", "Name this page's stream and whether to show the terminal; reload the page if it reconnected.");
|
|
108
|
+
if (!on) return detach(stream as string, key), { status: 200, body: { attached: false } };
|
|
109
|
+
const found = lookup(key);
|
|
110
|
+
if (!found?.live || !found.pane || !found.file) return no(409, "not_live", "This session is no longer live in a pane.");
|
|
111
|
+
if (!call) return no(503, "unavailable", "This server cannot reach Herdr.");
|
|
112
|
+
if (!s.keys.has(key) && s.keys.size >= PER_STREAM) return no(429, "too_many", `A page shows at most ${PER_STREAM} terminals; close one first.`);
|
|
113
|
+
const kept = readers.get(key), had = kept?.pane === found.pane && kept.file === found.file ? kept : (kept && end(kept, "The session moved to another pane."), undefined);
|
|
114
|
+
if (!had && readers.size >= PANES) return no(429, "too_many", `This server mirrors at most ${PANES} terminals at once; close one first.`);
|
|
115
|
+
const r: Reader = had ?? { key, pane: found.pane, file: found.file, viewers: new Map(), rows: [], cols: 0, seq: 0, changed: timer.now(), whole: 0, sized: 0, fails: 0, hidden: false, gone: false };
|
|
116
|
+
readers.set(key, r).get(key)!.viewers.set(stream as string, { send: s.send, at: 0, whole: true });
|
|
117
|
+
s.keys.add(key);
|
|
118
|
+
if (!had) r.timer = timer.set(() => void loop(r), Math.max(0, (started.get(key) ?? -FAST_MS) + FAST_MS - timer.now())); // one loop per reader; on and off again keeps 4 reads a second
|
|
119
|
+
else if (r.seq || r.hidden) deliver(r, []); // a page joining a running mirror gets every row at once
|
|
120
|
+
return { status: 200, body: { attached: true } };
|
|
121
|
+
},
|
|
122
|
+
/** Whether this stream holds the session's mirror, of this pane, and was sent a frame in the last 3 s (the key pad's precondition). */
|
|
123
|
+
fresh: (id: string, key: string, pane: string) => { const r = readers.get(key), v = r?.pane === pane ? r.viewers.get(id) : undefined; return Boolean(v && v.at && timer.now() - v.at <= FRESH_MS); },
|
|
124
|
+
/** The keys of the mirrors being read now. */
|
|
125
|
+
active: () => [...readers.keys()],
|
|
126
|
+
stop() {
|
|
127
|
+
readers.forEach((r) => void (timer.clear(r.timer), (r.gone = true)));
|
|
128
|
+
[readers, streams, started].forEach((m) => m.clear());
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
package/src/probe.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
// What Enter would submit from a session's own editor, for the key pad's Enter guard (keys.ts). The bridge (bridge.ts) answers the GUI
|
|
4
|
+
// server's probe with a class and at most a command name: the draft itself never leaves the session. The rule for which commands end or
|
|
5
|
+
// replace the session lives here, once, for the composer's typed text (control.ts) and the key pad's Enter alike.
|
|
6
|
+
|
|
7
|
+
/** Commands that end the session or switch its pane to another one: the page asks first, and the server refuses them unconfirmed. */
|
|
8
|
+
export const ENDING = /^\s*\/(quit|exit|new|resume|fork|clone|import|reload)(?=\s|$)/;
|
|
9
|
+
/** `command`: a draft that starts with `/` and is no ending command: with pi's command menu open (tab, backspace), Enter completes it and
|
|
10
|
+
* runs the result at once, so it may become any command, an ending one included. A `!` draft is one too: Enter runs it as a shell
|
|
11
|
+
* command, and `up` recalls earlier ones from history. */
|
|
12
|
+
export interface Editor { editor: "empty" | "text" | "command" | "unknown"; ending?: string }
|
|
13
|
+
const EDITORS = ["empty", "text", "command", "unknown"];
|
|
14
|
+
|
|
15
|
+
/** The session's editor as a class: empty, an ending command (by name), another slash command, other text, or unknown without a UI. */
|
|
16
|
+
export function probeEditor(ctx: Pick<ExtensionContext, "hasUI" | "ui"> | undefined): Editor {
|
|
17
|
+
let text: unknown;
|
|
18
|
+
try {
|
|
19
|
+
text = ctx?.hasUI ? ctx.ui.getEditorText() : undefined;
|
|
20
|
+
} catch {} // a UI that cannot say is unknown
|
|
21
|
+
if (typeof text !== "string") return { editor: "unknown" };
|
|
22
|
+
const ending = ENDING.exec(text)?.[1];
|
|
23
|
+
return !text.trim() ? { editor: "empty" } : ending ? { editor: "text", ending } : /^\s*[/!]/.test(text) ? { editor: "command" } : { editor: "text" };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A bridge's answer as the server takes it: one of the classes, and an ending only when it names one of the commands above. */
|
|
27
|
+
export function checkProbe(p: Record<string, unknown> | undefined): Editor | undefined {
|
|
28
|
+
if (!p || !EDITORS.includes(p.editor as string)) return undefined;
|
|
29
|
+
if (p.ending === undefined) return { editor: p.editor as Editor["editor"] };
|
|
30
|
+
return p.editor === "text" && typeof p.ending === "string" && ENDING.test(`/${p.ending}`) && /^[a-z]+$/.test(p.ending) ? { editor: "text", ending: p.ending } : undefined;
|
|
31
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{n as e}from"./post-B39ywXKd.js";var t=e=>e.multiple?`It takes several choices.`:e.freeform&&e.options.length?`It also takes your own words.`:``,n=(n,r)=>e(`div`,{class:`ask-one`},n.header&&e(`b`,{class:`ask-head`},n.header),e(`p`,{class:`ask-q`},n.question),n.context&&e(`div`,{class:`ask-ctx`},n.context),n.options.length>0&&e(`ol`,{class:`ask-opts`,"aria-label":`Options`},...n.options.map(t=>e(`li`,{class:`ask-opt`},e(`b`,null,t.title),t.description&&e(`small`,null,t.description)))),r&&t(n)&&e(`small`,{class:`ask-how`},t(n))),r=e=>e.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g,`�`),i=(e,t)=>(e.select?.choices??[]).map((e,t)=>({...e,i:t})).filter(e=>!(t&&e.localOnly)),a=(e,t,n)=>({...e.size?{choices:[...e].sort((e,t)=>e-t)}:{},...t.trim()?{text:t}:{},...n.trim()?{note:n}:{}}),o=(e,t,n)=>({dialog:e.dialog,digest:e.digest,answerId:n,...t}),s={digest:`The question changed; read it again.`,"not-allowed":`That answer does not fit the question.`,malformed:`The session could not read that answer.`};function c(e){let{outcome:t,reason:n,by:r}=e.body;return t===`accepted`?[`ok`,`Answered.`]:t===`stale`?[`error`,r===`local`?`It was already answered in the terminal.`:r===`cancel`?`It was dismissed in the terminal.`:`It is no longer open.`]:t===`rejected`?[`error`,s[String(n)]??`The session refused that answer.`]:t===`busy`?[`error`,`Another answer is on its way; check the terminal.`]:t===`unknown`||e.status===504?[`error`,`Not confirmed. Check the terminal.`,!0]:[`error`,n??`Refused (${e.status}).`]}var l=e=>[`accepted`,`stale`,`busy`,`unknown`].includes(String(e.body.outcome))||e.status===504,u=e=>e.answer?`d\n${e.answer.dialog}\n${e.answer.digest}`:`c\n${e.callId}\n${e.why??``}`;function d(s,d,f=location.hostname!==`127.0.0.1`){let p,m,h,g=!1,_=!1,v;for(let e of[`focusin`,`pointerdown`])document.addEventListener(e,e=>m&&!m.el.contains(e.target)&&(m.lost=void 0));let y=(e,t,n,...r)=>(e.dataset.tone=t,e.replaceChildren(n,...r)),b=()=>{if(!m)return;let e=document.activeElement;e&&e!==document.body&&e!==m.lost&&(m.lost=void 0),m.go.disabled=!g||_,m.controls.forEach(e=>e.disabled=!g||_||m.done),m.send&&(m.send.disabled=!g||_||m.done||!m.pick()),e&&m.el.contains(e)&&e.disabled&&(m.lost=e);let t=m.lost;t&&!t.disabled&&m.el.contains(t)&&(!document.activeElement||document.activeElement===document.body)&&(t.focus(),m.lost=void 0)};async function x(t,n){[t.sending,_,t.spell]=[!0,!0,!1],b(),y(t.note,``,`Sending…`);let r=await d(`answer`,n).catch(()=>void 0),i=e(`button`,{type:`button`,class:`cmp-link`},`Check again`);i.onclick=()=>void x(t,n);let[a,o,s]=r?c(r):[`error`,`Not confirmed. Check the terminal.`,!0];[t.done,t.sending,_,t.busy]=[!r||l(r),!1,!1,r?.body.outcome===`busy`],y(t.note,a,o,...s?[i]:[]),b(),t.note.scrollIntoView?.({block:`nearest`})}let S=(t,n,r,i,a)=>{let o=e(`button`,{type:`button`,class:`ask-opt`,role:t,"aria-checked":`false`},e(`b`,a?{"data-n":String(a)}:null,n),...r.filter(Boolean).map(t=>e(`small`,null,t)));return o.onclick=i,o};function C(t,n){let a,o=i(n,f).map(e=>S(`radio`,e.label,[e.note],()=>(a=e.i,o.forEach((t,r)=>t.setAttribute(`aria-checked`,String(i(n,f)[r].i===e.i))),b()),e.i+1)),s=n.select?.copy!==void 0&&e(`button`,{type:`button`,class:`cmp-link`},`Copy command`);return s&&(s.disabled=!navigator.clipboard,s.onclick=()=>void navigator.clipboard.writeText(n.select.copy).then(()=>y(t.note,`ok`,`Copied to this device's clipboard.`),()=>y(t.note,`error`,`The browser refused the clipboard.`))),t.pick=()=>a===void 0?void 0:{choice:a},t.controls=o,[e(`pre`,{class:`ask-title`},r(n.select.title)),e(`div`,{class:`ask-opts`,role:`radiogroup`,"aria-label":`Choices`},...o),e(`div`,{class:`ask-acts`},t.send,s,t.go)]}function w(t,n){let r=n.questions.map(()=>({picks:new Set,text:e(`input`,{type:`text`,maxlength:`8192`,class:`ask-own`,placeholder:`Or type your own answer`,"aria-label":`Your own answer`}),note:e(`input`,{type:`text`,maxlength:`2048`,class:`ask-own ask-memo`,placeholder:`Note (optional)`,"aria-label":`Note`})})),i=n.questions.map((n,i)=>{let a=r[i],o=n.multiple?`checkbox`:`radio`,s=n.options.map((e,t)=>S(o,e.title,[e.description,e.preview&&`It has a preview; see it in the terminal.`],()=>{n.multiple||(a.picks.clear(),a.text.value=``),a.picks.has(t)?a.picks.delete(t):a.picks.add(t),s.forEach((e,t)=>e.setAttribute(`aria-checked`,String(a.picks.has(t)))),b()}));return a.text.oninput=()=>(n.multiple||(a.picks.clear(),s.forEach(e=>e.setAttribute(`aria-checked`,`false`))),b()),t.controls.push(...s,a.text,a.note),e(`div`,{class:`ask-one`},n.header&&e(`b`,{class:`ask-head`},n.header),e(`p`,{class:`ask-q`},n.question),e(`div`,{class:`ask-opts`,role:n.multiple?`group`:`radiogroup`,"aria-label":`Options`},...s),a.text,a.note)});return t.pick=()=>r.every(e=>e.picks.size||e.text.value.trim())?{answers:r.map(e=>a(e.picks,e.text.value,e.note.value))}:void 0,[...i,e(`div`,{class:`ask-acts`},t.send,t.go)]}function T(r){let i=r.asks??(r.ask?[r.ask]:[]),a=i.length>1,s=r.answer,c={key:u(r),el:e(`section`,{class:`ask`}),note:e(`p`,{class:`ask-note`,role:`status`,"aria-live":`polite`}),go:e(`button`,{type:`button`,class:`cmp-link`},`Focus terminal`),controls:[],pick:()=>void 0,done:!1,sending:!1};c.go.onclick=async()=>{_=!0,b();let e=await d(`focus`,{}).catch(()=>void 0);y(c.note,e?.ok?``:`error`,e?.ok?``:e?.body.reason??`Herdr could not be reached.`),_=!1,b()},s&&((c.send=e(`button`,{type:`button`,class:`ask-send`},`Send answer`)).onclick=()=>void(c.pick()&&x(c,o(s,c.pick(),crypto.randomUUID()))));let l=e(`p`,{class:`ask-term`},r.why&&i.length?r.why:`${a?`Answer them`:`Answer it`} in the terminal. ${a||!i[0]?``:t(i[0])}`.trim());return c.el.append(...s?s.select?C(c,s):w(c,s):[...i.length?i.map(e=>n(e,a)):[e(`p`,{class:`ask-why`},r.why??`It waits on a dialog.`)],e(`div`,{class:`ask-acts`},l,c.go)],c.note),c}let E=(e,t)=>{let n=document.activeElement;m&&m!==t&&(m.focus=m.el.contains(n)?n:void 0),[p,m,h]=[e,t,t?.send?t:h],s.replaceChildren(...t?[t.el]:[]),b(),t?.focus&&(!document.activeElement||document.activeElement===document.body)&&t.focus.focus()};return{update(e,t){g=t,e&&(v=void clearTimeout(v)),!e&&(m?.done||m?.sending)?v??=setTimeout(()=>(v=void 0,E()),6e3):e?m&&(u(e)===m.key||!e.answer&&(m.done||m.sending)&&e.callId===p?.callId)?((m.busy||m.sending)&&!e.answer?m.spell=!0:m.busy&&m.spell&&e.answer&&([m.done,m.busy]=[!1,!1],y(m.note,``,`The other answer was not taken; this one can go.`)),p=e):E(e,h?.key===u(e)&&!h.done?h:T(e)):E(),b()}}}export{d as t};
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{a as e,c as t,d as n,f as r,i,l as a,m as o,o as s,s as c,u as l}from"./index-5qCqN1sA.js";import{t as u}from"./post-B39ywXKd.js";import{t as d}from"./ask-Dpo9zn5X.js";var f=(e,t)=>o(e.mark)-o(t.mark)||e.at-t.at,p=e=>{let t=e.filter(e=>o(e.mark)===0).length,n=e.length-t;return[t&&`${t} need${t===1?`s`:``} you`,n&&`${n} finished`].filter(Boolean).join(` · `)};function m(e){let t=[],n=[],r=()=>e.render(t.map(e=>e.alert),n),i=(i,a,o=!0)=>{let s=t.findIndex(e=>e.alert.key===i.key);if(s>=0&&!o)return;let c=s>=0?t.splice(s,1)[0]:{alert:i,back:a,raised:i.mark};!o&&t.length?t.splice(t.length-1,0,c):t.push(c),n=n.filter(e=>e.key!==i.key),r(),o&&e.enter(c.alert)};return{get open(){return t.map(e=>e.alert)},get queue(){return n},arrive(e){let i=t.find(t=>t.alert.key===e.key);if(i)[i.alert,i.raised]=[{...e,at:i.alert.at},e.mark];else{let t=n.find(t=>t.key===e.key);n=[...n.filter(t=>t.key!==e.key),t&&t.mark===e.mark?t:e].sort(f)}r()},prune(e){n=n.filter(t=>e(t.key)?.mark===t.mark);for(let n of t){let t=e(n.alert.key);n.alert=t?{...n.alert,...t,gone:void 0,at:n.alert.at}:{...n.alert,mark:`unknown`,note:void 0,pending:void 0,gone:!0}}r()},redraw:r,show:(t,n=!0)=>i(t,e.focused(),n),raise(n){let r=t.find(e=>e.alert.key===n);r&&r!==t.at(-1)&&i(r.alert,e.focused())},next(){n[0]&&i(n[0],e.focused())},close(a=`close`){let s=t.pop();if(!s)return!1;let c=t.at(-1),l=a===`close`?n.find(e=>o(e.mark)===0):void 0;return a===`later`&&s.alert.mark===s.raised&&(n=[...n,s.alert].sort(f)),!c&&l?(i(l,s.back),!0):(r(),!e.restore(s.back)&&c&&e.enter(c.alert),!0)}}}var h=`a, button, input, textarea, select, summary, label, [role=button]`;function g(e){let t=[],n,r=()=>e.render(t,n);return{get tiles(){return t},get active(){return n},open(i,a=!0){t.some(e=>e.id===i.id)||(t=[...t,i].slice(-4)),(a||!t.some(e=>e.id===n))&&(n=i.id),r(),a&&e.enter(t.find(e=>e.id===i.id))},close(e){let i=t.findIndex(t=>t.id===e);i<0||(t=t.filter(t=>t.id!==e),n===e&&(n=(t[i]??t[i-1])?.id),r())},activate(e){n=e,r()}}}function _(e){let t=new Set(e.flatMap(e=>e.kind===`result`&&e.callId?[e.callId]:[])),n=e.filter(e=>e.kind===`assistant`),r=n.at(-1),i=r?.tools?.find(e=>!t.has(e.id));return{said:(n.findLast(e=>e.text?.trim())?.text?.trim().split(`
|
|
2
|
-
`).filter(Boolean).at(-1))?.slice(0,280),pending:i&&`${i.name} · ${i.summary}`.slice(0,200),at:(r??e.findLast(e=>e.kind!==`result`))?.id}}var v=()=>a(`<section class="dialog" role="dialog" aria-modal="false" tabindex="-1"><header><b class="tag">alert</b><span class="where"></span><button class="x" type="button" aria-label="Close">×</button></header><div class="who">${i}<div><h3></h3><p><em class="role"></em> <span class="state"></span></p></div></div><div class="intel"><h4>Last transmission</h4><p class="said">…</p><p class="pending" hidden><b>waiting on</b> <span></span></p></div><div class="ask-root"></div><footer><a class="go">Open comms</a><button class="later" type="button">Later</button><span class="queued"></span></footer></section>`),y=()=>a(`<section class="tile" tabindex="-1"><header><svg><use href="#i-doc"/></svg><b></b><button class="x" type="button" aria-label="Close document">×</button></header><div class="tile-body"></div></section>`);function b(o){let[f,b,x,S,C]=[s(`dock`,HTMLElement),s(`work`,HTMLElement),s(`tiles`,HTMLElement),s(`tile-tabs`,HTMLElement),s(`alerts`,HTMLButtonElement)],w=new Map,T=(t,n,i=w.get(n.key))=>{let a=t.dataset.gone===`true`;r(t,`.said`,i?.failed?`History unavailable.`:i?.said??`No messages yet.`),t.querySelector(`.pending`).hidden=!i?.pending||a,r(t,`.pending span`,i?.pending??``),e(t.querySelector(`.go`),a?void 0:{view:`session`,ws:n.ws,key:n.key,...i?.at&&{at:i.at}})},E=(e,t={said:`…`})=>{w.set(e.key,t),o.recent(e.key).then(_,()=>({failed:!0})).then(n=>{let r=w.get(e.key)===t&&(w.set(e.key,n),f.querySelector(`.dialog[data-key="${CSS.escape(e.key)}"]`));r&&T(r,e)})},D=(e,n,i,a)=>{l(e,n.mark),e.dataset.gone=String(!!n.gone),e.style.transform=i?`translateY(${-12*i}px) scale(${1-.06*i})`:``,e.style.opacity=String(Math.max(0,1-.3*i)),e.toggleAttribute(`inert`,i>0),e.setAttribute(`aria-label`,`${n.name}: ${c(n.note,n.mark)}`),t(e.querySelector(`.plate`),n.role,n.mark),r(e,`h3`,n.name),r(e,`.role`,n.role===`outside`?`unaligned`:n.role),r(e,`.state`,n.gone?`left the net`:c(n.note,n.mark)),r(e,`.where`,o.label(n.ws)),r(e,`.tag`,n.gone?`gone`:{waiting:`question`,error:`error`,done:`finished`}[n.mark]??`update`),r(e,`.queued`,a.length?`Also queued: ${p(a)}`:``),w.has(n.key)||E(n),T(e,n);let s=O.get(e);s||O.set(e,s=d(e.querySelector(`.ask-root`),o.post?.(n.key)??((e,t)=>u(`sessions/${encodeURIComponent(n.key)}/${e}`,t)))),s.update(n.pending,n.mark===`waiting`&&document.body.dataset.stale!==`true`)},O=new WeakMap,k=m({render(e,o){n(f,e.map(e=>({...e,id:e.key})),v,(t,n)=>D(t,n,e.length-1-e.findIndex(e=>e.key===n.key),o));let s=o[0];C.disabled=!s,C.dataset.mark=s?.mark??`unknown`,r(C,`#alert-count`,String(o.length)),C.title=o.length?`Alerts: ${p(o)}`:``,r(C,`#alert-text`,s?`${s.name} · ${c(s.note,s.mark)}`:`no alerts`),n(C.querySelector(`.faces`),o.slice(0,3).map(e=>({...e,id:e.key})),()=>a(i),(e,n)=>t(e,n.role,n.mark))},enter:e=>f.querySelector(`[data-key="${CSS.escape(e.key)}"] .go`)?.focus(),focused:()=>document.activeElement instanceof HTMLElement&&document.activeElement!==document.body?document.activeElement:void 0,restore:e=>!!(e?.isConnected&&!e.closest(`[inert]`)&&(e.focus(),document.activeElement===e))});C.addEventListener(`click`,()=>k.next()),document.addEventListener(`click`,e=>{let t=t=>{let n=t.getBoundingClientRect();return e.clientX>=n.left&&e.clientX<=n.right&&e.clientY>=n.top&&e.clientY<=n.bottom},n=[...f.querySelectorAll(`.dialog:not(.leaving)`)],r=n.slice(0,-1).reverse().find(t);e.defaultPrevented||!r||!n.length||t(n.at(-1))||e.target?.closest?.(h)||(e.preventDefault(),e.stopPropagation(),k.raise(r.dataset.key))},!0),f.addEventListener(`click`,e=>{let t=e.target;t.closest(`.x`)?k.close():t.closest(`.later`)?k.close(`later`):t.closest(`.go`)&&queueMicrotask(()=>void k.close(`go`))});let A=0,j=()=>x.classList.toggle(`tabbed`,M.tiles.length>1&&b.clientWidth<460+M.tiles.length*400),M=g({render(e,t){x.hidden=!e.length,n(x,e,e=>{let t=y();return t.querySelector(`.tile-body`).append(e.body()),t.querySelector(`.x`).addEventListener(`click`,()=>M.close(e.id)),t},(e,n)=>{r(e,`header b`,n.title),e.classList.toggle(`active`,n.id===t)}),n(S,e,()=>a(`<button type="button" class="tab-btn"></button>`),(e,n)=>{e.textContent=n.title,e.classList.toggle(`active`,n.id===t),e.onclick=()=>M.activate(n.id)}),x.prepend(S),e.length!==A&&(A=e.length,j())},enter:e=>requestAnimationFrame(()=>x.querySelector(`.tile[data-key="${CSS.escape(e.id)}"]`)?.focus())});return new ResizeObserver(j).observe(b),{tiles:M,stack:k,alerts(e,t){for(let t of e)E(t),k.arrive(t);k.prune(e=>t.get(e))}}}export{b as mountPanels};
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./index-D8pxo5hc.css"])))=>i.map(i=>d[i]);
|
|
2
|
-
import{a as e,c as t,d as n,f as r,i,l as a,p as o,r as s,s as c,t as l,u}from"./index-5qCqN1sA.js";import{n as d,t as f}from"./post-B39ywXKd.js";import{t as p}from"./ask-Dpo9zn5X.js";var m=16384,h=`Not confirmed. Check the terminal before sending again.`,g=`The original already went to the session; what is here is your edited copy, unsent.`,_=/^\s*\/(quit|exit|new|resume|fork|clone|import|reload)(?=\s|$)/,v=new Map,y=e=>(t,n)=>f(`sessions/${encodeURIComponent(e)}/${t}`,n);function b(e,t,n){if(!e.ok)return[`error`,e.status===504?h:e.body.reason??`Refused (${e.status}).`];if(t&&!t.startsWith(n))return[`ok`,g]}var x=e=>{try{return e?.startsWith(`[`)?JSON.parse(e):e?[`error`,e]:void 0}catch{return[`error`,e]}},S=(e,t)=>e!==``&&!e.startsWith(t)?`Sent as it was; your edits stayed here, unsent.`:`Sent. It shows in the log once pi writes it.`,C=(e,t)=>({hidden:!e.live||!e.control||!!e.asking,disabled:t||!e.online}),w=(e,t)=>e.startsWith(t)?e.slice(t.length).replace(/^\n/,``):e;function T(e){if(!e.online)return`The stream is down; sending waits for it to come back.`;if(!e.live)return`This session is no longer live; its history stays readable.`;if(!e.control)return`No live pi pane backs this unit, so there is nothing to type into.`;if(e.mark===`waiting`)return e.answerable?`It waits on a dialog: answer it above, or in its terminal.`:`It waits on a dialog; answer it in its terminal.`}function E(e,t,n){let r=`sb-draft:${t}`,i=matchMedia(`(pointer: coarse)`).matches,a=d(`textarea`,{rows:`1`,maxlength:String(m),"aria-label":`Message`,enterkeyhint:i?`enter`:`send`}),o=d(`button`,{type:`submit`,class:`cmp-send`},`Send`),s=d(`button`,{type:`button`,class:`cmp-alt`,title:`Alt+Enter`},`Follow-up`),c=d(`button`,{type:`button`,class:`cmp-alt cmp-stop`},`Stop`),l=d(`p`,{class:`cmp-note`,role:`status`,"aria-live":`polite`}),u=d(`button`,{type:`button`,class:`cmp-link`},`Focus terminal`),f=d(`form`,{class:`composer`},d(`div`,{class:`cmp-row`},a,d(`div`,{class:`cmp-acts`},o,s,c)),d(`div`,{class:`cmp-foot`},l,u));e.replaceChildren(f);let p={name:``,control:!1,live:!1,mark:`unknown`,online:!1},g=!1,y=!1,E=e=>{try{e()}catch{}};E(()=>a.value=sessionStorage.getItem(r)??``);let D=()=>E(()=>a.value?sessionStorage.setItem(r,a.value):sessionStorage.removeItem(r)),O=()=>(a.style.height=`auto`,a.style.height=`${Math.min(a.scrollHeight,220)}px`),k=(e,t=``,...n)=>(l.dataset.tone=t,l.replaceChildren(e,...n)),A=(e,t,n)=>{let r=d(`button`,{type:`button`,class:`cmp-yes`},t),i=d(`button`,{type:`button`,class:`cmp-link`},`Cancel`);y=!0,r.onclick=()=>(y=!1,n()),i.onclick=()=>(y=!1,k(``),a.focus()),k(e,`ask`,r,i),r.focus()},[j,M]=[`${r}:note`,()=>{try{return sessionStorage.getItem(r)??``}catch{return}}],N=e=>E(()=>e?sessionStorage.setItem(j,JSON.stringify(e)):sessionStorage.removeItem(j));function P(e,i,a){let o={sent:a,answer:n(e,i)};return a!==void 0&&(N([`error`,h]),v.set(t,o),o.answer.then(e=>{let t=M();e.ok&&t!==void 0&&E(()=>sessionStorage.setItem(r,w(t,a))),N(b(e,t,a))},()=>N([`error`,h]))),F(o)}async function F(e,{sent:n}=e){g=!0,y=!1,L(),k(n===void 0?``:`Sending…`);try{let t=await e.answer;if(!f.isConnected)return;if(!t.ok)return t.status===504||N(),k(t.body.reason??`Refused (${t.status}).`,`error`);if(n===void 0)return k(``);N();let r=S(a.value,n);a.value=w(a.value,n),D(),O(),k(r,`ok`)}catch{k(h,`error`)}finally{v.get(t)===e&&v.delete(t),g=!1,L()}}function I(e,t){let n=a.value,r=T(p),i=t===n;if(g||y||!n.trim())return;if(r)return k(r,`error`);if(new TextEncoder().encode(n).length>16384)return k(`The text is longer than 16 KB; shorten it.`,`error`);let o=!i&&n.match(_)?.[0].trim();if(o)return A(`${o} ends this session or switches its pane to another one.`,`Send ${o}`,()=>I(e,n));P(`input`,{text:n,as:e,confirmed:i},n)}function L(){let e=T(p),t=p.mark===`working`;a.placeholder=e??`Message ${p.name} · ${i?``:`Enter sends, Shift+Enter adds a line · `}lands after any unsent text in its terminal`,o.disabled=g||!!e,s.hidden=c.hidden=!t||!!e,s.disabled=c.disabled=g,Object.assign(u,C(p,g))}f.addEventListener(`submit`,e=>(e.preventDefault(),I(`send`))),s.onclick=()=>I(`followUp`),c.onclick=()=>A(`Stop the current task? Queued messages go back to its terminal editor.`,`Stop`,()=>void(g||P(`keys`,{keys:[`esc`]}))),u.onclick=()=>void(g||P(`focus`,{})),a.addEventListener(`input`,()=>(D(),O())),f.addEventListener(`keydown`,e=>{e.key===`Escape`&&!e.isComposing&&(y||e.target===a&&a.value)&&(e.preventDefault(),y&&(y=!1,k(``),a.focus()))}),a.addEventListener(`keydown`,e=>{e.isComposing||e.keyCode===229||e.key!==`Enter`||e.shiftKey||i&&!e.altKey||(e.preventDefault(),I(e.altKey?`followUp`:`send`))});let R=window.visualViewport,z=()=>document.documentElement.style.setProperty(`--vvh`,`${R.height}px`);return R&&(R.addEventListener(`resize`,z),z()),O(),v.has(t)?F(v.get(t)):E(()=>{let e=x(sessionStorage.getItem(j));sessionStorage.removeItem(j),e&&k(e[1],e[0])}),{box:a,update:e=>(p=e,L()),ready:()=>!T(p)}}var D=200;function O(e,t){for(let n of e?.bases??[]){if(n.tower?.key===t)return{u:{key:t,name:n.tower.name,role:`tower`,mark:n.tower.mark,control:n.tower.control,pending:n.tower.pending,subs:[]},base:n};for(let[e,r]of[...n.tabs.map(e=>[e.label,e.units]),[void 0,n.outside]])for(let i of r){if(i.key===t)return{u:i,base:n,squad:e};let r=i.subs.find(e=>e.key===t);if(r)return{u:r,base:n,squad:e,launcher:i}}}}var k=()=>a(`<a class="token">${i}<span class="name"></span><span class="line"><em class="role"></em><span class="note"></span></span></a>`);function A(n,i,a){e(n,i.key?{view:`session`,ws:a,key:i.key}:void 0),n.toggleAttribute(`hidden`,!1),n.toggleAttribute(`data-alert`,!!i.key&&o(i.mark)),u(n,i.mark),t(n.querySelector(`.plate`),i.role,i.mark),r(n,`.name`,i.name),r(n,`.role`,i.role===`outside`?`unaligned`:i.role),r(n,`.note`,c(i.note,i.mark))}var j=()=>a(`<div class="col"><a class="token">${i}<span class="name"></span><span class="line"><em class="role"></em><span class="note"></span></span></a><div class="say"><p></p></div><div class="drones"></div></div>`),M=`<div class="zoom-in"><header class="base keep"><a class="tower big"><i class="sweep"></i><i class="pulse"></i><svg><use href="#i-tower"/></svg><b class="badge"></b></a><div class="title"><span class="crown"><svg><use href="#i-crown"/></svg>head tower</span><h2></h2><p></p></div><dl class="tally"><div><dt>Squads</dt><dd></dd></div><div><dt>Units</dt><dd></dd></div><div data-mark="working"><dt>Working</dt><dd></dd></div><div data-mark="waiting"><dt>Waiting</dt><dd></dd></div></dl></header><div class="squads"></div><div class="camps"><section class="tab squad camp" data-group="outposts"><h3><svg><use href="#i-outpost"/></svg><b>Outposts</b><small></small></h3><div class="formation"></div></section><section class="tab squad camp" data-group="outside"><h3><svg><use href="#i-outside"/></svg><b>Outside the net</b><small>unaligned pi panes</small></h3><div class="formation"></div></section></div><p class="lost" hidden></p></div>`,N=`<div class="dossier-in"><span class="tag">unit dossier</span><div class="portrait">${i}</div><h3></h3><p class="d-line"><em class="role"></em> <span class="state"></span></p><p class="d-note" hidden></p><dl class="specs"></dl><div class="d-drones" hidden><h4>Dispatched</h4><div class="formation"></div></div></div>`,P=`<div class="comms-in"><header class="comms-head"><span class="logo"><svg><use href="#i-comms"/></svg></span><div><h2><small>comms</small> <span></span></h2><p></p></div><span class="chan link" data-state="live"><i></i><b>channel open</b></span></header><div class="chat-root"></div><div class="ask-root"></div><div class="composer-root"></div></div>`;function F({data:i,restart:d,openTile:f,online:m}){let h=e=>document.getElementById(e),[g,_,v]=[`ws-view`,`session-view`,`dossier`].map(h),b={view:`map`},x,S={},C,w=[],T=!1,F,I,L,R=0,z,B,V=()=>b.view===`map`?``:b.ws,H=(t,n)=>{let r=document.createElement(`a`);return e(r,t),r.textContent=n,r};function U(){let t=(t,n,r)=>{let i=document.createElement(t);return i.textContent=n,r&&e(i,r),i},n=h(`crumbs`),r=b;if(r.view===`map`)return n.replaceChildren(t(`b`,`Straw Net`),t(`span`,`all bases`));let i=x?.bases.find(e=>e.id===r.ws)?.label??r.ws,a=r.view===`session`?O(x,r.key)?.u.name??F?.name??r.key:``;n.replaceChildren(t(`a`,`Straw Net`,{view:`map`}),r.view===`workspace`?t(`b`,i):t(`a`,i,{view:`workspace`,ws:r.ws}),...a?[t(`b`,a)]:[])}function W(e,t,n){let r=e=>[`map`,`workspace`,`session`].indexOf(e.view),i=r(e)<r(t)?`enter-back`:e.view===`workspace`&&n?`enter-zoom`:e.view===`session`&&t.view!==`session`?`enter-slide`:``,a=h({map:`map`,workspace:`ws-view`,session:`session-view`}[e.view]);if(i){if(n){let e=a.getBoundingClientRect();a.style.setProperty(`--ox`,`${Math.round(n.x+n.width/2-e.x)}px`),a.style.setProperty(`--oy`,`${Math.round(n.y+n.height/2-e.y)}px`)}a.classList.remove(`enter-zoom`,`enter-slide`,`enter-back`),a.offsetWidth,a.classList.add(i),setTimeout(()=>a.classList.remove(i),700)}}function G(){g.firstElementChild||g.append(a(M));let t=g.firstElementChild,i=x?.bases.find(e=>e.id===V()),c=t.querySelector(`.lost`);if(c.hidden=!!i,c.textContent=x?`This workspace is not part of the net.`:`Waiting for the net…`,t.classList.toggle(`void`,!i),!i)return;let l=t.querySelector(`.keep`);u(l,i.mark),l.classList.toggle(`head`,i.head);let d=t.querySelector(`.tower`);d.classList.toggle(`ghost`,!i.tower),u(d,i.tower?.mark??`unknown`),e(d,i.tower?{view:`session`,ws:i.id,key:i.tower.key}:void 0),d.toggleAttribute(`hidden`,!1),d.toggleAttribute(`data-alert`,!!(i.tower&&o(i.tower.mark))),r(t,`h2`,i.label),r(t,`.title p`,i.tower?`control tower · ${s[i.tower.mark]}`:`no tower yet`);let f=i.tabs.flatMap(e=>e.units.flatMap(e=>[e,...e.subs]));[i.tabs.length,f.length,f.filter(e=>e.mark===`working`).length,f.filter(e=>e.mark===`waiting`).length].forEach((e,n)=>t.querySelectorAll(`dd`)[n].textContent=String(e));let p=(e,t)=>{u(e,t.mark),A(e.querySelector(`.token`),t,i.id);let a=e.querySelector(`.say`),o=S[t.key]??``;a.hidden=!o,r(a,`p`,o),n(e.querySelector(`.drones`),t.subs,k,(e,t)=>(e.classList.add(`drone`),A(e,t,i.id)))};n(t.querySelector(`.squads`),i.tabs,()=>a(`<section class="tab squad"><h3><i class="flag"></i><b></b><small></small></h3><div class="formation"></div></section>`),(e,t)=>{r(e,`h3 b`,`squad ${t.label}`);let i=t.units.flatMap(e=>[e,...e.subs]);r(e,`h3 small`,`${i.length} unit${i.length===1?``:`s`}`),u(e,[`waiting`,`error`,`working`].find(e=>i.some(t=>t.mark===e))??`idle`),n(e.querySelector(`.formation`),t.units,j,p)});let m=(e,r)=>{let a=t.querySelector(`[data-group="${e}"]`);a.hidden=!r.length,n(a.querySelector(`.formation`),r,k,(e,t)=>A(e,t,i.id))};m(`outposts`,i.outposts.map(e=>({id:e.id,name:e.label,role:`outpost`,mark:e.mark,note:e.mark===`unknown`?`no agent running`:void 0}))),m(`outside`,i.outside)}function K(){if(b.view!==`session`)return;v.firstElementChild||v.append(a(N));let e=v.firstElementChild,i=O(x,b.key),l=i?.u,d=l?.mark??`unknown`;u(e,d),t(e.querySelector(`.plate`),l?.role??F?.role??`member`,d),r(e,`h3`,l?.name??F?.name??b.key),r(e,`.role`,l?.role===`outside`?`unaligned`:l?.role??F?.role??`session`),r(e,`.state`,l?c(l.note,d):`not in the net`);let f=e.querySelector(`.d-note`),p=F?.state;f.hidden=p!==`recent`&&p!==`unknown`,f.replaceChildren(p===`unknown`?`Not in the net (it may have ended more than ten minutes ago).`:`No longer live; its history stays readable for ten minutes.`),F?.successor&&f.append(` Now: `,H({view:`session`,ws:V(),key:F.successor},O(x,F.successor)?.u.name??`the new session`));let h=[[`model`,I],[`context`,L?`${L.toLocaleString()} tokens`:void 0],[`directory`,F?.header?.cwd],[`base`,i&&H({view:`workspace`,ws:i.base.id},i.base.label)],[`squad`,i?.squad],[`launcher`,i?.launcher&&H({view:`session`,ws:i.base.id,key:i.launcher.key},i.launcher.name)],[`unreadable`,F?.unreadable?`${F.unreadable} entr${F.unreadable===1?`y`:`ies`}`:void 0]];e.querySelector(`.specs`).replaceChildren(...h.filter(([,e])=>e).map(([e,t])=>{let n=a(`<div><dt></dt><dd></dd></div>`);return n.firstElementChild.textContent=e,n.lastElementChild.append(t),n}));let g=e.querySelector(`.d-drones`);g.hidden=!l?.subs.length,n(g.querySelector(`.formation`),(l?.subs??[]).map(e=>({...e,id:e.key})),k,(e,t)=>(e.classList.add(`drone`),A(e,t,V())));let y=_.querySelector(`.comms-head`);if(y){r(y,`h2 span`,l?.name??F?.name??``),r(y,`p`,[i?.base.label,i?.squad&&`squad ${i.squad}`].filter(Boolean).join(` · `));let e=y.querySelector(`.chan`),t=p===`live`||!p&&l;e.dataset.state=t?`live`:`closed`,r(e,`b`,t?`channel open`:p===`recent`?`channel closed`:`no channel`)}C?.setLive(d===`working`?`${c(l.note,d)}`:o(d)?s[d]:``,d),z?.update({name:l?.name??F?.name??b.key,control:!!l?.control,live:p?p===`live`:!!l,mark:d,online:m(),asking:!!l?.pending,answerable:!!l?.pending?.answer}),B?.update(l?.pending,d===`waiting`&&!!l?.control&&m())}let q=()=>setTimeout(()=>b.view===`session`&&(document.activeElement??document.body)===document.body&&(matchMedia(`(pointer: fine)`).matches&&z?.ready()?z.box:C?.log)?.focus({preventScroll:!0}));async function J(e){let t=++R,{mountChat:n}=await l(async()=>{let{mountChat:e}=await import(`./chat-BO71QXXf.js`);return{mountChat:e}},__vite__mapDeps([0]),import.meta.url);if(t!==R||b.view!==`session`)return;_.replaceChildren(a(P)),C=n(_.querySelector(`.chat-root`),i(e),{who:()=>O(x,e)?.u??{key:e,name:F?.name??e,role:F?.role??`member`,mark:`unknown`},link:t=>{let n=O(x,e)?.u.subs.find(e=>e.name===t)??x?.bases.flatMap(e=>[...e.tabs.flatMap(e=>e.units),...e.outside]).find(e=>e.name===t);return n&&H({view:`session`,ws:O(x,n.key)?.base.id??V(),key:n.key},t)},openTile:f});let r=i(e).post??y(e);[z,B]=[E(_.querySelector(`.composer-root`),e,r),p(_.querySelector(`.ask-root`),r)],C.reveal(b.at),w.splice(0).forEach(Y),K(),q()}function Y(e){C?.apply(e);for(let t of e.items??[])t.kind===`assistant`&&([I,L]=[t.model??I,t.ctx??L])}let X=()=>(U(),b.view===`workspace`?G():K());return{locate:e=>O(x,e),show(e,t,n){let[r,i]=b.view===`session`?[b.key,b.at]:[];(e.view!==`workspace`||b.view!==`workspace`||e.ws!==b.ws)&&(S={}),b=e,g.hidden=e.view!==`workspace`,_.hidden=v.hidden=e.view!==`session`,e.view===`session`&&e.key!==r?([C,z,B,F,I,L,w,T]=[void 0,void 0,void 0,void 0,void 0,void 0,[],!1],_.replaceChildren(a(`<p class="lost">Opening the channel…</p>`)),J(e.key)):e.view===`session`&&e.at!==i?(C?.reveal(e.at),q()):e.view!==`session`&&(R++,C=z=B=void 0),X(),e!==t&&W(e,t,n)},net(e){x=e,b.view!==`map`&&X()},cards(e){S={...S,...e},b.view===`workspace`&&G()},history(e){if(b.view===`session`&&e.key===b.key){if(F={...F,...e,items:void 0},e.reset&&([I,L]=[void 0,void 0]),document.hidden)return w.length<D?void w.push(e):void(T=!0);C?Y(e):w.push(e),K()}},wake(){if(T)return T=!1,w=[],d();w.splice(0).forEach(e=>C&&Y(e)),X()}}}export{F as mountFocus};
|