@workbench-kit/shell-react 0.0.2-prototype.0.2.33 → 0.0.2-prototype.0.2.35

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 (51) hide show
  1. package/README.md +32 -0
  2. package/package.json +9 -9
  3. package/src/commands/use-command-descriptors.ts +7 -3
  4. package/src/commands/use-extension-registry-command-descriptors.ts +23 -6
  5. package/src/devtools/use-workbench-devtools-snapshot.ts +39 -14
  6. package/src/devtools/workbench-devtools-snapshot.ts +32 -13
  7. package/src/editor/area.tsx +3 -2
  8. package/src/editor/state-storage.ts +31 -0
  9. package/src/editor/tab-context-menu.ts +8 -8
  10. package/src/editor/use-editor.ts +6 -6
  11. package/src/explorer/context-menu.ts +8 -4
  12. package/src/explorer/view.tsx +4 -3
  13. package/src/extensions/canonical-extension-descriptions.ts +58 -0
  14. package/src/extensions/context-menu.ts +9 -6
  15. package/src/extensions/extension-enablement-context.ts +15 -0
  16. package/src/extensions/extension-enablement-controller.ts +453 -0
  17. package/src/extensions/management-model.ts +111 -47
  18. package/src/extensions/theme-selection-protection.ts +98 -0
  19. package/src/extensions/uninstall-eligibility.ts +103 -0
  20. package/src/extensions/use-extension-management.ts +151 -67
  21. package/src/extensions/view.tsx +4 -0
  22. package/src/field-remap/chrome-labels.ts +58 -2
  23. package/src/field-remap/convert-palette.tsx +151 -6
  24. package/src/field-remap/demo.tsx +31 -2
  25. package/src/field-remap/flow.tsx +57 -13
  26. package/src/field-remap/history.ts +99 -0
  27. package/src/field-remap/index.ts +9 -1
  28. package/src/field-remap/panel.tsx +371 -97
  29. package/src/field-remap/preview-controller.ts +122 -0
  30. package/src/field-remap/preview.tsx +171 -0
  31. package/src/field-remap/view.css +72 -0
  32. package/src/index.ts +16 -1
  33. package/src/management/keybinding-overrides-storage.ts +28 -0
  34. package/src/management/preference-settings-storage.ts +28 -0
  35. package/src/management/settings.tsx +2 -0
  36. package/src/management/use-command-management.ts +31 -26
  37. package/src/management/use-keybinding-management.ts +25 -12
  38. package/src/shell/focused-extension-services.ts +138 -0
  39. package/src/shell/host-shell.tsx +13 -10
  40. package/src/shell/persistence-diagnostic-context.ts +11 -0
  41. package/src/shell/provider.tsx +373 -69
  42. package/src/shell/settings.tsx +25 -16
  43. package/src/shell/shell.tsx +119 -78
  44. package/src/shell/view-host.tsx +23 -22
  45. package/src/storage/local-json-storage.ts +43 -29
  46. package/src/storage/persistence-diagnostics.ts +72 -0
  47. package/src/workbench/appearance-storage.ts +28 -0
  48. package/src/workbench/command-host.tsx +19 -22
  49. package/src/workbench/command-palette.ts +14 -13
  50. package/src/workbench/layout-storage.ts +52 -5
  51. package/src/workbench/use-persisted-appearance.ts +38 -12
@@ -1,18 +1,20 @@
1
- import { useEffect, useMemo, useState, type JSX } from 'react';
2
1
  import {
3
- applyMappingOperators,
4
- convertToShape,
2
+ useEffect,
3
+ useImperativeHandle,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ type JSX,
8
+ type Ref,
9
+ } from 'react';
10
+ import {
5
11
  createBuiltinValueTransformRegistry,
6
- defineConversion,
7
- defineDataShape,
8
12
  findParentChildMappingConflicts,
9
- normalizeMappingOperators,
10
13
  projectSourceFields,
11
14
  projectTargetSlots,
12
15
  pruneMappingEdgesForShapes,
13
16
  sourceFieldsFromPlainObject,
14
17
  targetSlotsFromPlainObject,
15
- withConversionEdges,
16
18
  type MappingEdge,
17
19
  type MappingOperator,
18
20
  type SourceField,
@@ -32,13 +34,47 @@ import {
32
34
  type FieldRemapSampleId,
33
35
  } from './samples.js';
34
36
  import { jsonataValueTransform } from './jsonata-transform.js';
37
+ import {
38
+ areFieldRemapHistorySnapshotsEqual,
39
+ createFieldRemapHistorySnapshot,
40
+ createFieldRemapHistoryState,
41
+ recordFieldRemapHistory,
42
+ redoFieldRemapHistory,
43
+ undoFieldRemapHistory,
44
+ type FieldRemapHistorySnapshot,
45
+ } from './history.js';
35
46
  import {
36
47
  FieldRemapShapeIoEditor,
37
48
  ingestSourceShape,
38
49
  ingestTargetShape,
39
50
  } from './shape-io-editor.js';
51
+ import { createFieldRemapPreviewController } from './preview-controller.js';
52
+ import type { FieldRemapPreviewState } from './preview.js';
40
53
  import './view.css';
41
54
 
55
+ export type { FieldRemapHistorySnapshot } from './history.js';
56
+
57
+ export interface FieldRemapHistoryOwner {
58
+ readonly canUndo: boolean;
59
+ readonly canRedo: boolean;
60
+ readonly record: (current: FieldRemapHistorySnapshot, next: FieldRemapHistorySnapshot) => void;
61
+ readonly reset: (next: FieldRemapHistorySnapshot) => void;
62
+ readonly undo: () => FieldRemapHistorySnapshot | undefined;
63
+ readonly redo: () => FieldRemapHistorySnapshot | undefined;
64
+ }
65
+
66
+ export interface FieldRemapHistoryActions {
67
+ readonly canUndo: boolean;
68
+ readonly canRedo: boolean;
69
+ readonly undo: () => void;
70
+ readonly redo: () => void;
71
+ }
72
+
73
+ export interface FieldRemapHistoryAvailability {
74
+ readonly canUndo: boolean;
75
+ readonly canRedo: boolean;
76
+ }
77
+
42
78
  export interface FieldRemapPanelProps {
43
79
  /** Catalog sample id, or a full sample definition. Defaults to `nested-ab` labels/ids. */
44
80
  readonly sample?: FieldRemapSampleId | FieldRemapSampleDefinition | undefined;
@@ -71,6 +107,16 @@ export interface FieldRemapPanelProps {
71
107
  /** Optional controlled n→m operators (document v2). */
72
108
  readonly operators?: readonly MappingOperator[] | undefined;
73
109
  readonly onOperatorsChange?: ((operators: readonly MappingOperator[]) => void) | undefined;
110
+ /**
111
+ * Composite history owner for controlled or mixed-control mapping state. When either durable
112
+ * channel is controlled, the Panel never creates a partial private history.
113
+ */
114
+ readonly historyOwner?: FieldRemapHistoryOwner | undefined;
115
+ /** Imperative semantic undo/redo actions for host chrome. Keyboard routing remains host-owned. */
116
+ readonly historyActionsRef?: Ref<FieldRemapHistoryActions | null> | undefined;
117
+ /** Reports the active history owner's current undo/redo availability. */
118
+ readonly onHistoryAvailabilityChange?:
119
+ ((availability: FieldRemapHistoryAvailability) => void) | undefined;
74
120
  /** Controlled source field tree (host-owned shapes). */
75
121
  readonly sources?: readonly SourceField[] | undefined;
76
122
  readonly onSourcesChange?: ((fields: readonly SourceField[]) => void) | undefined;
@@ -90,6 +136,8 @@ export interface FieldRemapPanelProps {
90
136
  readonly showBindingsList?: FieldRemapFlowMapperProps['showBindingsList'];
91
137
  readonly showConvertPalette?: FieldRemapFlowMapperProps['showConvertPalette'];
92
138
  readonly emptyDetail?: FieldRemapFlowMapperProps['emptyDetail'];
139
+ /** Show the controller-owned preview snapshot in the nested Flow rail. */
140
+ readonly showFlowPreview?: boolean;
93
141
  /** Forwarded to {@link FieldRemapFlowMapper} Controls MiniMap toggle. */
94
142
  readonly onShowMinimapChange?: FieldRemapFlowMapperProps['onShowMinimapChange'];
95
143
  readonly onPaneContextMenu?: FieldRemapFlowMapperProps['onPaneContextMenu'];
@@ -101,11 +149,6 @@ export interface FieldRemapPanelProps {
101
149
  readonly t?: FieldRemapFlowMapperProps['t'];
102
150
  }
103
151
 
104
- type FieldRemapPreviewResult = {
105
- readonly output: Record<string, unknown>;
106
- readonly error?: string;
107
- };
108
-
109
152
  function resolveSample(sample: FieldRemapPanelProps['sample']): FieldRemapSampleDefinition {
110
153
  if (!sample) {
111
154
  return getFieldRemapSample('nested-ab');
@@ -116,6 +159,83 @@ function resolveSample(sample: FieldRemapPanelProps['sample']): FieldRemapSample
116
159
  return sample;
117
160
  }
118
161
 
162
+ function createFieldRemapPreviewSignature(): (value: unknown) => string {
163
+ const referenceIds = new WeakMap<object, number>();
164
+ let nextReferenceId = 1;
165
+ const referenceId = (value: object): number => {
166
+ const current = referenceIds.get(value);
167
+ if (current !== undefined) {
168
+ return current;
169
+ }
170
+ const next = nextReferenceId;
171
+ nextReferenceId += 1;
172
+ referenceIds.set(value, next);
173
+ return next;
174
+ };
175
+
176
+ const visit = (value: unknown, ancestors: Set<object>): string => {
177
+ if (value === null) {
178
+ return 'null';
179
+ }
180
+ if (typeof value === 'string') {
181
+ return JSON.stringify(value);
182
+ }
183
+ if (typeof value === 'number') {
184
+ return Number.isNaN(value) ? 'number:NaN' : `number:${String(value)}`;
185
+ }
186
+ if (typeof value === 'boolean' || typeof value === 'bigint') {
187
+ return `${typeof value}:${String(value)}`;
188
+ }
189
+ if (typeof value === 'undefined') {
190
+ return 'undefined';
191
+ }
192
+ if (typeof value === 'symbol') {
193
+ return `symbol:${String(value.description)}`;
194
+ }
195
+ if (typeof value === 'function') {
196
+ return `function:${referenceId(value)}`;
197
+ }
198
+
199
+ if (ancestors.has(value)) {
200
+ return `reference:${referenceId(value)}`;
201
+ }
202
+ ancestors.add(value);
203
+ try {
204
+ if (Array.isArray(value)) {
205
+ return `[${value.map((item) => visit(item, ancestors)).join(',')}]`;
206
+ }
207
+ if (value instanceof Date) {
208
+ return `date:${value.toISOString()}`;
209
+ }
210
+ if (value instanceof Map) {
211
+ return `map:{${[...value.entries()]
212
+ .map(([key, item]) => `${visit(key, ancestors)}=>${visit(item, ancestors)}`)
213
+ .sort()
214
+ .join(',')}}`;
215
+ }
216
+ if (value instanceof Set) {
217
+ return `set:{${[...value.values()]
218
+ .map((item) => visit(item, ancestors))
219
+ .sort()
220
+ .join(',')}}`;
221
+ }
222
+
223
+ const record = value as Record<string, unknown>;
224
+ const tag = Object.prototype.toString.call(value);
225
+ return `${tag}:{${Object.keys(record)
226
+ .sort()
227
+ .map((key) => `${JSON.stringify(key)}:${visit(record[key], ancestors)}`)
228
+ .join(',')}}`;
229
+ } catch {
230
+ return `object:${referenceId(value)}`;
231
+ } finally {
232
+ ancestors.delete(value);
233
+ }
234
+ };
235
+
236
+ return (value) => visit(value, new Set<object>());
237
+ }
238
+
119
239
  /**
120
240
  * Self-contained field-remap workbench panel:
121
241
  * schema columns A/B + optional convert wires (XYFlow) and `convertToShape` preview.
@@ -138,6 +258,9 @@ export function FieldRemapPanel({
138
258
  onEdgesChange,
139
259
  operators: operatorsProp,
140
260
  onOperatorsChange,
261
+ historyOwner,
262
+ historyActionsRef,
263
+ onHistoryAvailabilityChange,
141
264
  sources: sourcesProp,
142
265
  onSourcesChange,
143
266
  targets: targetsProp,
@@ -150,6 +273,7 @@ export function FieldRemapPanel({
150
273
  showBindingsList,
151
274
  showConvertPalette,
152
275
  emptyDetail,
276
+ showFlowPreview,
153
277
  onShowMinimapChange,
154
278
  onPaneContextMenu,
155
279
  onNodeContextMenu,
@@ -190,7 +314,12 @@ export function FieldRemapPanel({
190
314
  const [uncontrolledOperators, setUncontrolledOperators] = useState<readonly MappingOperator[]>(
191
315
  () => [...(operatorsProp ?? sample.operators ?? [])],
192
316
  );
193
- const [result, setResult] = useState<FieldRemapPreviewResult>({ output: {} });
317
+ const [preview, setPreview] = useState<FieldRemapPreviewState>({ status: 'loading' });
318
+ const [previewController] = useState(() => createFieldRemapPreviewController());
319
+ const previewControllerDisposeTimer = useRef<ReturnType<typeof setTimeout> | undefined>(
320
+ undefined,
321
+ );
322
+ const [previewSignature] = useState(() => createFieldRemapPreviewSignature());
194
323
  const [uncontrolledSourceSample, setUncontrolledSourceSample] = useState<unknown>(
195
324
  () => sourceSampleProp ?? sample.source,
196
325
  );
@@ -221,6 +350,26 @@ export function FieldRemapPanel({
221
350
  const sourceFields = sourcesControlled ? sourcesProp : uncontrolledSources;
222
351
  const targetSlots = targetsControlled ? targetsProp : uncontrolledTargets;
223
352
  const sourceSample = sourceSampleControlled ? sourceSampleProp : uncontrolledSourceSample;
353
+ const transformRegistryRevision = previewSignature(registry.list());
354
+ const previewRevision = previewSignature({
355
+ sources: sourceFields,
356
+ targets: targetSlots,
357
+ edges,
358
+ operators,
359
+ sourceSample,
360
+ sourceShapeId: sample.sourceIdPrefix,
361
+ targetShapeId: sample.targetIdPrefix,
362
+ sourceLabel: sample.sourceLabel,
363
+ targetLabel: sample.targetLabel,
364
+ transformRegistryRevision,
365
+ });
366
+ const historyOwnedByPanel = !edgesControlled && !operatorsControlled;
367
+ const [panelHistory, setPanelHistory] = useState(createFieldRemapHistoryState);
368
+ const currentHistorySnapshot = useMemo(
369
+ () => createFieldRemapHistorySnapshot(edges, operators),
370
+ [edges, operators],
371
+ );
372
+ const shapeRefs = useRef({ sources: sourceFields, targets: targetSlots });
224
373
 
225
374
  const flowSources = useMemo(
226
375
  () => projectSourceFields(sourceFields, { includeHidden }),
@@ -234,15 +383,6 @@ export function FieldRemapPanel({
234
383
  () => pruneMappingEdgesForShapes(edges, flowSources, flowTargets),
235
384
  [edges, flowSources, flowTargets],
236
385
  );
237
- const commitFlowEdges = (next: readonly MappingEdge[]) => {
238
- if (includeHidden) {
239
- commitEdges(next);
240
- return;
241
- }
242
- const visibleIds = new Set(flowEdges.map((edge) => edge.id));
243
- const preserved = edges.filter((edge) => !visibleIds.has(edge.id));
244
- commitEdges([...preserved, ...next]);
245
- };
246
386
 
247
387
  const commitEdges = (next: readonly MappingEdge[]) => {
248
388
  if (!edgesControlled) {
@@ -258,6 +398,147 @@ export function FieldRemapPanel({
258
398
  onOperatorsChange?.(next);
259
399
  };
260
400
 
401
+ const applyHistorySnapshot = (next: FieldRemapHistorySnapshot) => {
402
+ if (
403
+ !areFieldRemapHistorySnapshotsEqual(
404
+ createFieldRemapHistorySnapshot(edges, next.operators),
405
+ next,
406
+ )
407
+ ) {
408
+ commitEdges(next.edges);
409
+ }
410
+ if (
411
+ !areFieldRemapHistorySnapshotsEqual(
412
+ createFieldRemapHistorySnapshot(next.edges, operators),
413
+ next,
414
+ )
415
+ ) {
416
+ commitOperators(next.operators);
417
+ }
418
+ };
419
+
420
+ const recordSemanticSnapshot = (next: FieldRemapHistorySnapshot) => {
421
+ if (areFieldRemapHistorySnapshotsEqual(currentHistorySnapshot, next)) {
422
+ return;
423
+ }
424
+ if (historyOwnedByPanel) {
425
+ setPanelHistory((current) => recordFieldRemapHistory(current, currentHistorySnapshot, next));
426
+ } else {
427
+ historyOwner?.record(currentHistorySnapshot, next);
428
+ }
429
+ applyHistorySnapshot(next);
430
+ };
431
+
432
+ const commitFlowEdges = (next: readonly MappingEdge[]) => {
433
+ if (includeHidden) {
434
+ recordSemanticSnapshot(createFieldRemapHistorySnapshot(next, operators));
435
+ return;
436
+ }
437
+ const visibleIds = new Set(flowEdges.map((edge) => edge.id));
438
+ const preserved = edges.filter((edge) => !visibleIds.has(edge.id));
439
+ recordSemanticSnapshot(createFieldRemapHistorySnapshot([...preserved, ...next], operators));
440
+ };
441
+
442
+ const resetHistory = (next: FieldRemapHistorySnapshot) => {
443
+ if (historyOwnedByPanel) {
444
+ setPanelHistory(createFieldRemapHistoryState());
445
+ } else {
446
+ historyOwner?.reset(next);
447
+ }
448
+ };
449
+
450
+ const resetHistoryForShapes = (
451
+ nextEdges: readonly MappingEdge[],
452
+ nextSources: readonly SourceField[],
453
+ nextTargets: readonly TargetSlot[],
454
+ ) => {
455
+ shapeRefs.current = { sources: nextSources, targets: nextTargets };
456
+ const next = createFieldRemapHistorySnapshot(nextEdges, operators);
457
+ resetHistory(next);
458
+ applyHistorySnapshot(next);
459
+ };
460
+
461
+ const undo = () => {
462
+ if (historyOwnedByPanel) {
463
+ const result = undoFieldRemapHistory(panelHistory, currentHistorySnapshot);
464
+ if (!result) {
465
+ return;
466
+ }
467
+ setPanelHistory(result.state);
468
+ applyHistorySnapshot(result.snapshot);
469
+ return;
470
+ }
471
+ if (!historyOwner?.canUndo) {
472
+ return;
473
+ }
474
+ const next = historyOwner.undo();
475
+ if (next) {
476
+ applyHistorySnapshot(next);
477
+ }
478
+ };
479
+
480
+ const redo = () => {
481
+ if (historyOwnedByPanel) {
482
+ const result = redoFieldRemapHistory(panelHistory, currentHistorySnapshot);
483
+ if (!result) {
484
+ return;
485
+ }
486
+ setPanelHistory(result.state);
487
+ applyHistorySnapshot(result.snapshot);
488
+ return;
489
+ }
490
+ if (!historyOwner?.canRedo) {
491
+ return;
492
+ }
493
+ const next = historyOwner.redo();
494
+ if (next) {
495
+ applyHistorySnapshot(next);
496
+ }
497
+ };
498
+
499
+ const historyAvailability = {
500
+ canUndo: historyOwnedByPanel ? panelHistory.past.length > 0 : (historyOwner?.canUndo ?? false),
501
+ canRedo: historyOwnedByPanel
502
+ ? panelHistory.future.length > 0
503
+ : (historyOwner?.canRedo ?? false),
504
+ };
505
+
506
+ useImperativeHandle(historyActionsRef, () => ({ ...historyAvailability, undo, redo }), [
507
+ historyAvailability.canRedo,
508
+ historyAvailability.canUndo,
509
+ panelHistory,
510
+ currentHistorySnapshot,
511
+ historyOwner,
512
+ ]);
513
+
514
+ useEffect(() => {
515
+ onHistoryAvailabilityChange?.(historyAvailability);
516
+ }, [historyAvailability.canRedo, historyAvailability.canUndo, onHistoryAvailabilityChange]);
517
+
518
+ useEffect(() => {
519
+ if (shapeRefs.current.sources === sourceFields && shapeRefs.current.targets === targetSlots) {
520
+ return;
521
+ }
522
+ shapeRefs.current = { sources: sourceFields, targets: targetSlots };
523
+ const nextEdges = pruneMappingEdgesForShapes(edges, sourceFields, targetSlots);
524
+ const next = createFieldRemapHistorySnapshot(nextEdges, operators);
525
+ if (historyOwnedByPanel) {
526
+ setPanelHistory(createFieldRemapHistoryState());
527
+ } else {
528
+ historyOwner?.reset(next);
529
+ }
530
+ applyHistorySnapshot(next);
531
+ }, [
532
+ edges,
533
+ historyOwnedByPanel,
534
+ historyOwner,
535
+ onEdgesChange,
536
+ onOperatorsChange,
537
+ operators,
538
+ sourceFields,
539
+ targetSlots,
540
+ ]);
541
+
261
542
  const commitSources = (next: readonly SourceField[]) => {
262
543
  if (!sourcesControlled) {
263
544
  setUncontrolledSources(next);
@@ -272,84 +553,64 @@ export function FieldRemapPanel({
272
553
  onTargetsChange?.(next);
273
554
  };
274
555
 
275
- const shapes = useMemo(
276
- () => [
277
- defineDataShape({
278
- id: sample.sourceIdPrefix,
279
- label: sample.sourceLabel,
280
- role: 'source',
281
- fields: sourceFields,
282
- }),
283
- defineDataShape({
284
- id: sample.targetIdPrefix,
285
- label: sample.targetLabel,
286
- role: 'target',
287
- fields: targetSlots,
288
- }),
289
- ],
290
- [sample, sourceFields, targetSlots],
291
- );
292
-
293
556
  const conflicts = useMemo(
294
557
  () => findParentChildMappingConflicts(edges, sourceFields, targetSlots),
295
558
  [edges, sourceFields, targetSlots],
296
559
  );
297
560
 
298
561
  useEffect(() => {
299
- const controller = new AbortController();
300
- const conversion = withConversionEdges(
301
- defineConversion({
302
- id: `${sample.sourceIdPrefix}→${sample.targetIdPrefix}`,
303
- sourceShapeIds: [sample.sourceIdPrefix],
304
- targetShapeId: sample.targetIdPrefix,
305
- edges: [...sample.edges],
306
- }),
307
- edges,
308
- );
309
-
310
- void convertToShape({
311
- conversion,
312
- shapes,
313
- inputs: { [sample.sourceIdPrefix]: sourceSample },
314
- transforms: registry,
315
- signal: controller.signal,
316
- })
317
- .then(async (next) => {
318
- if (controller.signal.aborted) {
319
- return;
320
- }
321
- const normalizedOps = normalizeMappingOperators(operators);
322
- if (!normalizedOps?.length) {
323
- setResult({ output: next.output });
324
- return;
325
- }
326
- const merged = await applyMappingOperators({
327
- operators: normalizedOps,
328
- sources: sourceFields,
329
- targets: targetSlots,
330
- inputs: { [sample.sourceIdPrefix]: sourceSample },
331
- transforms: registry,
332
- output: next.output,
333
- signal: controller.signal,
334
- });
335
- if (!controller.signal.aborted) {
336
- setResult({ output: merged.output });
337
- }
338
- })
339
- .catch((error: unknown) => {
340
- if (controller.signal.aborted) {
341
- return;
342
- }
343
- setResult({
344
- output: {},
345
- error: error instanceof Error ? error.message : String(error),
346
- });
347
- });
562
+ if (previewControllerDisposeTimer.current !== undefined) {
563
+ clearTimeout(previewControllerDisposeTimer.current);
564
+ previewControllerDisposeTimer.current = undefined;
565
+ }
566
+ const unsubscribe = previewController.subscribe(() => {
567
+ setPreview(previewController.getSnapshot());
568
+ });
569
+ const currentSnapshot = previewController.getSnapshot();
570
+ if (currentSnapshot.status !== 'unavailable') {
571
+ setPreview(currentSnapshot);
572
+ }
348
573
 
349
574
  return () => {
350
- controller.abort();
575
+ unsubscribe();
576
+ const timer = setTimeout(() => {
577
+ if (previewControllerDisposeTimer.current === timer) {
578
+ previewControllerDisposeTimer.current = undefined;
579
+ previewController.dispose();
580
+ }
581
+ }, 0);
582
+ previewControllerDisposeTimer.current = timer;
351
583
  };
352
- }, [edges, operators, registry, sample, shapes, sourceFields, sourceSample, targetSlots]);
584
+ }, [previewController]);
585
+
586
+ useEffect(() => {
587
+ previewController.update({
588
+ kind: 'evaluate',
589
+ revision: previewRevision,
590
+ input: {
591
+ sources: sourceFields,
592
+ targets: targetSlots,
593
+ edges,
594
+ operators,
595
+ inputs: { [sample.sourceIdPrefix]: sourceSample },
596
+ transforms: registry,
597
+ sourceShapeIds: [sample.sourceIdPrefix],
598
+ targetShapeId: sample.targetIdPrefix,
599
+ sourceLabel: sample.sourceLabel,
600
+ targetLabel: sample.targetLabel,
601
+ },
602
+ });
603
+ }, [
604
+ edges,
605
+ operators,
606
+ previewController,
607
+ previewRevision,
608
+ registry,
609
+ sample,
610
+ sourceFields,
611
+ sourceSample,
612
+ targetSlots,
613
+ ]);
353
614
 
354
615
  const applySourceShape = (parsed: unknown) => {
355
616
  const ingested = ingestSourceShape(parsed, sample.sourceIdPrefix);
@@ -358,7 +619,11 @@ export function FieldRemapPanel({
358
619
  }
359
620
  setSourceJson(ingested.sampleJson);
360
621
  commitSources(ingested.fields);
361
- commitEdges(pruneMappingEdgesForShapes(edges, ingested.fields, targetSlots));
622
+ resetHistoryForShapes(
623
+ pruneMappingEdgesForShapes(edges, ingested.fields, targetSlots),
624
+ ingested.fields,
625
+ targetSlots,
626
+ );
362
627
  };
363
628
 
364
629
  const applyTargetShape = (parsed: unknown) => {
@@ -366,7 +631,11 @@ export function FieldRemapPanel({
366
631
  setTargetSample(parsed);
367
632
  setTargetJson(ingested.sampleJson);
368
633
  commitTargets(ingested.fields);
369
- commitEdges(pruneMappingEdgesForShapes(edges, sourceFields, ingested.fields));
634
+ resetHistoryForShapes(
635
+ pruneMappingEdgesForShapes(edges, sourceFields, ingested.fields),
636
+ sourceFields,
637
+ ingested.fields,
638
+ );
370
639
  };
371
640
 
372
641
  return (
@@ -428,7 +697,9 @@ export function FieldRemapPanel({
428
697
  transforms={registry}
429
698
  onEdgesChange={commitFlowEdges}
430
699
  operators={operators}
431
- onOperatorsChange={commitOperators}
700
+ onOperatorsChange={(next) =>
701
+ recordSemanticSnapshot(createFieldRemapHistorySnapshot(edges, next))
702
+ }
432
703
  sourceTitle={sample.sourceLabel}
433
704
  targetTitle={sample.targetLabel}
434
705
  showMinimap={showMinimap}
@@ -437,6 +708,7 @@ export function FieldRemapPanel({
437
708
  showBindingsList={showBindingsList}
438
709
  showConvertPalette={showConvertPalette}
439
710
  emptyDetail={emptyDetail}
711
+ {...(showFlowPreview ? { preview, showPreview: true } : {})}
440
712
  onShowMinimapChange={onShowMinimapChange}
441
713
  includeHidden={includeHidden}
442
714
  onIncludeHiddenChange={setIncludeHidden}
@@ -460,9 +732,9 @@ export function FieldRemapPanel({
460
732
  </p>
461
733
  ) : null}
462
734
 
463
- {result.error ? (
735
+ {preview.status === 'error' ? (
464
736
  <p className="workbench-field-remap-demo__error" role="alert">
465
- {result.error}
737
+ {preview.message}
466
738
  </p>
467
739
  ) : null}
468
740
 
@@ -473,7 +745,9 @@ export function FieldRemapPanel({
473
745
  </section>
474
746
  <section className="workbench-field-remap-demo__pane" aria-labelledby="field-remap-target">
475
747
  <h3 id="field-remap-target">{sample.targetLabel}</h3>
476
- <pre data-testid="field-remap-result">{JSON.stringify(result.output, null, 2)}</pre>
748
+ <pre data-testid="field-remap-result">
749
+ {JSON.stringify(preview.status === 'ready' ? preview.result.output : {}, null, 2)}
750
+ </pre>
477
751
  </section>
478
752
  </div>
479
753
  </div>