pi-roundtable-web 0.27.0 → 0.28.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 (253) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +23 -7
  3. package/dist/chunk-account-678dgbww.js +1 -0
  4. package/dist/chunk-action-menu-qpp9bdxh.js +1 -0
  5. package/dist/chunk-agent-x0yvvdq2.js +1 -0
  6. package/dist/chunk-arranged-rooms-6rdkk8t1.js +1 -0
  7. package/dist/chunk-bulk-actions-zbwdaxtc.js +1 -0
  8. package/dist/chunk-character-creator-215psy17.js +1 -0
  9. package/dist/chunk-codeeditor-jgj32247.js +2 -0
  10. package/dist/chunk-confirm-2pzrt272.js +1 -0
  11. package/dist/chunk-connectors-zha2d834.js +37 -0
  12. package/dist/chunk-conversations-fb071rez.js +1 -0
  13. package/dist/chunk-council-sheet-k5fycefd.js +2 -0
  14. package/dist/chunk-drag-04qfef58.js +1 -0
  15. package/dist/chunk-entry-mws704mc.js +1 -0
  16. package/dist/chunk-folder-list-qxhmczwv.js +1 -0
  17. package/dist/chunk-folds-b7asegfs.js +1 -0
  18. package/dist/chunk-gate-vxetvn8t.js +1 -0
  19. package/dist/chunk-group-scene-63yhh1mh.js +1 -0
  20. package/dist/chunk-group-table-zy4hmgk3.js +1 -0
  21. package/dist/chunk-host-icons-tyktazzz.js +1 -0
  22. package/dist/chunk-host-picture-ebhejn2k.js +1 -0
  23. package/dist/chunk-host-state-wajep0yr.js +1 -0
  24. package/dist/chunk-host-ui-pc9r24e3.js +1 -0
  25. package/dist/chunk-index-1af8vsyb.js +2 -0
  26. package/dist/{chunk-index-g1awv7rk.js → chunk-index-20xgx0k2.js} +2 -2
  27. package/dist/chunk-index-254701sm.js +3 -0
  28. package/dist/{chunk-index-ffbrm6tr.js → chunk-index-2822z600.js} +3 -3
  29. package/dist/{chunk-index-2fdns8tn.js → chunk-index-2hvn2kte.js} +2 -2
  30. package/dist/chunk-index-2syyyjnq.js +2 -0
  31. package/dist/chunk-index-2tr41qht.js +2 -0
  32. package/dist/chunk-index-2y8r4s4f.js +2 -0
  33. package/dist/chunk-index-31m1b2f5.js +2 -0
  34. package/dist/{chunk-index-ak6ecqy3.js → chunk-index-3gfbhmpb.js} +2 -2
  35. package/dist/chunk-index-3yy10a5h.js +2 -0
  36. package/dist/{chunk-index-sm7m4fej.js → chunk-index-4t6vnwap.js} +2 -2
  37. package/dist/{chunk-index-j4x9dq30.css → chunk-index-5hbq6g2e.css} +1 -1
  38. package/dist/chunk-index-5t2jkecy.js +2 -0
  39. package/dist/chunk-index-5vatmakn.js +2 -0
  40. package/dist/{chunk-index-zyvwhxga.js → chunk-index-6cg8xp1c.js} +1 -1
  41. package/dist/chunk-index-6yra9kdq.js +2 -0
  42. package/dist/{chunk-index-ac1ktr56.js → chunk-index-7qd604rq.js} +2 -2
  43. package/dist/chunk-index-9bdstf8t.js +2 -0
  44. package/dist/chunk-index-acj4xxgq.js +100 -0
  45. package/dist/chunk-index-aexy023z.js +2 -0
  46. package/dist/chunk-index-amxa2bn3.js +2 -0
  47. package/dist/chunk-index-bef0fc2m.js +2 -0
  48. package/dist/chunk-index-c6tphrjr.js +1 -0
  49. package/dist/chunk-index-c7dbvkrq.js +2 -0
  50. package/dist/chunk-index-cn49mawx.js +50 -0
  51. package/dist/{chunk-index-3d8s3565.js → chunk-index-e2gy763y.js} +2 -2
  52. package/dist/{chunk-index-31x6krbs.js → chunk-index-e35v554x.js} +3 -3
  53. package/dist/chunk-index-fepsbhr6.js +47 -0
  54. package/dist/chunk-index-gcmmwh8t.js +2 -0
  55. package/dist/{chunk-index-kbrg32sx.js → chunk-index-gwkhs8s3.js} +2 -2
  56. package/dist/chunk-index-h2vhv4kh.js +2 -0
  57. package/dist/chunk-index-h9ccz1r3.js +2 -0
  58. package/dist/{chunk-index-0w4bfjb5.js → chunk-index-jc3r4qam.js} +2 -2
  59. package/dist/{chunk-index-8d81qy69.js → chunk-index-m81362sc.js} +2 -2
  60. package/dist/{chunk-index-xq84gg8w.js → chunk-index-n0cmnwd2.js} +2 -2
  61. package/dist/chunk-index-n0ejy0wd.js +2 -0
  62. package/dist/chunk-index-nkdg1dfz.js +7 -0
  63. package/dist/chunk-index-p7jkr7gv.js +14 -0
  64. package/dist/{chunk-index-e2dqjde8.js → chunk-index-q5fyrtx4.js} +1 -1
  65. package/dist/{chunk-index-jgzygbt8.js → chunk-index-qxynr1hf.js} +2 -2
  66. package/dist/chunk-index-r7fdgzy1.js +2 -0
  67. package/dist/chunk-index-rgf4rvns.js +4 -0
  68. package/dist/chunk-index-rsd8rftk.js +2 -0
  69. package/dist/{chunk-index-rh0x9rzf.js → chunk-index-s53bxwkk.js} +2 -2
  70. package/dist/{chunk-index-xxeb2xp0.js → chunk-index-s9p27f9b.js} +3 -3
  71. package/dist/chunk-index-sdcpf5dp.js +2 -0
  72. package/dist/chunk-index-sm7kfb07.js +2 -0
  73. package/dist/chunk-index-sw9r4aa4.js +2 -0
  74. package/dist/chunk-index-t5bmsqqd.js +1 -0
  75. package/dist/chunk-index-v8mma5np.js +2 -0
  76. package/dist/{chunk-index-q7g62sgm.js → chunk-index-v9gbt5tz.js} +3 -3
  77. package/dist/chunk-index-vsfsv8hd.js +2 -0
  78. package/dist/chunk-index-wxft0aqb.js +2 -0
  79. package/dist/chunk-index-x60n1me6.js +2 -0
  80. package/dist/chunk-index-zcfr8jdv.js +13 -0
  81. package/dist/{chunk-index-w42rcbx1.js → chunk-index-zj0gjcvr.js} +2 -2
  82. package/dist/{chunk-index-2t30xwed.js → chunk-index-zt19fdmc.js} +2 -2
  83. package/dist/chunk-index-zvvwxkev.js +2 -0
  84. package/dist/chunk-lang-css-7q1dtpgm.js +1 -0
  85. package/dist/{chunk-lang-dockerfile-cnwqn660.js → chunk-lang-dockerfile-n4bt8jbt.js} +1 -1
  86. package/dist/{chunk-lang-go-yy6kgsxv.js → chunk-lang-go-9k4y107e.js} +3 -3
  87. package/dist/chunk-lang-html-xgvba8rn.js +1 -0
  88. package/dist/chunk-lang-javascript-vgxmgd0a.js +1 -0
  89. package/dist/chunk-lang-json-hr12bzs1.js +1 -0
  90. package/dist/chunk-lang-markdown-ttzcpgkg.js +6 -0
  91. package/dist/chunk-lang-python-ntj8maem.js +3 -0
  92. package/dist/chunk-lang-rust-gv8y859d.js +1 -0
  93. package/dist/{chunk-lang-shell-1rpgtepw.js → chunk-lang-shell-5zzs09b7.js} +1 -1
  94. package/dist/chunk-lang-sql-6vmze4pg.js +2 -0
  95. package/dist/{chunk-lang-toml-tbj7467c.js → chunk-lang-toml-tt0d4rw9.js} +1 -1
  96. package/dist/chunk-lang-typescript-2v652sh5.js +1 -0
  97. package/dist/chunk-lang-xml-r2sbqnjv.js +1 -0
  98. package/dist/chunk-lang-yaml-ex31qbph.js +1 -0
  99. package/dist/chunk-layout-actions-bfqtg71d.js +1 -0
  100. package/dist/chunk-menu-items-rztgekfj.js +1 -0
  101. package/dist/chunk-mode-toggle-m5kgb7c7.js +1 -0
  102. package/dist/chunk-note-row-40ypghn9.js +204 -0
  103. package/dist/chunk-notes-a8nqffbf.js +1 -0
  104. package/dist/chunk-notice-bell-rdnfmtf2.js +1 -0
  105. package/dist/{chunk-notices-fte9f196.js → chunk-notices-tefx93s6.js} +1 -1
  106. package/dist/chunk-onboarding-wvh0s3vg.js +1 -0
  107. package/dist/chunk-overview-zrx3bpwf.js +1 -0
  108. package/dist/chunk-palette-5xtdvy9a.js +1 -0
  109. package/dist/chunk-placement-items-rgyrktvy.js +1 -0
  110. package/dist/chunk-profile-tyb48b79.js +1 -0
  111. package/dist/chunk-prompt-history-wv0vdsjv.js +1 -0
  112. package/dist/chunk-prompts-75hgb0t0.js +1 -0
  113. package/dist/chunk-room-confirms-bcvhpcry.js +1 -0
  114. package/dist/chunk-room-menu-ha1evv71.js +1 -0
  115. package/dist/chunk-rooms-menu-button-ej3a2hyc.js +1 -0
  116. package/dist/chunk-schedules-a5pewkhd.js +1 -0
  117. package/dist/{chunk-seat-menu-r0k0eawr.js → chunk-seat-menu-eh3ymmks.js} +1 -1
  118. package/dist/chunk-sheet-hosts-d5zbwbbr.js +29 -0
  119. package/dist/{chunk-shortcut-help-a6pkjcq7.js → chunk-shortcut-help-bh9ntk9s.js} +2 -2
  120. package/dist/{chunk-shortcuts-cgt87jkz.js → chunk-shortcuts-6em0atk9.js} +1 -1
  121. package/dist/chunk-skill-editor-e43mwz01.js +8 -0
  122. package/dist/chunk-skills-vp107wrp.js +1 -0
  123. package/dist/chunk-start-over-line-xd1v2p0g.js +16 -0
  124. package/dist/chunk-switcher-list-dtfh87zy.js +1 -0
  125. package/dist/{chunk-tab-dot-vr11dfrw.js → chunk-tab-dot-qd640mjr.js} +1 -1
  126. package/dist/chunk-table-svb2xhfd.js +1 -0
  127. package/dist/chunk-table2d-5nrhfwft.js +1 -0
  128. package/dist/chunk-table3d-5cb66d99.js +4220 -0
  129. package/dist/chunk-thread-actions-926eab0w.js +1 -0
  130. package/dist/{chunk-thread-cards-c2mrp2gj.js → chunk-thread-cards-kgt22sbp.js} +1 -1
  131. package/dist/chunk-thread-list-j2v1pqkz.js +217 -0
  132. package/dist/chunk-transcript-a0p3vwh6.js +1 -0
  133. package/dist/chunk-waiting-cdyz7r1g.js +1 -0
  134. package/dist/chunk-waiting-chip-58ttx9av.js +28 -0
  135. package/dist/chunk-where-9jp6g029.js +1 -0
  136. package/dist/chunk-workspace-d34njy4p.js +578 -0
  137. package/dist/chunk-zh-tw-y3jb20wc.js +1 -0
  138. package/dist/index.html +1 -1
  139. package/dist/sw.js +1 -1
  140. package/package.json +3 -3
  141. package/src/api-types.ts +98 -1
  142. package/src/app/app.ts +4 -7
  143. package/src/app/desk.ts +19 -4
  144. package/src/app/room-log.ts +66 -18
  145. package/src/app/rooms.ts +15 -5
  146. package/src/app/routes.ts +60 -9
  147. package/src/app/skill-doc-routes.ts +12 -2
  148. package/src/app/surface.ts +26 -2
  149. package/src/app/team-routes.ts +3 -7
  150. package/src/authz.ts +1 -0
  151. package/src/cleanup-api.ts +9 -2
  152. package/src/connector-manage-api.ts +257 -0
  153. package/src/console-api.ts +60 -2
  154. package/src/features.ts +91 -0
  155. package/src/host/reads.ts +10 -1
  156. package/src/i18n/zh-tw.ts +107 -4
  157. package/src/web-plugin.ts +7 -1
  158. package/dist/chunk-account-1ecx5kt7.js +0 -1
  159. package/dist/chunk-action-menu-zh2n0ahf.js +0 -1
  160. package/dist/chunk-agent-ynm2tcbx.js +0 -1
  161. package/dist/chunk-arranged-rooms-qnz8vese.js +0 -1
  162. package/dist/chunk-bulk-actions-pr0mw0kc.js +0 -1
  163. package/dist/chunk-character-creator-caj4pmg4.js +0 -1
  164. package/dist/chunk-codeeditor-dga6s78p.js +0 -2
  165. package/dist/chunk-confirm-g76v5d9r.js +0 -1
  166. package/dist/chunk-connectors-j79ze3j1.js +0 -1
  167. package/dist/chunk-conversations-2farn72g.js +0 -1
  168. package/dist/chunk-council-sheet-s9sz1bzf.js +0 -2
  169. package/dist/chunk-drag-45e8222r.js +0 -1
  170. package/dist/chunk-entry-q11n47h9.js +0 -1
  171. package/dist/chunk-folder-list-4gcj7g7h.js +0 -1
  172. package/dist/chunk-folds-ten4ykfw.js +0 -1
  173. package/dist/chunk-gate-kcv1a4t0.js +0 -1
  174. package/dist/chunk-group-scene-12ghcvzd.js +0 -1
  175. package/dist/chunk-group-table-t458kdgn.js +0 -1
  176. package/dist/chunk-host-icons-h1k9k96a.js +0 -1
  177. package/dist/chunk-host-picture-3p0wzn3v.js +0 -1
  178. package/dist/chunk-host-state-7mdve7cb.js +0 -1
  179. package/dist/chunk-host-ui-bdgwa6sf.js +0 -1
  180. package/dist/chunk-index-02vfshtt.js +0 -2
  181. package/dist/chunk-index-0a81da8z.js +0 -2
  182. package/dist/chunk-index-12zyshae.js +0 -2
  183. package/dist/chunk-index-18pgxxw3.js +0 -2
  184. package/dist/chunk-index-2e4am3hw.js +0 -50
  185. package/dist/chunk-index-5bfqek1d.js +0 -2
  186. package/dist/chunk-index-64rsz6pf.js +0 -2
  187. package/dist/chunk-index-6bkcq9ka.js +0 -2
  188. package/dist/chunk-index-92zgb7my.js +0 -2
  189. package/dist/chunk-index-aamhjg5n.js +0 -2
  190. package/dist/chunk-index-b73y2gc0.js +0 -47
  191. package/dist/chunk-index-b8cpvr7y.js +0 -2
  192. package/dist/chunk-index-bhqczxr3.js +0 -2
  193. package/dist/chunk-index-cbqhzf40.js +0 -13
  194. package/dist/chunk-index-cdctnw71.js +0 -2
  195. package/dist/chunk-index-dj0tbfyb.js +0 -2
  196. package/dist/chunk-index-dnjftvtq.js +0 -2
  197. package/dist/chunk-index-ebg48jgy.js +0 -2
  198. package/dist/chunk-index-ek5mek4j.js +0 -14
  199. package/dist/chunk-index-f6q0stqx.js +0 -2
  200. package/dist/chunk-index-g3wrs02w.js +0 -1
  201. package/dist/chunk-index-j0m16cj1.js +0 -2
  202. package/dist/chunk-index-j4kk1m59.js +0 -2
  203. package/dist/chunk-index-kb3w11mn.js +0 -7
  204. package/dist/chunk-index-msqhgvfx.js +0 -2
  205. package/dist/chunk-index-pefwfm26.js +0 -2
  206. package/dist/chunk-index-pyfx77cj.js +0 -2
  207. package/dist/chunk-index-qfgkfq86.js +0 -2
  208. package/dist/chunk-index-sv9qpmhv.js +0 -1
  209. package/dist/chunk-index-t998yg8y.js +0 -2
  210. package/dist/chunk-index-tk4se135.js +0 -4
  211. package/dist/chunk-index-v6smgw77.js +0 -2
  212. package/dist/chunk-index-x4enmj72.js +0 -2
  213. package/dist/chunk-lang-css-y9133ktr.js +0 -1
  214. package/dist/chunk-lang-html-r2xmryek.js +0 -1
  215. package/dist/chunk-lang-javascript-1r6wd177.js +0 -1
  216. package/dist/chunk-lang-json-dx56g11y.js +0 -1
  217. package/dist/chunk-lang-markdown-fpcf2dfq.js +0 -6
  218. package/dist/chunk-lang-python-zzp95955.js +0 -3
  219. package/dist/chunk-lang-rust-214fh7sp.js +0 -1
  220. package/dist/chunk-lang-sql-vvp46hpj.js +0 -2
  221. package/dist/chunk-lang-typescript-y5w4tt4g.js +0 -1
  222. package/dist/chunk-lang-xml-tbcqmedk.js +0 -1
  223. package/dist/chunk-lang-yaml-jh56t3sv.js +0 -1
  224. package/dist/chunk-layout-actions-y73mpsh1.js +0 -1
  225. package/dist/chunk-menu-items-bza16bw6.js +0 -2
  226. package/dist/chunk-mode-toggle-nj9gnt3b.js +0 -1
  227. package/dist/chunk-note-row-6fmhrdng.js +0 -75
  228. package/dist/chunk-notes-wv0e6ssf.js +0 -1
  229. package/dist/chunk-notice-bell-9z27epd2.js +0 -1
  230. package/dist/chunk-onboarding-c5twahc3.js +0 -1
  231. package/dist/chunk-overview-k4ccprax.js +0 -1
  232. package/dist/chunk-palette-9rbp2fab.js +0 -1
  233. package/dist/chunk-placement-items-me598wsb.js +0 -1
  234. package/dist/chunk-profile-s7r7fcvz.js +0 -1
  235. package/dist/chunk-prompt-history-hf296e7v.js +0 -1
  236. package/dist/chunk-prompts-c8s0nhkm.js +0 -1
  237. package/dist/chunk-room-confirms-ga2sbvhc.js +0 -1
  238. package/dist/chunk-room-menu-7h4jym2b.js +0 -1
  239. package/dist/chunk-rooms-menu-button-pgtaanb6.js +0 -1
  240. package/dist/chunk-schedules-mbnwcxpc.js +0 -1
  241. package/dist/chunk-sheet-hosts-a5dfevy9.js +0 -1
  242. package/dist/chunk-skill-editor-67yk4x7t.js +0 -8
  243. package/dist/chunk-skills-rewemey4.js +0 -1
  244. package/dist/chunk-switcher-list-t8rbay2y.js +0 -1
  245. package/dist/chunk-table-52nh0azk.js +0 -1
  246. package/dist/chunk-table2d-g57vr6h0.js +0 -1
  247. package/dist/chunk-table3d-rp0w6kt4.js +0 -4220
  248. package/dist/chunk-thread-actions-qcxt1y1g.js +0 -1
  249. package/dist/chunk-thread-list-0071q455.js +0 -217
  250. package/dist/chunk-transcript-789yqysq.js +0 -1
  251. package/dist/chunk-where-hmwmme9e.js +0 -1
  252. package/dist/chunk-workspace-wr1qrtsh.js +0 -578
  253. package/dist/chunk-zh-tw-z5p5mpph.js +0 -1
@@ -1,10 +1,12 @@
1
1
  import type { SQL } from "bun";
2
2
  import type { Migration } from "pi-roundtable";
3
- import type {
4
- HostAuthorKind,
5
- PictureRef,
6
- RoomFile,
7
- RoomMessage,
3
+ import {
4
+ type HostAuthorKind,
5
+ type PictureRef,
6
+ type RoomFile,
7
+ type RoomIntercom,
8
+ type RoomMessage,
9
+ START_OVER_TEXT,
8
10
  } from "../api-types.ts";
9
11
  import { storedJson } from "../host/stored-json.ts";
10
12
 
@@ -27,6 +29,8 @@ export interface NewRoomMessage {
27
29
  at?: Date;
28
30
  /** An internal note: shown to the room's readers, never handed to the agent. */
29
31
  note?: true;
32
+ /** A message between agents, drawn as a small card. */
33
+ intercom?: RoomIntercom;
30
34
  }
31
35
 
32
36
  interface Row {
@@ -44,6 +48,7 @@ interface Row {
44
48
  picture: PictureRef | string | null;
45
49
  edited_at: Date | null;
46
50
  note: boolean | null;
51
+ intercom?: RoomIntercom | string | null;
47
52
  }
48
53
 
49
54
  /** The most messages one page of history holds. */
@@ -70,6 +75,9 @@ function messageOf(row: Row): RoomMessage {
70
75
  }),
71
76
  ...(row.edited_at === null ? {} : { edited: true as const }),
72
77
  ...(row.note ? { note: true as const } : {}),
78
+ ...(row.intercom == null
79
+ ? {}
80
+ : { intercom: storedJson(row.intercom, "intercom card") }),
73
81
  };
74
82
  }
75
83
 
@@ -128,6 +136,22 @@ export class RoomLog {
128
136
  },
129
137
  };
130
138
 
139
+ /** The column that marks a message between agents, with which way it went and the other agent. */
140
+ static readonly intercomMigration: Migration = {
141
+ name: "web-room-messages-intercom",
142
+ up: async (sql) => {
143
+ await sql`ALTER TABLE web_room_messages ADD COLUMN IF NOT EXISTS intercom jsonb`;
144
+ },
145
+ };
146
+
147
+ /** Finds a room's start-over lines without reading the whole room. */
148
+ static readonly startOverMigration: Migration = {
149
+ name: "web-room-messages-start-over",
150
+ up: async (sql) => {
151
+ await sql`CREATE INDEX IF NOT EXISTS web_room_messages_start_over ON web_room_messages (room, id) WHERE kind = 'system' AND text = 'Conversation started over'`;
152
+ },
153
+ };
154
+
131
155
  async append(message: NewRoomMessage): Promise<RoomMessage> {
132
156
  const [row] = await this.#insert(message, false);
133
157
  if (!row) throw new Error("the room log stored nothing");
@@ -153,24 +177,25 @@ export class RoomLog {
153
177
  async #insert(message: NewRoomMessage, once: boolean): Promise<Row[]> {
154
178
  const files = JSON.stringify(message.files ?? []);
155
179
  const picture = message.picture ? JSON.stringify(message.picture) : null;
180
+ const intercom = message.intercom ? JSON.stringify(message.intercom) : null;
156
181
  const at = message.at ?? new Date();
157
182
  return once
158
183
  ? await this.#sql`
159
184
  INSERT INTO web_room_messages (room, kind, principal_id, agent, author_name, text, files,
160
- author_kind, author_label, picture, reference, created_at, note)
185
+ author_kind, author_label, picture, reference, created_at, note, intercom)
161
186
  VALUES (${message.room}, ${message.kind}, ${message.principalId ?? null},
162
187
  ${message.agent ?? null}, ${message.author}, ${message.text}, ${files}::jsonb,
163
188
  ${message.authorKind ?? null}, ${message.authorLabel ?? null}, ${picture}::jsonb,
164
- ${message.reference ?? null}, ${at}, ${message.note === true})
189
+ ${message.reference ?? null}, ${at}, ${message.note === true}, ${intercom}::jsonb)
165
190
  ON CONFLICT (room, reference) WHERE reference IS NOT NULL DO NOTHING
166
191
  RETURNING *`
167
192
  : await this.#sql`
168
193
  INSERT INTO web_room_messages (room, kind, principal_id, agent, author_name, text, files,
169
- author_kind, author_label, picture, reference, created_at, note)
194
+ author_kind, author_label, picture, reference, created_at, note, intercom)
170
195
  VALUES (${message.room}, ${message.kind}, ${message.principalId ?? null},
171
196
  ${message.agent ?? null}, ${message.author}, ${message.text}, ${files}::jsonb,
172
197
  ${message.authorKind ?? null}, ${message.authorLabel ?? null}, ${picture}::jsonb,
173
- ${message.reference ?? null}, ${at}, ${message.note === true})
198
+ ${message.reference ?? null}, ${at}, ${message.note === true}, ${intercom}::jsonb)
174
199
  RETURNING *`;
175
200
  }
176
201
 
@@ -241,27 +266,50 @@ export class RoomLog {
241
266
  return found;
242
267
  }
243
268
 
244
- /** The newest `limit` messages older than `before` (all of them when absent), oldest first. */
269
+ /**
270
+ * The newest `limit` messages older than `before` (all of them when absent), oldest first, of
271
+ * the room's conversation: from its latest start-over line, which the page includes, on. With
272
+ * `until`, the id of one of its start-over lines, those of the conversation that line ended
273
+ * instead: older than the line, back to the start-over line before it.
274
+ */
245
275
  async page(
246
276
  room: string,
247
277
  before: number | undefined,
248
278
  limit: number,
279
+ until?: number,
249
280
  ): Promise<{ messages: RoomMessage[]; more: boolean }> {
250
281
  const count = Math.min(Math.max(Math.trunc(limit), 1), PAGE_LIMIT);
251
- const rows: Row[] =
252
- before === undefined
253
- ? await this.#sql`
254
- SELECT * FROM web_room_messages WHERE room = ${room}
255
- ORDER BY id DESC LIMIT ${count + 1}`
256
- : await this.#sql`
257
- SELECT * FROM web_room_messages WHERE room = ${room} AND id < ${before}
258
- ORDER BY id DESC LIMIT ${count + 1}`;
282
+ const start = await this.startOver(room, until);
283
+ // The live conversation includes its own start line; an archived one ends before the next.
284
+ const above = until === undefined ? (start ?? 1) - 1 : (start ?? 0);
285
+ const below =
286
+ until === undefined
287
+ ? before
288
+ : before === undefined
289
+ ? until
290
+ : Math.min(before, until);
291
+ const rows: Row[] = await this.#sql`
292
+ SELECT * FROM web_room_messages
293
+ WHERE room = ${room} AND id > ${above}
294
+ AND (${below ?? null}::bigint IS NULL OR id < ${below ?? null}::bigint)
295
+ ORDER BY id DESC LIMIT ${count + 1}`;
259
296
  return {
260
297
  messages: rows.slice(0, count).map(messageOf).reverse(),
261
298
  more: rows.length > count,
262
299
  };
263
300
  }
264
301
 
302
+ /** The id of the room's latest start-over line, or of the latest one older than `before`. */
303
+ async startOver(room: string, before?: number): Promise<number | undefined> {
304
+ const [row]: { id: string | number | bigint | null }[] = await this.#sql`
305
+ SELECT max(id) AS id FROM web_room_messages
306
+ WHERE room = ${room} AND kind = 'system' AND text = ${START_OVER_TEXT}
307
+ AND (${before ?? null}::bigint IS NULL OR id < ${before ?? null}::bigint)`;
308
+ return row?.id === null || row?.id === undefined
309
+ ? undefined
310
+ : Number(row.id);
311
+ }
312
+
265
313
  /** Deletes every message of a room that is gone for good; returns how many. */
266
314
  async removeRoom(room: string): Promise<number> {
267
315
  const rows: unknown[] = await this
package/src/app/rooms.ts CHANGED
@@ -5,6 +5,7 @@ import type {
5
5
  AgentTeam,
6
6
  AvatarStudio,
7
7
  ChannelKey,
8
+ ConversationMode,
8
9
  ConversationModes,
9
10
  } from "pi-roundtable";
10
11
  import {
@@ -15,7 +16,18 @@ import {
15
16
  isHelper,
16
17
  roomOf,
17
18
  } from "pi-roundtable";
18
- import type { RoomKind, RoomView } from "../api-types.ts";
19
+ import type { RoomKind, RoomMode, RoomView } from "../api-types.ts";
20
+
21
+ /** A conversation's modes as a room shows them: an inherited one names where it comes from. */
22
+ export function roomModeOf(mode: ConversationMode): RoomMode {
23
+ const from = mode.inheritedFrom;
24
+ return {
25
+ yolo: mode.yolo,
26
+ longRunning: mode.longRunning,
27
+ ...(from ? { inheritedFrom: from.name ?? from.channel } : {}),
28
+ };
29
+ }
30
+
19
31
  import type { PushMutes } from "../push/mutes.ts";
20
32
  import type { RoomLog } from "./room-log.ts";
21
33
  import type { WebThreads } from "./threads.ts";
@@ -176,12 +188,11 @@ export class Rooms {
176
188
  const thread = await this.#options.threads.get(room.name);
177
189
  if (!thread) return undefined;
178
190
  const { modes, activity, log } = this.#options;
179
- const { yolo, longRunning } = modes.peek(key);
180
191
  const last = (await log.lastOf([key])).get(key);
181
192
  return {
182
193
  home: false,
183
194
  readOnly: true,
184
- mode: { yolo, longRunning },
195
+ mode: roomModeOf(modes.peek(key)),
185
196
  ...activity(key),
186
197
  kind: "thread",
187
198
  name: thread.id,
@@ -207,11 +218,10 @@ export class Rooms {
207
218
  key: ChannelKey,
208
219
  rest: Pick<RoomView, "kind" | "name" | "displayName"> & Partial<RoomView>,
209
220
  ): RoomView => {
210
- const { yolo, longRunning } = modes.peek(key);
211
221
  return {
212
222
  home: key === home,
213
223
  readOnly: false,
214
- mode: { yolo, longRunning },
224
+ mode: roomModeOf(modes.peek(key)),
215
225
  ...(muted.has(key) ? { muted: true as const } : {}),
216
226
  ...(notes && rest.kind !== "thread" ? { notes: true as const } : {}),
217
227
  ...activity(key),
package/src/app/routes.ts CHANGED
@@ -2,6 +2,7 @@ import type {
2
2
  AgentDirectory,
3
3
  AgentTeam,
4
4
  AvatarStudio,
5
+ ChannelKey,
5
6
  ConversationModes,
6
7
  ConversationPort,
7
8
  IdentityService,
@@ -37,6 +38,7 @@ import type {
37
38
  WorkspaceRootsView,
38
39
  WorkspaceSave,
39
40
  } from "../api-types.ts";
41
+ import { isStartOver } from "../api-types.ts";
40
42
  import type { Authenticated } from "../auth/authenticator.ts";
41
43
  import { type ConsolePresentation, message } from "../features.ts";
42
44
  import type { Gate } from "../gate.ts";
@@ -56,7 +58,7 @@ import type { LayoutStore } from "./layout.ts";
56
58
  import { LayoutRoutes } from "./layout-routes.ts";
57
59
  import { PromptApi, type PromptApiOptions } from "./prompt-routes.ts";
58
60
  import { PAGE_LIMIT, type RoomLog } from "./room-log.ts";
59
- import type { ResolvedRoom, Rooms } from "./rooms.ts";
61
+ import { type ResolvedRoom, type Rooms, roomModeOf } from "./rooms.ts";
60
62
  import { SkillDocApi } from "./skill-doc-routes.ts";
61
63
  import type { AppSurface } from "./surface.ts";
62
64
  import { TeamApi } from "./team-routes.ts";
@@ -79,12 +81,12 @@ export interface AppApiOptions {
79
81
  presentation?: ConsolePresentation;
80
82
  surface: AppSurface;
81
83
  rooms: Rooms;
82
- log: Pick<RoomLog, "page" | "lastOf" | "removeRoom">;
84
+ log: Pick<RoomLog, "page" | "lastOf" | "removeRoom" | "get" | "startOver">;
83
85
  cards: Pick<CardLog, "during" | "removeRoom">;
84
86
  uploads: Uploads;
85
87
  /** Where stored files are read from. */
86
88
  files: FileDir;
87
- modes: Pick<ConversationModes, "set" | "peek">;
89
+ modes: Pick<ConversationModes, "set" | "peek" | "forget">;
88
90
  /** The rooms whose push the owners muted. */
89
91
  mutes: Pick<PushMutes, "set" | "setMany">;
90
92
  /** The dispatch threads, which go with the room they hang under. */
@@ -131,10 +133,14 @@ export interface AppApiOptions {
131
133
  add: readonly string[],
132
134
  remove: readonly string[],
133
135
  ): Promise<string[]>;
134
- /** The registry's documents: a skill's SKILL.md read, saved and made, and its versions. */
136
+ /** The registry's documents: a skill's SKILL.md read, saved, made and deleted, and its versions. */
135
137
  documents: Pick<
136
138
  SkillRegistry,
137
- "document" | "saveDocument" | "createDocument" | "documentHistory"
139
+ | "document"
140
+ | "saveDocument"
141
+ | "createDocument"
142
+ | "documentHistory"
143
+ | "delete"
138
144
  >;
139
145
  };
140
146
  /** Present when the host has prompt documents (the prompts plugin): the main prompts the owner edits. */
@@ -580,17 +586,23 @@ export class AppApi {
580
586
  const params = new URL(request.url).searchParams;
581
587
  const before = params.get("before");
582
588
  const limit = params.get("limit");
589
+ const archive = params.get("archive");
583
590
  const beforeId = before === null ? undefined : Number(before);
591
+ const archiveId = archive === null ? undefined : Number(archive);
584
592
  const count = limit === null ? PAGE_LIMIT : Number(limit);
585
593
  if (
586
594
  (beforeId !== undefined && !Number.isInteger(beforeId)) ||
595
+ (archiveId !== undefined && !Number.isInteger(archiveId)) ||
587
596
  !Number.isInteger(count) ||
588
597
  count < 1
589
598
  )
590
- throw bad("before and limit must be whole numbers.");
599
+ throw bad("before, limit and archive must be whole numbers.");
600
+ if (archiveId !== undefined)
601
+ return json(await this.#archived(room.key, archiveId, beforeId, count));
591
602
  const page = await log.page(room.key, beforeId, count);
592
603
  // The cards stand among the messages by the time they opened; a page that has older
593
- // messages leaves the cards before its first one to the older page.
604
+ // messages leaves the cards before its first one to the older page, and a conversation
605
+ // started over leaves the archived one's cards to it.
594
606
  const first = page.messages[0];
595
607
  const { reads, nameOf } = this.#options;
596
608
  const self = admission.visitor.principal.id;
@@ -599,7 +611,9 @@ export class AppApi {
599
611
  ...page,
600
612
  messages: withExpiredFiles(page.messages, this.#options.files),
601
613
  cards: await cards.during(room.key, {
602
- ...(page.more && first ? { from: first.at } : {}),
614
+ ...(first && (page.more || isStartOver(first))
615
+ ? { from: first.at }
616
+ : {}),
603
617
  ...(beforeId === undefined ? {} : { before: beforeId }),
604
618
  }),
605
619
  ...(reads && nameOf
@@ -624,13 +638,19 @@ export class AppApi {
624
638
  }
625
639
  if (Object.keys(change).length === 0)
626
640
  throw bad("Give yolo or longRunning.");
641
+ if (modes.peek(room.key).inheritedFrom)
642
+ throw bad(
643
+ "This room follows the modes of the conversation that made it; switch them there.",
644
+ );
627
645
  const mode = await modes.set(room.key, change, owner);
628
- return json({ mode: { yolo: mode.yolo, longRunning: mode.longRunning } });
646
+ return json({ mode: roomModeOf(mode) });
629
647
  }
630
648
  if (action === "start-over" && method === "POST" && parts.length === 4) {
631
649
  if (room.readOnly) throw notFound();
632
650
  await this.#owner(admission);
633
651
  await conversations.startFresh(room.key);
652
+ // The other pages read the room list again: its unread count and last line changed.
653
+ this.#options.changed();
634
654
  return json({ ok: true });
635
655
  }
636
656
  if (
@@ -653,6 +673,37 @@ export class AppApi {
653
673
  throw new HttpError(404, "There is no such API.");
654
674
  }
655
675
 
676
+ /**
677
+ * A page of the conversation a start-over line of the room ended, read only: its messages and
678
+ * the cards that opened within it. A line that is not one of the room's start-overs names none.
679
+ */
680
+ async #archived(
681
+ room: ChannelKey,
682
+ until: number,
683
+ before: number | undefined,
684
+ count: number,
685
+ ): Promise<MessagesView> {
686
+ const { log, cards } = this.#options;
687
+ const line = await log.get(room, until);
688
+ if (!line || !isStartOver(line)) throw notFound();
689
+ const page = await log.page(room, before, count, until);
690
+ const first = page.messages[0];
691
+ const previous = page.more ? undefined : await log.startOver(room, until);
692
+ const from = page.more
693
+ ? first?.at
694
+ : previous === undefined
695
+ ? undefined
696
+ : (await log.get(room, previous))?.at;
697
+ return {
698
+ ...page,
699
+ messages: withExpiredFiles(page.messages, this.#options.files),
700
+ cards: await cards.during(room, {
701
+ ...(from === undefined ? {} : { from }),
702
+ before: before ?? until,
703
+ }),
704
+ };
705
+ }
706
+
656
707
  /**
657
708
  * Reads the person's rooms up to their newest message in one call: the named ones, or every room
658
709
  * the list shows. A room they may not read is passed over as if it were not named. With `restore`
@@ -2,6 +2,7 @@ import type { SkillDocument, SkillRegistry } from "pi-roundtable";
2
2
  import { SkillConflictError } from "pi-roundtable";
3
3
  import type {
4
4
  SkillConflict,
5
+ SkillDeleted,
5
6
  SkillDocCreate,
6
7
  SkillDocSave,
7
8
  SkillDocView,
@@ -18,7 +19,11 @@ const MAX_SKILL_BYTES = 64 * 1024;
18
19
  export interface SkillDocApiOptions {
19
20
  skills: Pick<
20
21
  SkillRegistry,
21
- "document" | "saveDocument" | "createDocument" | "documentHistory"
22
+ | "document"
23
+ | "saveDocument"
24
+ | "createDocument"
25
+ | "documentHistory"
26
+ | "delete"
22
27
  >;
23
28
  /** The catalog the page lists, so a name it does not list is a 404 before the registry is asked. */
24
29
  catalog(): SkillView[];
@@ -34,7 +39,7 @@ const viewOf = (document: SkillDocument): SkillDocView => ({
34
39
 
35
40
  /**
36
41
  * The app's REST for a skill's SKILL.md: reading it whole, saving an agent-written skill's, making a
37
- * skill and listing the versions kept. The frontmatter rules are the skill tools': the registry
42
+ * skill, deleting an agent-written one and listing the versions kept. The frontmatter rules are the skill tools': the registry
38
43
  * checks them, and a refusal is a 400 with its words. Every call is an owner's; a save names the
39
44
  * hash it was edited from, and a skill that changed meanwhile answers 409 with what is there now.
40
45
  */
@@ -106,6 +111,11 @@ export class SkillDocApi {
106
111
  throw error;
107
112
  }
108
113
  }
114
+ // The registry refuses a skill it did not write, in its own words; deleting detaches it from every carrier.
115
+ if (!action && method === "DELETE")
116
+ return json({
117
+ message: await skills.delete(name),
118
+ } satisfies SkillDeleted);
109
119
  if (action === "history" && method === "GET" && parts.length === 4) {
110
120
  return json({
111
121
  versions: (await skills.documentHistory(name)).map((version) => ({
@@ -14,7 +14,12 @@ import {
14
14
  roomOf,
15
15
  type TurnProgress,
16
16
  } from "pi-roundtable";
17
- import type { RoomFile, RoomMessage, RoundView } from "../api-types.ts";
17
+ import {
18
+ type RoomFile,
19
+ type RoomMessage,
20
+ type RoundView,
21
+ START_OVER_TEXT,
22
+ } from "../api-types.ts";
18
23
  import type { OwnerDesk } from "./desk.ts";
19
24
  import { type FileBinding, type FileDir, typeByName } from "./files.ts";
20
25
  import type { NewRoomMessage, RoomLog } from "./room-log.ts";
@@ -314,6 +319,23 @@ export class AppSurface implements ChatSurface {
314
319
  }
315
320
 
316
321
  async sendReply(channel: ChannelKey, reply: OutboundReply): Promise<void> {
322
+ // A message between agents is a card beside the room's turn: it neither ends the turn nor
323
+ // names who speaks in it, and it notifies nobody.
324
+ if (reply.intercom) {
325
+ const { kind, peer, text } = reply.intercom;
326
+ await this.publish({
327
+ room: channel,
328
+ kind: "agent",
329
+ ...(reply.author?.agent ? { agent: reply.author.agent } : {}),
330
+ author: reply.author?.name ?? ASSISTANT,
331
+ text,
332
+ intercom: {
333
+ kind,
334
+ peer: { name: peer.name, displayName: peer.displayName },
335
+ },
336
+ });
337
+ return;
338
+ }
317
339
  this.#speaks(channel, reply.author?.agent);
318
340
  const entry = this.#activity.get(channel);
319
341
  if (entry) entry.outcome = reply.failed ? "failed" : "done";
@@ -357,7 +379,7 @@ export class AppSurface implements ChatSurface {
357
379
  room: channel,
358
380
  kind: "system",
359
381
  author: ASSISTANT,
360
- text: "Conversation started over",
382
+ text: START_OVER_TEXT,
361
383
  });
362
384
  }
363
385
 
@@ -367,6 +389,8 @@ export class AppSurface implements ChatSurface {
367
389
 
368
390
  interim(channel: ChannelKey, as?: PostAuthor): InterimPosts {
369
391
  return {
392
+ // The room's progress pane shows the tools a turn calls, so its live text leaves them out.
393
+ tools: false,
370
394
  post: async (text) => {
371
395
  this.#speaks(channel, as?.agent);
372
396
  const live = crypto.randomUUID();
@@ -76,7 +76,7 @@ export interface TeamApiOptions {
76
76
  log: Pick<RoomLog, "removeRoom">;
77
77
  cards: Pick<CardLog, "removeRoom">;
78
78
  mutes: Pick<PushMutes, "set">;
79
- modes: Pick<ConversationModes, "set">;
79
+ modes: Pick<ConversationModes, "forget">;
80
80
  /** The dispatch threads under a room, which go with it. */
81
81
  threads: Pick<WebThreads, "under" | "removeUnder">;
82
82
  /** The owners' shared arrangement, which drops a deleted room's pin and folder place. */
@@ -467,11 +467,7 @@ export class TeamApi {
467
467
  await cards.removeRoom(room);
468
468
  await mutes.set(room, false, by.principalId);
469
469
  await layout.forget(room);
470
- // A name made again later starts with the modes of a new room.
471
- await modes.set(
472
- room as ChannelKey,
473
- { yolo: false, longRunning: false },
474
- by,
475
- );
470
+ // A name made again later starts with the modes of a new room, and follows nothing.
471
+ await modes.forget(room as ChannelKey);
476
472
  }
477
473
  }
package/src/authz.ts CHANGED
@@ -42,6 +42,7 @@ export const ROUTES: readonly Rule[] = [
42
42
  row("*", "api/skills", "O"),
43
43
  row("*", "api/skills/:name", "O"),
44
44
  row("*", "api/connectors", "O"),
45
+ row("*", "api/connectors/**", "O"),
45
46
  row("*", "api/models", "O"),
46
47
  // The owner's conversations; a member's own are filtered inside the handler.
47
48
  row("*", "api/conversations", "V"),
@@ -22,10 +22,16 @@ export async function cleanupConversation(
22
22
  const cleanup = features?.cleanup;
23
23
  const key = raw as ChannelKey;
24
24
  const parsed = parseKey(raw);
25
- if (!cleanup || !parsed || parsed.kind === "group" || exclude?.(key))
26
- throw new HttpError(404, "There is no such conversation.");
25
+ // An agent's or group's room in the web app is named by its room key, which no session directory has.
27
26
  const owned =
28
27
  Boolean(team?.owns(key)) || Boolean(features?.party?.contains(key));
28
+ if (
29
+ !cleanup ||
30
+ (!parsed && !owned) ||
31
+ parsed?.kind === "group" ||
32
+ exclude?.(key)
33
+ )
34
+ throw new HttpError(404, "There is no such conversation.");
29
35
  const group =
30
36
  (await team?.status())?.groups.some(
31
37
  (g) => g.channelId !== undefined && `discord:${g.channelId}` === key,
@@ -33,6 +39,7 @@ export async function cleanupConversation(
33
39
  const stored =
34
40
  !owned &&
35
41
  !group &&
42
+ parsed !== undefined &&
36
43
  Boolean(conversationFiles(join(sessionsDir, parsed.dir)));
37
44
  if (action === "delete") {
38
45
  if (!stored)