@workbench-kit/shell-react 0.0.2-prototype.0.2.41 → 0.0.2-prototype.0.2.44

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 (39) hide show
  1. package/README.md +81 -2
  2. package/package.json +14 -10
  3. package/src/editor/workspace-reconcile.tsx +12 -7
  4. package/src/extensions/extension-enablement-controller.ts +40 -9
  5. package/src/extensions/theme-selection-protection.ts +80 -55
  6. package/src/field-remap/chrome-labels.ts +117 -0
  7. package/src/field-remap/convert-note-editor.tsx +119 -104
  8. package/src/field-remap/convert-palette.tsx +6 -0
  9. package/src/field-remap/demo.tsx +8 -2
  10. package/src/field-remap/detail-panel.tsx +535 -434
  11. package/src/field-remap/document-io.tsx +299 -0
  12. package/src/field-remap/drag-payload.ts +48 -0
  13. package/src/field-remap/flow-adapter.ts +216 -88
  14. package/src/field-remap/flow-ops.ts +150 -0
  15. package/src/field-remap/flow.tsx +1227 -272
  16. package/src/field-remap/index.ts +2 -0
  17. package/src/field-remap/io-class-browse.tsx +34 -22
  18. package/src/field-remap/keyboard.ts +16 -0
  19. package/src/field-remap/modal-detail.tsx +34 -0
  20. package/src/field-remap/panel.tsx +110 -14
  21. package/src/field-remap/transform-options-editor.tsx +68 -48
  22. package/src/field-remap/view.css +107 -189
  23. package/src/index.ts +7 -0
  24. package/src/keybinding-management-settings.ts +4 -0
  25. package/src/management/keybinding-overrides-storage.ts +48 -23
  26. package/src/management/keybinding-settings-view.tsx +31 -0
  27. package/src/management/keybinding-settings.tsx +4 -12
  28. package/src/management/use-keybinding-management.ts +72 -22
  29. package/src/shell/appearance-catalog.ts +453 -0
  30. package/src/shell/appearance-controller.ts +331 -0
  31. package/src/shell/appearance-presentation.ts +269 -0
  32. package/src/shell/provider.tsx +157 -19
  33. package/src/shell/settings.tsx +282 -153
  34. package/src/shell/shell.tsx +88 -18
  35. package/src/workbench/appearance-storage.ts +2 -2
  36. package/src/workbench/command-host-controller.tsx +223 -0
  37. package/src/workbench/command-host.tsx +100 -150
  38. package/src/workbench/keybinding-bridge.ts +84 -38
  39. package/src/workbench/shell-command-registration.ts +27 -2
package/README.md CHANGED
@@ -51,6 +51,28 @@ registry can use `registry-command-descriptors` without importing Provider
51
51
  context into that leaf bundle. The root barrel remains the discovery surface,
52
52
  not the default runtime import graph.
53
53
 
54
+ ### Provider-free command host
55
+
56
+ Hosts that already own command descriptors and execution can compose the canonical
57
+ Command Palette and Quick Open without `WorkbenchProvider`:
58
+
59
+ ```tsx
60
+ import { WorkbenchCommandHostController } from '@workbench-kit/shell-react/command-host-controller';
61
+
62
+ <WorkbenchCommandHostController
63
+ commands={commands}
64
+ executeCommand={executeCommand}
65
+ quickOpenProviders={quickOpenProviders}
66
+ />;
67
+ ```
68
+
69
+ `commands` is the complete palette descriptor set. The controller owns only overlay
70
+ state, hard Palette/Quick Open shortcuts, selection routing, and completion-driven
71
+ closing. Hosts continue to own command registration, descriptor projection, Quick Open
72
+ provider construction, persistence, and error reporting. Pass `shortcutBridge` only
73
+ when the host also wants the generic keybinding bridge; omit it or pass `false` to keep
74
+ that routing host-owned.
75
+
54
76
  ### Focused extension context migration
55
77
 
56
78
  `WorkbenchContextValue` no longer exposes the aggregate `ExtensionRegistry`.
@@ -98,6 +120,7 @@ import {
98
120
  FieldRemapFlowMapper,
99
121
  createJsonataValueTransform,
100
122
  } from '@workbench-kit/shell-react/field-remap';
123
+ import type { FieldRemapDocument } from '@workbench-kit/field-remap';
101
124
  import '@workbench-kit/shell-react/field-remap/view.css';
102
125
 
103
126
  // Uncontrolled demo:
@@ -111,14 +134,63 @@ import '@workbench-kit/shell-react/field-remap/view.css';
111
134
  for Flow-only embeds and custom bundler setups. The full barrel
112
135
  `import { FieldRemapPanel } from '@workbench-kit/shell-react'` stays supported.
113
136
 
137
+ ### n→m operators in direct Flow embeds
138
+
139
+ `FieldRemapFlowMapper` keeps durable edges and document-v2 combine/split operators
140
+ controlled. A host that enables operator authoring owns both arrays and commits each
141
+ complete next operator array through the same persistence/history boundary:
142
+
143
+ ```tsx
144
+ const [document, setDocument] = useState(initialDocument);
145
+ const commitDocumentChange = (next: FieldRemapDocument) => {
146
+ setDocument(next);
147
+ hostHistory.record(next);
148
+ persist(next);
149
+ };
150
+
151
+ <FieldRemapFlowMapper
152
+ sources={sources}
153
+ targets={targets}
154
+ transforms={registry}
155
+ edges={document.edges}
156
+ onEdgesChange={(next) => commitDocumentChange({ ...document, edges: next })}
157
+ operators={document.operators ?? []}
158
+ onOperatorsChange={(next) => commitDocumentChange({ ...document, operators: next })}
159
+ />;
160
+ ```
161
+
162
+ The presence of `onOperatorsChange` is the operator-authoring capability signal: it
163
+ enables the existing Add combine / Add split actions and routes operator wiring, detail,
164
+ and deletion mutations through that callback. Omitting the callback is intentional
165
+ inspect-only projection; supplied operators can still render and be selected, but operator
166
+ mutation chrome is absent. Do not infer writability from operator count, sample, chrome, or
167
+ labels. `readOnly` suppresses Flow authoring even when mutation callbacks are present.
168
+
169
+ `FieldRemapPanel` already supplies this wiring for its fully uncontrolled composite
170
+ `{ edges, operators }` state. If either durable channel is controlled, its existing
171
+ composite `historyOwner` contract applies as described below; consumers do not add a second
172
+ operator state layer merely to enable Panel authoring. Direct operator inventory drag/drop
173
+ and double-click placement are deferred to [#219](https://github.com/NewChoBo/workbench-kit/issues/219).
174
+
114
175
  ### Semantic history ownership
115
176
 
116
177
  `FieldRemapPanel` keeps a private composite `{ edges, operators }` undo/redo stack only
117
178
  when both durable channels are uncontrolled. If either channel is controlled, pass one
118
179
  `historyOwner` for the complete composite state; the Panel never creates a partial stack.
119
180
  `historyActionsRef` exposes host-chrome actions and
120
- `onHistoryAvailabilityChange` reports whether those actions are available. Keyboard
121
- routing remains host-owned.
181
+ `onHistoryAvailabilityChange` reports whether those actions are available. The Panel routes
182
+ available undo/redo chords through that same owner; direct `FieldRemapFlowMapper` consumers
183
+ remain responsible for host-owned history routing.
184
+
185
+ | Surface | Shortcut | Behavior |
186
+ | ------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
187
+ | Flow | `Escape` | Clears the active selection and unfinished drafts. If collapsed detail chrome owned focus, focus moves to the programmatic-only mapper root. |
188
+ | Flow | `Delete` / `Backspace` | Removes the eligible selected edge, convert step, operator, or draft through the existing mutation path. |
189
+ | Panel | `Ctrl/Cmd+Z` | Invokes the existing composite history owner's available undo action. |
190
+ | Panel | `Ctrl/Cmd+Shift+Z`, `Ctrl+Y` | Invokes the existing composite history owner's available redo action. |
191
+
192
+ Editable inputs, textareas, selects, contenteditable elements, and transform option editors
193
+ retain their native key behavior. Unavailable or read-only actions leave the event unconsumed.
122
194
 
123
195
  Only semantic edits coming from the Flow mapper create entries. Hidden mappings are
124
196
  reconstructed before an entry is recorded, so undo does not discard filtered state.
@@ -140,6 +212,13 @@ hooks so hosts avoid CSS/DOM workarounds:
140
212
  | `flowActionsRef` | `{ fitView(options?) }` using the same defaults as Controls fit-view. |
141
213
  | `labels` / `t` | Override edge-list / Convert palette chrome (e.g. “Field maps”). |
142
214
  | `ioChrome` (Panel) | `'browse' \| 'edit' \| 'none'` — prefer browse for inspect-only I/O. |
215
+ | `rewirePolicy` (Flow) | `'replace'` by default; `'reject'` preserves prior edges and reports impacted edge IDs. |
216
+ | `onConnectionFeedback` (Flow) | Receives one structured result at connection-attempt completion; hover validation stays silent. |
217
+ | `parentChildConflicts` (Flow) | Optional authoritative conflict projection; `undefined` derives from the supplied Flow inputs. |
218
+
219
+ Rejected attempts render one compact `role="status"` message. Standalone Flow embeds derive
220
+ parent/child conflicts with the domain detector; Panel computes the same conflicts from full shapes
221
+ before hidden-field projection and supplies that authoritative result, so it renders only once.
143
222
 
144
223
  ```tsx
145
224
  const flowActionsRef = useRef<FieldRemapFlowActions | null>(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workbench-kit/shell-react",
3
- "version": "0.0.2-prototype.0.2.41",
3
+ "version": "0.0.2-prototype.0.2.44",
4
4
  "private": false,
5
5
  "sideEffects": [
6
6
  "**/*.css"
@@ -11,10 +11,12 @@
11
11
  "./command-descriptors": "./src/commands/use-command-descriptors.ts",
12
12
  "./registry-command-descriptors": "./src/commands/use-extension-registry-command-descriptors.ts",
13
13
  "./command-host": "./src/workbench/command-host.tsx",
14
+ "./command-host-controller": "./src/workbench/command-host-controller.tsx",
14
15
  "./command-palette": "./src/workbench/command-palette.ts",
15
16
  "./field-remap": "./src/field-remap/index.ts",
16
17
  "./field-remap/view.css": "./src/field-remap/view.css",
17
18
  "./host-shell": "./src/shell/host-shell.tsx",
19
+ "./keybinding-management-settings": "./src/keybinding-management-settings.ts",
18
20
  "./layout-storage": "./src/workbench/layout-storage.ts",
19
21
  "./provider": "./src/shell/provider.tsx",
20
22
  "./shell": "./src/shell/shell.tsx"
@@ -31,14 +33,14 @@
31
33
  "@radix-ui/react-slot": "^1.3.0",
32
34
  "@xyflow/react": "^12.11.2",
33
35
  "jsonata": "^2.2.0",
34
- "@workbench-kit/platform": "0.0.2-prototype.0.2.41",
35
- "@workbench-kit/field-remap": "0.0.2-prototype.0.2.41",
36
- "@workbench-kit/workbench-extension-sdk": "0.0.2-prototype.0.2.41",
37
- "@workbench-kit/workbench-config": "0.0.2-prototype.0.2.41",
38
- "@workbench-kit/tokens": "0.0.2-prototype.0.2.41",
39
- "@workbench-kit/workbench-core": "0.0.2-prototype.0.2.41",
40
- "@workbench-kit/workspace": "0.0.2-prototype.0.2.41",
41
- "@workbench-kit/react": "0.0.2-prototype.0.2.41"
36
+ "@workbench-kit/platform": "0.0.2-prototype.0.2.44",
37
+ "@workbench-kit/field-remap": "0.0.2-prototype.0.2.44",
38
+ "@workbench-kit/tokens": "0.0.2-prototype.0.2.44",
39
+ "@workbench-kit/react": "0.0.2-prototype.0.2.44",
40
+ "@workbench-kit/workbench-config": "0.0.2-prototype.0.2.44",
41
+ "@workbench-kit/workbench-core": "0.0.2-prototype.0.2.44",
42
+ "@workbench-kit/workspace": "0.0.2-prototype.0.2.44",
43
+ "@workbench-kit/workbench-extension-sdk": "0.0.2-prototype.0.2.44"
42
44
  },
43
45
  "peerDependencies": {
44
46
  "react": "^19.0.0",
@@ -57,6 +59,8 @@
57
59
  },
58
60
  "scripts": {
59
61
  "test": "vitest run --config vitest.config.ts src",
60
- "typecheck": "tsc -p tsconfig.json --noEmit"
62
+ "typecheck": "tsc -p tsconfig.json --noEmit",
63
+ "typecheck:exact-optional": "pnpm --filter @workbench-kit/react exec tsc -p typecheck-exact-optional/tsconfig.emit.json && pnpm typecheck:exact-optional:consumer",
64
+ "typecheck:exact-optional:consumer": "tsc -p typecheck-exact-optional/tsconfig.emit.json && tsc -p typecheck-exact-optional/tsconfig.json --noEmit"
61
65
  }
62
66
  }
@@ -1,19 +1,24 @@
1
1
  import { useEffect } from 'react';
2
+ import type { EditorService } from '@workbench-kit/workbench-core';
2
3
 
3
4
  import { createWorkspaceFileAvailabilityChecker } from './workspace-file-availability.js';
4
- import { useEditorService } from './use-editor.js';
5
- import { useWorkbench } from '../shell/provider.js';
6
5
  import {
7
6
  isWorkspaceResourceService,
8
7
  useWorkspaceResourceState,
9
8
  } from '../workbench/workspace-view-state.js';
10
9
 
10
+ interface EditorWorkspaceReconcilerProps {
11
+ readonly editorService: EditorService;
12
+ readonly workspaceHostService?: unknown;
13
+ }
14
+
11
15
  /** Component-only module so Vite Fast Refresh can accept this boundary. */
12
- export function EditorWorkspaceReconciler(): null {
13
- const { workspaceHostPort } = useWorkbench();
14
- const editorService = useEditorService();
15
- const workspaceService = isWorkspaceResourceService(workspaceHostPort?.service)
16
- ? workspaceHostPort.service
16
+ export function EditorWorkspaceReconciler({
17
+ editorService,
18
+ workspaceHostService,
19
+ }: EditorWorkspaceReconcilerProps): null {
20
+ const workspaceService = isWorkspaceResourceService(workspaceHostService)
21
+ ? workspaceHostService
17
22
  : undefined;
18
23
  const workspaceState = useWorkspaceResourceState(workspaceService);
19
24
 
@@ -76,7 +76,11 @@ export class ExtensionEnablementController implements DisposableLike {
76
76
  private readonly storage: WorkbenchStorageAdapter | undefined;
77
77
  private readonly storageKey: string;
78
78
  private installedRecords: readonly InstalledExtensionRecord[];
79
- private themeSelectionProtection: ThemeSelectionProtectionSnapshot | undefined;
79
+ private readonly themeSelectionProtectionOwners = new Map<
80
+ number,
81
+ ThemeSelectionProtectionSnapshot
82
+ >();
83
+ private themeSelectionProtectionGeneration = 0;
80
84
  private disposed = false;
81
85
 
82
86
  constructor({
@@ -123,11 +127,27 @@ export class ExtensionEnablementController implements DisposableLike {
123
127
  };
124
128
  };
125
129
 
126
- setThemeSelectionProtection(snapshot: ThemeSelectionProtectionSnapshot | undefined): void {
127
- this.themeSelectionProtection =
128
- snapshot?.kind === 'known'
130
+ setThemeSelectionProtection(snapshot: ThemeSelectionProtectionSnapshot | undefined): () => void {
131
+ const generation = ++this.themeSelectionProtectionGeneration;
132
+ if (snapshot === undefined) {
133
+ this.themeSelectionProtectionOwners.clear();
134
+ return () => undefined;
135
+ }
136
+
137
+ this.themeSelectionProtectionOwners.set(
138
+ generation,
139
+ snapshot.kind === 'known'
129
140
  ? { ...snapshot, protectedThemeIds: [...snapshot.protectedThemeIds] }
130
- : snapshot;
141
+ : snapshot,
142
+ );
143
+ let disposed = false;
144
+ return () => {
145
+ if (disposed) {
146
+ return;
147
+ }
148
+ disposed = true;
149
+ this.themeSelectionProtectionOwners.delete(generation);
150
+ };
131
151
  }
132
152
 
133
153
  commitInstalledRecords(
@@ -228,6 +248,7 @@ export class ExtensionEnablementController implements DisposableLike {
228
248
  this.disposed = true;
229
249
  this.listeners.clear();
230
250
  this.registrationHandles.clear();
251
+ this.themeSelectionProtectionOwners.clear();
231
252
  this.registrationLifetime.dispose();
232
253
  }
233
254
 
@@ -345,15 +366,25 @@ export class ExtensionEnablementController implements DisposableLike {
345
366
  return { commitRequestedState: true, eligible: false, kind: 'reloadRequired' };
346
367
  }
347
368
 
348
- const selectionProtection = this.themeSelectionProtection;
369
+ const selectionProtections = [...this.themeSelectionProtectionOwners.values()];
370
+ const themeRegistryRevision = this.registry.themes.getRevision();
349
371
  if (
350
- selectionProtection?.kind !== 'known' ||
351
- selectionProtection.themeRegistryRevision !== this.registry.themes.getRevision()
372
+ selectionProtections.length === 0 ||
373
+ selectionProtections.some(
374
+ (selectionProtection) =>
375
+ selectionProtection.kind !== 'known' ||
376
+ selectionProtection.themeRegistryRevision !== themeRegistryRevision ||
377
+ !selectionProtection.isCurrent(),
378
+ )
352
379
  ) {
353
380
  return { commitRequestedState: false, eligible: false, kind: 'reloadRequired' };
354
381
  }
355
382
 
356
- const protectedThemeIds = new Set(selectionProtection.protectedThemeIds);
383
+ const protectedThemeIds = new Set(
384
+ selectionProtections.flatMap((selectionProtection) =>
385
+ selectionProtection.kind === 'known' ? selectionProtection.protectedThemeIds : [],
386
+ ),
387
+ );
357
388
 
358
389
  const themes = description.manifest.contributes?.themes ?? [];
359
390
  if (themes.some((theme) => protectedThemeIds.has(theme.id))) {
@@ -1,98 +1,123 @@
1
- import {
2
- DARK_THEME_PRESET_OPTIONS,
3
- LIGHT_THEME_PRESET_OPTIONS,
4
- WORKBENCH_COLOR_SCHEME_OPTIONS,
5
- } from '@workbench-kit/react/workbench';
6
1
  import type { ThemeRegistry } from '@workbench-kit/workbench-core';
7
2
 
8
- interface ThemeOptionLike {
9
- readonly id: string;
3
+ import {
4
+ createWorkbenchAppearanceCatalogSnapshot,
5
+ resolveWorkbenchAppearanceSelection,
6
+ type WorkbenchAppearanceCatalogSnapshot,
7
+ type WorkbenchAppearanceHostOptionInput,
8
+ } from '../shell/appearance-catalog.js';
9
+ import { classifyWorkbenchAppearanceThemeSelection } from '../shell/appearance-presentation.js';
10
+
11
+ interface ThemeSelectionProtectionBase {
12
+ readonly isCurrent: () => boolean;
13
+ readonly sourceFingerprint: string;
14
+ readonly themeRegistryRevision: number;
10
15
  }
11
16
 
12
17
  export type ThemeSelectionProtectionSnapshot =
13
- | {
18
+ | (ThemeSelectionProtectionBase & {
14
19
  readonly kind: 'known';
15
20
  readonly protectedThemeIds: readonly string[];
16
- readonly themeRegistryRevision: number;
17
- }
18
- | {
21
+ })
22
+ | (ThemeSelectionProtectionBase & {
19
23
  readonly kind: 'unknown';
20
- readonly themeRegistryRevision: number;
21
- };
24
+ });
22
25
 
23
26
  export interface ThemeSelectionProtectionInput {
27
+ readonly catalog?: WorkbenchAppearanceCatalogSnapshot | undefined;
24
28
  readonly darkPreset: string | undefined;
25
29
  readonly lightPreset: string | undefined;
26
30
  readonly theme: string | undefined;
27
- readonly themeOptions: readonly ThemeOptionLike[] | undefined;
31
+ readonly themeOptions: readonly WorkbenchAppearanceHostOptionInput[] | undefined;
28
32
  readonly themes: ThemeRegistry;
29
33
  }
30
34
 
31
35
  /**
32
- * Captures only selections that resolve to exactly one current option source.
33
- * Unknown values deliberately do not resolve to the first rendered option: the
34
- * soft lifecycle must fail closed instead of guessing which fallback is active.
36
+ * Captures only selections that resolve to exactly one eligible catalog row. The action guard
37
+ * reconstructs current own data and compares both revision and the canonical source fingerprint,
38
+ * so writable public contributions cannot reuse stale lifecycle protection.
35
39
  */
36
40
  export function createThemeSelectionProtectionSnapshot({
41
+ catalog: suppliedCatalog,
37
42
  darkPreset,
38
43
  lightPreset,
39
44
  theme,
40
45
  themeOptions,
41
46
  themes,
42
47
  }: ThemeSelectionProtectionInput): ThemeSelectionProtectionSnapshot {
43
- const themeRegistryRevision = themes.getRevision();
44
- const contributedThemes = themes.getThemes();
48
+ const catalog =
49
+ suppliedCatalog ??
50
+ createWorkbenchAppearanceCatalogSnapshot({ hostOptions: themeOptions, themes });
51
+ const base: ThemeSelectionProtectionBase = {
52
+ isCurrent: () => isCatalogCurrent(catalog, themes, themeOptions),
53
+ sourceFingerprint: catalog.sourceFingerprint,
54
+ themeRegistryRevision: catalog.themeRegistryRevision,
55
+ };
45
56
  const hasLightPreset = lightPreset !== undefined;
46
57
  const hasDarkPreset = darkPreset !== undefined;
47
-
48
- if (hasLightPreset !== hasDarkPreset) {
49
- return { kind: 'unknown', themeRegistryRevision };
50
- }
58
+ const themeSelection = classifyWorkbenchAppearanceThemeSelection(theme);
51
59
 
52
60
  if (hasLightPreset && hasDarkPreset) {
53
- const lightOptions = [
54
- ...LIGHT_THEME_PRESET_OPTIONS,
55
- ...contributedThemes.filter((candidate) => candidate.mode === 'light'),
56
- ];
57
- const darkOptions = [
58
- ...DARK_THEME_PRESET_OPTIONS,
59
- ...contributedThemes.filter((candidate) => candidate.mode === 'dark'),
60
- ];
61
+ if (themeSelection.kind !== 'base-preference') {
62
+ return Object.freeze({ ...base, kind: 'unknown' });
63
+ }
64
+
65
+ const lightResolution = resolveWorkbenchAppearanceSelection(
66
+ catalog,
67
+ 'light-preset',
68
+ lightPreset,
69
+ );
70
+ const darkResolution = resolveWorkbenchAppearanceSelection(catalog, 'dark-preset', darkPreset);
61
71
 
62
- if (
63
- !hasExactlyOneOption(theme, WORKBENCH_COLOR_SCHEME_OPTIONS) ||
64
- !hasExactlyOneOption(lightPreset, lightOptions) ||
65
- !hasExactlyOneOption(darkPreset, darkOptions)
66
- ) {
67
- return { kind: 'unknown', themeRegistryRevision };
72
+ if (lightResolution.status !== 'resolved' || darkResolution.status !== 'resolved') {
73
+ return Object.freeze({ ...base, kind: 'unknown' });
68
74
  }
69
75
 
70
- return {
76
+ return Object.freeze({
77
+ ...base,
71
78
  kind: 'known',
72
- protectedThemeIds: [theme, lightPreset, darkPreset],
73
- themeRegistryRevision,
74
- };
79
+ protectedThemeIds: Object.freeze([lightPreset, darkPreset]),
80
+ });
75
81
  }
76
82
 
77
- const themeSources = [...(themeOptions ?? []), ...contributedThemes];
78
- if (!hasExactlyOneOption(theme, themeSources)) {
79
- return { kind: 'unknown', themeRegistryRevision };
83
+ if (themeSelection.kind === 'base-preference') {
84
+ return Object.freeze({
85
+ ...base,
86
+ kind: 'known',
87
+ protectedThemeIds: Object.freeze([]),
88
+ });
89
+ }
90
+ const themeResolution = resolveWorkbenchAppearanceSelection(
91
+ catalog,
92
+ 'flat-theme',
93
+ themeSelection.rawTheme,
94
+ );
95
+ if (themeResolution.status !== 'resolved') {
96
+ return Object.freeze({ ...base, kind: 'unknown' });
80
97
  }
81
98
 
82
- return {
99
+ return Object.freeze({
100
+ ...base,
83
101
  kind: 'known',
84
- protectedThemeIds: [theme],
85
- themeRegistryRevision,
86
- };
102
+ protectedThemeIds: Object.freeze([themeSelection.rawTheme]),
103
+ });
87
104
  }
88
105
 
89
- function hasExactlyOneOption(
90
- selection: string | undefined,
91
- options: readonly ThemeOptionLike[],
92
- ): selection is string {
93
- if (!selection) {
106
+ function isCatalogCurrent(
107
+ captured: WorkbenchAppearanceCatalogSnapshot,
108
+ themes: ThemeRegistry,
109
+ themeOptions: readonly WorkbenchAppearanceHostOptionInput[] | undefined,
110
+ ): boolean {
111
+ try {
112
+ const current = createWorkbenchAppearanceCatalogSnapshot({
113
+ hostOptions: themeOptions,
114
+ themes,
115
+ });
116
+ return (
117
+ current.themeRegistryRevision === captured.themeRegistryRevision &&
118
+ current.sourceFingerprint === captured.sourceFingerprint
119
+ );
120
+ } catch {
94
121
  return false;
95
122
  }
96
-
97
- return options.filter((option) => option.id === selection).length === 1;
98
123
  }
@@ -37,6 +37,37 @@ export interface FieldRemapChromeLabels {
37
37
  readonly hideHiddenFields: string;
38
38
  readonly emptyDetailTitle: string;
39
39
  readonly emptyDetailDescription: string;
40
+ /** Additive inspection-only empty-detail copy. Omitted hosts use the English defaults. */
41
+ readonly readOnlyEmptyDetailTitle?: string;
42
+ readonly readOnlyEmptyDetailDescription?: string;
43
+ /** Additive selection-detail Modal copy. Omitted legacy label objects use English defaults. */
44
+ readonly detailModalTitle?: string;
45
+ readonly closeDetailModal?: string;
46
+ /** Additive current-v2 document import/export chrome copy. */
47
+ readonly exportDocumentJson?: string;
48
+ readonly copyDocumentJson?: string;
49
+ readonly exportDocumentTitle?: string;
50
+ readonly closeDocumentExport?: string;
51
+ readonly exportDocumentDescription?: string;
52
+ readonly exportDocumentLabel?: string;
53
+ readonly importDocumentJson?: string;
54
+ readonly importDocumentTitle?: string;
55
+ readonly closeDocumentImport?: string;
56
+ readonly importDocumentDescription?: string;
57
+ readonly importDocumentLabel?: string;
58
+ readonly importDocumentPlaceholder?: string;
59
+ readonly applyDocumentImport?: string;
60
+ readonly cancelDocumentImport?: string;
61
+ readonly documentCopied?: string;
62
+ readonly documentCopyFailed?: string;
63
+ readonly documentImportUnavailable?: string;
64
+ readonly documentImportInvalidJson?: string;
65
+ readonly documentImportUnsupportedVersion?: string;
66
+ readonly documentImportInvalidDocument?: string;
67
+ readonly documentImportDuplicateId?: string;
68
+ readonly documentImportIncompatibleSource?: string;
69
+ readonly documentImportIncompatibleTarget?: string;
70
+ readonly documentImportUnavailableTransform?: string;
40
71
  /** Additive Flow preview copy. Omitted legacy label objects use English defaults. */
41
72
  readonly previewTitle?: string;
42
73
  readonly previewLoading?: string;
@@ -76,6 +107,36 @@ export const defaultFieldRemapChromeLabels = {
76
107
  emptyDetailTitle: 'Start with a convert',
77
108
  emptyDetailDescription:
78
109
  'Use the Convert palette to place a convert, then wire source → draft → target. Or select an existing binding / convert note on the canvas.',
110
+ readOnlyEmptyDetailTitle: 'Inspect mappings',
111
+ readOnlyEmptyDetailDescription: 'Select a mapping to inspect its details.',
112
+ detailModalTitle: 'Mapping details',
113
+ closeDetailModal: 'Close details',
114
+ exportDocumentJson: 'Export JSON',
115
+ copyDocumentJson: 'Copy JSON',
116
+ exportDocumentTitle: 'Export mapping document',
117
+ closeDocumentExport: 'Close export',
118
+ exportDocumentDescription:
119
+ 'Copy the current version 2 mapping document or select the JSON manually.',
120
+ exportDocumentLabel: 'Current mapping document JSON',
121
+ importDocumentJson: 'Import JSON',
122
+ importDocumentTitle: 'Import mapping document',
123
+ closeDocumentImport: 'Close import',
124
+ importDocumentDescription:
125
+ 'Paste a current version 2 mapping document. A valid import replaces the complete mapping in one step.',
126
+ importDocumentLabel: 'Mapping document JSON',
127
+ importDocumentPlaceholder: 'Paste mapping document JSON',
128
+ applyDocumentImport: 'Validate and import',
129
+ cancelDocumentImport: 'Cancel',
130
+ documentCopied: 'Mapping document copied.',
131
+ documentCopyFailed: 'The mapping document could not be copied.',
132
+ documentImportUnavailable: 'Import is unavailable for this mapping.',
133
+ documentImportInvalidJson: 'Enter valid mapping document JSON.',
134
+ documentImportUnsupportedVersion: 'This mapping document version is not supported.',
135
+ documentImportInvalidDocument: 'This mapping document is invalid.',
136
+ documentImportDuplicateId: 'This mapping document contains duplicate identities.',
137
+ documentImportIncompatibleSource: 'This mapping document uses an unavailable source field.',
138
+ documentImportIncompatibleTarget: 'This mapping document uses an unavailable target field.',
139
+ documentImportUnavailableTransform: 'This mapping document uses an unavailable convert.',
79
140
  previewTitle: 'Sample preview',
80
141
  previewLoading: 'Updating preview…',
81
142
  previewError: 'Preview failed',
@@ -114,6 +175,34 @@ export const fieldRemapChromeLabelKeys = {
114
175
  hideHiddenFields: 'fieldRemap.hideHiddenFields',
115
176
  emptyDetailTitle: 'fieldRemap.emptyDetailTitle',
116
177
  emptyDetailDescription: 'fieldRemap.emptyDetailDescription',
178
+ readOnlyEmptyDetailTitle: 'fieldRemap.readOnlyEmptyDetailTitle',
179
+ readOnlyEmptyDetailDescription: 'fieldRemap.readOnlyEmptyDetailDescription',
180
+ detailModalTitle: 'fieldRemap.detailModalTitle',
181
+ closeDetailModal: 'fieldRemap.closeDetailModal',
182
+ exportDocumentJson: 'fieldRemap.exportDocumentJson',
183
+ copyDocumentJson: 'fieldRemap.copyDocumentJson',
184
+ exportDocumentTitle: 'fieldRemap.exportDocumentTitle',
185
+ closeDocumentExport: 'fieldRemap.closeDocumentExport',
186
+ exportDocumentDescription: 'fieldRemap.exportDocumentDescription',
187
+ exportDocumentLabel: 'fieldRemap.exportDocumentLabel',
188
+ importDocumentJson: 'fieldRemap.importDocumentJson',
189
+ importDocumentTitle: 'fieldRemap.importDocumentTitle',
190
+ closeDocumentImport: 'fieldRemap.closeDocumentImport',
191
+ importDocumentDescription: 'fieldRemap.importDocumentDescription',
192
+ importDocumentLabel: 'fieldRemap.importDocumentLabel',
193
+ importDocumentPlaceholder: 'fieldRemap.importDocumentPlaceholder',
194
+ applyDocumentImport: 'fieldRemap.applyDocumentImport',
195
+ cancelDocumentImport: 'fieldRemap.cancelDocumentImport',
196
+ documentCopied: 'fieldRemap.documentCopied',
197
+ documentCopyFailed: 'fieldRemap.documentCopyFailed',
198
+ documentImportUnavailable: 'fieldRemap.documentImportUnavailable',
199
+ documentImportInvalidJson: 'fieldRemap.documentImportInvalidJson',
200
+ documentImportUnsupportedVersion: 'fieldRemap.documentImportUnsupportedVersion',
201
+ documentImportInvalidDocument: 'fieldRemap.documentImportInvalidDocument',
202
+ documentImportDuplicateId: 'fieldRemap.documentImportDuplicateId',
203
+ documentImportIncompatibleSource: 'fieldRemap.documentImportIncompatibleSource',
204
+ documentImportIncompatibleTarget: 'fieldRemap.documentImportIncompatibleTarget',
205
+ documentImportUnavailableTransform: 'fieldRemap.documentImportUnavailableTransform',
117
206
  previewTitle: 'fieldRemap.previewTitle',
118
207
  previewLoading: 'fieldRemap.previewLoading',
119
208
  previewError: 'fieldRemap.previewError',
@@ -162,6 +251,34 @@ export function resolveFieldRemapChromeLabels(
162
251
  hideHiddenFields: resolve('hideHiddenFields'),
163
252
  emptyDetailTitle: resolve('emptyDetailTitle'),
164
253
  emptyDetailDescription: resolve('emptyDetailDescription'),
254
+ readOnlyEmptyDetailTitle: resolve('readOnlyEmptyDetailTitle'),
255
+ readOnlyEmptyDetailDescription: resolve('readOnlyEmptyDetailDescription'),
256
+ detailModalTitle: resolve('detailModalTitle'),
257
+ closeDetailModal: resolve('closeDetailModal'),
258
+ exportDocumentJson: resolve('exportDocumentJson'),
259
+ copyDocumentJson: resolve('copyDocumentJson'),
260
+ exportDocumentTitle: resolve('exportDocumentTitle'),
261
+ closeDocumentExport: resolve('closeDocumentExport'),
262
+ exportDocumentDescription: resolve('exportDocumentDescription'),
263
+ exportDocumentLabel: resolve('exportDocumentLabel'),
264
+ importDocumentJson: resolve('importDocumentJson'),
265
+ importDocumentTitle: resolve('importDocumentTitle'),
266
+ closeDocumentImport: resolve('closeDocumentImport'),
267
+ importDocumentDescription: resolve('importDocumentDescription'),
268
+ importDocumentLabel: resolve('importDocumentLabel'),
269
+ importDocumentPlaceholder: resolve('importDocumentPlaceholder'),
270
+ applyDocumentImport: resolve('applyDocumentImport'),
271
+ cancelDocumentImport: resolve('cancelDocumentImport'),
272
+ documentCopied: resolve('documentCopied'),
273
+ documentCopyFailed: resolve('documentCopyFailed'),
274
+ documentImportUnavailable: resolve('documentImportUnavailable'),
275
+ documentImportInvalidJson: resolve('documentImportInvalidJson'),
276
+ documentImportUnsupportedVersion: resolve('documentImportUnsupportedVersion'),
277
+ documentImportInvalidDocument: resolve('documentImportInvalidDocument'),
278
+ documentImportDuplicateId: resolve('documentImportDuplicateId'),
279
+ documentImportIncompatibleSource: resolve('documentImportIncompatibleSource'),
280
+ documentImportIncompatibleTarget: resolve('documentImportIncompatibleTarget'),
281
+ documentImportUnavailableTransform: resolve('documentImportUnavailableTransform'),
165
282
  previewTitle: resolve('previewTitle'),
166
283
  previewLoading: resolve('previewLoading'),
167
284
  previewError: resolve('previewError'),