@seriousgaming/sdk-tools 0.0.0-stage → 1.0.0-alpha.1

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 (391) hide show
  1. package/LICENSE +93 -0
  2. package/README.md +36 -2
  3. package/dist/bundling/logic-exports.d.ts +34 -0
  4. package/dist/bundling/node-builtins.d.ts +21 -0
  5. package/dist/bundling/transpiler.d.ts +7 -0
  6. package/dist/cli.js +27368 -0
  7. package/dist/client/dev-server.d.ts +59 -0
  8. package/dist/client/index.d.ts +28 -0
  9. package/dist/client/stub-services.d.ts +32 -0
  10. package/dist/client/test-session.d.ts +195 -0
  11. package/dist/client.js +16884 -0
  12. package/dist/core/asset-reference-translator.d.ts +46 -0
  13. package/dist/core/clock.d.ts +57 -0
  14. package/dist/core/faults.d.ts +65 -0
  15. package/dist/core/game-settings.d.ts +65 -0
  16. package/dist/core/history.d.ts +89 -0
  17. package/dist/core/listen-with-fallback.d.ts +30 -0
  18. package/dist/core/mutation-queue.d.ts +61 -0
  19. package/dist/core/package-scenario.d.ts +89 -0
  20. package/dist/core/participant-registry.d.ts +109 -0
  21. package/dist/core/pending-changes.d.ts +14 -0
  22. package/dist/core/protocol.d.ts +83 -0
  23. package/dist/core/scenario-drafts.d.ts +32 -0
  24. package/dist/core/session-endpoints.d.ts +108 -0
  25. package/dist/core/session-gates.d.ts +75 -0
  26. package/dist/core/session-host.d.ts +94 -0
  27. package/dist/core/session-refusals.d.ts +56 -0
  28. package/dist/core/session-rewind.d.ts +32 -0
  29. package/dist/core/session-roster.d.ts +128 -0
  30. package/dist/core/session-surfaces.d.ts +32 -0
  31. package/dist/core/session-ticks.d.ts +18 -0
  32. package/dist/core/session-timers.d.ts +84 -0
  33. package/dist/core/session-writes.d.ts +40 -0
  34. package/dist/core/session.d.ts +385 -0
  35. package/dist/core/settings-locks.d.ts +22 -0
  36. package/dist/core/team-names.d.ts +52 -0
  37. package/dist/core/team-state-store.d.ts +209 -0
  38. package/dist/core/tick-timer.d.ts +75 -0
  39. package/dist/dev/blob-links.d.ts +10 -0
  40. package/dist/dev/console/assets/index-DgFglsnA.js +204 -0
  41. package/dist/dev/console/index.html +12 -0
  42. package/dist/dev/console-path.d.ts +19 -0
  43. package/dist/dev/lockfile.d.ts +124 -0
  44. package/dist/dev/package-watcher.d.ts +21 -0
  45. package/dist/dev/routes.d.ts +67 -0
  46. package/dist/dev/shell.d.ts +260 -0
  47. package/dist/dev/surfaces.d.ts +41 -0
  48. package/dist/dev/tools-identity.d.ts +23 -0
  49. package/dist/dev/view/batch.d.ts +172 -0
  50. package/dist/dev/view/commands.d.ts +75 -0
  51. package/dist/dev/view/route.d.ts +58 -0
  52. package/dist/dev/view/view.d.ts +78 -0
  53. package/dist/dev/view/viewer.d.ts +32 -0
  54. package/dist/discovery/config.d.ts +9 -0
  55. package/dist/discovery/manifest-paths.d.ts +21 -0
  56. package/dist/discovery/removed-fields.d.ts +90 -0
  57. package/dist/docs/api/deck.md +386 -0
  58. package/dist/docs/api/index.md +67 -0
  59. package/dist/docs/api/sdk-commands.md +286 -0
  60. package/dist/docs/api/sdk-e2e.md +433 -0
  61. package/dist/docs/api/sdk-projection.md +207 -0
  62. package/dist/docs/api/sdk-testing.md +214 -0
  63. package/dist/docs/api/sdk-tools-client.md +560 -0
  64. package/dist/docs/api/sdk-ui-testing.md +231 -0
  65. package/dist/docs/api/sdk-ui.md +1269 -0
  66. package/dist/docs/api/sdk.md +2140 -0
  67. package/dist/docs/changelog-0.x.md +1156 -0
  68. package/dist/docs/changelog.md +236 -0
  69. package/dist/docs/cli-and-dev-server.md +217 -0
  70. package/dist/docs/cli-error-codes.md +154 -0
  71. package/dist/docs/cli-reference.md +2488 -0
  72. package/dist/docs/cli-simulation-testing.md +222 -0
  73. package/dist/docs/concepts/index.md +46 -0
  74. package/dist/docs/concepts/terminology.md +322 -0
  75. package/dist/docs/environment-variables.md +102 -0
  76. package/dist/docs/getting-started.md +145 -0
  77. package/dist/docs/guides/ai-facilitation.md +220 -0
  78. package/dist/docs/guides/capabilities.md +460 -0
  79. package/dist/docs/guides/endpoints.md +335 -0
  80. package/dist/docs/guides/engine-testing.md +477 -0
  81. package/dist/docs/guides/engine.md +527 -0
  82. package/dist/docs/guides/file-handling.md +405 -0
  83. package/dist/docs/guides/games.md +176 -0
  84. package/dist/docs/guides/global-settings.md +94 -0
  85. package/dist/docs/guides/guide.md +123 -0
  86. package/dist/docs/guides/hooks.md +183 -0
  87. package/dist/docs/guides/index.md +51 -0
  88. package/dist/docs/guides/internationalization.md +288 -0
  89. package/dist/docs/guides/llm-integration.md +635 -0
  90. package/dist/docs/guides/multi-team-games.md +586 -0
  91. package/dist/docs/guides/patterns.md +391 -0
  92. package/dist/docs/guides/presentation.md +196 -0
  93. package/dist/docs/guides/production-constraints.md +130 -0
  94. package/dist/docs/guides/scenario-editor.md +143 -0
  95. package/dist/docs/guides/scenarios.md +515 -0
  96. package/dist/docs/guides/session.md +972 -0
  97. package/dist/docs/guides/settings.md +458 -0
  98. package/dist/docs/guides/setup.md +193 -0
  99. package/dist/docs/guides/speech-generation.md +233 -0
  100. package/dist/docs/guides/starter-game.md +164 -0
  101. package/dist/docs/guides/testing.md +575 -0
  102. package/dist/docs/guides/tick-controller.md +569 -0
  103. package/dist/docs/guides/type-safety.md +516 -0
  104. package/dist/docs/guides/ui.md +1389 -0
  105. package/dist/docs/guides/upgrading.md +636 -0
  106. package/dist/docs/guides/validators.md +217 -0
  107. package/dist/docs/guides/waiting-room.md +244 -0
  108. package/dist/docs/index.md +41 -0
  109. package/dist/docs/recipes/act-on-the-session-from-an-endpoint.md +95 -0
  110. package/dist/docs/recipes/adapt-a-game-for-walkthroughs.md +133 -0
  111. package/dist/docs/recipes/adapt-ui-for-observers.md +74 -0
  112. package/dist/docs/recipes/call-endpoint-from-ui.md +109 -0
  113. package/dist/docs/recipes/check-a-package-from-a-script.md +117 -0
  114. package/dist/docs/recipes/create-capability.md +66 -0
  115. package/dist/docs/recipes/create-endpoint.md +127 -0
  116. package/dist/docs/recipes/create-engine.md +111 -0
  117. package/dist/docs/recipes/create-game.md +70 -0
  118. package/dist/docs/recipes/create-guide.md +75 -0
  119. package/dist/docs/recipes/create-hook.md +80 -0
  120. package/dist/docs/recipes/create-presentation.md +142 -0
  121. package/dist/docs/recipes/create-scenario-editor.md +102 -0
  122. package/dist/docs/recipes/create-scenario.md +98 -0
  123. package/dist/docs/recipes/create-session-ui.md +116 -0
  124. package/dist/docs/recipes/create-settings.md +88 -0
  125. package/dist/docs/recipes/create-setup.md +115 -0
  126. package/dist/docs/recipes/create-tick-controller.md +65 -0
  127. package/dist/docs/recipes/create-user-interface.md +96 -0
  128. package/dist/docs/recipes/create-validator.md +67 -0
  129. package/dist/docs/recipes/create-waiting-room-ui.md +109 -0
  130. package/dist/docs/recipes/declare-api-uses.md +67 -0
  131. package/dist/docs/recipes/design-state-for-concurrent-writes.md +96 -0
  132. package/dist/docs/recipes/distribute-package.md +80 -0
  133. package/dist/docs/recipes/e2e-testing.md +116 -0
  134. package/dist/docs/recipes/end-game-on-condition.md +154 -0
  135. package/dist/docs/recipes/fix-duplicate-packages.md +101 -0
  136. package/dist/docs/recipes/index.md +86 -0
  137. package/dist/docs/recipes/make-an-endpoint-external.md +101 -0
  138. package/dist/docs/recipes/negotiate-across-teams.md +164 -0
  139. package/dist/docs/recipes/observe-other-teams-state.md +119 -0
  140. package/dist/docs/recipes/publish-a-package.md +139 -0
  141. package/dist/docs/recipes/remember-a-viewers-choice.md +55 -0
  142. package/dist/docs/recipes/review-a-past-day.md +73 -0
  143. package/dist/docs/recipes/share-assets-across-scenarios.md +120 -0
  144. package/dist/docs/recipes/share-logic-between-engine-and-ui.md +130 -0
  145. package/dist/docs/recipes/show-all-teams-participants.md +109 -0
  146. package/dist/docs/recipes/start-from-an-empty-package.md +169 -0
  147. package/dist/docs/recipes/take-a-file-in-an-endpoint.md +82 -0
  148. package/dist/docs/recipes/test-a-multi-role-game.md +51 -0
  149. package/dist/docs/recipes/test-user-interface.md +95 -0
  150. package/dist/docs/recipes/test-with-engine.md +115 -0
  151. package/dist/docs/recipes/upload-and-display-files.md +148 -0
  152. package/dist/docs/recipes/use-request-play.md +65 -0
  153. package/dist/docs/recipes/use-request-tick.md +75 -0
  154. package/dist/docs/troubleshooting.md +816 -0
  155. package/dist/execution/blob-inputs.d.ts +57 -0
  156. package/dist/execution/bundle-reload.d.ts +42 -0
  157. package/dist/execution/config-resolver.d.ts +127 -0
  158. package/dist/execution/configuration-builder.d.ts +66 -0
  159. package/dist/execution/context-composition.d.ts +161 -0
  160. package/dist/execution/endpoint-actions.d.ts +9 -0
  161. package/dist/execution/endpoint-payloads.d.ts +32 -0
  162. package/dist/execution/endpoint-runner.d.ts +129 -0
  163. package/dist/execution/engine-state.d.ts +40 -0
  164. package/dist/execution/executor-callbacks.d.ts +56 -0
  165. package/dist/execution/executor.d.ts +436 -0
  166. package/dist/execution/gates.d.ts +45 -0
  167. package/dist/execution/hook-chain.d.ts +25 -0
  168. package/dist/execution/logic-calls.d.ts +190 -0
  169. package/dist/execution/logic-failure.d.ts +84 -0
  170. package/dist/execution/logic-loader.d.ts +54 -0
  171. package/dist/execution/logic-result.d.ts +76 -0
  172. package/dist/execution/tick-controller-call.d.ts +32 -0
  173. package/dist/execution/types.d.ts +48 -0
  174. package/dist/generation/ts-type.d.ts +39 -0
  175. package/dist/guide/locale.d.ts +12 -0
  176. package/dist/guide/video-registry.d.ts +42 -0
  177. package/dist/help-examples.d.ts +125 -0
  178. package/dist/host/adapter.d.ts +67 -0
  179. package/dist/host/host-channel.d.ts +222 -0
  180. package/dist/host/index.d.ts +20 -0
  181. package/dist/host/protocol.d.ts +783 -0
  182. package/dist/host/surfaces.d.ts +42 -0
  183. package/dist/host/theme.d.ts +55 -0
  184. package/dist/host.js +583 -0
  185. package/dist/prompts/index.d.ts +7 -0
  186. package/dist/prompts/schema-builder.d.ts +29 -0
  187. package/dist/prompts/types.d.ts +108 -0
  188. package/dist/prompts/validators.d.ts +32 -0
  189. package/dist/runtime/capability-scope.d.ts +6 -0
  190. package/dist/runtime/context-merge.d.ts +29 -0
  191. package/dist/runtime/context-projection.d.ts +41 -0
  192. package/dist/runtime/guarded-callbacks.d.ts +15 -0
  193. package/dist/runtime/index.d.ts +27 -0
  194. package/dist/runtime/manifest-version.d.ts +55 -0
  195. package/dist/runtime/tick-validation.d.ts +25 -0
  196. package/dist/runtime.js +346 -0
  197. package/dist/schema.json +1546 -0
  198. package/dist/server/vite-plugin.d.ts +46 -0
  199. package/dist/services/tts/client.d.ts +51 -0
  200. package/dist/services/tts/elevenlabs.d.ts +24 -0
  201. package/dist/services/tts/types.d.ts +44 -0
  202. package/dist/storage/asset-resolver.d.ts +49 -0
  203. package/dist/storage/blob-store.d.ts +41 -0
  204. package/dist/templates/endpoint/logic.test.ts.liquid +61 -0
  205. package/dist/templates/endpoint/logic.ts.liquid +25 -0
  206. package/dist/templates/engine/init.json.liquid +7 -0
  207. package/dist/templates/engine/logic-aggregate.test.ts.liquid +90 -0
  208. package/dist/templates/engine/logic-aggregate.ts.liquid +46 -0
  209. package/dist/templates/engine/logic.test.ts.liquid +61 -0
  210. package/dist/templates/engine/logic.ts.liquid +32 -0
  211. package/dist/templates/engine/run.json.liquid +7 -0
  212. package/dist/templates/guide/01-at-a-glance.md.liquid +19 -0
  213. package/dist/templates/guide/02-preparation.md.liquid +18 -0
  214. package/dist/templates/guide/03-running-the-session.md.liquid +37 -0
  215. package/dist/templates/hook/logic.test.ts.liquid +46 -0
  216. package/dist/templates/hook/logic.ts.liquid +12 -0
  217. package/dist/templates/package/CLAUDE.md.template +338 -0
  218. package/dist/templates/package/DESIGN.md.liquid +144 -0
  219. package/dist/templates/package/README.md.liquid +143 -0
  220. package/dist/templates/package/claude/rules/workflow-create-capability.md +26 -0
  221. package/dist/templates/package/claude/rules/workflow-create-endpoint.md +35 -0
  222. package/dist/templates/package/claude/rules/workflow-create-engine.md +35 -0
  223. package/dist/templates/package/claude/rules/workflow-create-guide.md +40 -0
  224. package/dist/templates/package/claude/rules/workflow-create-presentation.md +37 -0
  225. package/dist/templates/package/claude/rules/workflow-create-user-interface.md +31 -0
  226. package/dist/templates/package/claude/rules/workflow-publish-package.md +54 -0
  227. package/dist/templates/package/claude/rules/workflow-test-engine-or-ui.md +30 -0
  228. package/dist/templates/package/claude/rules/workflow-update-sdk-version.md +43 -0
  229. package/dist/templates/package/claude/settings.json.template +16 -0
  230. package/dist/templates/package/gitignore.template +15 -0
  231. package/dist/templates/package/global-setup.ts.template +21 -0
  232. package/dist/templates/package/globals.d.ts.template +2 -0
  233. package/dist/templates/package/oxfmtrc.json.template +14 -0
  234. package/dist/templates/package/oxlintrc.json.template +238 -0
  235. package/dist/templates/package/package.json.liquid +65 -0
  236. package/dist/templates/package/playwright.config.ts.liquid +19 -0
  237. package/dist/templates/package/pnpm-workspace.yaml.template +27 -0
  238. package/dist/templates/package/test-setup.ts.liquid +7 -0
  239. package/dist/templates/package/tsconfig.json.liquid +15 -0
  240. package/dist/templates/package/vite.config.ts.liquid +19 -0
  241. package/dist/templates/package/vitest.config.ts.liquid +25 -0
  242. package/dist/templates/package/vitest.engine.config.ts.liquid +12 -0
  243. package/dist/templates/presentation/index.html.liquid +20 -0
  244. package/dist/templates/presentation/src/App.tsx.liquid +49 -0
  245. package/dist/templates/presentation/src/Slides.tsx.liquid +81 -0
  246. package/dist/templates/presentation/src/i18n.ts.liquid +54 -0
  247. package/dist/templates/presentation/src/main.tsx.liquid +14 -0
  248. package/dist/templates/scenario_editor/src/App.test.tsx.liquid +30 -0
  249. package/dist/templates/scenario_editor/src/App.tsx.liquid +95 -0
  250. package/dist/templates/scenario_editor/src/i18n.ts.liquid +44 -0
  251. package/dist/templates/session/src/App.test.tsx.liquid +60 -0
  252. package/dist/templates/session/src/App.tsx.liquid +128 -0
  253. package/dist/templates/session/src/i18n.ts.liquid +53 -0
  254. package/dist/templates/session/src/main.tsx.liquid +48 -0
  255. package/dist/templates/session/src/theme.ts.liquid +46 -0
  256. package/dist/templates/settings-editor/index.html.liquid +12 -0
  257. package/dist/templates/settings-editor/src/main.tsx.liquid +48 -0
  258. package/dist/templates/settings-editor/src/theme.ts.liquid +24 -0
  259. package/dist/templates/setup/src/App.test.tsx.liquid +39 -0
  260. package/dist/templates/setup/src/App.tsx.liquid +102 -0
  261. package/dist/templates/setup/src/i18n.ts.liquid +47 -0
  262. package/dist/templates/starter-game/capabilities/briefing/capability.sdk.json +18 -0
  263. package/dist/templates/starter-game/capabilities/plan/capability.sdk.json +17 -0
  264. package/dist/templates/starter-game/capabilities/results/capability.sdk.json +61 -0
  265. package/dist/templates/starter-game/capabilities/roles/capability.sdk.json +17 -0
  266. package/dist/templates/starter-game/capabilities/weather/capability.sdk.json +23 -0
  267. package/dist/templates/starter-game/engine/defaults.ts +81 -0
  268. package/dist/templates/starter-game/engine/engine.sdk.json +12 -0
  269. package/dist/templates/starter-game/engine/logic.test.ts +221 -0
  270. package/dist/templates/starter-game/engine/logic.ts +149 -0
  271. package/dist/templates/starter-game/engine/test-helpers.ts +74 -0
  272. package/dist/templates/starter-game/engine/types.ts +139 -0
  273. package/dist/templates/starter-game/guide/content/en/01-at-a-glance.md +51 -0
  274. package/dist/templates/starter-game/guide/content/en/02-preparation.md +35 -0
  275. package/dist/templates/starter-game/guide/content/en/03-running-the-day.md +58 -0
  276. package/dist/templates/starter-game/guide/guide.sdk.json +10 -0
  277. package/dist/templates/starter-game/guide/guide.test.ts +108 -0
  278. package/dist/templates/starter-game/hooks/assignproducts/defaults.ts +37 -0
  279. package/dist/templates/starter-game/hooks/assignproducts/hook.sdk.json +13 -0
  280. package/dist/templates/starter-game/hooks/assignproducts/logic/logic.test.ts +77 -0
  281. package/dist/templates/starter-game/hooks/assignproducts/logic/logic.ts +47 -0
  282. package/dist/templates/starter-game/hooks/assignproducts/types.ts +55 -0
  283. package/dist/templates/starter-game/package.json +68 -0
  284. package/dist/templates/starter-game/playwright.config.ts +19 -0
  285. package/dist/templates/starter-game/scenarios/heatwave/scenario.sdk.json +20 -0
  286. package/dist/templates/starter-game/scenarios/ordinary_week/scenario.sdk.json +12 -0
  287. package/dist/templates/starter-game/seriousgaming.sdk.json +23 -0
  288. package/dist/templates/starter-game/session/defaults.ts +80 -0
  289. package/dist/templates/starter-game/session/index.html +12 -0
  290. package/dist/templates/starter-game/session/session.sdk.json +11 -0
  291. package/dist/templates/starter-game/session/src/App.test.tsx +307 -0
  292. package/dist/templates/starter-game/session/src/App.tsx +309 -0
  293. package/dist/templates/starter-game/session/src/Section.tsx +37 -0
  294. package/dist/templates/starter-game/session/src/TeamDoor.tsx +96 -0
  295. package/dist/templates/starter-game/session/src/format.ts +8 -0
  296. package/dist/templates/starter-game/session/src/i18n.ts +51 -0
  297. package/dist/templates/starter-game/session/src/main.tsx +48 -0
  298. package/dist/templates/starter-game/session/src/theme.ts +45 -0
  299. package/dist/templates/starter-game/session/types.ts +135 -0
  300. package/dist/templates/starter-game/settings/engine/settings.sdk.json +50 -0
  301. package/dist/templates/starter-game/settings/menu/settings.sdk.json +78 -0
  302. package/dist/templates/starter-game/settings/session/settings.sdk.json +23 -0
  303. package/dist/templates/starter-game/settings/ui/settings.sdk.json +25 -0
  304. package/dist/templates/starter-game/test-setup.ts +7 -0
  305. package/dist/templates/starter-game/testing/e2e/game.spec.ts +139 -0
  306. package/dist/templates/starter-game/testing/e2e/global-setup.ts +21 -0
  307. package/dist/templates/starter-game/testing/engine/game.engine.test.ts +171 -0
  308. package/dist/templates/starter-game/tsconfig.json +19 -0
  309. package/dist/templates/starter-game/ui/defaults.ts +60 -0
  310. package/dist/templates/starter-game/ui/index.html +12 -0
  311. package/dist/templates/starter-game/ui/src/App.test.tsx +138 -0
  312. package/dist/templates/starter-game/ui/src/App.tsx +175 -0
  313. package/dist/templates/starter-game/ui/src/DayBar.test.tsx +78 -0
  314. package/dist/templates/starter-game/ui/src/DayBar.tsx +72 -0
  315. package/dist/templates/starter-game/ui/src/HistoryPanel.test.tsx +74 -0
  316. package/dist/templates/starter-game/ui/src/HistoryPanel.tsx +120 -0
  317. package/dist/templates/starter-game/ui/src/MakeRow.tsx +129 -0
  318. package/dist/templates/starter-game/ui/src/Section.tsx +37 -0
  319. package/dist/templates/starter-game/ui/src/ThemeToggle.tsx +49 -0
  320. package/dist/templates/starter-game/ui/src/TodayPanel.test.tsx +179 -0
  321. package/dist/templates/starter-game/ui/src/TodayPanel.tsx +92 -0
  322. package/dist/templates/starter-game/ui/src/YesterdayPanel.tsx +142 -0
  323. package/dist/templates/starter-game/ui/src/format.ts +12 -0
  324. package/dist/templates/starter-game/ui/src/i18n.ts +95 -0
  325. package/dist/templates/starter-game/ui/src/main.tsx +48 -0
  326. package/dist/templates/starter-game/ui/src/menu.ts +18 -0
  327. package/dist/templates/starter-game/ui/src/test-helpers.ts +58 -0
  328. package/dist/templates/starter-game/ui/src/theme.ts +45 -0
  329. package/dist/templates/starter-game/ui/src/writes.ts +43 -0
  330. package/dist/templates/starter-game/ui/types.ts +137 -0
  331. package/dist/templates/starter-game/ui/ui.sdk.json +11 -0
  332. package/dist/templates/starter-game/validators/capacity/defaults.ts +37 -0
  333. package/dist/templates/starter-game/validators/capacity/logic/logic.test.ts +96 -0
  334. package/dist/templates/starter-game/validators/capacity/logic/logic.ts +45 -0
  335. package/dist/templates/starter-game/validators/capacity/types.ts +55 -0
  336. package/dist/templates/starter-game/validators/capacity/validator.sdk.json +11 -0
  337. package/dist/templates/starter-game/vite.config.ts +19 -0
  338. package/dist/templates/starter-game/vitest.config.ts +15 -0
  339. package/dist/templates/starter-game/vitest.engine.config.ts +19 -0
  340. package/dist/templates/starter-game/waiting_room/defaults.ts +40 -0
  341. package/dist/templates/starter-game/waiting_room/index.html +12 -0
  342. package/dist/templates/starter-game/waiting_room/src/App.test.tsx +102 -0
  343. package/dist/templates/starter-game/waiting_room/src/App.tsx +194 -0
  344. package/dist/templates/starter-game/waiting_room/src/i18n.ts +33 -0
  345. package/dist/templates/starter-game/waiting_room/src/main.tsx +48 -0
  346. package/dist/templates/starter-game/waiting_room/src/theme.ts +45 -0
  347. package/dist/templates/starter-game/waiting_room/types.ts +59 -0
  348. package/dist/templates/starter-game/waiting_room/waiting_room.sdk.json +10 -0
  349. package/dist/templates/tick-controller/logic.ts.liquid +8 -0
  350. package/dist/templates/ui/index.html.liquid +12 -0
  351. package/dist/templates/ui/src/App.test.tsx.liquid +41 -0
  352. package/dist/templates/ui/src/App.tsx.liquid +103 -0
  353. package/dist/templates/ui/src/i18n.ts.liquid +47 -0
  354. package/dist/templates/ui/src/main.tsx.liquid +48 -0
  355. package/dist/templates/ui/src/theme.ts.liquid +46 -0
  356. package/dist/templates/validator/logic.test.ts.liquid +42 -0
  357. package/dist/templates/validator/logic.ts.liquid +21 -0
  358. package/dist/templates/waiting-room/index.html.liquid +12 -0
  359. package/dist/templates/waiting-room/src/App.test.tsx.liquid +57 -0
  360. package/dist/templates/waiting-room/src/App.tsx.liquid +118 -0
  361. package/dist/templates/waiting-room/src/i18n.ts.liquid +44 -0
  362. package/dist/templates/waiting-room/src/main.tsx.liquid +48 -0
  363. package/dist/templates/waiting-room/src/theme.ts.liquid +46 -0
  364. package/dist/templating/resource-metadata.d.ts +131 -0
  365. package/dist/types.d.ts +14 -0
  366. package/dist/utils/atomic-write.d.ts +40 -0
  367. package/dist/utils/cli-error.d.ts +64 -0
  368. package/dist/utils/document-parser-client.d.ts +20 -0
  369. package/dist/utils/env-loader.d.ts +40 -0
  370. package/dist/utils/error-codes.d.ts +335 -0
  371. package/dist/utils/errors.d.ts +19 -0
  372. package/dist/utils/global-settings.d.ts +65 -0
  373. package/dist/utils/llm-client.d.ts +34 -0
  374. package/dist/utils/logger.d.ts +36 -0
  375. package/dist/utils/output.d.ts +109 -0
  376. package/dist/utils/platform.d.ts +14 -0
  377. package/dist/utils/prompt.d.ts +36 -0
  378. package/dist/utils/real-path.d.ts +6 -0
  379. package/dist/utils/schema-properties.d.ts +35 -0
  380. package/dist/validation/ajv.d.ts +17 -0
  381. package/dist/validation/facilitator-guide-validator.d.ts +27 -0
  382. package/dist/validation/findings.d.ts +40 -0
  383. package/dist/validation/game-constraints.d.ts +10 -0
  384. package/dist/validation/game-validator.d.ts +34 -0
  385. package/dist/validation/package-validation.d.ts +41 -0
  386. package/dist/validation/package-validator.d.ts +19 -0
  387. package/dist/validation/resource-validator.d.ts +79 -0
  388. package/dist/validation/schema-defaults-validator.d.ts +29 -0
  389. package/dist/validation/schema-validation.d.ts +115 -0
  390. package/dist/validation/semver.d.ts +9 -0
  391. package/package.json +122 -3
@@ -0,0 +1,972 @@
1
+ # Building session UIs
2
+
3
+ Build facilitator-only UIs that operate across all teams in a session at once: session-wide dashboards, cross-team comparisons, and global game controls.
4
+
5
+ **Keywords:** manage session, session UI, facilitator UI, session-level, cross-team, multi-team dashboard, facilitator controls, session dashboard, all teams, independent mode, shared-state mode
6
+
7
+ ## What you'll learn
8
+
9
+ - How to create a session UI
10
+ - How to read and write per-team state from a single session-level view
11
+ - How to target game flow controls at individual teams or all teams at once
12
+ - How to handle both independent and shared-state interaction modes
13
+ - How to configure the session UI per game and per scenario
14
+
15
+ ## What are session UIs?
16
+
17
+ The session UI is the facilitator's whole-session screen. The host renders it full-bleed beneath a thin band of its own chrome, and it owns everything below that: teams, participants, and game controls. A game wires in at most one, and a package that ships none falls back to the host's own roster page.
18
+
19
+ | Aspect | Session UI | Game UI |
20
+ | --------------------- | ----------------------------------------------------- | -------------------- |
21
+ | **Who sees it** | Facilitator only | Participants |
22
+ | **Scope** | Every team in the session | Single team |
23
+ | **Data model** | `SessionData` (shared store and teams map) | Flat context |
24
+ | **Where shown** | Full-bleed under the host's chrome | During game play |
25
+ | **Cardinality** | At most one per game | At most one per game |
26
+ | **Tick control** | Per-team or all-teams tick, undo, pause, reset | `requestTick` only |
27
+ | **Roster control** | Create and rename teams, rename and move participants | None |
28
+ | **Interaction modes** | Aware of independent vs shared-state | Not mode-aware |
29
+
30
+ Capability is inferred, not declared: shipping a session UI claims the whole canvas. There is no partial takeover and no per-capability negotiation.
31
+
32
+ ## Creating a session UI
33
+
34
+ The starter game already fills this slot, and `new` refuses a filled one with `RESOURCE_EXISTS`. In a package made by `new package`, change it with `update session` instead, or start from `new package --empty`.
35
+
36
+ ```bash
37
+ pnpm run sdk new session '{
38
+ "definition": {
39
+ "reads": ["price", "workers", "production"],
40
+ "writes": ["price", "workers"]
41
+ },
42
+ "rootDirectory": "."
43
+ }'
44
+ ```
45
+
46
+ This creates:
47
+
48
+ ```
49
+ session/
50
+ ├── session.sdk.json
51
+ ├── index.html
52
+ ├── types.ts
53
+ └── src/
54
+ ├── main.tsx
55
+ ├── App.tsx
56
+ ├── i18n.ts
57
+ └── theme.ts
58
+ ```
59
+
60
+ ## Configuring the surface
61
+
62
+ Give the session UI values a game can vary without a code change by putting them in a settings resource, `settings/session/settings.sdk.json`:
63
+
64
+ ```json
65
+ {
66
+ "definition": {
67
+ "settingsSchema": {
68
+ "type": "object",
69
+ "properties": {
70
+ "NoticeboardLabel": { "type": "string", "default": "Noticeboard" },
71
+ "ShowJoinLinks": { "type": "boolean", "default": true }
72
+ },
73
+ "additionalProperties": false
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ and listing it in `session/session.sdk.json`:
80
+
81
+ ```json
82
+ {
83
+ "definition": {
84
+ "reads": ["noticeboard"],
85
+ "writes": ["noticeboard"],
86
+ "settings": ["session"]
87
+ }
88
+ }
89
+ ```
90
+
91
+ The resolved values arrive as `settings` on
92
+ [`useSessionUserInterfaceData`](#usesessionuserinterfacedata), keyed by
93
+ settings identifier (`settings.session.ShowJoinLinks`) and typed as the
94
+ generated `Settings` from the session UI's `types.ts`. The surface receives
95
+ exactly the settings it lists, so it can also read the engine's by listing
96
+ `engine`, and never sees one it does not list.
97
+
98
+ The values resolve like any settings (see [Overriding settings](./settings.md#overriding-settings)).
99
+ To change a value for every session, edit its default. To vary it by
100
+ scenario, set it under the settings identifier in the scenario's
101
+ `settings`:
102
+
103
+ ```json
104
+ {
105
+ "definition": {
106
+ "locales": {
107
+ "en": {
108
+ "name": "Executive briefing"
109
+ }
110
+ },
111
+ "settings": {
112
+ "session": {
113
+ "ShowJoinLinks": false
114
+ }
115
+ }
116
+ },
117
+ "rootDirectory": "."
118
+ }
119
+ ```
120
+
121
+ Every layer is partial: set only the keys you want to change, and the rest
122
+ fall through. Settings are fixed when play starts. See
123
+ [Settings](./settings.md) for the whole model.
124
+
125
+ ## Building the session UI
126
+
127
+ Use `useSessionUserInterfaceData` to read session data and write per-team overrides. The data model carries the shared store once, and wraps each team's own context, tick count, and timer state, into a `SessionData` structure:
128
+
129
+ ```typescript
130
+ type SharedState = {
131
+ // The shared-scope capabilities this UI declares in reads and writes
132
+ context: Record<string, unknown>;
133
+ // Wrapper plumbing; nothing on screen needs it
134
+ revision: number;
135
+ };
136
+
137
+ type TeamState = {
138
+ // This team's own capabilities (team scope only), projected onto
139
+ // the capabilities this UI declares in reads and writes
140
+ context: Record<string, unknown>;
141
+ completedTicks: number; // ticks this team has completed
142
+ timer: TimerState; // the timer, as a game UI reads it
143
+ // Wrapper plumbing; nothing on screen needs it
144
+ revision: number;
145
+ };
146
+
147
+ type SessionData = {
148
+ shared: SharedState; // present even in a session with no teams
149
+ teams: Record<string, TeamState>;
150
+ };
151
+ ```
152
+
153
+ `timer` is the one [`TimerState`](../api/sdk-ui.md#timerstate) shape a game UI reads from `useUserInterfaceTimer()`, so a countdown is drawn as in [Displaying the tick timer](./ui.md#displaying-the-tick-timer).
154
+
155
+ Three notes on that shape. Each `context` carries only the capabilities this session UI declared (see [Capabilities](./capabilities.md#a-declaration-is-what-gets-delivered)), so declare everything the screen renders. A `scope: 'shared'` capability arrives once, in `shared.context`, and never in a team's `context`: read it there rather than off whichever team comes first. And `revision` is how the wrapper tracks updates: on a large session the host sends one team's state (or the shared store) rather than every team's, and the wrapper merges each one into the `SessionData` you read, asking for a fresh snapshot if it notices one went missing. You never need to read it, and you can leave it out of hand-written test fixtures.
156
+
157
+ The SDK types are generic (`SessionData<TTeamContext, TSharedContext>`, `TeamState<TTeamContext>` and `SharedState<TSharedContext>`), and the generated `types.ts` beside your session UI exports `SessionData` already filled in with its `TeamContext` and `SharedContext`. Pass it as the first generic of `useSessionUserInterfaceData` and both halves are typed:
158
+
159
+ ```typescript
160
+ import type { SessionData, Settings } from '../types';
161
+
162
+ const { sessionData } = useSessionUserInterfaceData<SessionData, Settings>();
163
+ const briefing = sessionData.shared.context.briefing.Message; // once, not off teams[0]
164
+ const price = sessionData.teams['team-1']?.context.price.Price; // a team's own
165
+ ```
166
+
167
+ The keys of `sessionData.teams` are stable team ids (`team-1`, `team-2`, and so on). Each id carries the team's zero-based `index` (`team-N` carries index N-1), and the id never renumbers. Because removing a team leaves a gap, the key set can be sparse (for example `team-1` and `team-3` with no `team-2`); iterate the keys rather than assuming a contiguous `1..N` range. Each `TeamInfo` from `useSessionUserInterfaceTeams` carries the matching `index`, so a UI can find its own slot with `teams.find((t) => t.teamId === participant.teamId)?.index`.
168
+
169
+ ### Reading session data
170
+
171
+ ```typescript
172
+ import { useSessionUserInterfaceData } from '@seriousgaming/sdk/ui';
173
+ import { Typography, Paper, Grid } from '@mui/material';
174
+
175
+ function App() {
176
+ const { sessionData, settings, setData } = useSessionUserInterfaceData();
177
+
178
+ return (
179
+ <Grid container spacing={2}>
180
+ {Object.entries(sessionData.teams).map(([teamId, team]) => (
181
+ <Grid item xs={6} key={teamId}>
182
+ <Paper sx={{ p: 2 }}>
183
+ <Typography variant="h6">{teamId} (Tick {team.completedTicks})</Typography>
184
+ <Typography>
185
+ Price: {(team.context.price as any)?.Price}
186
+ </Typography>
187
+ <Typography>
188
+ Production: {(team.context.production as any)?.Production}
189
+ </Typography>
190
+ </Paper>
191
+ </Grid>
192
+ ))}
193
+ </Grid>
194
+ );
195
+ }
196
+
197
+ export default App;
198
+ ```
199
+
200
+ ### Writing per-team overrides
201
+
202
+ The session-level `setData` requires a `teamId` parameter to specify which team to update, since the session UI is not scoped to one team:
203
+
204
+ ```typescript
205
+ import { useSessionUserInterfaceData } from '@seriousgaming/sdk/ui';
206
+ import { TextField } from '@mui/material';
207
+
208
+ function TeamPriceEditor({ teamId }: { teamId: string }) {
209
+ const { sessionData, setData } = useSessionUserInterfaceData();
210
+ const team = sessionData.teams[teamId];
211
+
212
+ return (
213
+ <TextField
214
+ label={`${teamId} Price`}
215
+ type="number"
216
+ value={(team.context.price as any)?.Price}
217
+ onChange={(e) =>
218
+ setData(teamId, {
219
+ price: { Price: Number(e.target.value) },
220
+ })
221
+ }
222
+ />
223
+ );
224
+ }
225
+ ```
226
+
227
+ Changes made via `setData` are sent immediately to the dev server and merged into the target team's context, so there is no 'Apply' button to press.
228
+
229
+ A team write may change only the capabilities this UI declares in `writes`. One that changes any other capability, or names a key that is not a capability, is refused as `denied` and none of it is applied. Sending a team's whole context back with one field edited is fine: a capability the write carries unchanged is not a change.
230
+
231
+ The one-argument form, `setData(update)`, writes `scope: 'shared'` capabilities session-wide instead, and merges optimistically into `sessionData.shared.context`. See [Facilitator-writable shared capabilities](./multi-team-games.md#facilitator-writable-shared-capabilities).
232
+
233
+ Type both forms with the generated types in the surface's `types.ts`. `SharedWrites` is the shared capabilities in this UI's `writes`, and `TeamWrites` the rest of them, so a team write naming a shared capability fails to compile rather than being refused when the session holds more than one team:
234
+
235
+ ```typescript
236
+ import type { SessionData, Settings, SharedWrites, TeamWrites } from '../types';
237
+
238
+ const { setData } = useSessionUserInterfaceData<SessionData, Settings, SharedWrites, TeamWrites>();
239
+
240
+ setData(teamId, { price: { Price: 12 } }); // TeamWrites
241
+ setData({ dials: { BasePrice: 12 } }); // SharedWrites
242
+ setData(teamId, { dials: { BasePrice: 12 } }); // compile error: dials is shared
243
+ ```
244
+
245
+ ### Removing a key
246
+
247
+ A merge cannot remove a key, so both forms take `deletes` as their last argument, as a player surface's `setData` does. The paths are removed after the update merges:
248
+
249
+ ```typescript
250
+ // Take one team's note down
251
+ setData(teamId, {}, { deletes: [['pinboard', 'Notes', noteId]] });
252
+
253
+ // Remove an offer from the shared store, for every team
254
+ setData({}, { deletes: [`market.Offers.${offerId}`] });
255
+ ```
256
+
257
+ The host holds each path to this UI's `writes`: a team delete must name a team-scope capability it writes, and a shared delete a shared one it writes. Anything else is refused as `denied`, and none of the write is applied. A shared capability has one value for the session, so remove from it with the shared form, never through a team. See [Design state for concurrent writes](../recipes/design-state-for-concurrent-writes.md) for when to delete rather than write `null`.
258
+
259
+ ### Per-team views
260
+
261
+ A common pattern is to iterate over teams and render a card or panel for each one:
262
+
263
+ ```typescript
264
+ import { useSessionUserInterfaceData } from '@seriousgaming/sdk/ui';
265
+ import { useSessionUserInterfaceTeams } from '@seriousgaming/sdk/ui';
266
+ import { Card, CardContent, Typography, Grid, TextField } from '@mui/material';
267
+
268
+ function SessionDashboard() {
269
+ const { sessionData, setData } = useSessionUserInterfaceData();
270
+ const teams = useSessionUserInterfaceTeams();
271
+
272
+ return (
273
+ <Grid container spacing={2}>
274
+ {teams.map((team) => {
275
+ const state = sessionData.teams[team.teamId];
276
+ if (!state) return null;
277
+
278
+ return (
279
+ <Grid item xs={12} md={6} key={team.teamId}>
280
+ <Card>
281
+ <CardContent>
282
+ <Typography variant="h6">{team.name}</Typography>
283
+ <Typography variant="body2">Tick: {state.completedTicks}</Typography>
284
+ <TextField
285
+ label="Workers"
286
+ type="number"
287
+ value={(state.context.workers as any)?.WorkerCount}
288
+ onChange={(e) =>
289
+ setData(team.teamId, {
290
+ workers: {
291
+ WorkerCount: Number(e.target.value),
292
+ },
293
+ })
294
+ }
295
+ />
296
+ </CardContent>
297
+ </Card>
298
+ </Grid>
299
+ );
300
+ })}
301
+ </Grid>
302
+ );
303
+ }
304
+ ```
305
+
306
+ ### Interaction mode awareness
307
+
308
+ The interaction mode lives on `useSessionUserInterfaceTeamConfiguration()`, alongside the team bounds: one hook for how teams work, one for which teams exist. Use it to adapt the UI, for example by hiding per-team tick controls in shared-state mode (where all teams tick together):
309
+
310
+ ```typescript
311
+ import { useSessionUserInterfaceTeamConfiguration } from '@seriousgaming/sdk/ui';
312
+ import { Alert } from '@mui/material';
313
+
314
+ function ModeIndicator() {
315
+ const { mode } = useSessionUserInterfaceTeamConfiguration();
316
+
317
+ return (
318
+ <Alert severity="info">
319
+ {mode === 'shared-state'
320
+ ? 'Shared-state mode: all teams tick together.'
321
+ : 'Independent mode: each team ticks on its own schedule.'}
322
+ </Alert>
323
+ );
324
+ }
325
+ ```
326
+
327
+ In **shared-state** mode, calling `requestTick()` without a `teamId` advances all teams simultaneously. In **independent** mode, you can target individual teams or advance all teams at once: with no `teamId`, every team that can still move advances by one from its own round, so a team that is behind stays behind, and a team that is complete or at the round limit is skipped.
328
+
329
+ ## Controlling game flow
330
+
331
+ Session UIs have full control over the game lifecycle via callbacks. Every game-flow callback accepts an optional `teamId`; when it is omitted the action applies to all teams.
332
+
333
+ Every game-flow callback except `pauseTimer` and `resumeTimer` returns a promise that resolves once the host has committed the change, and rejects with a `MutationError` when it will not. Await it when you want to report the outcome honestly; ignore it when you do not. Either way the resulting state arrives through the normal session data channel.
334
+
335
+ ### Triggering ticks
336
+
337
+ ```typescript
338
+ import { useSessionUserInterfaceCallbacks } from '@seriousgaming/sdk/ui';
339
+ import { useSessionUserInterfaceTeams } from '@seriousgaming/sdk/ui';
340
+ import { useSessionUserInterfaceTeamConfiguration } from '@seriousgaming/sdk/ui';
341
+ import { Button, Stack } from '@mui/material';
342
+
343
+ function TickControls() {
344
+ const { requestTick } = useSessionUserInterfaceCallbacks();
345
+ const teams = useSessionUserInterfaceTeams();
346
+ const { mode } = useSessionUserInterfaceTeamConfiguration();
347
+
348
+ return (
349
+ <Stack spacing={1}>
350
+ <Button onClick={() => requestTick()} variant="contained">
351
+ Advance All Teams
352
+ </Button>
353
+ {mode === 'independent' &&
354
+ teams.map((team) => (
355
+ <Button
356
+ key={team.teamId}
357
+ onClick={() => requestTick(team.teamId)}
358
+ variant="outlined"
359
+ >
360
+ Advance {team.name}
361
+ </Button>
362
+ ))}
363
+ </Stack>
364
+ );
365
+ }
366
+ ```
367
+
368
+ The session UI's `requestTick` control is how facilitators advance the game: there is no built-in next tick button. The scaffolded session UI wires a session-wide tick button out of the box; during development the console's dev drawer also has a developer-only **Tick** button.
369
+
370
+ ### Pausing and resuming the timer
371
+
372
+ When a game uses a [tick controller](./tick-controller.md) with automatic ticking, facilitators can pause and resume per-team or globally:
373
+
374
+ ```typescript
375
+ import { useSessionUserInterfaceCallbacks } from '@seriousgaming/sdk/ui';
376
+ import { Button, Stack } from '@mui/material';
377
+
378
+ function TimerControls({ teamId }: { teamId?: string }) {
379
+ const { pauseTimer, resumeTimer } = useSessionUserInterfaceCallbacks();
380
+
381
+ return (
382
+ <Stack direction="row" spacing={1}>
383
+ <Button onClick={() => pauseTimer(teamId)} variant="outlined" color="warning">
384
+ {teamId ? `Pause ${teamId}` : 'Pause All'}
385
+ </Button>
386
+ <Button onClick={() => resumeTimer(teamId)} variant="contained" color="success">
387
+ {teamId ? `Resume ${teamId}` : 'Resume All'}
388
+ </Button>
389
+ </Stack>
390
+ );
391
+ }
392
+ ```
393
+
394
+ While paused, manual ticks via `requestTick` still work. Timer state for each team is available in `sessionData.teams[teamId].timer`.
395
+
396
+ ### Extending a tick
397
+
398
+ Pausing is the wrong tool for 'give them two more minutes'. It stops the clock until someone remembers to start it again, and it does not move the deadline your countdown is drawn from. `extendTick` moves the deadline instead. It takes milliseconds:
399
+
400
+ ```typescript
401
+ import { useSessionUserInterfaceCallbacks } from '@seriousgaming/sdk/ui';
402
+ import { Button, Stack } from '@mui/material';
403
+
404
+ function ExtendControls({ teamId }: { teamId?: string }) {
405
+ const { extendTick } = useSessionUserInterfaceCallbacks();
406
+
407
+ return (
408
+ <Stack direction="row" spacing={1}>
409
+ <Button onClick={() => void extendTick(120_000, teamId)}>+2 min</Button>
410
+ <Button onClick={() => void extendTick(-30_000, teamId)}>-30 sec</Button>
411
+ </Stack>
412
+ );
413
+ }
414
+ ```
415
+
416
+ Three things worth knowing:
417
+
418
+ - **It moves one window, not the cadence.** The interval your tick controller returns for later ticks is untouched, so a session does not run permanently late because one tick ran long
419
+ - **A negative value pulls the deadline in**, clamped at now. It never fires a tick retroactively
420
+ - **Read the result back from `extendedMs`** on the team's timer rather than inferring it from a moved `nextTickAt`. A deadline that jumped is equally the signature of a freshly scheduled tick, so `extendedMs` is the only honest source for 'extended by 2m'
421
+
422
+ ```typescript
423
+ const { extendedMs } = sessionData.teams[teamId].timer;
424
+ ```
425
+
426
+ It rejects with a `MutationError` carrying `{ kind: 'denied' }` when there is no tick to move: automatic ticking is off, the game is complete, or the timer is paused. Resume first, then extend.
427
+
428
+ Deliberately, the [tick interval bounds](./tick-controller.md) do not apply. They bound the cadence a tick controller may schedule; a facilitator in the room already reaches outside both with an immediate `requestTick` and an indefinite `pauseTimer`.
429
+
430
+ ### Opening host overlays
431
+
432
+ Some things a facilitator wants are drawn by the host rather than by your game: the [facilitator guide](./guide.md), a team's print sheet. Your session UI owns the whole canvas, so without a way to ask, a facilitator has to leave it to reach them.
433
+
434
+ `openHostOverlay` asks. The host renders the overlay in its own chrome, above your session UI, where you can neither restyle nor suppress it:
435
+
436
+ ```typescript
437
+ import {
438
+ useSessionUserInterfaceCallbacks,
439
+ useSessionUserInterfaceHostOverlays,
440
+ } from '@seriousgaming/sdk/ui';
441
+ import { Button, Stack } from '@mui/material';
442
+
443
+ function OverlayControls() {
444
+ const overlays = useSessionUserInterfaceHostOverlays();
445
+ const { openHostOverlay } = useSessionUserInterfaceCallbacks();
446
+
447
+ return (
448
+ <Stack direction="row" spacing={1}>
449
+ {overlays.includes('facilitator-guide') && (
450
+ <Button onClick={() => void openHostOverlay('facilitator-guide')}>Guide</Button>
451
+ )}
452
+ {overlays.includes('print-sheets') && (
453
+ <Button onClick={() => void openHostOverlay('print-sheets')}>Print sheets</Button>
454
+ )}
455
+ </Stack>
456
+ );
457
+ }
458
+ ```
459
+
460
+ **Always gate on `useSessionUserInterfaceHostOverlays()`.** Which overlays exist is the host's answer, not yours: a production host and the dev server do not offer the same set, and the guide is only available when the active game declares one. A button for an overlay the host does not have is a button that cannot work. The list is empty until the host answers, and it can change mid-session when a scenario switch changes which guides the game declares.
461
+
462
+ The second argument narrows the overlay:
463
+
464
+ | Overlay | Second argument | Omitted means |
465
+ | --------------------- | ------------------------------------- | ------------------------------- |
466
+ | `'facilitator-guide'` | A guide identifier your game declares | The game's first declared guide |
467
+ | `'print-sheets'` | A team id | Every team |
468
+
469
+ It rejects with a `MutationError` carrying `{ kind: 'denied' }` for an overlay the host does not offer and for an argument that does not resolve.
470
+
471
+ ### Undoing and resetting
472
+
473
+ Facilitators can revert the last tick or reset games, targeted at a specific team or all teams:
474
+
475
+ ```typescript
476
+ import { useSessionUserInterfaceCallbacks } from '@seriousgaming/sdk/ui';
477
+ import { Button, Stack } from '@mui/material';
478
+
479
+ function GameControls() {
480
+ const { undoTick, resetGame } = useSessionUserInterfaceCallbacks();
481
+
482
+ return (
483
+ <Stack direction="row" spacing={1}>
484
+ <Button onClick={() => undoTick()} variant="outlined">
485
+ Undo All Teams
486
+ </Button>
487
+ <Button onClick={() => resetGame()} variant="outlined" color="error">
488
+ Reset All Teams
489
+ </Button>
490
+ </Stack>
491
+ );
492
+ }
493
+ ```
494
+
495
+ - **`undoTick(teamId?)`** puts teams back on the board they were holding when the last tick was taken, decisions and all: it un-runs the engine rather than re-dealing the tick. When `teamId` is omitted, reverts all teams; naming one is independent mode only, since shared-state teams advance together. With no history to undo, the promise rejects with a `MutationError` whose `outcome.kind` is `denied`.
496
+ - **`resetGame(teamId?)`** clears all context, deletes game-scoped blobs, and re-initialises from scratch. When `teamId` is omitted, resets all teams. Settings are preserved.
497
+
498
+ ### Opening and closing the waiting room
499
+
500
+ `setWaitingRoom` parks the session: every participant's game UI is replaced with the game's [waiting room UI](./waiting-room.md) until you close it again.
501
+
502
+ ```typescript
503
+ const { setWaitingRoom } = useSessionUserInterfaceCallbacks();
504
+
505
+ <Button onClick={() => setWaitingRoom(true)} variant="outlined">
506
+ Open waiting room
507
+ </Button>
508
+ ```
509
+
510
+ - **`setWaitingRoom(open)`** takes **no `teamId`**, unlike every other control above. The waiting room is one flag for the whole session, so there is no per-team variant to target
511
+ - Game state is untouched in both directions. Per-team context, the current tick, and timers all survive, which is the point: park a live session for a discussion and resume exactly where teams were
512
+ - It does not stop the clock. Automatic ticks keep firing while participants wait; halting play is the tick controller's decision, via `waitingRoomOpen`
513
+ - It resolves once the host confirms the change and rejects with a `MutationError` otherwise
514
+
515
+ The host provides its own toggle too, so building one is optional. Reach for this when you want the control to sit inside your own facilitator layout, or to pair it with a note you set on a shared capability at the same moment.
516
+
517
+ ### Controlling the roster
518
+
519
+ The session UI owns the roster. Five callbacks create, rename and delete teams, and rename and move participants:
520
+
521
+ ```typescript
522
+ const { createTeam, renameTeam, deleteTeam, renameParticipant, moveParticipant } =
523
+ useSessionUserInterfaceCallbacks();
524
+
525
+ await createTeam('Marketing'); // name is optional
526
+ await renameTeam('team-1', 'Sales');
527
+ await renameParticipant('participant-3', 'Sam');
528
+ await moveParticipant('participant-3', 'team-2');
529
+ await deleteTeam('team-3'); // independent mode, empty teams only
530
+ ```
531
+
532
+ Each resolves on commit and rejects with a `MutationError`. Three refusals arrive as `{ kind: 'denied' }`: the team count is at the effective maximum, a team is being added to a shared-state session (whose team count is fixed at creation, though the dev server relaxes that before the first tick as an authoring convenience), or the session is read-only. **A denial is terminal**: unlike `conflict`, waiting and re-sending cannot change the outcome, so surface it rather than looping.
533
+
534
+ Creating or erasing a participant is deliberately not exposed.
535
+
536
+ #### Deleting a team
537
+
538
+ `deleteTeam(teamId)` removes a team from an **independent-mode** session. Every runtime supports it, unlike `createTeam`, which a runtime may refuse. It is refused with `{ kind: 'denied' }` when:
539
+
540
+ - The session is shared-state, at any point, before the first tick included. A shared-state engine initialised over the whole cohort, so its team count is fixed for the session
541
+ - The team still has participants, connected or not. A participant is never erased, so move every one out with `moveParticipant` first
542
+ - It is the last team in the session
543
+ - The session does not hold the team, or is read-only
544
+
545
+ A deleted team's index is left as a gap, and no other team's index or id changes. The next `createTeam` takes the lowest free index back, as a new team with the default name: nothing of the deleted team returns with it. Undo never brings a deleted team back, and no hook fires.
546
+
547
+ Offer the control only where it can succeed, so a facilitator is not shown a button the host will refuse:
548
+
549
+ ```tsx
550
+ const { mode } = useSessionUserInterfaceTeamConfiguration();
551
+ const participants = useSessionUserInterfaceParticipants();
552
+ const { deleteTeam } = useSessionUserInterfaceCallbacks();
553
+
554
+ {
555
+ mode === 'independent' && (
556
+ <Button
557
+ disabled={teams.length <= 1 || participants.some((p) => p.teamId === team.teamId)}
558
+ onClick={() => void deleteTeam(team.teamId).catch(showDenial)}
559
+ >
560
+ Delete team
561
+ </Button>
562
+ );
563
+ }
564
+ ```
565
+
566
+ The host enforces every rule itself, so the disabled state is a convenience, not a guard. In the dev server every team starts with a placeholder participant, so move it out first, or add the team with `pnpm run sdk team add --participants 0`.
567
+
568
+ `renameTeam` is not the only writer. A team's own participants can rename it from a game UI or waiting room UI, so a session UI should render the name from `setTeamList` rather than from its own optimistic state: there is one name per team, not one per writer, and the last write wins. A blank name and one past the host's cap (60 characters) are both refused as terminal denials, for either writer.
569
+
570
+ #### Moving a participant
571
+
572
+ A move has a consequence worth designing for, and one that is easy to confuse it with:
573
+
574
+ - The participant's **browser loses local component state**, because their game UI remounts against the destination team. Ordinary remount behaviour, and rarely worth warning anyone about
575
+ - Their **earlier writes stay with the source team**. Anything they wrote through `setData` is in that team's context and does not travel, so they arrive with none of their own state
576
+
577
+ A move does not reset either team. The source team's context, including every pending pre-tick decision in it, is untouched.
578
+
579
+ The second consequence is what a game handles, and the mechanism is the [`participantMove` hook](./hooks.md): seed the destination on `phase: 'arrival'` the way `participantJoin` seeds a new participant. There is no transfer channel: a hook returns one team's context, so the departure hook's return value is invisible to arrival. A shared-state game carries state across through the shared store; an independent-mode game re-derives it. Warning a facilitator is the fallback for the case where it genuinely cannot be re-derived, not the default: they cannot implement a hook mid-session, so the warning is only useful where you already know re-seeding is impossible.
580
+
581
+ ## Working with participants
582
+
583
+ Session UIs can enumerate participants across all teams in the session. The hook uses the `Participant` type, which includes a `teamId` field for grouping participants by team.
584
+
585
+ ### Reading the participant list
586
+
587
+ ```typescript
588
+ import {
589
+ useSessionUserInterfaceParticipants,
590
+ type Participant,
591
+ } from '@seriousgaming/sdk/ui';
592
+ import { List, ListItem, ListItemText, Chip, Typography, Stack } from '@mui/material';
593
+
594
+ function AllParticipantsPanel() {
595
+ const participants = useSessionUserInterfaceParticipants();
596
+
597
+ // Group by team
598
+ const byTeam = participants.reduce<Record<string, Participant[]>>(
599
+ (acc, p) => {
600
+ (acc[p.teamId] ??= []).push(p);
601
+ return acc;
602
+ },
603
+ {}
604
+ );
605
+
606
+ return (
607
+ <>
608
+ {Object.entries(byTeam).map(([teamId, members]) => (
609
+ <div key={teamId}>
610
+ <Typography variant="h6">{teamId}</Typography>
611
+ <List>
612
+ {members.map((p) => (
613
+ <ListItem key={p.id}>
614
+ <ListItemText
615
+ primary={p.name}
616
+ />
617
+ <Chip
618
+ size="small"
619
+ label={p.connected ? 'Online' : 'Offline'}
620
+ color={p.connected ? 'success' : 'default'}
621
+ />
622
+ </ListItem>
623
+ ))}
624
+ </List>
625
+ </div>
626
+ ))}
627
+ </>
628
+ );
629
+ }
630
+ ```
631
+
632
+ The hook updates reactively whenever:
633
+
634
+ - A new participant joins any team
635
+ - A participant disconnects (`connected` flips to `false`; the entry stays so you can see who was here)
636
+
637
+ `Participant` exposes:
638
+
639
+ | Field | Type | Notes |
640
+ | ----------- | --------- | ------------------------------------------------------- |
641
+ | `id` | `string` | Stable identifier; survives reconnects within a session |
642
+ | `name` | `string` | Display name (defaults to `Participant N`) |
643
+ | `index` | `number` | Zero-based join order within the team |
644
+ | `connected` | `boolean` | Whether the participant has a live platform connection |
645
+ | `teamId` | `string` | The team this participant belongs to (e.g. `"team-1"`) |
646
+
647
+ ## Rendering inside the host
648
+
649
+ A session UI is not a standalone page. The host mounts it in an iframe under a thin band of its own chrome, and that changes several things a full-page layout takes for granted. None of it is enforced, and all of it is visible to a facilitator the moment it goes wrong.
650
+
651
+ ### Leave the ground to the host
652
+
653
+ Do not paint a page background. The host paints the canvas behind your session UI, and a surface painting its own reads as a panel bolted onto the page rather than part of it. It also never quite matches: the host's background is not a colour your theme exposes, so any value you pick is a guess that drifts the next time the host's palette moves.
654
+
655
+ MUI's `CssBaseline` paints `background.default` on the body, so override it:
656
+
657
+ ```tsx
658
+ import { CssBaseline, GlobalStyles, ThemeProvider } from '@mui/material';
659
+
660
+ const TRANSPARENT_BODY = { body: { backgroundColor: 'transparent' } };
661
+
662
+ <ThemeProvider theme={theme}>
663
+ <CssBaseline enableColorScheme />
664
+ <GlobalStyles styles={TRANSPARENT_BODY} />
665
+ {/* your surface */}
666
+ </ThemeProvider>;
667
+ ```
668
+
669
+ Paint backgrounds on the things that are meant to look raised, like cards and panels, not on the page.
670
+
671
+ ### Prefer full-screen dialogs
672
+
673
+ A centred modal inside an iframe is centred within the frame, not the viewport, and its backdrop dims only the frame. The host's chrome above stays bright, so the result reads as a dialog inside a box rather than a dialog over the page. Use `fullScreen`:
674
+
675
+ ```tsx
676
+ <Dialog open={open} onClose={close} fullScreen>
677
+ …
678
+ </Dialog>
679
+ ```
680
+
681
+ The same applies to anything anchored to the viewport. A toast pinned to the top, a drawer sliding in from an edge, a sticky bar: each lands against the frame's edge rather than the screen's. Your session UI cannot escape its bounds, so design the overlay to sit inside them deliberately instead of discovering where it lands.
682
+
683
+ ### Do not reproduce host chrome
684
+
685
+ The Game view / Platform view toggle, the language picker, the theme control, and the guide button all live in the chrome above your session UI. A second copy inside it looks like the same control and is not: yours cannot switch the host's view, and two language menus on one screen is a bug report waiting to happen.
686
+
687
+ When you want one of the host's own overlays, ask for it with [`openHostOverlay`](#opening-host-overlays) rather than rebuilding it.
688
+
689
+ ### Own your internal scroll
690
+
691
+ The session UI fills the canvas beneath the chrome at a fixed height. There is no resize handshake, so a surface that grows past the canvas is clipped rather than accommodated. Give your layout a real height and let the part that grows scroll:
692
+
693
+ ```tsx
694
+ <Box sx={{ height: '100%', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
695
+ <Header />
696
+ <Box sx={{ flex: 1, overflowY: 'auto', minHeight: 0 }}>{/* the long part */}</Box>
697
+ </Box>
698
+ ```
699
+
700
+ ### Take colour from the theme
701
+
702
+ Read colours from the theme hook below rather than hard-coding them, so your session UI follows the host's light and dark scheme. A surface with a fixed palette is the one thing on screen that does not change when a facilitator switches theme in the room's lighting.
703
+
704
+ ## Using the theme
705
+
706
+ Edit `src/theme.ts` to use the session theme hook:
707
+
708
+ ```typescript
709
+ import { useSessionUserInterfaceTheme } from '@seriousgaming/sdk/ui';
710
+ import { createTheme } from '@mui/material/styles';
711
+ import { useMemo } from 'react';
712
+
713
+ export function useAppTheme() {
714
+ const sdkTheme = useSessionUserInterfaceTheme();
715
+ return useMemo(
716
+ () =>
717
+ createTheme({
718
+ palette: {
719
+ mode: sdkTheme.mode,
720
+ primary: {
721
+ main: (sdkTheme.mode === 'light' ? sdkTheme.light : sdkTheme.dark).primary,
722
+ },
723
+ },
724
+ }),
725
+ [sdkTheme]
726
+ );
727
+ }
728
+ ```
729
+
730
+ ## Dev server behaviour
731
+
732
+ When you run `pnpm run sdk dev` for a game with session UIs:
733
+
734
+ 1. Your session UI **is** the console. The dev server renders it full-bleed in its canvas, under a band of chrome, and opens on it by default whenever the package ships one
735
+ 2. The chrome's **Game view / Platform view** toggle switches between your session UI and the dev server's own roster page, which is the fallback for a package that ships no session UI. It sits outside your iframe, so your session UI cannot suppress it
736
+ 3. The chrome also carries the language picker, the theme control, the facilitator guide and print sheets, the scenario and setup entries, and undo / reset / reload. Do not rebuild these inside your session UI; to open the guide or a print sheet, use [`openHostOverlay`](#opening-host-overlays)
737
+ 4. The route `/session` also works directly, which is useful for iterating on the session UI alone. Opened that way there is no chrome above it, so a host overlay opens in a new tab rather than in a dialog
738
+ 5. Any `setData` calls from the UI iframe are immediately forwarded to the dev server and reflected in the relevant team's game UI
739
+
740
+ Every surface (the console, each game UI, and the session UI) stays in sync via WebSocket.
741
+
742
+ ## Managing session UIs
743
+
744
+ ### Update definition
745
+
746
+ ```bash
747
+ pnpm run sdk update session '{
748
+ "definition": {
749
+ "reads": ["price", "workers"]
750
+ }
751
+ }'
752
+ ```
753
+
754
+ ### Delete
755
+
756
+ Empties the `session/` slot. A package declares whatever is on disk, so removing the directory is the whole of the removal:
757
+
758
+ ```bash
759
+ pnpm run sdk delete session
760
+ ```
761
+
762
+ ## Testing
763
+
764
+ `renderSessionUserInterface` from `@seriousgaming/sdk/ui/testing` renders a session UI with every provider it reads, seeded with `createMockTeamState` and `createMockSharedState`: see [Session UIs](./testing.md#session-uis) in the testing guide.
765
+
766
+ ## Hooks reference
767
+
768
+ ### useSessionUserInterfaceData
769
+
770
+ Read session-wide data and write per-team overrides:
771
+
772
+ ```typescript
773
+ import { useSessionUserInterfaceData } from '@seriousgaming/sdk/ui';
774
+
775
+ const { sessionData, settings, setData } = useSessionUserInterfaceData();
776
+ // sessionData: SessionData, the shared store once (shared.context), and a teams map with each team's own context, tick, and timer
777
+ // setData(partial): writes shared capabilities session-wide
778
+ // settings: the settings resources this surface lists in `settings`, keyed by identifier (see Configuring the surface)
779
+ // setData(teamId, partial): merges partial context into the specified team's state
780
+ // Both forms take { deletes } last: setData(teamId, {}, { deletes: ['pinboard.Notes.p1'] })
781
+ ```
782
+
783
+ ### useSessionUserInterfaceTeams
784
+
785
+ Get the team list:
786
+
787
+ ```typescript
788
+ import { useSessionUserInterfaceTeams } from '@seriousgaming/sdk/ui';
789
+
790
+ const teams = useSessionUserInterfaceTeams();
791
+ // TeamInfo[]: an array of { teamId, index, name }, sorted by index
792
+ ```
793
+
794
+ ### useSessionUserInterfaceTeamConfiguration
795
+
796
+ Get the interaction mode and the bounds the host will hold the session UI to:
797
+
798
+ ```typescript
799
+ import { useSessionUserInterfaceTeamConfiguration } from '@seriousgaming/sdk/ui';
800
+
801
+ const { mode, min, max, perTeam } = useSessionUserInterfaceTeamConfiguration();
802
+ // mode: 'independent' | 'shared-state'
803
+ // max: the effective maximum team count, already resolved as the lower of the
804
+ // game's declared maximum and the platform's own ceiling
805
+ // min, perTeam: advisory guidance to display, not rules to block on
806
+ ```
807
+
808
+ Every bound here is for rendering honest affordances. The host enforces all of them itself, so a surface that disables 'add team' at the maximum is being polite, not authoritative, and must still handle a denial.
809
+
810
+ ### useSessionUserInterfaceMeta
811
+
812
+ Read the session itself:
813
+
814
+ ```typescript
815
+ import { useSessionUserInterfaceMeta } from '@seriousgaming/sdk/ui';
816
+
817
+ const { sessionId, name, readOnly, waitingRoomOpen, joinPin } = useSessionUserInterfaceMeta();
818
+ ```
819
+
820
+ `joinPin` is the code participants enter to join, or `null` when joining needs none. How joining is secured is up to the runtime, so it need not be a PIN and some runtimes never set it: render it as an opaque string, and hide the panel when it is `null`. `readOnly` marks a frozen session, so a surface can disable its own controls and explain why rather than offering affordances that will be denied. `waitingRoomOpen` is the current value of a flag the session UI can also write through `setWaitingRoom`; since the host usually renders its own waiting-room control too, read the result back rather than assuming your write won.
821
+
822
+ ### useSessionUserInterfaceTeamLinks
823
+
824
+ Get each team's facilitator-only URLs, so a game draws its own QR codes and join sheets:
825
+
826
+ ```typescript
827
+ import { useSessionUserInterfaceTeamLinks } from '@seriousgaming/sdk/ui';
828
+
829
+ const teamLinks = useSessionUserInterfaceTeamLinks();
830
+ // TeamLinks[]: each { teamId, joinUrl, fallbackJoinUrl?, observeUrl }
831
+ ```
832
+
833
+ Encode `joinUrl` in a QR code: the host chose that form because a denser code is harder to scan across a room. Show `fallbackJoinUrl` on request rather than beside it, for the networks that block URL shorteners. `observeUrl` opens the team's game UI as an observer, with no participant identity and no presence.
834
+
835
+ ### useSessionUserInterfaceHostOverlays
836
+
837
+ Find out which host overlays this host offers, before rendering a control for one:
838
+
839
+ ```typescript
840
+ import { useSessionUserInterfaceHostOverlays } from '@seriousgaming/sdk/ui';
841
+
842
+ const overlays = useSessionUserInterfaceHostOverlays();
843
+ // HostOverlay[]: 'facilitator-guide' | 'print-sheets'
844
+ ```
845
+
846
+ Empty until the host answers, and empty is also the correct answer for a host that delegates none. It can change mid-session. See [Opening host overlays](#opening-host-overlays).
847
+
848
+ ### useSessionUserInterfaceHookFailures
849
+
850
+ Report hook chains that threw, on the surface the facilitator is actually looking at:
851
+
852
+ ```typescript
853
+ import { useSessionUserInterfaceHookFailures } from '@seriousgaming/sdk/ui';
854
+
855
+ const { failures, clearFailures } = useSessionUserInterfaceHookFailures();
856
+ // failures: { signal, teamId, message }[], kept until cleared
857
+ ```
858
+
859
+ A hook failure is deliberately not a failed mutation: hooks fire after the change they react to is applied, so the mutation that triggered them has already settled. Reporting it as failed would invite retrying an action that worked.
860
+
861
+ ### useSessionUserInterfaceLocale
862
+
863
+ Get the current locale:
864
+
865
+ ```typescript
866
+ import { useSessionUserInterfaceLocale } from '@seriousgaming/sdk/ui';
867
+
868
+ const { locale } = useSessionUserInterfaceLocale();
869
+ ```
870
+
871
+ ### useSessionUserInterfaceTheme
872
+
873
+ Read the host's colour scheme and palettes (`{ mode, light, dark }`, the same shape as the game UI's theme). See [Using the theme](#using-the-theme) for turning it into an MUI theme:
874
+
875
+ ```typescript
876
+ import { useSessionUserInterfaceTheme } from '@seriousgaming/sdk/ui';
877
+
878
+ function MyComponent() {
879
+ const theme = useSessionUserInterfaceTheme();
880
+ const colours = theme.mode === 'light' ? theme.light : theme.dark;
881
+ return <div style={{ color: colours.primary }}>Session Dashboard</div>;
882
+ }
883
+ ```
884
+
885
+ ### useSessionUserInterfaceParticipants
886
+
887
+ Reactive list of participants across all teams in the session:
888
+
889
+ ```typescript
890
+ import { useSessionUserInterfaceParticipants } from '@seriousgaming/sdk/ui';
891
+
892
+ function Roster() {
893
+ const participants = useSessionUserInterfaceParticipants();
894
+ return <div>{participants.length} participants across all teams</div>;
895
+ }
896
+ ```
897
+
898
+ Use this to enumerate participants for facilitator workflows like cross-team comparisons or session-wide reporting. See [Working with participants](#working-with-participants) above for the full field reference.
899
+
900
+ ### useSessionUserInterfaceCallbacks
901
+
902
+ Session-level facilitator callbacks. This is a standalone interface (`SessionUserInterfaceCallbacks`): it does not extend `UserInterfaceCallbacks`. All game-flow callbacks accept an optional `teamId` parameter; omitting it targets all teams.
903
+
904
+ ```typescript
905
+ import { useSessionUserInterfaceCallbacks } from '@seriousgaming/sdk/ui';
906
+
907
+ const { requestTick, pauseTimer, resumeTimer, undoTick, resetGame } =
908
+ useSessionUserInterfaceCallbacks();
909
+ ```
910
+
911
+ | Callback | Signature | Description |
912
+ | ------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
913
+ | `requestTick` | `(teamId?: string) => Promise<void>` | Advance the game by one tick; omit `teamId` to advance all teams |
914
+ | `pauseTimer` | `(teamId?: string) => void` | Pause automatic ticking; omit `teamId` to pause all teams |
915
+ | `resumeTimer` | `(teamId?: string) => void` | Resume automatic ticking; omit `teamId` to resume all teams |
916
+ | `undoTick` | `(teamId?: string) => Promise<void>` | Un-run the last tick, keeping the tick's decisions; omit `teamId` to undo all teams. Naming one is independent mode only |
917
+ | `resetGame` | `(teamId?: string) => Promise<void>` | Re-initialise the game from scratch; omit `teamId` to reset all teams |
918
+ | `extendTick` | `(byMs, teamId?) => Promise<void>` | Move the currently scheduled tick's deadline by `byMs` milliseconds; negative pulls it in |
919
+ | `openHostOverlay` | `(overlay, target?) => Promise<void>` | Ask the host to open the guide or a print sheet |
920
+ | `uploadBlob` | `(file: File) => Promise<string>` | Upload a file, returns a `blob:<id>` reference |
921
+ | `getAssetUrl` | `(reference: string) => string` | Resolve an `asset:`, `shared:`, or `blob:` reference to a URL |
922
+ | `readAsset` | `(reference: string) => Promise<{ data, mimeType, name }>` | Read bytes for an `asset:`, `shared:`, or `blob:` reference |
923
+ | `endpoints` | `Record<string, (request) => Promise<Response>>` | Call a declared endpoint by identifier |
924
+ | `setWaitingRoom` | `(open: boolean) => Promise<void>` | Open or close the session's waiting room; no `teamId` |
925
+ | `createTeam` | `(name?: string) => Promise<void>` | Add a team; the host assigns the index |
926
+ | `renameTeam` | `(teamId, name) => Promise<void>` | Rename a team |
927
+ | `renameParticipant` | `(participantId, name) => Promise<void>` | Rename a participant; the host resolves their team |
928
+ | `moveParticipant` | `(participantId, teamId) => Promise<void>` | Reassign a participant to another team |
929
+ | `deleteTeam` | `(teamId) => Promise<void>` | Delete an empty team; independent mode only, and never the last team |
930
+
931
+ The promise-returning callbacks resolve and reject as described in [Controlling game flow](#controlling-game-flow). The wrapper supplies every callback, so none needs an optional-chaining guard.
932
+
933
+ To generate text, extract document text, or synthesise speech, declare an [endpoint](../recipes/create-endpoint.md) that wraps `generateText`, `extractText`, or `generateSpeech`, then call it through `callbacks.endpoints['…']`. These callbacks are not exposed to UI iframes. See [Call an endpoint from a UI](../recipes/call-endpoint-from-ui.md).
934
+
935
+ ### Message protocol
936
+
937
+ The `SessionUserInterfaceWrapper` holds the fallback until every required message has arrived:
938
+
939
+ | Required message | Description |
940
+ | ---------------------- | ------------------------------------------------ |
941
+ | `setSessionData` | The shared store and all teams' state |
942
+ | `setSessionMeta` | Session identity and lifecycle state |
943
+ | `setSettings` | The settings this session UI declares, resolved |
944
+ | `setTheme` | Colour scheme and palettes |
945
+ | `setLocale` | Current locale string |
946
+ | `setAssetUrls` | Prefixes for resolving asset and blob references |
947
+ | `setTeamConfiguration` | Interaction mode and team bounds |
948
+
949
+ `setTeamConfiguration` gates first paint because its default cannot be rendered honestly: a surface painting before bounds arrive would hold a maximum of zero and disable the controls that read it.
950
+
951
+ Additional messages sent after initialisation:
952
+
953
+ | Additional message | Description |
954
+ | -------------------- | -------------------------------------------------------------------------------------------------------- |
955
+ | `setTeamList` | Updates the list of teams in the session (`TeamInfo[]`, each `{ teamId, index, name }`, sorted by index) |
956
+ | `setTeamLinks` | Updates each team's facilitator-only join and observe URLs |
957
+ | `setParticipantList` | Updates the participant roster across all teams |
958
+ | `hookFailed` | Reports a hook chain that threw |
959
+
960
+ ## Examples
961
+
962
+ The session UI follows the same file structure conventions as other UI resource types. The starter game's `session/` is a complete one: per-team status, join and observe links, a shared briefing, and the waiting room and guide toggles (see [A tour of the starter game](./starter-game.md#session)).
963
+
964
+ ## See also
965
+
966
+ - [Multi-team games](./multi-team-games.md) - Independent and shared-state interaction modes
967
+ - [Building user interfaces](./ui.md) - Building participant-facing game UIs
968
+ - [Building a setup screen](./setup.md) - The form at session creation
969
+ - [Building a scenario editor](./scenario-editor.md) - Authoring lasting scenarios
970
+ - [Tick controllers](./tick-controller.md) - Automatic tick timing
971
+ - [Testing](./testing.md) - Writing UI tests
972
+ - [CLI reference](../cli-reference.md) - `new`, `update`, and `delete session` commands