dreamboard 0.1.21 → 0.1.22

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 (186) hide show
  1. package/dist/{chunk-EYYWGWTO.js → chunk-2RCUHMGL.js} +343 -1911
  2. package/dist/chunk-2RCUHMGL.js.map +1 -0
  3. package/dist/{chunk-MOVHYB6E.js → chunk-AQ5UUNJS.js} +1018 -692
  4. package/dist/chunk-AQ5UUNJS.js.map +1 -0
  5. package/dist/{chunk-BMYC6772.js → chunk-H3O43F5P.js} +9765 -3068
  6. package/dist/chunk-H3O43F5P.js.map +1 -0
  7. package/dist/dev-host/dev-api-proxy-plugin.ts +330 -0
  8. package/dist/dev-host/dev-diagnostics.ts +62 -0
  9. package/dist/dev-host/dev-fallback-stylesheet.ts +50 -0
  10. package/dist/dev-host/dev-host-controller.ts +686 -0
  11. package/dist/dev-host/dev-host-player-query.ts +17 -0
  12. package/dist/dev-host/dev-host-session-transport.ts +52 -0
  13. package/dist/dev-host/dev-host-storage.ts +56 -0
  14. package/dist/dev-host/dev-log-relay-plugin.ts +469 -0
  15. package/dist/dev-host/dev-runtime-config.ts +14 -0
  16. package/dist/dev-host/dev-runtime-platform.ts +419 -0
  17. package/dist/dev-host/dev-virtual-modules-plugin.ts +63 -0
  18. package/dist/dev-host/host-main.css +182 -0
  19. package/dist/dev-host/host-main.tsx +754 -0
  20. package/dist/dev-host/index.html +56 -0
  21. package/dist/dev-host/plugin-main.ts +55 -0
  22. package/dist/dev-host/plugin.html +24 -0
  23. package/dist/dev-host/start-dev-server.ts +138 -0
  24. package/dist/dev-host/virtual-modules.d.ts +27 -0
  25. package/dist/dist-FEPN3BDN.js +16543 -0
  26. package/dist/dist-FEPN3BDN.js.map +1 -0
  27. package/dist/{dist-WJRJNFLI.js → dist-FRURQI7Q.js} +4 -2
  28. package/dist/index.js +3 -3
  29. package/dist/internal.js +159 -124
  30. package/dist/internal.js.map +1 -1
  31. package/dist/runtime-packages/tailwind-config/shared-styles.css +146 -0
  32. package/dist/runtime-packages/ui/src/components/.gitkeep +0 -0
  33. package/dist/runtime-packages/ui/src/components/accordion.tsx +66 -0
  34. package/dist/runtime-packages/ui/src/components/ai-elements/actions.tsx +65 -0
  35. package/dist/runtime-packages/ui/src/components/ai-elements/artifact.tsx +147 -0
  36. package/dist/runtime-packages/ui/src/components/ai-elements/branch.tsx +215 -0
  37. package/dist/runtime-packages/ui/src/components/ai-elements/canvas.tsx +22 -0
  38. package/dist/runtime-packages/ui/src/components/ai-elements/chain-of-thought.tsx +228 -0
  39. package/dist/runtime-packages/ui/src/components/ai-elements/code-block.tsx +179 -0
  40. package/dist/runtime-packages/ui/src/components/ai-elements/confirmation.tsx +158 -0
  41. package/dist/runtime-packages/ui/src/components/ai-elements/connection.tsx +28 -0
  42. package/dist/runtime-packages/ui/src/components/ai-elements/context.tsx +408 -0
  43. package/dist/runtime-packages/ui/src/components/ai-elements/controls.tsx +18 -0
  44. package/dist/runtime-packages/ui/src/components/ai-elements/conversation.tsx +97 -0
  45. package/dist/runtime-packages/ui/src/components/ai-elements/edge.tsx +140 -0
  46. package/dist/runtime-packages/ui/src/components/ai-elements/image.tsx +24 -0
  47. package/dist/runtime-packages/ui/src/components/ai-elements/inline-citation.tsx +287 -0
  48. package/dist/runtime-packages/ui/src/components/ai-elements/loader.tsx +96 -0
  49. package/dist/runtime-packages/ui/src/components/ai-elements/message.tsx +463 -0
  50. package/dist/runtime-packages/ui/src/components/ai-elements/node.tsx +71 -0
  51. package/dist/runtime-packages/ui/src/components/ai-elements/open-in-chat.tsx +365 -0
  52. package/dist/runtime-packages/ui/src/components/ai-elements/panel.tsx +15 -0
  53. package/dist/runtime-packages/ui/src/components/ai-elements/plan.tsx +142 -0
  54. package/dist/runtime-packages/ui/src/components/ai-elements/prompt-input.tsx +1380 -0
  55. package/dist/runtime-packages/ui/src/components/ai-elements/queue.tsx +274 -0
  56. package/dist/runtime-packages/ui/src/components/ai-elements/reasoning.tsx +182 -0
  57. package/dist/runtime-packages/ui/src/components/ai-elements/response.tsx +22 -0
  58. package/dist/runtime-packages/ui/src/components/ai-elements/shimmer.tsx +64 -0
  59. package/dist/runtime-packages/ui/src/components/ai-elements/sources.tsx +77 -0
  60. package/dist/runtime-packages/ui/src/components/ai-elements/suggestion.tsx +53 -0
  61. package/dist/runtime-packages/ui/src/components/ai-elements/task.tsx +87 -0
  62. package/dist/runtime-packages/ui/src/components/ai-elements/tool.tsx +165 -0
  63. package/dist/runtime-packages/ui/src/components/ai-elements/toolbar.tsx +16 -0
  64. package/dist/runtime-packages/ui/src/components/ai-elements/web-preview.tsx +263 -0
  65. package/dist/runtime-packages/ui/src/components/alert-dialog.tsx +157 -0
  66. package/dist/runtime-packages/ui/src/components/alert.tsx +66 -0
  67. package/dist/runtime-packages/ui/src/components/avatar.tsx +53 -0
  68. package/dist/runtime-packages/ui/src/components/badge.tsx +46 -0
  69. package/dist/runtime-packages/ui/src/components/button-group.tsx +83 -0
  70. package/dist/runtime-packages/ui/src/components/button.tsx +65 -0
  71. package/dist/runtime-packages/ui/src/components/card.tsx +92 -0
  72. package/dist/runtime-packages/ui/src/components/carousel.tsx +241 -0
  73. package/dist/runtime-packages/ui/src/components/collapsible.tsx +33 -0
  74. package/dist/runtime-packages/ui/src/components/command.tsx +184 -0
  75. package/dist/runtime-packages/ui/src/components/context-menu.tsx +252 -0
  76. package/dist/runtime-packages/ui/src/components/counter.tsx +11 -0
  77. package/dist/runtime-packages/ui/src/components/dialog.tsx +146 -0
  78. package/dist/runtime-packages/ui/src/components/drawer.tsx +132 -0
  79. package/dist/runtime-packages/ui/src/components/dropdown-menu.tsx +257 -0
  80. package/dist/runtime-packages/ui/src/components/form.tsx +168 -0
  81. package/dist/runtime-packages/ui/src/components/header.tsx +13 -0
  82. package/dist/runtime-packages/ui/src/components/hover-card.tsx +44 -0
  83. package/dist/runtime-packages/ui/src/components/input-group.tsx +178 -0
  84. package/dist/runtime-packages/ui/src/components/input.tsx +21 -0
  85. package/dist/runtime-packages/ui/src/components/item.tsx +193 -0
  86. package/dist/runtime-packages/ui/src/components/label.tsx +24 -0
  87. package/dist/runtime-packages/ui/src/components/navigation-menu.tsx +168 -0
  88. package/dist/runtime-packages/ui/src/components/popover.tsx +55 -0
  89. package/dist/runtime-packages/ui/src/components/progress.tsx +31 -0
  90. package/dist/runtime-packages/ui/src/components/radio-group.tsx +45 -0
  91. package/dist/runtime-packages/ui/src/components/resizable.tsx +56 -0
  92. package/dist/runtime-packages/ui/src/components/scroll-area.tsx +58 -0
  93. package/dist/runtime-packages/ui/src/components/select.tsx +187 -0
  94. package/dist/runtime-packages/ui/src/components/separator.tsx +28 -0
  95. package/dist/runtime-packages/ui/src/components/skeleton.tsx +13 -0
  96. package/dist/runtime-packages/ui/src/components/sonner.tsx +25 -0
  97. package/dist/runtime-packages/ui/src/components/spinner.tsx +16 -0
  98. package/dist/runtime-packages/ui/src/components/switch.tsx +31 -0
  99. package/dist/runtime-packages/ui/src/components/table.tsx +137 -0
  100. package/dist/runtime-packages/ui/src/components/tabs.tsx +74 -0
  101. package/dist/runtime-packages/ui/src/components/textarea.tsx +23 -0
  102. package/dist/runtime-packages/ui/src/components/theme-provider.tsx +73 -0
  103. package/dist/runtime-packages/ui/src/components/tooltip.tsx +61 -0
  104. package/dist/runtime-packages/ui/src/components/ui/file-upload.tsx +195 -0
  105. package/dist/runtime-packages/ui/src/hooks/.gitkeep +0 -0
  106. package/dist/runtime-packages/ui/src/index.ts +68 -0
  107. package/dist/runtime-packages/ui/src/lib/utils.ts +6 -0
  108. package/dist/runtime-packages/ui-host-runtime/src/actor-principal.ts +68 -0
  109. package/dist/runtime-packages/ui-host-runtime/src/components/host-controls.tsx +359 -0
  110. package/dist/runtime-packages/ui-host-runtime/src/components/host-feedback-toaster.tsx +274 -0
  111. package/dist/runtime-packages/ui-host-runtime/src/components/host-feedback.tsx +214 -0
  112. package/dist/runtime-packages/ui-host-runtime/src/components/host-session-metadata.tsx +135 -0
  113. package/dist/runtime-packages/ui-host-runtime/src/components/index.ts +5 -0
  114. package/dist/runtime-packages/ui-host-runtime/src/components/perf-overlay.tsx +194 -0
  115. package/dist/runtime-packages/ui-host-runtime/src/host-controls.tsx +1 -0
  116. package/dist/runtime-packages/ui-host-runtime/src/host-feedback.tsx +1 -0
  117. package/dist/runtime-packages/ui-host-runtime/src/host-session-transport.ts +162 -0
  118. package/dist/runtime-packages/ui-host-runtime/src/index.ts +2 -0
  119. package/dist/runtime-packages/ui-host-runtime/src/logger.ts +11 -0
  120. package/dist/runtime-packages/ui-host-runtime/src/perf.ts +253 -0
  121. package/dist/runtime-packages/ui-host-runtime/src/plugin-bridge.ts +195 -0
  122. package/dist/runtime-packages/ui-host-runtime/src/plugin-health-check.ts +138 -0
  123. package/dist/runtime-packages/ui-host-runtime/src/plugin-messages.ts +159 -0
  124. package/dist/runtime-packages/ui-host-runtime/src/plugin-session-gateway.ts +524 -0
  125. package/dist/runtime-packages/ui-host-runtime/src/runtime/index.ts +12 -0
  126. package/dist/runtime-packages/ui-host-runtime/src/screenshot/projection-to-snapshot.ts +122 -0
  127. package/dist/runtime-packages/ui-host-runtime/src/screenshot/static-store-api.ts +26 -0
  128. package/dist/runtime-packages/ui-host-runtime/src/session-ingress-controller.ts +477 -0
  129. package/dist/runtime-packages/ui-host-runtime/src/session-ingress.ts +209 -0
  130. package/dist/runtime-packages/ui-host-runtime/src/session-live-runtime.ts +112 -0
  131. package/dist/runtime-packages/ui-host-runtime/src/session-model.ts +318 -0
  132. package/dist/runtime-packages/ui-host-runtime/src/session-projection.ts +211 -0
  133. package/dist/runtime-packages/ui-host-runtime/src/session-state-reducer.ts +814 -0
  134. package/dist/runtime-packages/ui-host-runtime/src/sse-manager.ts +334 -0
  135. package/dist/runtime-packages/ui-host-runtime/src/unified-session-store.ts +180 -0
  136. package/dist/scaffold/assets/static/app/tsconfig.framework.json +3 -1
  137. package/dist/scaffold/assets/static/ui/tsconfig.framework.json +7 -19
  138. package/package.json +52 -17
  139. package/skills/dreamboard/SKILL.md +41 -75
  140. package/LICENSE +0 -89
  141. package/NOTICE +0 -1
  142. package/dist/chunk-BMYC6772.js.map +0 -1
  143. package/dist/chunk-EYYWGWTO.js.map +0 -1
  144. package/dist/chunk-MOVHYB6E.js.map +0 -1
  145. package/dist/scaffold/assets/static/ui/App.tsx +0 -22
  146. package/dist/src-CUL7EGGG.js +0 -634
  147. package/dist/src-CUL7EGGG.js.map +0 -1
  148. package/skills/dreamboard/references/authoring-lifecycle.md +0 -102
  149. package/skills/dreamboard/references/board-surfaces.md +0 -36
  150. package/skills/dreamboard/references/board-topology.md +0 -443
  151. package/skills/dreamboard/references/boards-and-topology.md +0 -100
  152. package/skills/dreamboard/references/card-actions.md +0 -107
  153. package/skills/dreamboard/references/cli.md +0 -120
  154. package/skills/dreamboard/references/core-concepts.md +0 -514
  155. package/skills/dreamboard/references/custom-renderers.md +0 -26
  156. package/skills/dreamboard/references/derived-values.md +0 -55
  157. package/skills/dreamboard/references/effects.md +0 -111
  158. package/skills/dreamboard/references/game-contract.md +0 -89
  159. package/skills/dreamboard/references/game-definition.md +0 -89
  160. package/skills/dreamboard/references/game-shell.md +0 -80
  161. package/skills/dreamboard/references/hand-surfaces.md +0 -33
  162. package/skills/dreamboard/references/index.md +0 -112
  163. package/skills/dreamboard/references/inputs-and-targets.md +0 -160
  164. package/skills/dreamboard/references/interactions.md +0 -158
  165. package/skills/dreamboard/references/manifest-fields.md +0 -137
  166. package/skills/dreamboard/references/manifest.md +0 -81
  167. package/skills/dreamboard/references/package-surfaces.md +0 -69
  168. package/skills/dreamboard/references/phases.md +0 -145
  169. package/skills/dreamboard/references/prompts-and-choices.md +0 -55
  170. package/skills/dreamboard/references/quickstart.md +0 -71
  171. package/skills/dreamboard/references/rule-authoring.md +0 -144
  172. package/skills/dreamboard/references/setup-bootstrap.md +0 -78
  173. package/skills/dreamboard/references/stages-and-zones.md +0 -59
  174. package/skills/dreamboard/references/static-views.md +0 -67
  175. package/skills/dreamboard/references/table-queries-and-ops.md +0 -103
  176. package/skills/dreamboard/references/testing-bases.md +0 -147
  177. package/skills/dreamboard/references/testing-generated-contracts.md +0 -142
  178. package/skills/dreamboard/references/testing-runtime-assertions.md +0 -124
  179. package/skills/dreamboard/references/testing-scenarios.md +0 -148
  180. package/skills/dreamboard/references/testing-ui-tests.md +0 -174
  181. package/skills/dreamboard/references/testing.md +0 -161
  182. package/skills/dreamboard/references/ui-architecture.md +0 -137
  183. package/skills/dreamboard/references/ui-components.md +0 -34
  184. package/skills/dreamboard/references/views.md +0 -72
  185. package/skills/dreamboard/references/workspace-layout.md +0 -136
  186. /package/dist/{dist-WJRJNFLI.js.map → dist-FRURQI7Q.js.map} +0 -0
@@ -0,0 +1,814 @@
1
+ import type {
2
+ GameplayPerspective,
3
+ GameplayViewport,
4
+ SessionContext,
5
+ UnifiedSessionModel,
6
+ } from "./session-model.js";
7
+ import {
8
+ getGameplayViewport,
9
+ getSessionContext,
10
+ resolveControllablePlayerIds,
11
+ withContext,
12
+ } from "./session-model.js";
13
+ import type {
14
+ NormalizedSessionEvent,
15
+ NormalizedSessionSnapshot,
16
+ } from "./session-ingress.js";
17
+
18
+ export type NotificationType =
19
+ | "YOUR_TURN"
20
+ | "PROMPT_OPENED"
21
+ | "ACTION_EXECUTED"
22
+ | "ACTION_REJECTED"
23
+ | "TURN_CHANGED"
24
+ | "STATE_CHANGED"
25
+ | "GAME_ENDED"
26
+ | "ERROR";
27
+
28
+ export type NotificationPayload =
29
+ | { type: "YOUR_TURN"; activePlayers: string[] }
30
+ | {
31
+ type: "PROMPT_OPENED";
32
+ promptId: string;
33
+ promptInstanceId: string;
34
+ targetPlayer: string;
35
+ title?: string;
36
+ }
37
+ | { type: "ACTION_EXECUTED"; playerId: string; actionType: string }
38
+ | { type: "ACTION_REJECTED"; reason: string; targetPlayer?: string }
39
+ | {
40
+ type: "TURN_CHANGED";
41
+ previousPlayers: string[];
42
+ currentPlayers: string[];
43
+ }
44
+ | { type: "STATE_CHANGED"; newState: string }
45
+ | {
46
+ type: "GAME_ENDED";
47
+ winner?: string;
48
+ finalScores: Record<string, number>;
49
+ reason: string;
50
+ }
51
+ | { type: "ERROR"; message: string; code?: string };
52
+
53
+ export interface Notification {
54
+ id: string;
55
+ type: NotificationType;
56
+ payload: NotificationPayload;
57
+ timestamp: number;
58
+ read: boolean;
59
+ }
60
+
61
+ export type HostFeedbackType =
62
+ | "ACTION_REJECTED"
63
+ | "PROMPT_OPENED"
64
+ | "YOUR_TURN";
65
+
66
+ export type HostFeedbackPayload =
67
+ | { type: "ACTION_REJECTED"; reason: string; targetPlayer?: string }
68
+ | {
69
+ type: "PROMPT_OPENED";
70
+ promptId: string;
71
+ promptInstanceId: string;
72
+ targetPlayer: string;
73
+ title?: string;
74
+ }
75
+ | { type: "YOUR_TURN"; activePlayers: string[] };
76
+
77
+ export interface HostFeedback {
78
+ id: string;
79
+ type: HostFeedbackType;
80
+ payload: HostFeedbackPayload;
81
+ timestamp: number;
82
+ }
83
+
84
+ export interface SSEEventEntry {
85
+ id: number;
86
+ eventType: string;
87
+ data: unknown;
88
+ timestamp: string;
89
+ }
90
+
91
+ export interface ConnectionState {
92
+ lobby: boolean;
93
+ gameplay: boolean;
94
+ error: string | null;
95
+ isConnected: boolean;
96
+ }
97
+
98
+ export interface ActivityState {
99
+ notifications: Notification[];
100
+ hostFeedback: HostFeedback[];
101
+ syncId: number;
102
+ lastSyncTimestamp: number | null;
103
+ lastAckTimestamp: number | null;
104
+ }
105
+
106
+ export interface DebugState {
107
+ sseEvents: SSEEventEntry[];
108
+ maxEvents: number;
109
+ }
110
+
111
+ export interface UnifiedSessionState {
112
+ session: UnifiedSessionModel;
113
+ connection: ConnectionState;
114
+ activity: ActivityState;
115
+ debug: DebugState;
116
+ }
117
+
118
+ export type SnapshotIngressSource =
119
+ | "short-code"
120
+ | "snapshot"
121
+ | "start"
122
+ | "dev-new";
123
+
124
+ export type EventIngressSource = "sse" | "submit-response";
125
+
126
+ export type SessionStateIngress =
127
+ | {
128
+ type: "command.loading";
129
+ target?: { sessionId?: string; shortCode?: string };
130
+ }
131
+ | { type: "command.failed"; message: string }
132
+ | {
133
+ type: "snapshot.loaded";
134
+ source: SnapshotIngressSource;
135
+ snapshot: NormalizedSessionSnapshot;
136
+ expectedPerspectivePlayerId?: string | null;
137
+ }
138
+ | {
139
+ type: "event.received";
140
+ source: EventIngressSource;
141
+ event: NormalizedSessionEvent;
142
+ debugEvent?: { eventType: string; data: unknown };
143
+ clientActionId?: string | null;
144
+ }
145
+ | {
146
+ type: "connection.prepared";
147
+ userId: string | null;
148
+ switchablePlayerIds: string[];
149
+ }
150
+ | {
151
+ type: "connection.changed";
152
+ channel: "lobby" | "gameplay";
153
+ connected: boolean;
154
+ }
155
+ | { type: "connection.failed"; message: string }
156
+ | { type: "connection.errorCleared" }
157
+ | {
158
+ type: "feedback.actionRejected";
159
+ reason: string;
160
+ targetPlayer?: string;
161
+ }
162
+ | { type: "local.playerSelected"; playerId: string; sessionId: string }
163
+ | { type: "activity.stateAcked" }
164
+ | { type: "activity.notificationRead"; id: string }
165
+ | { type: "activity.notificationsCleared" }
166
+ | { type: "activity.hostFeedbackDismissed"; id: string }
167
+ | { type: "activity.hostFeedbackCleared" }
168
+ | { type: "debug.sseEventsCleared" }
169
+ | { type: "streams.closed" }
170
+ | { type: "session.reset" };
171
+
172
+ export type SessionStateEffect =
173
+ | {
174
+ type: "requestGameplayResync";
175
+ sessionId: string;
176
+ playerId: string;
177
+ reason: string;
178
+ }
179
+ | {
180
+ type: "reconnectGameplay";
181
+ sessionId: string;
182
+ playerId: string;
183
+ source: "player-switch";
184
+ }
185
+ | {
186
+ type: "perf.storeApplied";
187
+ gameplayVersion: number;
188
+ syncId: number;
189
+ }
190
+ | {
191
+ type: "log.warn";
192
+ message: string;
193
+ };
194
+
195
+ export interface SessionStateReducerEnvironment {
196
+ fallbackToAllSeatsWhenUserIdMissing: boolean;
197
+ nextEventId: () => number;
198
+ nextNotificationId: () => string;
199
+ nowMs: () => number;
200
+ nowIso: () => string;
201
+ }
202
+
203
+ export interface SessionStateReducerResult {
204
+ state: UnifiedSessionState;
205
+ effects: SessionStateEffect[];
206
+ }
207
+
208
+ export function createInitialUnifiedSessionState(): UnifiedSessionState {
209
+ return {
210
+ session: { type: "idle" },
211
+ connection: {
212
+ lobby: false,
213
+ gameplay: false,
214
+ error: null,
215
+ isConnected: false,
216
+ },
217
+ activity: {
218
+ notifications: [],
219
+ hostFeedback: [],
220
+ syncId: 0,
221
+ lastSyncTimestamp: null,
222
+ lastAckTimestamp: null,
223
+ },
224
+ debug: { sseEvents: [], maxEvents: 500 },
225
+ };
226
+ }
227
+
228
+ function appendSseEvent(
229
+ state: UnifiedSessionState,
230
+ debugEvent: { eventType: string; data: unknown },
231
+ env: SessionStateReducerEnvironment,
232
+ ) {
233
+ const newEvent: SSEEventEntry = {
234
+ id: env.nextEventId(),
235
+ eventType: debugEvent.eventType,
236
+ data: debugEvent.data,
237
+ timestamp: env.nowIso(),
238
+ };
239
+ return [...state.debug.sseEvents, newEvent].slice(-state.debug.maxEvents);
240
+ }
241
+
242
+ function createActionRejectedArtifacts(
243
+ reason: string,
244
+ targetPlayer: string | undefined,
245
+ env: SessionStateReducerEnvironment,
246
+ ) {
247
+ return {
248
+ notification: {
249
+ id: env.nextNotificationId(),
250
+ type: "ACTION_REJECTED" as const,
251
+ payload: { type: "ACTION_REJECTED" as const, reason, targetPlayer },
252
+ timestamp: env.nowMs(),
253
+ read: false,
254
+ },
255
+ hostFeedback: {
256
+ id: env.nextNotificationId(),
257
+ type: "ACTION_REJECTED" as const,
258
+ payload: { type: "ACTION_REJECTED" as const, reason, targetPlayer },
259
+ timestamp: env.nowMs(),
260
+ },
261
+ };
262
+ }
263
+
264
+ function createPromptOpenedArtifacts(
265
+ prompt: {
266
+ interactionId: string;
267
+ context?: { to?: string; title?: string };
268
+ },
269
+ env: SessionStateReducerEnvironment,
270
+ ) {
271
+ const payload = {
272
+ type: "PROMPT_OPENED" as const,
273
+ promptId: prompt.interactionId,
274
+ promptInstanceId: prompt.interactionId,
275
+ targetPlayer: prompt.context?.to ?? "",
276
+ title: prompt.context?.title ?? undefined,
277
+ };
278
+ return {
279
+ notification: {
280
+ id: env.nextNotificationId(),
281
+ type: "PROMPT_OPENED" as const,
282
+ payload,
283
+ timestamp: env.nowMs(),
284
+ read: false,
285
+ },
286
+ hostFeedback: {
287
+ id: env.nextNotificationId(),
288
+ type: "PROMPT_OPENED" as const,
289
+ payload,
290
+ timestamp: env.nowMs(),
291
+ },
292
+ };
293
+ }
294
+
295
+ function getOpenedPrompts(
296
+ previous: ReadonlyArray<{ kind: string; interactionId: string }>,
297
+ next: ReadonlyArray<{
298
+ kind: string;
299
+ interactionId: string;
300
+ context?: { to?: string; title?: string };
301
+ }>,
302
+ ) {
303
+ const previousPromptIds = new Set(
304
+ previous
305
+ .filter((descriptor) => descriptor.kind === "prompt")
306
+ .map((prompt) => prompt.interactionId),
307
+ );
308
+ return next.filter(
309
+ (descriptor) =>
310
+ descriptor.kind === "prompt" &&
311
+ !previousPromptIds.has(descriptor.interactionId),
312
+ );
313
+ }
314
+
315
+ function hasBoardStaticHashMismatch(
316
+ gameplay: GameplayViewport | null,
317
+ incomingGameplay: GameplayViewport,
318
+ ): boolean {
319
+ const incomingHash = incomingGameplay.boardStaticHash ?? null;
320
+ const cachedHash = gameplay?.boardStaticHash ?? null;
321
+ return (
322
+ cachedHash !== null && incomingHash !== null && incomingHash !== cachedHash
323
+ );
324
+ }
325
+
326
+ function applyGameplaySnapshotToState(
327
+ state: UnifiedSessionState,
328
+ gameplay: GameplayViewport,
329
+ context: SessionContext,
330
+ options: {
331
+ nextSyncId: number;
332
+ perspective: GameplayPerspective;
333
+ debugEvent?: { eventType: string; data: unknown };
334
+ env: SessionStateReducerEnvironment;
335
+ },
336
+ ): UnifiedSessionState {
337
+ const previousGameplay = getGameplayViewport(state.session);
338
+ const promptArtifacts = getOpenedPrompts(
339
+ previousGameplay?.availableInteractions ?? [],
340
+ gameplay.availableInteractions,
341
+ ).map((prompt) => createPromptOpenedArtifacts(prompt, options.env));
342
+ return {
343
+ ...state,
344
+ session: {
345
+ type: "gameplay",
346
+ context,
347
+ perspective: options.perspective,
348
+ gameplay,
349
+ },
350
+ activity: {
351
+ ...state.activity,
352
+ notifications: [
353
+ ...state.activity.notifications,
354
+ ...promptArtifacts.map((artifact) => artifact.notification),
355
+ ],
356
+ hostFeedback: [
357
+ ...state.activity.hostFeedback,
358
+ ...promptArtifacts.map((artifact) => artifact.hostFeedback),
359
+ ],
360
+ syncId: options.nextSyncId,
361
+ lastSyncTimestamp: options.env.nowMs(),
362
+ },
363
+ debug: {
364
+ ...state.debug,
365
+ sseEvents: options.debugEvent
366
+ ? appendSseEvent(state, options.debugEvent, options.env)
367
+ : state.debug.sseEvents,
368
+ },
369
+ };
370
+ }
371
+
372
+ function assertExpectedPerspectivePlayer(
373
+ snapshot: NormalizedSessionSnapshot,
374
+ expectedPlayerId: string | null | undefined,
375
+ ) {
376
+ if (!expectedPlayerId || snapshot.type !== "gameplay") {
377
+ return;
378
+ }
379
+ if (snapshot.perspective.playerId !== expectedPlayerId) {
380
+ throw new Error(
381
+ `Switch snapshot resolved ${snapshot.perspective.playerId} instead of ${expectedPlayerId}.`,
382
+ );
383
+ }
384
+ }
385
+
386
+ function reduceSnapshotLoaded(
387
+ state: UnifiedSessionState,
388
+ ingress: Extract<SessionStateIngress, { type: "snapshot.loaded" }>,
389
+ env: SessionStateReducerEnvironment,
390
+ ): UnifiedSessionState {
391
+ assertExpectedPerspectivePlayer(
392
+ ingress.snapshot,
393
+ ingress.expectedPerspectivePlayerId,
394
+ );
395
+ const context = ingress.snapshot.context;
396
+ const nextSyncId = state.activity.syncId + 1;
397
+
398
+ if (ingress.snapshot.type !== "gameplay") {
399
+ const session: UnifiedSessionModel =
400
+ ingress.snapshot.type === "ended"
401
+ ? { type: "ended", context }
402
+ : {
403
+ type: "lobby",
404
+ context,
405
+ preferredPlayerId: ingress.snapshot.preferredPlayerId,
406
+ };
407
+ return {
408
+ ...state,
409
+ session,
410
+ activity: {
411
+ ...state.activity,
412
+ syncId: nextSyncId,
413
+ lastSyncTimestamp: env.nowMs(),
414
+ },
415
+ connection: { ...state.connection, error: null },
416
+ };
417
+ }
418
+
419
+ return {
420
+ ...applyGameplaySnapshotToState(state, ingress.snapshot.gameplay, context, {
421
+ nextSyncId,
422
+ perspective: ingress.snapshot.perspective,
423
+ env,
424
+ }),
425
+ connection: { ...state.connection, error: null },
426
+ };
427
+ }
428
+
429
+ function reduceEventReceived(
430
+ state: UnifiedSessionState,
431
+ ingress: Extract<SessionStateIngress, { type: "event.received" }>,
432
+ env: SessionStateReducerEnvironment,
433
+ ): SessionStateReducerResult {
434
+ const context = getSessionContext(state.session);
435
+ const currentGameplay = getGameplayViewport(state.session);
436
+ const nextSyncId = state.activity.syncId + 1;
437
+ const debugEvent = ingress.source === "sse" ? ingress.debugEvent : undefined;
438
+
439
+ switch (ingress.event.type) {
440
+ case "session.snapshot": {
441
+ const snapshotState = reduceSnapshotLoaded(
442
+ state,
443
+ {
444
+ type: "snapshot.loaded",
445
+ source: "snapshot",
446
+ snapshot: ingress.event.snapshot,
447
+ },
448
+ env,
449
+ );
450
+ return {
451
+ state: debugEvent
452
+ ? {
453
+ ...snapshotState,
454
+ debug: {
455
+ ...snapshotState.debug,
456
+ sseEvents: appendSseEvent(state, debugEvent, env),
457
+ },
458
+ }
459
+ : snapshotState,
460
+ effects: [],
461
+ };
462
+ }
463
+ case "session.lobbyUpdated": {
464
+ const nextContext = ingress.event.context;
465
+ if (!nextContext) return { state, effects: [] };
466
+ return {
467
+ state: {
468
+ ...state,
469
+ session: withContext(state.session, nextContext),
470
+ activity: {
471
+ ...state.activity,
472
+ syncId: nextSyncId,
473
+ lastSyncTimestamp: env.nowMs(),
474
+ },
475
+ debug: {
476
+ ...state.debug,
477
+ sseEvents: debugEvent
478
+ ? appendSseEvent(state, debugEvent, env)
479
+ : state.debug.sseEvents,
480
+ },
481
+ },
482
+ effects: [],
483
+ };
484
+ }
485
+ case "session.gameplayUpdated": {
486
+ if (!context || !ingress.event.context) return { state, effects: [] };
487
+ if (
488
+ ingress.source === "sse" &&
489
+ ((state.session.type === "gameplay" &&
490
+ ingress.event.perspective.playerId !==
491
+ state.session.perspective.playerId) ||
492
+ (state.session.type === "gameplayLoading" &&
493
+ ingress.event.perspective.playerId !==
494
+ state.session.requestedPlayerId))
495
+ ) {
496
+ return { state, effects: [] };
497
+ }
498
+ if (
499
+ currentGameplay &&
500
+ ingress.event.gameplay.version <= currentGameplay.version
501
+ ) {
502
+ return { state, effects: [] };
503
+ }
504
+ const updatedState = applyGameplaySnapshotToState(
505
+ state,
506
+ ingress.event.gameplay,
507
+ ingress.event.context,
508
+ {
509
+ nextSyncId,
510
+ perspective: ingress.event.perspective,
511
+ debugEvent,
512
+ env,
513
+ },
514
+ );
515
+ const effects: SessionStateEffect[] = [
516
+ {
517
+ type: "perf.storeApplied",
518
+ gameplayVersion: ingress.event.gameplay.version,
519
+ syncId: nextSyncId,
520
+ },
521
+ ];
522
+ if (hasBoardStaticHashMismatch(currentGameplay, ingress.event.gameplay)) {
523
+ effects.push({
524
+ type: "requestGameplayResync",
525
+ sessionId: context.identity.sessionId,
526
+ playerId: ingress.event.perspective.playerId,
527
+ reason: `boardStaticHash mismatch cached=${currentGameplay?.boardStaticHash} incoming=${ingress.event.gameplay.boardStaticHash}`,
528
+ });
529
+ }
530
+ return { state: updatedState, effects };
531
+ }
532
+ case "session.historyUpdated": {
533
+ const nextContext = ingress.event.context;
534
+ if (!nextContext) return { state, effects: [] };
535
+ return {
536
+ state: {
537
+ ...state,
538
+ session: withContext(state.session, nextContext),
539
+ activity: {
540
+ ...state.activity,
541
+ syncId: nextSyncId,
542
+ lastSyncTimestamp: env.nowMs(),
543
+ },
544
+ debug: {
545
+ ...state.debug,
546
+ sseEvents: debugEvent
547
+ ? appendSseEvent(state, debugEvent, env)
548
+ : state.debug.sseEvents,
549
+ },
550
+ },
551
+ effects: [],
552
+ };
553
+ }
554
+ case "session.ended": {
555
+ const nextContext = ingress.event.context;
556
+ if (!nextContext) return { state, effects: [] };
557
+ return {
558
+ state: {
559
+ ...state,
560
+ session: { type: "ended", context: nextContext },
561
+ activity: {
562
+ ...state.activity,
563
+ syncId: nextSyncId,
564
+ lastSyncTimestamp: env.nowMs(),
565
+ },
566
+ debug: {
567
+ ...state.debug,
568
+ sseEvents: debugEvent
569
+ ? appendSseEvent(state, debugEvent, env)
570
+ : state.debug.sseEvents,
571
+ },
572
+ },
573
+ effects: [],
574
+ };
575
+ }
576
+ case "session.error":
577
+ return {
578
+ state: {
579
+ ...state,
580
+ session: {
581
+ type: "error",
582
+ message: ingress.event.message,
583
+ context: context ?? undefined,
584
+ },
585
+ activity: {
586
+ ...state.activity,
587
+ syncId: nextSyncId,
588
+ lastSyncTimestamp: env.nowMs(),
589
+ },
590
+ debug: {
591
+ ...state.debug,
592
+ sseEvents: debugEvent
593
+ ? appendSseEvent(state, debugEvent, env)
594
+ : state.debug.sseEvents,
595
+ },
596
+ },
597
+ effects: [],
598
+ };
599
+ }
600
+ }
601
+
602
+ export function reduceSessionState(
603
+ state: UnifiedSessionState,
604
+ ingress: SessionStateIngress,
605
+ env: SessionStateReducerEnvironment,
606
+ ): SessionStateReducerResult {
607
+ switch (ingress.type) {
608
+ case "command.loading":
609
+ return {
610
+ state: {
611
+ ...state,
612
+ session: { type: "loading", target: ingress.target },
613
+ connection: { ...state.connection, error: null },
614
+ },
615
+ effects: [],
616
+ };
617
+ case "command.failed":
618
+ return {
619
+ state: {
620
+ ...state,
621
+ session: {
622
+ type: "error",
623
+ message: ingress.message,
624
+ context: getSessionContext(state.session) ?? undefined,
625
+ },
626
+ },
627
+ effects: [],
628
+ };
629
+ case "snapshot.loaded":
630
+ return {
631
+ state: reduceSnapshotLoaded(state, ingress, env),
632
+ effects: [],
633
+ };
634
+ case "event.received":
635
+ return reduceEventReceived(state, ingress, env);
636
+ case "connection.prepared": {
637
+ const context = getSessionContext(state.session);
638
+ if (!context) return { state, effects: [] };
639
+ return {
640
+ state: {
641
+ ...state,
642
+ session: withContext(state.session, {
643
+ ...context,
644
+ userId: ingress.userId,
645
+ switchablePlayerIds: ingress.switchablePlayerIds,
646
+ }),
647
+ connection: { ...state.connection, error: null },
648
+ },
649
+ effects: [],
650
+ };
651
+ }
652
+ case "connection.changed": {
653
+ const connection = {
654
+ ...state.connection,
655
+ [ingress.channel]: ingress.connected,
656
+ };
657
+ return {
658
+ state: {
659
+ ...state,
660
+ connection: {
661
+ ...connection,
662
+ isConnected: connection.lobby || connection.gameplay,
663
+ },
664
+ },
665
+ effects: [],
666
+ };
667
+ }
668
+ case "connection.failed":
669
+ return {
670
+ state: {
671
+ ...state,
672
+ connection: { ...state.connection, error: ingress.message },
673
+ },
674
+ effects: [],
675
+ };
676
+ case "connection.errorCleared":
677
+ return {
678
+ state: {
679
+ ...state,
680
+ connection: { ...state.connection, error: null },
681
+ },
682
+ effects: [],
683
+ };
684
+ case "feedback.actionRejected": {
685
+ const { notification, hostFeedback } = createActionRejectedArtifacts(
686
+ ingress.reason,
687
+ ingress.targetPlayer,
688
+ env,
689
+ );
690
+ return {
691
+ state: {
692
+ ...state,
693
+ activity: {
694
+ ...state.activity,
695
+ notifications: [...state.activity.notifications, notification],
696
+ hostFeedback: [...state.activity.hostFeedback, hostFeedback],
697
+ },
698
+ },
699
+ effects: [],
700
+ };
701
+ }
702
+ case "local.playerSelected": {
703
+ const context = getSessionContext(state.session);
704
+ if (!context) return { state, effects: [] };
705
+ const controllablePlayerIds = resolveControllablePlayerIds(
706
+ context.switchablePlayerIds,
707
+ context.seats,
708
+ context.userId,
709
+ env.fallbackToAllSeatsWhenUserIdMissing,
710
+ );
711
+ if (!controllablePlayerIds.includes(ingress.playerId)) {
712
+ return {
713
+ state,
714
+ effects: [
715
+ {
716
+ type: "log.warn",
717
+ message: `[UnifiedSession] Cannot switch to ${ingress.playerId} - not controllable`,
718
+ },
719
+ ],
720
+ };
721
+ }
722
+ return {
723
+ state: {
724
+ ...state,
725
+ session: {
726
+ type: "gameplayLoading",
727
+ context,
728
+ requestedPlayerId: ingress.playerId,
729
+ },
730
+ },
731
+ effects: [
732
+ {
733
+ type: "reconnectGameplay",
734
+ sessionId: ingress.sessionId,
735
+ playerId: ingress.playerId,
736
+ source: "player-switch",
737
+ },
738
+ ],
739
+ };
740
+ }
741
+ case "activity.stateAcked":
742
+ return {
743
+ state: {
744
+ ...state,
745
+ activity: { ...state.activity, lastAckTimestamp: env.nowMs() },
746
+ },
747
+ effects: [],
748
+ };
749
+ case "activity.notificationRead":
750
+ return {
751
+ state: {
752
+ ...state,
753
+ activity: {
754
+ ...state.activity,
755
+ notifications: state.activity.notifications.map((notification) =>
756
+ notification.id === ingress.id
757
+ ? { ...notification, read: true }
758
+ : notification,
759
+ ),
760
+ },
761
+ },
762
+ effects: [],
763
+ };
764
+ case "activity.notificationsCleared":
765
+ return {
766
+ state: {
767
+ ...state,
768
+ activity: { ...state.activity, notifications: [] },
769
+ },
770
+ effects: [],
771
+ };
772
+ case "activity.hostFeedbackDismissed":
773
+ return {
774
+ state: {
775
+ ...state,
776
+ activity: {
777
+ ...state.activity,
778
+ hostFeedback: state.activity.hostFeedback.filter(
779
+ (item) => item.id !== ingress.id,
780
+ ),
781
+ },
782
+ },
783
+ effects: [],
784
+ };
785
+ case "activity.hostFeedbackCleared":
786
+ return {
787
+ state: {
788
+ ...state,
789
+ activity: { ...state.activity, hostFeedback: [] },
790
+ },
791
+ effects: [],
792
+ };
793
+ case "debug.sseEventsCleared":
794
+ return {
795
+ state: { ...state, debug: { ...state.debug, sseEvents: [] } },
796
+ effects: [],
797
+ };
798
+ case "streams.closed":
799
+ return {
800
+ state: {
801
+ ...state,
802
+ connection: {
803
+ ...state.connection,
804
+ lobby: false,
805
+ gameplay: false,
806
+ isConnected: false,
807
+ },
808
+ },
809
+ effects: [],
810
+ };
811
+ case "session.reset":
812
+ return { state: createInitialUnifiedSessionState(), effects: [] };
813
+ }
814
+ }