pi-straw-boss 0.4.0 → 0.5.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/NOTICE.md ADDED
@@ -0,0 +1,28 @@
1
+ # Notices
2
+
3
+ ## QR Code generator library (`src/qr.ts`)
4
+
5
+ Parts of `src/qr.ts` (the module count, the GF(256) multiply, the block interleave) are adapted from Project Nayuki's [QR Code generator library](https://www.nayuki.io/page/qr-code-generator-library).
6
+
7
+ Copyright (c) Project Nayuki. (MIT License)
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
10
+ this software and associated documentation files (the "Software"), to deal in
11
+ the Software without restriction, including without limitation the rights to
12
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
13
+ the Software, and to permit persons to whom the Software is furnished to do so,
14
+ subject to the following conditions:
15
+
16
+ - The above copyright notice and this permission notice shall be included in
17
+ all copies or substantial portions of the Software.
18
+ - The Software is provided "as is", without warranty of any kind, express or
19
+ implied, including but not limited to the warranties of merchantability,
20
+ fitness for a particular purpose and noninfringement. In no event shall the
21
+ authors or copyright holders be liable for any claim, damages or other
22
+ liability, whether in an action of contract, tort or otherwise, arising from,
23
+ out of or in connection with the Software or the use or other dealings in the
24
+ Software.
25
+
26
+ ## Icons
27
+
28
+ The browser view's icons are credited in [`gui/ICONS.md`](gui/ICONS.md).
package/README.md CHANGED
@@ -12,9 +12,9 @@
12
12
 
13
13
  Run a few agents on the same project and they start stepping on each other: two dev servers on one port, two branches editing the same migration, and no agent knows the others exist. pi-straw-boss lets them find each other and coordinate, without a scheduler or a new service.
14
14
 
15
- - **Opt-in net.** Each session is asked once whether its work shares resources or needs to collaborate. Only those that say yes join.
16
- - **A main agent per tab.** The first to join a tab coordinates it; `/boss` takes over.
17
- - **A tower per workspace.** It opens on demand in its own tab, pinned first, and coordinates the main agents and the other towers. You don't need to talk to it.
15
+ - **Everyone joins.** Each pi in Herdr joins its tab's net at start as a member; nothing to ask or remember.
16
+ - **A main agent per tab, by promotion.** An agent whose work grows calls `boss_promote`, or is promoted when it first dispatches a subagent. A tab has one main, and nobody takes it over except you with `/boss`.
17
+ - **A tower per workspace.** It opens with the workspace's first main in its own tab, pinned first, and coordinates the main agents and the other towers. You don't need to talk to it.
18
18
  - **A live dashboard** in the tower: who joined, and what they and their subagents are doing.
19
19
  - **A browser view** of the whole net, `/boss gui`: every workspace as a base with its tower, agents, and subagents, animated and read-only. Click a base to zoom in, click a unit to read its chat as a live comms log; units that wait on you queue up as alerts.
20
20
  - **Broadcast** to a tab, a workspace, every tower, or every session.
@@ -30,7 +30,7 @@ You need pi running inside Herdr 0.9.4 or newer, and [pi-intercom](https://www.n
30
30
 
31
31
  ## Quick start
32
32
 
33
- 1. Open pi in a Herdr tab and give it work. When the work needs to share or coordinate, the agent joins the net. You can also type `/boss`.
33
+ 1. Open pi in a Herdr tab and give it work. It joins the net by itself; when its work grows it promotes itself to the tab's main agent, which also opens the workspace tower. You can take over with `/boss`.
34
34
  2. Type `/boss status` to see each tab's main agent and the towers.
35
35
  3. Type `/boss tower` to open the workspace's tower and watch its dashboard. Agents open it themselves when they call it.
36
36
  4. Type `/boss gui` from any session to see the whole net in your browser.
@@ -39,13 +39,14 @@ You need pi running inside Herdr 0.9.4 or newer, and [pi-intercom](https://www.n
39
39
 
40
40
  | Name | What it does |
41
41
  |---|---|
42
- | `boss_join` | Join this tab's net. |
42
+ | `boss_promote` | Become this tab's main agent (refused while it has one). |
43
43
  | `boss_status` | Main agents, this workspace's tower, and the other towers. |
44
44
  | `call_tower` | Message this workspace's tower, or another workspace's. |
45
45
  | `boss_broadcast` | Message a tab, a workspace, all towers, or every session. |
46
- | `/boss` | Join and become this tab's main agent. |
46
+ | `/boss` | Take over as this tab's main agent. |
47
47
  | `/boss status`, `/boss tower` | Show the status; open and focus the tower. |
48
- | `/boss gui`, `/boss gui stop` | Open the net in the browser (any session, joins nothing); stop its server. |
48
+ | `/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; a pending `ask_user` question shows there and in its alert, to answer in the terminal. |
49
+ | `/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. |
49
50
 
50
51
  Subagents and sessions in Herdr worktree workspaces never join: the session that launched them speaks for them. Outside Herdr the extension does nothing.
51
52
 
@@ -0,0 +1 @@
1
+ import{n as e}from"./post-B39ywXKd.js";function t(t,n){let r,i=!1,a=!1,o=e(`p`,{class:`ask-note`,role:`status`,"aria-live":`polite`}),s=e(`button`,{type:`button`,class:`cmp-link`},`Focus terminal`),c=()=>s.disabled=!i||a;s.onclick=async()=>{a=!0,c();try{let e=await n();o.dataset.tone=e.ok?``:`error`,o.textContent=e.ok?``:e.body.reason??`Refused (${e.status}).`}catch{o.dataset.tone=`error`,o.textContent=`Herdr could not be reached.`}finally{a=!1,c()}};function l(n){o.textContent=``;let r=n.ask,i=r?.multiple?`It takes several choices.`:r?.freeform&&r.options.length?`It also takes your own words.`:``,a=r?.options.length?[e(`ol`,{class:`ask-opts`,"aria-label":`Options`},...r.options.map(t=>e(`li`,{class:`ask-opt`},e(`b`,null,t.title),...t.description?[e(`small`,null,t.description)]:[])))]:[],c=r?[e(`p`,{class:`ask-q`},r.question),...r.context?[e(`div`,{class:`ask-ctx`},r.context)]:[],...a]:[e(`p`,{class:`ask-why`},n.why??`It waits on a dialog.`)];t.replaceChildren(e(`section`,{class:`ask`},...c,e(`div`,{class:`ask-acts`},e(`p`,{class:`ask-term`},`Answer it in the terminal. ${i}`.trim()),s),o))}return{update(e,n){i=n,e&&e.callId!==r?l(e):!e&&r&&t.replaceChildren(),r=e?.callId,c()}}}export{t};
@@ -0,0 +1,7 @@
1
+ import{l as e,n as t}from"./index-Cn7_VOw9.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
+ `).split(`
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
+ `)],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(`
5
+ `),t+1)))}else if(i.includes(`|`)&&o!==void 0&&f.test(o)&&o.includes(`-`)){let t=[];for(e+=2;e<n.length&&n[e].trim()&&n[e].includes(`|`);e++)t.push(p(n[e]));r.push(a(`table`,[a(`thead`,[a(`tr`,p(i).map(e=>a(`th`,l(e))))]),a(`tbody`,t.map(e=>a(`tr`,e.map(e=>a(`td`,l(e))))))]))}else if(g){let i=g[1].length,o=[];for(;e<n.length;){let r=n[e].match(u);if(!r||r[1].length!==i||/\d/.test(r[2])!==/\d/.test(g[2]))break;let s=[r[3]],c=r[3].match(/^\[([ xX])\]\s+(.*)$/);for(c&&(s[0]=c[2]),e++;e<n.length&&n[e].trim()&&n[e].search(/\S/)>i;e++)s.push(n[e].replace(RegExp(`^ {0,${i+2}}`),``));let l=h(s.join(`
6
+ `),t+1),d=l[0],f=c?.[1]!==` `;c&&(typeof d==`object`&&d.tag===`p`?d.kids:l).unshift(a(`i`,f?[`✓`]:[],{class:`check`,"data-done":String(f),"aria-label":f?`done`:`not done`})),o.push(a(`li`,l,c?{class:`task`}:void 0)),!n[e]?.trim()&&n[e+1]?.match(u)?.[1]?.length===i&&e++}r.push(a(/\d/.test(g[2])?`ol`:`ul`,o))}else{let t=[i];for(e++;e<n.length&&n[e].trim()&&!m(n[e],n[e+1]);e++)t.push(n[e]);r.push(a(`p`,l(t.join(`
7
+ `))))}}return r}function g(e){let t=e=>{if(typeof e==`string`)return document.createTextNode(e);let n=document.createElement(e.tag);for(let[t,r]of Object.entries(e.attrs??{}))n.setAttribute(t,r);return n.append(...e.kids.map(t)),n},n=document.createDocumentFragment();return n.append(...e.map(t)),n}function _(e=600){let t=[],n,r=!1,i=new Set,a=new Map,o=new Map,s=e=>e.forEach(e=>(i.delete(e.id),e.tools?.forEach(e=>(o.delete(e.id),a.delete(e.id)))));function c(e,t){let n=[];for(let r of e)i.has(r.id)||(i.add(r.id),r.kind===`result`&&r.callId?(a.set(r.callId,r),o.has(r.callId)&&t.add(o.get(r.callId))):(r.tools?.forEach(e=>o.set(e.id,r.id)),n.push(r)));return n}let l=e=>({rebuild:!1,added:[],touched:[],dropped:[],front:!1,...e});return{get items(){return t},get before(){return n},get detached(){return r},result:e=>a.get(e),reset(e){return[t,n,r]=[[],e.before,!1],i.clear(),a.clear(),o.clear(),t=c(e.items,new Set),l({rebuild:!0,added:t})},append(i){if(r)return l({});let a=new Set,o=c(i,a);t.push(...o);let u=t.length>e?t.splice(0,t.length-e):[];return s(u),u.length&&(n=t[0].id),l({added:o,touched:[...a],dropped:u.map(e=>e.id)})},prepend(i){let a=new Set,o=c(i.items,a);t.unshift(...o),n=i.before;let u=t.length>e?t.splice(e):[];return s(u),u.length&&(r=!0),l({added:o,touched:[...a],dropped:u.map(e=>e.id),front:!0})}}}var v=e=>e?new Date(e).toLocaleTimeString([],{hour:`2-digit`,minute:`2-digit`,hour12:!1}):``,y=e=>e>=1024?`${(e/1024).toFixed(e>=10240?0:1)} KB`:`${e} B`,b={intercom_message:`message from`,subagent_result:`report from`,subagent_ping:`question from`,subagent_status:`status of`,"pi-loop-monitor":`monitor`,"memory-context":`memory`,"doc-review":`form answers`},x=n=>{let r=e(`<span class="ava"><svg><use/></svg></span>`);return r.querySelector(`use`).setAttribute(`href`,`#i-${t[n]??n}`),r};function S(e){let t=[],r=0;for(let i of e.matchAll(/https?:\/\/[^\s<>)\]]+/g))t.push(e.slice(r,i.index),n(`a`,{href:i[0],rel:`noopener noreferrer`,target:`_blank`},i[0])),r=i.index+i[0].length;return[...t,e.slice(r)]}function C(e,t,{who:a,link:o,openTile:s}){let c=_(),l=new Map,u=n(`div`,{class:`chat-list`}),d=n(`div`,{class:`chat-top`}),f=n(`div`,{class:`chat-live`}),p=n(`button`,{class:`chat-pill`,type:`button`,hidden:``}),m=n(`p`,{class:`chat-empty`},`No transmissions yet.`),C=n(`div`,{class:`chat-scroll`,tabindex:`-1`,"aria-label":`Comms log`},d,u,f);e.replaceChildren(C,p);let w=r(),T=!1,E=!1,D,O,k=0,A=()=>C.scrollHeight-C.scrollTop-C.clientHeight<80,j=(e=w.button(c.detached))=>(p.hidden=!e,p.textContent=e??``),M=()=>(w.jump(),O=void 0,C.scrollTop=C.scrollHeight,j());async function N(e){T=!0;let n=await i(c,t,e,e=>(z(e),B(e))).finally(()=>T=!1),r=l.get(e);if(D===e){if(!n||!r)return M();w.hold(),O=r,r.scrollIntoView({block:`center`}),r.classList.remove(`lit`),r.offsetWidth,r.classList.add(`lit`),clearTimeout(k),k=window.setTimeout(()=>(O=void 0,r.classList.remove(`lit`)),2e3),j()}}let P=()=>D?void N(D):M(),F=(e,n,r,i)=>{e.disabled=!0,t.entry(r).then(t=>{let r=t&&i(t);if(r===void 0)return void(e.textContent=`unavailable`);n.textContent=r,e.remove()},()=>e.textContent=`unavailable`)},I=e=>s({id:`doc:${e}`,title:e.split(`/`).pop()||e,body:()=>{let r=n(`div`,{class:`md`},n(`p`,{class:`chat-empty`},`Loading…`));return t.doc(e).then(e=>r.replaceChildren(e===void 0?`Unavailable.`:g(h(e)))),r}});function L(e){let r=c.result(e.id),i=n(`pre`,{class:`out`},r?.text??``),a=r?.more?n(`button`,{class:`more`,type:`button`},`show all (${y(r.bytes??0)})`):null;a?.addEventListener(`click`,()=>F(a,i,r.id,e=>e.text));let o=r?r.isError?`error`:`done`:`working`,s=e.name===`show_doc`&&t.doc?n(`button`,{class:`more open`,type:`button`},`open beside`):null;return s?.addEventListener(`click`,t=>(t.preventDefault(),e.summary.split(`, `).forEach(I))),n(`details`,{class:`act`},n(`summary`,{"data-mark":o},n(`b`,{class:`badge`}),n(`code`,null,e.name),n(`span`,{class:`sum`},e.summary),s),n(`pre`,{class:`args`},e.args??``),r&&n(`div`,{class:`result`},r.images?n(`em`,null,`${r.images} image(s)`):null,i,a))}function R(e){let t=n(`time`,null,v(e.at));switch(e.kind){case`user`:return n(`article`,{class:`tx you`},n(`header`,null,t,n(`b`,null,`you`),x(`user`)),n(`div`,{class:`bubble`},...S(e.text??``),e.images?n(`em`,null,` · ${e.images} image(s)`):null));case`assistant`:{let r=a(),i=n(`pre`,{class:`thought-text`}),o=e.thought?n(`button`,{class:`thought`,type:`button`},e.thought<0?`◇ thought · redacted`:`◇ thought · ${e.thought.toLocaleString()} chars`):null;return e.thought&&e.thought>0&&o?.addEventListener(`click`,()=>F(o,i,e.id,e=>e.thoughtText)),n(`article`,{class:`tx in`},x(r.role),n(`div`,{class:`tx-body`},n(`header`,null,n(`b`,null,r.name),e.model?n(`span`,null,e.model):null,t,e.stop?n(`span`,{class:`stop`,"data-mark":`error`},e.stop):null),o,i,e.text?n(`div`,{class:`md`},g(h(e.text))):null,e.error?n(`p`,{class:`fault`},e.error):null,...(e.tools??[]).map(L)))}case`bash`:return n(`article`,{class:`tx sys`},n(`details`,{class:`act`},n(`summary`,{"data-mark":e.exit?`error`:`done`},n(`b`,{class:`badge`}),n(`code`,null,`shell`),n(`span`,{class:`sum`},`! ${e.command}`),e.exit?n(`span`,{class:`stop`,"data-mark":`error`},`exit ${e.exit}`):null),e.text?n(`pre`,{class:`out`},e.text):null));case`notice`:return n(`article`,{class:`tx sys`},n(`details`,{class:`notice`},n(`summary`,null,x(`comms`),n(`em`,null,`incoming`),b[e.type??``]??e.type??`notice`,` `,e.from&&o(e.from)||(e.from??``),t),n(`pre`,null,e.text??``)));case`divider`:return n(`article`,{class:`tx rule`},n(`details`,null,n(`summary`,null,e.type===`compaction`?`context compacted · ${(e.tokens??0).toLocaleString()} tokens summarized`:`branch summary`),n(`pre`,null,e.text??``)));default:return n(`article`,{class:`tx note`},e.text??``,` `,t)}}function z(e){e.rebuild&&(l.clear(),u.replaceChildren());for(let t of e.dropped)l.get(t)?.remove(),l.delete(t);let t=e.added.map(e=>(l.set(e.id,R(e)),l.get(e.id)));e.front?u.prepend(...t):u.append(...t),m.remove(),c.items.length||u.append(m),d.hidden=!c.before}let B=e=>e.touched.forEach(e=>{let t=l.get(e),n=c.items.find(t=>t.id===e);if(t&&n){let r=R(n);t.replaceWith(r),l.set(e,r)}}),V=async()=>{if(!T&&c.before&&E){T=!0;try{let e=C.scrollHeight,n=c.prepend(await t.older(c.before));z(n),B(n),C.scrollTop+=C.scrollHeight-e}finally{T=!1}c.before&&C.clientHeight&&d.getBoundingClientRect().bottom>=C.getBoundingClientRect().top&&V()}};new IntersectionObserver(e=>e.some(e=>e.isIntersecting)&&void V(),{root:C}).observe(d),p.addEventListener(`click`,()=>{D=void 0,c.detached?t.latest().then(e=>(z(c.reset(e)),M())):M()}),C.addEventListener(`scroll`,()=>(w.scrolled(A()),j()),{passive:!0});for(let e of[`wheel`,`touchstart`,`pointerdown`,`keydown`])C.addEventListener(e,()=>O=D=void 0,{passive:!0});let H=new ResizeObserver(()=>w.pinned&&!c.detached?C.scrollTop=C.scrollHeight:O?.scrollIntoView({block:`center`}));return H.observe(u),H.observe(C),{log:C,apply(e){if(e.reset)z(c.reset({items:e.items??[],before:e.before})),E=!0,P();else if(e.items?.length){let t=c.append(e.items);z(t),B(t),w.arrived(t.added.length)&&!c.detached?M():j()}},reveal(e){D=e,E&&P()},setLive(e,t){f.dataset.mark=t,f.lastChild?.textContent!==e&&f.replaceChildren(...e?[n(`b`,{class:`badge`}),n(`span`,null,e)]:[])}}}export{C as mountChat};
@@ -0,0 +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}.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>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{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-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 small{color:var(--dim);font-size:12px}.ask-acts{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:6px 12px;display:flex}.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);font-size:12px}.ask-note[data-tone=error]{color:#ff8a9c}.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{margin-left:auto}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{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}}
@@ -0,0 +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-vksDW2C_.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-CuAcYh0i.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,focus:e=>Z&&(()=>Z.data(e).post(`focus`,{}))})),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);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};
@@ -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-Cn7_VOw9.js";import{t as u}from"./post-B39ywXKd.js";import{t as d}from"./ask-DD_oYNss.js";var f=(e,t)=>o(e.mark)-o(t.mark)||e.at-t.at;function p(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)n.alert={...n.alert,...e(n.alert.key),at:n.alert.at};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)}}}function m(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 h(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 g=()=>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>`),_=()=>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 v(o){let[f,v,y,b,x]=[s(`dock`,HTMLElement),s(`work`,HTMLElement),s(`tiles`,HTMLElement),s(`tile-tabs`,HTMLElement),s(`alerts`,HTMLButtonElement)],S=new Map,C=(t,n,i=S.get(n.key))=>{r(t,`.said`,i?.failed?`History unavailable.`:i?.said??`No messages yet.`),t.querySelector(`.pending`).hidden=!i?.pending,r(t,`.pending span`,i?.pending??``),e(t.querySelector(`.go`),{view:`session`,ws:n.ws,key:n.key,...i?.at&&{at:i.at}})},w=(e,t={said:`…`})=>{S.set(e.key,t),o.recent(e.key).then(h,()=>({failed:!0})).then(n=>{let r=S.get(e.key)===t&&(S.set(e.key,n),f.querySelector(`.dialog[data-key="${CSS.escape(e.key)}"]`));r&&C(r,e)})},T=(e,n,i,a)=>{l(e,n.mark),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`,c(n.note,n.mark)),r(e,`.where`,o.label(n.ws)),r(e,`.tag`,{waiting:`question`,error:`error`,done:`finished`}[n.mark]??`update`),r(e,`.queued`,a?`+${a} queued`:``),S.has(n.key)||w(n),C(e,n);let s=E.get(e);s||E.set(e,s=d(e.querySelector(`.ask-root`),o.focus?.(n.key)??(()=>u(`sessions/${encodeURIComponent(n.key)}/focus`,{})))),s.update(n.pending,n.mark===`waiting`&&document.body.dataset.stale!==`true`)},E=new WeakMap,D=p({render(e,o){n(f,e.map(e=>({...e,id:e.key})),g,(t,n)=>T(t,n,e.length-1-e.findIndex(e=>e.key===n.key),o.length));let s=o[0];x.disabled=!s,x.dataset.mark=s?.mark??`unknown`,r(x,`#alert-count`,String(o.length)),r(x,`#alert-text`,s?`${s.name} · ${c(s.note,s.mark)}`:`no alerts`),n(x.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))});x.addEventListener(`click`,()=>D.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.preventDefault(),e.stopPropagation(),D.raise(r.dataset.key))},!0),f.addEventListener(`click`,e=>{let t=e.target;t.closest(`.x`)?D.close():t.closest(`.later`)?D.close(`later`):t.closest(`.go`)&&queueMicrotask(()=>void D.close(`go`))});let O=0,k=()=>y.classList.toggle(`tabbed`,A.tiles.length>1&&v.clientWidth<460+A.tiles.length*400),A=m({render(e,t){y.hidden=!e.length,n(y,e,e=>{let t=_();return t.querySelector(`.tile-body`).append(e.body()),t.querySelector(`.x`).addEventListener(`click`,()=>A.close(e.id)),t},(e,n)=>{r(e,`header b`,n.title),e.classList.toggle(`active`,n.id===t)}),n(b,e,()=>a(`<button type="button" class="tab-btn"></button>`),(e,n)=>{e.textContent=n.title,e.classList.toggle(`active`,n.id===t),e.onclick=()=>A.activate(n.id)}),y.prepend(b),e.length!==O&&(O=e.length,k())},enter:e=>requestAnimationFrame(()=>y.querySelector(`.tile[data-key="${CSS.escape(e.id)}"]`)?.focus())});return new ResizeObserver(k).observe(v),{tiles:A,stack:D,alerts(e,t){for(let t of e)w(t),D.arrive(t);D.prune(e=>t.get(e))}}}export{v as mountPanels};
@@ -0,0 +1 @@
1
+ import{n as e,t}from"./post-B39ywXKd.js";var n=`http://www.w3.org/2000/svg`;function r(e){let t=e.length,r=document.createElementNS(n,`svg`),i=document.createElementNS(n,`rect`),a=document.createElementNS(n,`path`);r.setAttribute(`viewBox`,`-4 -4 ${t+8} ${t+8}`),r.setAttribute(`class`,`qr`),r.setAttribute(`role`,`img`),r.setAttribute(`aria-label`,`Pairing QR code`);for(let[e,n]of Object.entries({x:`-4`,y:`-4`,width:String(t+8),height:String(t+8),fill:`#fff`}))i.setAttribute(e,n);return a.setAttribute(`d`,e.flatMap((e,t)=>[...e].flatMap((e,n)=>e===`1`?[`M${n} ${t}h1v1h-1z`]:[])).join(``)),a.setAttribute(`fill`,`#06101e`),r.append(i,a),r}function i(){return{id:`pair`,title:`Pair a phone`,body:()=>{let n=e(`div`,{class:`pair`}),i=e(`button`,{type:`button`,class:`later`},`New code`),a=()=>{n.replaceChildren(e(`p`,{class:`chat-empty`},`Asking for a pairing code…`)),t(`remote/pair`,{}).then(t=>{let a=t.body.rows,o=String(t.body.url??``);if(!t.ok||!Array.isArray(a))return n.replaceChildren(e(`p`,null,t.body.reason??`Refused (${t.status}).`),i);n.replaceChildren(r(a.map(String)),e(`p`,null,`Scan it with a phone signed in to Tailscale as this machine's user. It works once, within ten minutes.`),e(`p`,null,e(`code`,null,new URL(o).host)),i)},()=>n.replaceChildren(e(`p`,null,`The server did not answer.`),i))};return i.onclick=a,a(),n}}}export{i as pairTile};
@@ -0,0 +1 @@
1
+ var e=(...e)=>e.filter(e=>e!==!1&&e!=null);function t(t,n,...r){let i=document.createElement(t);for(let[e,t]of Object.entries(n??{}))i.setAttribute(e,t);return i.append(...e(...r)),i}var n=(e,t)=>fetch(`api/${e}`,{method:`POST`,cache:`no-store`,headers:{"content-type":`application/json`,"x-sb-csrf":document.querySelector(`meta[name="sb-csrf"]`)?.content??``},body:JSON.stringify(t)}).then(async e=>({ok:e.ok,status:e.status,body:await e.json().catch(()=>({}))}));export{t as n,n as t};
@@ -0,0 +1,2 @@
1
+ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./index-5NVJByUz.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-Cn7_VOw9.js";import{n as d,t as f}from"./post-B39ywXKd.js";import{t as p}from"./ask-DD_oYNss.js";var m=16384,h=`Not confirmed. Check the terminal before sending again.`,g=/^\s*\/(quit|exit|new|resume|fork|clone|import|reload)(?=\s|$)/,_=new Map,v=e=>(t,n)=>f(`sessions/${encodeURIComponent(e)}/${t}`,n),y=(e,t)=>e.startsWith(t)?e.slice(t.length).replace(/^\n/,``):e;function b(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`It waits on a dialog; answer it in its terminal.`}function x(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},v=!1,x=!1,S=e=>{try{e()}catch{}};S(()=>a.value=sessionStorage.getItem(r)??``);let C=()=>S(()=>a.value?sessionStorage.setItem(r,a.value):sessionStorage.removeItem(r)),w=()=>(a.style.height=`auto`,a.style.height=`${Math.min(a.scrollHeight,220)}px`),T=(e,t=``,...n)=>(l.dataset.tone=t,l.replaceChildren(e,...n)),E=(e,t,n)=>{let r=d(`button`,{type:`button`,class:`cmp-yes`},t),i=d(`button`,{type:`button`,class:`cmp-link`},`Cancel`);x=!0,r.onclick=()=>(x=!1,n()),i.onclick=()=>(x=!1,T(``),a.focus()),T(e,`ask`,r,i),r.focus()};function D(e,i,a){let o={sent:a,answer:n(e,i)},s=e=>S(()=>e?sessionStorage.setItem(`${r}:note`,h):sessionStorage.removeItem(`${r}:note`));return a!==void 0&&(s(!0),_.set(t,o),o.answer.then(e=>(e.ok&&S(()=>sessionStorage.setItem(r,y(sessionStorage.getItem(r)??``,a))),s(e.status===504)),()=>s(!0))),O(o)}async function O(e,{sent:n}=e){v=!0,x=!1,A(),T(n===void 0?``:`Sending…`);try{let t=await e.answer;if(!f.isConnected)return;if(!t.ok)return T(t.body.reason??`Refused (${t.status}).`,`error`);if(n===void 0)return T(``);let r=!a.value.startsWith(n);a.value=y(a.value,n),C(),w(),T(r?`Sent as it was; your edits stayed here, unsent.`:`Sent. It shows in the log once pi writes it.`,`ok`)}catch{T(h,`error`)}finally{_.get(t)===e&&_.delete(t),v=!1,A()}}function k(e,t){let n=a.value,r=b(p),i=t===n;if(v||x||!n.trim())return;if(r)return T(r,`error`);if(new TextEncoder().encode(n).length>16384)return T(`The text is longer than 16 KB; shorten it.`,`error`);let o=!i&&n.match(g)?.[0].trim();if(o)return E(`${o} ends this session or switches its pane to another one.`,`Send ${o}`,()=>k(e,n));D(`input`,{text:n,as:e,confirmed:i},n)}function A(){let e=b(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=v||!!e,s.hidden=c.hidden=!t||!!e,s.disabled=c.disabled=u.disabled=v,u.hidden=!p.live||!p.control}f.addEventListener(`submit`,e=>(e.preventDefault(),k(`send`))),s.onclick=()=>k(`followUp`),c.onclick=()=>E(`Stop the current task? Queued messages go back to its terminal editor.`,`Stop`,()=>void(v||D(`keys`,{keys:[`esc`]}))),u.onclick=()=>void(v||D(`focus`,{})),a.addEventListener(`input`,()=>(C(),w())),f.addEventListener(`keydown`,e=>{e.key===`Escape`&&!e.isComposing&&(x||e.target===a&&a.value)&&(e.preventDefault(),x&&(x=!1,T(``),a.focus()))}),a.addEventListener(`keydown`,e=>{e.isComposing||e.keyCode===229||e.key!==`Enter`||e.shiftKey||i&&!e.altKey||(e.preventDefault(),k(e.altKey?`followUp`:`send`))});let j=window.visualViewport,M=()=>document.documentElement.style.setProperty(`--vvh`,`${j.height}px`);return j&&(j.addEventListener(`resize`,M),M()),w(),_.has(t)?O(_.get(t)):S(()=>(T(sessionStorage.getItem(`${r}:note`)??``,`error`),sessionStorage.removeItem(`${r}:note`))),{box:a,update:e=>(p=e,A()),ready:()=>!b(p)}}var S=200;function C(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 w=()=>a(`<a class="token">${i}<span class="name"></span><span class="line"><em class="role"></em><span class="note"></span></span></a>`);function T(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 E=()=>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>`),D=`<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>`,O=`<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>`,k=`<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 A({data:i,restart:d,openTile:f,online:m}){let h=e=>document.getElementById(e),[g,_,y]=[`ws-view`,`session-view`,`dossier`].map(h),b={view:`map`},A,j={},M,N=[],P=!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=A?.bases.find(e=>e.id===r.ws)?.label??r.ws,a=r.view===`session`?C(A,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(D));let t=g.firstElementChild,i=A?.bases.find(e=>e.id===V()),c=t.querySelector(`.lost`);if(c.hidden=!!i,c.textContent=A?`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),T(e.querySelector(`.token`),t,i.id);let a=e.querySelector(`.say`),o=j[t.key]??``;a.hidden=!o,r(a,`p`,o),n(e.querySelector(`.drones`),t.subs,w,(e,t)=>(e.classList.add(`drone`),T(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,E,p)});let m=(e,r)=>{let a=t.querySelector(`[data-group="${e}"]`);a.hidden=!r.length,n(a.querySelector(`.formation`),r,w,(e,t)=>T(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;y.firstElementChild||y.append(a(O));let e=y.firstElementChild,i=C(A,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},C(A,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})),w,(e,t)=>(e.classList.add(`drone`),T(e,t,V())));let v=_.querySelector(`.comms-head`);if(v){r(v,`h2 span`,l?.name??F?.name??``),r(v,`p`,[i?.base.label,i?.squad&&`squad ${i.squad}`].filter(Boolean).join(` · `));let e=v.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`)}M?.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()}),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:M?.log)?.focus({preventScroll:!0}));async function J(e){let t=++R,{mountChat:n}=await l(async()=>{let{mountChat:e}=await import(`./chat-tEF6NvU3.js`);return{mountChat:e}},__vite__mapDeps([0]),import.meta.url);if(t!==R||b.view!==`session`)return;_.replaceChildren(a(k)),M=n(_.querySelector(`.chat-root`),i(e),{who:()=>C(A,e)?.u??{key:e,name:F?.name??e,role:F?.role??`member`,mark:`unknown`},link:t=>{let n=C(A,e)?.u.subs.find(e=>e.name===t)??A?.bases.flatMap(e=>[...e.tabs.flatMap(e=>e.units),...e.outside]).find(e=>e.name===t);return n&&H({view:`session`,ws:C(A,n.key)?.base.id??V(),key:n.key},t)},openTile:f});let r=i(e).post??v(e);[z,B]=[x(_.querySelector(`.composer-root`),e,r),p(_.querySelector(`.ask-root`),()=>r(`focus`,{}))],M.reveal(b.at),N.splice(0).forEach(Y),K(),q()}function Y(e){M?.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=>C(A,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)&&(j={}),b=e,g.hidden=e.view!==`workspace`,_.hidden=y.hidden=e.view!==`session`,e.view===`session`&&e.key!==r?([M,z,B,F,I,L,N,P]=[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?(M?.reveal(e.at),q()):e.view!==`session`&&(R++,M=z=B=void 0),X(),e!==t&&W(e,t,n)},net(e){A=e,b.view!==`map`&&X()},cards(e){j={...j,...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 N.length<S?void N.push(e):void(P=!0);M?Y(e):N.push(e),K()}},wake(){if(P)return P=!1,N=[],d();N.splice(0).forEach(e=>M&&Y(e)),X()}}}export{A as mountFocus};
@@ -2,12 +2,12 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content">
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-DWQG2ADm.js"></script>
10
- <link rel="stylesheet" crossorigin href="./assets/index-xkoHrvS5.css">
9
+ <script type="module" crossorigin src="./assets/index-Cn7_VOw9.js"></script>
10
+ <link rel="stylesheet" crossorigin href="./assets/index-5NVJByUz.css">
11
11
  </head>
12
12
  <body>
13
13
  <!-- Icons: game-icons.net, CC BY 3.0 (Lorc, Delapouite); see gui/ICONS.md -->
@@ -27,7 +27,7 @@
27
27
  <header class="hud">
28
28
  <a class="brand" href="./" data-route="" aria-label="Straw Net: all bases">
29
29
  <span class="logo"><svg><use href="#i-tower"/></svg></span>
30
- <div><h1>Straw Net</h1><p>read-only live view</p></div>
30
+ <div><h1>Straw Net</h1><p>live net · comms</p></div>
31
31
  </a>
32
32
  <dl class="stats" aria-label="Net totals">
33
33
  <div><dt>Bases</dt><dd id="s-bases">0</dd></div>
@@ -36,6 +36,7 @@
36
36
  <div data-mark="waiting"><dt>Waiting</dt><dd id="s-waiting">0</dd></div>
37
37
  </dl>
38
38
  <button class="alerts" id="alerts" type="button" data-mark="unknown" aria-label="Alerts" disabled><span class="faces" id="alert-faces"></span><span class="count"><b id="alert-count">0</b><small id="alert-text">no alerts</small></span></button>
39
+ <button class="pair-btn" id="pair" type="button" title="Pair a phone over Tailscale (/boss gui remote turns remote access on)">Phone</button>
39
40
  <div class="link" id="link" data-state="connecting"><i></i><span id="link-text">connecting</span></div>
40
41
  </header>
41
42
  <div class="banner" id="banner" role="status" hidden></div>
@@ -53,7 +54,7 @@
53
54
  </div>
54
55
  <aside class="side">
55
56
  <section class="panel dossier" id="dossier" hidden></section>
56
- <section class="panel"><h2>Net log</h2><ol class="log" id="log"></ol></section>
57
+ <details class="panel net-log" open><summary><h2>Net log</h2></summary><ol class="log" id="log"></ol></details>
57
58
  <section class="panel" id="legend"><h2>States</h2>
58
59
  <ul class="legend">
59
60
  <li data-mark="working"><b class="badge"></b>working</li>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-straw-boss",
3
- "version": "0.4.0",
3
+ "version": "0.5.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",
@@ -24,6 +24,7 @@
24
24
  "dist/gui",
25
25
  "prompts",
26
26
  "README.md",
27
+ "NOTICE.md",
27
28
  "LICENSE"
28
29
  ],
29
30
  "pi": {
@@ -49,6 +50,7 @@
49
50
  "@earendil-works/pi-coding-agent": "1.3.0",
50
51
  "@earendil-works/pi-tui": "1.3.0",
51
52
  "@types/node": "26.6.5",
53
+ "jsqr": "1.4.0",
52
54
  "typebox": "1.3.27",
53
55
  "typescript": "7.0.2",
54
56
  "vite": "8.3.4"
@@ -0,0 +1 @@
1
+ Straw Boss: you are a member of tab {{tab}}, which has no main agent yet. When your work grows, meaning several steps, dispatching workers, or touching more than one app, call `boss_promote` to become the tab's main agent; dispatching a subagent promotes you automatically. Report coordination friction, such as an agent, endpoint, or shared resource you cannot reach, with call_tower.
package/prompts/member.md CHANGED
@@ -1 +1 @@
1
- Straw Boss: you joined the net of tab {{tab}}, whose main agent is {{main}}. The tower of workspace {{workspace}} is {{tower}}. Take shared-resource and collaboration questions to the main agent through intercom. Report coordination friction, such as an agent, endpoint, or shared resource you cannot reach, with call_tower.
1
+ Straw Boss: you are a member of tab {{tab}}, whose main agent is {{main}}. The tower of workspace {{workspace}} is {{tower}}. Before you dispatch a subagent, tell {{main}} through intercom what you will do and which resources (ports, databases, files, branches) it uses; take other shared-resource questions there too. Report coordination friction, such as an agent, endpoint, or shared resource you cannot reach, with call_tower.
package/src/ask.ts ADDED
@@ -0,0 +1,76 @@
1
+ import { open, stat } from "node:fs/promises";
2
+ import { type Json, parse } from "./items.ts";
3
+ import type { Net } from "./net.ts";
4
+
5
+ // What a waiting session waits on, for display only: its newest assistant message's one tool call with no result yet, read from the end of
6
+ // its session file. The snapshot carries it so the alert and the session view can show an ask_user question (its context and options);
7
+ // the user answers it in the terminal. Nothing here sends anything: answering from the browser is deferred (see the spec, "Pending dialogs").
8
+
9
+ const TAIL = 262_144, CACHE = new Map<string, { sig: string; pending?: Pending }>();
10
+ export interface Option { title: string; description?: string }
11
+ /** `multiple`: it takes several choices; `freeform`: it takes the user's own words. */
12
+ export interface Ask { question: string; context?: string; options: Option[]; multiple: boolean; freeform: boolean }
13
+ /** What a waiting session waits on: the question when it is one ask_user question, else `why` there is nothing to show. */
14
+ export interface Pending { callId: string; tool: string; ask?: Ask; why?: string }
15
+ interface Call { id: string; name: string; args: Json }
16
+
17
+ const clip = (v: unknown, n: number) => String(v ?? "").slice(0, n);
18
+ const TITLES = ["title", "label", "text", "value", "name", "option"];
19
+ /** pi-ask-user's coerceOption: a plain value, or an object with one of its title keys and an optional description. */
20
+ const option = (o: unknown): Option | undefined => {
21
+ if (["string", "number", "boolean"].includes(typeof o)) return String(o).trim() ? { title: clip(String(o).trim(), 200) } : undefined;
22
+ const r = (o && typeof o === "object" ? o : {}) as Json, title = TITLES.map((k) => r[k]).find((v) => typeof v === "string" && v.trim());
23
+ return title ? { title: clip(title.trim(), 200), ...(typeof r.description === "string" && r.description.trim() ? { description: clip(r.description, 600) } : {}) } : undefined;
24
+ };
25
+
26
+ /** The unanswered tool calls of the newest assistant message, read from the file's last 256 KB; none once a user message follows it. */
27
+ export async function openCalls(file: string): Promise<Call[]> {
28
+ const fh = await open(file, "r");
29
+ try {
30
+ const { size } = await fh.stat(), start = Math.max(0, size - TAIL), buf = Buffer.alloc(size - start);
31
+ await fh.read(buf, 0, buf.length, start);
32
+ const answered = new Set<string>();
33
+ for (const line of buf.toString("utf8").split("\n").slice(start ? 1 : 0).reverse()) {
34
+ const m = line.includes('"message"') ? parse(line)?.message : undefined;
35
+ if (m?.role === "toolResult") answered.add(String(m.toolCallId));
36
+ if (m?.role === "user") return [];
37
+ if (m?.role === "assistant") return (Array.isArray(m.content) ? m.content : []).filter((b: Json) => b?.type === "toolCall" && !answered.has(String(b.id))).map((b: Json) => ({ id: String(b.id), name: String(b.name), args: b.arguments ?? {} }));
38
+ }
39
+ return [];
40
+ } finally {
41
+ await fh.close();
42
+ }
43
+ }
44
+
45
+ /** The question to show, or why there is none. Shown as plain text; the page never turns it into markup. */
46
+ export function askOf(a: Json): { ask: Ask } | { why: string } {
47
+ if (Array.isArray(a.questions)) return { why: "It asks several questions at once." };
48
+ if (typeof a.question !== "string" || !a.question.trim()) return { why: "Its question could not be read." };
49
+ const options = (Array.isArray(a.options) ? a.options : []).map(option).filter((o: Option | undefined): o is Option => Boolean(o)).slice(0, 50);
50
+ const context = typeof a.context === "string" && a.context.trim() ? clip(a.context.trim(), 8000) : undefined;
51
+ return { ask: { question: clip(a.question, 2000), ...(context && { context }), options, multiple: a.allowMultiple === true, freeform: !options.length || a.allowFreeform !== false } };
52
+ }
53
+
54
+ /** What a waiting session waits on, for the snapshot; cached while the file is unchanged. */
55
+ export async function pendingOf(file: string): Promise<Pending | undefined> {
56
+ const s = await stat(file).catch(() => undefined), sig = s ? `${s.size}:${s.mtimeMs}` : "", hit = CACHE.get(file);
57
+ if (hit?.sig === sig) return hit.pending;
58
+ const calls = s ? await openCalls(file).catch(() => []) : [], [c] = calls;
59
+ const shape = c?.name === "ask_user" ? askOf(c.args) : { why: c?.name === "secret_drop" ? "It waits for a secret." : "It waits on a dialog." };
60
+ const pending = c && { callId: c.id, tool: c.name, ...(calls.length > 1 ? { why: "It waits on more than one dialog." } : shape) };
61
+ CACHE.set(file, { sig, pending });
62
+ if (CACHE.size > 64) CACHE.delete(CACHE.keys().next().value!);
63
+ return pending;
64
+ }
65
+
66
+ /** Adds what each waiting unit waits on to a snapshot. */
67
+ export async function withPending(net: Net, fileOf: (key: string) => string | undefined): Promise<Net> {
68
+ const all = net.bases.flatMap((b) => [...(b.tower ? [b.tower] : []), ...[...b.tabs.flatMap((t) => t.units), ...b.outside].flatMap((u) => [u, ...u.subs])]);
69
+ const jobs: Promise<unknown>[] = [];
70
+ for (const u of all) {
71
+ const file = u.mark === "waiting" ? fileOf(u.key) : undefined;
72
+ if (file) jobs.push(pendingOf(file).then((pending) => pending && (u.pending = pending)));
73
+ }
74
+ await Promise.all(jobs);
75
+ return net;
76
+ }
package/src/control.ts ADDED
@@ -0,0 +1,130 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ import type { IncomingMessage } from "node:http";
3
+ import { HerdrError, type HerdrCall, type Pane } from "./herdr.ts";
4
+ import type { Found } from "./net.ts";
5
+
6
+ // Typing into a session from the page, through Herdr's own input path only, as if typed in the terminal:
7
+ // POST /<token>/api/sessions/<key>/input {text, as: "send" | "followUp", confirmed?} the text, then Enter (or Alt+Enter while it works)
8
+ // POST /<token>/api/sessions/<key>/keys {keys} keys from an allow list; never Ctrl+C or Ctrl+D
9
+ // POST /<token>/api/sessions/<key>/focus brings the session's pane forward in Herdr
10
+ // POST /<token>/api/remote/pair a new one-time pairing URL and its QR, while remote access is on
11
+ // 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
12
+ // 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
13
+ // 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.
14
+ // 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.
15
+
16
+ export const MAX_TEXT = 16_384, MAX_BODY = 65_536, MAX_KEYS = 16;
17
+ // Only Escape (Stop) until Phase 3: other keys act on whatever the terminal holds, so up then enter could resubmit a /reload unconfirmed.
18
+ export const KEYS = new Set(["esc"]);
19
+ /** Commands that end the session or switch its pane to another one: the page asks first, and the server refuses them unconfirmed. */
20
+ export const ENDING = /^\s*\/(quit|exit|new|resume|fork|clone|import|reload)(?=\s|$)/;
21
+ // Control characters would act as keys inside the paste (ESC[201~ ends it, after which anything is raw input, Ctrl+C included); tab and newline stay.
22
+ const CONTROL = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/;
23
+
24
+ export interface Reply { status: number; body: unknown }
25
+ export interface ControlDeps { lookup(key: string): Found | undefined; call?: HerdrCall; csrf: string; canonical(file?: string): string | undefined; pair?(): { url: string; rows: string[] } | undefined }
26
+
27
+ const no = (status: number, error: string, reason: string): Reply => ({ status, body: { error, reason } });
28
+ const same = (a: string, b: string) => a.length === b.length && timingSafeEqual(Buffer.from(a), Buffer.from(b));
29
+
30
+ /** The request body as JSON, or the reply that refuses it. A body past the limit is drained, not kept. */
31
+ function body(req: IncomingMessage): Promise<Reply | { json: Record<string, unknown> }> {
32
+ return new Promise((done) => {
33
+ const parts: Buffer[] = [];
34
+ let size = 0;
35
+ req.on("data", (chunk: Buffer) => ((size += chunk.length) <= MAX_BODY && parts.push(chunk)));
36
+ req.on("error", () => done(no(400, "bad_request", "The request could not be read.")));
37
+ req.on("end", () => {
38
+ if (size > MAX_BODY) return done(no(413, "too_large", "The request is larger than 64 KB."));
39
+ try {
40
+ const json = JSON.parse(Buffer.concat(parts).toString("utf8") || "{}");
41
+ done(json && typeof json === "object" && !Array.isArray(json) ? { json } : no(400, "bad_request", "The body is not a JSON object."));
42
+ } catch {
43
+ done(no(400, "bad_request", "The body is not JSON."));
44
+ }
45
+ });
46
+ });
47
+ }
48
+
49
+ /** Herdr's refusal as the page sees it; its own code is kept. */
50
+ function refused(error: unknown): Reply {
51
+ const code = error instanceof HerdrError ? error.code : "herdr_error";
52
+ if (code === "timeout") return no(504, code, "Herdr did not confirm it in time. Check the terminal before sending again.");
53
+ if (["agent_blocked", "agent_not_found", "agent_not_ready", "pane_not_found"].includes(code)) return no(409, code, code === "agent_blocked" ? "It is waiting on a dialog; answer it in the terminal." : "The pane no longer runs this session.");
54
+ return no(502, code, (error as Error).message);
55
+ }
56
+
57
+ /** The text as it will be typed, or why it cannot be: CRLF and lone CR become LF, then the size and character checks. */
58
+ export function checkText(text: unknown): string | Reply {
59
+ if (typeof text !== "string" || !text.trim()) return no(400, "bad_request", "There is no text to send.");
60
+ const typed = text.replace(/\r\n?/g, "\n");
61
+ if (Buffer.byteLength(typed) > MAX_TEXT) return no(413, "too_large", "The text is longer than 16 KB.");
62
+ if (CONTROL.test(typed)) return no(400, "control_character", "Control characters cannot be typed from the browser.");
63
+ return typed;
64
+ }
65
+
66
+ export function createControl({ lookup, call, csrf, canonical, pair }: ControlDeps) {
67
+ const busy = new Set<string>();
68
+ /** The origin checks, then the session checks, then one Herdr call. `parts` is the path after api/: sessions/<key>/<action>, or remote/pair. */
69
+ return async function handle(req: IncomingMessage, origin: string, [scope, key = "", action = ""]: string[]): Promise<Reply> {
70
+ if (req.method !== "POST") return no(405, "method", "Use POST.");
71
+ const site = req.headers["sec-fetch-site"], type = String(req.headers["content-type"] ?? "").split(";")[0]!.trim().toLowerCase();
72
+ if (req.headers.origin !== origin || (site !== undefined && site !== "same-origin")) return no(403, "origin", "Only the page itself may send input.");
73
+ if (type !== "application/json") return no(415, "content_type", "The body must be JSON.");
74
+ if (!same(String(req.headers["x-sb-csrf"] ?? ""), csrf)) return no(403, "csrf", "The page's token is out of date; reload the page.");
75
+ const read = await body(req);
76
+ if ("status" in read) return read;
77
+ if (scope === "remote") {
78
+ const paired = key === "pair" && !action ? pair?.() : undefined;
79
+ 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.");
80
+ }
81
+ if (!["input", "keys", "focus"].includes(action)) return no(404, "not_found", "No such action.");
82
+ const found = lookup(key);
83
+ if (!found) return no(404, "not_found", "No such session.");
84
+ if (!found.live || !found.pane || !found.file) return no(409, "not_live", "This session is no longer live in a pane.");
85
+ if (!call) return no(503, "unavailable", "This server cannot reach Herdr.");
86
+ const { json } = read;
87
+ let job: () => Promise<unknown>;
88
+ if (action === "input") {
89
+ const text = checkText(json.text);
90
+ if (typeof text !== "string") return text;
91
+ if (json.as !== "send" && json.as !== "followUp") return no(400, "bad_request", "`as` must be send or followUp.");
92
+ if (ENDING.test(text) && json.confirmed !== true) return no(409, "confirm", "This command ends or replaces the session; confirm to send it.");
93
+ job = async () => {
94
+ const pane = await live(found);
95
+ if (pane.agent_status === "blocked") throw no(409, "blocked", "It is waiting on a dialog; answer it in the terminal.");
96
+ // Enter sends a prompt when idle and a steering message while it works; Alt+Enter queues a follow-up, which only means something while it works.
97
+ return json.as === "followUp" && pane.agent_status === "working"
98
+ ? call("pane.send_input", { pane_id: pane.pane_id, text, keys: ["alt+enter"] }, 15_000)
99
+ : call("agent.prompt", { target: pane.pane_id, text }, 15_000);
100
+ };
101
+ } else if (action === "keys") {
102
+ const keys = json.keys;
103
+ if (!Array.isArray(keys) || !keys.length || keys.length > MAX_KEYS || !keys.every((k) => typeof k === "string" && KEYS.has(k))) return no(400, "key", "Only keys from the allow list can be sent (never Ctrl+C or Ctrl+D).");
104
+ job = async () => {
105
+ const pane = await live(found);
106
+ // A dialog may be a masked secret prompt; until the page can tell which (phase 3), only Escape reaches a waiting session.
107
+ if (pane.agent_status === "blocked" && keys.some((k) => k !== "esc")) throw no(409, "blocked", "While it waits on a dialog only Escape is sent from the browser; answer it in the terminal.");
108
+ return call("pane.send_input", { pane_id: pane.pane_id, keys }, 15_000);
109
+ };
110
+ } else job = async () => call("agent.focus", { target: (await live(found)).pane_id });
111
+ if (busy.has(key)) return no(429, "busy", "Another input to this session is still on its way.");
112
+ busy.add(key);
113
+ try {
114
+ await job();
115
+ return { status: 200, body: { accepted: true } };
116
+ } catch (error) {
117
+ return (error as Reply).status ? (error as Reply) : refused(error);
118
+ } finally {
119
+ busy.delete(key);
120
+ }
121
+ };
122
+
123
+ /** The pane as Herdr has it now: it must still run pi, on this session's file. */
124
+ async function live(found: Found): Promise<Pane> {
125
+ const { pane } = (await call!("pane.get", { pane_id: found.pane })) as { pane: Pane };
126
+ if (pane.agent !== "pi") throw no(409, "not_pi", "The pane no longer runs pi.");
127
+ if (canonical(pane.agent_session?.value) !== found.file) throw no(409, "moved", "The pane moved to another session.");
128
+ return pane;
129
+ }
130
+ }