@burdenoff/fluidgrids-fe-sdk 2026.1029.3 → 2026.1029.5

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.
@@ -1,6 +1,7 @@
1
- export { WorkflowBuilder } from './chunk-HYFLZNRK.js';
1
+ export { WorkflowBuilder } from './chunk-JUA57OVN.js';
2
2
  import './chunk-LGGHQ7UE.js';
3
- import './chunk-E2OQHCFG.js';
3
+ import './chunk-PIKYQE7P.js';
4
+ import './chunk-O24MDGZY.js';
4
5
  import './chunk-GEMNVFPK.js';
5
6
  import './chunk-IEVSFCFS.js';
6
7
  import './chunk-YXMFRQYV.js';
@@ -47,4 +47,63 @@ declare const AiCreditBalanceSlot: FC<FgAiCreditBalanceProps>;
47
47
  */
48
48
  declare function notifyFluidGridsAiCreditsUpdated(): void;
49
49
 
50
- export { AiCreditBalanceSlot, FLUIDGRIDS_AI_CREDITS_UPDATED_EVENT, FLUIDGRIDS_AI_PRODUCT_KEY, type FgAiCreditBalanceProps, FluidGridsAiCreditsBridge, type FluidGridsAiCreditsBridgeProps, notifyFluidGridsAiCreditsUpdated };
50
+ /** Mirror of fe-libs' `AssistantPreviewRun`. */
51
+ interface FgAssistantRun {
52
+ readonly runId: string;
53
+ readonly conversationId: string;
54
+ readonly messageId: string;
55
+ /** The workspace the run started in. */
56
+ readonly workspaceId: string | null;
57
+ /** The actor the run started as. */
58
+ readonly actorId: string | null;
59
+ }
60
+ /** Mirror of fe-libs' `action-result` preview event: one approved write's root field. */
61
+ interface FgAssistantActionResult {
62
+ kind: "action-result";
63
+ run: FgAssistantRun;
64
+ action: {
65
+ id: string;
66
+ operation: string;
67
+ responseKey: string;
68
+ /** Approved inputs, with sensitive arguments omitted. */
69
+ arguments: Readonly<Record<string, unknown>>;
70
+ /** The redacted GraphQL response. */
71
+ result: unknown;
72
+ };
73
+ }
74
+ /**
75
+ * Mirror of fe-libs' `AssistantPreviewEvent`. Only `action-result` is read;
76
+ * the other kinds are listed loosely so every fe-libs event is accepted.
77
+ */
78
+ type FgAssistantPreviewEvent = FgAssistantActionResult | {
79
+ kind: "start" | "snapshot" | "end";
80
+ run: FgAssistantRun;
81
+ };
82
+ /** Mirror of fe-libs' `AssistantPreviewController`, headless. */
83
+ interface FgAssistantPreviewController {
84
+ onEvent: (event: FgAssistantPreviewEvent) => void;
85
+ }
86
+ interface FluidGridsAssistantLiveCanvasOptions {
87
+ /**
88
+ * The signed-in actor now. When given, a write from a run started by another
89
+ * actor (a late result after an account switch) is ignored. Read on every
90
+ * event, so it may read a token lazily.
91
+ */
92
+ getActorId?: () => string | null;
93
+ }
94
+ /**
95
+ * Build the headless controller outside React (one per panel mount).
96
+ * `options` is read on every event.
97
+ */
98
+ declare function createFluidGridsAssistantLiveCanvas(options?: FluidGridsAssistantLiveCanvasOptions): FgAssistantPreviewController;
99
+ /**
100
+ * The headless preview controller for the shared assistant panel, STABLE for
101
+ * the life of the calling component (fe-libs requires a stable controller).
102
+ * The latest `getActorId` is used on every event without changing identity.
103
+ *
104
+ * const liveCanvas = useFluidGridsAssistantLiveCanvas({ getActorId });
105
+ * const panelProps = useMemo(() => ({ ..., preview: liveCanvas }), [liveCanvas, ...]);
106
+ */
107
+ declare function useFluidGridsAssistantLiveCanvas(options?: FluidGridsAssistantLiveCanvasOptions): FgAssistantPreviewController;
108
+
109
+ export { AiCreditBalanceSlot, FLUIDGRIDS_AI_CREDITS_UPDATED_EVENT, FLUIDGRIDS_AI_PRODUCT_KEY, type FgAiCreditBalanceProps, type FgAssistantActionResult, type FgAssistantPreviewController, type FgAssistantPreviewEvent, type FgAssistantRun, FluidGridsAiCreditsBridge, type FluidGridsAiCreditsBridgeProps, type FluidGridsAssistantLiveCanvasOptions, createFluidGridsAssistantLiveCanvas, notifyFluidGridsAiCreditsUpdated, useFluidGridsAssistantLiveCanvas };
package/dist/assistant.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { createFluidGridsAssistantLiveCanvas, useFluidGridsAssistantLiveCanvas } from './chunk-PIKYQE7P.js';
1
2
  export { AiCreditBalanceSlot, FLUIDGRIDS_AI_CREDITS_UPDATED_EVENT, FLUIDGRIDS_AI_PRODUCT_KEY, FluidGridsAiCreditsBridge, notifyFluidGridsAiCreditsUpdated } from './chunk-Y53CVZVV.js';
2
3
  export { FluidGridsAssistantBridge, useFluidGridsPageControls } from './chunk-PJAWNFJV.js';
3
4
  import './chunk-62GWEYJ5.js';
@@ -1,8 +1,12 @@
1
1
  import { isGuidedTourIntent, pickGuidedTour, withoutGuidedTourIntent, isCreateIntent, CreateWorkflowRefusalDialog, decideMissingWorkflow } from './chunk-LGGHQ7UE.js';
2
- import { useInstalledNodes, buildNodeCheck, FlowBuilderPage, buildRequiredNodeTypes } from './chunk-E2OQHCFG.js';
2
+ import { registerLiveCanvasHandler } from './chunk-PIKYQE7P.js';
3
+ import { useInstalledNodes, buildNodeCheck, FlowBuilderPage, buildRequiredNodeTypes } from './chunk-O24MDGZY.js';
3
4
  import { useLazyWorkflowDefinition, useCreateWorkflow } from './chunk-SRE2JVSJ.js';
4
- import { useFlowStore, refetchStoreNodes, BILLING_PLANS_PATH, codeBuildIdOf, mergeStickyNotes, blankWorkflowCreateInput, classifyCreateRefusal } from './chunk-2U56452G.js';
5
+ import { useFlowStore, refetchStoreNodes, BILLING_PLANS_PATH, codeBuildIdOf, mergeStickyNotes, blankWorkflowCreateInput, classifyCreateRefusal, useSaveStatusStore, workflowFingerprint, registerWorkflowNodeTypes, throwIfResultError } from './chunk-2U56452G.js';
6
+ import { getFluidGridsConfig } from './chunk-CY6KX4RG.js';
7
+ import { WorkflowDefinitionDocument } from './chunk-7PP6CRXS.js';
5
8
  import { useTr } from './chunk-UULVIT25.js';
9
+ import { getFluidGridsClient } from './chunk-NTWG3CJX.js';
6
10
  import { useLocation, useNavigate, useParams } from 'react-router';
7
11
  import { toast } from 'sonner';
8
12
  import { Icon } from '@iconify/react';
@@ -12,6 +16,654 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
12
16
  import { ErrorBoundary } from '@burdenoff/fe-libs/shared/components';
13
17
  import '@xyflow/react/dist/style.css';
14
18
 
19
+ // src/builder/live/canvas-base.ts
20
+ var base = null;
21
+ function currentCanvasBase() {
22
+ const flow = useFlowStore.getState();
23
+ if (!base || base.generation !== flow.loadGeneration || base.dbId !== flow.workflow.dbId) {
24
+ return null;
25
+ }
26
+ return base;
27
+ }
28
+ function setCanvasBase(next) {
29
+ base = next;
30
+ }
31
+ function snapshotOf(state) {
32
+ const { workflow } = state;
33
+ return {
34
+ generation: state.loadGeneration,
35
+ dbId: workflow.dbId,
36
+ nodes: workflow.nodes,
37
+ edges: workflow.edges,
38
+ name: workflow.name ?? "",
39
+ alias: workflow.alias ?? ""
40
+ };
41
+ }
42
+ function captureHydratedCanvasBase() {
43
+ const flow = useFlowStore.getState();
44
+ if (!flow.workflow.dbId || currentCanvasBase()) return false;
45
+ base = snapshotOf(flow);
46
+ return true;
47
+ }
48
+ function captureCanvasBaseIfInSync() {
49
+ const flow = useFlowStore.getState();
50
+ const { baseline } = useSaveStatusStore.getState();
51
+ if (!baseline || baseline.generation !== flow.loadGeneration) return false;
52
+ if (workflowFingerprint(flow.workflow) !== baseline.fingerprint) return false;
53
+ base = snapshotOf(flow);
54
+ return true;
55
+ }
56
+ function watchCanvasBase() {
57
+ if (!captureCanvasBaseIfInSync()) captureHydratedCanvasBase();
58
+ const unsubscribeStatus = useSaveStatusStore.subscribe((state, previous) => {
59
+ if (state.baseline !== previous.baseline) captureCanvasBaseIfInSync();
60
+ });
61
+ const unsubscribeFlow = useFlowStore.subscribe((state, previous) => {
62
+ if (state.loadGeneration !== previous.loadGeneration) base = null;
63
+ if (state.workflow.dbId && state.workflow !== previous.workflow) {
64
+ captureHydratedCanvasBase();
65
+ }
66
+ });
67
+ return () => {
68
+ unsubscribeStatus();
69
+ unsubscribeFlow();
70
+ };
71
+ }
72
+
73
+ // src/builder/live/assistant-writes.ts
74
+ var IN_PLACE = /* @__PURE__ */ new Set([
75
+ "updateWorkflowDefinition",
76
+ "pinNodeData",
77
+ "unpinNodeData",
78
+ "activateWorkflowDefinition",
79
+ "deactivateWorkflowDefinition"
80
+ ]);
81
+ var VERSION_RESPONSE = /* @__PURE__ */ new Set([
82
+ "createWorkflowVersion",
83
+ "restoreWorkflowVersion"
84
+ ]);
85
+ function isRecord(value) {
86
+ return typeof value === "object" && value !== null && !Array.isArray(value);
87
+ }
88
+ function text(value) {
89
+ return typeof value === "string" && value.length > 0 ? value : null;
90
+ }
91
+ function rootFieldResult(result, responseKey, operation) {
92
+ let value = result;
93
+ if (typeof value === "string") {
94
+ try {
95
+ value = JSON.parse(value);
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+ if (!isRecord(value)) return null;
101
+ if (Array.isArray(value.errors) && value.errors.length > 0) return null;
102
+ const data = isRecord(value.data) ? value.data : value;
103
+ const root = data[responseKey] ?? data[operation];
104
+ return isRecord(root) ? root : null;
105
+ }
106
+ function classifyWorkflowWrite(action) {
107
+ const { operation, responseKey } = action;
108
+ const args = action.arguments ?? {};
109
+ const root = rootFieldResult(action.result, responseKey, operation);
110
+ if (IN_PLACE.has(operation)) {
111
+ const dbId = text(args.id) ?? text(root?.id);
112
+ return dbId ? { kind: "in-place", dbId } : null;
113
+ }
114
+ if (VERSION_RESPONSE.has(operation)) {
115
+ if (root?.success === false) return null;
116
+ const workflow = isRecord(root?.workflow) ? root.workflow : null;
117
+ const workflowKey = text(args.workflowKey) ?? text(workflow?.workflowKey);
118
+ if (!workflowKey) return null;
119
+ return {
120
+ kind: "new-version",
121
+ workflowKey,
122
+ version: text(workflow?.version),
123
+ dbId: text(workflow?.id)
124
+ };
125
+ }
126
+ if (operation === "applyWorkflowProposal") {
127
+ const workflowKey = text(root?.appliedWorkflowKey);
128
+ if (!workflowKey) return null;
129
+ return {
130
+ kind: "new-version",
131
+ workflowKey,
132
+ version: text(root?.appliedVersion),
133
+ dbId: text(root?.appliedWorkflowId)
134
+ };
135
+ }
136
+ return null;
137
+ }
138
+
139
+ // src/builder/live/definition-canvas.ts
140
+ function isRecord2(value) {
141
+ return typeof value === "object" && value !== null && !Array.isArray(value);
142
+ }
143
+ function hasId(item) {
144
+ return isRecord2(item) && typeof item.id === "string" && item.id.length > 0;
145
+ }
146
+ function canvasFromDefinition(definitionJson) {
147
+ let definition = definitionJson;
148
+ if (typeof definition === "string") {
149
+ try {
150
+ definition = JSON.parse(definition);
151
+ } catch {
152
+ return null;
153
+ }
154
+ }
155
+ if (!isRecord2(definition)) return null;
156
+ const data = isRecord2(definition.data) ? definition.data : definition;
157
+ if (!Array.isArray(data.nodes)) return null;
158
+ const nodes = mergeStickyNotes({
159
+ nodes: data.nodes,
160
+ stickyNotes: Array.isArray(data.stickyNotes) ? data.stickyNotes : []
161
+ }).filter(hasId);
162
+ const edges = (Array.isArray(data.edges) ? data.edges : []).filter(
163
+ hasId
164
+ );
165
+ return { nodes, edges };
166
+ }
167
+
168
+ // src/builder/live/graph-merge.ts
169
+ var VOLATILE_NODE_KEYS = /* @__PURE__ */ new Set([
170
+ "selected",
171
+ "dragging",
172
+ "resizing",
173
+ "measured",
174
+ "positionAbsolute"
175
+ ]);
176
+ var VOLATILE_EDGE_KEYS = /* @__PURE__ */ new Set(["selected"]);
177
+ var NODE_STRUCTURE_KEYS = /* @__PURE__ */ new Set(["id", "type", "position", "data"]);
178
+ var CARRIED_NODE_KEYS = ["selected", "measured"];
179
+ var CARRIED_EDGE_KEYS = ["selected"];
180
+ function isRecord3(value) {
181
+ return typeof value === "object" && value !== null && !Array.isArray(value);
182
+ }
183
+ function stableStringify(value) {
184
+ return JSON.stringify(value, (_key, inner) => {
185
+ if (isRecord3(inner)) {
186
+ return Object.fromEntries(
187
+ Object.entries(inner).sort(
188
+ ([a], [b]) => a < b ? -1 : a > b ? 1 : 0
189
+ )
190
+ );
191
+ }
192
+ return inner;
193
+ }) ?? "";
194
+ }
195
+ function canonicalNode(node) {
196
+ const entries = Object.entries(node);
197
+ const data = node.data;
198
+ const dataRecord = isRecord3(data) ? data : null;
199
+ const kept = entries.filter(([key]) => {
200
+ if (VOLATILE_NODE_KEYS.has(key)) return false;
201
+ if (NODE_STRUCTURE_KEYS.has(key)) return true;
202
+ return !(dataRecord && key in dataRecord);
203
+ });
204
+ const projected = Object.fromEntries(kept);
205
+ if (dataRecord) projected.data = savedNodeSettings(dataRecord);
206
+ return stableStringify(projected);
207
+ }
208
+ function savedNodeSettings(data) {
209
+ const nodeData = data.nodeData;
210
+ if (!isRecord3(nodeData)) return data;
211
+ const opName = data.operation_name;
212
+ const rawOps = nodeData.operations;
213
+ const ops = Array.isArray(rawOps) ? rawOps : isRecord3(rawOps) && Array.isArray(rawOps.operations) ? rawOps.operations : [];
214
+ const current = typeof opName === "string" && opName.length > 0 ? ops.find((op) => isRecord3(op) && op.name === opName) : void 0;
215
+ if (!current) {
216
+ const { nodeData: _unsaved, ...rest } = data;
217
+ return rest;
218
+ }
219
+ return { ...data, nodeData: { ...nodeData, operations: [current] } };
220
+ }
221
+ function canonicalEdge(edge) {
222
+ return stableStringify(
223
+ Object.fromEntries(
224
+ Object.entries(edge).filter(([key]) => !VOLATILE_EDGE_KEYS.has(key))
225
+ )
226
+ );
227
+ }
228
+ function indexById(items) {
229
+ const map = /* @__PURE__ */ new Map();
230
+ for (const item of items) {
231
+ if (typeof item?.id === "string" && !map.has(item.id))
232
+ map.set(item.id, item);
233
+ }
234
+ return map;
235
+ }
236
+ function carry(from, onto, keys) {
237
+ const carried = {};
238
+ for (const key of keys) {
239
+ if (key in from) carried[key] = from[key];
240
+ }
241
+ return Object.keys(carried).length === 0 ? onto : { ...onto, ...carried };
242
+ }
243
+ function mergeItems(kind, base2, local, remote, canonical, carriedKeys) {
244
+ const baseById = indexById(base2);
245
+ const localById = indexById(local);
246
+ const remoteById = indexById(remote);
247
+ const counts = { added: 0, updated: 0, removed: 0 };
248
+ const touched = [];
249
+ const conflicts = [];
250
+ const items = [];
251
+ let localEdits = false;
252
+ const conflict = (id, reason) => conflicts.push({ kind, id, reason });
253
+ for (const [id, mine] of localById) {
254
+ const original = baseById.get(id);
255
+ const theirs = remoteById.get(id);
256
+ const mineKey = canonical(mine);
257
+ if (!original) {
258
+ localEdits = true;
259
+ if (theirs && canonical(theirs) !== mineKey) conflict(id, "both-changed");
260
+ items.push(mine);
261
+ continue;
262
+ }
263
+ const originalKey = canonical(original);
264
+ const localChanged = mineKey !== originalKey;
265
+ if (localChanged) localEdits = true;
266
+ if (!theirs) {
267
+ if (localChanged) {
268
+ conflict(id, "deleted-remotely");
269
+ items.push(mine);
270
+ } else {
271
+ counts.removed += 1;
272
+ }
273
+ continue;
274
+ }
275
+ const theirsKey = canonical(theirs);
276
+ if (theirsKey === originalKey || theirsKey === mineKey) {
277
+ items.push(mine);
278
+ } else if (!localChanged) {
279
+ items.push(carry(mine, theirs, carriedKeys));
280
+ counts.updated += 1;
281
+ touched.push(id);
282
+ } else {
283
+ conflict(id, "both-changed");
284
+ items.push(mine);
285
+ }
286
+ }
287
+ for (const [id, original] of baseById) {
288
+ if (localById.has(id)) continue;
289
+ localEdits = true;
290
+ const theirs = remoteById.get(id);
291
+ if (theirs && canonical(theirs) !== canonical(original)) {
292
+ conflict(id, "deleted-locally");
293
+ }
294
+ }
295
+ for (const [id, theirs] of remoteById) {
296
+ if (localById.has(id) || baseById.has(id)) continue;
297
+ items.push(theirs);
298
+ counts.added += 1;
299
+ touched.push(id);
300
+ }
301
+ return { items, counts, touched, conflicts, localEdits };
302
+ }
303
+ function mergeItemsWithoutBase(kind, local, remote, canonical) {
304
+ const localById = indexById(local);
305
+ const remoteById = indexById(remote);
306
+ const conflicts = [];
307
+ for (const [id, mine] of localById) {
308
+ const theirs = remoteById.get(id);
309
+ if (!theirs || canonical(theirs) !== canonical(mine)) {
310
+ conflicts.push({ kind, id, reason: "no-base" });
311
+ }
312
+ }
313
+ for (const id of remoteById.keys()) {
314
+ if (!localById.has(id)) conflicts.push({ kind, id, reason: "no-base" });
315
+ }
316
+ return {
317
+ items: [...localById.values()],
318
+ counts: { added: 0, updated: 0, removed: 0 },
319
+ touched: [],
320
+ conflicts,
321
+ // Unknown, so assume the canvas holds edits: the save baseline stays.
322
+ localEdits: true
323
+ };
324
+ }
325
+ function mergeWorkflowGraph(base2, local, remote) {
326
+ const nodes = base2 ? mergeItems(
327
+ "node",
328
+ base2.nodes,
329
+ local.nodes,
330
+ remote.nodes,
331
+ canonicalNode,
332
+ CARRIED_NODE_KEYS
333
+ ) : mergeItemsWithoutBase("node", local.nodes, remote.nodes, canonicalNode);
334
+ const edges = base2 ? mergeItems(
335
+ "edge",
336
+ base2.edges,
337
+ local.edges,
338
+ remote.edges,
339
+ canonicalEdge,
340
+ CARRIED_EDGE_KEYS
341
+ ) : mergeItemsWithoutBase("edge", local.edges, remote.edges, canonicalEdge);
342
+ const nodeIds = new Set(nodes.items.map((node) => node.id));
343
+ const localEdgeIds = new Set(local.edges.map((edge) => edge.id));
344
+ const remoteTouched = new Set(edges.touched);
345
+ let detached = 0;
346
+ const keptEdges = edges.items.filter((edge) => {
347
+ const { source, target } = edge;
348
+ const attached = (typeof source !== "string" || nodeIds.has(source)) && (typeof target !== "string" || nodeIds.has(target));
349
+ if (attached) return true;
350
+ if (!localEdgeIds.has(edge.id)) {
351
+ edges.counts.added -= 1;
352
+ return false;
353
+ }
354
+ if (remoteTouched.has(edge.id)) edges.counts.updated -= 1;
355
+ edges.conflicts.push({ kind: "edge", id: edge.id, reason: "detached" });
356
+ detached += 1;
357
+ return false;
358
+ });
359
+ const total = (c) => c.added + c.updated + c.removed;
360
+ return {
361
+ nodes: nodes.items,
362
+ edges: keptEdges,
363
+ counts: { nodes: nodes.counts, edges: edges.counts },
364
+ touchedNodeIds: nodes.touched,
365
+ conflicts: [...nodes.conflicts, ...edges.conflicts],
366
+ changed: total(nodes.counts) + total(edges.counts) + detached > 0,
367
+ localEdits: nodes.localEdits || edges.localEdits
368
+ };
369
+ }
370
+ function mergeScalar(base2, local, remote) {
371
+ if (local === base2) return { value: remote, conflict: false };
372
+ return { value: local, conflict: remote !== base2 && remote !== local };
373
+ }
374
+
375
+ // src/builder/live/live-canvas-sync.ts
376
+ var ASSISTANT_SYNC_HISTORY_KIND = "assistant:sync";
377
+ var SETTLE_MS = 1500;
378
+ var SEEN_LIMIT = 200;
379
+ function createLiveCanvasSync(deps) {
380
+ const now = deps.now ?? Date.now;
381
+ const seen = /* @__PURE__ */ new Set();
382
+ let running = null;
383
+ let queued = false;
384
+ const remember = (id) => {
385
+ if (seen.has(id)) return false;
386
+ seen.add(id);
387
+ if (seen.size > SEEN_LIMIT) {
388
+ const oldest = seen.values().next().value;
389
+ if (oldest !== void 0) seen.delete(oldest);
390
+ }
391
+ return true;
392
+ };
393
+ async function syncOnce() {
394
+ const flow = useFlowStore.getState();
395
+ const dbId = flow.workflow.dbId;
396
+ if (!dbId) return;
397
+ const generation = flow.loadGeneration;
398
+ let remote;
399
+ try {
400
+ remote = await deps.fetchDefinition(dbId);
401
+ } catch {
402
+ remote = null;
403
+ }
404
+ const after = useFlowStore.getState();
405
+ if (after.loadGeneration !== generation || after.workflow.dbId !== dbId) {
406
+ return;
407
+ }
408
+ const canvas = remote ? canvasFromDefinition(remote.definitionJson) : null;
409
+ if (!remote || !canvas) {
410
+ deps.notify({ kind: "failed" });
411
+ return;
412
+ }
413
+ applyRemote(remote, canvas);
414
+ }
415
+ function applyRemote(remote, canvas) {
416
+ const flow = useFlowStore.getState();
417
+ const { workflow, actions } = flow;
418
+ const localName = workflow.name ?? "";
419
+ const localAlias = workflow.alias ?? "";
420
+ const base2 = currentCanvasBase();
421
+ const merge = mergeWorkflowGraph(
422
+ base2,
423
+ { nodes: workflow.nodes, edges: workflow.edges },
424
+ canvas
425
+ );
426
+ const remoteName = remote.name ?? localName;
427
+ const remoteAlias = remote.alias ?? "";
428
+ const name = base2 ? mergeScalar(base2.name, localName, remoteName) : { value: localName};
429
+ const alias = base2 ? mergeScalar(base2.alias, localAlias, remoteAlias) : { value: localAlias};
430
+ const localEdits = !base2 || merge.localEdits || localName !== base2.name || localAlias !== base2.alias;
431
+ if (merge.changed) {
432
+ deps.registerNodeTypes(merge.nodes);
433
+ actions.nodes.setNodes(merge.nodes, {
434
+ historyKind: ASSISTANT_SYNC_HISTORY_KIND
435
+ });
436
+ actions.edges.setEdges(merge.edges, { skipHistory: true });
437
+ }
438
+ const patch = {};
439
+ if (name.value !== localName) patch.name = name.value;
440
+ if (alias.value !== localAlias) patch.alias = alias.value;
441
+ if (remote.status && remote.status !== workflow.status) {
442
+ patch.status = remote.status;
443
+ }
444
+ if (Object.keys(patch).length > 0) actions.setWorkflow(patch);
445
+ const synced = useFlowStore.getState();
446
+ setCanvasBase({
447
+ generation: synced.loadGeneration,
448
+ dbId: synced.workflow.dbId,
449
+ nodes: canvas.nodes,
450
+ edges: canvas.edges,
451
+ name: remoteName,
452
+ alias: remoteAlias
453
+ });
454
+ const status = useSaveStatusStore.getState();
455
+ if ((merge.changed || Object.keys(patch).length > 0) && !localEdits && status.baseline && status.baseline.generation === synced.loadGeneration) {
456
+ status.actions.setBaseline({
457
+ fingerprint: workflowFingerprint(synced.workflow),
458
+ generation: synced.loadGeneration,
459
+ settleUntil: now() + SETTLE_MS,
460
+ editRevision: synced.editRevision
461
+ });
462
+ }
463
+ deps.onSynced?.(remote);
464
+ const renamed = patch.name !== void 0 || patch.alias !== void 0;
465
+ if (merge.changed || merge.conflicts.length > 0 || renamed) {
466
+ deps.notify({ kind: "applied", merge, renamed });
467
+ }
468
+ }
469
+ function scheduleSync() {
470
+ if (running) {
471
+ queued = true;
472
+ return;
473
+ }
474
+ running = (async () => {
475
+ try {
476
+ do {
477
+ queued = false;
478
+ await syncOnce();
479
+ } while (queued);
480
+ } catch (error) {
481
+ console.error("[FluidGrids] assistant live canvas sync:", error);
482
+ } finally {
483
+ running = null;
484
+ }
485
+ })();
486
+ }
487
+ function handle(event) {
488
+ try {
489
+ const workspaceId = deps.workspaceId();
490
+ if (event.run.workspaceId && workspaceId && event.run.workspaceId !== workspaceId) {
491
+ return;
492
+ }
493
+ const write = classifyWorkflowWrite(event.action);
494
+ if (!write) return;
495
+ const { workflow } = useFlowStore.getState();
496
+ if (!workflow.dbId) return;
497
+ if (write.kind === "in-place") {
498
+ if (write.dbId !== workflow.dbId) return;
499
+ if (!remember(event.action.id)) return;
500
+ scheduleSync();
501
+ return;
502
+ }
503
+ const openKey = workflow.workflowKey || workflow.id;
504
+ if (write.workflowKey !== openKey) return;
505
+ if (write.dbId === workflow.dbId) {
506
+ if (remember(event.action.id)) scheduleSync();
507
+ return;
508
+ }
509
+ if (write.version !== null && write.version === workflow.version) return;
510
+ if (!remember(event.action.id)) return;
511
+ deps.notify({
512
+ kind: "new-version",
513
+ workflowKey: write.workflowKey,
514
+ version: write.version
515
+ });
516
+ } catch (error) {
517
+ console.error("[FluidGrids] assistant live canvas:", error);
518
+ }
519
+ }
520
+ return {
521
+ handle,
522
+ idle: async () => {
523
+ while (running) await running;
524
+ }
525
+ };
526
+ }
527
+
528
+ // src/builder/live/sync-notice.ts
529
+ var KEY = "fluidgrids.builder.assistantSync";
530
+ var NODE_PARTS = {
531
+ added: ["1 node added", "{{count}} nodes added"],
532
+ updated: ["1 node changed", "{{count}} nodes changed"],
533
+ removed: ["1 node removed", "{{count}} nodes removed"]
534
+ };
535
+ var EDGE_PARTS = {
536
+ added: ["1 connection added", "{{count}} connections added"],
537
+ updated: ["1 connection changed", "{{count}} connections changed"],
538
+ removed: ["1 connection removed", "{{count}} connections removed"]
539
+ };
540
+ function parts(tr, counts, kind) {
541
+ const words = kind === "nodes" ? NODE_PARTS : EDGE_PARTS;
542
+ return ["added", "updated", "removed"].filter((part) => counts[part] > 0).map((part) => {
543
+ const count = counts[part];
544
+ const [one, other] = words[part];
545
+ return count === 1 ? tr(`${KEY}.${kind}.${part}.one`, one) : tr(`${KEY}.${kind}.${part}.other`, other, { count });
546
+ });
547
+ }
548
+ function describeAppliedSync(tr, merge, renamed) {
549
+ const changes = [
550
+ ...parts(tr, merge.counts.nodes, "nodes"),
551
+ ...parts(tr, merge.counts.edges, "edges")
552
+ ];
553
+ if (renamed) changes.push(tr(`${KEY}.renamed`, "name changed"));
554
+ const nodeConflicts = merge.conflicts.filter((c) => c.kind === "node").length;
555
+ const edgeConflicts = merge.conflicts.length - nodeConflicts;
556
+ let conflicts = null;
557
+ if (nodeConflicts > 0 || edgeConflicts > 0) {
558
+ const what = [
559
+ nodeConflicts === 1 ? tr(`${KEY}.conflicts.nodes.one`, "1 node") : nodeConflicts > 1 ? tr(`${KEY}.conflicts.nodes.other`, "{{count}} nodes", {
560
+ count: nodeConflicts
561
+ }) : null,
562
+ edgeConflicts === 1 ? tr(`${KEY}.conflicts.edges.one`, "1 connection") : edgeConflicts > 1 ? tr(`${KEY}.conflicts.edges.other`, "{{count}} connections", {
563
+ count: edgeConflicts
564
+ }) : null
565
+ ].filter((item) => item !== null).join(tr(`${KEY}.and`, " and "));
566
+ conflicts = tr(
567
+ `${KEY}.conflicts.summary`,
568
+ "Kept your edits on {{what}} the assistant also changed",
569
+ { what }
570
+ );
571
+ }
572
+ return {
573
+ title: tr(`${KEY}.title`, "Assistant updated this workflow"),
574
+ summary: changes.length > 0 ? changes.join(", ") : null,
575
+ conflicts
576
+ };
577
+ }
578
+ function describeNewVersion(tr, version) {
579
+ if (!version) {
580
+ return {
581
+ title: tr(
582
+ `${KEY}.newVersion.unknown`,
583
+ "The assistant saved a new version of this workflow"
584
+ ),
585
+ action: null
586
+ };
587
+ }
588
+ return {
589
+ title: tr(
590
+ `${KEY}.newVersion.title`,
591
+ "The assistant saved version {{version}} of this workflow",
592
+ { version }
593
+ ),
594
+ action: tr(`${KEY}.newVersion.open`, "Open {{version}}", { version })
595
+ };
596
+ }
597
+ function describeFailedSync(tr) {
598
+ return tr(
599
+ `${KEY}.failed`,
600
+ "The assistant changed this workflow, but the canvas could not be refreshed. Reopen the workflow to see the change."
601
+ );
602
+ }
603
+
604
+ // src/builder/live/assistant-live-canvas-host.tsx
605
+ async function fetchDefinition(dbId) {
606
+ const { data } = throwIfResultError(
607
+ await getFluidGridsClient().query({
608
+ query: WorkflowDefinitionDocument,
609
+ variables: { id: dbId },
610
+ fetchPolicy: "network-only"
611
+ })
612
+ );
613
+ return data?.workflowDefinition ?? null;
614
+ }
615
+ function AssistantLiveCanvasHost() {
616
+ const tr = useTr();
617
+ const navigate = useNavigate();
618
+ const latest = useRef({ tr, navigate });
619
+ useEffect(() => {
620
+ latest.current = { tr, navigate };
621
+ });
622
+ useEffect(() => {
623
+ const notify = (notice) => {
624
+ const { tr: t, navigate: go } = latest.current;
625
+ if (notice.kind === "failed") {
626
+ toast.error(describeFailedSync(t));
627
+ return;
628
+ }
629
+ if (notice.kind === "new-version") {
630
+ const text3 = describeNewVersion(t, notice.version);
631
+ const { workflowKey, version } = notice;
632
+ toast.info(text3.title, {
633
+ dismissible: true,
634
+ duration: 1e4,
635
+ ...text3.action && version ? {
636
+ action: {
637
+ label: text3.action,
638
+ onClick: () => go(`../workflow/${workflowKey}/${version}`)
639
+ }
640
+ } : {}
641
+ });
642
+ return;
643
+ }
644
+ const text2 = describeAppliedSync(t, notice.merge, notice.renamed);
645
+ const description = [text2.summary, text2.conflicts].filter((line) => line !== null).join(". ");
646
+ const show = text2.conflicts ? toast.warning : toast.success;
647
+ show(text2.title, {
648
+ dismissible: true,
649
+ ...description ? { description } : {}
650
+ });
651
+ };
652
+ const sync = createLiveCanvasSync({
653
+ fetchDefinition,
654
+ registerNodeTypes: registerWorkflowNodeTypes,
655
+ notify,
656
+ workspaceId: () => getFluidGridsConfig().workspaceId || null
657
+ });
658
+ const stopWatching = watchCanvasBase();
659
+ const unregister = registerLiveCanvasHandler(sync.handle);
660
+ return () => {
661
+ unregister();
662
+ stopWatching();
663
+ };
664
+ }, []);
665
+ return null;
666
+ }
15
667
  var START_DELAY_MS = 800;
16
668
  function StartGuidedTour() {
17
669
  const location = useLocation();
@@ -118,9 +770,9 @@ function WorkflowItem({
118
770
  try {
119
771
  const { getGlobalStoreClient } = await import('./global-client-KLOVTVPP.js');
120
772
  const { InstallWorkflowNodeDocument } = await import('./global-operations-D2USVOPM.js');
121
- const { getFluidGridsConfig } = await import('./config-T7BQ7AO2.js');
773
+ const { getFluidGridsConfig: getFluidGridsConfig2 } = await import('./config-T7BQ7AO2.js');
122
774
  const storeClient = getGlobalStoreClient();
123
- const cfg = getFluidGridsConfig();
775
+ const cfg = getFluidGridsConfig2();
124
776
  for (let i = 0; i < nodesToInstall.length; i++) {
125
777
  const node = nodesToInstall[i];
126
778
  setInstallProgress({
@@ -552,6 +1204,7 @@ function WorkflowItem({
552
1204
  }
553
1205
  ),
554
1206
  workflow && !isLoading && /* @__PURE__ */ jsx(FlowBuilderPage, { workflow }),
1207
+ workflow && !isLoading && /* @__PURE__ */ jsx(AssistantLiveCanvasHost, {}),
555
1208
  /* @__PURE__ */ jsx(GuidedTourStarter, { ready: Boolean(workflow && !isLoading) })
556
1209
  ] });
557
1210
  }
@@ -1,4 +1,4 @@
1
- import { Card, FluidGridsMark } from './chunk-E2OQHCFG.js';
1
+ import { Card, FluidGridsMark } from './chunk-O24MDGZY.js';
2
2
  import { Input } from './chunk-IEVSFCFS.js';
3
3
  import { Button } from './chunk-BQXVT32O.js';
4
4
  import { useWorkflowDefinition } from './chunk-SRE2JVSJ.js';
@@ -40872,14 +40872,8 @@ function NodeDetailView() {
40872
40872
  outputFields
40873
40873
  } = config2;
40874
40874
  const [tab, setTab] = useState(
40875
- () => ndv?.tab ?? "parameters"
40875
+ "parameters"
40876
40876
  );
40877
- const requestedTab = ndv?.tab ? `${ndv.nodeId}:${ndv.tab}` : void 0;
40878
- const [shownRequest, setShownRequest] = useState(requestedTab);
40879
- if (requestedTab !== shownRequest) {
40880
- setShownRequest(requestedTab);
40881
- if (ndv?.tab) setTab(ndv.tab);
40882
- }
40883
40877
  const durableEnabled = useDurableFunctionNodesEnabled();
40884
40878
  const isCodeWorkflow = durableEnabled && node2?.type === CODE_WORKFLOW_NODE_TYPE;
40885
40879
  const [outputView, setOutputView] = useState("schema");
@@ -40960,12 +40954,13 @@ function NodeDetailView() {
40960
40954
  window.addEventListener("mouseup", cleanup);
40961
40955
  };
40962
40956
  useEffect(() => () => dragCleanupRef.current?.(), []);
40957
+ const requestedTab = ndv?.tab;
40963
40958
  useEffect(() => {
40964
40959
  if (open2) {
40965
- setTab("parameters");
40960
+ setTab(requestedTab ?? "parameters");
40966
40961
  setPane("parameters");
40967
40962
  }
40968
- }, [open2, ndv?.nodeId]);
40963
+ }, [open2, ndv?.nodeId, requestedTab]);
40969
40964
  const openClassicPanel = () => {
40970
40965
  if (!ndv) return;
40971
40966
  showNodePropertiesOf({
@@ -0,0 +1,58 @@
1
+ import { useRef, useEffect, useMemo } from 'react';
2
+
3
+ var LIVE_CANVAS_RELAY_KEY = /* @__PURE__ */ Symbol.for(
4
+ "burdenoff.fluidgrids.liveCanvas"
5
+ );
6
+ function relaySlot() {
7
+ const scope = globalThis;
8
+ const existing = scope[LIVE_CANVAS_RELAY_KEY];
9
+ if (existing) return existing;
10
+ const created = { handler: null };
11
+ scope[LIVE_CANVAS_RELAY_KEY] = created;
12
+ return created;
13
+ }
14
+ function registerLiveCanvasHandler(next) {
15
+ const slot = relaySlot();
16
+ slot.handler = next;
17
+ return () => {
18
+ if (slot.handler === next) slot.handler = null;
19
+ };
20
+ }
21
+ function isActionResult(event) {
22
+ if (typeof event !== "object" || event === null) return false;
23
+ const candidate = event;
24
+ return candidate.kind === "action-result" && typeof candidate.run === "object" && candidate.run !== null && typeof candidate.action === "object" && candidate.action !== null && typeof candidate.action.operation === "string" && typeof candidate.action.id === "string";
25
+ }
26
+ function deliverLiveCanvasEvent(event, options = {}) {
27
+ try {
28
+ if (!isActionResult(event)) return;
29
+ const current = relaySlot().handler;
30
+ if (!current) return;
31
+ if (options.getActorId) {
32
+ const actorId = options.getActorId();
33
+ if (event.run.actorId !== null && actorId !== null && event.run.actorId !== actorId) {
34
+ return;
35
+ }
36
+ }
37
+ current(event);
38
+ } catch (error) {
39
+ console.error("[FluidGrids] assistant live canvas:", error);
40
+ }
41
+ }
42
+ function createFluidGridsAssistantLiveCanvas(options = {}) {
43
+ return { onEvent: (event) => deliverLiveCanvasEvent(event, options) };
44
+ }
45
+ function useFluidGridsAssistantLiveCanvas(options = {}) {
46
+ const latest = useRef(options);
47
+ useEffect(() => {
48
+ latest.current = options;
49
+ });
50
+ return useMemo(
51
+ () => ({
52
+ onEvent: (event) => deliverLiveCanvasEvent(event, latest.current)
53
+ }),
54
+ []
55
+ );
56
+ }
57
+
58
+ export { createFluidGridsAssistantLiveCanvas, registerLiveCanvasHandler, useFluidGridsAssistantLiveCanvas };
@@ -1,4 +1,4 @@
1
- import { useImportWorkflowTemplate, FlowBuilderPage, useWorkflowTemplates, TemplateCard } from './chunk-E2OQHCFG.js';
1
+ import { useImportWorkflowTemplate, FlowBuilderPage, useWorkflowTemplates, TemplateCard } from './chunk-O24MDGZY.js';
2
2
  import { QuotaExhaustedDialog } from './chunk-X3LQJLV7.js';
3
3
  import { useWorkflowDefinitions, useDuplicateWorkflow } from './chunk-SRE2JVSJ.js';
4
4
  import { templatesControls } from './chunk-D3UWK7GY.js';
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  export { Label } from './chunk-VTQVPNUQ.js';
2
- export { WorkflowBuilder, WorkflowBuilderCore, WorkflowSearchRedirect } from './chunk-WJ3XAHT2.js';
3
- export { GuidedTourStarter, WorkflowBuilder as WorkflowBuilderPage } from './chunk-HYFLZNRK.js';
2
+ export { WorkflowBuilder, WorkflowBuilderCore, WorkflowSearchRedirect } from './chunk-MNPZMFLF.js';
3
+ export { GuidedTourStarter, WorkflowBuilder as WorkflowBuilderPage } from './chunk-JUA57OVN.js';
4
4
  export { FirstWorkflowChecklist, FirstWorkflowChecklistView, WorkflowsList, useCreateWorkflowFirst } from './chunk-XTXT4NHE.js';
5
5
  export { CreateWorkflowRefusalDialog, GUIDED_FIRST_WORKFLOW_NAV, GUIDED_FIRST_WORKFLOW_STATE, GUIDED_TOUR_STATE, firstWorkflowProgress, isGuidedTourIntent, pickGuidedTour } from './chunk-LGGHQ7UE.js';
6
+ import './chunk-PIKYQE7P.js';
6
7
  import './chunk-AD3BXTJS.js';
7
8
  export { ExecutionsListPage } from './chunk-DASEJQ4G.js';
8
9
  import { usePendingHumanRequests, HumanRequestCard } from './chunk-6Y6I2ODM.js';
@@ -17,10 +18,10 @@ export { SettingsPage } from './chunk-XRHZ3IEL.js';
17
18
  import './chunk-35SQULD5.js';
18
19
  export { useDataSinkData, useDataSinks, useInsightsData, useLogStreamDestinationActions, useLogStreamDestinations, useRunReplayActions } from './chunk-VYH65RLH.js';
19
20
  import './chunk-5ERAJC2I.js';
20
- import { ThemeProvider, extractTemplateGraph } from './chunk-REY7BCWM.js';
21
- export { TemplatePreview, TemplatesPage } from './chunk-REY7BCWM.js';
22
- import { external_exports, FlowBuilderPage, FluidGridsMark, useRunners, useRunnerActions, RunnerStatusPill, runnerInstallCommand } from './chunk-E2OQHCFG.js';
23
- export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Popover, PopoverContent, PopoverTrigger, RUNNER_INSTALL_SCRIPT_URL, RunOnSection, RunnerStatusPill, RunsUsageMeter, RunsUsageMeterView, readRunsQuota, runnerInstallCommand, runsQuotaLevel, summarizeRunsQuota, useRunnerActions, useRunners, useRunsQuota } from './chunk-E2OQHCFG.js';
21
+ import { ThemeProvider, extractTemplateGraph } from './chunk-TNPK2LZT.js';
22
+ export { TemplatePreview, TemplatesPage } from './chunk-TNPK2LZT.js';
23
+ import { external_exports, FlowBuilderPage, FluidGridsMark, useRunners, useRunnerActions, RunnerStatusPill, runnerInstallCommand } from './chunk-O24MDGZY.js';
24
+ export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Popover, PopoverContent, PopoverTrigger, RUNNER_INSTALL_SCRIPT_URL, RunOnSection, RunnerStatusPill, RunsUsageMeter, RunsUsageMeterView, readRunsQuota, runnerInstallCommand, runsQuotaLevel, summarizeRunsQuota, useRunnerActions, useRunners, useRunsQuota } from './chunk-O24MDGZY.js';
24
25
  import './chunk-GEMNVFPK.js';
25
26
  export { Input } from './chunk-IEVSFCFS.js';
26
27
  export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from './chunk-YXMFRQYV.js';
@@ -77,7 +78,7 @@ var WorkflowsList2 = lazy(
77
78
  () => import('./WorkflowsList-2OYIXGWQ.js').then((m) => ({ default: m.WorkflowsList }))
78
79
  );
79
80
  var WorkflowBuilder3 = lazy(
80
- () => import('./WorkflowBuilder-JXBWQG63.js').then((m) => ({
81
+ () => import('./WorkflowBuilder-EVZGOXZZ.js').then((m) => ({
81
82
  default: m.WorkflowBuilder
82
83
  }))
83
84
  );
package/dist/templates.js CHANGED
@@ -1,5 +1,5 @@
1
- export { TemplatePreview, TemplatesPage } from './chunk-REY7BCWM.js';
2
- import './chunk-E2OQHCFG.js';
1
+ export { TemplatePreview, TemplatesPage } from './chunk-TNPK2LZT.js';
2
+ import './chunk-O24MDGZY.js';
3
3
  import './chunk-GEMNVFPK.js';
4
4
  import './chunk-IEVSFCFS.js';
5
5
  import './chunk-YXMFRQYV.js';
@@ -1,7 +1,8 @@
1
- export { WorkflowBuilder, WorkflowBuilderCore, WorkflowSearchRedirect } from './chunk-WJ3XAHT2.js';
2
- export { WorkflowBuilder as WorkflowBuilderPage } from './chunk-HYFLZNRK.js';
1
+ export { WorkflowBuilder, WorkflowBuilderCore, WorkflowSearchRedirect } from './chunk-MNPZMFLF.js';
2
+ export { WorkflowBuilder as WorkflowBuilderPage } from './chunk-JUA57OVN.js';
3
3
  import './chunk-LGGHQ7UE.js';
4
- import './chunk-E2OQHCFG.js';
4
+ import './chunk-PIKYQE7P.js';
5
+ import './chunk-O24MDGZY.js';
5
6
  import './chunk-GEMNVFPK.js';
6
7
  import './chunk-IEVSFCFS.js';
7
8
  import './chunk-YXMFRQYV.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/fluidgrids-fe-sdk",
3
- "version": "2026.1029.3",
3
+ "version": "2026.1029.5",
4
4
  "description": "FluidGrids Workflow Builder SDK - Embeddable visual workflow automation",
5
5
  "type": "module",
6
6
  "imports": {