pi-roundtable-web 0.26.0 → 0.26.2

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 (226) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +32 -12
  3. package/dist/chunk-account-nhpd9j90.js +1 -0
  4. package/dist/chunk-action-menu-ec9jqtxq.js +1 -0
  5. package/dist/chunk-agent-d1xfrawq.js +1 -0
  6. package/dist/chunk-arranged-rooms-bga8yk2v.js +1 -0
  7. package/dist/chunk-character-creator-k67f77w5.js +1 -0
  8. package/dist/chunk-codeeditor-jm2sm333.js +2 -0
  9. package/dist/chunk-confirm-wc5xraev.js +1 -0
  10. package/dist/chunk-connectors-mhnw6d79.js +1 -0
  11. package/dist/chunk-conversations-yjyq4g1y.js +1 -0
  12. package/dist/{chunk-drag-v41gx2mc.js → chunk-drag-nyq52ck7.js} +1 -1
  13. package/dist/chunk-entry-red1597w.js +1 -0
  14. package/dist/chunk-folder-list-x56y26yq.js +1 -0
  15. package/dist/chunk-folds-3p7s2nvx.js +1 -0
  16. package/dist/{chunk-gate-8k61stsc.js → chunk-gate-h41bv4y9.js} +1 -1
  17. package/dist/chunk-group-scene-7eb6jyev.js +1 -0
  18. package/dist/chunk-group-table-rca48kt0.js +1 -0
  19. package/dist/chunk-host-icons-x318gjqp.js +1 -0
  20. package/dist/chunk-host-picture-xswcakgy.js +1 -0
  21. package/dist/chunk-host-state-bpahvk54.js +1 -0
  22. package/dist/chunk-host-ui-tsbwwk38.js +1 -0
  23. package/dist/{chunk-index-hby0ap91.js → chunk-index-029209sy.js} +2 -2
  24. package/dist/chunk-index-066vzexw.js +2 -0
  25. package/dist/chunk-index-0h4h76kw.js +2 -0
  26. package/dist/chunk-index-0n6ja7my.js +1 -0
  27. package/dist/chunk-index-0q15ysrb.js +1 -0
  28. package/dist/chunk-index-0xj30mzw.js +13 -0
  29. package/dist/{chunk-index-rhtvcbsq.js → chunk-index-1f0j335v.js} +2 -2
  30. package/dist/chunk-index-2f5nfekb.js +8 -0
  31. package/dist/chunk-index-31ays46f.js +2 -0
  32. package/dist/chunk-index-43k1rjmx.js +2 -0
  33. package/dist/chunk-index-4gk88a0x.js +4 -0
  34. package/dist/chunk-index-4jf4jvfd.css +1 -0
  35. package/dist/chunk-index-4mf29sbx.js +2 -0
  36. package/dist/chunk-index-51n20yhb.js +2 -0
  37. package/dist/{chunk-index-014rjv9k.js → chunk-index-6tpr6r6d.js} +2 -2
  38. package/dist/{chunk-index-e5sd30xy.js → chunk-index-7bbqtdce.js} +2 -2
  39. package/dist/chunk-index-84v7j6vh.js +50 -0
  40. package/dist/chunk-index-8f9jer8r.js +2 -0
  41. package/dist/chunk-index-91z3g774.js +1 -0
  42. package/dist/{chunk-index-dhpt8qr9.js → chunk-index-934zgd18.js} +2 -2
  43. package/dist/{chunk-index-awx6nnta.js → chunk-index-9nents16.js} +2 -2
  44. package/dist/chunk-index-9w0sar6s.js +2 -0
  45. package/dist/chunk-index-a768zmp6.js +2 -0
  46. package/dist/{chunk-index-nqfkwkfp.js → chunk-index-b7jjca66.js} +2 -2
  47. package/dist/chunk-index-c5x70d9g.js +2 -0
  48. package/dist/chunk-index-cmxs5y22.js +42 -0
  49. package/dist/chunk-index-cwc1vvpw.js +2 -0
  50. package/dist/chunk-index-d85x96q0.js +2 -0
  51. package/dist/chunk-index-datrrs6b.js +2 -0
  52. package/dist/chunk-index-dep2e3f3.js +2 -0
  53. package/dist/chunk-index-dtste2ed.js +2 -0
  54. package/dist/chunk-index-ewj52be9.js +2 -0
  55. package/dist/{chunk-index-pcfhpm8v.js → chunk-index-f3b5nch2.js} +142 -18
  56. package/dist/{chunk-index-km40bhsz.js → chunk-index-frwa2fmc.js} +2 -2
  57. package/dist/chunk-index-grkczq2c.js +2 -0
  58. package/dist/chunk-index-gzvkpe3e.js +2 -0
  59. package/dist/chunk-index-kfv996qe.js +920 -0
  60. package/dist/chunk-index-kw85ajap.js +138 -0
  61. package/dist/chunk-index-satam151.js +2 -0
  62. package/dist/{chunk-index-y8rnq8zf.js → chunk-index-smw1n2pt.js} +5 -5
  63. package/dist/chunk-index-th43f690.js +2 -0
  64. package/dist/chunk-index-ths2hxpe.js +1 -0
  65. package/dist/{chunk-index-wnjp6wv0.js → chunk-index-tmx30r1w.js} +2 -2
  66. package/dist/{chunk-index-hssjbr1k.js → chunk-index-y00wke93.js} +2 -2
  67. package/dist/chunk-index-y4sqmenv.js +2 -0
  68. package/dist/{chunk-index-66nskrdj.js → chunk-index-yrhbhmxz.js} +2 -2
  69. package/dist/chunk-index-zd76pr2k.js +2 -0
  70. package/dist/chunk-lang-css-fa7c0zfj.js +1 -0
  71. package/dist/{chunk-lang-dockerfile-hrav55t7.js → chunk-lang-dockerfile-46rk929v.js} +1 -1
  72. package/dist/{chunk-lang-go-x9ahv6e5.js → chunk-lang-go-qb0qdpg7.js} +3 -3
  73. package/dist/chunk-lang-html-47mf2y0v.js +1 -0
  74. package/dist/chunk-lang-javascript-d32qrvxz.js +1 -0
  75. package/dist/{chunk-lang-json-hsemyd0r.js → chunk-lang-json-1q4fp694.js} +1 -1
  76. package/dist/{chunk-lang-markdown-zkwb4c9n.js → chunk-lang-markdown-5w7t28wa.js} +4 -4
  77. package/dist/{chunk-lang-python-tewx7006.js → chunk-lang-python-3dq0p4rf.js} +2 -2
  78. package/dist/{chunk-lang-rust-0ydx84s1.js → chunk-lang-rust-70pa0agr.js} +1 -1
  79. package/dist/{chunk-lang-shell-gq1tmdz4.js → chunk-lang-shell-4pxt7hf4.js} +1 -1
  80. package/dist/{chunk-lang-sql-xt1qyycg.js → chunk-lang-sql-kgcbh2a9.js} +2 -2
  81. package/dist/{chunk-lang-toml-0hamc5fc.js → chunk-lang-toml-99t95pab.js} +1 -1
  82. package/dist/chunk-lang-typescript-4k1d8z7s.js +1 -0
  83. package/dist/{chunk-lang-xml-wf6h5pt2.js → chunk-lang-xml-mnbv9yfa.js} +1 -1
  84. package/dist/{chunk-lang-yaml-7vk521h1.js → chunk-lang-yaml-2by9g1dy.js} +1 -1
  85. package/dist/chunk-layout-actions-cgghzj1w.js +1 -0
  86. package/dist/chunk-menu-items-24cfwawa.js +2 -0
  87. package/dist/chunk-mode-toggle-v4baz3r1.js +1 -0
  88. package/dist/chunk-note-row-72n6cqyk.js +75 -0
  89. package/dist/chunk-notes-83s3tsey.js +1 -0
  90. package/dist/chunk-notice-bell-a4ty5h1p.js +1 -0
  91. package/dist/chunk-notices-evny44be.js +1 -0
  92. package/dist/chunk-onboarding-q1avjbrx.js +1 -0
  93. package/dist/chunk-overview-42h45ehd.js +1 -0
  94. package/dist/chunk-palette-e44qepmh.js +1 -0
  95. package/dist/chunk-placement-items-v466t0sv.js +1 -0
  96. package/dist/chunk-profile-bqcgkgk4.js +1 -0
  97. package/dist/chunk-prompt-history-hgbtcn4b.js +1 -0
  98. package/dist/chunk-prompts-czs8y6r1.js +1 -0
  99. package/dist/chunk-room-confirms-h8cdj3ng.js +1 -0
  100. package/dist/chunk-room-menu-7s0feajw.js +1 -0
  101. package/dist/chunk-rooms-menu-button-gawqvpsr.js +1 -0
  102. package/dist/{chunk-schedules-2ft6xxyw.js → chunk-schedules-zba7krac.js} +1 -1
  103. package/dist/{chunk-seat-menu-d3npdtx1.js → chunk-seat-menu-ca5p4e9q.js} +1 -1
  104. package/dist/chunk-sheet-hosts-ssbz6hf2.js +1 -0
  105. package/dist/chunk-skill-editor-n0y9pd90.js +8 -0
  106. package/dist/chunk-skills-ea4vb2s4.js +1 -0
  107. package/dist/chunk-switcher-list-pj5csqr9.js +1 -0
  108. package/dist/chunk-tab-dot-4tkpxqeh.js +1 -0
  109. package/dist/chunk-table-j8nmtdnn.js +1 -0
  110. package/dist/chunk-table-seats-qeytqe0m.js +1 -0
  111. package/dist/chunk-table2d-18y029k9.js +1 -0
  112. package/dist/chunk-table3d-q2s31ae0.js +4220 -0
  113. package/dist/chunk-thread-actions-w3eadnh7.js +1 -0
  114. package/dist/chunk-thread-cards-fbpky3c9.js +1 -0
  115. package/dist/chunk-thread-list-pghe84be.js +1 -0
  116. package/dist/chunk-transcript-eqzxgqys.js +1 -0
  117. package/dist/chunk-where-ygyr4jdj.js +1 -0
  118. package/dist/chunk-workspace-h0p32ygy.js +578 -0
  119. package/dist/chunk-zh-tw-k6xx1hp2.js +1 -0
  120. package/dist/index.html +1 -2
  121. package/dist/sw.js +1 -1
  122. package/package.json +2 -2
  123. package/src/api-types.ts +63 -3
  124. package/src/app/api-http.ts +1 -1
  125. package/src/app/app.ts +5 -0
  126. package/src/app/room-log.ts +16 -4
  127. package/src/app/rooms.ts +39 -0
  128. package/src/app/routes.ts +96 -2
  129. package/src/app/socket.ts +45 -9
  130. package/src/app/surface-threads.ts +1 -1
  131. package/src/app/surface.ts +2 -1
  132. package/src/app/threads.ts +51 -2
  133. package/src/auth/authenticator.ts +18 -5
  134. package/src/auth/login-page.ts +4 -2
  135. package/src/auth/oidc-pages.ts +2 -0
  136. package/src/auth/oidc.ts +39 -1
  137. package/src/auth/sessions.ts +10 -3
  138. package/src/authz.ts +2 -1
  139. package/src/console-server.ts +32 -6
  140. package/src/host/cards.ts +18 -5
  141. package/src/host/context.ts +72 -25
  142. package/src/host/forms.ts +3 -0
  143. package/src/host/frames.ts +8 -1
  144. package/src/host/host-app.ts +6 -4
  145. package/src/host/host-routes.ts +36 -3
  146. package/src/host/room-views.ts +6 -6
  147. package/src/host/rooms-service.ts +16 -1
  148. package/src/host/views.ts +3 -0
  149. package/src/i18n/zh-tw.ts +103 -79
  150. package/src/index.ts +15 -1
  151. package/src/options.ts +37 -1
  152. package/src/page-presentation.ts +47 -2
  153. package/src/pwa.ts +32 -4
  154. package/src/web-app.ts +47 -3
  155. package/src/web-plugin.ts +32 -10
  156. package/dist/chunk-account-hw49gd0d.js +0 -1
  157. package/dist/chunk-action-menu-rm5cyqkj.js +0 -1
  158. package/dist/chunk-agent-b017vswv.js +0 -1
  159. package/dist/chunk-arranged-rooms-q5v3dm1r.js +0 -1
  160. package/dist/chunk-character-creator-5dxcvd16.js +0 -1
  161. package/dist/chunk-codeeditor-pyt09fwg.js +0 -2
  162. package/dist/chunk-confirm-521yqmta.js +0 -1
  163. package/dist/chunk-connectors-hrm06nx8.js +0 -1
  164. package/dist/chunk-conversations-e26rrf01.js +0 -1
  165. package/dist/chunk-entry-jxxb3ath.js +0 -1
  166. package/dist/chunk-folder-list-b97xk12b.js +0 -1
  167. package/dist/chunk-folds-b8q7eags.js +0 -1
  168. package/dist/chunk-group-scene-dx6wt19m.js +0 -1
  169. package/dist/chunk-group-table-e4jr8r3a.js +0 -1
  170. package/dist/chunk-host-state-g7e7c0k5.js +0 -1
  171. package/dist/chunk-host-ui-x7k6n07v.js +0 -939
  172. package/dist/chunk-index-2d0kg096.js +0 -2
  173. package/dist/chunk-index-2qd81wbx.js +0 -2
  174. package/dist/chunk-index-2stynxw6.css +0 -1
  175. package/dist/chunk-index-459drsft.js +0 -2
  176. package/dist/chunk-index-4mfj596y.js +0 -2
  177. package/dist/chunk-index-bbsytycb.js +0 -2
  178. package/dist/chunk-index-c07p8k0x.js +0 -4
  179. package/dist/chunk-index-cq1e9xdz.js +0 -2
  180. package/dist/chunk-index-d1prgfbp.js +0 -2
  181. package/dist/chunk-index-g351921r.js +0 -1
  182. package/dist/chunk-index-k2qq1gdj.js +0 -2
  183. package/dist/chunk-index-ke9ynm0n.js +0 -2
  184. package/dist/chunk-index-kpscvg85.js +0 -2
  185. package/dist/chunk-index-m0d100mg.js +0 -2
  186. package/dist/chunk-index-mb72snk9.js +0 -2
  187. package/dist/chunk-index-mjqqver5.js +0 -4
  188. package/dist/chunk-index-nwqk447w.js +0 -2
  189. package/dist/chunk-index-q8h8xzbm.js +0 -2
  190. package/dist/chunk-index-r5vszgfs.js +0 -2
  191. package/dist/chunk-index-rjv7vrwd.js +0 -13
  192. package/dist/chunk-index-v23m38p6.js +0 -50
  193. package/dist/chunk-index-va0g7d88.js +0 -1
  194. package/dist/chunk-index-xgkd15n3.js +0 -2
  195. package/dist/chunk-index-z3k18atc.js +0 -2
  196. package/dist/chunk-lang-css-6bepqpnx.js +0 -1
  197. package/dist/chunk-lang-html-8jc72zt4.js +0 -1
  198. package/dist/chunk-lang-javascript-2p2qtbnf.js +0 -1
  199. package/dist/chunk-lang-typescript-82xf7z2e.js +0 -1
  200. package/dist/chunk-layout-actions-zh7bezk7.js +0 -1
  201. package/dist/chunk-menu-items-1n9qargb.js +0 -2
  202. package/dist/chunk-notes-t15bh04m.js +0 -1
  203. package/dist/chunk-notice-bell-w3y0h0sr.js +0 -1
  204. package/dist/chunk-notices-fa3g7qze.js +0 -1
  205. package/dist/chunk-onboarding-34v3nr17.js +0 -1
  206. package/dist/chunk-overview-km5vq375.js +0 -1
  207. package/dist/chunk-palette-a8txdgd0.js +0 -1
  208. package/dist/chunk-placement-items-wgaxsbvs.js +0 -1
  209. package/dist/chunk-profile-ypxdbsk1.js +0 -1
  210. package/dist/chunk-prompt-history-wm9dsy6s.js +0 -1
  211. package/dist/chunk-prompts-7mhhq7zs.js +0 -1
  212. package/dist/chunk-room-confirms-t11a1gsw.js +0 -1
  213. package/dist/chunk-room-menu-2tm2xtfx.js +0 -1
  214. package/dist/chunk-rooms-menu-button-wxnsxjwc.js +0 -1
  215. package/dist/chunk-sheet-hosts-vngvp7wv.js +0 -1
  216. package/dist/chunk-skill-editor-65n2hsq4.js +0 -8
  217. package/dist/chunk-skills-shkdwes8.js +0 -1
  218. package/dist/chunk-switcher-list-g6hhmr1t.js +0 -1
  219. package/dist/chunk-table-1b6bsq2y.js +0 -1
  220. package/dist/chunk-table-seats-vy1qhdks.js +0 -1
  221. package/dist/chunk-table2d-n26b7gd0.js +0 -1
  222. package/dist/chunk-table3d-8aysyf56.js +0 -4220
  223. package/dist/chunk-transcript-355faceq.js +0 -1
  224. package/dist/chunk-where-j5qdt8a4.js +0 -1
  225. package/dist/chunk-workspace-vrbs6zx1.js +0 -1
  226. package/dist/chunk-zh-tw-zeg600bk.js +0 -1
@@ -54,22 +54,46 @@ function sourceOf(message: RoomMessage): Source {
54
54
  };
55
55
  }
56
56
 
57
+ /** What `WebApp.rooms.context` takes. */
58
+ export interface HostRoomContextOptions {
59
+ /** The most messages to return, newest kept; default 15, at most 100. */
60
+ limit?: number;
61
+ /** The most characters of one message before it is cut with `…`; default 500. */
62
+ messageChars?: number;
63
+ }
64
+
65
+ /** The two ways a host room's recent messages are read. */
66
+ export interface HostRoomContext {
67
+ /** For a turn the room's message addressed: marks what it returns as handed over. */
68
+ read(
69
+ message: InboundMessage,
70
+ options?: ChannelContextOptions,
71
+ ): Promise<ChannelContext | undefined>;
72
+ /** For a host with no addressing message: the same block, and nothing is marked handed over. */
73
+ peek(
74
+ room: string,
75
+ options?: HostRoomContextOptions,
76
+ ): Promise<ChannelContext | undefined>;
77
+ }
78
+
57
79
  /**
58
- * The app surface's `channelContext` for a host room: what was said in it since the assistant
80
+ * The app surface's channel context of a host room: what was said in it since the assistant
59
81
  * last posted, before the message that addressed it, as the core's block of channel messages. An
60
82
  * outsider's words read as `external`, a notice line is not in the channel as far as the context
61
83
  * goes, and a message already handed to the conversation is not handed again. Anything but a host
62
- * room has none: an agent room is one person's.
84
+ * room has none: an agent room is one person's. `peek` reads the same block for the end of the
85
+ * room and leaves the handed-over marks alone, so a retry gets the same.
63
86
  */
64
- export function hostChannelContext(
87
+ export function hostRoomContext(
65
88
  ctx: Pick<HostContext, "registry" | "log">,
66
- ): (
67
- message: InboundMessage,
68
- options?: ChannelContextOptions,
69
- ) => Promise<ChannelContext | undefined> {
89
+ ): HostRoomContext {
70
90
  const seen = new Map<string, Set<string>>();
71
- return async (message, options = {}) => {
72
- const room = message.channel;
91
+ const build = async (
92
+ room: string,
93
+ addressed: number | undefined,
94
+ options: ChannelContextOptions,
95
+ consume: boolean,
96
+ ): Promise<ChannelContext | undefined> => {
73
97
  if (!ctx.registry.isHostRoom(room)) return undefined;
74
98
  const fetch = positive("fetch", options.fetch ?? DEFAULT_FETCH, PAGE_LIMIT);
75
99
  const keep = positive("keep", options.keep ?? DEFAULT_KEEP, 100);
@@ -78,12 +102,7 @@ export function hostChannelContext(
78
102
  options.messageChars ?? DEFAULT_CHARS,
79
103
  20_000,
80
104
  );
81
- const addressed = /^app-(\d+)$/.exec(message.messageId)?.[1];
82
- const page = await ctx.log.page(
83
- room,
84
- addressed === undefined ? undefined : Number(addressed),
85
- fetch,
86
- );
105
+ const page = await ctx.log.page(room, addressed, fetch);
87
106
  const sources = page.messages.map((one) => ({
88
107
  source: sourceOf(one),
89
108
  notice: authorKindOf(one) === "notice",
@@ -99,24 +118,26 @@ export function hostChannelContext(
99
118
  const lastEnd = considered.findLastIndex(
100
119
  (entry) => verdict(entry) === "end",
101
120
  );
102
- const known = seen.get(room) ?? new Set<string>();
121
+ const known = consume ? (seen.get(room) ?? new Set<string>()) : undefined;
103
122
  const fresh = considered
104
123
  .slice(lastEnd + 1)
105
124
  .filter(
106
125
  (entry) =>
107
126
  verdict(entry) === "keep" &&
108
- !known.has(entry.source.id) &&
127
+ !known?.has(entry.source.id) &&
109
128
  entry.source.text !== "",
110
129
  )
111
130
  .slice(-keep);
112
- // What was in the window is not handed over again, kept or not.
113
- for (const entry of considered.slice(lastEnd + 1))
114
- known.add(entry.source.id);
115
- seen.delete(room);
116
- seen.set(room, new Set([...known].slice(-SEEN_PER_ROOM)));
117
- if (seen.size > SEEN_ROOMS) {
118
- const oldest = seen.keys().next().value;
119
- if (oldest !== undefined) seen.delete(oldest);
131
+ if (known) {
132
+ // What was in the window is not handed over again, kept or not.
133
+ for (const entry of considered.slice(lastEnd + 1))
134
+ known.add(entry.source.id);
135
+ seen.delete(room);
136
+ seen.set(room, new Set([...known].slice(-SEEN_PER_ROOM)));
137
+ if (seen.size > SEEN_ROOMS) {
138
+ const oldest = seen.keys().next().value;
139
+ if (oldest !== undefined) seen.delete(oldest);
140
+ }
120
141
  }
121
142
  if (fresh.length === 0) return undefined;
122
143
  const messages: ChannelContextMessage[] = fresh.map(({ source }) => ({
@@ -138,4 +159,30 @@ export function hostChannelContext(
138
159
  }));
139
160
  return { messages, text: formatChannelContext(messages) };
140
161
  };
162
+ return {
163
+ read(message, options = {}) {
164
+ const addressed = /^app-(\d+)$/.exec(message.messageId)?.[1];
165
+ return build(
166
+ message.channel,
167
+ addressed === undefined ? undefined : Number(addressed),
168
+ options,
169
+ true,
170
+ );
171
+ },
172
+ peek(room, options = {}) {
173
+ const limit = options.limit ?? DEFAULT_KEEP;
174
+ return build(
175
+ room,
176
+ undefined,
177
+ {
178
+ keep: limit,
179
+ fetch: Math.min(PAGE_LIMIT, Math.max(DEFAULT_FETCH, limit * 3)),
180
+ ...(options.messageChars === undefined
181
+ ? {}
182
+ : { messageChars: options.messageChars }),
183
+ },
184
+ false,
185
+ );
186
+ },
187
+ };
141
188
  }
package/src/host/forms.ts CHANGED
@@ -125,6 +125,7 @@ export function checkCard(card: {
125
125
  state?: string | undefined;
126
126
  links?: readonly { label: string; url: string }[] | undefined;
127
127
  buttons?: readonly HostAction[] | undefined;
128
+ keep?: boolean | undefined;
128
129
  }): void {
129
130
  if (typeof card.title !== "string" || card.title.length > LIMITS.title)
130
131
  fail(`a card's title is at most ${LIMITS.title} characters`);
@@ -134,6 +135,8 @@ export function checkCard(card: {
134
135
  fail(`a card's tone is note, ok, warn or risk`);
135
136
  if (card.state !== undefined && !STATES.has(card.state))
136
137
  fail(`a card's state is open, working, done, superseded or expired`);
138
+ if (card.keep !== undefined && typeof card.keep !== "boolean")
139
+ fail("a card's keep is true or false");
137
140
  const links = card.links ?? [];
138
141
  if (links.length > LIMITS.links)
139
142
  fail(`a card takes at most ${LIMITS.links} links`);
@@ -60,7 +60,7 @@ export async function actInRoom(
60
60
  if (press.card !== undefined) {
61
61
  const card = await ctx.cards.get(room, press.card);
62
62
  if (!card) return { refused: "unknown" };
63
- offered = buttonsFor(viewer, found, access, card);
63
+ offered = await buttonsFor(viewer, found, access, card);
64
64
  } else {
65
65
  const header = await found.kind
66
66
  .header?.(viewer, found.id)
@@ -146,6 +146,13 @@ export function installFrames(ctx: HostContext): void {
146
146
  connection,
147
147
  audience: { room: frame.room },
148
148
  });
149
+ // So is a turn already running: a seat at the round table shows it from the first frame, not after the next change.
150
+ const activity = ctx.app.activityOf(asRoom(frame.room));
151
+ if (activity.typing)
152
+ ctx.sockets.deliver(
153
+ { type: "activity", room: frame.room, ...activity },
154
+ { connection, audience: { room: frame.room } },
155
+ );
149
156
  },
150
157
  );
151
158
 
@@ -19,7 +19,7 @@ import type { Uploads } from "../app/uploads.ts";
19
19
  import type { Viewer, WebAppRegistry } from "../web-app.ts";
20
20
  import { HostAccess } from "./access.ts";
21
21
  import type { HostCardLog } from "./cards.ts";
22
- import { hostChannelContext } from "./context.ts";
22
+ import { type HostRoomContext, hostRoomContext } from "./context.ts";
23
23
  import { installFrames } from "./frames.ts";
24
24
  import type { HostContext } from "./host-context.ts";
25
25
  import { hostArea, pagesArea } from "./host-routes.ts";
@@ -66,7 +66,7 @@ export class HostApp {
66
66
  readonly rooms: HostRoomsService;
67
67
  readonly pages: HostPagesService;
68
68
  readonly #context: HostContext;
69
- readonly #channelContext: ReturnType<typeof hostChannelContext>;
69
+ readonly #roomContext: HostRoomContext;
70
70
 
71
71
  constructor(options: HostAppOptions) {
72
72
  const now = options.now ?? Date.now;
@@ -102,6 +102,8 @@ export class HostApp {
102
102
  maxFileBytes: options.maxFileBytes,
103
103
  roomUrl: options.roomUrl,
104
104
  listed,
105
+ context: (room, contextOptions) =>
106
+ this.#roomContext.peek(room, contextOptions),
105
107
  now,
106
108
  logger: options.logger,
107
109
  });
@@ -126,7 +128,7 @@ export class HostApp {
126
128
  now,
127
129
  logger: options.logger,
128
130
  };
129
- this.#channelContext = hostChannelContext(this.#context);
131
+ this.#roomContext = hostRoomContext(this.#context);
130
132
  }
131
133
 
132
134
  /** Plugs the pieces into the registry: who reads, the frames, the areas, and the services a host reaches. */
@@ -153,7 +155,7 @@ export class HostApp {
153
155
  message: InboundMessage,
154
156
  options?: ChannelContextOptions,
155
157
  ): Promise<ChannelContext | undefined> {
156
- return this.#channelContext(message, options);
158
+ return this.#roomContext.read(message, options);
157
159
  }
158
160
 
159
161
  /** Whether a stored file bound to a room or a page may be served to this viewer now. */
@@ -1,4 +1,4 @@
1
- import type { HostCardView, RoomMessage } from "../api-types.ts";
1
+ import type { HostCardView, RoomMessage, SendWording } from "../api-types.ts";
2
2
  import { bad, bodyOf, HttpError } from "../app/api-http.ts";
3
3
  import { PAGE_LIMIT } from "../app/room-log.ts";
4
4
  import { json } from "../http.ts";
@@ -244,14 +244,15 @@ async function open(
244
244
  ctx.log.page(room, undefined, OPEN_LIMIT),
245
245
  ctx.cards.list(room),
246
246
  ]);
247
- const cards: HostCardView[] = stored.map((card) =>
248
- cardView(viewer, found, access, card),
247
+ const cards: HostCardView[] = await Promise.all(
248
+ stored.map((card) => cardView(viewer, found, access, card)),
249
249
  );
250
250
  return {
251
251
  room,
252
252
  kind: found.kind.kind,
253
253
  id: found.id,
254
254
  access: { send: [...access.send], act: access.act },
255
+ ...wordingOf(found.kind.sendWording),
255
256
  header,
256
257
  messages: withExpiredFiles(page.messages, ctx.files),
257
258
  more: page.more,
@@ -261,6 +262,38 @@ async function open(
261
262
  };
262
263
  }
263
264
 
265
+ /** The longest label and placeholder a kind may give, in characters. */
266
+ const LABEL_CHARS = 40;
267
+ const PLACEHOLDER_CHARS = 100;
268
+
269
+ const trimmed = (value: unknown, chars: number): string | undefined =>
270
+ typeof value === "string"
271
+ ? Array.from(value.trim()).slice(0, chars).join("") || undefined
272
+ : undefined;
273
+
274
+ /** The kind's wording of the two ways to write as the page may show it: text only, cut short; nothing when it gave none. */
275
+ function wordingOf(wording: SendWording | undefined): {
276
+ wording?: SendWording;
277
+ } {
278
+ const note = trimmed(wording?.note?.placeholder, PLACEHOLDER_CHARS);
279
+ const label = trimmed(wording?.turn?.label, LABEL_CHARS);
280
+ const turn = trimmed(wording?.turn?.placeholder, PLACEHOLDER_CHARS);
281
+ if (!note && !label && !turn) return {};
282
+ return {
283
+ wording: {
284
+ ...(note ? { note: { placeholder: note } } : {}),
285
+ ...(label || turn
286
+ ? {
287
+ turn: {
288
+ ...(label ? { label } : {}),
289
+ ...(turn ? { placeholder: turn } : {}),
290
+ },
291
+ }
292
+ : {}),
293
+ },
294
+ };
295
+ }
296
+
264
297
  async function older(
265
298
  ctx: HostContext,
266
299
  viewer: Viewer,
@@ -77,27 +77,27 @@ export interface FoundRoom {
77
77
  * The buttons of a host card this viewer may press: none once the card is settled, none without
78
78
  * `access.act`, and otherwise the card's own, narrowed by the kind's `buttons` when it has one.
79
79
  */
80
- export function buttonsFor(
80
+ export async function buttonsFor(
81
81
  viewer: Viewer,
82
82
  found: FoundRoom,
83
83
  access: RoomAccess,
84
84
  card: StoredHostCard,
85
- ): NonNullable<StoredHostCard["buttons"]> {
85
+ ): Promise<NonNullable<StoredHostCard["buttons"]>> {
86
86
  const all = card.buttons ?? [];
87
87
  if (!access.act || !pressable(card) || all.length === 0) return [];
88
88
  if (!found.kind.buttons) return all;
89
- const allowed = new Set(found.kind.buttons(viewer, found.id, card));
89
+ const allowed = new Set(await found.kind.buttons(viewer, found.id, card));
90
90
  return all.filter((button) => allowed.has(button.id));
91
91
  }
92
92
 
93
93
  /** A host card as one viewer sees it. */
94
- export function cardView(
94
+ export async function cardView(
95
95
  viewer: Viewer,
96
96
  found: FoundRoom,
97
97
  access: RoomAccess,
98
98
  card: StoredHostCard,
99
- ): HostCardView {
100
- const buttons = buttonsFor(viewer, found, access, card);
99
+ ): Promise<HostCardView> {
100
+ const buttons = await buttonsFor(viewer, found, access, card);
101
101
  return {
102
102
  id: card.id,
103
103
  room: card.room,
@@ -1,5 +1,6 @@
1
1
  import type { ChannelKey, Logger } from "pi-roundtable";
2
2
  import { PluginError } from "pi-roundtable";
3
+ import type { ChannelContext } from "pi-roundtable/discord";
3
4
  import type { ServerFrame } from "../api-types.ts";
4
5
  import { MAX_MESSAGE_CHARS } from "../api-types.ts";
5
6
  import { type FileDir, isFileId, safeName } from "../app/files.ts";
@@ -15,6 +16,7 @@ import type {
15
16
  } from "../web-app.ts";
16
17
  import type { HostAccess } from "./access.ts";
17
18
  import type { HostCardLog } from "./cards.ts";
19
+ import type { HostRoomContextOptions } from "./context.ts";
18
20
  import { checkCard } from "./forms.ts";
19
21
  import { cardView, checkPicture, type FoundRoom } from "./room-views.ts";
20
22
 
@@ -37,6 +39,11 @@ export interface HostRoomsOptions {
37
39
  roomUrl(room: ChannelKey, message?: number): string;
38
40
  /** Which kinds a person's last list showed, so the list's changes reach only them. */
39
41
  listed: ListedKinds;
42
+ /** A room's recent messages as a channel-context block, without marking them handed over. */
43
+ context(
44
+ room: string,
45
+ options?: HostRoomContextOptions,
46
+ ): Promise<ChannelContext | undefined>;
40
47
  now?: () => number;
41
48
  logger: Logger;
42
49
  }
@@ -272,6 +279,14 @@ export class HostRoomsService implements HostRooms {
272
279
  return this.#options.log.byReference(room, reference);
273
280
  }
274
281
 
282
+ async context(
283
+ room: ChannelKey,
284
+ options?: HostRoomContextOptions,
285
+ ): Promise<ChannelContext | undefined> {
286
+ this.#found(room);
287
+ return this.#options.context(room, options);
288
+ }
289
+
275
290
  // ── Cards ──
276
291
 
277
292
  async card(room: ChannelKey, card: HostCard): Promise<{ id: string }> {
@@ -313,7 +328,7 @@ export class HostRoomsService implements HostRooms {
313
328
  {
314
329
  type: "host_card",
315
330
  room,
316
- card: cardView(viewer, found, access, stored),
331
+ card: await cardView(viewer, found, access, stored),
317
332
  },
318
333
  { connection, audience: { room } },
319
334
  );
package/src/host/views.ts CHANGED
@@ -8,6 +8,7 @@ import type {
8
8
  PictureRef,
9
9
  ReadMarkView,
10
10
  RoomMessage,
11
+ SendWording,
11
12
  } from "../api-types.ts";
12
13
 
13
14
  /** One room of a kind, as the list shows it. */
@@ -47,6 +48,8 @@ export interface HostRoomView {
47
48
  id: string;
48
49
  /** What this viewer may do in the room. */
49
50
  access: { send: ("note" | "turn")[]; act: boolean };
51
+ /** The kind's own wording of the two ways to write, where it gave any. */
52
+ wording?: SendWording;
50
53
  header: HostHeaderView;
51
54
  /** The newest messages, oldest first. */
52
55
  messages: RoomMessage[];