@portel/photon 1.14.0 → 1.16.1
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.
- package/dist/auto-ui/beam/photon-management.d.ts +1 -1
- package/dist/auto-ui/beam/photon-management.d.ts.map +1 -1
- package/dist/auto-ui/beam/photon-management.js +5 -1
- package/dist/auto-ui/beam/photon-management.js.map +1 -1
- package/dist/auto-ui/beam/routes/api-config.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-config.js +31 -9
- package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
- package/dist/auto-ui/beam/routes/api-marketplace.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-marketplace.js +3 -0
- package/dist/auto-ui/beam/routes/api-marketplace.js.map +1 -1
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +205 -56
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/bridge/index.d.ts.map +1 -1
- package/dist/auto-ui/bridge/index.js +578 -0
- package/dist/auto-ui/bridge/index.js.map +1 -1
- package/dist/auto-ui/bridge/renderers.d.ts.map +1 -1
- package/dist/auto-ui/bridge/renderers.js +7 -3
- package/dist/auto-ui/bridge/renderers.js.map +1 -1
- package/dist/auto-ui/bridge/types.d.ts +6 -0
- package/dist/auto-ui/bridge/types.d.ts.map +1 -1
- package/dist/auto-ui/frontend/pure-view.html +289 -0
- package/dist/auto-ui/photon-bridge.d.ts +11 -0
- package/dist/auto-ui/photon-bridge.d.ts.map +1 -1
- package/dist/auto-ui/photon-bridge.js +75 -1
- package/dist/auto-ui/photon-bridge.js.map +1 -1
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +29 -3
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/beam-form.bundle.js +5707 -0
- package/dist/beam-form.bundle.js.map +7 -0
- package/dist/beam.bundle.js +2863 -895
- package/dist/beam.bundle.js.map +4 -4
- package/dist/claude-code-plugin.js +11 -3
- package/dist/claude-code-plugin.js.map +1 -1
- package/dist/cli/commands/build.js +1 -1
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +7 -5
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/info.d.ts.map +1 -1
- package/dist/cli/commands/info.js +18 -4
- package/dist/cli/commands/info.js.map +1 -1
- package/dist/cli/commands/mcp.js +2 -2
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +6 -2
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli-alias.d.ts.map +1 -1
- package/dist/cli-alias.js +3 -2
- package/dist/cli-alias.js.map +1 -1
- package/dist/daemon/client.d.ts +5 -0
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +50 -0
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/manager.d.ts +15 -0
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +142 -11
- package/dist/daemon/manager.js.map +1 -1
- package/dist/daemon/worker-manager.js +1 -1
- package/dist/daemon/worker-manager.js.map +1 -1
- package/dist/deploy/cloudflare.d.ts.map +1 -1
- package/dist/deploy/cloudflare.js +12 -10
- package/dist/deploy/cloudflare.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +37 -2
- package/dist/loader.js.map +1 -1
- package/dist/marketplace-manager.d.ts +9 -0
- package/dist/marketplace-manager.d.ts.map +1 -1
- package/dist/marketplace-manager.js +115 -42
- package/dist/marketplace-manager.js.map +1 -1
- package/dist/meta.d.ts +51 -0
- package/dist/meta.d.ts.map +1 -0
- package/dist/meta.js +320 -0
- package/dist/meta.js.map +1 -0
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +30 -5
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/photon-doc-extractor.d.ts +1 -0
- package/dist/photon-doc-extractor.d.ts.map +1 -1
- package/dist/photon-doc-extractor.js +33 -21
- package/dist/photon-doc-extractor.js.map +1 -1
- package/dist/photons/builder-compass.photon.d.ts +167 -0
- package/dist/photons/builder-compass.photon.d.ts.map +1 -0
- package/dist/photons/builder-compass.photon.js +816 -0
- package/dist/photons/builder-compass.photon.js.map +1 -0
- package/dist/photons/builder-compass.photon.ts +1129 -0
- package/dist/photons/docs/ui/docs.html +441 -0
- package/dist/photons/docs.photon.d.ts +237 -0
- package/dist/photons/docs.photon.d.ts.map +1 -0
- package/dist/photons/docs.photon.js +483 -0
- package/dist/photons/docs.photon.js.map +1 -0
- package/dist/photons/docs.photon.ts +536 -0
- package/dist/photons/maker.photon.d.ts.map +1 -1
- package/dist/photons/maker.photon.js +19 -2
- package/dist/photons/maker.photon.js.map +1 -1
- package/dist/photons/maker.photon.ts +18 -2
- package/dist/photons/slides.photon.d.ts +212 -0
- package/dist/photons/slides.photon.d.ts.map +1 -0
- package/dist/photons/slides.photon.js +355 -0
- package/dist/photons/slides.photon.js.map +1 -0
- package/dist/photons/slides.photon.ts +370 -0
- package/dist/photons/spreadsheet/ui/spreadsheet.html +779 -0
- package/dist/photons/spreadsheet.photon.d.ts +554 -0
- package/dist/photons/spreadsheet.photon.d.ts.map +1 -0
- package/dist/photons/spreadsheet.photon.js +1050 -0
- package/dist/photons/spreadsheet.photon.js.map +1 -0
- package/dist/photons/spreadsheet.photon.ts +1239 -0
- package/dist/photons/ui/builder-compass.html +1199 -0
- package/dist/photons/ui/builder-compass.photon.html +380 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +33 -59
- package/dist/server.js.map +1 -1
- package/dist/shared/error-handler.d.ts +8 -0
- package/dist/shared/error-handler.d.ts.map +1 -1
- package/dist/shared/error-handler.js +50 -0
- package/dist/shared/error-handler.js.map +1 -1
- package/dist/shared-utils.d.ts +16 -2
- package/dist/shared-utils.d.ts.map +1 -1
- package/dist/shared-utils.js +37 -3
- package/dist/shared-utils.js.map +1 -1
- package/dist/template-manager.d.ts.map +1 -1
- package/dist/template-manager.js +2 -1
- package/dist/template-manager.js.map +1 -1
- package/package.json +8 -3
package/dist/auto-ui/beam.js
CHANGED
|
@@ -13,7 +13,7 @@ import * as path from 'path';
|
|
|
13
13
|
import * as os from 'os';
|
|
14
14
|
import { fileURLToPath } from 'url';
|
|
15
15
|
import { createHash } from 'crypto';
|
|
16
|
-
import { setSecurityHeaders, SimpleRateLimiter } from '../shared/security.js';
|
|
16
|
+
import { setSecurityHeaders, SimpleRateLimiter, escapeHtml } from '../shared/security.js';
|
|
17
17
|
/**
|
|
18
18
|
* Check if shell integration has been installed (photon init cli).
|
|
19
19
|
* Cached at module load since it won't change during a Beam session.
|
|
@@ -94,7 +94,7 @@ import { logger, createLogger } from '../shared/logger.js';
|
|
|
94
94
|
import { getErrorMessage } from '../shared/error-handler.js';
|
|
95
95
|
import { toEnvVarName } from '../shared/config-docs.js';
|
|
96
96
|
import { MarketplaceManager } from '../marketplace-manager.js';
|
|
97
|
-
import { subscribeChannel } from '../daemon/client.js';
|
|
97
|
+
import { subscribeChannel, reloadDaemonPhoton } from '../daemon/client.js';
|
|
98
98
|
import { ensurePhotonEditorDeclaration, writePhotonEditorDeclaration, } from '../photon-editor-declarations.js';
|
|
99
99
|
import { ensureDaemon } from '../daemon/manager.js';
|
|
100
100
|
import { SchemaExtractor, } from '@portel/photon-core';
|
|
@@ -144,6 +144,11 @@ const extractCspFromSource = extractCspFromModule;
|
|
|
144
144
|
* Example: { "chat": ["mentions", "direct-messages"], "tasks": ["deadline", "assigned-to-me"] }
|
|
145
145
|
*/
|
|
146
146
|
const photonNotificationSubscriptions = new Map();
|
|
147
|
+
/**
|
|
148
|
+
* Track which state-changed channels we've already subscribed to,
|
|
149
|
+
* so dynamically discovered photons can be subscribed without duplicates.
|
|
150
|
+
*/
|
|
151
|
+
const subscribedStateChannels = new Set();
|
|
147
152
|
/**
|
|
148
153
|
* Generate the service worker JS that validates the Beam backend
|
|
149
154
|
* on PWA launch and shows a diagnostic page if something is wrong.
|
|
@@ -192,6 +197,7 @@ self.addEventListener('fetch', (event) => {
|
|
|
192
197
|
url.pathname.startsWith('/api/') ||
|
|
193
198
|
url.pathname === '/sw.js' ||
|
|
194
199
|
url.pathname === '/beam.bundle.js' ||
|
|
200
|
+
url.pathname === '/beam-form.bundle.js' ||
|
|
195
201
|
url.pathname === '/beam-ts-worker.js'
|
|
196
202
|
) return;
|
|
197
203
|
|
|
@@ -479,18 +485,43 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
479
485
|
logger.warn(`Asset repair check failed: ${getErrorMessage(error)}`);
|
|
480
486
|
}
|
|
481
487
|
// Discover all photons with namespace metadata (user photons + bundled photons)
|
|
482
|
-
const
|
|
483
|
-
//
|
|
488
|
+
const scannedPhotonList = await listPhotonFilesWithNamespace(workingDir);
|
|
489
|
+
// Deduplicate aliases that resolve to the same underlying file (for example,
|
|
490
|
+
// a marketplace photon and a local alias/symlink that both point at the same
|
|
491
|
+
// source). Beam should not show both copies in the sidebar.
|
|
492
|
+
const userPhotonListDetailed = [];
|
|
493
|
+
const seenPhotonKeys = new Set();
|
|
494
|
+
for (const photon of scannedPhotonList) {
|
|
495
|
+
let realPath = photon.filePath;
|
|
496
|
+
try {
|
|
497
|
+
realPath = realpathSync(photon.filePath);
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
// Fall back to the discovered path if realpath resolution fails.
|
|
501
|
+
}
|
|
502
|
+
const dedupeKey = `${photon.name}::${realPath}`;
|
|
503
|
+
if (seenPhotonKeys.has(dedupeKey))
|
|
504
|
+
continue;
|
|
505
|
+
seenPhotonKeys.add(dedupeKey);
|
|
506
|
+
userPhotonListDetailed.push(photon);
|
|
507
|
+
}
|
|
508
|
+
// Detect name collisions and generate friendly duplicate labels. We should
|
|
509
|
+
// never leak namespace/owner names into the sidebar just because there are
|
|
510
|
+
// multiple copies of a photon with the same short name.
|
|
484
511
|
const nameOccurrences = new Map();
|
|
485
512
|
for (const p of userPhotonListDetailed) {
|
|
486
513
|
nameOccurrences.set(p.name, (nameOccurrences.get(p.name) || 0) + 1);
|
|
487
514
|
}
|
|
488
|
-
// Build photon list
|
|
489
|
-
// Also track resolved paths from namespace scan
|
|
515
|
+
// Build photon list with short names plus a numeric suffix for duplicates.
|
|
516
|
+
// Also track resolved paths from namespace scan.
|
|
490
517
|
const namespacePaths = new Map(); // displayName → filePath
|
|
491
518
|
const userPhotonList = [];
|
|
519
|
+
const duplicateIndex = new Map();
|
|
492
520
|
for (const p of userPhotonListDetailed) {
|
|
493
|
-
const
|
|
521
|
+
const duplicateCount = nameOccurrences.get(p.name) || 0;
|
|
522
|
+
const nextIndex = (duplicateIndex.get(p.name) || 0) + 1;
|
|
523
|
+
duplicateIndex.set(p.name, nextIndex);
|
|
524
|
+
const displayName = duplicateCount > 1 ? `${p.name} (${nextIndex})` : p.name;
|
|
494
525
|
userPhotonList.push(displayName);
|
|
495
526
|
namespacePaths.set(displayName, p.filePath);
|
|
496
527
|
}
|
|
@@ -643,6 +674,25 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
643
674
|
}
|
|
644
675
|
// Get UI assets for linking
|
|
645
676
|
const uiAssets = mcp.assets?.ui || [];
|
|
677
|
+
// If loader didn't resolve UI assets but source has @ui tags,
|
|
678
|
+
// extract them from source to populate linkedUi for sidebar
|
|
679
|
+
if (uiAssets.length === 0 && schemaSource) {
|
|
680
|
+
const uiTagRegex = /^\s*\*\s*@ui\s+(\S+)(?:\s+(\S+))?/gm;
|
|
681
|
+
let uiMatch;
|
|
682
|
+
const classUiMatch = schemaSource.match(/^\s*\*\s*@ui\s+(\S+)\s+(\.\/\S+)/m);
|
|
683
|
+
if (classUiMatch) {
|
|
684
|
+
// Class-level @ui tag with file path: @ui dashboard ./ui/dashboard.html
|
|
685
|
+
const uiId = classUiMatch[1]; // e.g., "dashboard"
|
|
686
|
+
// Add synthetic asset entry for all methods tagged with this @ui id
|
|
687
|
+
schemas.forEach((schema) => {
|
|
688
|
+
// Check if method has @ui tag matching this id
|
|
689
|
+
const methodSource = schemaSource.match(new RegExp(`@ui\\s+${uiId}[\\s\\n]*\\*/[\\s\\n]*(?:async\\s+)?${schema.name}\\s*\\(`, 'm'));
|
|
690
|
+
if (methodSource) {
|
|
691
|
+
uiAssets.push({ id: uiId, linkedTool: schema.name });
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
646
696
|
// Filter out lifecycle methods
|
|
647
697
|
const lifecycleMethods = ['onInitialize', 'onShutdown', 'constructor'];
|
|
648
698
|
const methods = schemas
|
|
@@ -994,7 +1044,12 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
994
1044
|
return configurePhotonViaMCP(photonName, config, photons, photonMCPs, loader, savedConfig, workingDir, activeLoads);
|
|
995
1045
|
},
|
|
996
1046
|
reloadPhoton: async (photonName) => {
|
|
997
|
-
return reloadPhotonViaMCP(photonName, photons, photonMCPs, loader, savedConfig, broadcastPhotonChange, activeLoads)
|
|
1047
|
+
return reloadPhotonViaMCP(photonName, photons, photonMCPs, loader, savedConfig, broadcastPhotonChange, activeLoads, (name, path, isStateful) => {
|
|
1048
|
+
if (isStateful) {
|
|
1049
|
+
subscribeStatefulPhoton(name).catch(() => { });
|
|
1050
|
+
reloadDaemonPhoton(name, path, workingDir).catch(() => { });
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
998
1053
|
},
|
|
999
1054
|
removePhoton: async (photonName) => {
|
|
1000
1055
|
return removePhotonViaMCP(photonName, photons, photonMCPs, savedConfig, broadcastPhotonChange, workingDir);
|
|
@@ -1174,7 +1229,24 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
1174
1229
|
}
|
|
1175
1230
|
catch {
|
|
1176
1231
|
res.writeHead(404);
|
|
1177
|
-
res.end('Bundle not found. Run
|
|
1232
|
+
res.end('Bundle not found. Run the beam build script first.');
|
|
1233
|
+
}
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
// Serve form components bundle (invoke-form + custom inputs for pure-view)
|
|
1237
|
+
if (url.pathname === '/beam-form.bundle.js') {
|
|
1238
|
+
try {
|
|
1239
|
+
const formBundlePath = path.join(__dirname, '../../dist/beam-form.bundle.js');
|
|
1240
|
+
const content = await fs.readFile(formBundlePath, 'utf-8');
|
|
1241
|
+
res.writeHead(200, {
|
|
1242
|
+
'Content-Type': 'text/javascript',
|
|
1243
|
+
'Cache-Control': 'no-cache',
|
|
1244
|
+
});
|
|
1245
|
+
res.end(content);
|
|
1246
|
+
}
|
|
1247
|
+
catch {
|
|
1248
|
+
res.writeHead(404);
|
|
1249
|
+
res.end('Form bundle not found. Run the beam build script first.');
|
|
1178
1250
|
}
|
|
1179
1251
|
return;
|
|
1180
1252
|
}
|
|
@@ -1190,7 +1262,7 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
1190
1262
|
}
|
|
1191
1263
|
catch {
|
|
1192
1264
|
res.writeHead(404);
|
|
1193
|
-
res.end('TS worker not found. Run
|
|
1265
|
+
res.end('TS worker not found. Run the beam build script first.');
|
|
1194
1266
|
}
|
|
1195
1267
|
return;
|
|
1196
1268
|
}
|
|
@@ -1637,6 +1709,43 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
1637
1709
|
res.end(html);
|
|
1638
1710
|
return;
|
|
1639
1711
|
}
|
|
1712
|
+
// Pure view mode — serve lightweight bridge-powered page (no beam-app shell).
|
|
1713
|
+
// All view modes (form, result, embed) use pure-view.html with the bridge.
|
|
1714
|
+
// Form views lazy-load the form-components bundle for custom inputs.
|
|
1715
|
+
const viewParam = url.searchParams.get('view');
|
|
1716
|
+
if ((viewParam === 'result' || viewParam === 'embed' || viewParam === 'form') &&
|
|
1717
|
+
url.pathname !== '/' &&
|
|
1718
|
+
!url.pathname.startsWith('/api')) {
|
|
1719
|
+
try {
|
|
1720
|
+
const pathParts = url.pathname.split('/').filter(Boolean);
|
|
1721
|
+
const photonName = pathParts[0] || '';
|
|
1722
|
+
const methodName = pathParts[1] || '';
|
|
1723
|
+
const params = {};
|
|
1724
|
+
for (const [k, v] of url.searchParams) {
|
|
1725
|
+
if (k !== 'view')
|
|
1726
|
+
params[k] = v;
|
|
1727
|
+
}
|
|
1728
|
+
const pureViewPath = path.join(__dirname, 'frontend/pure-view.html');
|
|
1729
|
+
let html = await fs.readFile(pureViewPath, 'utf-8');
|
|
1730
|
+
// Replace placeholders
|
|
1731
|
+
const argsJson = escapeHtml(JSON.stringify(params));
|
|
1732
|
+
html = html
|
|
1733
|
+
.replaceAll('__PHOTON__', encodeURIComponent(photonName))
|
|
1734
|
+
.replaceAll('__METHOD__', escapeHtml(methodName))
|
|
1735
|
+
.replaceAll('__ARGS__', argsJson);
|
|
1736
|
+
// Add data-view for form and result modes
|
|
1737
|
+
if (viewParam === 'form' || viewParam === 'result') {
|
|
1738
|
+
html = html.replace('data-method=', `data-view="${escapeHtml(viewParam)}" data-method=`);
|
|
1739
|
+
}
|
|
1740
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1741
|
+
res.end(html);
|
|
1742
|
+
}
|
|
1743
|
+
catch (err) {
|
|
1744
|
+
res.writeHead(500);
|
|
1745
|
+
res.end('Error serving pure view: ' + String(err));
|
|
1746
|
+
}
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1640
1749
|
// Default route: Serve Lit App
|
|
1641
1750
|
if (url.pathname === '/' || !url.pathname.startsWith('/api')) {
|
|
1642
1751
|
try {
|
|
@@ -1646,6 +1755,8 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
1646
1755
|
if (_shellIntegrationInstalled) {
|
|
1647
1756
|
content = content.replace('</head>', '<script>window.__PHOTON_SHELL_INIT=true</script></head>');
|
|
1648
1757
|
}
|
|
1758
|
+
// Note: ?view=form and ?view=result are handled by pure-view.html above.
|
|
1759
|
+
// This default route only serves the full Beam app for the main UI.
|
|
1649
1760
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1650
1761
|
res.end(content);
|
|
1651
1762
|
}
|
|
@@ -2049,6 +2160,16 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2049
2160
|
}
|
|
2050
2161
|
// else: photon was removed while we were reloading — discard result
|
|
2051
2162
|
}
|
|
2163
|
+
// Subscribe to state-changed events for newly discovered stateful photons
|
|
2164
|
+
if (isStateful) {
|
|
2165
|
+
subscribeStatefulPhoton(photonName).catch((err) => {
|
|
2166
|
+
logger.warn(`Failed to subscribe dynamically to ${photonName}: ${getErrorMessage(err)}`);
|
|
2167
|
+
});
|
|
2168
|
+
// Tell daemon to reload so its instance has the new methods/code
|
|
2169
|
+
reloadDaemonPhoton(photonName, photonPath, workingDir).catch((err) => {
|
|
2170
|
+
logger.debug(`Daemon reload for ${photonName}: ${getErrorMessage(err)}`);
|
|
2171
|
+
});
|
|
2172
|
+
}
|
|
2052
2173
|
// If this photon is symlinked and was previously errored (or new), set up
|
|
2053
2174
|
// source-directory watchers that may have been skipped at startup.
|
|
2054
2175
|
if (isNewPhoton || !previouslyConfigured) {
|
|
@@ -2265,6 +2386,79 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2265
2386
|
startup.ready();
|
|
2266
2387
|
// Notify connected clients that photon list is now available
|
|
2267
2388
|
broadcastPhotonChange();
|
|
2389
|
+
// Subscribe a single stateful photon to its state-changed daemon channel.
|
|
2390
|
+
// Extracted so it can be called both at startup and when new photons are discovered.
|
|
2391
|
+
async function subscribeStatefulPhoton(photonName) {
|
|
2392
|
+
const instanceNames = ['default'];
|
|
2393
|
+
for (const instanceName of instanceNames) {
|
|
2394
|
+
const channel = `${photonName}:${instanceName}:state-changed`;
|
|
2395
|
+
if (subscribedStateChannels.has(channel))
|
|
2396
|
+
continue;
|
|
2397
|
+
subscribedStateChannels.add(channel);
|
|
2398
|
+
subscribeChannel(photonName, channel, (message) => {
|
|
2399
|
+
// Sync Beam's local instance from the daemon-persisted state file BEFORE
|
|
2400
|
+
// notifying the frontend. The daemon persists state to disk after mutations,
|
|
2401
|
+
// and Beam's local instance must match before _silentRefresh re-queries it.
|
|
2402
|
+
void (async () => {
|
|
2403
|
+
if (message?.patch?.length > 0) {
|
|
2404
|
+
const mcp = photonMCPs.get(photonName);
|
|
2405
|
+
if (mcp?.instance) {
|
|
2406
|
+
const instName = instanceName || 'default';
|
|
2407
|
+
const stateFile = path.join(workingDir, 'state', photonName, `${instName}.json`);
|
|
2408
|
+
try {
|
|
2409
|
+
const json = await fs.readFile(stateFile, 'utf-8');
|
|
2410
|
+
const state = JSON.parse(json);
|
|
2411
|
+
for (const [key, value] of Object.entries(state)) {
|
|
2412
|
+
const target = mcp.instance[key];
|
|
2413
|
+
if (target && typeof target.splice === 'function' && Array.isArray(value)) {
|
|
2414
|
+
target.splice(0, target.length, ...value);
|
|
2415
|
+
}
|
|
2416
|
+
else {
|
|
2417
|
+
mcp.instance[key] = value;
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
catch {
|
|
2422
|
+
// State file not yet written — ignore
|
|
2423
|
+
// State file not yet written — ignore
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
if (message?.instance === instanceName || !message?.instance) {
|
|
2428
|
+
broadcastNotification('state-changed', {
|
|
2429
|
+
photon: photonName,
|
|
2430
|
+
instance: instanceName,
|
|
2431
|
+
patches: message?.patches,
|
|
2432
|
+
method: message?.method,
|
|
2433
|
+
params: message?.params,
|
|
2434
|
+
data: message?.data,
|
|
2435
|
+
...(message?.patch && { patch: message.patch }),
|
|
2436
|
+
...(message?.inversePatch && { inversePatch: message.inversePatch }),
|
|
2437
|
+
});
|
|
2438
|
+
}
|
|
2439
|
+
})();
|
|
2440
|
+
}, {
|
|
2441
|
+
reconnect: true,
|
|
2442
|
+
workingDir,
|
|
2443
|
+
onReconnect: () => logger.debug(`📡 Reconnected ${channel} subscription`),
|
|
2444
|
+
onRefreshNeeded: () => {
|
|
2445
|
+
logger.info(`📡 Refresh needed for ${channel} (events lost during daemon restart)`);
|
|
2446
|
+
broadcastNotification('state-changed', {
|
|
2447
|
+
photon: photonName,
|
|
2448
|
+
instance: instanceName,
|
|
2449
|
+
method: '_refresh',
|
|
2450
|
+
patches: undefined,
|
|
2451
|
+
});
|
|
2452
|
+
},
|
|
2453
|
+
})
|
|
2454
|
+
.then(() => {
|
|
2455
|
+
logger.info(`📡 Subscribed to ${channel} for cross-client sync`);
|
|
2456
|
+
})
|
|
2457
|
+
.catch((err) => {
|
|
2458
|
+
logger.warn(`Failed to subscribe to ${channel}: ${getErrorMessage(err)}`);
|
|
2459
|
+
});
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2268
2462
|
// Auto-start daemon and subscribe to state-changed events for stateful photons
|
|
2269
2463
|
// Uses reconnect: true so subscriptions survive daemon restarts
|
|
2270
2464
|
const statefulPhotons = photons.filter((p) => p.stateful && p.configured);
|
|
@@ -2272,52 +2466,7 @@ export async function startBeam(rawWorkingDir, port) {
|
|
|
2272
2466
|
try {
|
|
2273
2467
|
await ensureDaemon();
|
|
2274
2468
|
for (const photon of statefulPhotons) {
|
|
2275
|
-
|
|
2276
|
-
// Subscribe to 'default' instance + any other instances that appear
|
|
2277
|
-
const instanceNames = ['default'];
|
|
2278
|
-
for (const instanceName of instanceNames) {
|
|
2279
|
-
// Channel is now instance-specific: photon:instance:state-changed
|
|
2280
|
-
const channel = `${photonName}:${instanceName}:state-changed`;
|
|
2281
|
-
subscribeChannel(photonName, channel, (message) => {
|
|
2282
|
-
// Only broadcast if instance matches (prevents cross-instance leakage)
|
|
2283
|
-
if (message?.instance === instanceName || !message?.instance) {
|
|
2284
|
-
// Minimal transmission: include instance and patches for global sync
|
|
2285
|
-
broadcastNotification('state-changed', {
|
|
2286
|
-
photon: photonName,
|
|
2287
|
-
instance: instanceName,
|
|
2288
|
-
// JSON Patch array for client-side state sync
|
|
2289
|
-
patches: message?.patches,
|
|
2290
|
-
// Keep legacy fields for backward compatibility
|
|
2291
|
-
method: message?.method,
|
|
2292
|
-
params: message?.params,
|
|
2293
|
-
data: message?.data,
|
|
2294
|
-
// Optional fields for undo/redo support
|
|
2295
|
-
...(message?.patch && { patch: message.patch }),
|
|
2296
|
-
...(message?.inversePatch && { inversePatch: message.inversePatch }),
|
|
2297
|
-
});
|
|
2298
|
-
}
|
|
2299
|
-
}, {
|
|
2300
|
-
reconnect: true,
|
|
2301
|
-
workingDir,
|
|
2302
|
-
onReconnect: () => logger.debug(`📡 Reconnected ${channel} subscription`),
|
|
2303
|
-
onRefreshNeeded: () => {
|
|
2304
|
-
logger.info(`📡 Refresh needed for ${channel} (events lost during daemon restart)`);
|
|
2305
|
-
// Broadcast minimal refresh signal to all clients
|
|
2306
|
-
broadcastNotification('state-changed', {
|
|
2307
|
-
photon: photonName,
|
|
2308
|
-
instance: instanceName,
|
|
2309
|
-
method: '_refresh',
|
|
2310
|
-
patches: undefined, // No patches, signal full refresh needed
|
|
2311
|
-
});
|
|
2312
|
-
},
|
|
2313
|
-
})
|
|
2314
|
-
.then(() => {
|
|
2315
|
-
logger.info(`📡 Subscribed to ${channel} for cross-client sync`);
|
|
2316
|
-
})
|
|
2317
|
-
.catch((err) => {
|
|
2318
|
-
logger.warn(`Failed to subscribe to ${channel}: ${getErrorMessage(err)}`);
|
|
2319
|
-
});
|
|
2320
|
-
}
|
|
2469
|
+
await subscribeStatefulPhoton(photon.name);
|
|
2321
2470
|
}
|
|
2322
2471
|
}
|
|
2323
2472
|
catch (err) {
|