@volter/editor-game 0.5.65

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 (302) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +23 -0
  3. package/contributions/asset-budget-asset.menu.ts +26 -0
  4. package/contributions/asset-budget.action.ts +18 -0
  5. package/contributions/asset-budget.document.tsx +21 -0
  6. package/contributions/asset-budget.menu.ts +22 -0
  7. package/contributions/audio-unlock.service.ts +17 -0
  8. package/contributions/audio.utility.tsx +15 -0
  9. package/contributions/autoplay.service.ts +48 -0
  10. package/contributions/bridge.command.ts +172 -0
  11. package/contributions/build-profiles.document.tsx +21 -0
  12. package/contributions/build-progress.status.tsx +45 -0
  13. package/contributions/build.action.ts +26 -0
  14. package/contributions/build.command.ts +27 -0
  15. package/contributions/build.header.tsx +37 -0
  16. package/contributions/build.menu.ts +31 -0
  17. package/contributions/build.service.ts +31 -0
  18. package/contributions/connection.status.tsx +43 -0
  19. package/contributions/coverage.service.ts +118 -0
  20. package/contributions/edit-mode-audio.service.ts +23 -0
  21. package/contributions/edit-mode-networking.service.ts +30 -0
  22. package/contributions/game-document.service.ts +25 -0
  23. package/contributions/game-eval.command.ts +210 -0
  24. package/contributions/game.layout.ts +19 -0
  25. package/contributions/gameplay.command.ts +255 -0
  26. package/contributions/generation.service.ts +96 -0
  27. package/contributions/generations.status.tsx +53 -0
  28. package/contributions/ingest.service.ts +80 -0
  29. package/contributions/instances.command.ts +75 -0
  30. package/contributions/navmesh.menu.ts +39 -0
  31. package/contributions/navmesh.service.ts +19 -0
  32. package/contributions/network.utility.tsx +17 -0
  33. package/contributions/play.command.ts +355 -0
  34. package/contributions/profiler.action.ts +16 -0
  35. package/contributions/profiler.menu.ts +22 -0
  36. package/contributions/profiler.utility.tsx +17 -0
  37. package/contributions/react/component-board.service.ts +21 -0
  38. package/contributions/react/design-time-mount.service.ts +60 -0
  39. package/contributions/react/pasteboard.action.ts +41 -0
  40. package/contributions/react/react-inspector.service.ts +85 -0
  41. package/contributions/react/story-documents.service.ts +44 -0
  42. package/contributions/scene-document.service.ts +32 -0
  43. package/contributions/state-watch.action.ts +17 -0
  44. package/contributions/state-watch.menu.ts +23 -0
  45. package/contributions/state-watch.utility.tsx +20 -0
  46. package/contributions/team-playtest.service.ts +124 -0
  47. package/contributions/three/camera-runtime.inspector.tsx +34 -0
  48. package/contributions/three/component-board.service.ts +24 -0
  49. package/contributions/three/component-verbs.command.ts +110 -0
  50. package/contributions/three/component-verbs.service.ts +92 -0
  51. package/contributions/three/constraints.inspector.tsx +34 -0
  52. package/contributions/three/model-asset-sections.service.ts +26 -0
  53. package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
  54. package/contributions/three/story-documents.service.ts +31 -0
  55. package/contributions/three/three-authoring.service.ts +66 -0
  56. package/contributions/transport.header.tsx +19 -0
  57. package/contributions/xstate-behavior.action.ts +42 -0
  58. package/contributions/xstate-behavior.document.tsx +73 -0
  59. package/contributions/xstate-behavior.inspector.tsx +28 -0
  60. package/contributions/xstate-behavior.menu.ts +23 -0
  61. package/package.json +144 -0
  62. package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
  63. package/src/asset-budget/asset-budget-model.ts +799 -0
  64. package/src/asset-budget/basis-encoder.ts +165 -0
  65. package/src/asset-budget/gltf-io.ts +154 -0
  66. package/src/asset-budget/gltf-optimize.ts +384 -0
  67. package/src/asset-budget/image-dims.ts +78 -0
  68. package/src/asset-budget/optimize-apply.ts +221 -0
  69. package/src/audio/AudioDebuggerPanel.tsx +457 -0
  70. package/src/audio/audio-debugger-model.ts +87 -0
  71. package/src/bridge/call.ts +60 -0
  72. package/src/bridge/dispatch.ts +459 -0
  73. package/src/bridge/live-frames.ts +25 -0
  74. package/src/bridge/screenshot.ts +316 -0
  75. package/src/build/BuildProfilesPanel.tsx +476 -0
  76. package/src/build/build-session.ts +247 -0
  77. package/src/build/format-bytes.ts +14 -0
  78. package/src/command-results.ts +36 -0
  79. package/src/coverage/live-authoring-surface.ts +30 -0
  80. package/src/coverage/live-project-verbs.ts +162 -0
  81. package/src/coverage/native-system-coverage.ts +143 -0
  82. package/src/coverage/root-coverage.ts +79 -0
  83. package/src/coverage/session-coverage.ts +193 -0
  84. package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
  85. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
  86. package/src/edit-mode/edit-mode-audio.ts +56 -0
  87. package/src/edit-mode/edit-mode-networking.ts +112 -0
  88. package/src/game-document/DevicePresetPicker.tsx +84 -0
  89. package/src/game-document/GameCaptureFrameButton.tsx +45 -0
  90. package/src/game-document/GameDocument.tsx +215 -0
  91. package/src/game-document/GamePanel.tsx +662 -0
  92. package/src/game-document/InstanceInspectorPicker.tsx +183 -0
  93. package/src/game-document/crowd-debug.ts +183 -0
  94. package/src/game-document/device-preview.ts +336 -0
  95. package/src/game-document/game-view-store.ts +107 -0
  96. package/src/game-document/physics-debug.ts +187 -0
  97. package/src/generation/GenerationActivity.tsx +421 -0
  98. package/src/generation/GenerationGallery.css +231 -0
  99. package/src/generation/generation-documents.tsx +338 -0
  100. package/src/generation/generation-jobs.ts +128 -0
  101. package/src/generation/generation-presentation.ts +257 -0
  102. package/src/host/adapter-reach.ts +445 -0
  103. package/src/host/adapter-runtime-bindings.ts +303 -0
  104. package/src/host/after-paint.ts +112 -0
  105. package/src/host/api/configurations.ts +66 -0
  106. package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
  107. package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
  108. package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
  109. package/src/host/authoring/contract-scenes-stories.ts +204 -0
  110. package/src/host/authoring/creation-site-related.ts +55 -0
  111. package/src/host/authoring/ephemeral-persistence.ts +29 -0
  112. package/src/host/authoring/gesture-persist.ts +84 -0
  113. package/src/host/authoring/ingest-data-writer.ts +232 -0
  114. package/src/host/authoring/ingest-source-persistence.ts +826 -0
  115. package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
  116. package/src/host/authoring/mounted-authoring.ts +41 -0
  117. package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
  118. package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
  119. package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
  120. package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
  121. package/src/host/authoring/pixi-isolation-assets.ts +25 -0
  122. package/src/host/authoring/pixi-live-write-target.ts +979 -0
  123. package/src/host/authoring/pixi-source-identity.ts +141 -0
  124. package/src/host/authoring/pixi-still-presentation.ts +71 -0
  125. package/src/host/authoring/pixi-structure-history.ts +237 -0
  126. package/src/host/authoring/pixi-transform-channels.ts +205 -0
  127. package/src/host/authoring/selection-remount-handoff.ts +23 -0
  128. package/src/host/authoring/source-persistence-backend.ts +373 -0
  129. package/src/host/authoring/source-refresh-revisions.ts +81 -0
  130. package/src/host/authoring/struct-write-pipe.ts +143 -0
  131. package/src/host/auto-frame-window.ts +89 -0
  132. package/src/host/binding-resolver.ts +393 -0
  133. package/src/host/browser-transpile.ts +631 -0
  134. package/src/host/canvas-entry-runtime.ts +95 -0
  135. package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
  136. package/src/host/components/HeaderTelemetry.tsx +341 -0
  137. package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
  138. package/src/host/components/ResolutionPicker.tsx +89 -0
  139. package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
  140. package/src/host/components/frame-debugger-model.ts +579 -0
  141. package/src/host/components/header-telemetry-model.ts +74 -0
  142. package/src/host/components/scene-document.tsx +447 -0
  143. package/src/host/components/utility-view-state.ts +87 -0
  144. package/src/host/components/world-root-stage-binding.tsx +133 -0
  145. package/src/host/components/world-root-stage.ts +1046 -0
  146. package/src/host/coverage/authoring-read-probe.ts +583 -0
  147. package/src/host/coverage/capability-coverage.ts +1587 -0
  148. package/src/host/coverage/coverage-accounting.ts +261 -0
  149. package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
  150. package/src/host/coverage/project-verb-coverage.ts +148 -0
  151. package/src/host/coverage/system-adapter-coverage.ts +373 -0
  152. package/src/host/design-system-stories/StoryLayout.tsx +104 -0
  153. package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
  154. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
  155. package/src/host/document-preview-three.ts +110 -0
  156. package/src/host/entry-adjudication.ts +89 -0
  157. package/src/host/game-location-guard.ts +150 -0
  158. package/src/host/game-module-access.ts +196 -0
  159. package/src/host/game-realm-page.ts +358 -0
  160. package/src/host/game-realm-reclaim.ts +55 -0
  161. package/src/host/game-realm-storage.ts +104 -0
  162. package/src/host/gameplay-export.ts +288 -0
  163. package/src/host/gameplay-recording.ts +717 -0
  164. package/src/host/gated-globals.ts +1511 -0
  165. package/src/host/history/json-history-resource.ts +254 -0
  166. package/src/host/ingest/registry.ts +261 -0
  167. package/src/host/instance-extract-actions.ts +120 -0
  168. package/src/host/instance-fork-actions.ts +120 -0
  169. package/src/host/play-control-hook.ts +26 -0
  170. package/src/host/playwright-shim.ts +479 -0
  171. package/src/host/projection/dom.ts +295 -0
  172. package/src/host/projection/pixi.ts +288 -0
  173. package/src/host/r3f-entry-runtime.ts +77 -0
  174. package/src/host/react-mount-runtime.ts +162 -0
  175. package/src/host/realm-services.ts +148 -0
  176. package/src/host/recording-preview.ts +106 -0
  177. package/src/host/roots/module-root.ts +203 -0
  178. package/src/host/roots/react-root.ts +181 -0
  179. package/src/host/same-realm-loop-gate.ts +544 -0
  180. package/src/host/scene-view-drawability.ts +69 -0
  181. package/src/host/sdk/tools.ts +31 -0
  182. package/src/host/served-bundle-runtime-modules.ts +331 -0
  183. package/src/host/server-log-bridge.ts +60 -0
  184. package/src/host/staged-projects.ts +24 -0
  185. package/src/host/stories/mounted-story-viewport-source.ts +64 -0
  186. package/src/host/stories/story-arg-descriptors.ts +50 -0
  187. package/src/host/stories/story-media-presence.ts +152 -0
  188. package/src/host/surface-content.ts +87 -0
  189. package/src/host/take-named-export.ts +23 -0
  190. package/src/host/three-ingest-runtime.ts +76 -0
  191. package/src/host/types-fastnoise-lite.d.ts +7 -0
  192. package/src/host/types-mikktspace.d.ts +20 -0
  193. package/src/host/types-troika-three-text.d.ts +7 -0
  194. package/src/host/use-active-performance-source.ts +53 -0
  195. package/src/host/viewport-pose-memory.ts +48 -0
  196. package/src/host/viewport-root-presentation.ts +40 -0
  197. package/src/ingest/active-ingest.ts +251 -0
  198. package/src/ingest/active-scene-navigation.ts +43 -0
  199. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
  200. package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
  201. package/src/ingest/capture-wait-report.ts +122 -0
  202. package/src/ingest/deferred-ingest-play.ts +216 -0
  203. package/src/ingest/deferred-ingest-session.ts +23 -0
  204. package/src/ingest/discovery-public-ingest.ts +242 -0
  205. package/src/ingest/dom-stub-mark.ts +7 -0
  206. package/src/ingest/entry-load.ts +56 -0
  207. package/src/ingest/game-contract-realm.ts +34 -0
  208. package/src/ingest/game-pointer-lock.ts +89 -0
  209. package/src/ingest/held-scene-repaint.ts +73 -0
  210. package/src/ingest/host-surface-box.ts +174 -0
  211. package/src/ingest/ingest-boot-viewport.ts +54 -0
  212. package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
  213. package/src/ingest/ingest-canvas-scene.ts +50 -0
  214. package/src/ingest/ingest-evidence-hook.ts +131 -0
  215. package/src/ingest/ingest-frame-snapshot.ts +183 -0
  216. package/src/ingest/ingest-play-commands.ts +134 -0
  217. package/src/ingest/ingest-play-control.ts +299 -0
  218. package/src/ingest/ingest-render-debug.ts +320 -0
  219. package/src/ingest/ingest-siblings.ts +487 -0
  220. package/src/ingest/ingest-status.ts +62 -0
  221. package/src/ingest/live-ingest-facet.ts +46 -0
  222. package/src/ingest/module-mode.ts +229 -0
  223. package/src/ingest/mount-canvas-ingest-root.ts +899 -0
  224. package/src/ingest/mount-coverage.ts +296 -0
  225. package/src/ingest/mount-dom-ingest-root.ts +282 -0
  226. package/src/ingest/mount-ingest-root.ts +744 -0
  227. package/src/ingest/mount-three-ingest-root.ts +366 -0
  228. package/src/ingest/resolve-canvas.ts +47 -0
  229. package/src/ingest/resolve-three.ts +123 -0
  230. package/src/ingest/served-bundle.ts +109 -0
  231. package/src/ingest/served-html-boot.ts +292 -0
  232. package/src/ingest/surface-canvas.ts +84 -0
  233. package/src/ingest/surface-dom.ts +84 -0
  234. package/src/ingest/surface-three.ts +128 -0
  235. package/src/ingest/types.ts +76 -0
  236. package/src/ingest/unmount-ingest-root.ts +224 -0
  237. package/src/navmesh/navmesh-actions.ts +27 -0
  238. package/src/navmesh/navmesh-handler.ts +237 -0
  239. package/src/navmesh/navmesh-workflow-store.ts +78 -0
  240. package/src/network/NetworkInspectorPanel.tsx +644 -0
  241. package/src/network/network-inspector-model.ts +225 -0
  242. package/src/play/play-boot-stall.ts +118 -0
  243. package/src/play/play-log-events.ts +26 -0
  244. package/src/play/play-mode.ts +2553 -0
  245. package/src/play/play-recording.ts +335 -0
  246. package/src/play/react-play-live-authoring.ts +165 -0
  247. package/src/play-bar/PlayBar.tsx +488 -0
  248. package/src/play-bar/PlayerCountPicker.tsx +100 -0
  249. package/src/profiler/FrameDebuggerPanel.tsx +458 -0
  250. package/src/profiler/PerformancePanel.tsx +1068 -0
  251. package/src/profiler/ProfilerPanel.tsx +53 -0
  252. package/src/profiler/frame-debugger-store.ts +97 -0
  253. package/src/profiler/main-thread-busy.ts +90 -0
  254. package/src/react/design-time-react-mount.ts +782 -0
  255. package/src/react/dom-authoring-adapter.ts +764 -0
  256. package/src/react/pasteboard-materialize.ts +154 -0
  257. package/src/react/react-inspector-section.tsx +2496 -0
  258. package/src/react/react-world-authoring-adapter.ts +3795 -0
  259. package/src/react/story-documents/story-args-section.ts +19 -0
  260. package/src/react/story-documents/story-documents.tsx +1380 -0
  261. package/src/react/story-paint-bounds.ts +94 -0
  262. package/src/react/ui-board-document.tsx +101 -0
  263. package/src/react/ui-board-title.ts +9 -0
  264. package/src/react/ui-component-board.ts +72 -0
  265. package/src/services/audio-pose-guard.ts +81 -0
  266. package/src/services/game-audio-unlock.ts +48 -0
  267. package/src/state-watch/StateWatchPanel.tsx +535 -0
  268. package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
  269. package/src/three/authoring/constraint-inspector-section.tsx +189 -0
  270. package/src/three/authoring/design-time-renderer.ts +189 -0
  271. package/src/three/authoring/model-asset-inspector-section.css +41 -0
  272. package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
  273. package/src/three/authoring/oid-source-persistence.ts +557 -0
  274. package/src/three/authoring/r3f-design-session.ts +1174 -0
  275. package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
  276. package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
  277. package/src/three/authoring/spatial-audio-handles.ts +215 -0
  278. package/src/three/authoring/spatial-collider-handles.ts +229 -0
  279. package/src/three/authoring/spatial-joint-handles.ts +114 -0
  280. package/src/three/authoring/spatial-light-handles.ts +178 -0
  281. package/src/three/authoring/spatial-lod-handles.ts +93 -0
  282. package/src/three/authoring/spatial-particle-handles.ts +368 -0
  283. package/src/three/authoring/three-authoring-adapter.ts +1953 -0
  284. package/src/three/authoring/three-scene-identity.ts +19 -0
  285. package/src/three/authoring/three-spatial-handles.ts +161 -0
  286. package/src/three/authoring/typed-three-inspector.ts +528 -0
  287. package/src/three/component-verbs/extract-menu.ts +71 -0
  288. package/src/three/component-verbs/fork-menu.ts +78 -0
  289. package/src/three/component-verbs/internals-menu.ts +91 -0
  290. package/src/three/story-documents/three-story-documents.tsx +607 -0
  291. package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
  292. package/src/three/three-board/board-framing.ts +412 -0
  293. package/src/three/three-board/board-layout.ts +401 -0
  294. package/src/three/three-board/board-scene.ts +901 -0
  295. package/src/three/three-board/three-component-board.ts +62 -0
  296. package/src/xstate/XStateBehaviorSection.tsx +130 -0
  297. package/src/xstate/XStateMachineInspector.tsx +566 -0
  298. package/src/xstate/character-animation-machine.fixture.ts +74 -0
  299. package/src/xstate/live-behaviors.ts +106 -0
  300. package/src/xstate/use-live-actor-state.ts +44 -0
  301. package/src/xstate/xstate-graph.ts +235 -0
  302. package/src/xstate/xstate-layout.ts +76 -0
@@ -0,0 +1,257 @@
1
+ /**
2
+ * What the Generations gallery SHOWS about a job — derived entirely from the
3
+ * durable `GenerationJob` ledger, never from a second store or a provider poll.
4
+ *
5
+ * The gallery is asset-first: a card's subject is the file the job produced,
6
+ * so everything here answers "which bytes, of what kind, under what name",
7
+ * plus the small amount of text (state, model, charge) that frames them.
8
+ */
9
+
10
+ import { ASSET_ROOTS, assetRootServingUrl } from '@volter/editor-core/asset-workflow/project-asset-roots';
11
+ import type { GenerationBilling, GenerationJob } from '@volter/editor-sdk/generations';
12
+
13
+ /**
14
+ * The state a card presents, which is NOT `job.status` alone.
15
+ *
16
+ * `succeeded` splits four ways because the server reconciler accepts a
17
+ * finished job's outputs AUTOMATICALLY — there is no manual add-to-project
18
+ * gate anywhere in this surface. Until `acceptedAt` lands, a succeeded job
19
+ * that still carries an `accept` operation is mid-SAVE; if that save recorded
20
+ * an error it is `save-failed` (the server stops after its own bounded
21
+ * attempts, so the card must stop claiming progress and offer the retry); and
22
+ * a succeeded job that never had an `accept` produced nothing to hold.
23
+ *
24
+ * `save-failed` is deliberately its own value rather than folded into
25
+ * `failed`: both read as a failure to the eye (same tone, same border), but
26
+ * only one of them has a save to retry, and only the other means the provider
27
+ * itself failed.
28
+ */
29
+ export type GenerationCardState =
30
+ | 'queued'
31
+ | 'running'
32
+ | 'saving'
33
+ | 'save-failed'
34
+ | 'ready'
35
+ | 'no-asset'
36
+ | 'failed'
37
+ | 'cancelled';
38
+
39
+ export function generationCardState(job: GenerationJob): GenerationCardState {
40
+ switch (job.status) {
41
+ case 'queued':
42
+ return 'queued';
43
+ case 'running':
44
+ return 'running';
45
+ case 'failed':
46
+ return 'failed';
47
+ case 'cancelled':
48
+ return 'cancelled';
49
+ default:
50
+ if (job.acceptedAt) return 'ready';
51
+ if (!job.accept) return 'no-asset';
52
+ return job.pollError ? 'save-failed' : 'saving';
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Whether forgetting the job now can lose nothing in flight — the one rule
58
+ * both the gallery card and the detail document offer "Remove" by.
59
+ */
60
+ export function generationRemovable(job: GenerationJob): boolean {
61
+ const state = generationCardState(job);
62
+ return state !== 'queued' && state !== 'running' && state !== 'saving';
63
+ }
64
+
65
+ /** Whether the job's own accept operation is still worth running by hand. */
66
+ export function generationSaveRetryable(job: GenerationJob, actionError: string | null): boolean {
67
+ return (
68
+ job.status === 'succeeded' &&
69
+ Boolean(job.accept) &&
70
+ !job.acceptedAt &&
71
+ Boolean(job.pollError ?? actionError)
72
+ );
73
+ }
74
+
75
+ /** Short, human state word for the card's status chip. */
76
+ export const GENERATION_STATE_LABEL: Record<GenerationCardState, string> = {
77
+ queued: 'Queued',
78
+ running: 'Running',
79
+ saving: 'Saving',
80
+ 'save-failed': "Couldn't save",
81
+ ready: 'In project',
82
+ 'no-asset': 'No file',
83
+ failed: 'Failed',
84
+ cancelled: 'Cancelled',
85
+ };
86
+
87
+ /** How a produced file can be shown. `file` means "no honest preview exists". */
88
+ export type GenerationPreviewKind = 'image' | 'video' | 'audio' | 'model' | 'file';
89
+
90
+ const PREVIEW_KINDS: ReadonlyMap<string, GenerationPreviewKind> = new Map([
91
+ ...(['png', 'jpg', 'jpeg', 'webp', 'gif', 'avif', 'svg', 'bmp'] as const).map(
92
+ (extension) => [extension, 'image'] as const,
93
+ ),
94
+ // Provider video output is what `<video controls>` decodes natively; an
95
+ // extension outside this set gets the file row, never a guessed player.
96
+ ...(['mp4', 'webm', 'ogv', 'mov', 'm4v'] as const).map(
97
+ (extension) => [extension, 'video'] as const,
98
+ ),
99
+ ...(['mp3', 'wav', 'ogg', 'oga', 'm4a', 'flac', 'aac'] as const).map(
100
+ (extension) => [extension, 'audio'] as const,
101
+ ),
102
+ ...(['glb', 'gltf', 'fbx', 'obj', 'stl', 'ply', 'dae', '3ds', 'usdz', 'spz'] as const).map(
103
+ (extension) => [extension, 'model'] as const,
104
+ ),
105
+ ]);
106
+
107
+ export interface GenerationOutput {
108
+ /** Project-relative path exactly as the ledger recorded it. */
109
+ readonly path: string;
110
+ /** File name, for the card's name and the preview's alt text. */
111
+ readonly name: string;
112
+ readonly kind: GenerationPreviewKind;
113
+ /**
114
+ * Root-relative URL the dev server answers, or null for an output that does
115
+ * does not live under either project asset root.
116
+ */
117
+ readonly url: string | null;
118
+ }
119
+
120
+ export function generationOutputExtension(path: string): string {
121
+ const name = path.slice(path.lastIndexOf('/') + 1).toLowerCase();
122
+ const dot = name.lastIndexOf('.');
123
+ return dot > 0 ? name.slice(dot + 1) : '';
124
+ }
125
+
126
+ export function generationOutputs(job: GenerationJob): readonly GenerationOutput[] {
127
+ return (job.outputPaths ?? []).map((path) => {
128
+ const root = ASSET_ROOTS.find((root) => path.startsWith(`${root}/`));
129
+ const url = root ? assetRootServingUrl(root, path.slice(root.length + 1)) : null;
130
+ return {
131
+ path,
132
+ name: path.slice(path.lastIndexOf('/') + 1),
133
+ kind: PREVIEW_KINDS.get(generationOutputExtension(path)) ?? 'file',
134
+ url,
135
+ };
136
+ });
137
+ }
138
+
139
+ function shorten(text: string, max: number): string {
140
+ const clean = text.trim();
141
+ return clean.length <= max ? clean : `${clean.slice(0, max - 1).trimEnd()}…`;
142
+ }
143
+
144
+ /**
145
+ * A concise card name.
146
+ *
147
+ * A job's `label` is the submitted PROMPT, already truncated by the provider
148
+ * extension — it reads as a sentence, not a name, so once files exist the file
149
+ * is the name. Two sibling outputs of one job are named by their folder.
150
+ */
151
+ export function generationTitle(job: GenerationJob): string {
152
+ const named = recordedOutputName(job);
153
+ if (named) return named;
154
+ const outputs = generationOutputs(job);
155
+ if (outputs.length === 0) return shorten(job.label, 64);
156
+ const first = outputs[0] as GenerationOutput;
157
+ const folder = descriptiveFolder(first.path);
158
+ if (outputs.length > 1) {
159
+ return folder ? `${folder} · ${outputs.length} files` : `${outputs.length} files`;
160
+ }
161
+ // `image-1.png` names nothing on its own. A descriptive folder carries it
162
+ // (`motion/video.mp4`); a per-request hash folder carries nothing, so the
163
+ // prompt is a better name than an opaque id.
164
+ if (!GENERIC_OUTPUT_NAME.test(first.name)) return first.name;
165
+ return folder ? `${folder}/${first.name}` : shorten(job.label, 64);
166
+ }
167
+
168
+ const GENERIC_OUTPUT_NAME = /^(image|video|audio|model|panorama|output)(-\d+)?\./;
169
+
170
+ /** A hash/uuid segment names nothing to a human; a written word does. */
171
+ function descriptiveFolder(path: string): string | null {
172
+ const folder = path.split('/').slice(-2, -1)[0];
173
+ if (!folder) return null;
174
+ const opaque = /^[0-9a-f]{8,}$/i.test(folder) || /^[0-9a-f-]{16,}$/i.test(folder);
175
+ return opaque ? null : folder;
176
+ }
177
+
178
+ /**
179
+ * The name the submitting operation asked its output to be written as, when it
180
+ * recorded one. This is the naming the accept operation itself owns; the
181
+ * gallery only reads it.
182
+ */
183
+ function recordedOutputName(job: GenerationJob): string | null {
184
+ const input = job.accept?.input;
185
+ if (!input || typeof input !== 'object') return null;
186
+ const value = (input as Record<string, unknown>)['outputName'];
187
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
188
+ }
189
+
190
+ /**
191
+ * The submitted prompt, when the job's own recorded operation input carries
192
+ * one. Providers keep native request shapes, so this reads the recorded
193
+ * `accept`/`poll` input a level or two deep and gives up rather than guessing —
194
+ * a card never invents prompt text.
195
+ */
196
+ export function generationPrompt(job: GenerationJob): string | null {
197
+ for (const root of [job.accept?.input, job.poll.input]) {
198
+ const found = findPrompt(root, 0);
199
+ if (found) return found;
200
+ }
201
+ return null;
202
+ }
203
+
204
+ function findPrompt(value: unknown, depth: number): string | null {
205
+ if (depth > 3 || typeof value !== 'object' || value === null) return null;
206
+ const record = value as Record<string, unknown>;
207
+ const direct = record['prompt'];
208
+ if (typeof direct === 'string' && direct.trim().length > 0) return direct.trim();
209
+ for (const nested of Object.values(record)) {
210
+ const found = findPrompt(nested, depth + 1);
211
+ if (found) return found;
212
+ }
213
+ return null;
214
+ }
215
+
216
+ /** Provider-native model/endpoint identity, e.g. `fal-ai/nano-banana-2/edit`. */
217
+ export function generationModelLabel(job: GenerationJob): string {
218
+ return job.operation;
219
+ }
220
+
221
+ export function generationRouteLabel(job: GenerationJob): string {
222
+ if (job.mode === 'managed') return 'VGAI subscription';
223
+ if (job.mode === 'direct') return 'Your provider key';
224
+ return 'Mock';
225
+ }
226
+
227
+ /** The charge, settled where the provider reported one, estimate otherwise. */
228
+ export function generationBillingLabel(billing: GenerationBilling): string {
229
+ if (billing.route === 'mock') return 'Free · mock';
230
+ if (billing.route === 'managed') {
231
+ if (billing.settledCredits !== undefined) return `${billing.settledCredits} credits`;
232
+ if (billing.estimatedCredits !== undefined) return `Up to ${billing.estimatedCredits} credits`;
233
+ return 'Credit estimate unavailable';
234
+ }
235
+ if (billing.settledAmount !== undefined) return `$${billing.settledAmount.toFixed(3)}`;
236
+ if (billing.estimatedAmount !== undefined) return `Up to $${billing.estimatedAmount.toFixed(3)}`;
237
+ return 'USD estimate unavailable';
238
+ }
239
+
240
+ /** True once the provider has settled the charge, so the figure is final. */
241
+ export function generationBillingSettled(billing: GenerationBilling): boolean {
242
+ if (billing.route === 'mock') return true;
243
+ if (billing.route === 'managed') return billing.settledCredits !== undefined;
244
+ return billing.settledAmount !== undefined;
245
+ }
246
+
247
+ /** Local to the gallery: coarse age, at the resolution a card can show. */
248
+ export function generationAge(iso: string): string {
249
+ const minutes = Math.floor((Date.now() - Date.parse(iso)) / 60_000);
250
+ if (!Number.isFinite(minutes) || minutes < 1) return 'just now';
251
+ if (minutes < 60) return `${minutes}m ago`;
252
+ const hours = Math.floor(minutes / 60);
253
+ if (hours < 24) return `${hours}h ago`;
254
+ const days = Math.floor(hours / 24);
255
+ if (days < 7) return `${days}d ago`;
256
+ return `${Math.floor(days / 7)}w ago`;
257
+ }