pi-roundtable-web 0.26.2 → 0.27.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.
Files changed (219) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +34 -12
  3. package/dist/chunk-account-1ecx5kt7.js +1 -0
  4. package/dist/chunk-action-menu-zh2n0ahf.js +1 -0
  5. package/dist/chunk-agent-ynm2tcbx.js +1 -0
  6. package/dist/chunk-arranged-rooms-qnz8vese.js +1 -0
  7. package/dist/chunk-bulk-actions-pr0mw0kc.js +1 -0
  8. package/dist/chunk-character-creator-caj4pmg4.js +1 -0
  9. package/dist/{chunk-codeeditor-jm2sm333.js → chunk-codeeditor-dga6s78p.js} +2 -2
  10. package/dist/chunk-confirm-g76v5d9r.js +1 -0
  11. package/dist/{chunk-connectors-mhnw6d79.js → chunk-connectors-j79ze3j1.js} +1 -1
  12. package/dist/{chunk-conversations-yjyq4g1y.js → chunk-conversations-2farn72g.js} +1 -1
  13. package/dist/chunk-council-sheet-s9sz1bzf.js +2 -0
  14. package/dist/{chunk-drag-nyq52ck7.js → chunk-drag-45e8222r.js} +1 -1
  15. package/dist/chunk-entry-q11n47h9.js +1 -0
  16. package/dist/chunk-folder-list-4gcj7g7h.js +1 -0
  17. package/dist/chunk-folds-ten4ykfw.js +1 -0
  18. package/dist/chunk-gate-kcv1a4t0.js +1 -0
  19. package/dist/chunk-group-scene-12ghcvzd.js +1 -0
  20. package/dist/chunk-group-table-t458kdgn.js +1 -0
  21. package/dist/chunk-host-icons-h1k9k96a.js +1 -0
  22. package/dist/chunk-host-picture-3p0wzn3v.js +1 -0
  23. package/dist/chunk-host-state-7mdve7cb.js +1 -0
  24. package/dist/chunk-host-ui-bdgwa6sf.js +1 -0
  25. package/dist/chunk-index-02vfshtt.js +2 -0
  26. package/dist/{chunk-index-31ays46f.js → chunk-index-0a81da8z.js} +2 -2
  27. package/dist/{chunk-index-y00wke93.js → chunk-index-0w4bfjb5.js} +2 -2
  28. package/dist/chunk-index-12zyshae.js +2 -0
  29. package/dist/chunk-index-18pgxxw3.js +2 -0
  30. package/dist/chunk-index-2e4am3hw.js +50 -0
  31. package/dist/{chunk-index-1f0j335v.js → chunk-index-2fdns8tn.js} +2 -2
  32. package/dist/{chunk-index-934zgd18.js → chunk-index-2t30xwed.js} +2 -2
  33. package/dist/{chunk-index-cmxs5y22.js → chunk-index-31x6krbs.js} +3 -3
  34. package/dist/{chunk-index-b7jjca66.js → chunk-index-3d8s3565.js} +2 -2
  35. package/dist/chunk-index-5bfqek1d.js +2 -0
  36. package/dist/chunk-index-64rsz6pf.js +2 -0
  37. package/dist/chunk-index-6bkcq9ka.js +2 -0
  38. package/dist/{chunk-index-9nents16.js → chunk-index-8d81qy69.js} +2 -2
  39. package/dist/chunk-index-92zgb7my.js +2 -0
  40. package/dist/chunk-index-aamhjg5n.js +2 -0
  41. package/dist/{chunk-index-ewj52be9.js → chunk-index-ac1ktr56.js} +2 -2
  42. package/dist/{chunk-index-6tpr6r6d.js → chunk-index-ak6ecqy3.js} +2 -2
  43. package/dist/{chunk-index-yrhbhmxz.js → chunk-index-b73y2gc0.js} +2 -2
  44. package/dist/chunk-index-b8cpvr7y.js +2 -0
  45. package/dist/{chunk-index-tmx30r1w.js → chunk-index-bhqczxr3.js} +2 -2
  46. package/dist/{chunk-index-0xj30mzw.js → chunk-index-cbqhzf40.js} +12 -12
  47. package/dist/chunk-index-cdctnw71.js +2 -0
  48. package/dist/chunk-index-dj0tbfyb.js +2 -0
  49. package/dist/chunk-index-dnjftvtq.js +2 -0
  50. package/dist/chunk-index-e2dqjde8.js +1 -0
  51. package/dist/{chunk-index-frwa2fmc.js → chunk-index-ebg48jgy.js} +2 -2
  52. package/dist/{chunk-index-smw1n2pt.js → chunk-index-ek5mek4j.js} +5 -5
  53. package/dist/chunk-index-f6q0stqx.js +2 -0
  54. package/dist/{chunk-index-kw85ajap.js → chunk-index-ffbrm6tr.js} +3 -3
  55. package/dist/chunk-index-g1awv7rk.js +2 -0
  56. package/dist/chunk-index-g3wrs02w.js +1 -0
  57. package/dist/chunk-index-j0m16cj1.js +2 -0
  58. package/dist/chunk-index-j4kk1m59.js +2 -0
  59. package/dist/chunk-index-j4x9dq30.css +1 -0
  60. package/dist/{chunk-index-7bbqtdce.js → chunk-index-jgzygbt8.js} +2 -2
  61. package/dist/chunk-index-kb3w11mn.js +7 -0
  62. package/dist/chunk-index-kbrg32sx.js +2 -0
  63. package/dist/chunk-index-msqhgvfx.js +2 -0
  64. package/dist/chunk-index-pefwfm26.js +2 -0
  65. package/dist/chunk-index-pyfx77cj.js +2 -0
  66. package/dist/{chunk-index-kfv996qe.js → chunk-index-q7g62sgm.js} +35 -3
  67. package/dist/chunk-index-qfgkfq86.js +2 -0
  68. package/dist/chunk-index-rh0x9rzf.js +2 -0
  69. package/dist/{chunk-index-029209sy.js → chunk-index-sm7m4fej.js} +2 -2
  70. package/dist/chunk-index-sv9qpmhv.js +1 -0
  71. package/dist/chunk-index-t998yg8y.js +2 -0
  72. package/dist/chunk-index-tk4se135.js +4 -0
  73. package/dist/chunk-index-v6smgw77.js +2 -0
  74. package/dist/chunk-index-w42rcbx1.js +2 -0
  75. package/dist/chunk-index-x4enmj72.js +2 -0
  76. package/dist/chunk-index-xq84gg8w.js +2 -0
  77. package/dist/{chunk-index-f3b5nch2.js → chunk-index-xxeb2xp0.js} +3 -3
  78. package/dist/{chunk-index-0n6ja7my.js → chunk-index-zyvwhxga.js} +1 -1
  79. package/dist/chunk-lang-css-y9133ktr.js +1 -0
  80. package/dist/{chunk-lang-dockerfile-46rk929v.js → chunk-lang-dockerfile-cnwqn660.js} +1 -1
  81. package/dist/{chunk-lang-go-qb0qdpg7.js → chunk-lang-go-yy6kgsxv.js} +3 -3
  82. package/dist/chunk-lang-html-r2xmryek.js +1 -0
  83. package/dist/chunk-lang-javascript-1r6wd177.js +1 -0
  84. package/dist/{chunk-lang-json-1q4fp694.js → chunk-lang-json-dx56g11y.js} +1 -1
  85. package/dist/{chunk-lang-markdown-5w7t28wa.js → chunk-lang-markdown-fpcf2dfq.js} +5 -5
  86. package/dist/{chunk-lang-python-3dq0p4rf.js → chunk-lang-python-zzp95955.js} +2 -2
  87. package/dist/{chunk-lang-rust-70pa0agr.js → chunk-lang-rust-214fh7sp.js} +1 -1
  88. package/dist/{chunk-lang-shell-4pxt7hf4.js → chunk-lang-shell-1rpgtepw.js} +1 -1
  89. package/dist/{chunk-lang-sql-kgcbh2a9.js → chunk-lang-sql-vvp46hpj.js} +2 -2
  90. package/dist/{chunk-lang-toml-99t95pab.js → chunk-lang-toml-tbj7467c.js} +1 -1
  91. package/dist/chunk-lang-typescript-y5w4tt4g.js +1 -0
  92. package/dist/{chunk-lang-xml-mnbv9yfa.js → chunk-lang-xml-tbcqmedk.js} +1 -1
  93. package/dist/{chunk-lang-yaml-2by9g1dy.js → chunk-lang-yaml-jh56t3sv.js} +1 -1
  94. package/dist/chunk-layout-actions-y73mpsh1.js +1 -0
  95. package/dist/chunk-menu-items-bza16bw6.js +2 -0
  96. package/dist/chunk-mode-toggle-nj9gnt3b.js +1 -0
  97. package/dist/chunk-note-row-6fmhrdng.js +75 -0
  98. package/dist/chunk-notes-wv0e6ssf.js +1 -0
  99. package/dist/chunk-notice-bell-9z27epd2.js +1 -0
  100. package/dist/{chunk-notices-evny44be.js → chunk-notices-fte9f196.js} +1 -1
  101. package/dist/chunk-onboarding-c5twahc3.js +1 -0
  102. package/dist/{chunk-overview-42h45ehd.js → chunk-overview-k4ccprax.js} +1 -1
  103. package/dist/chunk-palette-9rbp2fab.js +1 -0
  104. package/dist/chunk-placement-items-me598wsb.js +1 -0
  105. package/dist/{chunk-profile-bqcgkgk4.js → chunk-profile-s7r7fcvz.js} +1 -1
  106. package/dist/chunk-prompt-history-hf296e7v.js +1 -0
  107. package/dist/chunk-prompts-c8s0nhkm.js +1 -0
  108. package/dist/chunk-room-confirms-ga2sbvhc.js +1 -0
  109. package/dist/chunk-room-menu-7h4jym2b.js +1 -0
  110. package/dist/chunk-rooms-menu-button-pgtaanb6.js +1 -0
  111. package/dist/chunk-schedules-mbnwcxpc.js +1 -0
  112. package/dist/{chunk-seat-menu-ca5p4e9q.js → chunk-seat-menu-r0k0eawr.js} +1 -1
  113. package/dist/chunk-sheet-hosts-a5dfevy9.js +1 -0
  114. package/dist/chunk-shortcut-help-a6pkjcq7.js +26 -0
  115. package/dist/chunk-shortcuts-cgt87jkz.js +1 -0
  116. package/dist/chunk-skill-editor-67yk4x7t.js +8 -0
  117. package/dist/chunk-skills-rewemey4.js +1 -0
  118. package/dist/chunk-switcher-list-t8rbay2y.js +1 -0
  119. package/dist/{chunk-tab-dot-4tkpxqeh.js → chunk-tab-dot-vr11dfrw.js} +1 -1
  120. package/dist/chunk-table-52nh0azk.js +1 -0
  121. package/dist/chunk-table2d-g57vr6h0.js +1 -0
  122. package/dist/chunk-table3d-rp0w6kt4.js +4220 -0
  123. package/dist/chunk-thread-actions-qcxt1y1g.js +1 -0
  124. package/dist/chunk-thread-cards-c2mrp2gj.js +1 -0
  125. package/dist/chunk-thread-list-0071q455.js +217 -0
  126. package/dist/chunk-transcript-789yqysq.js +1 -0
  127. package/dist/chunk-where-hmwmme9e.js +1 -0
  128. package/dist/{chunk-workspace-h0p32ygy.js → chunk-workspace-wr1qrtsh.js} +2 -2
  129. package/dist/chunk-zh-tw-z5p5mpph.js +1 -0
  130. package/dist/index.html +1 -1
  131. package/dist/sw.js +1 -1
  132. package/package.json +3 -3
  133. package/src/api-types.ts +73 -0
  134. package/src/app/app.ts +38 -4
  135. package/src/app/helper-routes.ts +106 -0
  136. package/src/app/rooms.ts +18 -3
  137. package/src/app/routes.ts +125 -2
  138. package/src/app/surface.ts +18 -1
  139. package/src/app/team-routes.ts +21 -1
  140. package/src/app/threads.ts +11 -0
  141. package/src/host/host-app.ts +15 -1
  142. package/src/host/read-all.ts +80 -0
  143. package/src/host/reads.ts +67 -0
  144. package/src/i18n/zh-tw.ts +64 -13
  145. package/dist/chunk-account-nhpd9j90.js +0 -1
  146. package/dist/chunk-action-menu-ec9jqtxq.js +0 -1
  147. package/dist/chunk-agent-d1xfrawq.js +0 -1
  148. package/dist/chunk-arranged-rooms-bga8yk2v.js +0 -1
  149. package/dist/chunk-character-creator-k67f77w5.js +0 -1
  150. package/dist/chunk-confirm-wc5xraev.js +0 -1
  151. package/dist/chunk-entry-red1597w.js +0 -1
  152. package/dist/chunk-folder-list-x56y26yq.js +0 -1
  153. package/dist/chunk-folds-3p7s2nvx.js +0 -1
  154. package/dist/chunk-gate-h41bv4y9.js +0 -1
  155. package/dist/chunk-group-scene-7eb6jyev.js +0 -1
  156. package/dist/chunk-group-table-rca48kt0.js +0 -1
  157. package/dist/chunk-host-icons-x318gjqp.js +0 -1
  158. package/dist/chunk-host-picture-xswcakgy.js +0 -1
  159. package/dist/chunk-host-state-bpahvk54.js +0 -1
  160. package/dist/chunk-host-ui-tsbwwk38.js +0 -1
  161. package/dist/chunk-index-066vzexw.js +0 -2
  162. package/dist/chunk-index-0h4h76kw.js +0 -2
  163. package/dist/chunk-index-2f5nfekb.js +0 -8
  164. package/dist/chunk-index-43k1rjmx.js +0 -2
  165. package/dist/chunk-index-4gk88a0x.js +0 -4
  166. package/dist/chunk-index-4jf4jvfd.css +0 -1
  167. package/dist/chunk-index-4mf29sbx.js +0 -2
  168. package/dist/chunk-index-51n20yhb.js +0 -2
  169. package/dist/chunk-index-84v7j6vh.js +0 -50
  170. package/dist/chunk-index-8f9jer8r.js +0 -2
  171. package/dist/chunk-index-91z3g774.js +0 -1
  172. package/dist/chunk-index-9w0sar6s.js +0 -2
  173. package/dist/chunk-index-a768zmp6.js +0 -2
  174. package/dist/chunk-index-c5x70d9g.js +0 -2
  175. package/dist/chunk-index-cwc1vvpw.js +0 -2
  176. package/dist/chunk-index-d85x96q0.js +0 -2
  177. package/dist/chunk-index-datrrs6b.js +0 -2
  178. package/dist/chunk-index-dep2e3f3.js +0 -2
  179. package/dist/chunk-index-dtste2ed.js +0 -2
  180. package/dist/chunk-index-grkczq2c.js +0 -2
  181. package/dist/chunk-index-gzvkpe3e.js +0 -2
  182. package/dist/chunk-index-satam151.js +0 -2
  183. package/dist/chunk-index-th43f690.js +0 -2
  184. package/dist/chunk-index-ths2hxpe.js +0 -1
  185. package/dist/chunk-index-y4sqmenv.js +0 -2
  186. package/dist/chunk-index-zd76pr2k.js +0 -2
  187. package/dist/chunk-lang-css-fa7c0zfj.js +0 -1
  188. package/dist/chunk-lang-html-47mf2y0v.js +0 -1
  189. package/dist/chunk-lang-javascript-d32qrvxz.js +0 -1
  190. package/dist/chunk-lang-typescript-4k1d8z7s.js +0 -1
  191. package/dist/chunk-layout-actions-cgghzj1w.js +0 -1
  192. package/dist/chunk-menu-items-24cfwawa.js +0 -2
  193. package/dist/chunk-mode-toggle-v4baz3r1.js +0 -1
  194. package/dist/chunk-note-row-72n6cqyk.js +0 -75
  195. package/dist/chunk-notes-83s3tsey.js +0 -1
  196. package/dist/chunk-notice-bell-a4ty5h1p.js +0 -1
  197. package/dist/chunk-onboarding-q1avjbrx.js +0 -1
  198. package/dist/chunk-palette-e44qepmh.js +0 -1
  199. package/dist/chunk-placement-items-v466t0sv.js +0 -1
  200. package/dist/chunk-prompt-history-hgbtcn4b.js +0 -1
  201. package/dist/chunk-prompts-czs8y6r1.js +0 -1
  202. package/dist/chunk-room-confirms-h8cdj3ng.js +0 -1
  203. package/dist/chunk-room-menu-7s0feajw.js +0 -1
  204. package/dist/chunk-rooms-menu-button-gawqvpsr.js +0 -1
  205. package/dist/chunk-schedules-zba7krac.js +0 -1
  206. package/dist/chunk-sheet-hosts-ssbz6hf2.js +0 -1
  207. package/dist/chunk-skill-editor-n0y9pd90.js +0 -8
  208. package/dist/chunk-skills-ea4vb2s4.js +0 -1
  209. package/dist/chunk-switcher-list-pj5csqr9.js +0 -1
  210. package/dist/chunk-table-j8nmtdnn.js +0 -1
  211. package/dist/chunk-table-seats-qeytqe0m.js +0 -1
  212. package/dist/chunk-table2d-18y029k9.js +0 -1
  213. package/dist/chunk-table3d-q2s31ae0.js +0 -4220
  214. package/dist/chunk-thread-actions-w3eadnh7.js +0 -1
  215. package/dist/chunk-thread-cards-fbpky3c9.js +0 -1
  216. package/dist/chunk-thread-list-pghe84be.js +0 -1
  217. package/dist/chunk-transcript-eqzxgqys.js +0 -1
  218. package/dist/chunk-where-ygyr4jdj.js +0 -1
  219. package/dist/chunk-zh-tw-k6xx1hp2.js +0 -1
package/dist/sw.js CHANGED
@@ -1 +1 @@
1
- var o=self,i=new URL(o.registration.scope);var d=new URL("index.html",i).href,h=new Set(["index.html","account-zk4efb87.css","chunk-account-nhpd9j90.js","chunk-action-menu-ec9jqtxq.js","chunk-agent-d1xfrawq.js","chunk-arranged-rooms-bga8yk2v.js","chunk-character-creator-k67f77w5.js","chunk-confirm-wc5xraev.js","chunk-connectors-mhnw6d79.js","chunk-conversations-yjyq4g1y.js","chunk-drag-nyq52ck7.js","chunk-entry-red1597w.js","chunk-folder-list-x56y26yq.js","chunk-folds-3p7s2nvx.js","chunk-gate-h41bv4y9.js","chunk-group-scene-7eb6jyev.js","chunk-group-table-rca48kt0.js","chunk-host-icons-x318gjqp.js","chunk-host-picture-xswcakgy.js","chunk-host-state-bpahvk54.js","chunk-host-ui-tsbwwk38.js","chunk-index-029209sy.js","chunk-index-066vzexw.js","chunk-index-0h4h76kw.js","chunk-index-0n6ja7my.js","chunk-index-0q15ysrb.js","chunk-index-1f0j335v.js","chunk-index-2f5nfekb.js","chunk-index-31ays46f.js","chunk-index-43k1rjmx.js","chunk-index-4gk88a0x.js","chunk-index-4jf4jvfd.css","chunk-index-4mf29sbx.js","chunk-index-51n20yhb.js","chunk-index-6tpr6r6d.js","chunk-index-7bbqtdce.js","chunk-index-84v7j6vh.js","chunk-index-8f9jer8r.js","chunk-index-91z3g774.js","chunk-index-934zgd18.js","chunk-index-9nents16.js","chunk-index-9w0sar6s.js","chunk-index-a768zmp6.js","chunk-index-b7jjca66.js","chunk-index-c5x70d9g.js","chunk-index-cmxs5y22.js","chunk-index-cwc1vvpw.js","chunk-index-d85x96q0.js","chunk-index-datrrs6b.js","chunk-index-dep2e3f3.js","chunk-index-dtste2ed.js","chunk-index-ewj52be9.js","chunk-index-f3b5nch2.js","chunk-index-frwa2fmc.js","chunk-index-grkczq2c.js","chunk-index-gzvkpe3e.js","chunk-index-kfv996qe.js","chunk-index-kw85ajap.js","chunk-index-satam151.js","chunk-index-smw1n2pt.js","chunk-index-th43f690.js","chunk-index-ths2hxpe.js","chunk-index-tmx30r1w.js","chunk-index-y00wke93.js","chunk-index-y4sqmenv.js","chunk-index-yrhbhmxz.js","chunk-index-zd76pr2k.js","chunk-layout-actions-cgghzj1w.js","chunk-menu-items-24cfwawa.js","chunk-mode-toggle-v4baz3r1.js","chunk-note-row-72n6cqyk.js","chunk-notes-83s3tsey.js","chunk-notice-bell-a4ty5h1p.js","chunk-notices-evny44be.js","chunk-onboarding-q1avjbrx.js","chunk-overview-42h45ehd.js","chunk-palette-e44qepmh.js","chunk-placement-items-v466t0sv.js","chunk-profile-bqcgkgk4.js","chunk-prompt-history-hgbtcn4b.js","chunk-prompts-czs8y6r1.js","chunk-room-confirms-h8cdj3ng.js","chunk-room-menu-7s0feajw.js","chunk-rooms-menu-button-gawqvpsr.js","chunk-schedules-zba7krac.js","chunk-seat-menu-ca5p4e9q.js","chunk-sheet-hosts-ssbz6hf2.js","chunk-skill-editor-n0y9pd90.js","chunk-skills-ea4vb2s4.js","chunk-switcher-list-pj5csqr9.js","chunk-tab-dot-4tkpxqeh.js","chunk-table-j8nmtdnn.js","chunk-table-seats-qeytqe0m.js","chunk-table2d-18y029k9.js","chunk-thread-actions-w3eadnh7.js","chunk-thread-cards-fbpky3c9.js","chunk-thread-list-pghe84be.js","chunk-transcript-eqzxgqys.js","chunk-where-ygyr4jdj.js","chunk-workspace-h0p32ygy.js","chunk-zh-tw-k6xx1hp2.js"].map((e)=>new URL(e,i).href)),w=/\/chunk-table3d-[a-z0-9]+\.js$/,g=/\/chunk-(?:codeeditor|lang-[a-z]+)-[a-z0-9]+\.js$/,k=["api/","auth/","login","files/","avatars/"],m=(e)=>{let n=e.pathname.slice(i.pathname.length);return k.some((t)=>n.startsWith(t))};async function v(e){let n=await caches.open("roundtable-web-d442d154a5a4");try{let t=await fetch(e);if(t.ok&&new URL(e.url).href===i.href)await n.put(d,t.clone());return t}catch{let t=await n.match(d);if(t)return t;throw Error("offline and the shell is not cached")}}async function W(e){let n=await caches.open("roundtable-web-d442d154a5a4"),t=await n.match(e);if(t)return t;let a=await fetch(e);if(a.ok)await n.put(e,a.clone());return a}var r=(e,n)=>typeof e==="string"&&e?e.slice(0,n):void 0;function l(e){if(typeof e!=="string")return i.href;let n=URL.parse(e,i);return n&&n.origin===i.origin&&n.pathname.startsWith(i.pathname)?n.href:i.href}var E=new Set(["notice","test"]);async function u(e,n){if(!e?.tag||!e.kind||!E.has(e.kind))return;try{await fetch(new URL("api/push/receipts",i),{method:"POST",credentials:"include",headers:{"content-type":"application/json"},body:JSON.stringify({tag:e.tag,event:n})})}catch{}}async function y(e){if(typeof e!=="number"||!Number.isSafeInteger(e)||e<0)return;try{if(e>0)await o.navigator.setAppBadge?.(e);else await o.navigator.clearAppBadge?.()}catch{}}async function P(e){let n={};try{let s=e.data?.json();if(typeof s==="object"&&s!==null)n=s}catch{}let t=r(n.tag,200),a=r(n.kind,16);if(await y(n.badge),a==="clear"){if(t)for(let s of await o.registration.getNotifications({tag:t}))s.close();return}await o.registration.showNotification(r(n.title,120)??"Roundtable",{body:r(n.body,240),icon:new URL("icons/192.png",i).href,badge:new URL("icons/badge-96.png",i).href,...t?{tag:t,renotify:!0}:{},...n.urgency==="high"?{requireInteraction:!0}:{},data:{url:l(n.url),...t?{tag:t}:{},...a?{kind:a}:{}}}),await u({...t?{tag:t}:{},...a?{kind:a}:{}},"received")}async function b(e){e.notification.close();let n=e.notification.data,t=l(n?.url),a=r(n?.tag,200),s=r(n?.kind,16),f=u({...a?{tag:a}:{},...s?{kind:s}:{}},"opened"),c=(await o.clients.matchAll({type:"window",includeUncontrolled:!0})).find((p)=>p.url.startsWith(i.href));if(c)await c.focus(),c.postMessage({type:"roundtable:open",url:t});else await o.clients.openWindow(t);await f}o.addEventListener("install",(e)=>{e.waitUntil(caches.open("roundtable-web-d442d154a5a4").then((n)=>n.addAll([...h])).then(()=>o.skipWaiting()))});o.addEventListener("activate",(e)=>{e.waitUntil(caches.keys().then((n)=>Promise.all(n.filter((t)=>t.startsWith("roundtable-web-")&&t!=="roundtable-web-d442d154a5a4").map((t)=>caches.delete(t)))).then(()=>o.clients.claim()))});o.addEventListener("fetch",(e)=>{let{request:n}=e;if(n.method!=="GET")return;let t=new URL(n.url);if(t.origin!==i.origin||!t.pathname.startsWith(i.pathname))return;if(m(t))return;if(n.mode==="navigate")e.respondWith(v(n));else if(h.has(t.origin+t.pathname)||w.test(t.pathname)||g.test(t.pathname))e.respondWith(W(n))});o.addEventListener("push",(e)=>{e.waitUntil(P(e))});o.addEventListener("notificationclick",(e)=>{e.waitUntil(b(e))});
1
+ var o=self,i=new URL(o.registration.scope);var d=new URL("index.html",i).href,h=new Set(["index.html","account-zk4efb87.css","chunk-account-1ecx5kt7.js","chunk-action-menu-zh2n0ahf.js","chunk-agent-ynm2tcbx.js","chunk-arranged-rooms-qnz8vese.js","chunk-bulk-actions-pr0mw0kc.js","chunk-character-creator-caj4pmg4.js","chunk-confirm-g76v5d9r.js","chunk-connectors-j79ze3j1.js","chunk-conversations-2farn72g.js","chunk-council-sheet-s9sz1bzf.js","chunk-drag-45e8222r.js","chunk-entry-q11n47h9.js","chunk-folder-list-4gcj7g7h.js","chunk-folds-ten4ykfw.js","chunk-gate-kcv1a4t0.js","chunk-group-scene-12ghcvzd.js","chunk-group-table-t458kdgn.js","chunk-host-icons-h1k9k96a.js","chunk-host-picture-3p0wzn3v.js","chunk-host-state-7mdve7cb.js","chunk-host-ui-bdgwa6sf.js","chunk-index-02vfshtt.js","chunk-index-0a81da8z.js","chunk-index-0q15ysrb.js","chunk-index-0w4bfjb5.js","chunk-index-12zyshae.js","chunk-index-18pgxxw3.js","chunk-index-2e4am3hw.js","chunk-index-2fdns8tn.js","chunk-index-2t30xwed.js","chunk-index-31x6krbs.js","chunk-index-3d8s3565.js","chunk-index-5bfqek1d.js","chunk-index-64rsz6pf.js","chunk-index-6bkcq9ka.js","chunk-index-8d81qy69.js","chunk-index-92zgb7my.js","chunk-index-aamhjg5n.js","chunk-index-ac1ktr56.js","chunk-index-ak6ecqy3.js","chunk-index-b73y2gc0.js","chunk-index-b8cpvr7y.js","chunk-index-bhqczxr3.js","chunk-index-cdctnw71.js","chunk-index-dj0tbfyb.js","chunk-index-dnjftvtq.js","chunk-index-e2dqjde8.js","chunk-index-ebg48jgy.js","chunk-index-ek5mek4j.js","chunk-index-f6q0stqx.js","chunk-index-ffbrm6tr.js","chunk-index-g1awv7rk.js","chunk-index-g3wrs02w.js","chunk-index-j0m16cj1.js","chunk-index-j4kk1m59.js","chunk-index-j4x9dq30.css","chunk-index-jgzygbt8.js","chunk-index-kb3w11mn.js","chunk-index-kbrg32sx.js","chunk-index-msqhgvfx.js","chunk-index-pefwfm26.js","chunk-index-pyfx77cj.js","chunk-index-q7g62sgm.js","chunk-index-qfgkfq86.js","chunk-index-rh0x9rzf.js","chunk-index-sm7m4fej.js","chunk-index-sv9qpmhv.js","chunk-index-t998yg8y.js","chunk-index-tk4se135.js","chunk-index-v6smgw77.js","chunk-index-w42rcbx1.js","chunk-index-x4enmj72.js","chunk-index-xq84gg8w.js","chunk-index-xxeb2xp0.js","chunk-index-zyvwhxga.js","chunk-layout-actions-y73mpsh1.js","chunk-menu-items-bza16bw6.js","chunk-mode-toggle-nj9gnt3b.js","chunk-note-row-6fmhrdng.js","chunk-notes-wv0e6ssf.js","chunk-notice-bell-9z27epd2.js","chunk-notices-fte9f196.js","chunk-onboarding-c5twahc3.js","chunk-overview-k4ccprax.js","chunk-palette-9rbp2fab.js","chunk-placement-items-me598wsb.js","chunk-profile-s7r7fcvz.js","chunk-prompt-history-hf296e7v.js","chunk-prompts-c8s0nhkm.js","chunk-room-confirms-ga2sbvhc.js","chunk-room-menu-7h4jym2b.js","chunk-rooms-menu-button-pgtaanb6.js","chunk-schedules-mbnwcxpc.js","chunk-seat-menu-r0k0eawr.js","chunk-sheet-hosts-a5dfevy9.js","chunk-shortcut-help-a6pkjcq7.js","chunk-shortcuts-cgt87jkz.js","chunk-skill-editor-67yk4x7t.js","chunk-skills-rewemey4.js","chunk-switcher-list-t8rbay2y.js","chunk-tab-dot-vr11dfrw.js","chunk-table-52nh0azk.js","chunk-table2d-g57vr6h0.js","chunk-thread-actions-qcxt1y1g.js","chunk-thread-cards-c2mrp2gj.js","chunk-thread-list-0071q455.js","chunk-transcript-789yqysq.js","chunk-where-hmwmme9e.js","chunk-workspace-wr1qrtsh.js","chunk-zh-tw-z5p5mpph.js"].map((e)=>new URL(e,i).href)),w=/\/chunk-table3d-[a-z0-9]+\.js$/,g=/\/chunk-(?:codeeditor|lang-[a-z]+)-[a-z0-9]+\.js$/,k=["api/","auth/","login","files/","avatars/"],m=(e)=>{let n=e.pathname.slice(i.pathname.length);return k.some((t)=>n.startsWith(t))};async function v(e){let n=await caches.open("roundtable-web-7a28cdb79a14");try{let t=await fetch(e);if(t.ok&&new URL(e.url).href===i.href)await n.put(d,t.clone());return t}catch{let t=await n.match(d);if(t)return t;throw Error("offline and the shell is not cached")}}async function W(e){let n=await caches.open("roundtable-web-7a28cdb79a14"),t=await n.match(e);if(t)return t;let a=await fetch(e);if(a.ok)await n.put(e,a.clone());return a}var r=(e,n)=>typeof e==="string"&&e?e.slice(0,n):void 0;function l(e){if(typeof e!=="string")return i.href;let n=URL.parse(e,i);return n&&n.origin===i.origin&&n.pathname.startsWith(i.pathname)?n.href:i.href}var E=new Set(["notice","test"]);async function u(e,n){if(!e?.tag||!e.kind||!E.has(e.kind))return;try{await fetch(new URL("api/push/receipts",i),{method:"POST",credentials:"include",headers:{"content-type":"application/json"},body:JSON.stringify({tag:e.tag,event:n})})}catch{}}async function y(e){if(typeof e!=="number"||!Number.isSafeInteger(e)||e<0)return;try{if(e>0)await o.navigator.setAppBadge?.(e);else await o.navigator.clearAppBadge?.()}catch{}}async function P(e){let n={};try{let s=e.data?.json();if(typeof s==="object"&&s!==null)n=s}catch{}let t=r(n.tag,200),a=r(n.kind,16);if(await y(n.badge),a==="clear"){if(t)for(let s of await o.registration.getNotifications({tag:t}))s.close();return}await o.registration.showNotification(r(n.title,120)??"Roundtable",{body:r(n.body,240),icon:new URL("icons/192.png",i).href,badge:new URL("icons/badge-96.png",i).href,...t?{tag:t,renotify:!0}:{},...n.urgency==="high"?{requireInteraction:!0}:{},data:{url:l(n.url),...t?{tag:t}:{},...a?{kind:a}:{}}}),await u({...t?{tag:t}:{},...a?{kind:a}:{}},"received")}async function b(e){e.notification.close();let n=e.notification.data,t=l(n?.url),a=r(n?.tag,200),s=r(n?.kind,16),f=u({...a?{tag:a}:{},...s?{kind:s}:{}},"opened"),c=(await o.clients.matchAll({type:"window",includeUncontrolled:!0})).find((p)=>p.url.startsWith(i.href));if(c)await c.focus(),c.postMessage({type:"roundtable:open",url:t});else await o.clients.openWindow(t);await f}o.addEventListener("install",(e)=>{e.waitUntil(caches.open("roundtable-web-7a28cdb79a14").then((n)=>n.addAll([...h])).then(()=>o.skipWaiting()))});o.addEventListener("activate",(e)=>{e.waitUntil(caches.keys().then((n)=>Promise.all(n.filter((t)=>t.startsWith("roundtable-web-")&&t!=="roundtable-web-7a28cdb79a14").map((t)=>caches.delete(t)))).then(()=>o.clients.claim()))});o.addEventListener("fetch",(e)=>{let{request:n}=e;if(n.method!=="GET")return;let t=new URL(n.url);if(t.origin!==i.origin||!t.pathname.startsWith(i.pathname))return;if(m(t))return;if(n.mode==="navigate")e.respondWith(v(n));else if(h.has(t.origin+t.pathname)||w.test(t.pathname)||g.test(t.pathname))e.respondWith(W(n))});o.addEventListener("push",(e)=>{e.waitUntil(P(e))});o.addEventListener("notificationclick",(e)=>{e.waitUntil(b(e))});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-roundtable-web",
3
- "version": "0.26.2",
3
+ "version": "0.27.0",
4
4
  "description": "A web console for the owners of a pi-roundtable host: conversations, transcripts, and memory notes, with live updates",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -43,7 +43,7 @@
43
43
  "jose": "6.2.12"
44
44
  },
45
45
  "peerDependencies": {
46
- "pi-roundtable": ">=0.26.0 <0.27.0"
46
+ "pi-roundtable": ">=0.27.0 <0.28.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@biomejs/biome": "2.5.15",
@@ -70,7 +70,7 @@
70
70
  "@types/react": "19.3.0",
71
71
  "@types/react-dom": "19.3.0",
72
72
  "@types/three": "0.186.0",
73
- "pi-roundtable": "0.26.2",
73
+ "pi-roundtable": "0.27.0",
74
74
  "react": "19.3.0",
75
75
  "react-dom": "19.3.0",
76
76
  "react-markdown": "10.1.0",
package/src/api-types.ts CHANGED
@@ -507,6 +507,11 @@ export interface RoomView {
507
507
  members?: string[];
508
508
  /** A thread's parent room. */
509
509
  parent?: string;
510
+ /**
511
+ * A temp knight's room: `parent` is the room of the knight that made it, and `finished` is set
512
+ * when it has nothing running, queued or waiting. Absent on every permanent knight.
513
+ */
514
+ helper?: { parent: string; parentName: string; finished?: true };
510
515
  /** A thread's room takes no messages from the page. */
511
516
  readOnly: boolean;
512
517
  /**
@@ -528,6 +533,8 @@ export interface RoomView {
528
533
  agent?: string;
529
534
  /** A turn runs that Stop would end. */
530
535
  stoppable: boolean;
536
+ /** A group room: the round running now, who has spoken and who is next; absent when none runs. */
537
+ round?: RoundView;
531
538
  /** The newest stored message, if any. */
532
539
  last?: RoomMessage;
533
540
  /** Messages past this person's read cursor; absent when none. */
@@ -573,6 +580,44 @@ export interface ThreadsDismissed {
573
580
  dismissed: number;
574
581
  }
575
582
 
583
+ /** What one clear-all across rooms dismissed: the finished threads' room keys, by the room they hang under. */
584
+ export interface ThreadsDismissedAll extends ThreadsDismissed {
585
+ threads: Record<string, string[]>;
586
+ }
587
+
588
+ /**
589
+ * What a system line of a room starts with when its knight cleared its temp knights: the rest is a
590
+ * JSON list of `{ name, displayName }`, which the page words and offers the undo of.
591
+ */
592
+ export const HELPERS_ENDED = "Temp knights archived: ";
593
+
594
+ /** `POST api/rooms/archive-finished-helpers`: the temp knights archived, by the room of the knight that made them. */
595
+ export interface HelpersArchived {
596
+ archived: { room: string; names: string[] }[];
597
+ }
598
+
599
+ /** `POST api/rooms/restore-helpers`: the temp knights that came back; a name no longer archived is passed over. */
600
+ export interface HelpersRestored {
601
+ restored: string[];
602
+ }
603
+
604
+ /** A cursor that marking rooms read moved: where it was (0 for a room never read) and where it is. */
605
+ export interface ReadMove {
606
+ room: string;
607
+ from: number;
608
+ to: number;
609
+ }
610
+
611
+ /** What marking rooms read moved; the page keeps it to undo the marking. */
612
+ export interface ReadAllView {
613
+ moved: ReadMove[];
614
+ }
615
+
616
+ /** What undoing a marking put back: the rooms whose cursor went back. */
617
+ export interface ReadAllUndone {
618
+ restored: string[];
619
+ }
620
+
576
621
  /** What taking dismissed threads back onto the room list restored. */
577
622
  export interface ThreadsRestored {
578
623
  restored: number;
@@ -761,6 +806,8 @@ export interface AgentProfileView {
761
806
  avatar: string;
762
807
  character: CharacterSpec;
763
808
  room: string;
809
+ /** A temp knight's profile: the room of the knight that made it, and its name. */
810
+ helper?: { parent: string; parentName: string };
764
811
  }
765
812
 
766
813
  export interface ModelsView {
@@ -955,6 +1002,8 @@ export interface ArchivedEntry {
955
1002
  character?: CharacterSpec;
956
1003
  /** A group's members that are still on the team. */
957
1004
  members?: string[];
1005
+ /** A temp knight's parent, by name; the archive page groups it under that knight. */
1006
+ parent?: string;
958
1007
  }
959
1008
 
960
1009
  /** `GET api/archive`. */
@@ -1159,6 +1208,28 @@ export type ClientFrame =
1159
1208
  | { type: "cancel"; prompt: string };
1160
1209
 
1161
1210
  /** What the server sends: one JSON object per WebSocket text message. */
1211
+ /**
1212
+ * Where a group's round stands: the members' answers to one owner message, one after another.
1213
+ * `id` is the group message that opened the round, so a new round is told from the last one.
1214
+ */
1215
+ export interface RoundView {
1216
+ id: number;
1217
+ state: "running" | "ended";
1218
+ /** The member speaking now, by name. */
1219
+ current?: string;
1220
+ /** The members still to speak, in order, a handoff appended. */
1221
+ next: string[];
1222
+ /** The members who have replied so far, in order. */
1223
+ spoken: string[];
1224
+ /** The replies made so far, and the most a round holds. */
1225
+ replies: number;
1226
+ limit: number;
1227
+ /** Why the round ended: every member spoke, it reached its limit, or the owner stopped it. */
1228
+ ended?: "done" | "limit" | "stopped";
1229
+ /** The owner's messages waiting in the room for the next round. */
1230
+ queued?: number;
1231
+ }
1232
+
1162
1233
  export type ServerFrame =
1163
1234
  /** The socket is authenticated; sent first, and followed by every open card. */
1164
1235
  | {
@@ -1193,6 +1264,8 @@ export type ServerFrame =
1193
1264
  /** Only on the frame where `typing` turns false. */
1194
1265
  outcome?: "done" | "failed";
1195
1266
  }
1267
+ /** Where a group room's round stands; sent at each step and when it ends. */
1268
+ | { type: "round"; room: string; round: RoundView }
1196
1269
  /** Which agents work now, anywhere (Discord included); sent after `ready` and when it changes. */
1197
1270
  | { type: "presence"; agents: Record<string, { busy: boolean }> }
1198
1271
  | { type: "prompt"; room: string; prompt: PromptFrame }
package/src/app/app.ts CHANGED
@@ -13,8 +13,8 @@ import type {
13
13
  ScheduleStore,
14
14
  WebSocketRoute,
15
15
  } from "pi-roundtable";
16
- import { channelKey } from "pi-roundtable";
17
- import { APP_CLOSE_EXPIRED } from "../api-types.ts";
16
+ import { agentRoomKey, channelKey, isHelper, roomOf } from "pi-roundtable";
17
+ import { APP_CLOSE_EXPIRED, HELPERS_ENDED } from "../api-types.ts";
18
18
  import type { Authenticated, Authenticator } from "../auth/authenticator.ts";
19
19
  import type { ConsolePresentation } from "../features.ts";
20
20
  import type { Gate } from "../gate.ts";
@@ -209,6 +209,11 @@ export class OwnerApp {
209
209
  const quiet = quietClock
210
210
  ? (at: number) => quietClock.quietAt(at)
211
211
  : undefined;
212
+ const held = roomMuted(
213
+ mutes,
214
+ threads,
215
+ channelKey(options.surface, "thread."),
216
+ );
212
217
  const mounted = mountNotices({
213
218
  database: options.database,
214
219
  logger,
@@ -246,7 +251,16 @@ export class OwnerApp {
246
251
  visible: (principalId) => sockets.anyVisible(principalId),
247
252
  preview: options.push.preview,
248
253
  ...(quiet ? { quiet } : {}),
249
- muted: roomMuted(mutes, threads, channelKey(options.surface, "thread.")),
254
+ muted: async (room) => {
255
+ const named = roomOf(room as ChannelKey, [options.surface]);
256
+ // A temp knight's answers reach its knight's room, which pushes; its own room stays quiet.
257
+ if (
258
+ named?.kind === "agent" &&
259
+ isHelper(agents.directory.agent(named.name))
260
+ )
261
+ return true;
262
+ return held(room);
263
+ },
250
264
  urlOf: options.push.urlOf,
251
265
  logger,
252
266
  });
@@ -334,7 +348,10 @@ export class OwnerApp {
334
348
  log,
335
349
  modes,
336
350
  mutes,
337
- activity: (room) => surface.activityOf(room),
351
+ activity: (room) => {
352
+ const round = surface.roundOf(room);
353
+ return { ...surface.activityOf(room), ...(round ? { round } : {}) };
354
+ },
338
355
  // The agent rooms are the owners'; where there are several, they can leave each other notes.
339
356
  readers: async () => (await identity.owners()).length > 1,
340
357
  });
@@ -398,6 +415,23 @@ export class OwnerApp {
398
415
  },
399
416
  push,
400
417
  });
418
+ // A knight clearing its own temp knights leaves a line in its room, with the undo.
419
+ agents.team.onHelpersEnded((event) => {
420
+ const named = event.names.map((name) => ({
421
+ name,
422
+ displayName: agents.directory.agent(name)?.displayName ?? name,
423
+ }));
424
+ void surface
425
+ .publish({
426
+ room: agentRoomKey(options.surface, event.parent),
427
+ kind: "system",
428
+ author: "Notice",
429
+ text: `${HELPERS_ENDED}${JSON.stringify(named)}`,
430
+ })
431
+ .catch((error) =>
432
+ logger.warn({ err: error }, "the temp knights' line was not posted"),
433
+ );
434
+ });
401
435
  this.#api = new AppApi({
402
436
  ...(options.presentation ? { presentation: options.presentation } : {}),
403
437
  surface,
@@ -0,0 +1,106 @@
1
+ import type {
2
+ AgentDirectory,
3
+ AgentTeam,
4
+ ChannelKey,
5
+ Speaker,
6
+ } from "pi-roundtable";
7
+ import { agentRoomKey, isHelper, roomOf } from "pi-roundtable";
8
+ import type { HelpersArchived, HelpersRestored } from "../api-types.ts";
9
+ import { json } from "../http.ts";
10
+ import { bad, bodyOf } from "./api-http.ts";
11
+
12
+ /** The most rooms or names one request of these routes names. */
13
+ const MAX_NAMED = 500;
14
+
15
+ export interface HelperRoutesOptions {
16
+ surface: string;
17
+ directory: Pick<AgentDirectory, "agent" | "agents">;
18
+ team: Pick<
19
+ AgentTeam,
20
+ "finishedHelpers" | "archiveHelpers" | "restoreHelpers"
21
+ >;
22
+ changed(): void;
23
+ }
24
+
25
+ /** A list of at most 500 strings, or undefined when the body leaves it out. */
26
+ function listOf(value: unknown, field: string): string[] | undefined {
27
+ if (value === undefined) return undefined;
28
+ if (
29
+ !Array.isArray(value) ||
30
+ value.length > MAX_NAMED ||
31
+ !value.every((one) => typeof one === "string" && one.length <= 300)
32
+ )
33
+ throw bad(`${field} must be a list of at most ${MAX_NAMED} strings.`);
34
+ return value;
35
+ }
36
+
37
+ /**
38
+ * The two bulk calls on temp knights, for owners: archiving the finished ones of some knights (or
39
+ * of every knight) and putting back the ones an archive took. They reach the same team operations
40
+ * as the knight's own `helper_end`.
41
+ */
42
+ export class HelperRoutes {
43
+ readonly #options: HelperRoutesOptions;
44
+
45
+ constructor(options: HelperRoutesOptions) {
46
+ this.#options = options;
47
+ }
48
+
49
+ /**
50
+ * `POST api/rooms/archive-finished-helpers`: archives the finished temp knights of the named
51
+ * knights' rooms, or of every knight when none is named. Answers what it archived by room.
52
+ */
53
+ async archiveFinished(request: Request, owner: Speaker): Promise<Response> {
54
+ const { surface, directory, team } = this.#options;
55
+ const keys = listOf((await bodyOf(request)).rooms, "rooms");
56
+ let parents: string[] | undefined;
57
+ if (keys) {
58
+ parents = [];
59
+ for (const key of new Set(keys)) {
60
+ const named = roomOf(key as ChannelKey, [surface]);
61
+ const agent =
62
+ named?.kind === "agent" ? directory.agent(named.name) : undefined;
63
+ if (!agent || isHelper(agent))
64
+ throw bad(`"${key}" is not the room of a knight.`);
65
+ parents.push(agent.name);
66
+ }
67
+ }
68
+ const finished = team.finishedHelpers(parents);
69
+ const byParent = new Map<string, string[]>();
70
+ for (const helper of finished)
71
+ byParent.set(helper.parent as string, [
72
+ ...(byParent.get(helper.parent as string) ?? []),
73
+ helper.name,
74
+ ]);
75
+ const done = new Set(
76
+ await team.archiveHelpers(
77
+ finished.map((helper) => helper.name),
78
+ owner.principalId ?? owner.name,
79
+ ),
80
+ );
81
+ const archived = [...byParent]
82
+ .map(([parent, names]) => ({
83
+ room: agentRoomKey(surface, parent),
84
+ names: names.filter((name) => done.has(name)),
85
+ }))
86
+ .filter((entry) => entry.names.length > 0);
87
+ if (archived.length > 0) this.#options.changed();
88
+ return json({ archived } satisfies HelpersArchived);
89
+ }
90
+
91
+ /**
92
+ * `POST api/rooms/restore-helpers`: puts back archived temp knights, the undo of an archive. A
93
+ * name that is not a temp knight is refused by name; one no longer archived is passed over.
94
+ */
95
+ async restore(request: Request): Promise<Response> {
96
+ const { directory, team } = this.#options;
97
+ const names = listOf((await bodyOf(request)).names, "names");
98
+ if (!names) throw bad("Give names, the temp knights to restore.");
99
+ for (const name of names)
100
+ if (!isHelper(directory.agent(name)))
101
+ throw bad(`"${name}" is not a temp knight.`);
102
+ const restored = await team.restoreHelpers([...new Set(names)]);
103
+ if (restored.length > 0) this.#options.changed();
104
+ return json({ restored } satisfies HelpersRestored);
105
+ }
106
+ }
package/src/app/rooms.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  channelKey,
13
13
  characterOf,
14
14
  groupRoomKey,
15
+ isHelper,
15
16
  roomOf,
16
17
  } from "pi-roundtable";
17
18
  import type { RoomKind, RoomView } from "../api-types.ts";
@@ -44,7 +45,7 @@ export interface RoomsOptions {
44
45
  mutes: Pick<PushMutes, "all">;
45
46
  activity(
46
47
  room: ChannelKey,
47
- ): Pick<RoomView, "typing" | "stoppable" | "phase" | "agent">;
48
+ ): Pick<RoomView, "typing" | "stoppable" | "phase" | "agent" | "round">;
48
49
  /**
49
50
  * Whether more than one person reads the agent and group rooms, so a message may be an
50
51
  * internal note. Absent: one person does, and a room takes only turns.
@@ -127,7 +128,9 @@ export class Rooms {
127
128
  const coordinator = team.coordinator();
128
129
  const home =
129
130
  (coordinator ? directory.agent(coordinator) : undefined) ??
130
- directory.agents().find((agent) => agent.status === "active");
131
+ directory
132
+ .agents()
133
+ .find((agent) => agent.status === "active" && !isHelper(agent));
131
134
  return home?.status === "active"
132
135
  ? agentRoomKey(surface, home.name)
133
136
  : undefined;
@@ -144,7 +147,7 @@ export class Rooms {
144
147
  [
145
148
  ...directory
146
149
  .agents()
147
- .filter((agent) => agent.status === "active")
150
+ .filter((agent) => agent.status === "active" && !isHelper(agent))
148
151
  .map((agent) => agentRoomKey(surface, agent.name)),
149
152
  ...directory
150
153
  .groups()
@@ -225,6 +228,18 @@ export class Rooms {
225
228
  avatar: this.avatarOf(agent),
226
229
  character: characterOf(agent),
227
230
  ...(agent.name === coordinator ? { coordinator: true as const } : {}),
231
+ ...(agent.parent
232
+ ? {
233
+ helper: {
234
+ parent: agentRoomKey(surface, agent.parent),
235
+ parentName:
236
+ directory.agent(agent.parent)?.displayName ?? agent.parent,
237
+ ...(agent.finishedAt ? { finished: true as const } : {}),
238
+ },
239
+ // A temp knight's answers reach its knight's room, which pushes; its cards still do.
240
+ muted: true as const,
241
+ }
242
+ : {}),
228
243
  }),
229
244
  ),
230
245
  ...groups.map((group: AgentGroup) =>
package/src/app/routes.ts CHANGED
@@ -11,7 +11,7 @@ import type {
11
11
  SkillRegistry,
12
12
  Speaker,
13
13
  } from "pi-roundtable";
14
- import { PluginError } from "pi-roundtable";
14
+ import { channelKey, PluginError } from "pi-roundtable";
15
15
  import { CharacterError } from "pi-roundtable/characters";
16
16
  import { AgentError } from "pi-roundtable/kit";
17
17
  import type {
@@ -19,6 +19,8 @@ import type {
19
19
  CharacterRefusal,
20
20
  MessagesView,
21
21
  ModelsView,
22
+ ReadAllUndone,
23
+ ReadAllView,
22
24
  RoomOneView,
23
25
  RoomsView,
24
26
  SchedulesView,
@@ -26,6 +28,7 @@ import type {
26
28
  SessionView,
27
29
  SkillView,
28
30
  ThreadsDismissed,
31
+ ThreadsDismissedAll,
29
32
  ThreadsRestored,
30
33
  WorkspaceConflict,
31
34
  WorkspaceNew,
@@ -38,6 +41,7 @@ import type { Authenticated } from "../auth/authenticator.ts";
38
41
  import { type ConsolePresentation, message } from "../features.ts";
39
42
  import type { Gate } from "../gate.ts";
40
43
  import type { HostApp } from "../host/host-app.ts";
44
+ import { MAX_READ_ALL } from "../host/read-all.ts";
41
45
  import type { RoomReads } from "../host/reads.ts";
42
46
  import { readsOf, withExpiredFiles } from "../host/room-views.ts";
43
47
  import { json } from "../http.ts";
@@ -47,6 +51,7 @@ import { viewerOf, type WebAppRegistry } from "../web-app.ts";
47
51
  import { bad, bodyOf, HttpError, notFound } from "./api-http.ts";
48
52
  import type { CardLog } from "./card-log.ts";
49
53
  import { type FileDir, fileResponse, isFileId } from "./files.ts";
54
+ import { HelperRoutes } from "./helper-routes.ts";
50
55
  import type { LayoutStore } from "./layout.ts";
51
56
  import { LayoutRoutes } from "./layout-routes.ts";
52
57
  import { PromptApi, type PromptApiOptions } from "./prompt-routes.ts";
@@ -66,6 +71,8 @@ import {
66
71
 
67
72
  /** The most threads one request puts back on the room list: more than the list ever shows. */
68
73
  const MAX_RESTORED = 100;
74
+ /** The most rooms one bulk request names. */
75
+ const MAX_BULK_ROOMS = MAX_READ_ALL;
69
76
 
70
77
  export interface AppApiOptions {
71
78
  /** The host's wording: a refusal reaches a client of the API in this locale when the catalog has it. */
@@ -87,6 +94,7 @@ export interface AppApiOptions {
87
94
  | "removeUnder"
88
95
  | "dismiss"
89
96
  | "dismissFinished"
97
+ | "dismissFinishedUnder"
90
98
  | "restore"
91
99
  | "restoreFinished"
92
100
  >;
@@ -111,6 +119,9 @@ export interface AppApiOptions {
111
119
  | "remove"
112
120
  | "createGroup"
113
121
  | "updateGroup"
122
+ | "finishedHelpers"
123
+ | "archiveHelpers"
124
+ | "restoreHelpers"
114
125
  >;
115
126
  /** Present when the host has the skills addon: the catalog an agent's skills are chosen from, and the change of what it carries. */
116
127
  skills?: {
@@ -132,7 +143,7 @@ export interface AppApiOptions {
132
143
  /** The read cursors, for each room's unread count and who has read what. */
133
144
  reads?: Pick<RoomReads, "unread" | "cursorOf" | "marks">;
134
145
  /** Who may be served a file bound to a host room or a page. */
135
- host?: Pick<HostApp, "mayRead">;
146
+ host?: Pick<HostApp, "mayRead" | "readAll" | "unreadAll">;
136
147
  /** The name the app shows for a person. */
137
148
  nameOf?(principalId: string): Promise<string>;
138
149
  /** Present when the workspace pane is served. */
@@ -158,6 +169,22 @@ export interface AppApiOptions {
158
169
  logger: Logger;
159
170
  }
160
171
 
172
+ /** A cursor move as the page sends it back to undo a marking. */
173
+ const isReadMove = (
174
+ value: unknown,
175
+ ): value is { room: string; from: number; to: number } => {
176
+ if (typeof value !== "object" || value === null) return false;
177
+ const { room, from, to } = value as Record<string, unknown>;
178
+ return (
179
+ typeof room === "string" &&
180
+ room.length <= 300 &&
181
+ Number.isSafeInteger(from) &&
182
+ (from as number) >= 0 &&
183
+ Number.isSafeInteger(to) &&
184
+ (to as number) >= 0
185
+ );
186
+ };
187
+
161
188
  /** A refusal of the workspace pane as the HTTP error the owner reads. */
162
189
  function refusalOf(refusal: WorkspaceRefusal): HttpError {
163
190
  const { status } = refusal;
@@ -249,6 +276,7 @@ export class AppApi {
249
276
  readonly #options: AppApiOptions;
250
277
  readonly #team: TeamApi;
251
278
  readonly #layout: LayoutRoutes;
279
+ readonly #helpers: HelperRoutes;
252
280
  readonly #prompts: PromptApi | undefined;
253
281
  readonly #skillDocs: SkillDocApi | undefined;
254
282
 
@@ -262,6 +290,12 @@ export class AppApi {
262
290
  owner: (admission) => this.#owner(admission),
263
291
  changed: () => options.changed(),
264
292
  });
293
+ this.#helpers = new HelperRoutes({
294
+ surface: options.surface.surface,
295
+ directory: options.directory,
296
+ team: options.team,
297
+ changed: () => options.changed(),
298
+ });
265
299
  this.#prompts = options.prompts
266
300
  ? new PromptApi({ prompts: options.prompts })
267
301
  : undefined;
@@ -508,6 +542,20 @@ export class AppApi {
508
542
  ...(await this.#layout.view()),
509
543
  } satisfies RoomsView);
510
544
  }
545
+ // No room key is a bare word: the bulk calls take that place.
546
+ if (key === "read-all" && !action && method === "POST")
547
+ return this.#readAll(request, admission);
548
+ if (key === "dismiss-finished-threads" && !action && method === "POST")
549
+ return this.#dismissAllFinished(request, admission);
550
+ if (key === "archive-finished-helpers" && !action && method === "POST")
551
+ return this.#helpers.archiveFinished(
552
+ request,
553
+ await this.#owner(admission),
554
+ );
555
+ if (key === "restore-helpers" && !action && method === "POST") {
556
+ await this.#owner(admission);
557
+ return this.#helpers.restore(request);
558
+ }
511
559
  const room = await rooms.resolve(key);
512
560
  if (!room) throw notFound();
513
561
  if (!action && method === "GET") {
@@ -605,6 +653,81 @@ export class AppApi {
605
653
  throw new HttpError(404, "There is no such API.");
606
654
  }
607
655
 
656
+ /**
657
+ * Reads the person's rooms up to their newest message in one call: the named ones, or every room
658
+ * the list shows. A room they may not read is passed over as if it were not named. With `restore`
659
+ * instead, the cursors a call moved go back, where the person has not read on since.
660
+ */
661
+ async #readAll(
662
+ request: Request,
663
+ admission: Authenticated,
664
+ ): Promise<Response> {
665
+ const { host } = this.#options;
666
+ if (!host) throw new HttpError(404, "There is no such API.");
667
+ const viewer = viewerOf(admission.visitor);
668
+ const body = await bodyOf(request);
669
+ if (body.restore !== undefined) {
670
+ const { restore } = body;
671
+ if (
672
+ !Array.isArray(restore) ||
673
+ restore.length > MAX_BULK_ROOMS ||
674
+ !restore.every(isReadMove)
675
+ )
676
+ throw bad("restore must be a list of at most 500 moves.");
677
+ const restored = await host.unreadAll(viewer, restore);
678
+ if (restored.length) this.#options.changed();
679
+ return json({ restored } satisfies ReadAllUndone);
680
+ }
681
+ const named = await this.#named(body.rooms);
682
+ const keys =
683
+ named ?? (await this.#options.rooms.list()).map((one) => one.key);
684
+ const moved = await host.readAll(viewer, keys);
685
+ if (moved.length) this.#options.changed();
686
+ return json({
687
+ moved: moved.map(({ room, from, to }) => ({ room, from, to })),
688
+ } satisfies ReadAllView);
689
+ }
690
+
691
+ /** Takes every finished thread under the named rooms, or under every room the list shows, off the list. */
692
+ async #dismissAllFinished(
693
+ request: Request,
694
+ admission: Authenticated,
695
+ ): Promise<Response> {
696
+ await this.#owner(admission);
697
+ const { rooms, threads, surface } = this.#options;
698
+ const named = await this.#named((await bodyOf(request)).rooms);
699
+ const listed = named ?? (await rooms.list()).map((one) => one.key);
700
+ const parents: string[] = [];
701
+ for (const key of new Set(listed)) {
702
+ const one = await rooms.resolve(key);
703
+ if (one && one.kind !== "thread") parents.push(one.key);
704
+ }
705
+ const dismissed = await threads.dismissFinishedUnder(parents);
706
+ const byParent: Record<string, string[]> = {};
707
+ for (const { id, parent } of dismissed)
708
+ byParent[parent] = [
709
+ ...(byParent[parent] ?? []),
710
+ channelKey(surface.surface, id),
711
+ ];
712
+ if (dismissed.length) this.#options.changed();
713
+ return json({
714
+ dismissed: dismissed.length,
715
+ threads: byParent,
716
+ } satisfies ThreadsDismissedAll);
717
+ }
718
+
719
+ /** The room keys a bulk body names; undefined when it names none, meaning every room. */
720
+ async #named(value: unknown): Promise<string[] | undefined> {
721
+ if (value === undefined) return undefined;
722
+ if (
723
+ !Array.isArray(value) ||
724
+ value.length > MAX_BULK_ROOMS ||
725
+ !value.every((one) => typeof one === "string" && one.length <= 300)
726
+ )
727
+ throw bad("rooms must be a list of at most 500 keys.");
728
+ return value;
729
+ }
730
+
608
731
  /** Takes one finished thread off the room list for every owner; an open one is refused. */
609
732
  async #dismiss(
610
733
  room: ResolvedRoom,