@workbench-kit/shell-react 0.0.2-prototype.0.2.13 → 0.0.2-prototype.0.2.15

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/README.md CHANGED
@@ -92,6 +92,21 @@ const flowActionsRef = useRef<FieldRemapFlowActions | null>(null);
92
92
  />;
93
93
  ```
94
94
 
95
+ ## Shell chrome labels / `t()`
96
+
97
+ `WorkbenchShell` accepts optional `labels` and `t(key, fallback)` for high-visibility chrome
98
+ (ActivityBar / StatusBar aria names, Profile/Settings secondary items, command palette).
99
+ English defaults apply when neither is set. See
100
+ `resolveWorkbenchShellChromeLabels` / `workbenchShellChromeLabelKeys` and
101
+ [Consumer Capabilities — Shell chrome label injection](../../docs/workbench/consumer-capabilities.md#shell-chrome-label--t-injection-126).
102
+
103
+ ```tsx
104
+ <WorkbenchShell
105
+ t={(key, fallback) => hostTranslate(key, fallback)}
106
+ labels={{ settingsLabel: 'Settings' }}
107
+ />
108
+ ```
109
+
95
110
  ## Related docs
96
111
 
97
112
  - [Component Map](../../docs/guides/component-map.md)
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.13",
3
+ "version": "0.0.2-prototype.0.2.15",
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.13",
25
- "@workbench-kit/react": "0.0.2-prototype.0.2.13",
26
- "@workbench-kit/platform": "0.0.2-prototype.0.2.13",
27
- "@workbench-kit/tokens": "0.0.2-prototype.0.2.13",
28
- "@workbench-kit/workbench-core": "0.0.2-prototype.0.2.13",
29
- "@workbench-kit/workbench-config": "0.0.2-prototype.0.2.13",
30
- "@workbench-kit/workspace": "0.0.2-prototype.0.2.13"
27
+ "@workbench-kit/platform": "0.0.2-prototype.0.2.15",
28
+ "@workbench-kit/field-remap": "0.0.2-prototype.0.2.15",
29
+ "@workbench-kit/tokens": "0.0.2-prototype.0.2.15",
30
+ "@workbench-kit/react": "0.0.2-prototype.0.2.15",
31
+ "@workbench-kit/workbench-config": "0.0.2-prototype.0.2.15",
32
+ "@workbench-kit/workbench-core": "0.0.2-prototype.0.2.15",
33
+ "@workbench-kit/workspace": "0.0.2-prototype.0.2.15"
31
34
  },
32
35
  "peerDependencies": {
33
36
  "react": "^19.0.0",
@@ -0,0 +1,7 @@
1
+ export function getCommandErrorMessage(error: unknown) {
2
+ if (error instanceof Error) {
3
+ return error.message;
4
+ }
5
+
6
+ return String(error);
7
+ }
@@ -6,6 +6,7 @@ import {
6
6
  type WorkbenchCommandDescriptor,
7
7
  } from '@workbench-kit/react/workbench';
8
8
 
9
+ import { getCommandErrorMessage } from './command-error-message.js';
9
10
  import { parseWorkbenchChatCommandInput } from './command-input.js';
10
11
  import { useWorkbench } from '../shell/provider.js';
11
12
  import { useWorkbenchCommandDescriptors } from '../commands/use-command-descriptors.js';
@@ -145,11 +146,3 @@ export function useWorkbenchChatCommandSurface({
145
146
  runInputAsCommand,
146
147
  };
147
148
  }
148
-
149
- function getCommandErrorMessage(error: unknown) {
150
- if (error instanceof Error) {
151
- return error.message;
152
- }
153
-
154
- return String(error);
155
- }
@@ -7,6 +7,7 @@ import {
7
7
  } from '@workbench-kit/react/workbench';
8
8
  import type { ChatCommandProposal, ChatMessage } from '@workbench-kit/react/workbench/chat';
9
9
 
10
+ import { getCommandErrorMessage } from './command-error-message.js';
10
11
  import { type WorkbenchChatCommandRunResult } from './command-surface.js';
11
12
  import { useWorkbench } from '../shell/provider.js';
12
13
 
@@ -24,14 +25,6 @@ export interface UseWorkbenchChatCommandProposalsOptions {
24
25
  policyInput?: ResolveWorkbenchCommandExecutionPolicyInput | undefined;
25
26
  }
26
27
 
27
- function getCommandErrorMessage(error: unknown) {
28
- if (error instanceof Error) {
29
- return error.message;
30
- }
31
-
32
- return String(error);
33
- }
34
-
35
28
  function createProposalId(commandId: string) {
36
29
  return `proposal-${commandId}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
37
30
  }
@@ -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';
@@ -331,7 +331,11 @@
331
331
  display: flex;
332
332
  flex-direction: column;
333
333
  gap: 0.65rem;
334
+ box-sizing: border-box;
334
335
  min-width: 0;
336
+ min-height: 0;
337
+ height: 100%;
338
+ overflow: hidden;
335
339
  padding: 0.75rem;
336
340
  border: 1px solid var(--vscode-panel-border, var(--color-border));
337
341
  border-radius: 0.375rem;
@@ -354,14 +358,21 @@
354
358
  color: var(--vscode-descriptionForeground, var(--color-text-muted));
355
359
  }
356
360
 
361
+ .workbench-field-remap-convert-palette__header,
362
+ .workbench-field-remap-convert-palette__place,
363
+ .workbench-field-remap-convert-palette__operators {
364
+ flex: 0 0 auto;
365
+ }
366
+
357
367
  .workbench-field-remap-convert-palette__list {
358
368
  list-style: none;
359
369
  margin: 0;
360
370
  padding: 0;
361
371
  display: flex;
372
+ flex: 1 1 auto;
362
373
  flex-direction: column;
363
374
  gap: 0.35rem;
364
- max-height: min(22rem, 45vh);
375
+ min-height: 0;
365
376
  overflow: auto;
366
377
  }
367
378
 
package/src/index.ts CHANGED
@@ -286,6 +286,14 @@ export {
286
286
  type WorkbenchSecondaryActivityItemsInput,
287
287
  type WorkbenchSecondaryActivityRoute,
288
288
  } from './shell/secondary-actions.js';
289
+ export {
290
+ defaultWorkbenchShellChromeLabels,
291
+ resolveWorkbenchShellChromeLabels,
292
+ workbenchShellChromeLabelKeys,
293
+ type WorkbenchI18n,
294
+ type WorkbenchShellChromeLabels,
295
+ type WorkbenchTranslate,
296
+ } from './shell/chrome-labels.js';
289
297
  export {
290
298
  WorkbenchProfileModal,
291
299
  type WorkbenchProfileDetail,
@@ -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
  }