@workbench-kit/shell-react 0.0.2-prototype.0.2.32 → 0.0.2-prototype.0.2.34

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 (52) 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 +87 -2
  23. package/src/field-remap/convert-palette.tsx +164 -18
  24. package/src/field-remap/demo.tsx +48 -3
  25. package/src/field-remap/detail-panel.tsx +6 -5
  26. package/src/field-remap/flow.tsx +341 -155
  27. package/src/field-remap/history.ts +99 -0
  28. package/src/field-remap/index.ts +9 -1
  29. package/src/field-remap/panel.tsx +388 -97
  30. package/src/field-remap/preview-controller.ts +122 -0
  31. package/src/field-remap/preview.tsx +171 -0
  32. package/src/field-remap/view.css +154 -2
  33. package/src/index.ts +16 -1
  34. package/src/management/keybinding-overrides-storage.ts +28 -0
  35. package/src/management/preference-settings-storage.ts +28 -0
  36. package/src/management/settings.tsx +2 -0
  37. package/src/management/use-command-management.ts +31 -26
  38. package/src/management/use-keybinding-management.ts +25 -12
  39. package/src/shell/focused-extension-services.ts +138 -0
  40. package/src/shell/host-shell.tsx +13 -10
  41. package/src/shell/persistence-diagnostic-context.ts +11 -0
  42. package/src/shell/provider.tsx +373 -69
  43. package/src/shell/settings.tsx +25 -16
  44. package/src/shell/shell.tsx +119 -78
  45. package/src/shell/view-host.tsx +23 -22
  46. package/src/storage/local-json-storage.ts +43 -29
  47. package/src/storage/persistence-diagnostics.ts +72 -0
  48. package/src/workbench/appearance-storage.ts +28 -0
  49. package/src/workbench/command-host.tsx +19 -22
  50. package/src/workbench/command-palette.ts +14 -13
  51. package/src/workbench/layout-storage.ts +52 -5
  52. 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;
@@ -83,6 +129,15 @@ export interface FieldRemapPanelProps {
83
129
  readonly targetShape?: unknown;
84
130
  /** Forwarded to {@link FieldRemapFlowMapper} (default true). */
85
131
  readonly showMinimap?: boolean | undefined;
132
+ /** Forwarded Flow chrome preset. `embed` hides the hint and binding list by default. */
133
+ readonly chrome?: FieldRemapFlowMapperProps['chrome'];
134
+ /** Explicit Flow chrome overrides take precedence over {@link FieldRemapPanelProps.chrome}. */
135
+ readonly showFlowHint?: FieldRemapFlowMapperProps['showFlowHint'];
136
+ readonly showBindingsList?: FieldRemapFlowMapperProps['showBindingsList'];
137
+ readonly showConvertPalette?: FieldRemapFlowMapperProps['showConvertPalette'];
138
+ readonly emptyDetail?: FieldRemapFlowMapperProps['emptyDetail'];
139
+ /** Show the controller-owned preview snapshot in the nested Flow rail. */
140
+ readonly showFlowPreview?: boolean;
86
141
  /** Forwarded to {@link FieldRemapFlowMapper} Controls MiniMap toggle. */
87
142
  readonly onShowMinimapChange?: FieldRemapFlowMapperProps['onShowMinimapChange'];
88
143
  readonly onPaneContextMenu?: FieldRemapFlowMapperProps['onPaneContextMenu'];
@@ -94,11 +149,6 @@ export interface FieldRemapPanelProps {
94
149
  readonly t?: FieldRemapFlowMapperProps['t'];
95
150
  }
96
151
 
97
- type FieldRemapPreviewResult = {
98
- readonly output: Record<string, unknown>;
99
- readonly error?: string;
100
- };
101
-
102
152
  function resolveSample(sample: FieldRemapPanelProps['sample']): FieldRemapSampleDefinition {
103
153
  if (!sample) {
104
154
  return getFieldRemapSample('nested-ab');
@@ -109,6 +159,83 @@ function resolveSample(sample: FieldRemapPanelProps['sample']): FieldRemapSample
109
159
  return sample;
110
160
  }
111
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
+
112
239
  /**
113
240
  * Self-contained field-remap workbench panel:
114
241
  * schema columns A/B + optional convert wires (XYFlow) and `convertToShape` preview.
@@ -131,6 +258,9 @@ export function FieldRemapPanel({
131
258
  onEdgesChange,
132
259
  operators: operatorsProp,
133
260
  onOperatorsChange,
261
+ historyOwner,
262
+ historyActionsRef,
263
+ onHistoryAvailabilityChange,
134
264
  sources: sourcesProp,
135
265
  onSourcesChange,
136
266
  targets: targetsProp,
@@ -138,6 +268,12 @@ export function FieldRemapPanel({
138
268
  sourceSample: sourceSampleProp,
139
269
  targetShape: targetShapeProp,
140
270
  showMinimap,
271
+ chrome,
272
+ showFlowHint,
273
+ showBindingsList,
274
+ showConvertPalette,
275
+ emptyDetail,
276
+ showFlowPreview,
141
277
  onShowMinimapChange,
142
278
  onPaneContextMenu,
143
279
  onNodeContextMenu,
@@ -178,7 +314,12 @@ export function FieldRemapPanel({
178
314
  const [uncontrolledOperators, setUncontrolledOperators] = useState<readonly MappingOperator[]>(
179
315
  () => [...(operatorsProp ?? sample.operators ?? [])],
180
316
  );
181
- 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());
182
323
  const [uncontrolledSourceSample, setUncontrolledSourceSample] = useState<unknown>(
183
324
  () => sourceSampleProp ?? sample.source,
184
325
  );
@@ -209,6 +350,26 @@ export function FieldRemapPanel({
209
350
  const sourceFields = sourcesControlled ? sourcesProp : uncontrolledSources;
210
351
  const targetSlots = targetsControlled ? targetsProp : uncontrolledTargets;
211
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 });
212
373
 
213
374
  const flowSources = useMemo(
214
375
  () => projectSourceFields(sourceFields, { includeHidden }),
@@ -222,15 +383,6 @@ export function FieldRemapPanel({
222
383
  () => pruneMappingEdgesForShapes(edges, flowSources, flowTargets),
223
384
  [edges, flowSources, flowTargets],
224
385
  );
225
- const commitFlowEdges = (next: readonly MappingEdge[]) => {
226
- if (includeHidden) {
227
- commitEdges(next);
228
- return;
229
- }
230
- const visibleIds = new Set(flowEdges.map((edge) => edge.id));
231
- const preserved = edges.filter((edge) => !visibleIds.has(edge.id));
232
- commitEdges([...preserved, ...next]);
233
- };
234
386
 
235
387
  const commitEdges = (next: readonly MappingEdge[]) => {
236
388
  if (!edgesControlled) {
@@ -246,6 +398,147 @@ export function FieldRemapPanel({
246
398
  onOperatorsChange?.(next);
247
399
  };
248
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
+
249
542
  const commitSources = (next: readonly SourceField[]) => {
250
543
  if (!sourcesControlled) {
251
544
  setUncontrolledSources(next);
@@ -260,84 +553,64 @@ export function FieldRemapPanel({
260
553
  onTargetsChange?.(next);
261
554
  };
262
555
 
263
- const shapes = useMemo(
264
- () => [
265
- defineDataShape({
266
- id: sample.sourceIdPrefix,
267
- label: sample.sourceLabel,
268
- role: 'source',
269
- fields: sourceFields,
270
- }),
271
- defineDataShape({
272
- id: sample.targetIdPrefix,
273
- label: sample.targetLabel,
274
- role: 'target',
275
- fields: targetSlots,
276
- }),
277
- ],
278
- [sample, sourceFields, targetSlots],
279
- );
280
-
281
556
  const conflicts = useMemo(
282
557
  () => findParentChildMappingConflicts(edges, sourceFields, targetSlots),
283
558
  [edges, sourceFields, targetSlots],
284
559
  );
285
560
 
286
561
  useEffect(() => {
287
- const controller = new AbortController();
288
- const conversion = withConversionEdges(
289
- defineConversion({
290
- id: `${sample.sourceIdPrefix}→${sample.targetIdPrefix}`,
291
- sourceShapeIds: [sample.sourceIdPrefix],
292
- targetShapeId: sample.targetIdPrefix,
293
- edges: [...sample.edges],
294
- }),
295
- edges,
296
- );
297
-
298
- void convertToShape({
299
- conversion,
300
- shapes,
301
- inputs: { [sample.sourceIdPrefix]: sourceSample },
302
- transforms: registry,
303
- signal: controller.signal,
304
- })
305
- .then(async (next) => {
306
- if (controller.signal.aborted) {
307
- return;
308
- }
309
- const normalizedOps = normalizeMappingOperators(operators);
310
- if (!normalizedOps?.length) {
311
- setResult({ output: next.output });
312
- return;
313
- }
314
- const merged = await applyMappingOperators({
315
- operators: normalizedOps,
316
- sources: sourceFields,
317
- targets: targetSlots,
318
- inputs: { [sample.sourceIdPrefix]: sourceSample },
319
- transforms: registry,
320
- output: next.output,
321
- signal: controller.signal,
322
- });
323
- if (!controller.signal.aborted) {
324
- setResult({ output: merged.output });
325
- }
326
- })
327
- .catch((error: unknown) => {
328
- if (controller.signal.aborted) {
329
- return;
330
- }
331
- setResult({
332
- output: {},
333
- error: error instanceof Error ? error.message : String(error),
334
- });
335
- });
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
+ }
336
573
 
337
574
  return () => {
338
- 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;
339
583
  };
340
- }, [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
+ ]);
341
614
 
342
615
  const applySourceShape = (parsed: unknown) => {
343
616
  const ingested = ingestSourceShape(parsed, sample.sourceIdPrefix);
@@ -346,7 +619,11 @@ export function FieldRemapPanel({
346
619
  }
347
620
  setSourceJson(ingested.sampleJson);
348
621
  commitSources(ingested.fields);
349
- commitEdges(pruneMappingEdgesForShapes(edges, ingested.fields, targetSlots));
622
+ resetHistoryForShapes(
623
+ pruneMappingEdgesForShapes(edges, ingested.fields, targetSlots),
624
+ ingested.fields,
625
+ targetSlots,
626
+ );
350
627
  };
351
628
 
352
629
  const applyTargetShape = (parsed: unknown) => {
@@ -354,7 +631,11 @@ export function FieldRemapPanel({
354
631
  setTargetSample(parsed);
355
632
  setTargetJson(ingested.sampleJson);
356
633
  commitTargets(ingested.fields);
357
- commitEdges(pruneMappingEdgesForShapes(edges, sourceFields, ingested.fields));
634
+ resetHistoryForShapes(
635
+ pruneMappingEdgesForShapes(edges, sourceFields, ingested.fields),
636
+ sourceFields,
637
+ ingested.fields,
638
+ );
358
639
  };
359
640
 
360
641
  return (
@@ -416,10 +697,18 @@ export function FieldRemapPanel({
416
697
  transforms={registry}
417
698
  onEdgesChange={commitFlowEdges}
418
699
  operators={operators}
419
- onOperatorsChange={commitOperators}
700
+ onOperatorsChange={(next) =>
701
+ recordSemanticSnapshot(createFieldRemapHistorySnapshot(edges, next))
702
+ }
420
703
  sourceTitle={sample.sourceLabel}
421
704
  targetTitle={sample.targetLabel}
422
705
  showMinimap={showMinimap}
706
+ chrome={chrome}
707
+ showFlowHint={showFlowHint}
708
+ showBindingsList={showBindingsList}
709
+ showConvertPalette={showConvertPalette}
710
+ emptyDetail={emptyDetail}
711
+ {...(showFlowPreview ? { preview, showPreview: true } : {})}
423
712
  onShowMinimapChange={onShowMinimapChange}
424
713
  includeHidden={includeHidden}
425
714
  onIncludeHiddenChange={setIncludeHidden}
@@ -443,9 +732,9 @@ export function FieldRemapPanel({
443
732
  </p>
444
733
  ) : null}
445
734
 
446
- {result.error ? (
735
+ {preview.status === 'error' ? (
447
736
  <p className="workbench-field-remap-demo__error" role="alert">
448
- {result.error}
737
+ {preview.message}
449
738
  </p>
450
739
  ) : null}
451
740
 
@@ -456,7 +745,9 @@ export function FieldRemapPanel({
456
745
  </section>
457
746
  <section className="workbench-field-remap-demo__pane" aria-labelledby="field-remap-target">
458
747
  <h3 id="field-remap-target">{sample.targetLabel}</h3>
459
- <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>
460
751
  </section>
461
752
  </div>
462
753
  </div>