@neta-art/cohub 8.23.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/dist/board/animation.d.ts +57 -181
  2. package/dist/board/animation.js +374 -220
  3. package/dist/board/core/arrow-geometry.d.ts +33 -18
  4. package/dist/board/core/arrow-geometry.js +283 -52
  5. package/dist/board/core/draw-geometry.d.ts +22 -36
  6. package/dist/board/core/draw-geometry.js +148 -85
  7. package/dist/board/core/export-assets.d.ts +3 -6
  8. package/dist/board/core/export-assets.js +0 -2
  9. package/dist/board/core/export-plan.d.ts +12 -63
  10. package/dist/board/core/export-plan.js +55 -94
  11. package/dist/board/core/palette.d.ts +4 -21
  12. package/dist/board/core/palette.js +8 -13
  13. package/dist/board/core/scene.d.ts +33 -0
  14. package/dist/board/core/scene.js +302 -0
  15. package/dist/board/core/shape-definition.d.ts +12 -24
  16. package/dist/board/core/shape-definition.js +0 -19
  17. package/dist/board/core/shape-types.d.ts +6 -120
  18. package/dist/board/core/shape-types.js +5 -5
  19. package/dist/board/core/text-metrics.d.ts +8 -11
  20. package/dist/board/core/text-metrics.js +5 -34
  21. package/dist/board/core/tool-styles.d.ts +6 -16
  22. package/dist/board/core/tool-styles.js +17 -25
  23. package/dist/board/export/index.d.ts +10 -17
  24. package/dist/board/export/index.js +22 -21
  25. package/dist/board/export/scene.d.ts +10 -16
  26. package/dist/board/export/scene.js +17 -30
  27. package/dist/board/geometry.d.ts +27 -56
  28. package/dist/board/geometry.js +7 -67
  29. package/dist/board/headless/index.d.ts +7 -27
  30. package/dist/board/headless/index.js +20 -19
  31. package/dist/board/headless/sketch.d.ts +19 -0
  32. package/dist/board/headless/sketch.js +146 -0
  33. package/dist/board/image-key.d.ts +2 -12
  34. package/dist/board/image-key.js +3 -13
  35. package/dist/board/index.d.ts +18 -20
  36. package/dist/board/index.js +17 -17
  37. package/dist/board/media-playback.d.ts +3 -6
  38. package/dist/board/media-playback.js +6 -6
  39. package/dist/board/presets.d.ts +150 -0
  40. package/dist/board/presets.js +226 -0
  41. package/dist/board/render/board-background.d.ts +5 -3
  42. package/dist/board/render/board-background.js +8 -7
  43. package/dist/board/render/clip.d.ts +14 -0
  44. package/dist/board/render/clip.js +47 -0
  45. package/dist/board/render/index.d.ts +8 -5
  46. package/dist/board/render/index.js +7 -4
  47. package/dist/board/render/media-interaction.d.ts +4 -5
  48. package/dist/board/render/media-interaction.js +1 -2
  49. package/dist/board/render/palette.d.ts +6 -2
  50. package/dist/board/render/palette.js +7 -1
  51. package/dist/board/render/renderers/arrow-card-renderer.js +69 -71
  52. package/dist/board/render/renderers/audio-card-renderer.js +3 -3
  53. package/dist/board/render/renderers/base-card-renderer.d.ts +6 -17
  54. package/dist/board/render/renderers/base-card-renderer.js +1 -24
  55. package/dist/board/render/renderers/board-renderer-registry.d.ts +20 -50
  56. package/dist/board/render/renderers/board-renderer-registry.js +6 -7
  57. package/dist/board/render/renderers/draw-card-renderer.js +89 -56
  58. package/dist/board/render/renderers/effect-card-renderer.d.ts +6 -0
  59. package/dist/board/render/renderers/effect-card-renderer.js +197 -0
  60. package/dist/board/render/renderers/far-plate.d.ts +4 -21
  61. package/dist/board/render/renderers/far-plate.js +15 -34
  62. package/dist/board/render/renderers/file-card-renderer.d.ts +4 -25
  63. package/dist/board/render/renderers/file-card-renderer.js +8 -58
  64. package/dist/board/render/renderers/frame-card-renderer.js +28 -28
  65. package/dist/board/render/renderers/image-card-renderer.d.ts +4 -3
  66. package/dist/board/render/renderers/image-card-renderer.js +0 -9
  67. package/dist/board/render/renderers/shape-card-renderer.d.ts +9 -0
  68. package/dist/board/render/renderers/shape-card-renderer.js +240 -0
  69. package/dist/board/render/renderers/sketch-card-renderer.d.ts +5 -0
  70. package/dist/board/render/renderers/sketch-card-renderer.js +98 -0
  71. package/dist/board/render/renderers/task-card-renderer.d.ts +0 -1
  72. package/dist/board/render/renderers/task-card-renderer.js +14 -16
  73. package/dist/board/render/renderers/text-card-renderer.d.ts +1 -3
  74. package/dist/board/render/renderers/text-card-renderer.js +56 -46
  75. package/dist/board/render/renderers/unknown-card-renderer.js +4 -10
  76. package/dist/board/render/renderers/video-card-renderer.d.ts +4 -2
  77. package/dist/board/render/renderers/video-card-renderer.js +2 -1
  78. package/dist/board/render/sketch-host.d.ts +14 -0
  79. package/dist/board/render/sketch-host.js +216 -0
  80. package/dist/board/render/stroke.d.ts +12 -0
  81. package/dist/board/render/stroke.js +90 -0
  82. package/dist/board/render/text-measurement.d.ts +6 -26
  83. package/dist/board/render/text-measurement.js +11 -30
  84. package/dist/board/replay.d.ts +6 -31
  85. package/dist/board/replay.js +45 -203
  86. package/dist/board/runtime/board-player.d.ts +24 -0
  87. package/dist/board/runtime/board-player.js +161 -0
  88. package/dist/board/task.d.ts +2 -10
  89. package/dist/board/task.js +1 -10
  90. package/dist/chunks/environment.d.ts +2296 -3864
  91. package/dist/chunks/host-bridge.d.ts +253 -0
  92. package/dist/chunks/http.d.ts +238 -58
  93. package/dist/chunks/http.js +1061 -1034
  94. package/dist/chunks/transport.js +113 -59
  95. package/dist/chunks/websocket.d.ts +1 -1
  96. package/dist/chunks/websocket.js +1 -1
  97. package/dist/host-bridge.d.ts +54 -0
  98. package/dist/host-bridge.js +322 -0
  99. package/dist/http.d.ts +3 -3
  100. package/dist/index.d.ts +76 -193
  101. package/dist/index.js +2037 -1191
  102. package/dist/protocol/dist/board-constants.d.ts +1 -29
  103. package/dist/protocol/dist/board-constants.js +2 -115
  104. package/dist/protocol/dist/board-layout.d.ts +59 -0
  105. package/dist/protocol/dist/board-layout.js +482 -0
  106. package/dist/protocol/dist/board-model.d.ts +2052 -0
  107. package/dist/protocol/dist/board-model.js +592 -0
  108. package/dist/protocol/dist/board-patch.d.ts +86 -0
  109. package/dist/protocol/dist/board-patch.js +734 -0
  110. package/dist/protocol/dist/board-schema.d.ts +33 -0
  111. package/dist/protocol/dist/board-schema.js +59 -0
  112. package/dist/protocol/dist/board-upgrade.d.ts +1 -0
  113. package/dist/protocol/dist/board-url.d.ts +1 -3
  114. package/dist/protocol/dist/board-url.js +2 -2
  115. package/dist/protocol/dist/board.d.ts +37 -232
  116. package/dist/protocol/dist/board.js +62 -160
  117. package/dist/protocol/dist/display/actions.d.ts +1 -0
  118. package/dist/protocol/dist/display/index.d.ts +2 -0
  119. package/dist/protocol/dist/index.d.ts +11 -12
  120. package/dist/protocol/dist/model/metrics.d.ts +2 -0
  121. package/dist/protocol/dist/model/session.d.ts +2 -1
  122. package/dist/protocol/dist/realtime/types.d.ts +2 -2
  123. package/dist/protocol/dist/runtime/index.d.ts +1 -0
  124. package/dist/space-picker.d.ts +1 -0
  125. package/dist/space-picker.js +1 -0
  126. package/dist/types.d.ts +2 -5
  127. package/package.json +9 -1
  128. package/dist/board/core/connections.d.ts +0 -108
  129. package/dist/board/core/connections.js +0 -455
  130. package/dist/board/mutation.d.ts +0 -6
  131. package/dist/board/mutation.js +0 -14
  132. package/dist/board/render/connection-layer.d.ts +0 -53
  133. package/dist/board/render/connection-layer.js +0 -375
  134. package/dist/board/render/renderers/geo-card-renderer.d.ts +0 -5
  135. package/dist/board/render/renderers/geo-card-renderer.js +0 -127
  136. package/dist/board/semantic-document.d.ts +0 -37
  137. package/dist/board/semantic-document.js +0 -288
  138. package/dist/board/semantic-mutation.d.ts +0 -10
  139. package/dist/board/semantic-mutation.js +0 -209
  140. package/dist/protocol/dist/board-animation.js +0 -34
  141. package/dist/protocol/dist/board-authoring.d.ts +0 -1326
  142. package/dist/protocol/dist/board-authoring.js +0 -302
  143. package/dist/protocol/dist/board-capability-registry.d.ts +0 -2
  144. package/dist/protocol/dist/board-capability-registry.js +0 -123
  145. package/dist/protocol/dist/board-codec.d.ts +0 -3
  146. package/dist/protocol/dist/board-codec.js +0 -383
  147. package/dist/protocol/dist/board-composition.d.ts +0 -383
  148. package/dist/protocol/dist/board-composition.js +0 -310
  149. package/dist/protocol/dist/board-connection.d.ts +0 -317
  150. package/dist/protocol/dist/board-connection.js +0 -219
  151. package/dist/protocol/dist/board-document.d.ts +0 -1566
  152. package/dist/protocol/dist/board-document.js +0 -456
  153. package/dist/protocol/dist/board-effect.d.ts +0 -96
  154. package/dist/protocol/dist/board-effect.js +0 -54
  155. package/dist/protocol/dist/board-geometry.d.ts +0 -15
  156. package/dist/protocol/dist/board-geometry.js +0 -137
  157. package/dist/protocol/dist/board-node.d.ts +0 -8
  158. package/dist/protocol/dist/board-node.js +0 -252
  159. /package/dist/protocol/dist/{board-animation.d.ts → host-bridge.d.ts} +0 -0
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { $ as BoardEffectInputSchema, A as SkillsApi, B as DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS, C as SessionPatchReducer, D as ReferencesApi, E as SessionAccessApi, F as ChannelsApi, G as parseDesktopCommand, H as DESKTOP_COMMAND_VERSION, I as DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, J as NAVIGATION_ERROR_MESSAGE_MAX_LENGTH, K as parseDesktopCommandId, L as DESKTOP_COMMAND_MAX_TIMEOUT_MS, M as ModelsApi, N as GenerationsApi, O as SearchApi, P as CronJobsApi, Q as BoardSemanticMutationSchema, R as DESKTOP_COMMAND_PAYLOAD_MAX_BYTES, S as parseAssistantMessageCommit, T as ensureRealtimeConnected, U as isDesktopCallMethod, V as DESKTOP_COMMAND_TERMINAL_TTL_SECONDS, W as isTerminalDesktopCommandStatus, X as BoardItemPatchSchema, Y as BoardAuthoringItemSchema, Z as BoardSemanticCommandSchema, _ as PublicInviteApi, a as DesktopCommandsApi, at as BoardCompositionSchema, b as SessionGenerationStreamClient, c as ReferralsApi, ct as parseBoardCompositionInput, d as TasksApi, dt as BOARD_BUILTIN_CAPABILITIES, et as BoardEffectSchema, f as BoardClient, ft as BOARD_BUILTIN_CLIP_KINDS, g as SpacesApi, h as SpaceWebhooksApi, i as WorkCommerceApi, it as BoardCompositionInputSchema, j as PromptsApi, k as PublicAssetsApi, l as UsersApi, lt as BoardConnectionSchema, m as SpacePublicFilesApi, mt as DEFAULT_BOARD_RENDER_LIMITS, n as createHttpClient, nt as BOARD_ANIMATION_CHANNELS, o as UiCommandsApi, ot as BoardProceduralClipSchema, p as SpaceClient, pt as BOARD_BUILTIN_EFFECT_KINDS, q as resolveOpenSurface, r as AppCommerceApi, rt as BOARD_ANIMATION_CHANNEL_CAPABILITIES, s as AppsApi, st as BoardTrackSchema, t as CohubHttpClient, tt as parseBoardEffectInput, u as UserApi, ut as BOARD_ARROW_STROKE_SIZE, v as buildSpaceInvitePath, w as createSessionPatchReducer, x as createSessionGenerationStreamClient, y as buildSpacePath, z as DESKTOP_COMMAND_PENDING_TTL_SECONDS } from "./chunks/http.js";
2
- import { _ as requestSourceToHeaders, a as sanitizeAccessToken, b as REALTIME_ROOM_EVENT_NAME_PATTERN, c as REQUEST_SOURCE_VIA_MAX_LENGTH, d as isRequestSourceEmpty, f as isRequestSourceUuid, g as readRequestSourceFromEnv, h as parseRequestSourceFromHeaders, i as matchesUnauthorizedErrorToken, l as hasRequestSourceIdentity, m as normalizeRequestSource, n as HttpTransport, o as COHUB_SOURCE_HEADER, p as mergeRequestSourceIntoMeta, r as joinApiUrl, s as COHUB_SOURCE_HEADER_NAMES, t as HttpError, u as isRequestSourceClientId, v as resolveRequestSourceChannel, y as isUuid } from "./chunks/transport.js";
1
+ import { $ as resolveOpenSurface, A as ReferencesApi, B as DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, C as SessionPatchReducer, D as connectDisplay, E as DisplayConnection, F as ModelsApi, G as DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS, H as DESKTOP_COMMAND_OPEN_TIMEOUT_MS, I as GenerationsApi, J as defaultDesktopCommandTimeoutMs, K as DESKTOP_COMMAND_TERMINAL_TTL_SECONDS, L as CronJobsApi, M as PublicAssetsApi, N as SkillsApi, O as ensureRealtimeConnected, P as PromptsApi, Q as parseDesktopCommandId, R as ChannelsApi, S as parseAssistantMessageCommit, T as SpaceDisplaysApi, U as DESKTOP_COMMAND_PAYLOAD_MAX_BYTES, V as DESKTOP_COMMAND_MAX_TIMEOUT_MS, W as DESKTOP_COMMAND_PENDING_TTL_SECONDS, X as isTerminalDesktopCommandStatus, Y as isDesktopCallMethod, Z as parseDesktopCommand, _ as PublicInviteApi, a as DesktopCommandsApi, at as normalizeDisplayPoint, b as SessionGenerationStreamClient, c as ReferralsApi, d as TasksApi, et as NAVIGATION_ERROR_MESSAGE_MAX_LENGTH, f as BoardClient, g as SpacesApi, h as SpaceWebhooksApi, i as WorkCommerceApi, it as DISPLAY_SYSTEM_ACTIONS, j as SearchApi, k as SessionAccessApi, l as UsersApi, m as SpacePublicFilesApi, n as createHttpClient, o as UiCommandsApi, ot as splitDisplayText, p as SpaceClient, q as DESKTOP_COMMAND_VERSION, r as AppCommerceApi, rt as DISPLAY_INPUT_MAX_TEXT, s as AppsApi, t as CohubHttpClient, tt as DISPLAY_ELEMENT_REF_PATTERN, u as UserApi, v as buildSpaceInvitePath, w as createSessionPatchReducer, x as createSessionGenerationStreamClient, y as buildSpacePath, z as DESKTOP_COMMAND_ACCEPT_TIMEOUT_MS } from "./chunks/http.js";
2
+ import { A as isUuid, C as isRequestSourceUuid, D as readRequestSourceFromEnv, E as parseRequestSourceFromHeaders, M as isUuidOrShortUuid, O as requestSourceToHeaders, S as isRequestSourceEmpty, T as normalizeRequestSource, _ as COHUB_SOURCE_HEADER, a as sanitizeAccessToken, b as hasRequestSourceIdentity, i as matchesUnauthorizedErrorToken, j as isUuidLike, k as resolveRequestSourceChannel, n as HttpTransport, o as REALTIME_ROOM_EVENT_NAME_PATTERN, r as joinApiUrl, t as HttpError, v as COHUB_SOURCE_HEADER_NAMES, w as mergeRequestSourceIntoMeta, x as isRequestSourceClientId, y as REQUEST_SOURCE_VIA_MAX_LENGTH } from "./chunks/transport.js";
3
3
  import { a as normalizeWebsocketUrl, c as resolveExecutionAppId, d as resolveWebBaseUrl, f as resolveWebsocketUrl, g as scopeListHasPermission, h as isUserLevelPermission, i as normalizeVoiceInputWebsocketUrl, l as resolveExecutionToken, m as USER_LEVEL_PERMISSIONS, n as getCohubContext, o as resolveApiBaseUrl, p as PERMISSIONS, r as normalizeBaseUrl, s as resolveCohubEnvironment, t as COHUB_ENVIRONMENTS, u as resolveVoiceInputWebsocketUrl } from "./chunks/environment.js";
4
4
  import { a as extractBillingPayload, c as isFeatureNotEntitledError, i as FEATURE_NOT_ENTITLED_ERROR_CODE, l as isHttpErrorCode, n as createWebsocketClient, o as isBillingAccessBlockedCode, r as BILLING_ACCESS_BLOCKED_ERROR_CODE, s as isBillingAccessBlockedError, t as WebsocketClient } from "./chunks/websocket.js";
5
5
  import { VoiceApi, VoiceInputClient, createVoiceInputClient } from "./voice-input.js";
@@ -71,335 +71,1579 @@ const appAuthorizationResultSchema = z.discriminatedUnion("status", [
71
71
  code: z.string()
72
72
  })
73
73
  ]);
74
- //#endregion
75
- //#region ../protocol/dist/board.js
76
- const BOARD_DOCUMENT_KIND = "cohub.board";
77
- const BOARD_MANIFEST_KIND = "cohub.board.manifest";
78
- const idSchema = z.string().min(1).max(160);
79
- const jsonObjectSchema$1 = z.record(z.string(), z.unknown());
80
- const finiteSchema$1 = z.number().finite();
81
- z.object({
82
- kind: z.literal(BOARD_MANIFEST_KIND),
83
- version: z.literal(1),
84
- boardId: z.string().uuid(),
85
- title: z.string().min(1).max(255)
86
- });
87
- z.object({
88
- centerX: finiteSchema$1,
89
- centerY: finiteSchema$1,
90
- zoom: finiteSchema$1.positive()
74
+ const GiB = 1073741824;
75
+ /** Maximum size of a single uploaded file. */
76
+ const UPLOAD_MAX_FILE_BYTES = 1 * GiB;
77
+ /** Maximum combined size of one upload batch. */
78
+ const UPLOAD_MAX_BATCH_BYTES = 2 * GiB;
79
+ /** Maximum file count of one upload batch. */
80
+ const UPLOAD_MAX_BATCH_FILES = 2e3;
81
+ const BOARD_CAMERA_TARGET = "camera";
82
+ const BoardIdSchema = z.string().min(1).max(160).regex(/^[^\s/]+$/, "ids must not contain whitespace or '/'").refine((value) => value !== BOARD_CAMERA_TARGET, "\"camera\" is reserved");
83
+ const finite$1 = z.number().finite();
84
+ const nonNegative = finite$1.nonnegative();
85
+ const ratio = finite$1.min(0).max(1);
86
+ const BoardVec2Schema = z.object({
87
+ x: finite$1,
88
+ y: finite$1
89
+ }).strict();
90
+ const BoardSizeSchema = z.object({
91
+ width: finite$1.positive(),
92
+ height: finite$1.positive()
93
+ }).strict();
94
+ const BOARD_COLOR_TOKENS = [
95
+ "brand",
96
+ "neutral",
97
+ "black",
98
+ "white",
99
+ "blue",
100
+ "green",
101
+ "amber",
102
+ "violet",
103
+ "rose"
104
+ ];
105
+ const CSS_NAMED_COLORS = new Set("transparent aliceblue antiquewhite aqua aquamarine azure beige bisque blanchedalmond blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkgrey darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkslategrey darkturquoise darkviolet deeppink deepskyblue dimgray dimgrey dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray grey greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightgrey lightpink lightsalmon lightseagreen lightskyblue lightslategray lightslategrey lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray slategrey snow springgreen steelblue tan teal thistle tomato turquoise violet wheat whitesmoke yellow yellowgreen".split(" "));
106
+ function isBoardColorString(value) {
107
+ const color = value.trim().toLowerCase();
108
+ return BOARD_COLOR_TOKENS.includes(color) || CSS_NAMED_COLORS.has(color) || /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(color) || /^(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\([^()]*\)$/.test(color);
109
+ }
110
+ const colorString = z.string().max(120).refine(isBoardColorString, "expected a palette token or a CSS color");
111
+ const BoardColorSchema = z.union([colorString, z.object({
112
+ light: colorString,
113
+ dark: colorString
114
+ }).strict()]);
115
+ const BoardStyleSchema = z.object({
116
+ fill: BoardColorSchema.optional(),
117
+ fillOpacity: ratio.optional(),
118
+ stroke: BoardColorSchema.optional(),
119
+ strokeWidth: finite$1.min(0).max(64).optional(),
120
+ dash: z.enum([
121
+ "solid",
122
+ "dashed",
123
+ "dotted"
124
+ ]).optional(),
125
+ radius: nonNegative.optional(),
126
+ trim: ratio.optional()
127
+ }).strict();
128
+ function isSafeBoardSourcePath(value) {
129
+ return value.length > 0 && value.length <= 4096 && !value.startsWith("/") && !value.includes("\\") && value.split("/").every((part) => part.length > 0 && part !== "." && part !== "..");
130
+ }
131
+ const srcSchema = z.string().refine(isSafeBoardSourcePath, "src must be a relative Space file path");
132
+ const itemEnvelope = {
133
+ parent: BoardIdSchema.optional(),
134
+ z: finite$1.optional(),
135
+ position: BoardVec2Schema.default({
136
+ x: 0,
137
+ y: 0
138
+ }),
139
+ rotation: finite$1.default(0),
140
+ scale: z.union([finite$1, BoardVec2Schema]).default(1),
141
+ origin: BoardVec2Schema.default({
142
+ x: .5,
143
+ y: .5
144
+ }),
145
+ opacity: ratio.default(1),
146
+ style: BoardStyleSchema.default({}),
147
+ locked: z.boolean().optional(),
148
+ metadata: z.record(z.string(), z.unknown()).optional()
149
+ };
150
+ const boxed = (width, height) => ({
151
+ ...itemEnvelope,
152
+ size: BoardSizeSchema.default({
153
+ width,
154
+ height
155
+ })
91
156
  });
92
- const BoardCameraFocusSchema = z.discriminatedUnion("type", [
157
+ const unsized = { ...itemEnvelope };
158
+ const fontSize = finite$1.min(2).max(512);
159
+ const BOARD_TEXT_ALIGNS = [
160
+ "left",
161
+ "center",
162
+ "right"
163
+ ];
164
+ const BOARD_SHAPE_GEOMETRIES = [
165
+ "rectangle",
166
+ "rounded",
167
+ "ellipse",
168
+ "diamond",
169
+ "triangle",
170
+ "path"
171
+ ];
172
+ const BoardFrameItemSchema = z.object({
173
+ ...boxed(480, 320),
174
+ type: z.literal("frame"),
175
+ props: z.object({
176
+ label: z.string().max(280).default(""),
177
+ clip: z.boolean().default(true)
178
+ }).strict().prefault({})
179
+ }).strict();
180
+ const BoardTextItemSchema = z.object({
181
+ ...unsized,
182
+ type: z.literal("text"),
183
+ props: z.object({
184
+ text: z.string().default(""),
185
+ fontSize: fontSize.default(24),
186
+ fontWeight: finite$1.min(100).max(900).default(500),
187
+ font: z.string().min(1).max(120).default("sans"),
188
+ align: z.enum(BOARD_TEXT_ALIGNS).default("left"),
189
+ lineHeight: finite$1.min(.5).max(4).default(4 / 3),
190
+ width: finite$1.positive().optional(),
191
+ reveal: ratio.default(1),
192
+ caret: z.boolean().default(false)
193
+ }).strict().prefault({})
194
+ }).strict();
195
+ const BoardShapeItemSchema = z.object({
196
+ ...boxed(240, 160),
197
+ type: z.literal("shape"),
198
+ props: z.object({
199
+ geometry: z.enum(BOARD_SHAPE_GEOMETRIES).default("rectangle"),
200
+ path: z.string().min(1).max(2e5).optional(),
201
+ text: z.string().default(""),
202
+ fontSize: fontSize.default(20),
203
+ align: z.enum(BOARD_TEXT_ALIGNS).default("center")
204
+ }).strict().refine((props) => props.geometry !== "path" || Boolean(props.path), {
205
+ message: "path geometry requires props.path",
206
+ path: ["path"]
207
+ }).prefault({})
208
+ }).strict();
209
+ const BoardDrawPointSchema = z.object({
210
+ x: finite$1,
211
+ y: finite$1,
212
+ p: ratio.default(.5)
213
+ }).strict();
214
+ const BoardDrawItemSchema = z.object({
215
+ ...unsized,
216
+ type: z.literal("draw"),
217
+ props: z.object({ points: z.array(BoardDrawPointSchema).min(1).max(1e5) }).strict()
218
+ }).strict();
219
+ const BoardArrowAnchorSchema = z.union([
220
+ z.literal("auto"),
93
221
  z.object({
94
- type: z.literal("rect"),
95
- rect: z.object({
96
- x: finiteSchema$1,
97
- y: finiteSchema$1,
98
- width: finiteSchema$1.positive(),
99
- height: finiteSchema$1.positive()
100
- })
101
- }),
222
+ side: z.enum([
223
+ "top",
224
+ "right",
225
+ "bottom",
226
+ "left"
227
+ ]),
228
+ offset: ratio.default(.5)
229
+ }).strict(),
102
230
  z.object({
103
- type: z.literal("item"),
104
- itemId: idSchema
105
- }),
231
+ x: ratio,
232
+ y: ratio
233
+ }).strict()
234
+ ]);
235
+ const BoardArrowBindingSchema = z.object({
236
+ item: BoardIdSchema,
237
+ anchor: BoardArrowAnchorSchema.default("auto"),
238
+ port: z.string().min(1).max(120).optional()
239
+ }).strict();
240
+ const BoardArrowEndSchema = z.union([BoardVec2Schema, BoardArrowBindingSchema]);
241
+ const BOARD_ARROW_ROUTES = [
242
+ "straight",
243
+ "curve",
244
+ "orthogonal"
245
+ ];
246
+ const BoardArrowItemSchema = z.object({
247
+ ...unsized,
248
+ type: z.literal("arrow"),
249
+ props: z.object({
250
+ start: BoardArrowEndSchema,
251
+ end: BoardArrowEndSchema,
252
+ route: z.enum(BOARD_ARROW_ROUTES).default("straight"),
253
+ bend: finite$1.min(-.85).max(.85).default(0),
254
+ waypoints: z.array(BoardVec2Schema).max(64).default([]),
255
+ arrowStart: z.boolean().default(false),
256
+ arrowEnd: z.boolean().default(true),
257
+ label: z.string().max(280).default(""),
258
+ fontSize: fontSize.default(14),
259
+ relation: z.string().max(64).regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/, "relation must be a lowercase slug").optional()
260
+ }).strict()
261
+ }).strict();
262
+ const fileProps = {
263
+ src: srcSchema,
264
+ snapshot: z.object({
265
+ title: z.string().optional(),
266
+ mimeType: z.string().optional(),
267
+ size: nonNegative.optional(),
268
+ mtimeMs: nonNegative.optional(),
269
+ naturalWidth: finite$1.positive().optional(),
270
+ naturalHeight: finite$1.positive().optional(),
271
+ durationMs: nonNegative.optional(),
272
+ excerpt: z.string().optional(),
273
+ coverPath: z.string().optional(),
274
+ coverUrl: z.string().optional()
275
+ }).strict().optional()
276
+ };
277
+ const timedFileProps = {
278
+ ...fileProps,
279
+ time: nonNegative.optional()
280
+ };
281
+ const BoardCropSchema = z.object({
282
+ x: ratio,
283
+ y: ratio,
284
+ w: ratio,
285
+ h: ratio
286
+ }).strict();
287
+ const BoardImageItemSchema = z.object({
288
+ ...boxed(640, 360),
289
+ type: z.literal("image"),
290
+ props: z.object({
291
+ ...fileProps,
292
+ crop: BoardCropSchema.optional()
293
+ }).strict()
294
+ }).strict();
295
+ const BoardVideoItemSchema = z.object({
296
+ ...boxed(640, 360),
297
+ type: z.literal("video"),
298
+ props: z.object(timedFileProps).strict()
299
+ }).strict();
300
+ const BoardAudioItemSchema = z.object({
301
+ ...boxed(480, 96),
302
+ type: z.literal("audio"),
303
+ props: z.object(timedFileProps).strict()
304
+ }).strict();
305
+ const BoardFileItemSchema = z.object({
306
+ ...boxed(360, 220),
307
+ type: z.literal("file"),
308
+ props: z.object(fileProps).strict()
309
+ }).strict();
310
+ const httpsUrl = z.string().max(4096).url();
311
+ const artifactBase = {
312
+ id: z.string().min(1).max(240),
313
+ title: z.string().max(240).optional(),
314
+ url: httpsUrl,
315
+ mimeType: z.string().max(160).optional()
316
+ };
317
+ const BoardTaskArtifactSchema = z.discriminatedUnion("type", [
106
318
  z.object({
107
- type: z.literal("items"),
108
- itemIds: z.array(idSchema).min(1).max(1e3)
109
- }),
319
+ ...artifactBase,
320
+ type: z.literal("image"),
321
+ naturalWidth: finite$1.positive().optional(),
322
+ naturalHeight: finite$1.positive().optional()
323
+ }).strict(),
110
324
  z.object({
111
- type: z.literal("frame"),
112
- frameId: idSchema
113
- })
325
+ ...artifactBase,
326
+ type: z.literal("video"),
327
+ previewUrl: httpsUrl.optional(),
328
+ durationMs: z.number().int().positive().optional(),
329
+ naturalWidth: finite$1.positive().optional(),
330
+ naturalHeight: finite$1.positive().optional()
331
+ }).strict(),
332
+ z.object({
333
+ ...artifactBase,
334
+ type: z.literal("audio"),
335
+ previewUrl: httpsUrl.optional(),
336
+ durationMs: z.number().int().positive().optional()
337
+ }).strict(),
338
+ z.object({
339
+ id: z.string().min(1).max(240),
340
+ type: z.literal("text"),
341
+ title: z.string().max(240).optional(),
342
+ textExcerpt: z.string().min(1).max(480)
343
+ }).strict()
344
+ ]);
345
+ const BoardTaskSnapshotSchema = z.object({
346
+ taskType: z.string().min(1).max(120),
347
+ status: z.enum([
348
+ "pending",
349
+ "running",
350
+ "completed",
351
+ "failed"
352
+ ]),
353
+ title: z.string().min(1).max(240),
354
+ model: z.string().max(160).optional(),
355
+ promptExcerpt: z.string().max(480).optional(),
356
+ artifactCount: z.number().int().nonnegative(),
357
+ artifacts: z.array(BoardTaskArtifactSchema).max(6).default([]),
358
+ updatedAt: z.string().optional()
359
+ }).strict();
360
+ const BoardTaskItemSchema = z.object({
361
+ ...boxed(420, 240),
362
+ type: z.literal("task"),
363
+ props: z.object({
364
+ taskRunId: z.string().min(1).max(160),
365
+ snapshot: BoardTaskSnapshotSchema
366
+ }).strict()
367
+ }).strict();
368
+ const BOARD_EFFECT_KINDS = [
369
+ "particles",
370
+ "trail",
371
+ "impact",
372
+ "flash",
373
+ "glow"
374
+ ];
375
+ const BoardEffectItemSchema = z.object({
376
+ ...boxed(240, 240),
377
+ type: z.literal("effect"),
378
+ props: z.object({
379
+ kind: z.enum(BOARD_EFFECT_KINDS),
380
+ rate: nonNegative.max(2e4).default(40),
381
+ life: finite$1.positive().max(6e4).default(1200),
382
+ particleSize: finite$1.positive().max(512).default(6),
383
+ speed: nonNegative.max(1e4).default(120),
384
+ direction: finite$1.default(-90),
385
+ spread: finite$1.min(0).max(360).default(360),
386
+ gravity: finite$1.default(0),
387
+ intensity: nonNegative.max(10).default(1),
388
+ follow: BoardIdSchema.optional(),
389
+ seed: z.string().min(1).max(160).optional()
390
+ }).strict()
391
+ }).strict();
392
+ const BoardSketchItemSchema = z.object({
393
+ ...boxed(480, 320),
394
+ type: z.literal("sketch"),
395
+ props: z.object({
396
+ src: srcSchema.refine((path) => /\.m?js$/.test(path), "sketch src must be a .js or .mjs module"),
397
+ params: z.record(z.string(), z.unknown()).default({}),
398
+ seed: z.string().min(1).max(160).optional()
399
+ }).strict()
400
+ }).strict();
401
+ const BOARD_EXTENSION_TYPE_PATTERN = /^[a-z][a-z0-9]*(?:\.[a-z0-9-]+)+$/;
402
+ const BoardExtensionItemSchema = z.object({
403
+ ...itemEnvelope,
404
+ size: BoardSizeSchema.default({
405
+ width: 240,
406
+ height: 160
407
+ }),
408
+ type: z.string().regex(BOARD_EXTENSION_TYPE_PATTERN),
409
+ props: z.record(z.string(), z.unknown()).default({})
410
+ }).strict();
411
+ const BOARD_ITEM_SCHEMAS = {
412
+ frame: BoardFrameItemSchema,
413
+ text: BoardTextItemSchema,
414
+ shape: BoardShapeItemSchema,
415
+ draw: BoardDrawItemSchema,
416
+ arrow: BoardArrowItemSchema,
417
+ image: BoardImageItemSchema,
418
+ video: BoardVideoItemSchema,
419
+ audio: BoardAudioItemSchema,
420
+ file: BoardFileItemSchema,
421
+ task: BoardTaskItemSchema,
422
+ effect: BoardEffectItemSchema,
423
+ sketch: BoardSketchItemSchema
424
+ };
425
+ const BOARD_ITEM_TYPES = Object.keys(BOARD_ITEM_SCHEMAS);
426
+ function isBuiltinItemType(type) {
427
+ return Object.hasOwn(BOARD_ITEM_SCHEMAS, type);
428
+ }
429
+ function boardItemSchema(type) {
430
+ if (isBuiltinItemType(type)) return BOARD_ITEM_SCHEMAS[type];
431
+ return BOARD_EXTENSION_TYPE_PATTERN.test(type) ? BoardExtensionItemSchema : null;
432
+ }
433
+ function issuesToDiagnostics(error, path) {
434
+ return error.issues.map((issue) => {
435
+ return {
436
+ severity: "error",
437
+ code: "INVALID_ITEM",
438
+ path: [path, ...issue.path.map(String)].filter(Boolean).join("."),
439
+ message: issue.code === "unrecognized_keys" ? `unknown ${issue.keys.length === 1 ? "property" : "properties"} ${issue.keys.join(", ")}` : issue.message
440
+ };
441
+ });
442
+ }
443
+ function parseBoardItem(value, path = "item") {
444
+ const type = value && typeof value === "object" && !Array.isArray(value) ? value.type : void 0;
445
+ if (typeof type !== "string") return {
446
+ ok: false,
447
+ diagnostics: [{
448
+ severity: "error",
449
+ code: "INVALID_ITEM",
450
+ path: `${path}.type`,
451
+ message: "type is required"
452
+ }]
453
+ };
454
+ const schema = boardItemSchema(type);
455
+ if (!schema) return {
456
+ ok: false,
457
+ diagnostics: [{
458
+ severity: "error",
459
+ code: "INVALID_ITEM",
460
+ path: `${path}.type`,
461
+ message: `unknown type ${type}; expected ${BOARD_ITEM_TYPES.join(", ")} or a namespaced extension type`
462
+ }]
463
+ };
464
+ const parsed = schema.safeParse(value);
465
+ return parsed.success ? {
466
+ ok: true,
467
+ item: parsed.data
468
+ } : {
469
+ ok: false,
470
+ diagnostics: issuesToDiagnostics(parsed.error, path)
471
+ };
472
+ }
473
+ const CSS_EASE_KEYWORDS = /* @__PURE__ */ new Set([
474
+ "linear",
475
+ "ease",
476
+ "ease-in",
477
+ "ease-out",
478
+ "ease-in-out",
479
+ "step-start",
480
+ "step-end"
114
481
  ]);
115
- const BoardCameraFocusParamsSchema = z.object({
116
- focus: BoardCameraFocusSchema,
117
- fit: z.enum(["contain", "cover"]).default("contain"),
118
- padding: finiteSchema$1.nonnegative().default(32),
119
- minZoom: finiteSchema$1.positive().optional(),
120
- maxZoom: finiteSchema$1.positive().optional()
121
- }).superRefine((value, context) => {
122
- if (value.minZoom !== void 0 && value.maxZoom !== void 0 && value.minZoom > value.maxZoom) context.addIssue({
482
+ function isBoardEase(value) {
483
+ const ease = value.trim();
484
+ if (CSS_EASE_KEYWORDS.has(ease)) return true;
485
+ const bezier = /^cubic-bezier\(\s*([^,()]+),\s*([^,()]+),\s*([^,()]+),\s*([^,()]+)\)$/.exec(ease);
486
+ if (bezier) {
487
+ const [x1, , x2] = bezier.slice(1).map(Number);
488
+ return bezier.slice(1).every((part) => Number.isFinite(Number(part))) && x1 >= 0 && x1 <= 1 && x2 >= 0 && x2 <= 1;
489
+ }
490
+ return /^steps\(\s*[1-9]\d*\s*(?:,\s*(?:start|end|jump-start|jump-end|jump-none|jump-both)\s*)?\)$/.test(ease);
491
+ }
492
+ const BoardEaseSchema = z.string().max(120).refine(isBoardEase, "expected a CSS easing: linear, ease-in-out, cubic-bezier(…) or steps(…)");
493
+ const BoardKeyframeSchema = z.object({
494
+ at: nonNegative,
495
+ value: z.unknown(),
496
+ ease: BoardEaseSchema.optional()
497
+ }).strict();
498
+ const BoardTrackSchema = z.object({
499
+ target: z.string().min(1).max(160),
500
+ property: z.string().min(1).max(200),
501
+ keyframes: z.array(BoardKeyframeSchema).min(1).max(1e5),
502
+ composite: z.enum(["replace", "add"]).default("replace"),
503
+ interpolation: z.enum([
504
+ "auto",
505
+ "step",
506
+ "spline"
507
+ ]).default("auto"),
508
+ orient: z.boolean().optional()
509
+ }).strict().superRefine((track, context) => {
510
+ for (let index = 1; index < track.keyframes.length; index += 1) if ((track.keyframes[index]?.at ?? 0) < (track.keyframes[index - 1]?.at ?? 0)) context.addIssue({
123
511
  code: "custom",
124
- message: "minZoom must not exceed maxZoom",
125
- path: ["minZoom"]
512
+ message: "keyframes must be ordered by at",
513
+ path: [
514
+ "keyframes",
515
+ index,
516
+ "at"
517
+ ]
126
518
  });
127
519
  });
128
- z.object({
129
- nodeId: idSchema,
130
- type: z.string().min(1).max(40),
131
- parentId: idSchema.nullable(),
132
- orderKey: z.string().max(4096).nullable(),
133
- x: finiteSchema$1,
134
- y: finiteSchema$1,
135
- width: finiteSchema$1.positive(),
136
- height: finiteSchema$1.positive(),
137
- rotation: finiteSchema$1,
138
- refKind: z.string().max(40).nullable(),
139
- refPath: z.string().max(4096).nullable(),
140
- refUrl: z.string().max(4096).nullable(),
141
- view: jsonObjectSchema$1,
142
- style: jsonObjectSchema$1,
143
- data: jsonObjectSchema$1
144
- });
145
- const stripBoardServerFields = (value) => {
146
- if (!value || typeof value !== "object" || Array.isArray(value)) return value;
147
- const { boardId: _boardId, revision: _revision, ...input } = value;
148
- return input;
149
- };
150
- const BoardCreateEffectInputSchema = z.preprocess(stripBoardServerFields, BoardEffectInputSchema);
151
- const BoardCreateCompositionInputSchema = z.preprocess(stripBoardServerFields, BoardCompositionInputSchema);
152
- const BoardCreateInputSchema = z.object({
153
- path: z.string().min(1),
154
- /** Reused by clients when board creation is interrupted and retried. */
155
- mutationId: z.string().max(128).optional(),
520
+ const BoardMarkerSchema = z.object({
521
+ at: nonNegative,
522
+ label: z.string().max(280).optional(),
523
+ pause: z.boolean().optional()
524
+ }).strict();
525
+ const BoardAnimationHeaderSchema = z.object({
526
+ name: z.string().max(280).optional(),
527
+ duration: finite$1.positive().max(864e5),
528
+ play: z.enum([
529
+ "manual",
530
+ "auto",
531
+ "always"
532
+ ]).default("manual"),
533
+ delay: nonNegative.default(0),
534
+ loop: z.boolean().default(false),
535
+ end: z.enum(["hold", "reset"]).default("hold"),
536
+ markers: z.array(BoardMarkerSchema).max(1e4).default([])
537
+ }).strict();
538
+ const BoardAnimationSchema = BoardAnimationHeaderSchema.extend({ tracks: z.record(BoardIdSchema, BoardTrackSchema).default({}) }).strict();
539
+ const BOARD_ENTER_PRESETS = [
540
+ "fade-in",
541
+ "rise",
542
+ "drop",
543
+ "pop",
544
+ "deal"
545
+ ];
546
+ const BoardBackgroundSchema = z.object({
547
+ kind: z.enum([
548
+ "solid",
549
+ "dots",
550
+ "grid",
551
+ "image"
552
+ ]).default("dots"),
553
+ color: BoardColorSchema.optional(),
554
+ imageUrl: z.string().url().max(4096).optional(),
555
+ fit: z.enum([
556
+ "cover",
557
+ "contain",
558
+ "repeat"
559
+ ]).optional(),
560
+ opacity: ratio.optional()
561
+ }).strict();
562
+ const BoardSettingsSchema = z.object({
156
563
  title: z.string().min(1).max(255).optional(),
157
- metadata: jsonObjectSchema$1.optional(),
158
- items: z.array(BoardAuthoringItemSchema).max(5e4).optional(),
159
- connections: z.array(BoardConnectionSchema).max(5e4).optional(),
160
- effects: z.array(BoardCreateEffectInputSchema).max(5e4).optional(),
161
- compositions: z.array(BoardCreateCompositionInputSchema).max(5e4).optional()
162
- });
163
- const jsonSchema$1 = (schema) => z.toJSONSchema(schema);
164
- const authoringSchemaFactories = {
165
- item: () => jsonSchema$1(BoardAuthoringItemSchema),
166
- itemPatch: () => jsonSchema$1(BoardItemPatchSchema),
167
- mutation: () => jsonSchema$1(BoardSemanticMutationSchema),
168
- composition: () => jsonSchema$1(BoardCompositionInputSchema),
169
- effect: () => jsonSchema$1(BoardEffectInputSchema),
170
- create: () => jsonSchema$1(BoardCreateInputSchema)
564
+ background: BoardBackgroundSchema.default({ kind: "dots" }),
565
+ grid: z.object({
566
+ visible: z.boolean().default(false),
567
+ size: finite$1.min(4).max(512).default(24)
568
+ }).strict().optional(),
569
+ enter: z.object({
570
+ preset: z.enum(BOARD_ENTER_PRESETS),
571
+ duration: finite$1.positive().max(1e4).optional()
572
+ }).strict().optional()
573
+ }).strict();
574
+ const patchObject = z.record(z.string(), z.unknown());
575
+ const BoardPatchSchema = z.object({
576
+ board: patchObject.nullable().optional(),
577
+ items: z.record(BoardIdSchema, patchObject.nullable()).optional(),
578
+ animations: z.record(BoardIdSchema, patchObject.and(z.object({ tracks: z.record(BoardIdSchema, patchObject.nullable()).optional() })).nullable()).optional()
579
+ }).strict();
580
+ //#endregion
581
+ //#region ../protocol/dist/board-json.js
582
+ /** Structural equality for JSON-shaped semantic values. */
583
+ function boardJsonEquals(a, b) {
584
+ if (a === b) return true;
585
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return Number.isNaN(a) && Number.isNaN(b);
586
+ const aArray = Array.isArray(a);
587
+ const bArray = Array.isArray(b);
588
+ if (aArray !== bArray) return false;
589
+ if (aArray && bArray) return a.length === b.length && a.every((value, index) => boardJsonEquals(value, b[index]));
590
+ const aRecord = a;
591
+ const bRecord = b;
592
+ const keys = Object.keys(aRecord);
593
+ return keys.length === Object.keys(bRecord).length && keys.every((key) => Object.hasOwn(bRecord, key) && boardJsonEquals(aRecord[key], bRecord[key]));
594
+ }
595
+ //#endregion
596
+ //#region ../protocol/dist/board-layout.js
597
+ const IDENTITY_MATRIX = [
598
+ 1,
599
+ 0,
600
+ 0,
601
+ 1,
602
+ 0,
603
+ 0
604
+ ];
605
+ function multiplyMatrix(parent, child) {
606
+ const [a1, b1, c1, d1, tx1, ty1] = parent;
607
+ const [a2, b2, c2, d2, tx2, ty2] = child;
608
+ return [
609
+ a1 * a2 + c1 * b2,
610
+ b1 * a2 + d1 * b2,
611
+ a1 * c2 + c1 * d2,
612
+ b1 * c2 + d1 * d2,
613
+ a1 * tx2 + c1 * ty2 + tx1,
614
+ b1 * tx2 + d1 * ty2 + ty1
615
+ ];
616
+ }
617
+ function invertMatrix(matrix) {
618
+ const [a, b, c, d, tx, ty] = matrix;
619
+ const det = a * d - b * c || 1e-12;
620
+ return [
621
+ d / det,
622
+ -b / det,
623
+ -c / det,
624
+ a / det,
625
+ (c * ty - d * tx) / det,
626
+ (b * tx - a * ty) / det
627
+ ];
628
+ }
629
+ function applyMatrix(matrix, point) {
630
+ const [a, b, c, d, tx, ty] = matrix;
631
+ return {
632
+ x: a * point.x + c * point.y + tx,
633
+ y: b * point.x + d * point.y + ty
634
+ };
635
+ }
636
+ function scaleVector(scale) {
637
+ return typeof scale === "number" ? {
638
+ x: scale,
639
+ y: scale
640
+ } : scale;
641
+ }
642
+ function isWideCharacter(code) {
643
+ return code >= 4352 && code <= 4447 || code >= 11904 && code <= 42191 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65072 && code <= 65103 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 127744 && code <= 129791 || code >= 131072 && code <= 262141;
644
+ }
645
+ function estimateTextWidth(text, fontSize) {
646
+ let ems = 0;
647
+ for (const character of text) {
648
+ const code = character.codePointAt(0) ?? 0;
649
+ if (character === " ") ems += .28;
650
+ else if (isWideCharacter(code)) ems += 1;
651
+ else if (/[il.,:;'|!]/.test(character)) ems += .3;
652
+ else if (/[mwMW@]/.test(character)) ems += .85;
653
+ else if (/[A-Z0-9]/.test(character)) ems += .64;
654
+ else ems += .55;
655
+ }
656
+ return ems * fontSize;
657
+ }
658
+ function measureLine(text, fontSize, fontWeight, font) {
659
+ return estimateTextWidth(text, fontSize);
660
+ }
661
+ function wrapParagraph(paragraph, width, measure) {
662
+ if (!paragraph) return [""];
663
+ const tokens = paragraph.match(/[\u1100-\u115f\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\uff00-\uff60]|[^\s\u1100-\u115f\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\uff00-\uff60]+|\s+/g) ?? [paragraph];
664
+ const lines = [];
665
+ let line = "";
666
+ for (const token of tokens) {
667
+ const candidate = line + token;
668
+ if (!line || measure(candidate.trimEnd()) <= width) {
669
+ line = candidate;
670
+ continue;
671
+ }
672
+ lines.push(line.trimEnd());
673
+ line = /^\s+$/.test(token) ? "" : token;
674
+ }
675
+ lines.push(line.trimEnd());
676
+ return lines;
677
+ }
678
+ const textSizeCache = /* @__PURE__ */ new Map();
679
+ const TEXT_CACHE_LIMIT = 4096;
680
+ function clampBoardFontSize(fontSize) {
681
+ return Math.min(512, Math.max(2, fontSize));
682
+ }
683
+ function layoutBoardText(props) {
684
+ const fontSize = clampBoardFontSize(props.fontSize ?? 24);
685
+ const key = `${fontSize}|${props.fontWeight}|${props.font}|${props.lineHeight}|${props.width ?? ""}|${props.text}`;
686
+ const cached = textSizeCache.get(key);
687
+ if (cached) return cached;
688
+ const measure = (text) => measureLine(text, fontSize, props.fontWeight ?? 500, props.font ?? "sans");
689
+ const paragraphs = (props.text || "").split("\n");
690
+ const lines = props.width ? paragraphs.flatMap((paragraph) => wrapParagraph(paragraph, props.width, measure)) : paragraphs;
691
+ const lineHeight = fontSize * (props.lineHeight ?? 4 / 3);
692
+ const widest = lines.reduce((max, line) => Math.max(max, measure(line)), 0);
693
+ const layout = {
694
+ width: Math.max(props.width ?? 0, widest, fontSize * .5),
695
+ height: Math.max(1, lines.length) * lineHeight,
696
+ lines,
697
+ lineHeight
698
+ };
699
+ if (textSizeCache.size >= TEXT_CACHE_LIMIT) textSizeCache.clear();
700
+ textSizeCache.set(key, layout);
701
+ return layout;
702
+ }
703
+ function drawSampleRadius(strokeWidth, pressure) {
704
+ const clamped = Math.min(1, Math.max(0, pressure));
705
+ return Math.max(.5, strokeWidth / 2 * (.5 + clamped));
706
+ }
707
+ function drawPointsBounds(points, strokeWidth) {
708
+ if (points.length === 0) return {
709
+ x: 0,
710
+ y: 0,
711
+ width: 1,
712
+ height: 1
713
+ };
714
+ let minX = Number.POSITIVE_INFINITY;
715
+ let minY = Number.POSITIVE_INFINITY;
716
+ let maxX = Number.NEGATIVE_INFINITY;
717
+ let maxY = Number.NEGATIVE_INFINITY;
718
+ for (const point of points) {
719
+ const radius = drawSampleRadius(strokeWidth, point.p);
720
+ minX = Math.min(minX, point.x - radius);
721
+ minY = Math.min(minY, point.y - radius);
722
+ maxX = Math.max(maxX, point.x + radius);
723
+ maxY = Math.max(maxY, point.y + radius);
724
+ }
725
+ return {
726
+ x: minX,
727
+ y: minY,
728
+ width: Math.max(1, maxX - minX),
729
+ height: Math.max(1, maxY - minY)
730
+ };
731
+ }
732
+ function pointsRect(points) {
733
+ let minX = Number.POSITIVE_INFINITY;
734
+ let minY = Number.POSITIVE_INFINITY;
735
+ let maxX = Number.NEGATIVE_INFINITY;
736
+ let maxY = Number.NEGATIVE_INFINITY;
737
+ for (const point of points) {
738
+ minX = Math.min(minX, point.x);
739
+ minY = Math.min(minY, point.y);
740
+ maxX = Math.max(maxX, point.x);
741
+ maxY = Math.max(maxY, point.y);
742
+ }
743
+ if (!Number.isFinite(minX)) return {
744
+ x: 0,
745
+ y: 0,
746
+ width: 1,
747
+ height: 1
748
+ };
749
+ return {
750
+ x: minX,
751
+ y: minY,
752
+ width: Math.max(1, maxX - minX),
753
+ height: Math.max(1, maxY - minY)
754
+ };
755
+ }
756
+ function isArrowBinding(end) {
757
+ return typeof end.item === "string";
758
+ }
759
+ function arrowBindings(item) {
760
+ if (item.type !== "arrow") return [];
761
+ const props = item.props;
762
+ const ids = [props.start, props.end].filter(isArrowBinding).map((end) => end.item);
763
+ return [...new Set(ids)];
764
+ }
765
+ function bendControl(start, end, bend) {
766
+ const mid = {
767
+ x: (start.x + end.x) / 2,
768
+ y: (start.y + end.y) / 2
769
+ };
770
+ if (!bend) return mid;
771
+ const dx = end.x - start.x;
772
+ const dy = end.y - start.y;
773
+ const length = Math.hypot(dx, dy) || 1;
774
+ const offset = bend * length;
775
+ return {
776
+ x: mid.x + -dy / length * offset,
777
+ y: mid.y + dx / length * offset
778
+ };
779
+ }
780
+ const MAX_DEPTH = 64;
781
+ function createBoardLayout(initialLookup, dependents) {
782
+ let lookup = initialLookup;
783
+ const boxes = /* @__PURE__ */ new Map();
784
+ const matrices = /* @__PURE__ */ new Map();
785
+ const bounds = /* @__PURE__ */ new Map();
786
+ function matrixOf(id, depth = 0) {
787
+ const cached = matrices.get(id);
788
+ if (cached) return cached;
789
+ const item = lookup(id);
790
+ if (!item) return IDENTITY_MATRIX;
791
+ const matrix = multiplyMatrix(item.parent && depth < MAX_DEPTH && lookup(item.parent) ? matrixOf(item.parent, depth + 1) : IDENTITY_MATRIX, localMatrixOf(id));
792
+ matrices.set(id, matrix);
793
+ return matrix;
794
+ }
795
+ function localMatrixOf(id) {
796
+ const item = lookup(id);
797
+ if (!item) return IDENTITY_MATRIX;
798
+ if (item.type === "arrow") return translate(item.position);
799
+ const box = boxOf(id);
800
+ const scale = scaleVector(item.scale);
801
+ const radians = item.rotation * Math.PI / 180;
802
+ const cos = Math.cos(radians);
803
+ const sin = Math.sin(radians);
804
+ const ox = box.x + item.origin.x * box.width;
805
+ const oy = box.y + item.origin.y * box.height;
806
+ const a = cos * scale.x;
807
+ const b = sin * scale.x;
808
+ const c = -sin * scale.y;
809
+ const d = cos * scale.y;
810
+ return [
811
+ a,
812
+ b,
813
+ c,
814
+ d,
815
+ item.position.x + ox - (a * ox + c * oy),
816
+ item.position.y + oy - (b * ox + d * oy)
817
+ ];
818
+ }
819
+ function boxOf(id, depth = 0) {
820
+ const cached = boxes.get(id);
821
+ if (cached) return cached;
822
+ const item = lookup(id);
823
+ let box;
824
+ if (!item) box = {
825
+ x: 0,
826
+ y: 0,
827
+ width: 1,
828
+ height: 1
829
+ };
830
+ else if ("size" in item && item.size) box = {
831
+ x: 0,
832
+ y: 0,
833
+ width: item.size.width,
834
+ height: item.size.height
835
+ };
836
+ else if (item.type === "text") {
837
+ const layout = layoutBoardText(item.props);
838
+ box = {
839
+ x: 0,
840
+ y: 0,
841
+ width: layout.width,
842
+ height: layout.height
843
+ };
844
+ } else if (item.type === "draw") box = drawPointsBounds(item.props.points, item.style.strokeWidth ?? 4);
845
+ else if (item.type === "arrow") {
846
+ const props = item.props;
847
+ const toLocal = invertMatrix(multiplyMatrix(parentOf(item, depth), translate(item.position)));
848
+ const start = isArrowBinding(props.start) ? applyMatrix(toLocal, resolveBoundEnd(item, props.start, props.end, depth)) : props.start;
849
+ const end = isArrowBinding(props.end) ? applyMatrix(toLocal, resolveBoundEnd(item, props.end, props.start, depth)) : props.end;
850
+ const points = [
851
+ start,
852
+ end,
853
+ ...props.waypoints
854
+ ];
855
+ if (props.bend) points.push(bendControl(start, end, props.bend));
856
+ const rect = pointsRect(points);
857
+ const pad = Math.max(8, (item.style.strokeWidth ?? 2.5) * 2);
858
+ box = {
859
+ x: rect.x - pad,
860
+ y: rect.y - pad,
861
+ width: rect.width + pad * 2,
862
+ height: rect.height + pad * 2
863
+ };
864
+ } else box = {
865
+ x: 0,
866
+ y: 0,
867
+ width: 240,
868
+ height: 160
869
+ };
870
+ boxes.set(id, box);
871
+ return box;
872
+ }
873
+ function parentOf(item, depth) {
874
+ return item.parent && depth < MAX_DEPTH && lookup(item.parent) ? matrixOf(item.parent, depth + 1) : IDENTITY_MATRIX;
875
+ }
876
+ function resolveBoundEnd(arrow, end, other, depth) {
877
+ const target = lookup(end.item);
878
+ const arrowSpace = multiplyMatrix(parentOf(arrow, depth), translate(arrow.position));
879
+ if (!target || target.type === "arrow" || depth >= MAX_DEPTH) return applyMatrix(arrowSpace, {
880
+ x: 0,
881
+ y: 0
882
+ });
883
+ const frame = frameOf(end.item, depth + 1);
884
+ const toward = isArrowBinding(other) ? lookup(other.item) && lookup(other.item)?.type !== "arrow" ? center(frameOf(other.item, depth + 1)) : center(frame) : applyMatrix(arrowSpace, other);
885
+ return anchorPoint(frame, end.anchor, toward);
886
+ }
887
+ function frameOf(id, depth = 0) {
888
+ boxOf(id, depth);
889
+ const box = boxes.get(id);
890
+ const matrix = matrixOf(id, depth);
891
+ const [a, b, c, d] = matrix;
892
+ const centerPoint = applyMatrix(matrix, {
893
+ x: box.x + box.width / 2,
894
+ y: box.y + box.height / 2
895
+ });
896
+ const width = box.width * Math.hypot(a, b);
897
+ const height = box.height * Math.hypot(c, d);
898
+ return {
899
+ x: centerPoint.x - width / 2,
900
+ y: centerPoint.y - height / 2,
901
+ width,
902
+ height,
903
+ rotation: Math.atan2(b, a) * 180 / Math.PI
904
+ };
905
+ }
906
+ function boundsOf(id) {
907
+ const cached = bounds.get(id);
908
+ if (cached) return cached;
909
+ const box = boxOf(id);
910
+ const matrix = matrixOf(id);
911
+ const rect = pointsRect([
912
+ applyMatrix(matrix, {
913
+ x: box.x,
914
+ y: box.y
915
+ }),
916
+ applyMatrix(matrix, {
917
+ x: box.x + box.width,
918
+ y: box.y
919
+ }),
920
+ applyMatrix(matrix, {
921
+ x: box.x + box.width,
922
+ y: box.y + box.height
923
+ }),
924
+ applyMatrix(matrix, {
925
+ x: box.x,
926
+ y: box.y + box.height
927
+ })
928
+ ]);
929
+ bounds.set(id, rect);
930
+ return rect;
931
+ }
932
+ function setLookup(nextLookup) {
933
+ lookup = nextLookup;
934
+ }
935
+ function invalidate(ids) {
936
+ if (!ids) {
937
+ boxes.clear();
938
+ matrices.clear();
939
+ bounds.clear();
940
+ return;
941
+ }
942
+ const queue = [...ids];
943
+ const seen = /* @__PURE__ */ new Set();
944
+ while (queue.length) {
945
+ const id = queue.pop();
946
+ if (seen.has(id)) continue;
947
+ seen.add(id);
948
+ boxes.delete(id);
949
+ matrices.delete(id);
950
+ bounds.delete(id);
951
+ if (dependents) for (const next of dependents(id)) queue.push(next);
952
+ }
953
+ }
954
+ return {
955
+ has: (id) => lookup(id) !== void 0,
956
+ box: (id) => boxOf(id),
957
+ matrix: (id) => matrixOf(id),
958
+ localMatrix: (id) => localMatrixOf(id),
959
+ parentMatrix: (id) => {
960
+ const item = lookup(id);
961
+ return item ? parentOf(item, 0) : IDENTITY_MATRIX;
962
+ },
963
+ bounds: boundsOf,
964
+ frame: (id) => frameOf(id),
965
+ arrowEnd: (id, which) => {
966
+ const item = lookup(id);
967
+ if (item?.type !== "arrow") return {
968
+ x: 0,
969
+ y: 0
970
+ };
971
+ const arrow = item;
972
+ const [end, other] = which === "start" ? [arrow.props.start, arrow.props.end] : [arrow.props.end, arrow.props.start];
973
+ return isArrowBinding(end) ? resolveBoundEnd(arrow, end, other, 0) : applyMatrix(matrixOf(id), end);
974
+ },
975
+ invalidate,
976
+ setLookup
977
+ };
978
+ }
979
+ function translate(point) {
980
+ return [
981
+ 1,
982
+ 0,
983
+ 0,
984
+ 1,
985
+ point.x,
986
+ point.y
987
+ ];
988
+ }
989
+ function center(frame) {
990
+ return {
991
+ x: frame.x + frame.width / 2,
992
+ y: frame.y + frame.height / 2
993
+ };
994
+ }
995
+ function rotateAround(point, pivot, degrees) {
996
+ if (!degrees) return point;
997
+ const radians = degrees * Math.PI / 180;
998
+ const cos = Math.cos(radians);
999
+ const sin = Math.sin(radians);
1000
+ const dx = point.x - pivot.x;
1001
+ const dy = point.y - pivot.y;
1002
+ return {
1003
+ x: pivot.x + dx * cos - dy * sin,
1004
+ y: pivot.y + dx * sin + dy * cos
1005
+ };
1006
+ }
1007
+ function framePoint(frame, nx, ny) {
1008
+ return rotateAround({
1009
+ x: frame.x + nx * frame.width,
1010
+ y: frame.y + ny * frame.height
1011
+ }, center(frame), frame.rotation);
1012
+ }
1013
+ function autoAnchorSide(frame, toward) {
1014
+ const local = rotateAround(toward, center(frame), -frame.rotation);
1015
+ const c = center(frame);
1016
+ const dx = (local.x - c.x) / Math.max(frame.width / 2, 1e-4);
1017
+ const dy = (local.y - c.y) / Math.max(frame.height / 2, 1e-4);
1018
+ if (Math.abs(dx) >= Math.abs(dy)) return dx >= 0 ? "right" : "left";
1019
+ return dy >= 0 ? "bottom" : "top";
1020
+ }
1021
+ function sideAnchor(side, offset) {
1022
+ switch (side) {
1023
+ case "top": return {
1024
+ x: offset,
1025
+ y: 0
1026
+ };
1027
+ case "bottom": return {
1028
+ x: offset,
1029
+ y: 1
1030
+ };
1031
+ case "left": return {
1032
+ x: 0,
1033
+ y: offset
1034
+ };
1035
+ default: return {
1036
+ x: 1,
1037
+ y: offset
1038
+ };
1039
+ }
1040
+ }
1041
+ function anchorPoint(frame, anchor, toward) {
1042
+ if (anchor === "auto") {
1043
+ const normalized = sideAnchor(autoAnchorSide(frame, toward), .5);
1044
+ return framePoint(frame, normalized.x, normalized.y);
1045
+ }
1046
+ if ("side" in anchor) {
1047
+ const normalized = sideAnchor(anchor.side, anchor.offset);
1048
+ return framePoint(frame, normalized.x, normalized.y);
1049
+ }
1050
+ return framePoint(frame, anchor.x, anchor.y);
1051
+ }
1052
+ //#endregion
1053
+ //#region ../protocol/dist/board-patch.js
1054
+ function slot(map, key) {
1055
+ let value = map[key];
1056
+ if (!value) {
1057
+ value = {};
1058
+ map[key] = value;
1059
+ }
1060
+ return value;
1061
+ }
1062
+ function emptyDelta() {
1063
+ return {
1064
+ items: {},
1065
+ animations: {},
1066
+ tracks: {}
1067
+ };
1068
+ }
1069
+ function withoutEmpty(value) {
1070
+ for (const [key, entry] of Object.entries(value)) if (entry === void 0 || isRecord$6(entry) && Object.keys(entry).length === 0) delete value[key];
1071
+ return value;
1072
+ }
1073
+ function isRecord$6(value) {
1074
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1075
+ }
1076
+ function mergePatch(target, patch) {
1077
+ if (!isRecord$6(patch)) return patch;
1078
+ const result = isRecord$6(target) ? { ...target } : {};
1079
+ for (const [key, value] of Object.entries(patch)) if (value === null) delete result[key];
1080
+ else result[key] = mergePatch(result[key], value);
1081
+ return result;
1082
+ }
1083
+ function def(schema) {
1084
+ return schema._zod.def;
1085
+ }
1086
+ function unwrap(schema) {
1087
+ let current = schema;
1088
+ for (let guard = 0; guard < 16; guard += 1) {
1089
+ const d = def(current);
1090
+ if (d.type === "optional" || d.type === "default" || d.type === "nullable" || d.type === "readonly" || d.type === "prefault") current = d.innerType;
1091
+ else if (d.type === "pipe") current = d.in;
1092
+ else return current;
1093
+ }
1094
+ return current;
1095
+ }
1096
+ function unwrapObjectShape(schema) {
1097
+ const d = def(unwrap(schema));
1098
+ return d.type === "object" ? d.shape : null;
1099
+ }
1100
+ function schemaAtPath(schema, path) {
1101
+ let current = schema;
1102
+ for (const segment of path) {
1103
+ const d = def(unwrap(current));
1104
+ if (d.type === "object") {
1105
+ const shape = d.shape;
1106
+ if (!Object.hasOwn(shape, segment)) return null;
1107
+ current = shape[segment];
1108
+ } else if (d.type === "record") current = d.valueType;
1109
+ else if (d.type === "union") {
1110
+ const branch = d.options.find((option) => unwrapObjectShape(option)?.[segment]);
1111
+ if (!branch) return null;
1112
+ current = unwrapObjectShape(branch)[segment];
1113
+ } else return null;
1114
+ }
1115
+ return current;
1116
+ }
1117
+ const NOT_ANIMATABLE = /* @__PURE__ */ new Set([
1118
+ "type",
1119
+ "parent",
1120
+ "z",
1121
+ "locked",
1122
+ "metadata"
1123
+ ]);
1124
+ const CAMERA_PROPERTIES = {
1125
+ focus: {
1126
+ kind: "discrete",
1127
+ schema: z.union([z.string().min(1), z.object({
1128
+ x: z.number().finite(),
1129
+ y: z.number().finite(),
1130
+ width: z.number().finite().positive(),
1131
+ height: z.number().finite().positive()
1132
+ }).strict()])
1133
+ },
1134
+ zoom: {
1135
+ kind: "number",
1136
+ schema: z.number().finite().positive(),
1137
+ multiplicative: true
1138
+ },
1139
+ center: {
1140
+ kind: "vec2",
1141
+ schema: BoardVec2Schema
1142
+ },
1143
+ shake: {
1144
+ kind: "number",
1145
+ schema: z.number().finite().nonnegative()
1146
+ }
171
1147
  };
172
- /**
173
- * JSON Schemas for the semantic authoring surface. Each property is enumerable
174
- * for Object.keys/JSON.stringify, but generated only when that property is
175
- * requested. This keeps protocol imports cheap while capabilities still returns
176
- * the complete machine-readable schema object.
177
- */
178
- const BOARD_AUTHORING_SCHEMAS = {};
179
- for (const key of Object.keys(authoringSchemaFactories)) {
180
- let cached;
181
- Object.defineProperty(BOARD_AUTHORING_SCHEMAS, key, {
182
- enumerable: true,
183
- configurable: false,
184
- get() {
185
- cached ??= authoringSchemaFactories[key]();
186
- return cached;
1148
+ const ANIMATION_PROPERTIES = { time: {
1149
+ kind: "number",
1150
+ schema: z.number().finite().nonnegative()
1151
+ } };
1152
+ function kindOf(schema, path) {
1153
+ const leaf = path[path.length - 1];
1154
+ if (path.length === 1 && leaf === "scale") return "scale";
1155
+ if (schema === BoardColorSchema || unwrap(schema) === BoardColorSchema) return "color";
1156
+ if (def(unwrap(schema)).type === "number") return "number";
1157
+ const shape = unwrapObjectShape(schema);
1158
+ if (shape && Object.keys(shape).length === 2 && "x" in shape && "y" in shape) return "vec2";
1159
+ return "discrete";
1160
+ }
1161
+ function resolveBoardProperty(targetType, property) {
1162
+ if (targetType === "camera") {
1163
+ const spec = CAMERA_PROPERTIES[property];
1164
+ return spec ? {
1165
+ ok: true,
1166
+ spec
1167
+ } : {
1168
+ ok: false,
1169
+ message: `camera has no property ${property}; expected ${Object.keys(CAMERA_PROPERTIES).join(", ")}`
1170
+ };
1171
+ }
1172
+ if (targetType === "animation") {
1173
+ const spec = ANIMATION_PROPERTIES[property];
1174
+ return spec ? {
1175
+ ok: true,
1176
+ spec
1177
+ } : {
1178
+ ok: false,
1179
+ message: `animations are driven through time`
1180
+ };
1181
+ }
1182
+ const path = property.split(".");
1183
+ if (path.some((segment) => !segment) || NOT_ANIMATABLE.has(path[0])) return {
1184
+ ok: false,
1185
+ message: `${property} cannot be animated`
1186
+ };
1187
+ const schema = boardItemSchema(targetType);
1188
+ if (!schema) return {
1189
+ ok: false,
1190
+ message: `unknown item type ${targetType}`
1191
+ };
1192
+ if (!isBuiltinItemType(targetType) && path[0] === "props") return {
1193
+ ok: true,
1194
+ spec: {
1195
+ kind: "discrete",
1196
+ schema: z.unknown()
187
1197
  }
188
- });
1198
+ };
1199
+ const leaf = schemaAtPath(schema, path);
1200
+ if (!leaf) return {
1201
+ ok: false,
1202
+ message: `${targetType} has no property ${property}`
1203
+ };
1204
+ const kind = kindOf(leaf, path);
1205
+ return {
1206
+ ok: true,
1207
+ spec: {
1208
+ kind,
1209
+ schema: unwrap(leaf),
1210
+ ...property === "opacity" || kind === "scale" ? { multiplicative: true } : {}
1211
+ }
1212
+ };
1213
+ }
1214
+ function lastKeyframeAt(trackPatches) {
1215
+ let end = 0;
1216
+ for (const track of trackPatches) {
1217
+ const keyframes = track?.keyframes;
1218
+ if (!Array.isArray(keyframes)) continue;
1219
+ for (const keyframe of keyframes) {
1220
+ const at = keyframe?.at;
1221
+ if (typeof at === "number" && Number.isFinite(at)) end = Math.max(end, at);
1222
+ }
1223
+ }
1224
+ return end > 0 ? end : void 0;
1225
+ }
1226
+ function trackReferences(track) {
1227
+ const refs = /* @__PURE__ */ new Set([track.target]);
1228
+ if (track.target === "camera" && track.property === "focus") {
1229
+ for (const keyframe of track.keyframes) if (typeof keyframe.value === "string") refs.add(keyframe.value);
1230
+ }
1231
+ refs.delete(BOARD_CAMERA_TARGET);
1232
+ return [...refs];
1233
+ }
1234
+ function pruneBoardTrack(track, deleted) {
1235
+ if (deleted.has(track.target)) return null;
1236
+ if (track.target !== "camera" || track.property !== "focus") return track;
1237
+ const keyframes = track.keyframes.filter((entry) => typeof entry.value !== "string" || !deleted.has(entry.value));
1238
+ if (keyframes.length === track.keyframes.length) return track;
1239
+ return keyframes.length ? {
1240
+ ...track,
1241
+ keyframes
1242
+ } : null;
1243
+ }
1244
+ function error(path, code, message) {
1245
+ return {
1246
+ severity: "error",
1247
+ code,
1248
+ path,
1249
+ message
1250
+ };
189
1251
  }
190
- z.object({
191
- include: z.array(z.enum([
192
- "nodes",
193
- "connections",
194
- "effects",
195
- "compositions",
196
- "playback"
197
- ])).optional(),
198
- nodeIds: z.array(idSchema).max(5e4).optional(),
199
- connectionIds: z.array(idSchema).max(5e4).optional(),
200
- effectIds: z.array(idSchema).max(5e4).optional(),
201
- compositionIds: z.array(idSchema).max(5e4).optional(),
202
- viewport: z.object({
203
- x: finiteSchema$1,
204
- y: finiteSchema$1,
205
- width: finiteSchema$1.positive(),
206
- height: finiteSchema$1.positive()
207
- }).optional()
208
- });
209
- z.object({
210
- /** Return transactions with `version < before`. Omit for the newest page. */
211
- before: z.number().int().positive().optional(),
212
- limit: z.number().int().min(1).max(500).default(200),
213
- /** Include the current rows on the newest page (default true). Tail refreshes turn this off. */
214
- snapshot: z.boolean().default(true)
215
- });
216
- /** Persisted on `boards.metadata.playback`: how a Board plays when opened. */
217
- const BoardPlaybackPolicySchema = z.object({
218
- compositionId: idSchema,
219
- /** Delay before the first local playback after opening the Board, in milliseconds. */
220
- delayMs: finiteSchema$1.nonnegative().default(0)
221
- });
222
- function parseBoardPlaybackPolicy(metadata) {
223
- const parsed = BoardPlaybackPolicySchema.safeParse(metadata.playback);
224
- return parsed.success ? parsed.data : null;
1252
+ function stripId(value) {
1253
+ if (!("id" in value)) return value;
1254
+ const { id: _id, ...rest } = value;
1255
+ return rest;
225
1256
  }
226
- z.discriminatedUnion("type", [
227
- z.object({
228
- commandId: idSchema,
229
- type: z.literal("play"),
230
- compositionId: idSchema,
231
- position: finiteSchema$1.nonnegative().optional(),
232
- timeScale: finiteSchema$1.positive().max(4).optional(),
233
- shared: z.boolean().optional(),
234
- seed: idSchema.optional()
235
- }),
236
- z.object({
237
- commandId: idSchema,
238
- type: z.literal("pause"),
239
- playbackId: z.string().uuid()
240
- }),
241
- z.object({
242
- commandId: idSchema,
243
- type: z.literal("seek"),
244
- playbackId: z.string().uuid(),
245
- position: finiteSchema$1.nonnegative()
246
- }),
247
- z.object({
248
- commandId: idSchema,
249
- type: z.literal("stop"),
250
- playbackId: z.string().uuid()
251
- })
252
- ]);
253
- //#endregion
254
- //#region ../protocol/dist/board-animation.js
255
- const extensionKindSchema = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/).max(160);
256
- const jsonObjectSchema = z.record(z.string(), z.unknown());
257
- const finiteSchema = z.number().finite();
258
- /** Parameters shared by the built-in deal entrance preset. */
259
- const BoardDealParamsSchema = z.object({
260
- lift: finiteSchema.nonnegative().max(2e3).optional(),
261
- swing: finiteSchema.nonnegative().max(2e3).optional(),
262
- curve: finiteSchema.nonnegative().max(2e3).optional(),
263
- tilt: finiteSchema.nonnegative().max(180).optional(),
264
- scale: finiteSchema.positive().max(2).optional(),
265
- duration: finiteSchema.positive().max(1e4).optional(),
266
- landing: finiteSchema.nonnegative().max(1e4).optional()
267
- }).strict();
268
- /** A versioned, reusable animation preset reference. */
269
- const BoardAnimationSpecSchema = z.object({
270
- kind: extensionKindSchema,
271
- kindVersion: z.number().int().positive().default(1),
272
- params: jsonObjectSchema.default({})
273
- }).strict().superRefine((spec, context) => {
274
- if (spec.kind !== "effects.deal" || spec.kindVersion !== 1) return;
275
- const parsed = BoardDealParamsSchema.safeParse(spec.params);
276
- if (parsed.success) return;
277
- context.addIssue({
278
- code: "custom",
279
- message: parsed.error.issues[0]?.message ?? "invalid effects.deal parameters",
280
- path: ["params", ...parsed.error.issues[0]?.path ?? []]
281
- });
282
- });
283
- /** Board-wide default motion policies. Omitted policies mean no motion. */
284
- const BoardMotionSchema = z.object({ enter: BoardAnimationSpecSchema.optional() }).strict();
285
- //#endregion
286
- //#region ../protocol/dist/board-capability-registry.js
287
- const CLIPS = new Set(BOARD_BUILTIN_CLIP_KINDS);
288
- const EFFECTS = new Set(BOARD_BUILTIN_EFFECT_KINDS);
289
- const BUILTIN_VERSIONS = new Set(BOARD_BUILTIN_CAPABILITIES.map((c) => `${c.kind}:${c.id}@${c.version}`));
290
- /**
291
- * True when a built-in renderer exists for exactly this kind@version. A known
292
- * kind at an unknown version is not built-in: it must not pass as one.
293
- */
294
- function isBuiltinBoardCapability(kind, id, version) {
295
- return BUILTIN_VERSIONS.has(`${kind}:${id}@${version}`);
296
- }
297
- const record$1 = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
298
- const finite$1 = (value) => typeof value === "number" && Number.isFinite(value);
299
- function validateBuiltinBoardClip(clip, path = "clip") {
300
- if (!CLIPS.has(clip.kind)) return [];
301
- const errors = [];
302
- const error = (message, suffix, coordinateSpace, code = "INVALID_BOARD_CLIP") => {
303
- errors.push({
304
- severity: "error",
305
- code,
306
- message,
307
- path: `${path}.${suffix}`,
308
- ...coordinateSpace ? { coordinateSpace } : {}
309
- });
1257
+ function applyBoardPatch(state, input, options = {}) {
1258
+ const parsedPatch = BoardPatchSchema.safeParse(input);
1259
+ if (!parsedPatch.success) return {
1260
+ ok: false,
1261
+ diagnostics: parsedPatch.error.issues.map((issue) => error(issue.path.map(String).join("."), "INVALID_PATCH", issue.message))
310
1262
  };
311
- if ((clip.kind.startsWith("motion.") || clip.kind.startsWith("draw.") || clip.kind === "text.reveal" || clip.kind === "effects.trail") && clip.target.type !== "item") error(`${clip.kind} must target an item`, "target");
312
- if (clip.kind.startsWith("camera.") && clip.target.type !== "camera") error(`${clip.kind} must target the camera`, "target");
313
- if (clip.kind === "motion.path") {
314
- const points = clip.params.points;
315
- if (!Array.isArray(points) || points.length < 2 || points.length > 1e4) error("motion path must contain 2 to 10000 world-offset points", "params.points", "world-offset");
316
- else for (const [index, point] of points.entries()) if (!record$1(point) || !finite$1(point.x) || !finite$1(point.y)) error("motion path point must contain finite x and y", `params.points.${index}`, "world-offset");
317
- }
318
- if (clip.kind === "effects.particles") {
319
- const count = clip.params.count;
320
- if (!Number.isSafeInteger(count) || count < 1 || count > DEFAULT_BOARD_RENDER_LIMITS.particles) error(`particle count must be an integer from 1 to ${DEFAULT_BOARD_RENDER_LIMITS.particles}`, "params.count", void 0, "INVALID_PARTICLE_COUNT");
321
- const bounds = clip.params.bounds;
322
- if (!record$1(bounds) || !finite$1(bounds.x) || !finite$1(bounds.y) || !finite$1(bounds.width) || !finite$1(bounds.height) || bounds.width <= 0 || bounds.height <= 0) error("particles require positive finite world bounds", "params.bounds", "world", "PARTICLE_BOUNDS_REQUIRED");
323
- }
324
- if (clip.kind === "camera.focus") {
325
- const parsed = BoardCameraFocusParamsSchema.safeParse(clip.params);
326
- if (!parsed.success) error(parsed.error.issues[0]?.message ?? "invalid camera focus", "params", "world");
327
- }
328
- return errors;
329
- }
330
- function validateBuiltinBoardEffect(effect, path = "effect") {
1263
+ const patch = parsedPatch.data;
331
1264
  const diagnostics = [];
332
- if (effect.lifecycle === "on-enter") {
333
- if (effect.target.type !== "item") diagnostics.push({
334
- severity: "error",
335
- code: "INVALID_BOARD_EFFECT",
336
- message: "on-enter effects must target an item",
337
- path: `${path}.target`
338
- });
339
- if (effect.timeOrigin !== "activation") diagnostics.push({
340
- severity: "error",
341
- code: "INVALID_BOARD_EFFECT",
342
- message: "on-enter effects must use activation time",
343
- path: `${path}.timeOrigin`
344
- });
345
- } else if (EFFECTS.has(effect.kind) && effect.target.type !== "item") diagnostics.push({
346
- severity: "error",
347
- code: "INVALID_BOARD_EFFECT",
348
- message: `${effect.kind} must target an item`,
349
- path: `${path}.target`
350
- });
351
- if (effect.kind === "effects.deal" && effect.kindVersion === 1) {
352
- if (effect.lifecycle !== "on-enter") diagnostics.push({
353
- severity: "error",
354
- code: "INVALID_BOARD_EFFECT",
355
- message: "effects.deal must use the on-enter lifecycle",
356
- path: `${path}.lifecycle`
357
- });
358
- const parsed = BoardDealParamsSchema.safeParse(effect.params);
359
- if (!parsed.success) diagnostics.push({
360
- severity: "error",
361
- code: "INVALID_BOARD_EFFECT",
362
- message: parsed.error.issues[0]?.message ?? "invalid effects.deal parameters",
363
- path: `${path}.params`
364
- });
1265
+ const nextItems = /* @__PURE__ */ new Map();
1266
+ const nextAnimations = /* @__PURE__ */ new Map();
1267
+ const nextTracks = /* @__PURE__ */ new Map();
1268
+ let nextSettings;
1269
+ const item = (id) => nextItems.has(id) ? nextItems.get(id) ?? void 0 : state.item(id);
1270
+ const animation = (id) => nextAnimations.has(id) ? nextAnimations.get(id) ?? void 0 : state.animation(id);
1271
+ const setTrack = (animationId, trackId, value) => {
1272
+ const tracks = nextTracks.get(animationId) ?? /* @__PURE__ */ new Map();
1273
+ tracks.set(trackId, value);
1274
+ nextTracks.set(animationId, tracks);
1275
+ };
1276
+ const track = (animationId, trackId) => {
1277
+ const pending = nextTracks.get(animationId);
1278
+ if (pending?.has(trackId)) return pending.get(trackId) ?? void 0;
1279
+ return state.track(animationId, trackId);
1280
+ };
1281
+ if (patch.board !== void 0) {
1282
+ const merged = patch.board === null ? {} : mergePatch(state.settings(), patch.board);
1283
+ const parsed = BoardSettingsSchema.safeParse(merged);
1284
+ if (parsed.success) nextSettings = parsed.data;
1285
+ else for (const issue of parsed.error.issues) diagnostics.push(error(["board", ...issue.path.map(String)].join("."), "INVALID_BOARD", issue.message));
1286
+ }
1287
+ const zByParent = /* @__PURE__ */ new Map();
1288
+ for (const [id, value] of Object.entries(patch.items ?? {})) {
1289
+ if (value === null) {
1290
+ if (state.item(id)) nextItems.set(id, null);
1291
+ continue;
1292
+ }
1293
+ const current = state.item(id);
1294
+ const body = stripId(value);
1295
+ const candidate = !current || typeof body.type === "string" && body.type !== current.type ? { ...body } : mergePatch(current, body);
1296
+ if (candidate.z === void 0) {
1297
+ const parent = typeof candidate.parent === "string" ? candidate.parent : void 0;
1298
+ const key = parent ?? "";
1299
+ const top = zByParent.get(key) ?? state.maxZ(parent);
1300
+ candidate.z = current?.z ?? top + 1;
1301
+ if (!current) zByParent.set(key, top + 1);
1302
+ }
1303
+ const parsed = parseBoardItem(candidate, `items.${id}`);
1304
+ if (!parsed.ok) {
1305
+ diagnostics.push(...parsed.diagnostics);
1306
+ continue;
1307
+ }
1308
+ if (current && boardJsonEquals(current, parsed.item)) continue;
1309
+ nextItems.set(id, parsed.item);
1310
+ }
1311
+ for (const [id, value] of Object.entries(patch.animations ?? {})) {
1312
+ if (value === null) {
1313
+ if (!state.animation(id)) continue;
1314
+ nextAnimations.set(id, null);
1315
+ for (const trackId of state.trackIds(id)) setTrack(id, trackId, null);
1316
+ continue;
1317
+ }
1318
+ const { tracks, ...headerPatch } = stripId(value);
1319
+ if (state.item(id) || nextItems.get(id)) {
1320
+ diagnostics.push(error(`animations.${id}`, "ID_CONFLICT", `${id} is already an item id`));
1321
+ continue;
1322
+ }
1323
+ const current = state.animation(id);
1324
+ if (!current || Object.keys(headerPatch).length > 0) {
1325
+ const header = current ? mergePatch(current, headerPatch) : {
1326
+ ...headerPatch,
1327
+ ...headerPatch.duration === void 0 ? { duration: lastKeyframeAt([...Object.values(tracks ?? {}), ...state.trackIds(id).map((trackId) => state.track(id, trackId))]) } : {}
1328
+ };
1329
+ const parsed = BoardAnimationHeaderSchema.safeParse(header);
1330
+ if (!parsed.success) {
1331
+ for (const issue of parsed.error.issues) diagnostics.push(error([
1332
+ "animations",
1333
+ id,
1334
+ ...issue.path.map(String)
1335
+ ].join("."), "INVALID_ANIMATION", issue.message));
1336
+ continue;
1337
+ }
1338
+ if (!current || !boardJsonEquals(current, parsed.data)) nextAnimations.set(id, parsed.data);
1339
+ }
1340
+ for (const [trackId, trackPatch] of Object.entries(tracks ?? {})) {
1341
+ const existing = state.track(id, trackId);
1342
+ if (trackPatch === null) {
1343
+ if (existing) setTrack(id, trackId, null);
1344
+ continue;
1345
+ }
1346
+ const parsed = BoardTrackSchema.safeParse(existing ? mergePatch(existing, trackPatch) : trackPatch);
1347
+ if (!parsed.success) {
1348
+ for (const issue of parsed.error.issues) diagnostics.push(error([
1349
+ "animations",
1350
+ id,
1351
+ "tracks",
1352
+ trackId,
1353
+ ...issue.path.map(String)
1354
+ ].join("."), "INVALID_TRACK", issue.message));
1355
+ continue;
1356
+ }
1357
+ if (!existing || !boardJsonEquals(existing, parsed.data)) setTrack(id, trackId, parsed.data);
1358
+ }
1359
+ }
1360
+ for (const id of nextItems.keys()) if (nextItems.get(id) && animation(id)) diagnostics.push(error(`items.${id}`, "ID_CONFLICT", `${id} is already an animation id`));
1361
+ if (diagnostics.length) return {
1362
+ ok: false,
1363
+ diagnostics
1364
+ };
1365
+ const deletedItems = [...nextItems].filter(([, value]) => value === null).map(([id]) => id);
1366
+ const deletionLayout = createBoardLayout((id) => item(id) ?? state.item(id));
1367
+ const pendingDeletes = [...deletedItems];
1368
+ while (pendingDeletes.length) {
1369
+ const id = pendingDeletes.pop();
1370
+ for (const child of state.children(id)) {
1371
+ if (nextItems.has(child) && nextItems.get(child)?.parent !== id) continue;
1372
+ if (options.cascade) {
1373
+ if (nextItems.get(child) !== null) {
1374
+ nextItems.set(child, null);
1375
+ pendingDeletes.push(child);
1376
+ }
1377
+ } else diagnostics.push(error(`items.${id}`, "ITEM_REFERENCED", `${id} still has child ${child}; delete it too or use cascade`));
1378
+ }
1379
+ for (const arrowId of state.binders(id)) {
1380
+ const arrow = item(arrowId);
1381
+ if (arrow?.type !== "arrow") continue;
1382
+ const props = { ...arrow.props };
1383
+ for (const which of ["start", "end"]) {
1384
+ const end = props[which];
1385
+ if (!isArrowBinding(end) || end.item !== id) continue;
1386
+ const world = options.resolveArrowEnd?.(arrowId, which) ?? deletionLayout.arrowEnd(arrowId, which);
1387
+ props[which] = applyMatrix(invertMatrix(deletionLayout.matrix(arrowId)), world);
1388
+ }
1389
+ nextItems.set(arrowId, {
1390
+ ...arrow,
1391
+ props
1392
+ });
1393
+ }
1394
+ for (const ref of state.trackRefs(id)) {
1395
+ const current = track(ref.animation, ref.track);
1396
+ if (!current || !trackReferences(current).includes(id)) continue;
1397
+ if (options.cascade) setTrack(ref.animation, ref.track, pruneBoardTrack(current, /* @__PURE__ */ new Set([id])));
1398
+ else diagnostics.push(error(`items.${id}`, "ITEM_REFERENCED", `${id} is animated by ${ref.animation}.${ref.track}; delete the track or use cascade`));
1399
+ }
1400
+ }
1401
+ for (const [id, value] of nextAnimations) {
1402
+ if (value !== null) continue;
1403
+ for (const ref of state.trackRefs(id)) {
1404
+ if (ref.animation === id || track(ref.animation, ref.track) === void 0) continue;
1405
+ if (options.cascade) setTrack(ref.animation, ref.track, null);
1406
+ else diagnostics.push(error(`animations.${id}`, "ANIMATION_REFERENCED", `${id} is nested in ${ref.animation}.${ref.track}; delete the track or use cascade`));
1407
+ }
1408
+ }
1409
+ const validationState = {
1410
+ ...state,
1411
+ trackIds: (id) => [.../* @__PURE__ */ new Set([...state.trackIds(id), ...nextTracks.get(id)?.keys() ?? []])]
1412
+ };
1413
+ const validationItems = new Map(nextItems);
1414
+ const descendants = [];
1415
+ for (const [id, value] of nextItems) if (value && value.parent !== state.item(id)?.parent) descendants.push(...state.children(id));
1416
+ const seenDescendants = /* @__PURE__ */ new Set();
1417
+ while (descendants.length) {
1418
+ const id = descendants.pop();
1419
+ if (seenDescendants.has(id)) continue;
1420
+ seenDescendants.add(id);
1421
+ validationItems.set(id, item(id) ?? null);
1422
+ descendants.push(...state.children(id));
1423
+ }
1424
+ for (const [id, value] of validationItems) {
1425
+ if (!value) continue;
1426
+ if (value.parent !== void 0) {
1427
+ const parent = item(value.parent);
1428
+ if (!parent) diagnostics.push(error(`items.${id}.parent`, "PARENT_NOT_FOUND", `parent ${value.parent} does not exist`));
1429
+ else if (parent.type !== "frame") diagnostics.push(error(`items.${id}.parent`, "INVALID_PARENT", `parent ${value.parent} is a ${parent.type}; only frames contain items`));
1430
+ else {
1431
+ let cursor = value.parent;
1432
+ for (let depth = 0; cursor; depth += 1) {
1433
+ if (depth >= 64) {
1434
+ diagnostics.push(error(`items.${id}.parent`, "PARENT_DEPTH", "parent hierarchy exceeds 64 levels"));
1435
+ break;
1436
+ }
1437
+ if (cursor === id) {
1438
+ diagnostics.push(error(`items.${id}.parent`, "PARENT_CYCLE", `${id} cannot be inside itself`));
1439
+ break;
1440
+ }
1441
+ cursor = item(cursor)?.parent;
1442
+ }
1443
+ }
1444
+ }
1445
+ for (const bound of arrowBindings(value)) {
1446
+ const target = item(bound);
1447
+ if (!target) diagnostics.push(error(`items.${id}.props`, "BINDING_NOT_FOUND", `bound item ${bound} does not exist`));
1448
+ else if (bound === id || target.type === "arrow") diagnostics.push(error(`items.${id}.props`, "INVALID_BINDING", "arrows bind to items other than arrows"));
1449
+ }
1450
+ }
1451
+ for (const [id, value] of nextItems) {
1452
+ if (!value || !state.item(id) || state.item(id)?.type === value.type) continue;
1453
+ if (value.type !== "frame") {
1454
+ for (const child of state.children(id)) if (item(child)?.parent === id) diagnostics.push(error(`items.${child}.parent`, "INVALID_PARENT", `${id} is not a frame`));
1455
+ }
1456
+ if (value.type === "arrow") for (const binder of state.binders(id)) {
1457
+ const arrow = item(binder);
1458
+ if (arrow && arrowBindings(arrow).includes(id)) diagnostics.push(error(`items.${binder}.props`, "INVALID_BINDING", "arrows cannot bind to arrows"));
1459
+ }
1460
+ for (const ref of state.trackRefs(id)) {
1461
+ const current = track(ref.animation, ref.track);
1462
+ if (current) diagnostics.push(...validateTrack(`animations.${ref.animation}.tracks.${ref.track}`, ref.animation, current, item, animation, track, validationState));
1463
+ }
1464
+ }
1465
+ for (const [animationId, tracks] of nextTracks) {
1466
+ if (!animation(animationId)) {
1467
+ for (const [trackId, value] of tracks) if (value) diagnostics.push(error(`animations.${animationId}.tracks.${trackId}`, "ANIMATION_NOT_FOUND", `animation ${animationId} does not exist`));
1468
+ continue;
1469
+ }
1470
+ for (const [trackId, value] of tracks) {
1471
+ if (!value) continue;
1472
+ diagnostics.push(...validateTrack(`animations.${animationId}.tracks.${trackId}`, animationId, value, item, animation, track, validationState));
1473
+ }
1474
+ }
1475
+ if (diagnostics.length) return {
1476
+ ok: false,
1477
+ diagnostics
1478
+ };
1479
+ const before = emptyDelta();
1480
+ const after = emptyDelta();
1481
+ if (nextSettings && !boardJsonEquals(nextSettings, state.settings())) {
1482
+ before.board = state.settings();
1483
+ after.board = nextSettings;
1484
+ }
1485
+ for (const [id, value] of nextItems) {
1486
+ const previous = state.item(id) ?? null;
1487
+ if (boardJsonEquals(previous, value)) continue;
1488
+ before.items[id] = previous;
1489
+ after.items[id] = value;
1490
+ }
1491
+ for (const [id, value] of nextAnimations) {
1492
+ before.animations[id] = state.animation(id) ?? null;
1493
+ after.animations[id] = value;
1494
+ }
1495
+ for (const [animationId, tracks] of nextTracks) for (const [trackId, value] of tracks) {
1496
+ const previous = state.track(animationId, trackId) ?? null;
1497
+ if (boardJsonEquals(previous, value)) continue;
1498
+ slot(before.tracks, animationId)[trackId] = previous;
1499
+ slot(after.tracks, animationId)[trackId] = value;
1500
+ }
1501
+ return {
1502
+ ok: true,
1503
+ before: withoutEmpty(before),
1504
+ after: withoutEmpty(after),
1505
+ diagnostics: []
1506
+ };
1507
+ }
1508
+ function validateTrack(path, animationId, value, item, animation, track, state) {
1509
+ const diagnostics = [];
1510
+ let targetType;
1511
+ if (value.target === "camera") targetType = BOARD_CAMERA_TARGET;
1512
+ else if (item(value.target)) targetType = item(value.target).type;
1513
+ else if (animation(value.target)) {
1514
+ targetType = "animation";
1515
+ if (value.target === animationId || nestsInto(value.target, animationId, animation, track, state)) return [error(`${path}.target`, "ANIMATION_CYCLE", `${animationId} cannot contain itself through ${value.target}`)];
1516
+ } else return [error(`${path}.target`, "TARGET_NOT_FOUND", `target ${value.target} is not an item, animation or camera`)];
1517
+ const resolved = resolveBoardProperty(targetType, value.property);
1518
+ if (!resolved.ok) return [error(`${path}.property`, "INVALID_PROPERTY", resolved.message)];
1519
+ for (const [index, keyframe] of value.keyframes.entries()) {
1520
+ const parsed = resolved.spec.schema.safeParse(keyframe.value);
1521
+ if (!parsed.success) {
1522
+ diagnostics.push(error(`${path}.keyframes.${index}.value`, "INVALID_KEYFRAME", parsed.error.issues[0]?.message ?? "invalid value"));
1523
+ continue;
1524
+ }
1525
+ if (value.property === "focus" && typeof keyframe.value === "string" && !item(keyframe.value)) diagnostics.push(error(`${path}.keyframes.${index}.value`, "TARGET_NOT_FOUND", `focus item ${keyframe.value} does not exist`));
365
1526
  }
1527
+ if (value.interpolation === "spline" && resolved.spec.kind !== "vec2") diagnostics.push(error(`${path}.interpolation`, "INVALID_TRACK", "spline interpolation needs a position-like property"));
366
1528
  return diagnostics;
367
1529
  }
368
- function estimateBuiltinBoardEffectCost(effect) {
369
- return isBuiltinBoardCapability("effect", effect.kind, effect.kindVersion) ? { drawCalls: 1 } : {};
1530
+ function nestsInto(from, into, animation, track, state) {
1531
+ const seen = /* @__PURE__ */ new Set();
1532
+ const stack = [from];
1533
+ while (stack.length) {
1534
+ const current = stack.pop();
1535
+ if (current === into) return true;
1536
+ if (seen.has(current) || !animation(current)) continue;
1537
+ seen.add(current);
1538
+ for (const trackId of state.trackIds(current)) {
1539
+ const value = track(current, trackId);
1540
+ if (value && value.property === "time" && animation(value.target)) stack.push(value.target);
1541
+ }
1542
+ }
1543
+ return false;
370
1544
  }
371
- function estimateBuiltinBoardClipCost(clip) {
372
- switch (clip.kind) {
373
- case "effects.particles": {
374
- const count = Number.isSafeInteger(clip.params.count) ? Math.max(0, clip.params.count) : 0;
375
- return {
376
- particles: count,
377
- vertices: count * 4,
378
- dynamicVertices: count * 4,
379
- drawCalls: 1,
380
- bufferBytes: count * 48,
381
- simulationSteps: count
382
- };
1545
+ function createDocumentState(document) {
1546
+ let children = null;
1547
+ let binders = null;
1548
+ let refs = null;
1549
+ const index = () => {
1550
+ if (children) return;
1551
+ children = /* @__PURE__ */ new Map();
1552
+ binders = /* @__PURE__ */ new Map();
1553
+ refs = /* @__PURE__ */ new Map();
1554
+ for (const [id, value] of Object.entries(document.items)) {
1555
+ if (value.parent) children.set(value.parent, [...children.get(value.parent) ?? [], id]);
1556
+ for (const bound of arrowBindings(value)) binders.set(bound, [...binders.get(bound) ?? [], id]);
383
1557
  }
384
- case "effects.trail": return {
385
- vertices: 32,
386
- dynamicVertices: 32,
387
- drawCalls: 1,
388
- bufferBytes: 1024,
389
- simulationSteps: 16
390
- };
391
- case "effects.impact":
392
- case "effects.flash": return {
393
- vertices: 64,
394
- drawCalls: 1
395
- };
396
- case "draw.reveal": return {
397
- drawCalls: 1,
398
- dynamicVertices: 1
1558
+ for (const [animationId, value] of Object.entries(document.animations)) for (const [trackId, track] of Object.entries(value.tracks)) for (const ref of trackReferences(track)) refs.set(ref, [...refs.get(ref) ?? [], {
1559
+ animation: animationId,
1560
+ track: trackId
1561
+ }]);
1562
+ };
1563
+ return {
1564
+ settings: () => document.board,
1565
+ item: (id) => document.items[id],
1566
+ animation: (id) => {
1567
+ const value = document.animations[id];
1568
+ if (!value) return void 0;
1569
+ const { tracks: _tracks, ...header } = value;
1570
+ return header;
1571
+ },
1572
+ track: (animationId, trackId) => document.animations[animationId]?.tracks[trackId],
1573
+ trackIds: (animationId) => Object.keys(document.animations[animationId]?.tracks ?? {}),
1574
+ children: (id) => {
1575
+ index();
1576
+ return children?.get(id) ?? [];
1577
+ },
1578
+ binders: (id) => {
1579
+ index();
1580
+ return binders?.get(id) ?? [];
1581
+ },
1582
+ trackRefs: (id) => {
1583
+ index();
1584
+ return refs?.get(id) ?? [];
1585
+ },
1586
+ maxZ: (parent) => {
1587
+ let max = 0;
1588
+ for (const value of Object.values(document.items)) if (value.parent === parent && (value.z ?? 0) > max) max = value.z ?? 0;
1589
+ return max;
1590
+ }
1591
+ };
1592
+ }
1593
+ function applyBoardDelta(document, delta) {
1594
+ const items = delta.items ? { ...document.items } : document.items;
1595
+ for (const [id, value] of Object.entries(delta.items ?? {})) if (value) items[id] = value;
1596
+ else delete items[id];
1597
+ const animations = delta.animations || delta.tracks ? { ...document.animations } : document.animations;
1598
+ for (const [id, value] of Object.entries(delta.animations ?? {})) if (value) animations[id] = {
1599
+ ...value,
1600
+ tracks: animations[id]?.tracks ?? {}
1601
+ };
1602
+ else delete animations[id];
1603
+ for (const [animationId, tracks] of Object.entries(delta.tracks ?? {})) {
1604
+ const current = animations[animationId];
1605
+ if (!current) continue;
1606
+ const nextTracks = { ...current.tracks };
1607
+ for (const [trackId, value] of Object.entries(tracks)) if (value) nextTracks[trackId] = value;
1608
+ else delete nextTracks[trackId];
1609
+ animations[animationId] = {
1610
+ ...current,
1611
+ tracks: nextTracks
399
1612
  };
400
- case "motion.path": return { simulationSteps: Array.isArray(clip.params.points) ? clip.params.points.length : 0 };
401
- default: return {};
402
1613
  }
1614
+ return {
1615
+ board: delta.board ?? document.board,
1616
+ items,
1617
+ animations
1618
+ };
1619
+ }
1620
+ function applyBoardPatchToDocument(document, patch, options = {}) {
1621
+ const result = applyBoardPatch(createDocumentState(document), patch, options);
1622
+ return result.ok ? {
1623
+ ok: true,
1624
+ document: applyBoardDelta(document, result.after),
1625
+ before: result.before,
1626
+ after: result.after
1627
+ } : result;
1628
+ }
1629
+ function parseBoardDocument(input) {
1630
+ if (!isRecord$6(input)) return {
1631
+ ok: false,
1632
+ diagnostics: [error("", "INVALID_DOCUMENT", "expected an object")]
1633
+ };
1634
+ const result = applyBoardPatchToDocument({
1635
+ board: BoardSettingsSchema.parse({}),
1636
+ items: {},
1637
+ animations: {}
1638
+ }, {
1639
+ ...isRecord$6(input.board) ? { board: input.board } : {},
1640
+ ...isRecord$6(input.items) ? { items: input.items } : {},
1641
+ ...isRecord$6(input.animations) ? { animations: input.animations } : {}
1642
+ });
1643
+ return result.ok ? {
1644
+ ok: true,
1645
+ document: result.document
1646
+ } : result;
403
1647
  }
404
1648
  //#endregion
405
1649
  //#region ../protocol/dist/board-url.js
@@ -449,588 +1693,343 @@ function isBlockedIpv6(host) {
449
1693
  const low = Number.parseInt(parts[7] ?? "0", 16);
450
1694
  return isBlockedIpv4(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
451
1695
  }
452
- if (parts.slice(0, 6).every((part) => part === "0000")) return true;
453
- const first = Number.parseInt(parts[0] ?? "0", 16);
454
- if ((first & 65024) === 64512) return true;
455
- if ((first & 65472) === 65152 || (first & 65472) === 65216) return true;
456
- if ((first & 65280) === 65280) return true;
457
- return parts[0] === "2001" && parts[1] === "0db8";
458
- }
459
- function isPublicBoardRemoteAddress(value) {
460
- const address = value.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
461
- if (parseIpv4(address)) return !isBlockedIpv4(address);
462
- return address.includes(":") && !isBlockedIpv6(address);
463
- }
464
- function isBlockedHost(hostname) {
465
- const host = hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
466
- if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) return true;
467
- if (parseIpv4(host) || host.includes(":")) return !isPublicBoardRemoteAddress(host);
468
- return false;
469
- }
470
- /**
471
- * Normalize a browser-loadable public HTTP(S) URL. This blocks explicit local
472
- * addresses; any future server-side fetcher must additionally validate DNS
473
- * resolution to defend against rebinding.
474
- */
475
- function normalizeBoardRemoteUrl(value) {
476
- if (typeof value !== "string") return void 0;
477
- const input = value.trim();
478
- if (!input || input.length > 4096) return void 0;
479
- try {
480
- const url = new URL(input);
481
- if (url.protocol !== "https:" && url.protocol !== "http:") return void 0;
482
- if (url.username || url.password || isBlockedHost(url.hostname)) return;
483
- const normalized = url.toString();
484
- return normalized.length <= 4096 ? normalized : void 0;
485
- } catch {
486
- return;
487
- }
488
- }
489
- const BoardRemoteUrlSchema = z.string().max(BOARD_REMOTE_URL_MAX_LENGTH).refine((value) => normalizeBoardRemoteUrl(value) !== void 0, { message: "URL must be a public HTTP(S) URL without credentials" });
490
- //#endregion
491
- //#region ../protocol/dist/board-document.js
492
- const BoardFrameSchema = z.object({
493
- x: z.number().finite(),
494
- y: z.number().finite(),
495
- width: z.number().finite().positive(),
496
- height: z.number().finite().positive(),
497
- rotation: z.number().finite().default(0)
498
- });
499
- /**
500
- * The board camera. This is local UI state, not synced content: semantic ops
501
- * (see diffBoardDocuments) never describe the viewport, and the editor holds
502
- * the live camera separately from the persisted document. Here it only serves
503
- * as an initial camera hint when a document is first loaded.
504
- */
505
- const BoardViewportSchema = z.object({
506
- x: z.number().finite(),
507
- y: z.number().finite(),
508
- zoom: z.number().finite().min(.05).max(8)
509
- });
510
- const BoardAppearanceSchema = z.object({
511
- /** Legacy field retained when reading older Board documents; not used for rendering. */
512
- theme: z.string().min(1).optional(),
513
- /** Board-wide motion policies. Omitted policies mean no motion. */
514
- motion: BoardMotionSchema.optional(),
515
- /** Legacy field retained when reading older Board documents; not used for rendering. */
516
- mood: z.enum([
517
- "clean",
518
- "playful",
519
- "arcane",
520
- "cyber",
521
- "natural"
522
- ]).optional(),
523
- background: z.object({
524
- kind: z.enum([
525
- "solid",
526
- "dots",
527
- "grid",
528
- "image",
529
- "shader",
530
- "custom"
531
- ]).default("dots"),
532
- color: z.string().optional(),
533
- imageUrl: BoardRemoteUrlSchema.optional(),
534
- fit: z.enum([
535
- "cover",
536
- "contain",
537
- "repeat"
538
- ]).optional(),
539
- position: z.enum([
540
- "center",
541
- "top",
542
- "bottom",
543
- "left",
544
- "right"
545
- ]).optional(),
546
- opacity: z.number().finite().min(0).max(1).optional()
547
- }).default({ kind: "solid" }),
548
- grid: z.object({
549
- visible: z.boolean().default(false),
550
- size: z.number().finite().min(4).default(24),
551
- opacity: z.number().finite().min(0).max(1).default(.12)
552
- }).default({
553
- visible: false,
554
- size: 24,
555
- opacity: .12
556
- })
557
- });
558
- /** Optional visual chrome still carried by a few older shapes. */
559
- const BoardItemStyleSchema = z.object({
560
- variant: z.string().min(1).default("default"),
561
- theme: z.string().min(1).optional(),
562
- accentColor: z.string().optional(),
563
- size: z.enum([
564
- "sm",
565
- "md",
566
- "lg"
567
- ]).default("md"),
568
- emphasis: z.enum([
569
- "normal",
570
- "rare",
571
- "epic",
572
- "legendary"
573
- ]).default("normal"),
574
- effects: z.array(z.string().min(1)).default([])
575
- });
576
- const SpaceFileRefSchema = z.object({
577
- kind: z.literal("space-file"),
578
- path: z.string().min(1)
579
- });
580
- const BoardMediaSnapshotSchema = z.object({
581
- title: z.string().optional(),
582
- mimeType: z.string().optional(),
583
- size: z.number().finite().nonnegative().optional(),
584
- mtimeMs: z.number().finite().nonnegative().optional(),
585
- /** Intrinsic pixel size once known. */
586
- naturalWidth: z.number().finite().positive().optional(),
587
- naturalHeight: z.number().finite().positive().optional()
588
- });
589
- const BoardItemBaseSchema = z.object({
590
- id: z.string().min(1),
591
- frame: BoardFrameSchema,
592
- parentId: z.string().min(1).nullable().optional(),
593
- /** When true the shape cannot be moved, resized, or deleted. */
594
- locked: z.boolean().optional(),
595
- style: BoardItemStyleSchema.optional(),
596
- metadata: z.record(z.string(), z.unknown()).optional()
597
- });
598
- /** Freestanding text — no card chrome; its frame always follows the glyphs. */
599
- const BoardTextItemSchema = BoardItemBaseSchema.extend({
600
- type: z.literal("text"),
601
- text: z.string().default(""),
602
- color: z.string().min(1).default("neutral"),
603
- fontSize: z.number().finite().min(2).max(512).default(24)
604
- });
605
- const BoardGeoItemSchema = BoardItemBaseSchema.extend({
606
- type: z.literal("geo"),
607
- geo: z.string().min(1).default("rectangle"),
608
- text: z.string().default(""),
609
- color: z.string().min(1).default("brand"),
610
- fillOpacity: z.number().finite().min(0).max(1).default(0)
611
- });
612
- /** A raw freehand sample. Pressure defaults to 0.5 (mouse) when absent. */
613
- const DrawPointSchema = z.object({
614
- x: z.number().finite(),
615
- y: z.number().finite(),
616
- p: z.number().finite().min(0).max(1).default(.5)
617
- });
618
- /** A world-space point. */
619
- const BoardPointSchema = z.object({
620
- x: z.number().finite(),
621
- y: z.number().finite()
622
- });
623
- const BoardDrawItemSchema = BoardItemBaseSchema.extend({
624
- type: z.literal("draw"),
625
- points: z.array(DrawPointSchema).default([]),
626
- color: z.string().min(1).default("brand"),
627
- size: z.number().finite().positive().default(4)
628
- });
629
- /**
630
- * A free arrow — a standalone annotation stroke between two world points.
631
- *
632
- * Arrows do not relate nodes. A relation between two nodes is a
633
- * `BoardConnection`, which is stored separately and resolves its geometry from
634
- * the live node frames. Keeping the two apart is what lets an arrow be a plain
635
- * shape (its own frame, freely movable) while a connection stays purely
636
- * semantic — neither has to pretend to be the other.
637
- */
638
- const BoardArrowItemSchema = BoardItemBaseSchema.extend({
639
- type: z.literal("arrow"),
640
- start: BoardPointSchema,
641
- end: BoardPointSchema,
642
- bend: z.number().finite().min(-.85).max(.85).default(0),
643
- color: z.string().min(1).default("brand"),
644
- size: z.number().finite().positive().default(BOARD_ARROW_STROKE_SIZE),
645
- arrowStart: z.boolean().default(false),
646
- arrowEnd: z.boolean().default(true),
647
- label: z.string().default("")
648
- });
649
- /** A frame container for organising shapes. */
650
- const BoardFrameItemSchema = BoardItemBaseSchema.extend({
651
- type: z.literal("frame"),
652
- label: z.string().default("Frame"),
653
- color: z.string().min(1).default("neutral")
654
- });
655
- /** Image node — space file only, natural aspect, no chrome. */
656
- const BoardImageItemSchema = BoardItemBaseSchema.extend({
657
- type: z.literal("image"),
658
- ref: SpaceFileRefSchema,
659
- snapshot: BoardMediaSnapshotSchema.optional(),
660
- /** Optional normalized crop in source image space (0..1). */
661
- crop: z.object({
662
- x: z.number().finite().min(0).max(1).default(0),
663
- y: z.number().finite().min(0).max(1).default(0),
664
- w: z.number().finite().min(0).max(1).default(1),
665
- h: z.number().finite().min(0).max(1).default(1)
666
- }).optional()
667
- });
668
- /** Video node — space file only. Playback state is local UI, never synced. */
669
- const BoardVideoItemSchema = BoardItemBaseSchema.extend({
670
- type: z.literal("video"),
671
- ref: SpaceFileRefSchema,
672
- snapshot: BoardMediaSnapshotSchema.optional()
673
- });
674
- /** Audio node — space file only. Playback state is local UI, never synced. */
675
- const BoardAudioItemSchema = BoardItemBaseSchema.extend({
676
- type: z.literal("audio"),
677
- ref: SpaceFileRefSchema,
678
- snapshot: BoardMediaSnapshotSchema.extend({ durationMs: z.number().finite().nonnegative().optional() }).optional()
679
- });
680
- /**
681
- * Cached display facts for a file card.
682
- *
683
- * Purely derived from the referenced file and versioned by `mtimeMs`, so it is a
684
- * cache and never a second source of truth: the workspace file remains
685
- * authoritative, and a stale snapshot is detectable rather than silently wrong.
686
- * It is stored so that opening a board renders complete cards immediately, with
687
- * no per-node file read on the first paint.
688
- */
689
- const BoardFileSnapshotSchema = z.object({
690
- title: z.string().optional(),
691
- mimeType: z.string().optional(),
692
- size: z.number().finite().nonnegative().optional(),
693
- mtimeMs: z.number().finite().nonnegative().optional(),
694
- /** Cleaned leading prose. Capped when written (see FILE_EXCERPT_MAX_CHARS). */
695
- excerpt: z.string().optional(),
696
- /** Cover image inside the space, already resolved to a workspace path. */
697
- coverPath: z.string().optional(),
698
- /** Cover image at an absolute https URL, as declared by the file itself. */
699
- coverUrl: z.string().optional()
700
- });
701
- /**
702
- * File node — a thumbnail entry point to any workspace file.
703
- *
704
- * This is the fallback for every dropped file that is not natively an image or
705
- * video, including binaries and unknown extensions: a board should never refuse
706
- * a file, only present it with less detail. Presentation tiers are derived from
707
- * the snapshot (see filePreviewKind), not stored, so there is no display state
708
- * to drift from the facts.
709
- */
710
- const BoardFileItemSchema = BoardItemBaseSchema.extend({
711
- type: z.literal("file"),
712
- ref: SpaceFileRefSchema,
713
- snapshot: BoardFileSnapshotSchema.optional()
714
- });
715
- const BoardTaskMediaArtifactFields = {
716
- id: z.string().min(1).max(240),
717
- title: z.string().max(240).optional(),
718
- url: BoardRemoteUrlSchema,
719
- mimeType: z.string().max(160).optional()
720
- };
721
- const BoardTaskArtifactSchema = z.discriminatedUnion("type", [
722
- z.object({
723
- ...BoardTaskMediaArtifactFields,
724
- type: z.literal("image"),
725
- naturalWidth: z.number().positive().optional(),
726
- naturalHeight: z.number().positive().optional()
727
- }).strict(),
728
- z.object({
729
- ...BoardTaskMediaArtifactFields,
730
- type: z.literal("video"),
731
- previewUrl: BoardRemoteUrlSchema.optional(),
732
- durationMs: z.number().int().positive().optional(),
733
- naturalWidth: z.number().positive().optional(),
734
- naturalHeight: z.number().positive().optional()
735
- }).strict(),
736
- z.object({
737
- ...BoardTaskMediaArtifactFields,
738
- type: z.literal("audio"),
739
- previewUrl: BoardRemoteUrlSchema.optional(),
740
- durationMs: z.number().int().positive().optional()
741
- }).strict(),
742
- z.object({
743
- id: z.string().min(1).max(240),
744
- type: z.literal("text"),
745
- title: z.string().max(240).optional(),
746
- textExcerpt: z.string().min(1).max(480)
747
- }).strict()
748
- ]);
749
- /**
750
- * Cached task facts used for an immediate first paint. The task run remains the
751
- * source of truth and live clients refresh this projection by `taskRunId`.
752
- */
753
- const BoardTaskSnapshotSchema = z.object({
754
- taskType: z.string().min(1).max(120),
755
- status: z.enum([
756
- "pending",
757
- "running",
758
- "completed",
759
- "failed"
760
- ]),
761
- title: z.string().min(1).max(240),
762
- model: z.string().max(160).optional(),
763
- promptExcerpt: z.string().max(480).optional(),
764
- artifactCount: z.number().int().nonnegative(),
765
- artifacts: z.array(BoardTaskArtifactSchema).max(6).default([]),
766
- updatedAt: z.string().optional()
767
- }).strict();
768
- /** A stable reference to a task run with a small, replaceable display cache. */
769
- const BoardTaskItemSchema = BoardItemBaseSchema.extend({
770
- type: z.literal("task"),
771
- taskRunId: z.string().min(1),
772
- snapshot: BoardTaskSnapshotSchema
773
- });
774
- /**
775
- * A forward-compatible carrier for shape types this client does not recognise.
776
- * Its discriminant is the literal `"unknown"` so the item union still narrows
777
- * cleanly on `type`. The real type string and every original field are preserved
778
- * verbatim in `raw`.
779
- */
780
- const UNKNOWN_BOARD_ITEM_TYPE = "unknown";
781
- const BoardItemSchema = z.any().transform((raw) => parseBoardItemLoose(raw));
1696
+ if (parts.slice(0, 6).every((part) => part === "0000")) return true;
1697
+ const first = Number.parseInt(parts[0] ?? "0", 16);
1698
+ if ((first & 65024) === 64512) return true;
1699
+ if ((first & 65472) === 65152 || (first & 65472) === 65216) return true;
1700
+ if ((first & 65280) === 65280) return true;
1701
+ return parts[0] === "2001" && parts[1] === "0db8";
1702
+ }
1703
+ function isPublicBoardRemoteAddress(value) {
1704
+ const address = value.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
1705
+ if (parseIpv4(address)) return !isBlockedIpv4(address);
1706
+ return address.includes(":") && !isBlockedIpv6(address);
1707
+ }
1708
+ function isBlockedHost(hostname) {
1709
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
1710
+ if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) return true;
1711
+ if (parseIpv4(host) || host.includes(":")) return !isPublicBoardRemoteAddress(host);
1712
+ return false;
1713
+ }
782
1714
  /**
783
- * Parse a single item leniently: known types are validated and normalised;
784
- * anything else becomes a lossless unknown item. Never throws on shape data —
785
- * a malformed known item degrades to unknown rather than failing the document.
1715
+ * Normalize a browser-loadable public HTTP(S) URL. This blocks explicit local
1716
+ * addresses; any future server-side fetcher must additionally validate DNS
1717
+ * resolution to defend against rebinding.
786
1718
  */
787
- function parseBoardItemLoose(raw) {
788
- if (!raw || typeof raw !== "object") return makeUnknownItem(raw);
789
- switch (raw.type) {
790
- case "image": {
791
- const parsed = BoardImageItemSchema.safeParse(raw);
792
- return parsed.success ? parsed.data : makeUnknownItem(raw);
793
- }
794
- case "video": {
795
- const parsed = BoardVideoItemSchema.safeParse(raw);
796
- return parsed.success ? parsed.data : makeUnknownItem(raw);
797
- }
798
- case "audio": {
799
- const parsed = BoardAudioItemSchema.safeParse(raw);
800
- return parsed.success ? parsed.data : makeUnknownItem(raw);
801
- }
802
- case "file": {
803
- const parsed = BoardFileItemSchema.safeParse(raw);
804
- return parsed.success ? parsed.data : makeUnknownItem(raw);
805
- }
806
- case "task": {
807
- const parsed = BoardTaskItemSchema.safeParse(raw);
808
- return parsed.success ? parsed.data : makeUnknownItem(raw);
809
- }
810
- case "text": {
811
- const parsed = BoardTextItemSchema.safeParse(raw);
812
- return parsed.success ? parsed.data : makeUnknownItem(raw);
813
- }
814
- case "geo": {
815
- const parsed = BoardGeoItemSchema.safeParse(raw);
816
- return parsed.success ? parsed.data : makeUnknownItem(raw);
817
- }
818
- case "draw": {
819
- const parsed = BoardDrawItemSchema.safeParse(raw);
820
- return parsed.success ? parsed.data : makeUnknownItem(raw);
821
- }
822
- case "arrow": {
823
- const parsed = BoardArrowItemSchema.safeParse(raw);
824
- return parsed.success ? parsed.data : makeUnknownItem(raw);
825
- }
826
- case "frame": {
827
- const parsed = BoardFrameItemSchema.safeParse(raw);
828
- return parsed.success ? parsed.data : makeUnknownItem(raw);
829
- }
830
- default: return makeUnknownItem(raw);
1719
+ function normalizeBoardRemoteUrl(value) {
1720
+ if (typeof value !== "string") return void 0;
1721
+ const input = value.trim();
1722
+ if (!input || input.length > 4096) return void 0;
1723
+ try {
1724
+ const url = new URL(input);
1725
+ if (url.protocol !== "https:" && url.protocol !== "http:") return void 0;
1726
+ if (url.username || url.password || isBlockedHost(url.hostname)) return;
1727
+ const normalized = url.toString();
1728
+ return normalized.length <= 4096 ? normalized : void 0;
1729
+ } catch {
1730
+ return;
831
1731
  }
832
1732
  }
833
- function makeUnknownItem(raw) {
834
- const record = raw && typeof raw === "object" ? raw : {};
835
- const frameParsed = BoardFrameSchema.safeParse(record.frame);
836
- const styleParsed = BoardItemStyleSchema.safeParse(record.style);
1733
+ z.string().max(BOARD_REMOTE_URL_MAX_LENGTH).refine((value) => normalizeBoardRemoteUrl(value) !== void 0, { message: "URL must be a public HTTP(S) URL without credentials" });
1734
+ //#endregion
1735
+ //#region ../protocol/dist/turn-origin.js
1736
+ const record$2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
1737
+ const normalizeSessionTurnOrigin = (value) => {
1738
+ const input = record$2(value);
1739
+ if (!input || ![
1740
+ "prompt",
1741
+ "scheduled_prompt",
1742
+ "background_task",
1743
+ "hook"
1744
+ ].includes(String(input.kind))) return null;
1745
+ const source = normalizeRequestSource(input);
1746
+ if (!source?.spaceId || !source.sessionId || !source.turnId) return null;
837
1747
  return {
838
- id: typeof record.id === "string" && record.id ? record.id : "unknown",
839
- type: UNKNOWN_BOARD_ITEM_TYPE,
840
- frame: frameParsed.success ? frameParsed.data : {
841
- x: 0,
842
- y: 0,
843
- width: 120,
844
- height: 80,
845
- rotation: 0
846
- },
847
- ...typeof record.parentId === "string" || record.parentId === null ? { parentId: record.parentId } : {},
848
- ...record.locked === true ? { locked: true } : {},
849
- ...styleParsed.success && record.style ? { style: styleParsed.data } : {},
850
- ...record.metadata && typeof record.metadata === "object" ? { metadata: record.metadata } : {},
851
- raw: record
1748
+ kind: input.kind,
1749
+ spaceId: source.spaceId,
1750
+ sessionId: source.sessionId,
1751
+ turnId: source.turnId,
1752
+ ...source.toolCallId ? { toolCallId: source.toolCallId } : {},
1753
+ ...typeof input.depth === "number" && Number.isSafeInteger(input.depth) && input.depth > 0 ? { depth: input.depth } : {}
852
1754
  };
853
- }
854
- z.object({
855
- kind: z.literal(BOARD_DOCUMENT_KIND),
856
- version: z.literal(1),
857
- appearance: BoardAppearanceSchema.default({
858
- background: { kind: "solid" },
859
- grid: {
860
- visible: false,
861
- size: 24,
862
- opacity: .12
863
- }
864
- }),
865
- viewport: BoardViewportSchema,
866
- items: z.array(BoardItemSchema),
867
- /**
868
- * Node relations. Separate from `items` because a connection has no frame of
869
- * its own — its geometry is derived from the nodes it joins, so it is a
870
- * relation over the item set rather than a member of it.
871
- *
872
- * Connections referencing a missing node are dropped on parse: a relation to
873
- * nothing is not a relation, and keeping one would let an invisible dangling
874
- * edge accumulate silently. Callers that need to know write through the
875
- * transaction API, which reports the reference error instead.
876
- */
877
- connections: z.array(BoardConnectionSchema).default([])
878
- });
1755
+ };
1756
+ /** Reads durable provenance; legacy background tasks only knew their caller's Session/Turn. */
1757
+ const readSessionTurnOrigin = (meta, spaceId) => {
1758
+ const input = record$2(meta);
1759
+ const origin = normalizeSessionTurnOrigin(input?.origin);
1760
+ if (origin) return origin;
1761
+ const context = record$2(input?.context);
1762
+ if (context?.kind !== "background_bash_task" || !isUuidLike(spaceId)) return null;
1763
+ const legacy = record$2(context.origin);
1764
+ return normalizeSessionTurnOrigin({
1765
+ ...legacy,
1766
+ spaceId,
1767
+ kind: "background_task"
1768
+ });
1769
+ };
879
1770
  //#endregion
880
- //#region ../protocol/dist/board-node.js
881
- const BOARD_COLOR_IDS = [
882
- "brand",
883
- "neutral",
884
- "black",
885
- "white",
886
- "blue",
887
- "green",
888
- "amber",
889
- "violet",
890
- "rose"
891
- ];
892
- const BoardColorIdSchema = z.enum(BOARD_COLOR_IDS);
893
- const BOARD_GEO_KINDS = [
894
- "rectangle",
895
- "rounded",
896
- "ellipse",
897
- "diamond",
898
- "triangle"
899
- ];
900
- const BoardGeoKindSchema = z.enum(BOARD_GEO_KINDS);
901
- const BOARD_NATIVE_NODE_TYPES = [
902
- "image",
903
- "video",
904
- "audio",
905
- "file",
906
- "task",
907
- "text",
908
- "geo",
909
- "draw",
910
- "arrow",
911
- "frame"
1771
+ //#region ../protocol/dist/sent-turns.js
1772
+ const KINDS = [
1773
+ "prompt",
1774
+ "scheduled_prompt",
1775
+ "background_task",
1776
+ "hook"
912
1777
  ];
913
- const metadataSchema = z.record(z.string(), z.unknown());
914
- const commonData = {
915
- locked: z.boolean().optional(),
916
- metadata: metadataSchema.optional()
1778
+ const record$1 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
1779
+ const normalizeSentTurnRef = (value) => {
1780
+ const input = record$1(value);
1781
+ if (!input) return null;
1782
+ if (!isUuidLike(input.sessionId) || !isUuidLike(input.turnId)) return null;
1783
+ const kind = String(input.kind);
1784
+ if (!KINDS.includes(kind)) return null;
1785
+ const source = normalizeRequestSource({
1786
+ spaceId: input.spaceId,
1787
+ toolCallId: input.toolCallId
1788
+ });
1789
+ return {
1790
+ ...source?.spaceId ? { spaceId: source.spaceId } : {},
1791
+ sessionId: input.sessionId,
1792
+ turnId: input.turnId,
1793
+ kind,
1794
+ ...source?.toolCallId ? { toolCallId: source.toolCallId } : {}
1795
+ };
917
1796
  };
918
- const pointSchema = z.object({
919
- x: z.number().finite(),
920
- y: z.number().finite(),
921
- p: z.number().finite().min(0).max(1).default(.5)
922
- }).strict();
923
- const worldPointSchema = z.object({
924
- x: z.number().finite(),
925
- y: z.number().finite()
926
- }).strict();
927
- const strokeSizeSchema = z.number().finite().min(1).max(64);
928
- const mediaViewSchema = z.object({
929
- title: z.string().optional(),
930
- mimeType: z.string().optional(),
931
- size: z.number().finite().nonnegative().optional(),
932
- mtimeMs: z.number().finite().nonnegative().optional(),
933
- naturalWidth: z.number().finite().positive().optional(),
934
- naturalHeight: z.number().finite().positive().optional()
935
- }).strict();
936
- const audioViewSchema = mediaViewSchema.extend({ durationMs: z.number().finite().nonnegative().optional() });
937
- const dataSchemas = {
938
- text: z.object({
939
- ...commonData,
940
- text: z.string().default(""),
941
- color: BoardColorIdSchema.default("neutral"),
942
- fontSize: z.number().finite().min(2).max(512).default(24)
943
- }).strict(),
944
- geo: z.object({
945
- ...commonData,
946
- geo: BoardGeoKindSchema.default("rectangle"),
947
- text: z.string().default(""),
948
- color: BoardColorIdSchema.default("brand"),
949
- fillOpacity: z.number().finite().min(0).max(1).default(0)
950
- }).strict(),
951
- draw: z.object({
952
- ...commonData,
953
- points: z.array(pointSchema).min(1),
954
- color: BoardColorIdSchema.default("brand"),
955
- size: strokeSizeSchema.default(4)
956
- }).strict(),
957
- arrow: z.object({
958
- ...commonData,
959
- start: worldPointSchema,
960
- end: worldPointSchema,
961
- bend: z.number().finite().min(-.85).max(.85).default(0),
962
- color: BoardColorIdSchema.default("brand"),
963
- size: strokeSizeSchema.default(BOARD_ARROW_STROKE_SIZE),
964
- arrowStart: z.boolean().default(false),
965
- arrowEnd: z.boolean().default(true),
966
- label: z.string().default("")
967
- }).strict(),
968
- frame: z.object({
969
- ...commonData,
970
- label: z.string().default("Frame"),
971
- color: BoardColorIdSchema.default("neutral")
972
- }).strict(),
973
- image: z.object({
974
- ...commonData,
975
- crop: z.object({
976
- x: z.number().finite().min(0).max(1),
977
- y: z.number().finite().min(0).max(1),
978
- w: z.number().finite().min(0).max(1),
979
- h: z.number().finite().min(0).max(1)
980
- }).strict().optional()
981
- }).strict(),
982
- video: z.object(commonData).strict(),
983
- audio: z.object(commonData).strict(),
984
- file: z.object(commonData).strict(),
985
- task: z.object({
986
- ...commonData,
987
- taskRunId: z.string().min(1)
988
- }).strict()
1797
+ /** Deduplicates by child Turn, so a retried request cannot render one twice. */
1798
+ const readSentTurns = (meta) => {
1799
+ const raw = record$1(meta)?.messagesSent;
1800
+ if (!Array.isArray(raw)) return [];
1801
+ const seen = /* @__PURE__ */ new Set();
1802
+ const refs = [];
1803
+ for (const value of raw) {
1804
+ const ref = normalizeSentTurnRef(value);
1805
+ if (!ref || seen.has(ref.turnId)) continue;
1806
+ seen.add(ref.turnId);
1807
+ refs.push(ref);
1808
+ }
1809
+ return refs;
989
1810
  };
990
- const fileViewSchema = z.object({
991
- title: z.string().optional(),
992
- mimeType: z.string().optional(),
993
- size: z.number().finite().nonnegative().optional(),
994
- mtimeMs: z.number().finite().nonnegative().optional(),
995
- excerpt: z.string().optional(),
996
- coverPath: z.string().optional(),
997
- coverUrl: z.string().url().optional()
998
- }).strict();
999
- const taskViewSchema = BoardTaskSnapshotSchema;
1000
- const emptyViewSchema = z.object({}).strict();
1001
- const viewSchemas = {
1002
- image: mediaViewSchema,
1003
- video: mediaViewSchema,
1004
- audio: audioViewSchema,
1005
- file: fileViewSchema,
1006
- task: taskViewSchema,
1007
- text: emptyViewSchema,
1008
- geo: emptyViewSchema,
1009
- draw: emptyViewSchema,
1010
- arrow: emptyViewSchema,
1011
- frame: emptyViewSchema
1811
+ //#endregion
1812
+ //#region ../protocol/dist/display/actions.js
1813
+ const DISPLAY_ACTION_TYPES = [
1814
+ "tap",
1815
+ "long_press",
1816
+ "swipe",
1817
+ "scroll",
1818
+ "type",
1819
+ "key",
1820
+ "system",
1821
+ "wait"
1822
+ ];
1823
+ const ACTION_GAP_MS = 150;
1824
+ const TAP_MS = 60;
1825
+ const MULTI_TAP_GAP_MS = 80;
1826
+ const LONG_PRESS_MS = 800;
1827
+ const SWIPE_MS = 300;
1828
+ const SCROLL_MS = 400;
1829
+ const WAIT_MS = 500;
1830
+ const MAX_STEP_MS = 1e4;
1831
+ var DisplayActionError = class extends Error {
1832
+ constructor(message) {
1833
+ super(message);
1834
+ this.name = "DisplayActionError";
1835
+ }
1012
1836
  };
1013
- const nodeEnvelopeSchema = z.object({
1014
- nodeId: z.string().min(1).max(160),
1015
- type: z.string().min(1).max(40),
1016
- parentId: z.string().min(1).max(160).nullable(),
1017
- orderKey: z.string().max(4096).nullable(),
1018
- x: z.number().finite(),
1019
- y: z.number().finite(),
1020
- width: z.number().finite().positive(),
1021
- height: z.number().finite().positive(),
1022
- rotation: z.number().finite(),
1023
- refKind: z.string().max(40).nullable(),
1024
- refPath: z.string().max(4096).nullable(),
1025
- refUrl: z.string().max(4096).nullable(),
1026
- view: z.record(z.string(), z.unknown()),
1027
- style: z.record(z.string(), z.unknown()),
1028
- data: z.record(z.string(), z.unknown())
1029
- }).strict();
1030
- function jsonSchema(schema) {
1031
- return z.toJSONSchema(schema);
1837
+ const MODIFIER_ALIASES = {
1838
+ control: "Control",
1839
+ ctrl: "Control",
1840
+ alt: "Alt",
1841
+ option: "Alt",
1842
+ shift: "Shift",
1843
+ meta: "Meta",
1844
+ cmd: "Meta",
1845
+ command: "Meta",
1846
+ super: "Meta",
1847
+ win: "Meta"
1848
+ };
1849
+ function displayKeyEvents(key, modifiers = [], t) {
1850
+ const at = t === void 0 ? {} : { t };
1851
+ return [
1852
+ ...modifiers.map((modifier) => ({
1853
+ type: "key",
1854
+ action: "down",
1855
+ key: modifier,
1856
+ ...at
1857
+ })),
1858
+ {
1859
+ type: "key",
1860
+ action: "press",
1861
+ key,
1862
+ ...at
1863
+ },
1864
+ ...[...modifiers].reverse().map((modifier) => ({
1865
+ type: "key",
1866
+ action: "up",
1867
+ key: modifier,
1868
+ ...at
1869
+ }))
1870
+ ];
1871
+ }
1872
+ function parseDisplayKeyCombo(combo) {
1873
+ const parts = combo.length > 1 && combo.endsWith("++") ? [...combo.slice(0, -2).split("+"), "+"] : combo.length > 1 ? combo.split("+") : [combo];
1874
+ const key = parts.pop() ?? "";
1875
+ const modifiers = parts.map((part) => MODIFIER_ALIASES[part.trim().toLowerCase()]);
1876
+ if (!key || modifiers.some((modifier) => !modifier)) throw new DisplayActionError(`unknown key combo "${combo}"`);
1877
+ return {
1878
+ key: MODIFIER_ALIASES[key.toLowerCase()] ?? key,
1879
+ modifiers: [...new Set(modifiers)]
1880
+ };
1881
+ }
1882
+ function compileDisplayActions(actions, width, height) {
1883
+ const events = [];
1884
+ let t = 0;
1885
+ const point = (action, xKey = "x", yKey = "y") => {
1886
+ const x = action[xKey];
1887
+ const y = action[yKey];
1888
+ if (typeof x !== "number" || typeof y !== "number" || !Number.isFinite(x) || !Number.isFinite(y)) throw new DisplayActionError(`${action.type} needs ${xKey} and ${yKey}`);
1889
+ if (x < 0 || y < 0 || x >= width || y >= height) throw new DisplayActionError(`${action.type} at (${x}, ${y}) is outside the ${width}×${height} image`);
1890
+ return normalizeDisplayPoint(x, y, width, height);
1891
+ };
1892
+ const duration = (action, fallback) => Math.round(Math.min(MAX_STEP_MS, Math.max(1, action.duration_ms ?? fallback)));
1893
+ const element = (ref, elementAction, text) => {
1894
+ if (!DISPLAY_ELEMENT_REF_PATTERN.test(ref)) throw new DisplayActionError(`"${ref}" is not an element ref such as e3.12`);
1895
+ events.push({
1896
+ type: "element",
1897
+ ref,
1898
+ action: elementAction,
1899
+ ...text === void 0 ? {} : { text },
1900
+ t
1901
+ });
1902
+ t += TAP_MS;
1903
+ };
1904
+ for (const action of actions) {
1905
+ if (action.ref !== void 0) {
1906
+ if (action.type === "tap" && (action.count ?? 1) === 1 && (action.button ?? "primary") === "primary") element(action.ref, "click");
1907
+ else if (action.type === "long_press") element(action.ref, "longPress");
1908
+ else if (action.type === "type") {
1909
+ if (action.text === void 0 || action.text.length > 4096) throw new DisplayActionError(`type into an element needs text of at most ${DISPLAY_INPUT_MAX_TEXT} characters`);
1910
+ element(action.ref, "setText", action.text);
1911
+ } else if (action.type === "scroll") element(action.ref, action.direction === "up" || action.direction === "left" ? "scrollBackward" : "scrollForward");
1912
+ else throw new DisplayActionError(`${action.type} cannot take a ref; use coordinates`);
1913
+ t += ACTION_GAP_MS;
1914
+ continue;
1915
+ }
1916
+ switch (action.type) {
1917
+ case "tap": {
1918
+ const at = {
1919
+ ...point(action),
1920
+ ...action.button && action.button !== "primary" ? { button: action.button } : {}
1921
+ };
1922
+ const count = Math.round(Math.min(3, Math.max(1, action.count ?? 1)));
1923
+ for (let tap = 0; tap < count; tap++) {
1924
+ if (tap > 0) t += MULTI_TAP_GAP_MS;
1925
+ events.push({
1926
+ type: "pointer",
1927
+ action: "down",
1928
+ ...at,
1929
+ t
1930
+ }, {
1931
+ type: "pointer",
1932
+ action: "up",
1933
+ ...at,
1934
+ t: t + TAP_MS
1935
+ });
1936
+ t += TAP_MS;
1937
+ }
1938
+ break;
1939
+ }
1940
+ case "long_press": {
1941
+ const at = point(action);
1942
+ const hold = duration(action, LONG_PRESS_MS);
1943
+ events.push({
1944
+ type: "pointer",
1945
+ action: "down",
1946
+ ...at,
1947
+ t
1948
+ }, {
1949
+ type: "pointer",
1950
+ action: "up",
1951
+ ...at,
1952
+ t: t + hold
1953
+ });
1954
+ t += hold;
1955
+ break;
1956
+ }
1957
+ case "swipe": {
1958
+ const from = point(action);
1959
+ const to = point(action, "to_x", "to_y");
1960
+ const length = duration(action, SWIPE_MS);
1961
+ events.push({
1962
+ type: "pointer",
1963
+ action: "down",
1964
+ ...from,
1965
+ t
1966
+ }, {
1967
+ type: "pointer",
1968
+ action: "move",
1969
+ x: (from.x + to.x) / 2,
1970
+ y: (from.y + to.y) / 2,
1971
+ t: t + Math.round(length / 2)
1972
+ }, {
1973
+ type: "pointer",
1974
+ action: "up",
1975
+ ...to,
1976
+ t: t + length
1977
+ });
1978
+ t += length;
1979
+ break;
1980
+ }
1981
+ case "scroll": {
1982
+ const at = typeof action.x === "number" || typeof action.y === "number" ? point(action) : {
1983
+ x: .5,
1984
+ y: .5
1985
+ };
1986
+ const amount = Math.min(1, Math.max(.1, action.amount ?? .5));
1987
+ const direction = action.direction ?? "down";
1988
+ const dx = direction === "right" ? amount : direction === "left" ? -amount : 0;
1989
+ const dy = direction === "down" ? amount : direction === "up" ? -amount : 0;
1990
+ events.push({
1991
+ type: "scroll",
1992
+ ...at,
1993
+ dx,
1994
+ dy,
1995
+ t
1996
+ });
1997
+ t += SCROLL_MS;
1998
+ break;
1999
+ }
2000
+ case "type":
2001
+ if (!action.text) throw new DisplayActionError("type needs text");
2002
+ for (const text of splitDisplayText(action.text)) events.push({
2003
+ type: "text",
2004
+ text,
2005
+ t
2006
+ });
2007
+ break;
2008
+ case "key": {
2009
+ if (!action.key) throw new DisplayActionError("key needs a key");
2010
+ const { key, modifiers } = parseDisplayKeyCombo(action.key);
2011
+ events.push(...displayKeyEvents(key, modifiers, t));
2012
+ break;
2013
+ }
2014
+ case "system":
2015
+ if (!action.action) throw new DisplayActionError("system needs an action");
2016
+ events.push({
2017
+ type: "system",
2018
+ action: action.action,
2019
+ t
2020
+ });
2021
+ break;
2022
+ case "wait":
2023
+ t += duration(action, WAIT_MS);
2024
+ break;
2025
+ default: throw new DisplayActionError(`unknown action ${String(action.type)}`);
2026
+ }
2027
+ t += ACTION_GAP_MS;
2028
+ if (t > 6e4) throw new DisplayActionError("the actions take longer than a minute; split them up");
2029
+ }
2030
+ if (events.length === 0) throw new DisplayActionError("no action touches the display");
2031
+ return events;
1032
2032
  }
1033
- jsonSchema(nodeEnvelopeSchema), Object.fromEntries(BOARD_NATIVE_NODE_TYPES.map((type) => [type, jsonSchema(dataSchemas[type])])), Object.fromEntries(BOARD_NATIVE_NODE_TYPES.map((type) => [type, jsonSchema(viewSchemas[type])]));
1034
2033
  //#endregion
1035
2034
  //#region ../protocol/dist/generation/catalog.js
1036
2035
  function isGenerationModelHidden(model) {
@@ -1936,6 +2935,54 @@ function fingerprintProjectionTurns(turns) {
1936
2935
  return `${turns.length}:${fingerprint(turns)}`;
1937
2936
  }
1938
2937
  //#endregion
2938
+ //#region ../protocol/dist/core/image.js
2939
+ /** Pi only models inline image bytes; remote images travel through it under this MIME type with the URL as `data`. */
2940
+ const IMAGE_URL_MIME_TYPE = "application/x-cohub-image-url";
2941
+ const imageUrlContent = (url) => ({
2942
+ type: "image",
2943
+ data: url,
2944
+ mimeType: IMAGE_URL_MIME_TYPE
2945
+ });
2946
+ const DATA_URL_PATTERN = /^data:([^;,]*)((?:;[^;,]*)*);base64,/i;
2947
+ function parseBase64DataUrl(value) {
2948
+ const match = DATA_URL_PATTERN.exec(value.trimStart());
2949
+ if (!match) return null;
2950
+ return {
2951
+ mediaType: match[1]?.trim().toLowerCase() || null,
2952
+ data: value.trimStart().slice(match[0].length).trim()
2953
+ };
2954
+ }
2955
+ const stripDataUrlPrefix = (data) => parseBase64DataUrl(data)?.data ?? data.trim();
2956
+ const IMAGE_SIGNATURES = [
2957
+ ["image/png", (b) => b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71],
2958
+ ["image/jpeg", (b) => b[0] === 255 && b[1] === 216 && b[2] === 255],
2959
+ ["image/gif", (b) => b[0] === 71 && b[1] === 73 && b[2] === 70],
2960
+ ["image/webp", (b) => b[0] === 82 && b[1] === 73 && b[2] === 70 && b[3] === 70 && b[8] === 87 && b[9] === 69 && b[10] === 66 && b[11] === 80]
2961
+ ];
2962
+ function sniffImageMimeType(bytes) {
2963
+ return IMAGE_SIGNATURES.find(([, matches]) => matches(bytes))?.[0] ?? null;
2964
+ }
2965
+ function sniffBase64ImageMimeType(data) {
2966
+ try {
2967
+ return sniffImageMimeType(Uint8Array.from(atob(stripDataUrlPrefix(data).slice(0, 16)), (char) => char.charCodeAt(0)));
2968
+ } catch {
2969
+ return null;
2970
+ }
2971
+ }
2972
+ function imageBlockToPi(block) {
2973
+ if (block.source.type === "url") {
2974
+ const url = block.source.url.trim();
2975
+ return url ? imageUrlContent(url) : null;
2976
+ }
2977
+ const data = stripDataUrlPrefix(block.source.data);
2978
+ if (!data) return null;
2979
+ return {
2980
+ type: "image",
2981
+ data,
2982
+ mimeType: block.source.media_type && block.source.media_type !== "application/octet-stream" ? block.source.media_type : sniffBase64ImageMimeType(data) ?? "application/octet-stream"
2983
+ };
2984
+ }
2985
+ //#endregion
1939
2986
  //#region ../protocol/dist/runtime/context.js
1940
2987
  const createEmptyUsage = () => ({
1941
2988
  input: 0,
@@ -1953,19 +3000,15 @@ const createEmptyUsage = () => ({
1953
3000
  });
1954
3001
  /**
1955
3002
  * Project one block as itself, or drop it. Nothing is ever rewritten into prompt text.
1956
- * URL images, system notes and unknown blocks have no native representation here; the
1957
- * durable copy stays in the database and is not faked into the model's input.
3003
+ * Remote images stay URLs for the model runtime to resolve; system notes and unknown blocks
3004
+ * have no native representation here, and the durable copy stays in the database.
1958
3005
  */
1959
- function projectBlock(block) {
3006
+ function projectBlock(block, remoteImages = true) {
1960
3007
  if (block.type === "text") return {
1961
3008
  type: "text",
1962
3009
  text: block.text
1963
3010
  };
1964
- if (block.type === "image") return block.source.type === "base64" ? {
1965
- type: "image",
1966
- data: block.source.data,
1967
- mimeType: block.source.media_type
1968
- } : null;
3011
+ if (block.type === "image") return remoteImages || block.source.type === "base64" ? imageBlockToPi(block) : null;
1969
3012
  if (block.type === "thinking") return {
1970
3013
  type: "thinking",
1971
3014
  thinking: block.thinking,
@@ -1973,7 +3016,7 @@ function projectBlock(block) {
1973
3016
  };
1974
3017
  return null;
1975
3018
  }
1976
- const projectContent = (content) => content.map(projectBlock).filter((block) => block !== null);
3019
+ const projectContent = (content) => content.map((block) => projectBlock(block)).filter((block) => block !== null);
1977
3020
  const isCompaction = (message) => message.role === "system" && message.content.some((block) => block.type === "system_note" && block.note_type === "compacted");
1978
3021
  /** A generation placeholder must never be projected as if it were a finished result. */
1979
3022
  const isPendingGenerationResult = (message) => message.role === "assistant" && message.meta?.messageKind === "generation_result" && message.meta?.generationStatus !== "completed" && message.meta?.generationStatus !== "failed";
@@ -2030,7 +3073,7 @@ function contextToPiMessages(messages, options = {}) {
2030
3073
  name: block.name,
2031
3074
  arguments: block.input
2032
3075
  }];
2033
- const projected = projectBlock(block);
3076
+ const projected = projectBlock(block, false);
2034
3077
  return projected ? [projected] : [];
2035
3078
  });
2036
3079
  if (content.length) result.push({
@@ -2092,7 +3135,7 @@ const resolveHarness = (meta) => {
2092
3135
  return isLocalHarness(value) ? value : "cohub";
2093
3136
  };
2094
3137
  const runtimeCapabilitiesSchema = z.object({
2095
- harnesses: z.array(z.enum(["pi", "codex"])).min(1).max(2),
3138
+ harnesses: z.array(z.enum(["pi", "codex"])).max(2),
2096
3139
  models: z.array(z.object({
2097
3140
  harness: z.enum(["pi", "codex"]),
2098
3141
  provider: z.string().max(100),
@@ -2334,6 +3377,7 @@ const runtimeCommandSchema = z.discriminatedUnion("type", [
2334
3377
  ]);
2335
3378
  //#endregion
2336
3379
  //#region ../protocol/dist/public-identifiers.js
3380
+ const HOME_SPACE_SLUG = "home";
2337
3381
  const USERNAME_PATTERN = /^(?!-)(?!.*--)[a-z0-9-]{1,39}(?<!-)$/;
2338
3382
  const SPACE_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9_-]{0,78}[a-z0-9])?$/;
2339
3383
  /**
@@ -4888,6 +5932,10 @@ attach: attachAppEmbed },
4888
5932
  ensureRealtimeConnected(this.websocketClient);
4889
5933
  return this.websocketClient.on("event", handler);
4890
5934
  }
5935
+ /** Current realtime transport state; pair with {@link onConnection} for changes. */
5936
+ get connectionState() {
5937
+ return this.websocketClient.state;
5938
+ }
4891
5939
  onConnection(handler) {
4892
5940
  const connectingCleanup = this.websocketClient.on("connecting", (payload) => {
4893
5941
  handler({
@@ -5027,6 +6075,34 @@ const parseWorkRef = parseAppRef;
5027
6075
  /** @deprecated Use `formatAppRef`. */
5028
6076
  const formatWorkRef = formatAppRef;
5029
6077
  //#endregion
6078
+ //#region src/space-ref.ts
6079
+ const canonicalUuid = (value) => {
6080
+ const hex = value.replaceAll("-", "").toLowerCase();
6081
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
6082
+ };
6083
+ function parseSpaceRef(value) {
6084
+ const trimmed = value.trim();
6085
+ if (isUuidOrShortUuid(trimmed)) return {
6086
+ kind: "id",
6087
+ id: canonicalUuid(trimmed)
6088
+ };
6089
+ const parts = trimmed.split("/");
6090
+ if (parts.length === 2) {
6091
+ const username = parseUsername(parts[0]);
6092
+ const slug = parseSpaceSlug(parts[1]);
6093
+ return username && slug ? {
6094
+ kind: "public",
6095
+ username,
6096
+ slug
6097
+ } : null;
6098
+ }
6099
+ const slug = parts.length === 1 ? parseSpaceSlug(trimmed) : null;
6100
+ return slug ? {
6101
+ kind: "owned",
6102
+ slug
6103
+ } : null;
6104
+ }
6105
+ //#endregion
5030
6106
  //#region src/app-grant-cache.ts
5031
6107
  const STORAGE_PREFIX = "cohub:work-grants";
5032
6108
  const CACHE_VERSION = 1;
@@ -5236,6 +6312,7 @@ const AUTHORIZE_TIMEOUT_MS = 3e4;
5236
6312
  const MAX_SILENT_SPACE_ATTEMPTS = 4;
5237
6313
  const timeoutSignal = (ms) => typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(ms) : void 0;
5238
6314
  const spaceListSignal = () => timeoutSignal(SPACE_LIST_TIMEOUT_MS);
6315
+ const SPACE_LIST_PAGE_SIZE = 100;
5239
6316
  var AppAuthorizationError = class extends Error {
5240
6317
  status;
5241
6318
  code;
@@ -5848,58 +6925,67 @@ function createAppBridgeCore(config) {
5848
6925
  ...typeof value.isPinned === "boolean" ? { isPinned: value.isPinned } : {}
5849
6926
  };
5850
6927
  }
5851
- /**
5852
- * Lists the viewer's spaces for the consent dialog picker. Fetched by the
5853
- * host with the viewer's own token — the app only ever learns the space
5854
- * the viewer picks, never the list.
5855
- */
5856
- async function listViewerSpaces() {
5857
- const request = async (forceRefresh = false) => {
6928
+ async function viewerFetch(path, init = {}) {
6929
+ const send = async (forceRefresh) => {
5858
6930
  const userToken = await getAccessToken({ forceRefresh });
5859
- if (!userToken) {
5860
- await startSignIn();
5861
- throw new AppLoginRedirect();
5862
- }
5863
- return fetch(`${apiOrigin}/api/spaces`, {
5864
- headers: { Authorization: `Bearer ${userToken}` },
6931
+ if (!userToken) return null;
6932
+ return fetch(`${apiOrigin}${path}`, {
6933
+ ...init,
6934
+ headers: {
6935
+ ...init.headers,
6936
+ Authorization: `Bearer ${userToken}`
6937
+ },
5865
6938
  signal: spaceListSignal()
5866
6939
  });
5867
6940
  };
6941
+ const response = await send(false);
6942
+ return response?.status === 401 ? send(true) : response;
6943
+ }
6944
+ /** Owned or joined Spaces only; a public Space the viewer can merely view is not theirs to grant. */
6945
+ async function lookupViewerSpace(spaceId) {
6946
+ const response = await viewerFetch(`/api/spaces/${encodeURIComponent(spaceId)}`);
6947
+ if (response?.status === 403 || response?.status === 404) return null;
6948
+ if (!response?.ok) throw new Error("Space lookup failed.");
6949
+ const space = await response.json();
6950
+ return space?.relation === "owner" || space?.relation === "member" ? toSpaceOption(space) : null;
6951
+ }
6952
+ /** The first page seeds the picker; named Spaces are looked up one by one. */
6953
+ async function listViewerSpaces(requested, hints) {
5868
6954
  try {
5869
- let response = await request();
5870
- if (response?.status === 401) response = await request(true);
5871
- if (!response?.ok) return null;
5872
- const spaces = await response.json();
5873
- if (!Array.isArray(spaces)) return null;
5874
- return spaces.flatMap((space) => {
6955
+ const response = await viewerFetch(`/api/spaces?limit=${SPACE_LIST_PAGE_SIZE}`);
6956
+ if (!response) {
6957
+ await startSignIn();
6958
+ throw new AppLoginRedirect();
6959
+ }
6960
+ if (!response.ok) return null;
6961
+ const body = await response.json();
6962
+ if (!Array.isArray(body?.items)) return null;
6963
+ const spaces = body.items.flatMap((space) => {
5875
6964
  const option = toSpaceOption(space);
5876
6965
  return option ? [option] : [];
5877
6966
  });
6967
+ const missing = [.../* @__PURE__ */ new Set([requested, ...hints])].filter((id) => Boolean(id) && !spaces.some((space) => space.id === id));
6968
+ const found = await Promise.all(missing.map((id) => id === requested ? lookupViewerSpace(id) : lookupViewerSpace(id).catch(() => null)));
6969
+ return [...spaces, ...found.filter((space) => space !== null)];
5878
6970
  } catch (error) {
5879
6971
  if (error instanceof AppLoginRedirect) throw error;
5880
6972
  return null;
5881
6973
  }
5882
6974
  }
5883
- /**
5884
- * Ensures the viewer has a Space to target when their accessible list is
5885
- * empty. `GET /api/spaces/default` returns their home Space, creating it on
5886
- * first use — the same path taken when they first enter Cohub.
5887
- */
5888
- async function ensureDefaultSpace() {
5889
- const request = async (forceRefresh = false) => {
5890
- const userToken = await getAccessToken({ forceRefresh });
5891
- if (!userToken) return null;
5892
- return fetch(`${apiOrigin}/api/spaces/default`, {
5893
- headers: { Authorization: `Bearer ${userToken}` },
5894
- signal: spaceListSignal()
5895
- });
5896
- };
6975
+ /** The viewer's own Home, created on first use when they have no Space. */
6976
+ async function ensureHomeSpace() {
5897
6977
  try {
5898
- let response = await request();
5899
- if (response?.status === 401) response = await request(true);
6978
+ const response = await viewerFetch("/api/me/spaces/home", {
6979
+ method: "POST",
6980
+ headers: {
6981
+ "Content-Type": "application/json",
6982
+ [COHUB_SOURCE_HEADER.via]: "app"
6983
+ },
6984
+ body: JSON.stringify({ appId: app.id })
6985
+ });
5900
6986
  if (!response?.ok) return null;
5901
- const payload = await response.json();
5902
- return payload?.space ? toSpaceOption(payload.space) : null;
6987
+ const space = await response.json();
6988
+ return space ? toSpaceOption(space) : null;
5903
6989
  } catch {
5904
6990
  return null;
5905
6991
  }
@@ -5917,6 +7003,11 @@ function createAppBridgeCore(config) {
5917
7003
  localStorage.setItem(pickedSpaceStorageKey, spaceId);
5918
7004
  } catch {}
5919
7005
  }
7006
+ function contextSpaceIds() {
7007
+ const invocation = activeInvocation !== void 0 ? activeInvocation : config.getInvocation?.() ?? config.invocation;
7008
+ const shell = config.getShell?.() ?? config.shell;
7009
+ return [invocation?.spaceId ?? void 0, shell?.space?.id ?? void 0];
7010
+ }
5920
7011
  /**
5921
7012
  * Resolves the default target for a Space-bound request to a Space the
5922
7013
  * viewer controls — preferring wherever the app was invoked, then the last
@@ -5924,19 +7015,14 @@ function createAppBridgeCore(config) {
5924
7015
  */
5925
7016
  function resolveDefaultSpaceId(candidates) {
5926
7017
  if (!candidates?.length) return void 0;
5927
- const accessible = (id) => id && candidates.some((space) => space.id === id) ? id : void 0;
5928
- const invocation = activeInvocation !== void 0 ? activeInvocation : config.getInvocation?.() ?? config.invocation;
5929
- const shell = config.getShell?.() ?? config.shell;
5930
- return accessible(invocation?.spaceId) ?? accessible(shell?.space?.id) ?? accessible(readLastPickedSpace()) ?? candidates[0]?.id;
7018
+ return [...contextSpaceIds(), readLastPickedSpace()].find((id) => id && candidates.some((space) => space.id === id)) ?? candidates[0]?.id;
5931
7019
  }
5932
7020
  /**
5933
7021
  * The Space the app is currently in, from trusted host context only (no
5934
7022
  * network). Used to attempt silent renewal before loading the Space list.
5935
7023
  */
5936
7024
  function resolveContextSpaceId() {
5937
- const invocation = activeInvocation !== void 0 ? activeInvocation : config.getInvocation?.() ?? config.invocation;
5938
- const shell = config.getShell?.() ?? config.shell;
5939
- return invocation?.spaceId ?? shell?.space?.id ?? void 0;
7025
+ return contextSpaceIds().find(Boolean);
5940
7026
  }
5941
7027
  /**
5942
7028
  * Creates the Space with the viewer's own token — the same `POST /api/spaces`
@@ -6288,10 +7374,10 @@ function createAppBridgeCore(config) {
6288
7374
  const preListTargets = !spaceLevel ? listGrantedAppSpacesForScopes(viewerUuid, app.id, scopes, app.spaceId) : spaceId === void 0 && !selectSpace && contextSpaceId && hasGrantedAppScopes(viewerUuid, app.id, scopes, contextSpaceId) ? [contextSpaceId] : [];
6289
7375
  if (!alwaysAsk && await attemptSilentRenewal(reserved, viewerUuid, scopes, preListTargets)) return;
6290
7376
  if (state.pendingAuth !== reserved) return;
6291
- let spaces = spaceLevel ? await listViewerSpaces() : void 0;
7377
+ let spaces = spaceLevel ? await listViewerSpaces(spaceId, [...contextSpaceIds(), lastPicked]) : void 0;
6292
7378
  if (state.pendingAuth !== reserved) return;
6293
7379
  if (spaces && spaces.length === 0) {
6294
- const fallback = await ensureDefaultSpace();
7380
+ const fallback = await ensureHomeSpace();
6295
7381
  if (state.pendingAuth !== reserved) return;
6296
7382
  if (fallback) spaces = [fallback];
6297
7383
  }
@@ -6402,9 +7488,14 @@ function createAppBridgeCore(config) {
6402
7488
  * Only a Space the host actually loaded may become the target, so a UI bug
6403
7489
  * cannot mint a grant against an arbitrary id.
6404
7490
  */
6405
- function setSelectedSpace(spaceId) {
6406
- if (!state.pendingAuth || state.authSaving || !state.canChangeSpace) return;
6407
- if (!state.pendingAuth.spaces?.some((space) => space.id === spaceId)) return;
7491
+ function setSelectedSpace(space) {
7492
+ const pending = state.pendingAuth;
7493
+ if (!pending || state.authSaving || !state.canChangeSpace || !pending.spaces) return;
7494
+ const spaceId = typeof space === "string" ? space : space.id;
7495
+ if (!pending.spaces.some((candidate) => candidate.id === spaceId)) {
7496
+ if (typeof space === "string") return;
7497
+ pending.spaces = [...pending.spaces, space];
7498
+ }
6408
7499
  if (state.selectedSpaceId === spaceId) return;
6409
7500
  state.selectedSpaceId = spaceId;
6410
7501
  notify();
@@ -6655,9 +7746,12 @@ const DEFAULT_MEDIA_TYPES = {
6655
7746
  audio: "audio/mpeg"
6656
7747
  };
6657
7748
  function outputType(value) {
6658
- return value === "image" || value === "video" || value === "audio" || value === "text" ? value : null;
7749
+ return value === "image" || value === "video" || value === "audio" ? value : null;
6659
7750
  }
6660
- /** Covers fold into the media they depict instead of listing as images. */
7751
+ /**
7752
+ * Only media blocks become outputs; text such as a revised prompt or a refusal
7753
+ * stays in the raw result. Covers fold into the media they depict.
7754
+ */
6661
7755
  function projectOutputs(run) {
6662
7756
  const blocks = generationOutput(run);
6663
7757
  const covers = generationCovers(blocks);
@@ -6670,20 +7764,17 @@ function projectOutputs(run) {
6670
7764
  const type = outputType(block.type);
6671
7765
  if (!type || coverIndexes.has(index)) return [];
6672
7766
  const size = blockNaturalSize(block);
6673
- const text = type === "text" ? blockText(block) ?? null : null;
6674
- if (type === "text" && !text) return [];
6675
7767
  return [{
6676
7768
  index,
6677
7769
  type,
6678
- url: type === "text" ? null : blockUrl(block) ?? null,
7770
+ url: blockUrl(block) ?? null,
6679
7771
  previewUrl: blockPreviewUrl(block) ?? coverUrl(index) ?? null,
6680
7772
  title: blockTitle(block) ?? null,
6681
7773
  mimeType: blockMimeType(block) ?? null,
6682
- text,
6683
7774
  width: size.naturalWidth ?? null,
6684
7775
  height: size.naturalHeight ?? null,
6685
7776
  durationMs: blockDurationMs(block) ?? null,
6686
- deferred: type !== "text" && isDeferred(block)
7777
+ deferred: isDeferred(block)
6687
7778
  }];
6688
7779
  });
6689
7780
  }
@@ -6723,249 +7814,4 @@ function generationOutputSource(result, outputIndex) {
6723
7814
  return `data:${declared && MEDIA_TYPE_PATTERN.test(declared) ? declared : DEFAULT_MEDIA_TYPES[String(block.type)] ?? "application/octet-stream"};base64,${data}`;
6724
7815
  }
6725
7816
  //#endregion
6726
- //#region src/board/animation.ts
6727
- function track(input) {
6728
- return BoardTrackSchema.parse(input);
6729
- }
6730
- function proceduralClip(input) {
6731
- return BoardProceduralClipSchema.parse({
6732
- ...input,
6733
- kindVersion: input.kindVersion ?? 1
6734
- });
6735
- }
6736
- function composition(input) {
6737
- return BoardCompositionSchema.parse(input);
6738
- }
6739
- function compileComposition(input) {
6740
- return composition({
6741
- id: input.id,
6742
- name: input.name,
6743
- timeline: {
6744
- duration: input.duration,
6745
- tracks: (input.tracks ?? []).map(track),
6746
- clips: (input.clips ?? []).map(proceduralClip),
6747
- markers: input.markers ?? []
6748
- },
6749
- playback: input.playback ?? {
6750
- loop: false,
6751
- endBehavior: "hold",
6752
- reducedMotion: { mode: "base" }
6753
- },
6754
- metadata: input.metadata ?? {},
6755
- revision: 0
6756
- });
6757
- }
6758
- const clamp01 = (value) => Math.max(0, Math.min(1, value));
6759
- function sampleEasing(easing, value) {
6760
- const t = clamp01(value);
6761
- switch (easing) {
6762
- case "ease-in-quad": return t * t;
6763
- case "ease-out-quad": return 1 - (1 - t) ** 2;
6764
- case "ease-in-out-quad": return t < .5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
6765
- case "ease-in-cubic": return t ** 3;
6766
- case "ease-out-cubic": return 1 - (1 - t) ** 3;
6767
- case "ease-in-out-cubic": return t < .5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2;
6768
- case "ease-out-quart": return 1 - (1 - t) ** 4;
6769
- case "ease-out-expo": return t === 1 ? 1 : 1 - 2 ** (-10 * t);
6770
- default: return t;
6771
- }
6772
- }
6773
- function interpolate(left, right, progress) {
6774
- if (typeof left === "number" && typeof right === "number") return left + (right - left) * progress;
6775
- if (left && right && typeof left === "object" && typeof right === "object" && !Array.isArray(left) && !Array.isArray(right)) {
6776
- const a = left;
6777
- const b = right;
6778
- const result = {};
6779
- for (const key of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) result[key] = interpolate(a[key], b[key], progress);
6780
- return result;
6781
- }
6782
- return progress < 1 ? left : right;
6783
- }
6784
- /** Deterministically sample one validated Track without touching scene state. */
6785
- function sampleTrack(trackValue, time) {
6786
- const frames = trackValue.keyframes;
6787
- const first = frames[0];
6788
- const last = frames.at(-1);
6789
- if (!first || !last) return null;
6790
- if (time < first.time) {
6791
- if (trackValue.fill !== "backwards" && trackValue.fill !== "both") return null;
6792
- return {
6793
- target: trackValue.target,
6794
- channel: trackValue.channel,
6795
- value: first.value
6796
- };
6797
- }
6798
- if (time > last.time) {
6799
- if (trackValue.fill !== "forwards" && trackValue.fill !== "both") return null;
6800
- return {
6801
- target: trackValue.target,
6802
- channel: trackValue.channel,
6803
- value: last.value
6804
- };
6805
- }
6806
- let low = 0;
6807
- let high = frames.length;
6808
- while (low < high) {
6809
- const middle = low + high >>> 1;
6810
- if ((frames[middle]?.time ?? Number.POSITIVE_INFINITY) <= time) low = middle + 1;
6811
- else high = middle;
6812
- }
6813
- const left = frames[Math.max(0, low - 1)] ?? first;
6814
- const right = frames[Math.min(frames.length - 1, low)] ?? last;
6815
- if (left === right || trackValue.interpolation === "step") return {
6816
- target: trackValue.target,
6817
- channel: trackValue.channel,
6818
- value: left.value
6819
- };
6820
- const span = Math.max(Number.EPSILON, right.time - left.time);
6821
- const progress = sampleEasing(right.easing ?? "linear", (time - left.time) / span);
6822
- return {
6823
- target: trackValue.target,
6824
- channel: trackValue.channel,
6825
- value: interpolate(left.value, right.value, progress)
6826
- };
6827
- }
6828
- function sampleCompositionTracks(value, time) {
6829
- const position = Math.max(0, Math.min(value.timeline.duration, time));
6830
- return value.timeline.tracks.flatMap((item) => {
6831
- const sampled = sampleTrack(item, position);
6832
- return sampled ? [sampled] : [];
6833
- });
6834
- }
6835
- const ZERO_COST = {
6836
- particles: 0,
6837
- vertices: 0,
6838
- dynamicVertices: 0,
6839
- drawCalls: 0,
6840
- filterPasses: 0,
6841
- renderTexturePixels: 0,
6842
- textureBytes: 0,
6843
- bufferBytes: 0,
6844
- simulationSteps: 0
6845
- };
6846
- const DEFAULT_BOARD_LIMITS = DEFAULT_BOARD_RENDER_LIMITS;
6847
- function builtinDefinition(capability) {
6848
- return {
6849
- ...capability,
6850
- validate() {
6851
- return [];
6852
- },
6853
- estimateCost(params) {
6854
- return estimateBuiltinBoardClipCost({
6855
- kind: capability.id,
6856
- params
6857
- });
6858
- }
6859
- };
6860
- }
6861
- var BoardExtensionRegistry = class {
6862
- #extensions = /* @__PURE__ */ new Map();
6863
- #presets = /* @__PURE__ */ new Map();
6864
- constructor() {
6865
- for (const capability of BOARD_BUILTIN_CAPABILITIES) this.register(builtinDefinition(capability));
6866
- }
6867
- register(definition) {
6868
- const key = `${definition.kind}:${definition.id}@${definition.version}`;
6869
- const target = definition.kind === "preset" ? this.#presets : this.#extensions;
6870
- if (target.has(key)) throw new Error(`Board extension is already registered: ${key}`);
6871
- target.set(key, definition);
6872
- return this;
6873
- }
6874
- capabilities() {
6875
- return [...this.#extensions.values(), ...this.#presets.values()].map((definition) => ({
6876
- kind: definition.kind,
6877
- id: definition.id,
6878
- version: definition.version,
6879
- ...definition.digest ? { digest: definition.digest } : {},
6880
- ...definition.renderers ? { renderers: definition.renderers } : {},
6881
- ...definition.fallbackId ? { fallbackId: definition.fallbackId } : {},
6882
- ...definition.schema ? { schema: definition.schema } : {}
6883
- }));
6884
- }
6885
- compilePreset(id, version, params) {
6886
- const preset = this.#presets.get(`preset:${id}@${version}`);
6887
- if (!preset) throw new Error(`Unknown Board preset: ${id}@${version}`);
6888
- return preset.compile(params);
6889
- }
6890
- validate(input) {
6891
- const parsed = BoardCompositionSchema.safeParse(input.composition);
6892
- if (!parsed.success) return {
6893
- valid: false,
6894
- diagnostics: parsed.error.issues.map((issue) => ({
6895
- severity: "error",
6896
- code: "INVALID_COMPOSITION",
6897
- message: issue.message,
6898
- path: issue.path.join(".")
6899
- })),
6900
- peakCost: { ...ZERO_COST }
6901
- };
6902
- const diagnostics = [];
6903
- const composition = parsed.data;
6904
- const cost = { ...ZERO_COST };
6905
- for (const effect of input.effects ?? []) {
6906
- diagnostics.push(...validateBuiltinBoardEffect(effect, `effects.${effect.id}`));
6907
- if (!this.#extensions.has(`effect:${effect.kind}@${effect.kindVersion}`)) diagnostics.push({
6908
- severity: "warning",
6909
- code: "UNKNOWN_EFFECT",
6910
- message: `No renderer is registered for ${effect.kind}@${effect.kindVersion}`,
6911
- path: `effects.${effect.id}`
6912
- });
6913
- }
6914
- const events = [];
6915
- for (const clip of composition.timeline.clips) {
6916
- const definition = this.#extensions.get(`clip:${clip.kind}@${clip.kindVersion}`);
6917
- if (!definition) {
6918
- diagnostics.push({
6919
- severity: "warning",
6920
- code: "UNKNOWN_CLIP",
6921
- message: `No renderer is registered for ${clip.kind}@${clip.kindVersion}`
6922
- });
6923
- continue;
6924
- }
6925
- diagnostics.push(...validateBuiltinBoardClip(clip, `composition.timeline.clips.${clip.id}`));
6926
- diagnostics.push(...definition.validate?.(clip.params) ?? []);
6927
- const estimate = definition.estimateCost(clip.params, input.profile ?? "high");
6928
- const clipCost = { ...ZERO_COST };
6929
- for (const key of Object.keys(clipCost)) clipCost[key] = estimate[key] ?? 0;
6930
- events.push({
6931
- at: clip.start,
6932
- direction: 1,
6933
- cost: clipCost
6934
- });
6935
- events.push({
6936
- at: clip.start + clip.duration,
6937
- direction: -1,
6938
- cost: clipCost
6939
- });
6940
- }
6941
- events.sort((left, right) => left.at - right.at || left.direction - right.direction);
6942
- const active = { ...ZERO_COST };
6943
- for (const event of events) for (const key of Object.keys(active)) {
6944
- active[key] += event.cost[key] * event.direction;
6945
- cost[key] = Math.max(cost[key], active[key]);
6946
- }
6947
- for (const effect of input.effects ?? []) {
6948
- const estimate = estimateBuiltinBoardEffectCost(effect);
6949
- for (const key of Object.keys(cost)) cost[key] += estimate[key] ?? 0;
6950
- }
6951
- const limits = input.limits ?? DEFAULT_BOARD_LIMITS;
6952
- for (const key of Object.keys(cost)) if (cost[key] > limits[key]) diagnostics.push({
6953
- severity: "warning",
6954
- code: "RENDER_BUDGET_EXCEEDED",
6955
- message: `${key} peaks at ${cost[key]}, above ${limits[key]}`,
6956
- path: key,
6957
- adaptation: { quality: "lower" }
6958
- });
6959
- return {
6960
- valid: !diagnostics.some((item) => item.severity === "error"),
6961
- diagnostics,
6962
- peakCost: cost
6963
- };
6964
- }
6965
- };
6966
- function createBoardExtensionRegistry() {
6967
- return new BoardExtensionRegistry();
6968
- }
6969
- const BOARD_CHANNELS = BOARD_ANIMATION_CHANNELS;
6970
- //#endregion
6971
- export { APP_APPEARANCE_TOKENS, APP_COMPOSER_CHIP_CONTENT_MAX_BYTES, APP_COMPOSER_CHIP_KEY_MAX_LENGTH, APP_COMPOSER_CHIP_LABEL_MAX_LENGTH, APP_SURFACE_READY_TIMEOUT_MS, APP_SURFACE_REQUEST_TIMEOUT_MS, AppCommerceApi, AppRealtimeApi, AppRefParseError, AppRoom, AppRuntimeApi, AppRuntimeApi as WorkRuntimeApi, AppRuntimeError, AppSurfaceApi, AppWindowApi, AppsApi, BILLING_ACCESS_BLOCKED_ERROR_CODE, BOARD_ANIMATION_CHANNEL_CAPABILITIES, BOARD_CHANNELS, BOARD_COLOR_IDS, BOARD_GEO_KINDS, BillingApi, BoardAnimationSpecSchema, BoardAuthoringItemSchema, BoardClient, BoardCompositionInputSchema, BoardCompositionSchema, BoardDealParamsSchema, BoardEffectInputSchema, BoardEffectSchema, BoardExtensionRegistry, BoardItemPatchSchema, BoardPlaybackPolicySchema, BoardSemanticCommandSchema, COHUB_ENVIRONMENTS, COHUB_SOURCE_HEADER, COHUB_SOURCE_HEADER_NAMES, CohubClient, CohubHttpClient, DEFAULT_BOARD_LIMITS, DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, DESKTOP_COMMAND_MAX_TIMEOUT_MS, DESKTOP_COMMAND_PAYLOAD_MAX_BYTES, DESKTOP_COMMAND_PENDING_TTL_SECONDS, DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS, DESKTOP_COMMAND_TERMINAL_TTL_SECONDS, DESKTOP_COMMAND_VERSION, DesktopCommandsApi, FEATURE_NOT_ENTITLED_ERROR_CODE, GenerationPolicyError, HttpError, NATIVE_INGEST_MAX_TURNS, NATIVE_SYNC_SOURCE, OriginBrokerTransport, PERMISSIONS, ParentBridgeTransport, PopupBrokerTransport, REQUEST_SOURCE_VIA_MAX_LENGTH, RUNTIME_ARCHIVE_SEGMENT_BYTES, RUNTIME_MAX_FRAME_BYTES, RUNTIME_PROTOCOL_VERSION, RUNTIME_RECOVERY_BATCH_SIZE, ReferencesApi, ReferralsApi, SETTLED_TURN_STATUSES, SessionGenerationStreamClient, SessionPatchReducer, SpacePublicFilesApi, SpaceWebhooksApi, USER_LEVEL_PERMISSIONS, UiCommandsApi, UsersApi, VoiceApi, VoiceInputClient, WebsocketClient, WorkCommerceApi, WorkRealtimeApi, WorkRefParseError, WorkRoom, WorkSurfaceApi, appAppearanceVar, applyAppAppearance, assertGenerationRequestAllowedByPolicy, attachAppEmbed, buildAppSurfaceRequest, buildSpaceInvitePath, buildSpacePath, clearGrantedAppScopes, compileComposition, composition, contextToPiMessages, createAppBridgeCore, createAppBridgeCore as createWorkBridgeCore, createAppRuntime, createAppRuntime as createWorkRuntime, createBoardExtensionRegistry, createCohubClient, createHttpClient, createOriginAppResolver, createSessionGenerationStreamClient, createSessionPatchReducer, createSlugAppIdResolver, createSlugAppIdResolver as createSlugWorkIdResolver, createVoiceInputClient, createWebsocketClient, decodeGenerationPolicy, encodeGenerationPolicy, extractBillingPayload, filterDiscoverableGenerationModels, filterGenerationDeclarationsByPolicy, filterSpacePickerItems, findGenerationModelPolicy, fingerprintProjectionTurns, formatAppRef, formatWorkRef, generationOutputSource, getAllowedGenerationModelIds, getCohubContext, harnessArchiveIndexSchema, hasGrantedAppScopes, hasRequestSourceIdentity, isActiveGenerationTask, isAppId, isBillingAccessBlockedCode, isBillingAccessBlockedError, isDesktopCallMethod, isFeatureNotEntitledError, isGenerationModelHidden, isHttpErrorCode, isLocalHarness, isNativeClientTurn, isProjectionCompaction, isRequestSourceClientId, isRequestSourceEmpty, isRequestSourceUuid, isTerminalDesktopCommandStatus, isUserLevelPermission, isUuid, isWorkId, joinApiUrl, matchesUnauthorizedErrorToken, mergeRequestSourceIntoMeta, nativeIngestTurnSchema, nativeRuntimeEventSchema, nativeTurnCompleteSchema, nativeTurnProgressSchema, normalizeBaseUrl, normalizeGenerationPolicy, normalizeRequestSource, normalizeSpacePickerQuery, normalizeVoiceInputWebsocketUrl, normalizeWebsocketUrl, orderSpacePickerItems, parseAppRef, parseAppSurfaceReady, parseAppSurfaceResponse, parseAssistantMessageCommit, parseBoardCompositionInput, parseBoardEffectInput, parseBoardPlaybackPolicy, parseDesktopCommand, parseGenerationPolicyFromEnv, parseRequestSourceFromHeaders, parseWorkRef, proceduralClip, projectNativeMessageMeta, projectNativeSession, readRequestSourceFromEnv, requestSourceToHeaders, resolveApiBaseUrl, resolveAppTransport, resolveAppTransport as resolveWorkTransport, resolveCohubEnvironment, resolveExecutionAppId, resolveExecutionToken, resolveHarness, resolveOpenSurface, resolveRequestSourceChannel, resolveVoiceInputWebsocketUrl, resolveWebBaseUrl, resolveWebsocketUrl, runtimeCommandSchema, runtimeEventSchema, runtimeNativeStopSchema, runtimeReadySchema, sampleCompositionTracks, sampleEasing, sampleTrack, sanitizeAccessToken, scopeListHasPermission, selectRuntimeContextMessages, selectSpacePickerItems, serializeProjection, serializeProjectionRecords, setGrantedAppScopes, toGenerationTaskView, track, trimProjectionTurnsToCompaction, validateArchiveBoundary };
7817
+ export { APP_APPEARANCE_TOKENS, APP_COMPOSER_CHIP_CONTENT_MAX_BYTES, APP_COMPOSER_CHIP_KEY_MAX_LENGTH, APP_COMPOSER_CHIP_LABEL_MAX_LENGTH, APP_SURFACE_READY_TIMEOUT_MS, APP_SURFACE_REQUEST_TIMEOUT_MS, AppCommerceApi, AppRealtimeApi, AppRefParseError, AppRoom, AppRuntimeApi, AppRuntimeApi as WorkRuntimeApi, AppRuntimeError, AppSurfaceApi, AppWindowApi, AppsApi, BILLING_ACCESS_BLOCKED_ERROR_CODE, BOARD_COLOR_TOKENS, BOARD_ITEM_TYPES, BillingApi, BoardAnimationSchema, BoardClient, BoardPatchSchema, BoardTrackSchema, COHUB_ENVIRONMENTS, COHUB_SOURCE_HEADER, COHUB_SOURCE_HEADER_NAMES, CohubClient, CohubHttpClient, DESKTOP_COMMAND_ACCEPT_TIMEOUT_MS, DESKTOP_COMMAND_DEFAULT_TIMEOUT_MS, DESKTOP_COMMAND_MAX_TIMEOUT_MS, DESKTOP_COMMAND_OPEN_TIMEOUT_MS, DESKTOP_COMMAND_PAYLOAD_MAX_BYTES, DESKTOP_COMMAND_PENDING_TTL_SECONDS, DESKTOP_COMMAND_SETTLEMENT_GRACE_SECONDS, DESKTOP_COMMAND_TERMINAL_TTL_SECONDS, DESKTOP_COMMAND_VERSION, DISPLAY_ACTION_TYPES, DISPLAY_ELEMENT_REF_PATTERN, DISPLAY_SYSTEM_ACTIONS, DesktopCommandsApi, DisplayActionError, DisplayConnection, FEATURE_NOT_ENTITLED_ERROR_CODE, GenerationPolicyError, HOME_SPACE_SLUG, HttpError, NATIVE_INGEST_MAX_TURNS, NATIVE_SYNC_SOURCE, OriginBrokerTransport, PERMISSIONS, ParentBridgeTransport, PopupBrokerTransport, REQUEST_SOURCE_VIA_MAX_LENGTH, RUNTIME_ARCHIVE_SEGMENT_BYTES, RUNTIME_MAX_FRAME_BYTES, RUNTIME_PROTOCOL_VERSION, RUNTIME_RECOVERY_BATCH_SIZE, ReferencesApi, ReferralsApi, SETTLED_TURN_STATUSES, SessionGenerationStreamClient, SessionPatchReducer, SpaceDisplaysApi, SpacePublicFilesApi, SpaceWebhooksApi, UPLOAD_MAX_BATCH_BYTES, UPLOAD_MAX_BATCH_FILES, UPLOAD_MAX_FILE_BYTES, USER_LEVEL_PERMISSIONS, UiCommandsApi, UsersApi, VoiceApi, VoiceInputClient, WebsocketClient, WorkCommerceApi, WorkRealtimeApi, WorkRefParseError, WorkRoom, WorkSurfaceApi, appAppearanceVar, applyAppAppearance, assertGenerationRequestAllowedByPolicy, attachAppEmbed, buildAppSurfaceRequest, buildSpaceInvitePath, buildSpacePath, clearGrantedAppScopes, compileDisplayActions, connectDisplay, contextToPiMessages, createAppBridgeCore, createAppBridgeCore as createWorkBridgeCore, createAppRuntime, createAppRuntime as createWorkRuntime, createCohubClient, createHttpClient, createOriginAppResolver, createSessionGenerationStreamClient, createSessionPatchReducer, createSlugAppIdResolver, createSlugAppIdResolver as createSlugWorkIdResolver, createVoiceInputClient, createWebsocketClient, decodeGenerationPolicy, defaultDesktopCommandTimeoutMs, encodeGenerationPolicy, extractBillingPayload, filterDiscoverableGenerationModels, filterGenerationDeclarationsByPolicy, filterSpacePickerItems, findGenerationModelPolicy, fingerprintProjectionTurns, formatAppRef, formatWorkRef, generationOutputSource, getAllowedGenerationModelIds, getCohubContext, harnessArchiveIndexSchema, hasGrantedAppScopes, hasRequestSourceIdentity, isActiveGenerationTask, isAppId, isBillingAccessBlockedCode, isBillingAccessBlockedError, isDesktopCallMethod, isFeatureNotEntitledError, isGenerationModelHidden, isHttpErrorCode, isLocalHarness, isNativeClientTurn, isProjectionCompaction, isRequestSourceClientId, isRequestSourceEmpty, isRequestSourceUuid, isTerminalDesktopCommandStatus, isUserLevelPermission, isUuid, isWorkId, joinApiUrl, matchesUnauthorizedErrorToken, mergeRequestSourceIntoMeta, nativeIngestTurnSchema, nativeRuntimeEventSchema, nativeTurnCompleteSchema, nativeTurnProgressSchema, normalizeBaseUrl, normalizeDisplayPoint, normalizeGenerationPolicy, normalizeRequestSource, normalizeSessionTurnOrigin, normalizeSpacePickerQuery, normalizeVoiceInputWebsocketUrl, normalizeWebsocketUrl, orderSpacePickerItems, parseAppRef, parseAppSurfaceReady, parseAppSurfaceResponse, parseAssistantMessageCommit, parseBoardDocument, parseBoardItem, parseDesktopCommand, parseGenerationPolicyFromEnv, parseRequestSourceFromHeaders, parseSpaceRef, parseWorkRef, projectNativeMessageMeta, projectNativeSession, readRequestSourceFromEnv, readSentTurns, readSessionTurnOrigin, requestSourceToHeaders, resolveApiBaseUrl, resolveAppTransport, resolveAppTransport as resolveWorkTransport, resolveCohubEnvironment, resolveExecutionAppId, resolveExecutionToken, resolveHarness, resolveOpenSurface, resolveRequestSourceChannel, resolveVoiceInputWebsocketUrl, resolveWebBaseUrl, resolveWebsocketUrl, runtimeCommandSchema, runtimeEventSchema, runtimeNativeStopSchema, runtimeReadySchema, sanitizeAccessToken, scopeListHasPermission, selectRuntimeContextMessages, selectSpacePickerItems, serializeProjection, serializeProjectionRecords, setGrantedAppScopes, toGenerationTaskView, trimProjectionTurnsToCompaction, validateArchiveBoundary };