@portel/photon 1.20.1 → 1.22.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 (148) hide show
  1. package/README.md +5 -5
  2. package/dist/ag-ui/adapter.d.ts +4 -1
  3. package/dist/ag-ui/adapter.d.ts.map +1 -1
  4. package/dist/ag-ui/adapter.js +58 -3
  5. package/dist/ag-ui/adapter.js.map +1 -1
  6. package/dist/ag-ui/types.d.ts +12 -0
  7. package/dist/ag-ui/types.d.ts.map +1 -1
  8. package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
  9. package/dist/auto-ui/beam/routes/api-browse.js +8 -49
  10. package/dist/auto-ui/beam/routes/api-browse.js.map +1 -1
  11. package/dist/auto-ui/beam/routes/api-config.d.ts +1 -1
  12. package/dist/auto-ui/beam/routes/api-config.d.ts.map +1 -1
  13. package/dist/auto-ui/beam/routes/api-config.js +79 -1
  14. package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
  15. package/dist/auto-ui/beam.d.ts.map +1 -1
  16. package/dist/auto-ui/beam.js +23 -31
  17. package/dist/auto-ui/beam.js.map +1 -1
  18. package/dist/auto-ui/bridge/index.d.ts.map +1 -1
  19. package/dist/auto-ui/bridge/index.js +107 -11
  20. package/dist/auto-ui/bridge/index.js.map +1 -1
  21. package/dist/auto-ui/bridge/renderers.d.ts +14 -0
  22. package/dist/auto-ui/bridge/renderers.d.ts.map +1 -1
  23. package/dist/auto-ui/bridge/renderers.js +680 -57
  24. package/dist/auto-ui/bridge/renderers.js.map +1 -1
  25. package/dist/auto-ui/frontend/index.html +3 -3
  26. package/dist/auto-ui/frontend/pure-view.html +19 -19
  27. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  28. package/dist/auto-ui/streamable-http-transport.js +53 -2
  29. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  30. package/dist/auto-ui/ui-resolver.d.ts +25 -0
  31. package/dist/auto-ui/ui-resolver.d.ts.map +1 -0
  32. package/dist/auto-ui/ui-resolver.js +95 -0
  33. package/dist/auto-ui/ui-resolver.js.map +1 -0
  34. package/dist/beam-form.bundle.js +7 -7
  35. package/dist/beam-form.bundle.js.map +1 -1
  36. package/dist/beam.bundle.js +905 -185
  37. package/dist/beam.bundle.js.map +4 -4
  38. package/dist/cli/commands/build.d.ts.map +1 -1
  39. package/dist/cli/commands/build.js +9 -5
  40. package/dist/cli/commands/build.js.map +1 -1
  41. package/dist/cli/commands/init.d.ts.map +1 -1
  42. package/dist/cli/commands/init.js +93 -53
  43. package/dist/cli/commands/init.js.map +1 -1
  44. package/dist/cli/commands/publish.d.ts +14 -0
  45. package/dist/cli/commands/publish.d.ts.map +1 -0
  46. package/dist/cli/commands/publish.js +126 -0
  47. package/dist/cli/commands/publish.js.map +1 -0
  48. package/dist/cli/commands/run.d.ts.map +1 -1
  49. package/dist/cli/commands/run.js +2 -0
  50. package/dist/cli/commands/run.js.map +1 -1
  51. package/dist/cli/index.d.ts.map +1 -1
  52. package/dist/cli/index.js +3 -0
  53. package/dist/cli/index.js.map +1 -1
  54. package/dist/cli.d.ts +4 -0
  55. package/dist/cli.d.ts.map +1 -1
  56. package/dist/cli.js +11 -1
  57. package/dist/cli.js.map +1 -1
  58. package/dist/context.d.ts +6 -0
  59. package/dist/context.d.ts.map +1 -1
  60. package/dist/context.js +17 -5
  61. package/dist/context.js.map +1 -1
  62. package/dist/daemon/client.d.ts +9 -1
  63. package/dist/daemon/client.d.ts.map +1 -1
  64. package/dist/daemon/client.js +54 -1
  65. package/dist/daemon/client.js.map +1 -1
  66. package/dist/daemon/manager.d.ts +3 -0
  67. package/dist/daemon/manager.d.ts.map +1 -1
  68. package/dist/daemon/manager.js +88 -38
  69. package/dist/daemon/manager.js.map +1 -1
  70. package/dist/daemon/ownership.d.ts +12 -0
  71. package/dist/daemon/ownership.d.ts.map +1 -0
  72. package/dist/daemon/ownership.js +55 -0
  73. package/dist/daemon/ownership.js.map +1 -0
  74. package/dist/daemon/protocol.d.ts +4 -2
  75. package/dist/daemon/protocol.d.ts.map +1 -1
  76. package/dist/daemon/protocol.js +15 -2
  77. package/dist/daemon/protocol.js.map +1 -1
  78. package/dist/daemon/server.js +557 -83
  79. package/dist/daemon/server.js.map +1 -1
  80. package/dist/daemon/session-manager.d.ts +9 -1
  81. package/dist/daemon/session-manager.d.ts.map +1 -1
  82. package/dist/daemon/session-manager.js +54 -1
  83. package/dist/daemon/session-manager.js.map +1 -1
  84. package/dist/daemon/worker-manager.d.ts +12 -0
  85. package/dist/daemon/worker-manager.d.ts.map +1 -1
  86. package/dist/daemon/worker-manager.js +89 -6
  87. package/dist/daemon/worker-manager.js.map +1 -1
  88. package/dist/loader.d.ts +17 -9
  89. package/dist/loader.d.ts.map +1 -1
  90. package/dist/loader.js +415 -141
  91. package/dist/loader.js.map +1 -1
  92. package/dist/photon-cli-runner.d.ts.map +1 -1
  93. package/dist/photon-cli-runner.js +26 -2
  94. package/dist/photon-cli-runner.js.map +1 -1
  95. package/dist/photons/canvas/ui/canvas.photon.html +1493 -0
  96. package/dist/photons/canvas.photon.d.ts +400 -0
  97. package/dist/photons/canvas.photon.d.ts.map +1 -0
  98. package/dist/photons/canvas.photon.js +662 -0
  99. package/dist/photons/canvas.photon.js.map +1 -0
  100. package/dist/photons/canvas.photon.ts +814 -0
  101. package/dist/photons/publish.photon.d.ts +97 -0
  102. package/dist/photons/publish.photon.d.ts.map +1 -0
  103. package/dist/photons/publish.photon.js +569 -0
  104. package/dist/photons/publish.photon.js.map +1 -0
  105. package/dist/photons/publish.photon.ts +683 -0
  106. package/dist/photons/ui/canvas.photon.html +624 -0
  107. package/dist/resource-server.d.ts.map +1 -1
  108. package/dist/resource-server.js +7 -1
  109. package/dist/resource-server.js.map +1 -1
  110. package/dist/server.d.ts +7 -0
  111. package/dist/server.d.ts.map +1 -1
  112. package/dist/server.js +67 -37
  113. package/dist/server.js.map +1 -1
  114. package/dist/shared/error-handler.d.ts +1 -0
  115. package/dist/shared/error-handler.d.ts.map +1 -1
  116. package/dist/shared/error-handler.js +68 -10
  117. package/dist/shared/error-handler.js.map +1 -1
  118. package/dist/shared/logger.d.ts.map +1 -1
  119. package/dist/shared/logger.js +34 -0
  120. package/dist/shared/logger.js.map +1 -1
  121. package/dist/shared-utils.d.ts.map +1 -1
  122. package/dist/shared-utils.js +2 -2
  123. package/dist/shared-utils.js.map +1 -1
  124. package/dist/telemetry/context.d.ts +24 -0
  125. package/dist/telemetry/context.d.ts.map +1 -0
  126. package/dist/telemetry/context.js +17 -0
  127. package/dist/telemetry/context.js.map +1 -0
  128. package/dist/telemetry/logs.d.ts +38 -0
  129. package/dist/telemetry/logs.d.ts.map +1 -0
  130. package/dist/telemetry/logs.js +108 -0
  131. package/dist/telemetry/logs.js.map +1 -0
  132. package/dist/telemetry/metrics.d.ts +71 -0
  133. package/dist/telemetry/metrics.d.ts.map +1 -0
  134. package/dist/telemetry/metrics.js +184 -0
  135. package/dist/telemetry/metrics.js.map +1 -0
  136. package/dist/telemetry/otel.d.ts +20 -1
  137. package/dist/telemetry/otel.d.ts.map +1 -1
  138. package/dist/telemetry/otel.js +79 -2
  139. package/dist/telemetry/otel.js.map +1 -1
  140. package/dist/telemetry/sdk.d.ts +49 -0
  141. package/dist/telemetry/sdk.d.ts.map +1 -0
  142. package/dist/telemetry/sdk.js +110 -0
  143. package/dist/telemetry/sdk.js.map +1 -0
  144. package/dist/tsx-compiler.d.ts +23 -0
  145. package/dist/tsx-compiler.d.ts.map +1 -0
  146. package/dist/tsx-compiler.js +221 -0
  147. package/dist/tsx-compiler.js.map +1 -0
  148. package/package.json +7 -7
@@ -0,0 +1,662 @@
1
+ /**
2
+ * Canvas — co-creative scene graph
3
+ *
4
+ * Shared canvas where AI agents and humans collaboratively place,
5
+ * move, resize, and update rendered elements. Either party can start,
6
+ * either can edit, control passes back and forth fluidly.
7
+ *
8
+ * Each element renders using the runtime's 50+ format renderers
9
+ * (metric, chart:bar, table, gauge, timeline, etc.).
10
+ *
11
+ * @description Co-creative canvas for AI and humans
12
+ * @icon 🎨
13
+ * @stateful
14
+ */
15
+ export default class Canvas {
16
+ // Injected by runtime — declared for capability detection
17
+ emit;
18
+ formats;
19
+ memory;
20
+ /** Scene graph: element ID → element */
21
+ _scene = {};
22
+ _nextZ = 1;
23
+ _loaded = false;
24
+ /** Turn state: who has control */
25
+ _turn = { agent: 'human', since: Date.now() };
26
+ /** Max snapshots to keep per canvas */
27
+ static MAX_SNAPSHOTS = 100;
28
+ /** Load scene from persistent storage (or from fork data) */
29
+ async _load() {
30
+ if (this._loaded)
31
+ return;
32
+ this._loaded = true;
33
+ try {
34
+ const saved = await this.memory.get('scene');
35
+ if (saved) {
36
+ this._scene = saved.scene || {};
37
+ this._nextZ = saved.nextZ || 1;
38
+ if (saved.turn)
39
+ this._turn = saved.turn;
40
+ }
41
+ }
42
+ catch {
43
+ // First run or corrupted — start fresh
44
+ }
45
+ // Check for fork data in global scope (written by another instance's fork())
46
+ if (Object.keys(this._scene).length === 0) {
47
+ try {
48
+ const globalKeys = await this.memory.keys('global');
49
+ for (const key of globalKeys) {
50
+ // Match fork data addressed to this instance
51
+ if (key.startsWith('canvas-fork:')) {
52
+ const forkData = await this.memory.get(key, 'global');
53
+ if (forkData && forkData.scene) {
54
+ this._scene = forkData.scene;
55
+ this._nextZ = forkData.nextZ || 1;
56
+ this._turn = forkData.turn || this._turn;
57
+ await this.memory.delete(key, 'global'); // consume
58
+ await this._save('forked');
59
+ break;
60
+ }
61
+ }
62
+ }
63
+ }
64
+ catch {
65
+ // Fork check failed — continue with empty scene
66
+ }
67
+ }
68
+ }
69
+ /** Save scene to persistent storage + auto-snapshot */
70
+ async _save(action) {
71
+ await this.memory.set('scene', {
72
+ scene: this._scene,
73
+ nextZ: this._nextZ,
74
+ turn: this._turn,
75
+ });
76
+ // Auto-snapshot: record every mutation
77
+ const timeline = (await this.memory.get('timeline')) || [];
78
+ timeline.push({
79
+ ts: Date.now(),
80
+ action: action || 'edit',
81
+ scene: JSON.parse(JSON.stringify(this._scene)),
82
+ elementCount: Object.keys(this._scene).length,
83
+ });
84
+ // Trim old snapshots
85
+ if (timeline.length > Canvas.MAX_SNAPSHOTS) {
86
+ timeline.splice(0, timeline.length - Canvas.MAX_SNAPSHOTS);
87
+ }
88
+ await this.memory.set('timeline', timeline);
89
+ }
90
+ /**
91
+ * Open the canvas
92
+ * @ui canvas
93
+ * @readOnly
94
+ */
95
+ async main() {
96
+ await this._load();
97
+ return { elements: Object.values(this._scene), turn: this._turn };
98
+ }
99
+ /**
100
+ * Place or update an element on the canvas.
101
+ * Creates if new, merges if exists — only provided fields change.
102
+ *
103
+ * @param id Element identifier
104
+ * @param format Renderer format (metric, chart:bar, table, gauge, etc.)
105
+ * @param data Data matching the format spec
106
+ * @param x Horizontal position in pixels
107
+ * @param y Vertical position in pixels
108
+ * @param w Width in pixels
109
+ * @param h Height in pixels
110
+ * @param z Z-order layer (higher = on top)
111
+ * @param label Human-readable label shown on the element
112
+ */
113
+ async put({ id, format, data, x, y, w, h, z, label, }) {
114
+ await this._load();
115
+ const existing = this._scene[id];
116
+ const element = {
117
+ id,
118
+ format: format ?? existing?.format ?? 'card',
119
+ data: data !== undefined ? data : existing?.data,
120
+ x: x ?? existing?.x ?? 50,
121
+ y: y ?? existing?.y ?? 50,
122
+ w: w ?? existing?.w ?? 300,
123
+ h: h ?? existing?.h ?? 200,
124
+ z: z ?? existing?.z ?? this._nextZ++,
125
+ label: label ?? existing?.label,
126
+ createdBy: existing?.createdBy ?? 'ai',
127
+ updatedAt: Date.now(),
128
+ };
129
+ this._scene[id] = element;
130
+ const action = existing ? `update ${id}` : `add ${id}`;
131
+ await this._save(action);
132
+ // Emit scene change — flows through SSE → bridge → onEmit
133
+ this.emit({
134
+ emit: 'scene:put',
135
+ element,
136
+ });
137
+ return element;
138
+ }
139
+ /**
140
+ * Remove an element from the canvas
141
+ * @param id Element identifier to remove
142
+ */
143
+ async remove({ id }) {
144
+ await this._load();
145
+ const existed = id in this._scene;
146
+ delete this._scene[id];
147
+ await this._save(`remove ${id}`);
148
+ this.emit({
149
+ emit: 'scene:remove',
150
+ id,
151
+ });
152
+ return { removed: existed, id };
153
+ }
154
+ /**
155
+ * Clear all elements from the canvas
156
+ * @destructive
157
+ */
158
+ async clear() {
159
+ await this._load();
160
+ const count = Object.keys(this._scene).length;
161
+ this._scene = {};
162
+ this._nextZ = 1;
163
+ await this._save('clear');
164
+ this.emit({
165
+ emit: 'scene:clear',
166
+ });
167
+ return { cleared: count };
168
+ }
169
+ /**
170
+ * Get the full scene graph — all elements with positions, sizes, and data
171
+ * @readOnly
172
+ */
173
+ async scene() {
174
+ await this._load();
175
+ return {
176
+ elements: Object.values(this._scene),
177
+ count: Object.keys(this._scene).length,
178
+ turn: this._turn,
179
+ };
180
+ }
181
+ /**
182
+ * Pass control to another agent or back to the human.
183
+ * The recipient sees a status banner with the optional message.
184
+ *
185
+ * @param to Who gets control next (e.g. 'human', 'ai', agent name)
186
+ * @param message Optional message explaining what to do next
187
+ */
188
+ async pass({ to, message }) {
189
+ await this._load();
190
+ this._turn = { agent: to, message, since: Date.now() };
191
+ await this._save();
192
+ this.emit({
193
+ emit: 'turn:change',
194
+ turn: this._turn,
195
+ });
196
+ return this._turn;
197
+ }
198
+ /**
199
+ * Lock an element so only the specified agent can modify it.
200
+ *
201
+ * @param id Element to lock
202
+ * @param agent Agent name claiming the lock
203
+ */
204
+ async lock({ id, agent }) {
205
+ await this._load();
206
+ const el = this._scene[id];
207
+ if (!el)
208
+ return { error: 'Element not found', id };
209
+ if (el.locked && el.locked !== agent) {
210
+ return { error: `Locked by ${el.locked}`, id };
211
+ }
212
+ el.locked = agent;
213
+ el.updatedAt = Date.now();
214
+ await this._save(`lock ${id}`);
215
+ this.emit({ emit: 'scene:put', element: el });
216
+ return el;
217
+ }
218
+ /**
219
+ * Unlock an element, allowing anyone to modify it.
220
+ *
221
+ * @param id Element to unlock
222
+ */
223
+ async unlock({ id }) {
224
+ await this._load();
225
+ const el = this._scene[id];
226
+ if (!el)
227
+ return { error: 'Element not found', id };
228
+ delete el.locked;
229
+ el.updatedAt = Date.now();
230
+ await this._save(`unlock ${id}`);
231
+ this.emit({ emit: 'scene:put', element: el });
232
+ return el;
233
+ }
234
+ /**
235
+ * Describe the current canvas layout in natural language.
236
+ * Useful for AI agents to understand spatial arrangement without a screenshot.
237
+ * @readOnly
238
+ */
239
+ async describe() {
240
+ await this._load();
241
+ const els = Object.values(this._scene);
242
+ if (els.length === 0) {
243
+ return {
244
+ description: 'The canvas is empty.',
245
+ count: 0,
246
+ };
247
+ }
248
+ // Sort by z-order (back to front)
249
+ const sorted = [...els].sort((a, b) => a.z - b.z);
250
+ // Compute canvas bounds
251
+ const maxX = Math.max(...els.map((e) => e.x + e.w));
252
+ const maxY = Math.max(...els.map((e) => e.y + e.h));
253
+ // Describe each element with spatial context
254
+ const descriptions = sorted.map((el) => {
255
+ const cx = el.x + el.w / 2;
256
+ const cy = el.y + el.h / 2;
257
+ const hPos = cx < maxX * 0.33 ? 'left' : cx > maxX * 0.66 ? 'right' : 'center';
258
+ const vPos = cy < maxY * 0.33 ? 'top' : cy > maxY * 0.66 ? 'bottom' : 'middle';
259
+ const pos = vPos === 'middle' && hPos === 'center' ? 'center' : `${vPos}-${hPos}`;
260
+ const size = `${el.w}x${el.h}`;
261
+ const lock = el.locked ? ` [locked by ${el.locked}]` : '';
262
+ const label = el.label || el.id;
263
+ return `- "${label}" (${el.format}, ${size}) at ${pos}, placed by ${el.createdBy || 'unknown'}${lock}`;
264
+ });
265
+ // Detect spatial patterns
266
+ const patterns = [];
267
+ const xGroups = this._groupBy(els, (e) => Math.round(e.y / 50) * 50);
268
+ const rows = Object.values(xGroups).filter((g) => g.length > 1);
269
+ if (rows.length > 0) {
270
+ patterns.push(`${rows.length} row(s) of aligned elements`);
271
+ }
272
+ const yGroups = this._groupBy(els, (e) => Math.round(e.x / 50) * 50);
273
+ const cols = Object.values(yGroups).filter((g) => g.length > 1);
274
+ if (cols.length > 0) {
275
+ patterns.push(`${cols.length} column(s) of aligned elements`);
276
+ }
277
+ const summary = `Canvas has ${els.length} element(s) spanning ${maxX}x${maxY}px.` +
278
+ (patterns.length > 0 ? ' Layout: ' + patterns.join(', ') + '.' : '') +
279
+ ` Turn: ${this._turn.agent}.`;
280
+ return {
281
+ description: summary,
282
+ elements: descriptions,
283
+ bounds: { width: maxX, height: maxY },
284
+ turn: this._turn,
285
+ count: els.length,
286
+ };
287
+ }
288
+ _groupBy(items, keyFn) {
289
+ const groups = {};
290
+ for (const item of items) {
291
+ const key = keyFn(item);
292
+ if (!groups[key])
293
+ groups[key] = [];
294
+ groups[key].push(item);
295
+ }
296
+ return groups;
297
+ }
298
+ /**
299
+ * Request a screenshot from the canvas UI.
300
+ * Returns the latest captured screenshot as a base64 data URL.
301
+ * The UI captures the screenshot and sends it back via the capture method.
302
+ * @readOnly
303
+ */
304
+ async screenshot() {
305
+ await this._load();
306
+ // Emit request — client will capture and call 'capture' with the data
307
+ this.emit({ emit: 'canvas:screenshot-request' });
308
+ // Return the last captured screenshot if available
309
+ const last = await this.memory.get('last-screenshot');
310
+ return {
311
+ available: !!last,
312
+ dataUrl: last || null,
313
+ hint: last
314
+ ? 'Screenshot available. Pass the dataUrl to a multimodal model.'
315
+ : 'Screenshot requested. Call screenshot() again after a moment to retrieve it.',
316
+ };
317
+ }
318
+ /**
319
+ * Store a screenshot captured by the canvas UI.
320
+ * Called by the client after a screenshot-request event.
321
+ * @internal
322
+ * @param dataUrl Base64 data URL of the captured image
323
+ */
324
+ async capture({ dataUrl }) {
325
+ await this.memory.set('last-screenshot', dataUrl);
326
+ return { stored: true };
327
+ }
328
+ /**
329
+ * Register a custom component that can be used as a format on the canvas.
330
+ * Once registered, use the component name as the format in put().
331
+ *
332
+ * @param name Component name (used as format value in put)
333
+ * @param html HTML template string. Use {{key}} placeholders for data binding.
334
+ * @param defaults Default data values for the component
335
+ */
336
+ async registerComponent({ name, html, defaults, }) {
337
+ await this._load();
338
+ const components = (await this.memory.get('components')) || {};
339
+ components[name] = { html, defaults: defaults || {} };
340
+ await this.memory.set('components', components);
341
+ this.emit({
342
+ emit: 'component:registered',
343
+ name,
344
+ html,
345
+ defaults: defaults || {},
346
+ });
347
+ return { registered: name };
348
+ }
349
+ /**
350
+ * List registered custom components with their templates
351
+ * @readOnly
352
+ */
353
+ async listComponents() {
354
+ const components = (await this.memory.get('components')) || {};
355
+ return Object.entries(components).map(([name, spec]) => ({
356
+ name,
357
+ html: spec.html,
358
+ defaults: spec.defaults || {},
359
+ }));
360
+ }
361
+ /**
362
+ * View the canvas timeline — a history of every change.
363
+ * Each entry has a timestamp, action label, and element count.
364
+ * @readOnly
365
+ */
366
+ async history() {
367
+ const timeline = (await this.memory.get('timeline')) || [];
368
+ return timeline.map((entry, i) => ({
369
+ index: i,
370
+ time: new Date(entry.ts).toISOString(),
371
+ action: entry.action,
372
+ elements: entry.elementCount,
373
+ }));
374
+ }
375
+ /**
376
+ * Get the full timeline with scene data for animated playback.
377
+ * Each frame contains the complete scene state so the client can
378
+ * compute Magic Move transitions between consecutive frames.
379
+ * @readOnly
380
+ * @internal
381
+ */
382
+ async playback() {
383
+ const timeline = (await this.memory.get('timeline')) || [];
384
+ return timeline.map((entry, i) => ({
385
+ index: i,
386
+ action: entry.action,
387
+ elements: Object.values(entry.scene),
388
+ }));
389
+ }
390
+ /**
391
+ * Save a named checkpoint at the current state.
392
+ * Checkpoints appear in the timeline with a label for easy reference.
393
+ *
394
+ * @param label Name for this checkpoint (e.g. 'before reorganizing')
395
+ */
396
+ async checkpoint({ label }) {
397
+ await this._load();
398
+ await this._save(`checkpoint: ${label}`);
399
+ this.emit({
400
+ emit: 'timeline:checkpoint',
401
+ label,
402
+ ts: Date.now(),
403
+ });
404
+ return { checkpointed: label, elements: Object.keys(this._scene).length };
405
+ }
406
+ /**
407
+ * Restore the canvas to a previous point in the timeline.
408
+ * Replaces the current scene with the snapshot at that index.
409
+ *
410
+ * @param index Timeline index to restore (from history())
411
+ */
412
+ async restore({ index }) {
413
+ const timeline = (await this.memory.get('timeline')) || [];
414
+ if (index < 0 || index >= timeline.length) {
415
+ return { error: `Invalid index ${index}. Timeline has ${timeline.length} entries.` };
416
+ }
417
+ const snapshot = timeline[index];
418
+ this._scene = JSON.parse(JSON.stringify(snapshot.scene));
419
+ this._nextZ = Math.max(0, ...Object.values(this._scene).map((e) => e.z || 0)) + 1;
420
+ await this._save(`restore to #${index} (${snapshot.action})`);
421
+ // Emit full scene refresh
422
+ this.emit({
423
+ emit: 'scene:restore',
424
+ elements: Object.values(this._scene),
425
+ });
426
+ return {
427
+ restored: index,
428
+ action: snapshot.action,
429
+ time: new Date(snapshot.ts).toISOString(),
430
+ elements: Object.keys(this._scene).length,
431
+ };
432
+ }
433
+ /**
434
+ * Fork the canvas at a timeline point into a new instance.
435
+ * Creates a new canvas instance with the scene from that snapshot.
436
+ *
437
+ * @param name New instance name (e.g. 'dashboard-v2')
438
+ * @param index Timeline index to fork from (defaults to current state)
439
+ */
440
+ async fork({ name, index }) {
441
+ await this._load();
442
+ let forkScene;
443
+ let forkAction;
444
+ if (index !== undefined) {
445
+ const timeline = (await this.memory.get('timeline')) || [];
446
+ if (index < 0 || index >= timeline.length) {
447
+ return { error: `Invalid index ${index}. Timeline has ${timeline.length} entries.` };
448
+ }
449
+ forkScene = timeline[index].scene;
450
+ forkAction = `forked from #${index}`;
451
+ }
452
+ else {
453
+ forkScene = this._scene;
454
+ forkAction = 'forked from current';
455
+ }
456
+ // Store fork data in global scope so the target instance can find it
457
+ const forkData = {
458
+ scene: JSON.parse(JSON.stringify(forkScene)),
459
+ nextZ: Math.max(0, ...Object.values(forkScene).map((e) => e.z || 0)) + 1,
460
+ turn: { agent: 'human', since: Date.now() },
461
+ };
462
+ await this.memory.set(`canvas-fork:${name}`, forkData, 'global');
463
+ this.emit({
464
+ emit: 'timeline:fork',
465
+ name,
466
+ elements: Object.keys(forkScene).length,
467
+ });
468
+ return {
469
+ forked: name,
470
+ from: forkAction,
471
+ elements: Object.keys(forkScene).length,
472
+ hint: `Open canvas/${name} to use the forked canvas`,
473
+ };
474
+ }
475
+ /**
476
+ * Export the current canvas as a standalone photon.
477
+ * Compiles the scene graph into a .photon.ts (data methods) and
478
+ * .photon.html (CSS grid layout with format renderers).
479
+ *
480
+ * @param name Photon name for the exported file (e.g. 'my-dashboard')
481
+ * @param description One-line description of the exported photon
482
+ * @readOnly
483
+ */
484
+ async export({ name, description }) {
485
+ await this._load();
486
+ const els = Object.values(this._scene);
487
+ if (els.length === 0) {
488
+ return { error: 'Canvas is empty — nothing to export' };
489
+ }
490
+ const desc = description || `Exported from canvas on ${new Date().toISOString().split('T')[0]}`;
491
+ const sorted = [...els].sort((a, b) => a.z - b.z);
492
+ // ── Detect layout: rows and columns ──
493
+ const rows = this._detectRows(sorted);
494
+ // ── Generate .photon.ts ──
495
+ const methodEntries = sorted.map((el) => {
496
+ const safeName = el.id.replace(/[^a-zA-Z0-9]/g, '_');
497
+ const dataStr = JSON.stringify(el.data, null, 4);
498
+ return [
499
+ ` /**`,
500
+ ` * ${el.label || el.id}`,
501
+ ` * @format ${el.format}`,
502
+ ` * @readOnly`,
503
+ ` */`,
504
+ ` ${safeName}() {`,
505
+ ` return ${dataStr};`,
506
+ ` }`,
507
+ ].join('\n');
508
+ });
509
+ const tsFile = [
510
+ `/**`,
511
+ ` * ${name}`,
512
+ ` *`,
513
+ ` * ${desc}`,
514
+ ` *`,
515
+ ` * @description ${desc}`,
516
+ ` * @ui main`,
517
+ ` */`,
518
+ `export default class ${this._toPascalCase(name)} {`,
519
+ ` /**`,
520
+ ` * Dashboard view`,
521
+ ` * @ui main`,
522
+ ` * @readOnly`,
523
+ ` */`,
524
+ ` main() {`,
525
+ ` return {`,
526
+ ...sorted.map((el) => {
527
+ const safeName = el.id.replace(/[^a-zA-Z0-9]/g, '_');
528
+ return ` ${safeName}: this.${safeName}(),`;
529
+ }),
530
+ ` };`,
531
+ ` }`,
532
+ ``,
533
+ ...methodEntries,
534
+ `}`,
535
+ ``,
536
+ ].join('\n');
537
+ // ── Generate .photon.html with CSS grid ──
538
+ const gridCells = rows
539
+ .map((row, ri) => row
540
+ .map((el) => {
541
+ const safeName = el.id.replace(/[^a-zA-Z0-9]/g, '_');
542
+ return [
543
+ ` <div class="cell" data-method="${safeName}" data-format="${el.format}">`,
544
+ ` <div class="cell-label">${el.label || el.id}</div>`,
545
+ ` <div class="cell-body" id="${safeName}"></div>`,
546
+ ` </div>`,
547
+ ].join('\n');
548
+ })
549
+ .join('\n'))
550
+ .join('\n');
551
+ // Compute grid template from row structure
552
+ const maxCols = Math.max(...rows.map((r) => r.length));
553
+ const colTemplate = `repeat(${maxCols}, 1fr)`;
554
+ const rowTemplate = rows.map(() => 'auto').join(' ');
555
+ const htmlFile = [
556
+ `<style>`,
557
+ ` * { box-sizing: border-box; margin: 0; padding: 0; }`,
558
+ ` body {`,
559
+ ` font-family: var(--font-family-sans, -apple-system, BlinkMacSystemFont, sans-serif);`,
560
+ ` background: var(--color-surface, #1a1b26);`,
561
+ ` color: var(--color-on-surface, #e6e6e6);`,
562
+ ` padding: 16px;`,
563
+ ` }`,
564
+ ` .grid {`,
565
+ ` display: grid;`,
566
+ ` grid-template-columns: ${colTemplate};`,
567
+ ` grid-template-rows: ${rowTemplate};`,
568
+ ` gap: 12px;`,
569
+ ` }`,
570
+ ` .cell {`,
571
+ ` background: var(--color-surface-container, #1e2030);`,
572
+ ` border: 1px solid var(--color-outline-variant, #333);`,
573
+ ` border-radius: 8px;`,
574
+ ` padding: 12px;`,
575
+ ` min-height: 120px;`,
576
+ ` }`,
577
+ ` .cell-label {`,
578
+ ` font-size: 11px;`,
579
+ ` color: var(--color-on-surface-muted, #999);`,
580
+ ` margin-bottom: 8px;`,
581
+ ` text-transform: uppercase;`,
582
+ ` letter-spacing: 0.5px;`,
583
+ ` }`,
584
+ ` .cell-body { min-height: 0; }`,
585
+ `</style>`,
586
+ ``,
587
+ `<div class="grid">`,
588
+ gridCells,
589
+ `</div>`,
590
+ ``,
591
+ `<script>`,
592
+ `(function() {`,
593
+ ` window.photon.onResult(function(result) {`,
594
+ ` if (!result) return;`,
595
+ ` var cells = document.querySelectorAll('.cell');`,
596
+ ` for (var i = 0; i < cells.length; i++) {`,
597
+ ` var method = cells[i].getAttribute('data-method');`,
598
+ ` var format = cells[i].getAttribute('data-format');`,
599
+ ` var body = cells[i].querySelector('.cell-body');`,
600
+ ` if (method && result[method] && body) {`,
601
+ ` window.photon.render(body, result[method], format);`,
602
+ ` }`,
603
+ ` }`,
604
+ ` });`,
605
+ `})();`,
606
+ `</script>`,
607
+ ].join('\n');
608
+ return {
609
+ name,
610
+ files: {
611
+ [`${name}.photon.ts`]: tsFile,
612
+ [`${name}/ui/main.html`]: htmlFile,
613
+ },
614
+ elements: sorted.length,
615
+ grid: `${maxCols} columns x ${rows.length} rows`,
616
+ };
617
+ }
618
+ _detectRows(els) {
619
+ if (els.length === 0)
620
+ return [];
621
+ // Group elements into rows by y-proximity (within 50px = same row)
622
+ const sorted = [...els].sort((a, b) => a.y - b.y || a.x - b.x);
623
+ const rows = [];
624
+ let currentRow = [sorted[0]];
625
+ let rowY = sorted[0].y;
626
+ for (let i = 1; i < sorted.length; i++) {
627
+ if (Math.abs(sorted[i].y - rowY) < 50) {
628
+ currentRow.push(sorted[i]);
629
+ }
630
+ else {
631
+ rows.push(currentRow.sort((a, b) => a.x - b.x));
632
+ currentRow = [sorted[i]];
633
+ rowY = sorted[i].y;
634
+ }
635
+ }
636
+ rows.push(currentRow.sort((a, b) => a.x - b.x));
637
+ return rows;
638
+ }
639
+ _toPascalCase(str) {
640
+ return str
641
+ .replace(/[^a-zA-Z0-9]+/g, ' ')
642
+ .split(' ')
643
+ .filter(Boolean)
644
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
645
+ .join('');
646
+ }
647
+ /**
648
+ * List all available render formats with expected data shapes
649
+ * @readOnly
650
+ * @format table
651
+ */
652
+ listFormats() {
653
+ const catalog = this.formats;
654
+ if (!catalog || typeof catalog !== 'object')
655
+ return [];
656
+ return Object.entries(catalog).map(([name, spec]) => ({
657
+ format: name,
658
+ data: spec.data,
659
+ }));
660
+ }
661
+ }
662
+ //# sourceMappingURL=canvas.photon.js.map