dreamboard 0.1.21 → 0.1.23

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-GN7232BY.js} +1025 -695
  4. package/dist/chunk-GN7232BY.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,754 @@
1
+ /// <reference lib="dom" />
2
+
3
+ import "./host-main.css";
4
+ import { useState } from "react";
5
+ import { createRoot } from "react-dom/client";
6
+ import { Wrench, X } from "lucide-react";
7
+ import {
8
+ Drawer,
9
+ DrawerClose,
10
+ DrawerContent,
11
+ DrawerDescription,
12
+ DrawerHeader,
13
+ DrawerTitle,
14
+ DrawerTrigger,
15
+ } from "@dreamboard/ui/components/drawer";
16
+ import { client } from "@dreamboard/api-client/client.gen";
17
+ import {
18
+ HostFeedbackToaster,
19
+ HostHistoryNavigator,
20
+ HostPlayerSwitcher,
21
+ HostSessionMetadata,
22
+ HostSessionToolbar,
23
+ PerfOverlay,
24
+ type HostControllablePlayer,
25
+ } from "@dreamboard/ui-host-runtime/components";
26
+ import {
27
+ SSEManager,
28
+ createUnifiedSessionStore,
29
+ unifiedSessionSelectors,
30
+ type HistoryState,
31
+ type HostFeedback,
32
+ } from "@dreamboard/ui-host-runtime/runtime";
33
+ import devConfig from "virtual:dreamboard-dev-config";
34
+ import {
35
+ createDevDiagnosticsLogger,
36
+ formatConsoleArgs,
37
+ resolveDevDiagnosticsLevel,
38
+ shouldRelayDevLog,
39
+ stringifyForRelay,
40
+ type DevLogEnvelope,
41
+ } from "./dev-diagnostics.js";
42
+ import {
43
+ DevHostController,
44
+ type DevHostRuntimeError,
45
+ } from "./dev-host-controller.js";
46
+ import { createDevHostSessionTransport } from "./dev-host-session-transport.js";
47
+ import {
48
+ SessionStorageDevHostStorage,
49
+ type ActiveSession,
50
+ } from "./dev-host-storage.js";
51
+ import { resolveInitialDevHostPlayerId } from "./dev-host-player-query.js";
52
+
53
+ const diagnosticsLevel = resolveDevDiagnosticsLevel(devConfig.debug);
54
+ const devLogger = createDevDiagnosticsLogger(diagnosticsLevel);
55
+ const storage = new SessionStorageDevHostStorage(window.sessionStorage);
56
+ const hostSessionTransport = createDevHostSessionTransport();
57
+ let runtimeDisposed = false;
58
+
59
+ const store = createUnifiedSessionStore({
60
+ createSseManager: () =>
61
+ new SSEManager({
62
+ transport: hostSessionTransport,
63
+ logger: {
64
+ log: (...args: unknown[]) => {
65
+ if (runtimeDisposed) {
66
+ return;
67
+ }
68
+ devLogger.log(...args);
69
+ },
70
+ warn: (...args: unknown[]) => {
71
+ if (runtimeDisposed) {
72
+ return;
73
+ }
74
+ devLogger.warn(...args);
75
+ },
76
+ error: (...args: unknown[]) => {
77
+ if (runtimeDisposed) {
78
+ return;
79
+ }
80
+ devLogger.error(...args);
81
+ controller.handleSseTransportError(args);
82
+ },
83
+ },
84
+ }),
85
+ logger: devLogger,
86
+ transport: hostSessionTransport,
87
+ fallbackToAllSeatsWhenUserIdMissing: !devConfig.userId,
88
+ });
89
+
90
+ const controller = new DevHostController(
91
+ store,
92
+ storage,
93
+ {
94
+ autoStartGame: devConfig.autoStartGame,
95
+ compiledResultId: devConfig.compiledResultId,
96
+ createDevSessionSnapshot: hostSessionTransport.createDevSessionSnapshot,
97
+ debug: devConfig.debug,
98
+ fallbackSession: devConfig.initialSession,
99
+ gameId: devConfig.gameId,
100
+ initialPlayerId: resolveInitialDevHostPlayerId(window.location.search),
101
+ playerCount: devConfig.playerCount,
102
+ setupProfileId: devConfig.setupProfileId,
103
+ slug: devConfig.slug,
104
+ userId: devConfig.userId,
105
+ },
106
+ devLogger,
107
+ );
108
+
109
+ // The browser never sees the bearer token. All backend traffic is
110
+ // same-origin and the CLI's reverse-proxy middleware (`/api/*`) injects
111
+ // `Authorization: Bearer <fresh>` on the wire.
112
+ client.setConfig({ baseUrl: "" });
113
+ installProxyAuthErrorInterceptor();
114
+
115
+ const app = document.getElementById("app");
116
+ if (!(app instanceof HTMLElement)) {
117
+ throw new Error("Missing root app container.");
118
+ }
119
+ const root = createRoot(app);
120
+
121
+ function installProxyAuthErrorInterceptor(): void {
122
+ client.interceptors.response.use(async (response) => {
123
+ if (response.status !== 401) {
124
+ return response;
125
+ }
126
+ const contentType = response.headers.get("content-type") ?? "";
127
+ if (!contentType.includes("application/json")) {
128
+ return response;
129
+ }
130
+ // Clone first so downstream handlers can still read the body.
131
+ const clone = response.clone();
132
+ type AuthErrorPayload = { error?: unknown; message?: unknown };
133
+ let payload: AuthErrorPayload | null = null;
134
+ try {
135
+ payload = (await clone.json()) as AuthErrorPayload;
136
+ } catch {
137
+ return response;
138
+ }
139
+ if (payload?.error !== "session_invalid") {
140
+ return response;
141
+ }
142
+ const detail =
143
+ typeof payload.message === "string" && payload.message.length > 0
144
+ ? payload.message
145
+ : "Stored Dreamboard session is no longer valid.";
146
+ controller.reportRuntimeError({
147
+ title: "Session expired",
148
+ summary: `${detail} Run \`dreamboard login\` in your terminal, then reload this page.`,
149
+ violations: [],
150
+ });
151
+ return response;
152
+ });
153
+ }
154
+
155
+ const restoreConsoleRelay = installConsoleRelay("host");
156
+ const removeWindowErrorRelay = installWindowErrorRelay("host");
157
+ installSseRelay();
158
+ window.addEventListener("message", handlePluginLogMessage);
159
+ window.addEventListener("pagehide", disposeHostRuntime);
160
+ window.addEventListener("beforeunload", disposeHostRuntime);
161
+
162
+ const unsubscribeStoreRender = store.subscribe(() => {
163
+ render();
164
+ });
165
+ const unsubscribeControllerRender = controller.subscribe(() => {
166
+ render();
167
+ });
168
+
169
+ void controller.initialize();
170
+
171
+ if (import.meta.hot) {
172
+ import.meta.hot.accept();
173
+ import.meta.hot.dispose(() => {
174
+ disposeHostRuntime();
175
+ });
176
+ }
177
+
178
+ function render(): void {
179
+ if (runtimeDisposed) {
180
+ return;
181
+ }
182
+
183
+ const state = store.getState();
184
+ const controllerState = controller.getSnapshot();
185
+ const session = state.getPluginSnapshot().session;
186
+ const seats = unifiedSessionSelectors.seats(state);
187
+ const controllablePlayers: HostControllablePlayer[] =
188
+ session.controllablePlayerIds.map((playerId) => {
189
+ const seat = seats.find((entry) => entry.playerId === playerId);
190
+ return {
191
+ playerId,
192
+ displayName: seat?.displayName || playerId,
193
+ };
194
+ });
195
+ const isHost = unifiedSessionSelectors.isSessionHost(state);
196
+ const phase = unifiedSessionSelectors.sessionType(state);
197
+
198
+ root.render(
199
+ <DevHostApp
200
+ session={controllerState.session}
201
+ phase={phase}
202
+ bootstrapStatus={unifiedSessionSelectors.bootstrapStatus(state)}
203
+ isConnected={unifiedSessionSelectors.isConnected(state)}
204
+ connectionError={unifiedSessionSelectors.connectionError(state)}
205
+ runtimeError={controllerState.runtimeError}
206
+ syncId={unifiedSessionSelectors.syncId(state)}
207
+ pluginReady={controllerState.pluginReady}
208
+ controllablePlayers={controllablePlayers}
209
+ controllingPlayerId={
210
+ unifiedSessionSelectors.currentPlayerId(state) ??
211
+ session.controllingPlayerId
212
+ }
213
+ canStart={phase === "lobby" && unifiedSessionSelectors.canStart(state)}
214
+ isHost={isHost}
215
+ history={unifiedSessionSelectors.history(state)}
216
+ hostFeedback={unifiedSessionSelectors.hostFeedback(state)}
217
+ iframeSrc={controllerState.iframeSrc}
218
+ seedValue={controllerState.seedValue}
219
+ isCreatingSession={controllerState.isCreatingSession}
220
+ onSeedChange={(value) => controller.setSeedValue(value)}
221
+ onCreateSession={() => void controller.createNewSession()}
222
+ onStartGame={() => void controller.startGameFromSidebar()}
223
+ onSwitchPlayer={(playerId) => controller.switchPlayer(playerId)}
224
+ onRestoreHistory={(entryId) => controller.restoreHistoryEntry(entryId)}
225
+ onDismissHostFeedback={(feedbackId) =>
226
+ store.getState().dismissHostFeedback(feedbackId)
227
+ }
228
+ onDismissRuntimeError={() => controller.dismissRuntimeError()}
229
+ onIframeReady={(element) => {
230
+ controller.setIframe(element);
231
+ }}
232
+ onIframeLoad={() => {
233
+ controller.onIframeLoad();
234
+ }}
235
+ />,
236
+ );
237
+ }
238
+
239
+ type DevHostAppProps = {
240
+ session: ActiveSession;
241
+ phase: string;
242
+ bootstrapStatus: "loading" | "lobby" | "renderable" | "error";
243
+ isConnected: boolean;
244
+ connectionError: string | null;
245
+ runtimeError: DevHostRuntimeError | null;
246
+ syncId: number;
247
+ pluginReady: boolean;
248
+ controllablePlayers: HostControllablePlayer[];
249
+ controllingPlayerId: string | null;
250
+ canStart: boolean;
251
+ isHost: boolean;
252
+ history: HistoryState | null;
253
+ hostFeedback: HostFeedback[];
254
+ iframeSrc: string;
255
+ seedValue: string;
256
+ isCreatingSession: boolean;
257
+ onSeedChange: (value: string) => void;
258
+ onCreateSession: () => void;
259
+ onStartGame: () => void;
260
+ onSwitchPlayer: (playerId: string) => void;
261
+ onRestoreHistory: (entryId: string) => Promise<void>;
262
+ onDismissHostFeedback: (feedbackId: string) => void;
263
+ onDismissRuntimeError: () => void;
264
+ onIframeReady: (element: HTMLIFrameElement | null) => void;
265
+ onIframeLoad: () => void;
266
+ };
267
+
268
+ function DevHostApp({
269
+ session,
270
+ phase,
271
+ bootstrapStatus,
272
+ isConnected,
273
+ connectionError,
274
+ runtimeError,
275
+ syncId,
276
+ pluginReady,
277
+ controllablePlayers,
278
+ controllingPlayerId,
279
+ canStart,
280
+ isHost,
281
+ history,
282
+ hostFeedback,
283
+ iframeSrc,
284
+ seedValue,
285
+ isCreatingSession,
286
+ onSeedChange,
287
+ onCreateSession,
288
+ onStartGame,
289
+ onSwitchPlayer,
290
+ onRestoreHistory,
291
+ onDismissHostFeedback,
292
+ onDismissRuntimeError,
293
+ onIframeReady,
294
+ onIframeLoad,
295
+ }: DevHostAppProps) {
296
+ const [isSidebarOpen, setIsSidebarOpen] = useState(() =>
297
+ storage.loadSidebarOpen(),
298
+ );
299
+ const needsBootstrap = phase !== "error" && bootstrapStatus === "loading";
300
+
301
+ const handleToggleSidebar = (open: boolean) => {
302
+ setIsSidebarOpen(open);
303
+ storage.persistSidebarOpen(open);
304
+ };
305
+
306
+ return (
307
+ <div className="relative flex h-full w-full overflow-hidden bg-transparent font-sans text-foreground">
308
+ <HostFeedbackToaster
309
+ feedback={hostFeedback}
310
+ onDismiss={onDismissHostFeedback}
311
+ />
312
+ <main className="absolute inset-0 z-0 flex flex-col bg-transparent">
313
+ <iframe
314
+ ref={onIframeReady}
315
+ src={iframeSrc}
316
+ referrerPolicy="no-referrer"
317
+ title="Dreamboard UI Plugin"
318
+ className="h-full w-full flex-1 border-0 bg-background"
319
+ onLoad={onIframeLoad}
320
+ />
321
+ {needsBootstrap ? (
322
+ <div className="pointer-events-none absolute inset-0 flex items-center justify-center bg-[#f7f1da]/92 px-6 text-center backdrop-blur-[2px]">
323
+ <div className="max-w-md rounded-lg border border-border bg-white px-5 py-4 shadow-md">
324
+ <p className="text-xs font-semibold uppercase tracking-[0.2em] text-muted-foreground">
325
+ Dev Host
326
+ </p>
327
+ <h2 className="mt-2 text-xl font-semibold text-foreground">
328
+ Waiting for session bootstrap
329
+ </h2>
330
+ <p className="mt-3 text-sm font-medium text-foreground">
331
+ Attaching to{" "}
332
+ <span className="font-semibold">{session.shortCode}</span> (
333
+ {session.sessionId}).
334
+ </p>
335
+ <p className="mt-2 text-sm text-muted-foreground">
336
+ The host will create a fresh session automatically if the
337
+ current one cannot finish bootstrapping.
338
+ </p>
339
+ {connectionError ? (
340
+ <p className="mt-3 rounded-md border border-border bg-[#ffe1d6] px-3 py-2 text-sm font-semibold text-foreground">
341
+ {connectionError}
342
+ </p>
343
+ ) : null}
344
+ </div>
345
+ </div>
346
+ ) : null}
347
+ {runtimeError ? (
348
+ <RuntimeErrorOverlay
349
+ error={runtimeError}
350
+ onDismiss={onDismissRuntimeError}
351
+ />
352
+ ) : null}
353
+ </main>
354
+
355
+ <Drawer
356
+ open={isSidebarOpen}
357
+ onOpenChange={handleToggleSidebar}
358
+ direction="left"
359
+ >
360
+ {!isSidebarOpen ? (
361
+ <DrawerTrigger asChild>
362
+ <button
363
+ type="button"
364
+ className="absolute left-4 top-4 z-50 flex items-center gap-2 rounded-lg border border-border bg-[#fff9c4] px-3 py-1.5 text-sm font-semibold text-foreground shadow-md transition-colors hover:bg-[#ffe566]"
365
+ >
366
+ <Wrench className="h-4 w-4" />
367
+ Dev Tools
368
+ </button>
369
+ </DrawerTrigger>
370
+ ) : null}
371
+
372
+ <DrawerContent className="dev-drawer-content h-full w-[380px] max-w-[calc(100vw-1rem)] border-r border-border p-0 shadow-xl backdrop-blur-md">
373
+ <div className="dev-drawer-surface flex h-full flex-col overflow-hidden">
374
+ <DrawerHeader className="dev-drawer-header relative overflow-hidden border-b border-border px-5 pb-5 pt-4">
375
+ <DrawerDescription className="sr-only">
376
+ Developer controls for the local Dreamboard host session.
377
+ </DrawerDescription>
378
+ <div className="relative flex items-start justify-between gap-4">
379
+ <div>
380
+ <p className="dev-drawer-label mb-2 inline-block rounded-md px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
381
+ Dreamboard Dev Host
382
+ </p>
383
+ <DrawerTitle className="truncate pr-2 text-xl font-semibold text-foreground">
384
+ {devConfig.slug}
385
+ </DrawerTitle>
386
+ <div className="mt-3 flex flex-wrap items-center gap-2">
387
+ <div className="dev-drawer-session-pill inline-flex max-w-full items-center gap-2 rounded-md border border-border px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.14em]">
388
+ <span className="dev-drawer-session-dot h-2.5 w-2.5 rounded-full border border-border" />
389
+ <span className="truncate">{session.shortCode}</span>
390
+ </div>
391
+ <StatusPill
392
+ label={describePhase(phase, isConnected, pluginReady)}
393
+ />
394
+ <div className="dev-drawer-note inline-flex items-center rounded-md border border-border/40 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
395
+ Sync {syncId}
396
+ </div>
397
+ </div>
398
+ </div>
399
+ <DrawerClose asChild>
400
+ <button
401
+ type="button"
402
+ className="dev-drawer-close relative z-10 rounded-md border border-border p-1.5 transition-colors hover:bg-accent"
403
+ title="Hide Dev Tools"
404
+ >
405
+ <X className="h-4 w-4" />
406
+ </button>
407
+ </DrawerClose>
408
+ </div>
409
+ </DrawerHeader>
410
+
411
+ <div className="flex-1 overflow-y-auto px-4 pb-5 pt-4">
412
+ <div className="space-y-4">
413
+ <section className="dev-drawer-card relative overflow-hidden rounded-lg border border-border p-4 shadow-sm">
414
+ <p className="dev-drawer-card-title mb-3 border-b border-border/40 pb-2 text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
415
+ Session
416
+ </p>
417
+ <HostSessionMetadata
418
+ gameId={session.gameId}
419
+ sessionId={session.sessionId}
420
+ shortCode={session.shortCode}
421
+ />
422
+ <div className="dev-drawer-note mt-4 rounded-md border border-border/40 px-3 py-2 text-sm">
423
+ Backend: {devConfig.apiBaseUrl}
424
+ </div>
425
+ </section>
426
+
427
+ <section className="dev-drawer-controls-card relative overflow-hidden rounded-lg border border-border p-4 shadow-sm">
428
+ <p className="dev-drawer-card-title mb-4 border-b border-border/40 pb-2 text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
429
+ Controls
430
+ </p>
431
+ <div className="space-y-3">
432
+ <label
433
+ className="block text-sm font-semibold text-foreground"
434
+ htmlFor="seed-input"
435
+ >
436
+ Seed
437
+ </label>
438
+ <input
439
+ id="seed-input"
440
+ type="number"
441
+ inputMode="numeric"
442
+ className="dev-drawer-input w-full rounded-md border border-border px-3 py-2 text-sm font-medium font-sans outline-none transition focus:ring-2 focus:ring-primary/30"
443
+ value={seedValue}
444
+ onChange={(event) => onSeedChange(event.target.value)}
445
+ />
446
+ <button
447
+ type="button"
448
+ className="dev-drawer-primary-button w-full rounded-md border border-border px-4 py-2 text-sm font-semibold shadow-sm transition-colors hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
449
+ disabled={isCreatingSession}
450
+ onClick={onCreateSession}
451
+ >
452
+ {isCreatingSession ? "Creating..." : "New Session"}
453
+ </button>
454
+ </div>
455
+
456
+ <div className="mt-4 space-y-3">
457
+ {canStart ? (
458
+ <button
459
+ type="button"
460
+ className="dev-drawer-danger-button w-full rounded-md border border-border px-4 py-2 text-sm font-semibold shadow-sm transition-colors"
461
+ onClick={onStartGame}
462
+ >
463
+ Start Game
464
+ </button>
465
+ ) : null}
466
+ <div className="dev-drawer-toolbar-shell rounded-md border border-dashed border-border/50 p-3">
467
+ <HostSessionToolbar className="justify-center flex-wrap gap-2">
468
+ <HostPlayerSwitcher
469
+ controllablePlayers={controllablePlayers}
470
+ controllingPlayerId={controllingPlayerId}
471
+ onSwitchPlayer={onSwitchPlayer}
472
+ />
473
+ <HostHistoryNavigator
474
+ isHost={isHost}
475
+ history={history}
476
+ onRestoreHistory={onRestoreHistory}
477
+ />
478
+ </HostSessionToolbar>
479
+ </div>
480
+ </div>
481
+ </section>
482
+ </div>
483
+ </div>
484
+ </div>
485
+ </DrawerContent>
486
+ </Drawer>
487
+ <PerfOverlay />
488
+ </div>
489
+ );
490
+ }
491
+
492
+ function StatusPill({ label }: { label: string }) {
493
+ return (
494
+ <div className="dev-drawer-note inline-flex items-center rounded-md border border-border/40 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.16em] text-muted-foreground">
495
+ {label}
496
+ </div>
497
+ );
498
+ }
499
+
500
+ /**
501
+ * Full-viewport overlay that surfaces runtime failures (e.g. reducer
502
+ * `initialize` rejections, session-start 500s) to the game author instead of
503
+ * burying them in the backend log. Each violation is rendered as its own
504
+ * block so the JS stack trace — which the backend already parses into a
505
+ * separate violation entry — stays readable with file/line detail intact.
506
+ */
507
+ function RuntimeErrorOverlay({
508
+ error,
509
+ onDismiss,
510
+ }: {
511
+ error: DevHostRuntimeError;
512
+ onDismiss: () => void;
513
+ }) {
514
+ const headlineViolation =
515
+ error.violations.length > 0 ? error.violations[0] : null;
516
+ const stackViolations = error.violations.slice(1);
517
+
518
+ return (
519
+ <div className="absolute inset-0 z-40 flex items-center justify-center bg-[#2d2d2d]/70 px-6 py-8 backdrop-blur-[2px]">
520
+ <div className="relative max-h-[85vh] w-full max-w-2xl overflow-hidden rounded-lg border border-border bg-[#fff7e5] shadow-xl">
521
+ <div className="flex items-start justify-between gap-4 border-b border-border bg-[#ffd3d3] px-5 py-4">
522
+ <div>
523
+ <p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-foreground/70">
524
+ Runtime error
525
+ </p>
526
+ <h2 className="mt-1 text-xl font-semibold text-foreground">
527
+ {error.title}
528
+ </h2>
529
+ </div>
530
+ <button
531
+ type="button"
532
+ onClick={onDismiss}
533
+ className="shrink-0 rounded-md border border-border bg-white p-1.5 transition-colors hover:bg-accent"
534
+ title="Dismiss"
535
+ >
536
+ <X className="h-4 w-4" />
537
+ </button>
538
+ </div>
539
+ <div className="max-h-[calc(85vh-88px)] overflow-y-auto px-5 py-4">
540
+ <p className="text-sm font-medium text-foreground">{error.summary}</p>
541
+ {headlineViolation ? (
542
+ <div className="mt-4 rounded-md border border-border bg-white px-3 py-2">
543
+ <p className="text-[10px] font-semibold uppercase tracking-[0.2em] text-muted-foreground">
544
+ {headlineViolation.code ?? "message"}
545
+ {headlineViolation.field ? ` · ${headlineViolation.field}` : ""}
546
+ </p>
547
+ <p className="mt-1 whitespace-pre-wrap break-words text-sm font-semibold text-foreground">
548
+ {headlineViolation.message}
549
+ </p>
550
+ </div>
551
+ ) : null}
552
+ {stackViolations.length > 0 ? (
553
+ <details className="mt-4" open>
554
+ <summary className="cursor-pointer text-[11px] font-semibold uppercase tracking-[0.2em] text-foreground/70">
555
+ Stack trace
556
+ </summary>
557
+ <pre className="mt-2 max-h-64 overflow-auto rounded-md border border-border bg-[#1f1f1f] p-3 text-[11px] leading-relaxed text-[#f8f8f2]">
558
+ {stackViolations
559
+ .map((violation) => violation.message)
560
+ .join("\n")}
561
+ </pre>
562
+ </details>
563
+ ) : null}
564
+ {error.correlationId ? (
565
+ <p className="mt-4 text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
566
+ Request ID · {error.correlationId}
567
+ </p>
568
+ ) : null}
569
+ </div>
570
+ </div>
571
+ </div>
572
+ );
573
+ }
574
+
575
+ function installSseRelay(): void {
576
+ let lastLoggedEventId = 0;
577
+ store.subscribe((state) => {
578
+ const nextEntries = unifiedSessionSelectors
579
+ .sseEvents(state)
580
+ .filter((entry) => entry.id > lastLoggedEventId);
581
+ if (nextEntries.length === 0) {
582
+ return;
583
+ }
584
+
585
+ for (const entry of nextEntries) {
586
+ lastLoggedEventId = entry.id;
587
+ relayBrowserLog({
588
+ source: "sse",
589
+ level: "info",
590
+ message: devConfig.debug
591
+ ? `${entry.eventType} ${stringifyForRelay(entry.data)}`
592
+ : `${entry.eventType} toUser=${getMessageRecipient(entry.data) ?? "-"}`,
593
+ });
594
+ }
595
+ });
596
+ }
597
+
598
+ function getMessageRecipient(message: unknown): unknown {
599
+ return message && typeof message === "object" && "toUser" in message
600
+ ? (message as { toUser?: unknown }).toUser
601
+ : null;
602
+ }
603
+
604
+ function installConsoleRelay(source: "host"): () => void {
605
+ const original = {
606
+ log: console.log.bind(console),
607
+ warn: console.warn.bind(console),
608
+ error: console.error.bind(console),
609
+ };
610
+
611
+ console.log = (...args: unknown[]) => {
612
+ original.log(...args);
613
+ relayBrowserLog({
614
+ source,
615
+ level: "log",
616
+ message: formatConsoleArgs(args),
617
+ });
618
+ };
619
+ console.warn = (...args: unknown[]) => {
620
+ original.warn(...args);
621
+ relayBrowserLog({
622
+ source,
623
+ level: "warn",
624
+ message: formatConsoleArgs(args),
625
+ });
626
+ };
627
+ console.error = (...args: unknown[]) => {
628
+ original.error(...args);
629
+ relayBrowserLog({
630
+ source,
631
+ level: "error",
632
+ message: formatConsoleArgs(args),
633
+ });
634
+ };
635
+
636
+ return () => {
637
+ console.log = original.log;
638
+ console.warn = original.warn;
639
+ console.error = original.error;
640
+ };
641
+ }
642
+
643
+ function installWindowErrorRelay(source: "host"): () => void {
644
+ const onError = (event: ErrorEvent) => {
645
+ if (runtimeDisposed || shouldIgnoreBrowserError(event.error)) {
646
+ return;
647
+ }
648
+ relayBrowserLog({
649
+ source,
650
+ level: "error",
651
+ message: `window.error ${event.message}`,
652
+ });
653
+ };
654
+ const onUnhandledRejection = (event: PromiseRejectionEvent) => {
655
+ if (runtimeDisposed || shouldIgnoreBrowserError(event.reason)) {
656
+ return;
657
+ }
658
+ relayBrowserLog({
659
+ source,
660
+ level: "error",
661
+ message: `unhandledrejection ${stringifyForRelay(event.reason)}`,
662
+ });
663
+ };
664
+
665
+ window.addEventListener("error", onError);
666
+ window.addEventListener("unhandledrejection", onUnhandledRejection);
667
+
668
+ return () => {
669
+ window.removeEventListener("error", onError);
670
+ window.removeEventListener("unhandledrejection", onUnhandledRejection);
671
+ };
672
+ }
673
+
674
+ function handlePluginLogMessage(event: MessageEvent): void {
675
+ if (!controller.matchesPluginWindow(event.source)) {
676
+ return;
677
+ }
678
+
679
+ const payload = event.data as Partial<DevLogEnvelope> & { type?: string };
680
+ if (
681
+ !payload ||
682
+ typeof payload !== "object" ||
683
+ payload.type !== "dreamboard-dev-console"
684
+ ) {
685
+ return;
686
+ }
687
+
688
+ relayBrowserLog({
689
+ source: "plugin",
690
+ level:
691
+ payload.level === "warn" ||
692
+ payload.level === "error" ||
693
+ payload.level === "log"
694
+ ? payload.level
695
+ : "log",
696
+ message:
697
+ typeof payload.message === "string"
698
+ ? payload.message
699
+ : stringifyForRelay(payload.message),
700
+ });
701
+ }
702
+
703
+ function relayBrowserLog(payload: DevLogEnvelope): void {
704
+ if (runtimeDisposed || !shouldRelayDevLog(diagnosticsLevel, payload)) {
705
+ return;
706
+ }
707
+
708
+ void fetch("/__dreamboard_dev/log", {
709
+ method: "POST",
710
+ headers: { "content-type": "application/json" },
711
+ body: JSON.stringify(payload),
712
+ keepalive: true,
713
+ }).catch(() => {
714
+ // Ignore log relay failures to avoid recursive console noise.
715
+ });
716
+ }
717
+
718
+ function disposeHostRuntime(): void {
719
+ if (runtimeDisposed) {
720
+ return;
721
+ }
722
+
723
+ runtimeDisposed = true;
724
+ removeWindowErrorRelay();
725
+ restoreConsoleRelay();
726
+ window.removeEventListener("message", handlePluginLogMessage);
727
+ window.removeEventListener("pagehide", disposeHostRuntime);
728
+ window.removeEventListener("beforeunload", disposeHostRuntime);
729
+ unsubscribeStoreRender();
730
+ unsubscribeControllerRender();
731
+ controller.dispose();
732
+ root.unmount();
733
+ }
734
+
735
+ function shouldIgnoreBrowserError(value: unknown): boolean {
736
+ return value instanceof Error && value.name === "AbortError";
737
+ }
738
+
739
+ function describePhase(
740
+ phase: string,
741
+ isConnected: boolean,
742
+ isPluginReady: boolean,
743
+ ): string {
744
+ if (phase === "error") {
745
+ return "Error";
746
+ }
747
+ if (!isConnected) {
748
+ return "Connecting SSE";
749
+ }
750
+ if (!isPluginReady) {
751
+ return "Loading iframe";
752
+ }
753
+ return phase === "gameplay" ? "Live gameplay" : phase;
754
+ }