@workbench-kit/shell-react 0.0.2-prototype.0.2.14 → 0.0.2-prototype.0.2.16

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/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@workbench-kit/shell-react",
3
- "version": "0.0.2-prototype.0.2.14",
3
+ "version": "0.0.2-prototype.0.2.16",
4
4
  "private": false,
5
+ "sideEffects": [
6
+ "**/*.css"
7
+ ],
5
8
  "type": "module",
6
9
  "exports": {
7
10
  ".": "./src/index.ts",
@@ -21,13 +24,13 @@
21
24
  "@xyflow/react": "^12.11.2",
22
25
  "jsonata": "^2.2.0",
23
26
  "react-table-mapping": "^1.0.3",
24
- "@workbench-kit/field-remap": "0.0.2-prototype.0.2.14",
25
- "@workbench-kit/tokens": "0.0.2-prototype.0.2.14",
26
- "@workbench-kit/workbench-config": "0.0.2-prototype.0.2.14",
27
- "@workbench-kit/workbench-core": "0.0.2-prototype.0.2.14",
28
- "@workbench-kit/workspace": "0.0.2-prototype.0.2.14",
29
- "@workbench-kit/react": "0.0.2-prototype.0.2.14",
30
- "@workbench-kit/platform": "0.0.2-prototype.0.2.14"
27
+ "@workbench-kit/react": "0.0.2-prototype.0.2.16",
28
+ "@workbench-kit/workbench-config": "0.0.2-prototype.0.2.16",
29
+ "@workbench-kit/field-remap": "0.0.2-prototype.0.2.16",
30
+ "@workbench-kit/tokens": "0.0.2-prototype.0.2.16",
31
+ "@workbench-kit/workbench-core": "0.0.2-prototype.0.2.16",
32
+ "@workbench-kit/platform": "0.0.2-prototype.0.2.16",
33
+ "@workbench-kit/workspace": "0.0.2-prototype.0.2.16"
31
34
  },
32
35
  "peerDependencies": {
33
36
  "react": "^19.0.0",
@@ -1,5 +1,4 @@
1
1
  import {
2
- createBrowserWorkbenchStorage,
3
2
  type EditorGroupState,
4
3
  type EditorLayoutDirection,
5
4
  type EditorLayoutNode,
@@ -10,11 +9,16 @@ import {
10
9
  } from '@workbench-kit/workbench-core';
11
10
 
12
11
  import { isRecord } from '../is-record.js';
12
+ import {
13
+ readLocalJsonStorage,
14
+ resolveLocalWorkbenchStorage,
15
+ writeLocalJsonStorage,
16
+ } from '../storage/local-json-storage.js';
13
17
 
14
18
  export const DEFAULT_WORKBENCH_EDITOR_STATE_STORAGE_KEY = 'workbench-kit/.workbench/editors';
15
19
 
16
20
  export function isWorkbenchEditorStatePersistenceAvailable(): boolean {
17
- return createBrowserWorkbenchStorage({ kind: 'local' }) !== undefined;
21
+ return resolveLocalWorkbenchStorage() !== undefined;
18
22
  }
19
23
 
20
24
  export function editorStateToStorageValue(state: EditorState): EditorState {
@@ -42,17 +46,7 @@ export function readPersistedEditorState(
42
46
  storageKey = DEFAULT_WORKBENCH_EDITOR_STATE_STORAGE_KEY,
43
47
  storage?: WorkbenchStorageReader,
44
48
  ): EditorState | undefined {
45
- const resolvedStorage = storage ?? createBrowserWorkbenchStorage({ kind: 'local' });
46
- if (!resolvedStorage) return undefined;
47
-
48
- try {
49
- const raw = resolvedStorage.getItem(storageKey);
50
- if (!raw) return undefined;
51
-
52
- return parseEditorStateStorageValue(JSON.parse(raw) as unknown);
53
- } catch {
54
- return undefined;
55
- }
49
+ return readLocalJsonStorage(storageKey, parseEditorStateStorageValue, () => undefined, storage);
56
50
  }
57
51
 
58
52
  export function writePersistedEditorState(
@@ -60,14 +54,9 @@ export function writePersistedEditorState(
60
54
  storageKey = DEFAULT_WORKBENCH_EDITOR_STATE_STORAGE_KEY,
61
55
  storage?: WorkbenchStorageWriter,
62
56
  ): void {
63
- const resolvedStorage = storage ?? createBrowserWorkbenchStorage({ kind: 'local' });
64
- if (!resolvedStorage) return;
65
-
66
- try {
67
- resolvedStorage.setItem(storageKey, JSON.stringify(editorStateToStorageValue(state), null, 2));
68
- } catch {
69
- // Ignore quota and security errors so the shell keeps working offline.
70
- }
57
+ writeLocalJsonStorage(storageKey, state, storage, {
58
+ toStorageValue: editorStateToStorageValue,
59
+ });
71
60
  }
72
61
 
73
62
  function parseEditorStateStorageValue(value: unknown): EditorState | undefined {
@@ -49,10 +49,9 @@ export function ConvertNoteEditor({
49
49
 
50
50
  const portTypes = useMemo(() => edgePortTypes(edge, sources, targets), [edge, sources, targets]);
51
51
 
52
- const optionFields = useMemo(
53
- () => optionFieldsForStep(transforms, transformId),
54
- [transforms, transformId],
55
- );
52
+ // ValueTransformRegistry is intentionally mutable (`register`). Resolve its
53
+ // display contract on render so a host rerender sees replacement definitions.
54
+ const optionFields = optionFieldsForStep(transforms, transformId);
56
55
 
57
56
  const optionValue = useMemo(() => {
58
57
  if (!transformId) {
@@ -63,18 +62,14 @@ export function ConvertNoteEditor({
63
62
  );
64
63
  }, [chain, edge.transformOptionSteps, edge.transformOptions, stepIndex, transformId]);
65
64
 
66
- const replaceCatalog = useMemo(
67
- () =>
68
- listCompatibleTransforms({
69
- registry: transforms,
70
- edge,
71
- stepIndex,
72
- sourceType: portTypes.sourceType,
73
- targetType: portTypes.targetType,
74
- mode: 'replace',
75
- }),
76
- [edge, portTypes.sourceType, portTypes.targetType, stepIndex, transforms],
77
- );
65
+ const replaceCatalog = listCompatibleTransforms({
66
+ registry: transforms,
67
+ edge,
68
+ stepIndex,
69
+ sourceType: portTypes.sourceType,
70
+ targetType: portTypes.targetType,
71
+ mode: 'replace',
72
+ });
78
73
 
79
74
  if (!transformId) {
80
75
  return null;
@@ -1,4 +1,4 @@
1
- import { useMemo, type JSX } from 'react';
1
+ import type { JSX } from 'react';
2
2
  import { Button } from '@workbench-kit/react/primitives';
3
3
  import type { ValueTransformRegistry } from '@workbench-kit/field-remap';
4
4
 
@@ -28,10 +28,9 @@ export function FieldRemapConvertPalette({
28
28
  onAddSplit,
29
29
  chromeLabels = defaultFieldRemapChromeLabels,
30
30
  }: FieldRemapConvertPaletteProps): JSX.Element {
31
- const catalog = useMemo(
32
- () => transforms.list().filter((definition) => definition.id !== 'identity'),
33
- [transforms],
34
- );
31
+ // The registry supports replacing definitions through `register`; resolve
32
+ // display metadata on each host render even when the registry object is stable.
33
+ const catalog = transforms.list().filter((definition) => definition.id !== 'identity');
35
34
 
36
35
  return (
37
36
  <aside
@@ -0,0 +1,149 @@
1
+ import type { Edge, Node } from '@xyflow/react';
2
+ import type { ValueTransformRegistry } from '@workbench-kit/field-remap';
3
+
4
+ import type { FieldRemapFlowEdgeData, FieldRemapFlowNodeData } from './flow-adapter.js';
5
+ import type { FieldRemapSelection } from './flow-ops.js';
6
+
7
+ type GraphSyncToken = string | number | boolean | null | undefined;
8
+
9
+ function appendToken(parts: string[], value: GraphSyncToken): void {
10
+ const encoded =
11
+ value === undefined
12
+ ? 'undefined'
13
+ : value === null
14
+ ? 'null'
15
+ : `${typeof value}:${String(value)}`;
16
+ parts.push(`${encoded.length}:${encoded}`);
17
+ }
18
+
19
+ function appendTransformRegistrySignature(
20
+ parts: string[],
21
+ transforms: ValueTransformRegistry,
22
+ ): void {
23
+ const definitions = transforms
24
+ .list()
25
+ .slice()
26
+ .sort((left, right) => left.id.localeCompare(right.id));
27
+ appendToken(parts, definitions.length);
28
+ for (const definition of definitions) {
29
+ appendToken(parts, definition.id);
30
+ appendToken(parts, definition.label);
31
+ appendToken(parts, definition.description);
32
+ appendToken(parts, definition.category);
33
+ appendToken(parts, definition.outputType);
34
+ appendToken(parts, definition.inputTypes?.length ?? 0);
35
+ for (const inputType of definition.inputTypes ?? []) {
36
+ appendToken(parts, inputType);
37
+ }
38
+ appendToken(parts, definition.optionFields?.length ?? 0);
39
+ for (const option of definition.optionFields ?? []) {
40
+ appendToken(parts, option.key);
41
+ appendToken(parts, option.label);
42
+ appendToken(parts, option.kind);
43
+ }
44
+ }
45
+ }
46
+
47
+ export function createTransformRegistrySignature(transforms: ValueTransformRegistry): string {
48
+ const parts: string[] = [];
49
+ appendTransformRegistrySignature(parts, transforms);
50
+ return parts.join('');
51
+ }
52
+
53
+ function appendSelection(parts: string[], selection: FieldRemapSelection): void {
54
+ appendToken(parts, selection?.kind);
55
+ if (!selection) {
56
+ return;
57
+ }
58
+ if (selection.kind === 'edge' || selection.kind === 'transformStep') {
59
+ appendToken(parts, selection.edgeId);
60
+ appendToken(parts, selection.kind === 'transformStep' ? selection.stepIndex : undefined);
61
+ return;
62
+ }
63
+ appendToken(parts, selection.kind === 'draft' ? selection.localId : selection.operatorId);
64
+ }
65
+
66
+ /**
67
+ * Stable signature for the controlled values copied into XYFlow's internal node store.
68
+ * This intentionally lists display fields instead of serializing arbitrary props.
69
+ */
70
+ export function createFieldRemapGraphSyncKey(input: {
71
+ readonly nodes: readonly Node<FieldRemapFlowNodeData>[];
72
+ readonly edges: readonly Edge<FieldRemapFlowEdgeData>[];
73
+ readonly selection: FieldRemapSelection;
74
+ readonly transformRegistrySignature: string;
75
+ }): string {
76
+ const parts: string[] = [];
77
+ appendToken(parts, input.nodes.length);
78
+ for (const node of input.nodes) {
79
+ appendToken(parts, node.id);
80
+ appendToken(parts, node.type);
81
+ appendToken(parts, node.position.x);
82
+ appendToken(parts, node.position.y);
83
+ appendToken(parts, node.selected);
84
+
85
+ const data = node.data;
86
+ appendToken(parts, data.kind);
87
+ if (data.kind === 'source-object' || data.kind === 'target-object') {
88
+ appendToken(parts, data.title);
89
+ appendToken(parts, data.schemaRole);
90
+ appendToken(parts, data.ports.length);
91
+ for (const port of data.ports) {
92
+ appendToken(parts, port.fieldId);
93
+ appendToken(parts, port.label);
94
+ appendToken(parts, port.dataType);
95
+ }
96
+ continue;
97
+ }
98
+ if (data.kind === 'transform') {
99
+ appendToken(parts, data.mappingEdgeId);
100
+ appendToken(parts, data.stepIndex);
101
+ appendToken(parts, data.transformId);
102
+ appendToken(parts, data.label);
103
+ appendToken(parts, data.selected);
104
+ continue;
105
+ }
106
+ if (data.kind === 'draft-transform') {
107
+ appendToken(parts, data.localId);
108
+ appendToken(parts, data.transformId);
109
+ appendToken(parts, data.label);
110
+ appendToken(parts, data.sourceFieldId);
111
+ appendToken(parts, data.targetSlotId);
112
+ continue;
113
+ }
114
+ appendToken(parts, data.operatorId);
115
+ appendToken(parts, data.label);
116
+ if (data.kind === 'combine-operator') {
117
+ appendToken(parts, data.inputFieldIds.length);
118
+ for (const fieldId of data.inputFieldIds) {
119
+ appendToken(parts, fieldId);
120
+ }
121
+ appendToken(parts, data.outputSlotId);
122
+ continue;
123
+ }
124
+ appendToken(parts, data.inputFieldId);
125
+ appendToken(parts, data.outputSlotIds.length);
126
+ for (const slotId of data.outputSlotIds) {
127
+ appendToken(parts, slotId);
128
+ }
129
+ }
130
+
131
+ appendToken(parts, input.edges.length);
132
+ for (const edge of input.edges) {
133
+ appendToken(parts, edge.id);
134
+ appendToken(parts, edge.type);
135
+ appendToken(parts, edge.source);
136
+ appendToken(parts, edge.sourceHandle);
137
+ appendToken(parts, edge.target);
138
+ appendToken(parts, edge.targetHandle);
139
+ appendToken(parts, edge.selected);
140
+ appendToken(parts, edge.data?.mappingEdgeId);
141
+ appendToken(parts, edge.data?.draftLocalId);
142
+ appendToken(parts, edge.data?.operatorId);
143
+ appendToken(parts, edge.data?.segment);
144
+ }
145
+
146
+ appendSelection(parts, input.selection);
147
+ appendToken(parts, input.transformRegistrySignature);
148
+ return parts.join('');
149
+ }
@@ -60,6 +60,7 @@ import {
60
60
  type FieldRemapTargetObjectNodeData,
61
61
  type FieldRemapTransformNodeData,
62
62
  } from './flow-adapter.js';
63
+ import { createFieldRemapGraphSyncKey, createTransformRegistrySignature } from './flow-sync-key.js';
63
64
  import {
64
65
  addTransformStepToEdge,
65
66
  bindDraftSource,
@@ -414,6 +415,7 @@ function FieldRemapFlowCanvas({
414
415
  const first = transforms.list().find((definition) => definition.id !== 'identity');
415
416
  return first?.id ?? '';
416
417
  });
418
+ const transformRegistrySignature = createTransformRegistrySignature(transforms);
417
419
 
418
420
  const graph = useMemo(
419
421
  () =>
@@ -427,7 +429,17 @@ function FieldRemapFlowCanvas({
427
429
  targetTitle,
428
430
  drafts,
429
431
  }),
430
- [sources, targets, edges, transforms, operators, sourceTitle, targetTitle, drafts],
432
+ [
433
+ sources,
434
+ targets,
435
+ edges,
436
+ transforms,
437
+ operators,
438
+ sourceTitle,
439
+ targetTitle,
440
+ drafts,
441
+ transformRegistrySignature,
442
+ ],
431
443
  );
432
444
 
433
445
  const nodesWithSelection = useMemo(
@@ -461,22 +473,15 @@ function FieldRemapFlowCanvas({
461
473
  const [nodes, setNodes, onNodesChange] = useNodesState(nodesWithSelection);
462
474
  const [flowEdges, setFlowEdges, onFlowEdgesChange] = useEdgesState(graph.edges);
463
475
 
464
- // Sync controlled graph by topology/selection signature only. Depending on
465
- // `nodesWithSelection` (new array each paint) re-enters xyflow StoreUpdater and
466
- // can hit "Maximum update depth exceeded" in embed hosts with tight flex layouts.
467
- const graphSyncKey = useMemo(
468
- () =>
469
- [
470
- ...graph.nodes.map((node) => node.id),
471
- ...graph.edges.map(
472
- (edge) =>
473
- `${edge.id}:${edge.source}:${edge.target}:${edge.sourceHandle ?? ''}:${edge.targetHandle ?? ''}`,
474
- ),
475
- selection ? JSON.stringify(selection) : '',
476
- ...drafts.map((draft) => draft.localId),
477
- ].join('|'),
478
- [drafts, graph.edges, graph.nodes, selection],
479
- );
476
+ // Depending directly on `nodesWithSelection` (a new array after each graph
477
+ // calculation) re-enters XYFlow's StoreUpdater. The explicit signature keeps
478
+ // that loop guard while still tracking every value copied into rendered nodes.
479
+ const graphSyncKey = createFieldRemapGraphSyncKey({
480
+ nodes: nodesWithSelection,
481
+ edges: graph.edges,
482
+ selection,
483
+ transformRegistrySignature,
484
+ });
480
485
  const nodesWithSelectionRef = useRef(nodesWithSelection);
481
486
  const graphEdgesRef = useRef(graph.edges);
482
487
  nodesWithSelectionRef.current = nodesWithSelection;
@@ -611,7 +616,18 @@ function FieldRemapFlowCanvas({
611
616
  onEdgesChange([...withoutTarget, result.edge]);
612
617
  setSelection({ kind: 'edge', edgeId: result.edge.id });
613
618
  },
614
- [connectionContext, drafts, edges, onEdgesChange, setSelection, sources, targets, transforms],
619
+ [
620
+ connectionContext,
621
+ drafts,
622
+ edges,
623
+ onEdgesChange,
624
+ onOperatorsChange,
625
+ operators,
626
+ setSelection,
627
+ sources,
628
+ targets,
629
+ transforms,
630
+ ],
615
631
  );
616
632
 
617
633
  const onEdgesDelete = useCallback(
@@ -694,7 +710,7 @@ function FieldRemapFlowCanvas({
694
710
  stepIndex: data.stepIndex,
695
711
  });
696
712
  },
697
- [edges, onEdgesChange, setSelection],
713
+ [edges, onEdgesChange, onOperatorsChange, operators, selection, setSelection],
698
714
  );
699
715
 
700
716
  const onKeyDown = useCallback(
@@ -31,12 +31,6 @@ export interface FieldRemapSampleDefinition {
31
31
  readonly operators?: readonly MappingOperator[];
32
32
  }
33
33
 
34
- /** Alias kept so older URIs still open a known sample. */
35
- export const FIELD_REMAP_LEGACY_SAMPLE_ALIASES: Readonly<Record<string, FieldRemapSampleId>> = {
36
- 'interactive-bindings': 'nested-ab',
37
- 't-order-invoice': 't-event-time',
38
- };
39
-
40
34
  export const FIELD_REMAP_SAMPLES: readonly FieldRemapSampleDefinition[] = [
41
35
  {
42
36
  id: 'nested-ab',
@@ -386,14 +380,7 @@ export function isFieldRemapSampleId(value: string): value is FieldRemapSampleId
386
380
  }
387
381
 
388
382
  export function resolveFieldRemapSampleId(value: string | undefined): FieldRemapSampleId {
389
- if (!value) {
390
- return 'nested-ab';
391
- }
392
- const aliased = FIELD_REMAP_LEGACY_SAMPLE_ALIASES[value];
393
- if (aliased) {
394
- return aliased;
395
- }
396
- if (isFieldRemapSampleId(value)) {
383
+ if (value && isFieldRemapSampleId(value)) {
397
384
  return value;
398
385
  }
399
386
  return 'nested-ab';
@@ -1,5 +1,6 @@
1
1
  export const SAMPLE_FIELD_REMAP_VIEW_RENDER_KIND =
2
2
  'workbench-kit.samples.field-remap.view' as const;
3
+ export const SAMPLE_FIELD_REMAP_VIEW_ID = 'workbench-kit.samples.field-remap.panel' as const;
3
4
 
4
5
  export interface SampleFieldRemapViewRenderData {
5
6
  readonly kind: typeof SAMPLE_FIELD_REMAP_VIEW_RENDER_KIND;
@@ -1,5 +1,6 @@
1
1
  import type { JSX } from 'react';
2
2
  import { WorkbenchActionSidebar } from '@workbench-kit/react/layout';
3
+ import type { ViewHostFactory } from '@workbench-kit/workbench-core';
3
4
 
4
5
  import './view.css';
5
6
 
@@ -10,6 +11,7 @@ import {
10
11
  } from './samples.js';
11
12
  import {
12
13
  isSampleFieldRemapViewRenderData,
14
+ SAMPLE_FIELD_REMAP_VIEW_ID,
13
15
  SAMPLE_FIELD_REMAP_VIEW_RENDER_KIND,
14
16
  type SampleFieldRemapViewRenderData,
15
17
  } from './view-data.js';
@@ -43,6 +45,23 @@ export interface SampleFieldRemapViewProps {
43
45
  readonly className?: string | undefined;
44
46
  }
45
47
 
48
+ export const SAMPLE_FIELD_REMAP_VIEW_HOST_FACTORY: ViewHostFactory = {
49
+ id: 'workbench-kit.samples.field-remap.react-view-host',
50
+ priority: 100,
51
+ canCreate: ({ viewId }) => viewId === SAMPLE_FIELD_REMAP_VIEW_ID,
52
+ create: ({ provider }) => {
53
+ const host = provider.resolveViewHost();
54
+ const render = host.render.bind(host);
55
+
56
+ host.render = () => {
57
+ const renderData = render();
58
+ return isSampleFieldRemapViewRenderData(renderData) ? <SampleFieldRemapView /> : renderData;
59
+ };
60
+
61
+ return host;
62
+ },
63
+ };
64
+
46
65
  /** Sidebar: catalog of field-remap / table-mapping samples. */
47
66
  export function SampleFieldRemapView({ className }: SampleFieldRemapViewProps = {}): JSX.Element {
48
67
  const editorService = useEditorService();
package/src/index.ts CHANGED
@@ -80,7 +80,11 @@ export {
80
80
  isBuiltinEditorMarkdownPreviewRenderData,
81
81
  type BuiltinEditorMarkdownPreviewRenderData,
82
82
  } from './editor/markdown-document-view-data.js';
83
- export { SampleJdwLabView, type SampleJdwLabViewProps } from './jdw/lab-view.js';
83
+ export {
84
+ SAMPLE_JDW_LAB_VIEW_HOST_FACTORY,
85
+ SampleJdwLabView,
86
+ type SampleJdwLabViewProps,
87
+ } from './jdw/lab-view.js';
84
88
  export { JdwWidgetFormView, type JdwWidgetFormViewProps } from './jdw/widget-form-view.js';
85
89
  export { JdwWidgetPreviewView, type JdwWidgetPreviewViewProps } from './jdw/widget-preview-view.js';
86
90
  export {
@@ -186,6 +190,7 @@ export {
186
190
  type FieldRemapSampleId,
187
191
  } from './field-remap/samples.js';
188
192
  export {
193
+ SAMPLE_FIELD_REMAP_VIEW_HOST_FACTORY,
189
194
  SampleFieldRemapView,
190
195
  buildFieldRemapEditorUri,
191
196
  parseFieldRemapEditorSampleId,
@@ -1,4 +1,5 @@
1
1
  export const SAMPLE_JDW_LAB_VIEW_RENDER_KIND = 'workbench-kit.samples.jdw.view' as const;
2
+ export const SAMPLE_JDW_LAB_VIEW_ID = 'workbench-kit.samples.jdw.panel' as const;
2
3
 
3
4
  export interface SampleJdwLabViewRenderData {
4
5
  readonly kind: typeof SAMPLE_JDW_LAB_VIEW_RENDER_KIND;
@@ -1,4 +1,5 @@
1
1
  import { WorkbenchActionSidebar } from '@workbench-kit/react/layout';
2
+ import type { ViewHostFactory } from '@workbench-kit/workbench-core';
2
3
 
3
4
  import './lab-view.css';
4
5
 
@@ -7,6 +8,7 @@ import { useActiveEditorTab } from '../editor/use-editor.js';
7
8
  import { useActiveWorkspacePath } from '../explorer/use-active-workspace-path.js';
8
9
  import {
9
10
  isSampleJdwLabViewRenderData,
11
+ SAMPLE_JDW_LAB_VIEW_ID,
10
12
  SAMPLE_JDW_LAB_VIEW_RENDER_KIND,
11
13
  type SampleJdwLabViewRenderData,
12
14
  } from './lab-view-data.js';
@@ -24,6 +26,31 @@ export interface SampleJdwLabViewProps {
24
26
  readonly widgetTreePath?: string | undefined;
25
27
  }
26
28
 
29
+ export const SAMPLE_JDW_LAB_VIEW_HOST_FACTORY: ViewHostFactory = {
30
+ id: 'workbench-kit.samples.jdw.react-view-host',
31
+ priority: 100,
32
+ canCreate: ({ viewId }) => viewId === SAMPLE_JDW_LAB_VIEW_ID,
33
+ create: ({ provider }) => {
34
+ const host = provider.resolveViewHost();
35
+ const render = host.render.bind(host);
36
+
37
+ host.render = () => {
38
+ const renderData = render();
39
+
40
+ return isSampleJdwLabViewRenderData(renderData) ? (
41
+ <SampleJdwLabView
42
+ templateJdwPath={renderData.templateJdwPath}
43
+ widgetTreePath={renderData.widgetTreePath}
44
+ />
45
+ ) : (
46
+ renderData
47
+ );
48
+ };
49
+
50
+ return host;
51
+ },
52
+ };
53
+
27
54
  function pathMatches(activePath: string | undefined, candidate: string): boolean {
28
55
  if (!activePath) {
29
56
  return false;
@@ -3,36 +3,27 @@ import {
3
3
  type WorkbenchKeybindingDefinition,
4
4
  } from '@workbench-kit/workbench-config';
5
5
  import {
6
- createBrowserWorkbenchStorage,
7
6
  type WorkbenchStorageReader,
8
7
  type WorkbenchStorageWriter,
9
8
  } from '@workbench-kit/workbench-core';
10
9
 
10
+ import {
11
+ readLocalJsonStorage,
12
+ resolveLocalWorkbenchStorage,
13
+ writeLocalJsonStorage,
14
+ } from '../storage/local-json-storage.js';
15
+
11
16
  export const DEFAULT_WORKBENCH_KEYBINDING_STORAGE_KEY = 'workbench-kit/.workbench/keybindings';
12
17
 
13
18
  export function isWorkbenchKeybindingPersistenceAvailable(): boolean {
14
- return createBrowserWorkbenchStorage({ kind: 'local' }) !== undefined;
19
+ return resolveLocalWorkbenchStorage() !== undefined;
15
20
  }
16
21
 
17
22
  export function readPersistedKeybindingOverrides(
18
23
  storageKey = DEFAULT_WORKBENCH_KEYBINDING_STORAGE_KEY,
19
24
  storage?: WorkbenchStorageReader,
20
25
  ): readonly WorkbenchKeybindingDefinition[] {
21
- const resolvedStorage = storage ?? createBrowserWorkbenchStorage({ kind: 'local' });
22
- if (!resolvedStorage) {
23
- return [];
24
- }
25
-
26
- try {
27
- const raw = resolvedStorage.getItem(storageKey);
28
- if (!raw) {
29
- return [];
30
- }
31
-
32
- return parseWorkbenchKeybindingsConfig(JSON.parse(raw) as unknown);
33
- } catch {
34
- return [];
35
- }
26
+ return readLocalJsonStorage(storageKey, parseWorkbenchKeybindingsConfig, () => [], storage);
36
27
  }
37
28
 
38
29
  export function writePersistedKeybindingOverrides(
@@ -40,10 +31,5 @@ export function writePersistedKeybindingOverrides(
40
31
  storageKey = DEFAULT_WORKBENCH_KEYBINDING_STORAGE_KEY,
41
32
  storage?: WorkbenchStorageWriter,
42
33
  ): void {
43
- const resolvedStorage = storage ?? createBrowserWorkbenchStorage({ kind: 'local' });
44
- if (!resolvedStorage) {
45
- return;
46
- }
47
-
48
- resolvedStorage.setItem(storageKey, JSON.stringify(overrides, null, 2));
34
+ writeLocalJsonStorage(storageKey, overrides, storage, { errorMode: 'throw' });
49
35
  }
@@ -3,37 +3,28 @@ import {
3
3
  type WorkbenchSettingsConfig,
4
4
  } from '@workbench-kit/workbench-config';
5
5
  import {
6
- createBrowserWorkbenchStorage,
7
6
  type WorkbenchStorageReader,
8
7
  type WorkbenchStorageWriter,
9
8
  } from '@workbench-kit/workbench-core';
10
9
 
10
+ import {
11
+ readLocalJsonStorage,
12
+ resolveLocalWorkbenchStorage,
13
+ writeLocalJsonStorage,
14
+ } from '../storage/local-json-storage.js';
15
+
11
16
  export const DEFAULT_WORKBENCH_LOCAL_PREFERENCE_STORAGE_KEY =
12
17
  'workbench-kit/.workbench/settings.local';
13
18
 
14
19
  export function isWorkbenchLocalPreferencePersistenceAvailable(): boolean {
15
- return createBrowserWorkbenchStorage({ kind: 'local' }) !== undefined;
20
+ return resolveLocalWorkbenchStorage() !== undefined;
16
21
  }
17
22
 
18
23
  export function readPersistedLocalPreferences(
19
24
  storageKey = DEFAULT_WORKBENCH_LOCAL_PREFERENCE_STORAGE_KEY,
20
25
  storage?: WorkbenchStorageReader,
21
26
  ): WorkbenchSettingsConfig {
22
- const resolvedStorage = storage ?? createBrowserWorkbenchStorage({ kind: 'local' });
23
- if (!resolvedStorage) {
24
- return {};
25
- }
26
-
27
- try {
28
- const raw = resolvedStorage.getItem(storageKey);
29
- if (!raw) {
30
- return {};
31
- }
32
-
33
- return parseWorkbenchSettingsConfig(JSON.parse(raw) as unknown);
34
- } catch {
35
- return {};
36
- }
27
+ return readLocalJsonStorage(storageKey, parseWorkbenchSettingsConfig, () => ({}), storage);
37
28
  }
38
29
 
39
30
  export function writePersistedLocalPreferences(
@@ -41,10 +32,5 @@ export function writePersistedLocalPreferences(
41
32
  storageKey = DEFAULT_WORKBENCH_LOCAL_PREFERENCE_STORAGE_KEY,
42
33
  storage?: WorkbenchStorageWriter,
43
34
  ): void {
44
- const resolvedStorage = storage ?? createBrowserWorkbenchStorage({ kind: 'local' });
45
- if (!resolvedStorage) {
46
- return;
47
- }
48
-
49
- resolvedStorage.setItem(storageKey, JSON.stringify(values, null, 2));
35
+ writeLocalJsonStorage(storageKey, values, storage, { errorMode: 'throw' });
50
36
  }
@@ -34,6 +34,7 @@ import {
34
34
  type WorkbenchEditorSavePort,
35
35
  type WorkbenchExtensionDescription,
36
36
  type WorkbenchLayoutStateInput,
37
+ type ViewHostFactory,
37
38
  } from '@workbench-kit/workbench-core';
38
39
  import {
39
40
  ContextKeyService,
@@ -140,6 +141,8 @@ export interface WorkbenchProviderProps {
140
141
  persistLayout?: boolean;
141
142
  persistLocalPreferences?: boolean;
142
143
  userCommands?: readonly WorkbenchUserCommandDefinition[];
144
+ /** Host-owned framework adapters registered before extension activation. */
145
+ viewHostFactories?: readonly ViewHostFactory[] | undefined;
143
146
  workspaceHostPort?: WorkbenchWorkspaceHostPort | undefined;
144
147
  }
145
148
 
@@ -212,6 +215,7 @@ export function WorkbenchProvider({
212
215
  persistLayout,
213
216
  persistLocalPreferences,
214
217
  userCommands = [],
218
+ viewHostFactories,
215
219
  workspaceHostPort,
216
220
  }: WorkbenchProviderProps) {
217
221
  const hostAvailableExtensions = availableExtensions ?? DEFAULT_AVAILABLE_EXTENSIONS;
@@ -335,6 +339,9 @@ export function WorkbenchProvider({
335
339
 
336
340
  const services = useMemo<WorkbenchProviderServices>(() => {
337
341
  const extensionRegistry = new ExtensionRegistry();
342
+ const viewHostFactoryDisposables = (viewHostFactories ?? []).map((factory) =>
343
+ extensionRegistry.viewHostFactories.register(factory),
344
+ );
338
345
  const editorDocumentViewProviders = extensionRegistry.editorDocumentViews;
339
346
  const editorDocumentViewProviderDisposables = [
340
347
  ...(includeDefaultDocumentViewProviders === false
@@ -433,6 +440,9 @@ export function WorkbenchProvider({
433
440
  for (const disposable of editorDocumentViewProviderDisposables) {
434
441
  disposable.dispose();
435
442
  }
443
+ for (const disposable of viewHostFactoryDisposables) {
444
+ disposable.dispose();
445
+ }
436
446
  extensionRegistry.dispose();
437
447
  layoutService.dispose();
438
448
  preferenceService.dispose();
@@ -465,6 +475,7 @@ export function WorkbenchProvider({
465
475
  resolvedInitialLayout,
466
476
  resolvedInitialLocalPreferences,
467
477
  userCommands,
478
+ viewHostFactories,
468
479
  workspaceHostPort,
469
480
  ]);
470
481
 
@@ -473,7 +484,11 @@ export function WorkbenchProvider({
473
484
  return undefined;
474
485
  }
475
486
 
476
- const disposable = services.layoutService.onDidChangeLayout(({ state }) => {
487
+ const disposable = services.layoutService.onDidChangeLayout(({ state, transient }) => {
488
+ if (transient) {
489
+ return;
490
+ }
491
+
477
492
  writePersistedWorkbenchLayout(state, layoutStorageKey, layoutStorage);
478
493
  });
479
494
 
@@ -70,7 +70,12 @@ import {
70
70
  createWorkbenchShellActivityItems,
71
71
  } from './model.js';
72
72
  import { mergeWorkbenchStatusSections } from '../workbench/status-sections.js';
73
- import { renderDefaultBottomPanel, renderDefaultPrimarySidebar } from './view-host.js';
73
+ import {
74
+ getVisibleWorkbenchViews,
75
+ renderDefaultAuxiliarySidebar,
76
+ renderDefaultBottomPanel,
77
+ renderDefaultPrimarySidebar,
78
+ } from './view-host.js';
74
79
  import { WorkbenchShellTitleBarLayoutControls } from './titlebar-layout-controls.js';
75
80
  import { WorkbenchProfileModal, type WorkbenchProfileInput } from '../workbench/profile-modal.js';
76
81
  import { useContextKeyRevision } from '../commands/use-context-key-revision.js';
@@ -205,6 +210,10 @@ export function WorkbenchShell({
205
210
  preferenceService,
206
211
  } = useWorkbench();
207
212
  const contextKeyRevision = useContextKeyRevision(contextKeyService);
213
+ const contextKeySnapshot = useMemo(
214
+ () => contextKeyService.createSnapshot(),
215
+ [contextKeyRevision, contextKeyService],
216
+ );
208
217
  const forceRender = useForceRender();
209
218
  const [preferenceRevision, bumpPreferenceRevision] = useReducer((count: number) => count + 1, 0);
210
219
  const [isHelpOpen, setHelpOpen] = useState(false);
@@ -247,9 +256,9 @@ export function WorkbenchShell({
247
256
  () =>
248
257
  filterActivitiesByWhenClause(
249
258
  extensionRegistry.activities.getActivities(),
250
- contextKeyService.createSnapshot(),
259
+ contextKeySnapshot,
251
260
  ),
252
- [contextKeyRevision, contextKeyService, extensionRegistry],
261
+ [contextKeySnapshot, extensionRegistry],
253
262
  );
254
263
  const activityItems = sortActivityBarItems(
255
264
  createWorkbenchShellActivityItems({
@@ -429,20 +438,44 @@ export function WorkbenchShell({
429
438
  return;
430
439
  }
431
440
 
432
- for (const view of extensionRegistry.views.getViews(activeViewContainerId)) {
441
+ for (const view of getVisibleWorkbenchViews(
442
+ extensionRegistry,
443
+ activeViewContainerId,
444
+ contextKeySnapshot,
445
+ )) {
433
446
  void extensionRegistry.activateView(view.id).then(forceRender);
434
447
  }
435
- }, [activeViewContainerId, extensionRegistry, forceRender]);
448
+ }, [activeViewContainerId, contextKeySnapshot, extensionRegistry, forceRender]);
436
449
 
437
450
  useEffect(() => {
438
451
  if (!activePanelViewContainerId) {
439
452
  return;
440
453
  }
441
454
 
442
- for (const view of extensionRegistry.views.getViews(activePanelViewContainerId)) {
455
+ for (const view of getVisibleWorkbenchViews(
456
+ extensionRegistry,
457
+ activePanelViewContainerId,
458
+ contextKeySnapshot,
459
+ )) {
443
460
  void extensionRegistry.activateView(view.id).then(forceRender);
444
461
  }
445
- }, [activePanelViewContainerId, extensionRegistry, forceRender]);
462
+ }, [activePanelViewContainerId, contextKeySnapshot, extensionRegistry, forceRender]);
463
+
464
+ useEffect(() => {
465
+ if (!layout.auxiliaryBar.visible) {
466
+ return;
467
+ }
468
+
469
+ for (const container of extensionRegistry.views.getViewContainers('auxiliarybar')) {
470
+ for (const view of getVisibleWorkbenchViews(
471
+ extensionRegistry,
472
+ container.id,
473
+ contextKeySnapshot,
474
+ )) {
475
+ void extensionRegistry.activateView(view.id).then(forceRender);
476
+ }
477
+ }
478
+ }, [contextKeySnapshot, extensionRegistry, forceRender, layout.auxiliaryBar.visible]);
446
479
 
447
480
  useEffect(() => {
448
481
  if (extensionRegistry.capabilityRegistry.has(WORKBENCH_SETTINGS_CAPABILITY_ID)) {
@@ -601,6 +634,7 @@ export function WorkbenchShell({
601
634
  activeViewContainerId,
602
635
  catalogUrl,
603
636
  catalogTrustPolicy,
637
+ contextKeySnapshot,
604
638
  ),
605
639
  onSizePxChange: (sizePx) => {
606
640
  layoutService.setSideBarSizePercent(
@@ -613,13 +647,19 @@ export function WorkbenchShell({
613
647
  }}
614
648
  auxiliarySidebar={{
615
649
  isVisible: layout.auxiliaryBar.visible,
616
- node: <aside aria-label="Secondary Side Bar" className="workbench-auxiliary-side-bar" />,
650
+ node: renderDefaultAuxiliarySidebar(
651
+ extensionRegistry,
652
+ contextKeySnapshot,
653
+ catalogUrl,
654
+ catalogTrustPolicy,
655
+ ),
617
656
  }}
618
657
  bottomPanel={{
619
658
  isVisible: layout.panel.visible,
620
659
  node: renderDefaultBottomPanel(extensionRegistry, activePanelViewContainerId, {
621
660
  catalogTrustPolicy,
622
661
  catalogUrl,
662
+ contextKeys: contextKeySnapshot,
623
663
  onActiveViewContainerChange: (viewContainerId) => {
624
664
  layoutService.setActivePanelViewContainer(viewContainerId);
625
665
  if (!layout.panel.visible) {
@@ -5,7 +5,12 @@ import type {
5
5
  ViewHost,
6
6
  ViewHostFactoryRegistry,
7
7
  ViewProvider,
8
+ WorkbenchViewContribution,
8
9
  } from '@workbench-kit/workbench-core';
10
+ import {
11
+ filterWorkbenchContributionsByWhenClause,
12
+ type WorkbenchContextKeySnapshot,
13
+ } from '@workbench-kit/platform';
9
14
 
10
15
  import { BuiltinChatView } from '../chat/view.js';
11
16
  import { isBuiltinChatViewRenderData } from '../chat/view-data.js';
@@ -15,10 +20,6 @@ import { BuiltinExplorerView } from '../explorer/view.js';
15
20
  import { isBuiltinExplorerViewRenderData } from '../explorer/view-data.js';
16
21
  import { BuiltinExtensionsView } from '../extensions/view.js';
17
22
  import { isBuiltinExtensionsViewRenderData } from '../extensions/view-data.js';
18
- import { SampleJdwLabView } from '../jdw/lab-view.js';
19
- import { isSampleJdwLabViewRenderData } from '../jdw/lab-view-data.js';
20
- import { SampleFieldRemapView } from '../field-remap/view.js';
21
- import { isSampleFieldRemapViewRenderData } from '../field-remap/view-data.js';
22
23
  import { BuiltinSearchView } from '../explorer/search-view.js';
23
24
  import { isBuiltinSearchViewRenderData } from '../explorer/search-view-data.js';
24
25
 
@@ -27,10 +28,11 @@ export function renderDefaultPrimarySidebar(
27
28
  activeViewContainerId: string | undefined,
28
29
  catalogUrl?: string | undefined,
29
30
  catalogTrustPolicy?: ExtensionCatalogTrustPolicy | undefined,
31
+ contextKeys: WorkbenchContextKeySnapshot = {},
30
32
  ) {
31
33
  const views = activeViewContainerId
32
- ? extensionRegistry.views.getViews(activeViewContainerId)
33
- : extensionRegistry.views.getViews();
34
+ ? getVisibleWorkbenchViews(extensionRegistry, activeViewContainerId, contextKeys)
35
+ : filterWorkbenchContributionsByWhenClause(extensionRegistry.views.getViews(), contextKeys);
34
36
  if (views.length === 0) {
35
37
  return <aside aria-label="Primary sidebar" />;
36
38
  }
@@ -38,20 +40,29 @@ export function renderDefaultPrimarySidebar(
38
40
  return (
39
41
  <aside
40
42
  aria-label="Primary sidebar"
41
- className="workbench-primary-side-bar shell-react-primary-sidebar"
43
+ className="workbench-primary-side-bar shell-react-sidebar-host"
42
44
  >
43
- {views.map((view) => (
44
- <section key={view.id} data-view-id={view.id}>
45
- <WorkbenchViewHost
46
- catalogTrustPolicy={catalogTrustPolicy}
47
- catalogUrl={catalogUrl}
48
- fallback={view.name}
49
- provider={extensionRegistry.views.getViewProvider(view.id)}
50
- viewHostFactories={extensionRegistry.viewHostFactories}
51
- viewId={view.id}
52
- />
53
- </section>
54
- ))}
45
+ {renderWorkbenchViews(extensionRegistry, views, { catalogTrustPolicy, catalogUrl })}
46
+ </aside>
47
+ );
48
+ }
49
+
50
+ export function renderDefaultAuxiliarySidebar(
51
+ extensionRegistry: Pick<ExtensionRegistry, 'viewHostFactories' | 'views'>,
52
+ contextKeys: WorkbenchContextKeySnapshot,
53
+ catalogUrl?: string | undefined,
54
+ catalogTrustPolicy?: ExtensionCatalogTrustPolicy | undefined,
55
+ ) {
56
+ const views = extensionRegistry.views
57
+ .getViewContainers('auxiliarybar')
58
+ .flatMap((container) => getVisibleWorkbenchViews(extensionRegistry, container.id, contextKeys));
59
+
60
+ return (
61
+ <aside
62
+ aria-label="Secondary Side Bar"
63
+ className="workbench-auxiliary-side-bar shell-react-sidebar-host"
64
+ >
65
+ {renderWorkbenchViews(extensionRegistry, views, { catalogTrustPolicy, catalogUrl })}
55
66
  </aside>
56
67
  );
57
68
  }
@@ -62,6 +73,7 @@ export function renderDefaultBottomPanel(
62
73
  options: {
63
74
  catalogTrustPolicy?: ExtensionCatalogTrustPolicy | undefined;
64
75
  catalogUrl?: string | undefined;
76
+ contextKeys?: WorkbenchContextKeySnapshot | undefined;
65
77
  onActiveViewContainerChange?: ((viewContainerId: string) => void) | undefined;
66
78
  } = {},
67
79
  ) {
@@ -80,7 +92,11 @@ export function renderDefaultBottomPanel(
80
92
  ? activeViewContainerId
81
93
  : containers[0]?.id;
82
94
  const views = resolvedActiveViewContainerId
83
- ? extensionRegistry.views.getViews(resolvedActiveViewContainerId)
95
+ ? getVisibleWorkbenchViews(
96
+ extensionRegistry,
97
+ resolvedActiveViewContainerId,
98
+ options.contextKeys ?? {},
99
+ )
84
100
  : [];
85
101
 
86
102
  return (
@@ -111,24 +127,56 @@ export function renderDefaultBottomPanel(
111
127
  {views.length === 0 ? (
112
128
  <div className="workbench-bottom-panel__empty">No views in this panel container.</div>
113
129
  ) : (
114
- views.map((view) => (
115
- <section key={view.id} data-view-id={view.id} className="workbench-bottom-panel__view">
116
- <WorkbenchViewHost
117
- catalogTrustPolicy={options.catalogTrustPolicy}
118
- catalogUrl={options.catalogUrl}
119
- fallback={view.name}
120
- provider={extensionRegistry.views.getViewProvider(view.id)}
121
- viewHostFactories={extensionRegistry.viewHostFactories}
122
- viewId={view.id}
123
- />
124
- </section>
125
- ))
130
+ renderWorkbenchViews(extensionRegistry, views, {
131
+ catalogTrustPolicy: options.catalogTrustPolicy,
132
+ catalogUrl: options.catalogUrl,
133
+ sectionClassName: 'workbench-bottom-panel__view',
134
+ })
126
135
  )}
127
136
  </div>
128
137
  </section>
129
138
  );
130
139
  }
131
140
 
141
+ export function getVisibleWorkbenchViews(
142
+ extensionRegistry: Pick<ExtensionRegistry, 'views'>,
143
+ viewContainerId: string,
144
+ contextKeys: WorkbenchContextKeySnapshot,
145
+ ): WorkbenchViewContribution[] {
146
+ return filterWorkbenchContributionsByWhenClause(
147
+ extensionRegistry.views.getViews(viewContainerId),
148
+ contextKeys,
149
+ );
150
+ }
151
+
152
+ function renderWorkbenchViews(
153
+ extensionRegistry: Pick<ExtensionRegistry, 'viewHostFactories' | 'views'>,
154
+ views: readonly WorkbenchViewContribution[],
155
+ options: {
156
+ catalogTrustPolicy?: ExtensionCatalogTrustPolicy | undefined;
157
+ catalogUrl?: string | undefined;
158
+ sectionClassName?: string | undefined;
159
+ },
160
+ ): ReactNode {
161
+ return views.map((view) => (
162
+ <section
163
+ key={view.id}
164
+ className={options.sectionClassName}
165
+ data-view-container-id={view.containerId}
166
+ data-view-id={view.id}
167
+ >
168
+ <WorkbenchViewHost
169
+ catalogTrustPolicy={options.catalogTrustPolicy}
170
+ catalogUrl={options.catalogUrl}
171
+ fallback={view.name}
172
+ provider={extensionRegistry.views.getViewProvider(view.id)}
173
+ viewHostFactories={extensionRegistry.viewHostFactories}
174
+ viewId={view.id}
175
+ />
176
+ </section>
177
+ ));
178
+ }
179
+
132
180
  export function WorkbenchViewHost({
133
181
  catalogTrustPolicy,
134
182
  catalogUrl,
@@ -268,18 +316,5 @@ export function toWorkbenchViewHostReactNode(
268
316
  );
269
317
  }
270
318
 
271
- if (isSampleJdwLabViewRenderData(value)) {
272
- return (
273
- <SampleJdwLabView
274
- templateJdwPath={value.templateJdwPath}
275
- widgetTreePath={value.widgetTreePath}
276
- />
277
- );
278
- }
279
-
280
- if (isSampleFieldRemapViewRenderData(value)) {
281
- return <SampleFieldRemapView />;
282
- }
283
-
284
319
  return toReactNode(value, fallback);
285
320
  }
@@ -0,0 +1,66 @@
1
+ import {
2
+ createBrowserWorkbenchStorage,
3
+ type WorkbenchStorageAdapter,
4
+ type WorkbenchStorageReader,
5
+ type WorkbenchStorageWriter,
6
+ } from '@workbench-kit/workbench-core';
7
+
8
+ export function resolveLocalWorkbenchStorage<
9
+ TStorage extends WorkbenchStorageReader | WorkbenchStorageWriter = WorkbenchStorageAdapter,
10
+ >(storage?: TStorage): TStorage | WorkbenchStorageAdapter | undefined {
11
+ return storage ?? createBrowserWorkbenchStorage({ kind: 'local' });
12
+ }
13
+
14
+ export function readLocalJsonStorage<T>(
15
+ storageKey: string,
16
+ parse: (value: unknown) => T,
17
+ fallback: () => T,
18
+ storage?: WorkbenchStorageReader,
19
+ ): T {
20
+ const resolvedStorage = resolveLocalWorkbenchStorage(storage);
21
+ if (!resolvedStorage) {
22
+ return fallback();
23
+ }
24
+
25
+ try {
26
+ const raw = resolvedStorage.getItem(storageKey);
27
+ if (!raw) {
28
+ return fallback();
29
+ }
30
+
31
+ return parse(JSON.parse(raw) as unknown);
32
+ } catch {
33
+ return fallback();
34
+ }
35
+ }
36
+
37
+ export function writeLocalJsonStorage<T>(
38
+ storageKey: string,
39
+ value: T,
40
+ storage?: WorkbenchStorageWriter,
41
+ options: {
42
+ readonly errorMode?: 'ignore' | 'throw';
43
+ readonly toStorageValue?: (value: T) => unknown;
44
+ } = {},
45
+ ): void {
46
+ const resolvedStorage = resolveLocalWorkbenchStorage(storage);
47
+ if (!resolvedStorage) {
48
+ return;
49
+ }
50
+
51
+ try {
52
+ resolvedStorage.setItem(
53
+ storageKey,
54
+ JSON.stringify((options.toStorageValue ?? identity)(value), null, 2),
55
+ );
56
+ } catch (error) {
57
+ if (options.errorMode === 'throw') {
58
+ throw error;
59
+ }
60
+ // Local storage is best-effort; quota, security, and serialization errors stay non-fatal.
61
+ }
62
+ }
63
+
64
+ function identity<T>(value: T): T {
65
+ return value;
66
+ }
@@ -7,11 +7,16 @@ import {
7
7
  type WorkbenchColorSchemePreference,
8
8
  } from '@workbench-kit/react/workbench';
9
9
  import {
10
- createBrowserWorkbenchStorage,
11
10
  type WorkbenchStorageReader,
12
11
  type WorkbenchStorageWriter,
13
12
  } from '@workbench-kit/workbench-core';
14
13
 
14
+ import {
15
+ readLocalJsonStorage,
16
+ resolveLocalWorkbenchStorage,
17
+ writeLocalJsonStorage,
18
+ } from '../storage/local-json-storage.js';
19
+
15
20
  export type { WorkbenchAppearanceSettings } from '@workbench-kit/react/workbench/themePresets';
16
21
 
17
22
  export const DEFAULT_WORKBENCH_APPEARANCE_STORAGE_KEY = 'workbench-kit/.workbench/appearance';
@@ -24,28 +29,19 @@ export const DEFAULT_WORKBENCH_APPEARANCE: WorkbenchAppearanceSettings = {
24
29
  };
25
30
 
26
31
  export function isWorkbenchAppearancePersistenceAvailable(): boolean {
27
- return createBrowserWorkbenchStorage({ kind: 'local' }) !== undefined;
32
+ return resolveLocalWorkbenchStorage() !== undefined;
28
33
  }
29
34
 
30
35
  export function readPersistedWorkbenchAppearance(
31
36
  storageKey = DEFAULT_WORKBENCH_APPEARANCE_STORAGE_KEY,
32
37
  storage?: WorkbenchStorageReader,
33
38
  ): WorkbenchAppearanceSettings {
34
- const resolvedStorage = storage ?? createBrowserWorkbenchStorage({ kind: 'local' });
35
- if (!resolvedStorage) {
36
- return DEFAULT_WORKBENCH_APPEARANCE;
37
- }
38
-
39
- try {
40
- const raw = resolvedStorage.getItem(storageKey);
41
- if (!raw) {
42
- return DEFAULT_WORKBENCH_APPEARANCE;
43
- }
44
-
45
- return normalizeWorkbenchAppearance(JSON.parse(raw) as unknown);
46
- } catch {
47
- return DEFAULT_WORKBENCH_APPEARANCE;
48
- }
39
+ return readLocalJsonStorage(
40
+ storageKey,
41
+ normalizeWorkbenchAppearance,
42
+ () => DEFAULT_WORKBENCH_APPEARANCE,
43
+ storage,
44
+ );
49
45
  }
50
46
 
51
47
  export function writePersistedWorkbenchAppearance(
@@ -53,16 +49,7 @@ export function writePersistedWorkbenchAppearance(
53
49
  storageKey = DEFAULT_WORKBENCH_APPEARANCE_STORAGE_KEY,
54
50
  storage?: WorkbenchStorageWriter,
55
51
  ): void {
56
- const resolvedStorage = storage ?? createBrowserWorkbenchStorage({ kind: 'local' });
57
- if (!resolvedStorage) {
58
- return;
59
- }
60
-
61
- try {
62
- resolvedStorage.setItem(storageKey, JSON.stringify(settings, null, 2));
63
- } catch {
64
- // Ignore quota and security errors so the shell keeps working offline.
65
- }
52
+ writeLocalJsonStorage(storageKey, settings, storage);
66
53
  }
67
54
 
68
55
  function normalizeWorkbenchAppearance(value: unknown): WorkbenchAppearanceSettings {
@@ -3,7 +3,6 @@ import {
3
3
  type WorkbenchLayoutConfig,
4
4
  } from '@workbench-kit/workbench-config';
5
5
  import {
6
- createBrowserWorkbenchStorage,
7
6
  createWorkbenchLayoutState,
8
7
  type WorkbenchStorageReader,
9
8
  type WorkbenchStorageWriter,
@@ -11,10 +10,16 @@ import {
11
10
  type WorkbenchLayoutStateInput,
12
11
  } from '@workbench-kit/workbench-core';
13
12
 
13
+ import {
14
+ readLocalJsonStorage,
15
+ resolveLocalWorkbenchStorage,
16
+ writeLocalJsonStorage,
17
+ } from '../storage/local-json-storage.js';
18
+
14
19
  export const DEFAULT_WORKBENCH_LAYOUT_STORAGE_KEY = 'workbench-kit/.workbench/layout';
15
20
 
16
21
  export function isWorkbenchLayoutPersistenceAvailable(): boolean {
17
- return createBrowserWorkbenchStorage({ kind: 'local' }) !== undefined;
22
+ return resolveLocalWorkbenchStorage() !== undefined;
18
23
  }
19
24
 
20
25
  export function workbenchLayoutConfigToInput(
@@ -81,17 +86,12 @@ export function readPersistedWorkbenchLayout(
81
86
  storageKey = DEFAULT_WORKBENCH_LAYOUT_STORAGE_KEY,
82
87
  storage?: WorkbenchStorageReader,
83
88
  ): WorkbenchLayoutStateInput | undefined {
84
- const resolvedStorage = storage ?? createBrowserWorkbenchStorage({ kind: 'local' });
85
- if (!resolvedStorage) return undefined;
86
-
87
- try {
88
- const raw = resolvedStorage.getItem(storageKey);
89
- if (!raw) return undefined;
90
-
91
- return workbenchLayoutConfigToInput(parseWorkbenchLayoutConfig(JSON.parse(raw) as unknown));
92
- } catch {
93
- return undefined;
94
- }
89
+ return readLocalJsonStorage(
90
+ storageKey,
91
+ (value) => workbenchLayoutConfigToInput(parseWorkbenchLayoutConfig(value)),
92
+ () => undefined,
93
+ storage,
94
+ );
95
95
  }
96
96
 
97
97
  export function writePersistedWorkbenchLayout(
@@ -99,17 +99,9 @@ export function writePersistedWorkbenchLayout(
99
99
  storageKey = DEFAULT_WORKBENCH_LAYOUT_STORAGE_KEY,
100
100
  storage?: WorkbenchStorageWriter,
101
101
  ): void {
102
- const resolvedStorage = storage ?? createBrowserWorkbenchStorage({ kind: 'local' });
103
- if (!resolvedStorage) return;
104
-
105
- try {
106
- resolvedStorage.setItem(
107
- storageKey,
108
- JSON.stringify(workbenchLayoutStateToStorageValue(state), null, 2),
109
- );
110
- } catch {
111
- // Ignore quota and security errors so the shell keeps working offline.
112
- }
102
+ writeLocalJsonStorage(storageKey, state, storage, {
103
+ toStorageValue: workbenchLayoutStateToStorageValue,
104
+ });
113
105
  }
114
106
 
115
107
  export function resolvePersistedWorkbenchLayout(