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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +32 -0
  2. package/package.json +9 -9
  3. package/src/commands/use-command-descriptors.ts +7 -3
  4. package/src/commands/use-extension-registry-command-descriptors.ts +23 -6
  5. package/src/devtools/use-workbench-devtools-snapshot.ts +39 -14
  6. package/src/devtools/workbench-devtools-snapshot.ts +32 -13
  7. package/src/editor/area.tsx +3 -2
  8. package/src/editor/state-storage.ts +31 -0
  9. package/src/editor/tab-context-menu.ts +8 -8
  10. package/src/editor/use-editor.ts +6 -6
  11. package/src/explorer/context-menu.ts +8 -4
  12. package/src/explorer/view.tsx +4 -3
  13. package/src/extensions/canonical-extension-descriptions.ts +58 -0
  14. package/src/extensions/context-menu.ts +9 -6
  15. package/src/extensions/extension-enablement-context.ts +15 -0
  16. package/src/extensions/extension-enablement-controller.ts +453 -0
  17. package/src/extensions/management-model.ts +111 -47
  18. package/src/extensions/theme-selection-protection.ts +98 -0
  19. package/src/extensions/uninstall-eligibility.ts +103 -0
  20. package/src/extensions/use-extension-management.ts +151 -67
  21. package/src/extensions/view.tsx +4 -0
  22. package/src/field-remap/chrome-labels.ts +58 -2
  23. package/src/field-remap/convert-palette.tsx +151 -6
  24. package/src/field-remap/demo.tsx +31 -2
  25. package/src/field-remap/flow.tsx +57 -13
  26. package/src/field-remap/history.ts +99 -0
  27. package/src/field-remap/index.ts +9 -1
  28. package/src/field-remap/panel.tsx +371 -97
  29. package/src/field-remap/preview-controller.ts +122 -0
  30. package/src/field-remap/preview.tsx +171 -0
  31. package/src/field-remap/view.css +72 -0
  32. package/src/index.ts +16 -1
  33. package/src/management/keybinding-overrides-storage.ts +28 -0
  34. package/src/management/preference-settings-storage.ts +28 -0
  35. package/src/management/settings.tsx +2 -0
  36. package/src/management/use-command-management.ts +31 -26
  37. package/src/management/use-keybinding-management.ts +25 -12
  38. package/src/shell/focused-extension-services.ts +138 -0
  39. package/src/shell/host-shell.tsx +13 -10
  40. package/src/shell/persistence-diagnostic-context.ts +11 -0
  41. package/src/shell/provider.tsx +373 -69
  42. package/src/shell/settings.tsx +25 -16
  43. package/src/shell/shell.tsx +119 -78
  44. package/src/shell/view-host.tsx +23 -22
  45. package/src/storage/local-json-storage.ts +43 -29
  46. package/src/storage/persistence-diagnostics.ts +72 -0
  47. package/src/workbench/appearance-storage.ts +28 -0
  48. package/src/workbench/command-host.tsx +19 -22
  49. package/src/workbench/command-palette.ts +14 -13
  50. package/src/workbench/layout-storage.ts +52 -5
  51. package/src/workbench/use-persisted-appearance.ts +38 -12
@@ -7,21 +7,35 @@ import {
7
7
  type ExtensionFeatureSpec,
8
8
  type ExtensionInstallPlan,
9
9
  type ExtensionInstallPlanInstallSource,
10
- type ExtensionRegistry,
11
10
  type InstalledExtensionRecord,
12
11
  type WorkbenchExtensionDescription,
13
12
  } from '@workbench-kit/workbench-core';
13
+ import type { WorkbenchExtensionCatalogReader } from '../shell/provider.js';
14
14
  import type {
15
15
  ExtensionCatalogBrowseEntry,
16
16
  ExtensionManagementDiagnosticSummary,
17
17
  ExtensionManagementEntry,
18
18
  ExtensionManagementFeatureSummary,
19
+ ExtensionManagementTransition,
19
20
  } from '@workbench-kit/react/workbench/management';
21
+ import {
22
+ createExtensionUninstallEvaluation,
23
+ type ExtensionUninstallEligibility,
24
+ } from './uninstall-eligibility.js';
25
+ import { createCanonicalExtensionDescriptionSnapshot } from './canonical-extension-descriptions.js';
26
+
27
+ export interface ExtensionUninstallActionConstraint {
28
+ readonly eligibility: Exclude<ExtensionUninstallEligibility, { readonly kind: 'eligible' }>;
29
+ readonly extensionId: string;
30
+ }
20
31
 
21
32
  export interface CreateExtensionManagementEntriesInput {
22
33
  readonly availableExtensions: readonly WorkbenchExtensionDescription[];
23
- readonly extensionRegistry: ExtensionRegistry;
34
+ readonly extensionCatalog: WorkbenchExtensionCatalogReader;
24
35
  readonly installedRecords: readonly InstalledExtensionRecord[];
36
+ readonly transition?:
37
+ (ExtensionManagementTransition & { readonly extensionId: string }) | undefined;
38
+ readonly uninstallActionConstraint?: ExtensionUninstallActionConstraint | undefined;
25
39
  }
26
40
 
27
41
  export interface CreateExtensionCatalogBrowseEntriesInput extends CreateExtensionManagementEntriesInput {
@@ -37,27 +51,55 @@ export interface ExtensionInstallPlanningContext extends CreateExtensionManageme
37
51
 
38
52
  export function createExtensionManagementEntries({
39
53
  availableExtensions,
40
- extensionRegistry,
54
+ extensionCatalog,
41
55
  installedRecords,
56
+ transition,
57
+ uninstallActionConstraint,
42
58
  }: CreateExtensionManagementEntriesInput): readonly ExtensionManagementEntry[] {
43
59
  const installedById = new Map(installedRecords.map((record) => [record.id, record]));
44
- const extensionFeatures = createExtensionManagementFeatureMaps(
60
+ const liveExtensions = extensionCatalog.getExtensions();
61
+ const liveExtensionIds = new Set(liveExtensions.map((extension) => extension.manifest.id));
62
+ const canonicalDescriptions = createCanonicalExtensionDescriptionSnapshot({
45
63
  availableExtensions,
46
- extensionRegistry,
64
+ liveExtensions,
65
+ });
66
+ const uninstallEvaluation = createExtensionUninstallEvaluation({
67
+ canonicalDescriptions,
68
+ installedRecords,
69
+ });
70
+ const extensionFeatures = createExtensionManagementFeatureMaps(
71
+ canonicalDescriptions.descriptions,
72
+ extensionCatalog,
47
73
  );
48
- const bundledEntries = availableExtensions
74
+ return canonicalDescriptions.descriptions
49
75
  .map((extension) => {
50
76
  const installed = installedById.get(extension.manifest.id);
51
77
  const isBuiltin = extension.manifest.id.startsWith('workbench-kit.builtin.');
78
+ const isLive = liveExtensionIds.has(extension.manifest.id);
52
79
  const featureState = resolveExtensionManagementFeatureState(
53
80
  extension.manifest.id,
54
81
  extensionFeatures,
55
82
  );
83
+ const uninstallEligibility =
84
+ installed && !isBuiltin
85
+ ? uninstallEvaluation.getEligibility(extension.manifest.id)
86
+ : undefined;
87
+ const actionConstraint =
88
+ uninstallActionConstraint?.extensionId === extension.manifest.id
89
+ ? uninstallActionConstraint.eligibility
90
+ : undefined;
56
91
 
57
92
  return {
58
- category: installed?.category ?? (isBuiltin ? 'builtin' : 'sample'),
93
+ ...(uninstallEligibility?.kind === 'eligible' && !actionConstraint
94
+ ? { canUninstall: true }
95
+ : {}),
96
+ category: installed?.category ?? (isBuiltin ? 'builtin' : isLive ? 'installed' : 'sample'),
59
97
  description: extension.manifest.displayName,
60
- diagnostics: featureState.diagnostics,
98
+ diagnostics: mergeUninstallDiagnostics(
99
+ featureState.diagnostics,
100
+ uninstallEligibility,
101
+ actionConstraint,
102
+ ),
61
103
  displayName: extension.manifest.displayName,
62
104
  enabled: isBuiltin ? true : (installed?.enabled ?? false),
63
105
  features: featureState.features,
@@ -65,53 +107,77 @@ export function createExtensionManagementEntries({
65
107
  installedAt: installed?.installedAt,
66
108
  manifestUrl: installed?.manifestUrl,
67
109
  source: isBuiltin ? ('bundled' as const) : ('installed' as const),
110
+ ...(transition?.extensionId === extension.manifest.id
111
+ ? { transition: { kind: transition.kind, message: transition.message } }
112
+ : {}),
68
113
  } satisfies ExtensionManagementEntry;
69
114
  })
70
- .filter((entry) => entry.source === 'bundled' || installedById.has(entry.id));
71
-
72
- const activeExtensions = extensionRegistry
73
- .getExtensions()
74
115
  .filter(
75
- (extension) =>
76
- !availableExtensions.some((bundled) => bundled.manifest.id === extension.manifest.id),
116
+ (entry) =>
117
+ entry.source === 'bundled' || installedById.has(entry.id) || liveExtensionIds.has(entry.id),
77
118
  )
78
- .map((extension) => {
79
- const installed = installedById.get(extension.manifest.id);
80
- const featureState = resolveExtensionManagementFeatureState(
81
- extension.manifest.id,
82
- extensionFeatures,
83
- );
119
+ .sort((left, right) => left.displayName.localeCompare(right.displayName));
120
+ }
84
121
 
85
- return {
86
- category: installed?.category ?? 'installed',
87
- description: extension.manifest.displayName,
88
- diagnostics: featureState.diagnostics,
89
- displayName: extension.manifest.displayName,
90
- enabled: installed?.enabled ?? true,
91
- features: featureState.features,
92
- id: extension.manifest.id,
93
- installedAt: installed?.installedAt,
94
- manifestUrl: installed?.manifestUrl,
95
- source: 'installed',
96
- } satisfies ExtensionManagementEntry;
97
- });
122
+ function mergeUninstallDiagnostics(
123
+ diagnostics: readonly ExtensionManagementDiagnosticSummary[] | undefined,
124
+ eligibility: ExtensionUninstallEligibility | undefined,
125
+ actionConstraint?: ExtensionUninstallActionConstraint['eligibility'] | undefined,
126
+ ): readonly ExtensionManagementDiagnosticSummary[] | undefined {
127
+ const merged = [
128
+ ...(diagnostics ?? []),
129
+ ...toUninstallDiagnostics(eligibility),
130
+ ...toUninstallDiagnostics(actionConstraint),
131
+ ];
132
+ const byMessage = new Map(merged.map((diagnostic) => [diagnostic.message, diagnostic]));
133
+ return byMessage.size > 0 ? [...byMessage.values()] : undefined;
134
+ }
98
135
 
99
- return [...bundledEntries, ...activeExtensions].sort((left, right) =>
100
- left.displayName.localeCompare(right.displayName),
101
- );
136
+ function toUninstallDiagnostics(
137
+ eligibility: ExtensionUninstallEligibility | undefined,
138
+ ): readonly ExtensionManagementDiagnosticSummary[] {
139
+ if (!eligibility || eligibility.kind === 'eligible') {
140
+ return [];
141
+ }
142
+ if (eligibility.kind === 'ineligibleTarget') {
143
+ return [
144
+ {
145
+ message:
146
+ eligibility.reason === 'builtin'
147
+ ? `Cannot uninstall built-in extensions: ${eligibility.diagnosticExtensionIds.join(', ')}.`
148
+ : `Cannot uninstall because these persisted targets are no longer installed: ${eligibility.diagnosticExtensionIds.join(', ')}.`,
149
+ severity: 'error',
150
+ },
151
+ ];
152
+ }
153
+
154
+ const result: ExtensionManagementDiagnosticSummary[] = [];
155
+ if (eligibility.dependentExtensionIds.length > 0) {
156
+ result.push({
157
+ message: `Cannot uninstall because these installed extensions depend on it: ${eligibility.dependentExtensionIds.join(', ')}.`,
158
+ severity: 'error',
159
+ });
160
+ }
161
+ if (eligibility.unresolvedExtensionIds.length > 0) {
162
+ result.push({
163
+ message: `Cannot verify uninstall safety because these extension manifests are unavailable or ambiguous: ${eligibility.unresolvedExtensionIds.join(', ')}.`,
164
+ severity: 'error',
165
+ });
166
+ }
167
+ return result;
102
168
  }
103
169
 
104
170
  export function createExtensionCatalogBrowseEntries({
105
171
  availableExtensions,
106
172
  catalogEntries,
107
- extensionRegistry,
173
+ extensionCatalog,
108
174
  installedRecords,
109
175
  }: CreateExtensionCatalogBrowseEntriesInput): readonly ExtensionCatalogBrowseEntry[] {
110
176
  const installedIds = new Set(installedRecords.map((record) => record.id));
111
177
  const installContext = createExtensionInstallPlanningContext({
112
178
  availableExtensions,
113
179
  catalogEntries,
114
- extensionRegistry,
180
+ extensionCatalog,
115
181
  installedRecords,
116
182
  });
117
183
 
@@ -139,21 +205,19 @@ export function createExtensionCatalogBrowseEntries({
139
205
  export function createExtensionInstallPlanningContext({
140
206
  availableExtensions,
141
207
  catalogEntries,
142
- extensionRegistry,
208
+ extensionCatalog,
143
209
  installedRecords,
144
210
  }: CreateExtensionCatalogBrowseEntriesInput): ExtensionInstallPlanningContext {
145
211
  const installableExtensions = mergeUniqueExtensionDescriptions([
146
212
  ...availableExtensions,
147
- ...extensionRegistry.getExtensions(),
213
+ ...extensionCatalog.getExtensions(),
148
214
  ]);
149
215
 
150
216
  return {
151
217
  availableExtensions: installableExtensions,
152
- enabledExtensionIds: extensionRegistry
153
- .getExtensions()
154
- .map((extension) => extension.manifest.id),
155
- extensionRegistry,
156
- hostCapabilityIds: extensionRegistry.capabilityRegistry.listProviderIds(),
218
+ enabledExtensionIds: extensionCatalog.getExtensions().map((extension) => extension.manifest.id),
219
+ extensionCatalog,
220
+ hostCapabilityIds: extensionCatalog.listCapabilityProviderIds(),
157
221
  installSources: createExtensionInstallSources(catalogEntries, installableExtensions),
158
222
  installedRecords,
159
223
  };
@@ -220,7 +284,7 @@ function mergeUniqueExtensionDescriptions(
220
284
 
221
285
  function createExtensionManagementFeatureMaps(
222
286
  availableExtensions: readonly WorkbenchExtensionDescription[],
223
- extensionRegistry: ExtensionRegistry,
287
+ extensionCatalog: WorkbenchExtensionCatalogReader,
224
288
  ) {
225
289
  return {
226
290
  bundledFeaturesById: new Map(
@@ -230,7 +294,7 @@ function createExtensionManagementFeatureMaps(
230
294
  ]),
231
295
  ),
232
296
  inspectionsById: new Map(
233
- extensionRegistry
297
+ extensionCatalog
234
298
  .getFeatureInspections()
235
299
  .map((inspection) => [inspection.feature.id, inspection]),
236
300
  ),
@@ -0,0 +1,98 @@
1
+ import {
2
+ DARK_THEME_PRESET_OPTIONS,
3
+ LIGHT_THEME_PRESET_OPTIONS,
4
+ WORKBENCH_COLOR_SCHEME_OPTIONS,
5
+ } from '@workbench-kit/react/workbench';
6
+ import type { ThemeRegistry } from '@workbench-kit/workbench-core';
7
+
8
+ interface ThemeOptionLike {
9
+ readonly id: string;
10
+ }
11
+
12
+ export type ThemeSelectionProtectionSnapshot =
13
+ | {
14
+ readonly kind: 'known';
15
+ readonly protectedThemeIds: readonly string[];
16
+ readonly themeRegistryRevision: number;
17
+ }
18
+ | {
19
+ readonly kind: 'unknown';
20
+ readonly themeRegistryRevision: number;
21
+ };
22
+
23
+ export interface ThemeSelectionProtectionInput {
24
+ readonly darkPreset: string | undefined;
25
+ readonly lightPreset: string | undefined;
26
+ readonly theme: string | undefined;
27
+ readonly themeOptions: readonly ThemeOptionLike[] | undefined;
28
+ readonly themes: ThemeRegistry;
29
+ }
30
+
31
+ /**
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.
35
+ */
36
+ export function createThemeSelectionProtectionSnapshot({
37
+ darkPreset,
38
+ lightPreset,
39
+ theme,
40
+ themeOptions,
41
+ themes,
42
+ }: ThemeSelectionProtectionInput): ThemeSelectionProtectionSnapshot {
43
+ const themeRegistryRevision = themes.getRevision();
44
+ const contributedThemes = themes.getThemes();
45
+ const hasLightPreset = lightPreset !== undefined;
46
+ const hasDarkPreset = darkPreset !== undefined;
47
+
48
+ if (hasLightPreset !== hasDarkPreset) {
49
+ return { kind: 'unknown', themeRegistryRevision };
50
+ }
51
+
52
+ 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
+
62
+ if (
63
+ !hasExactlyOneOption(theme, WORKBENCH_COLOR_SCHEME_OPTIONS) ||
64
+ !hasExactlyOneOption(lightPreset, lightOptions) ||
65
+ !hasExactlyOneOption(darkPreset, darkOptions)
66
+ ) {
67
+ return { kind: 'unknown', themeRegistryRevision };
68
+ }
69
+
70
+ return {
71
+ kind: 'known',
72
+ protectedThemeIds: [theme, lightPreset, darkPreset],
73
+ themeRegistryRevision,
74
+ };
75
+ }
76
+
77
+ const themeSources = [...(themeOptions ?? []), ...contributedThemes];
78
+ if (!hasExactlyOneOption(theme, themeSources)) {
79
+ return { kind: 'unknown', themeRegistryRevision };
80
+ }
81
+
82
+ return {
83
+ kind: 'known',
84
+ protectedThemeIds: [theme],
85
+ themeRegistryRevision,
86
+ };
87
+ }
88
+
89
+ function hasExactlyOneOption(
90
+ selection: string | undefined,
91
+ options: readonly ThemeOptionLike[],
92
+ ): selection is string {
93
+ if (!selection) {
94
+ return false;
95
+ }
96
+
97
+ return options.filter((option) => option.id === selection).length === 1;
98
+ }
@@ -0,0 +1,103 @@
1
+ import type { InstalledExtensionRecord } from '@workbench-kit/workbench-core';
2
+
3
+ import type { CanonicalExtensionDescriptionSnapshot } from './canonical-extension-descriptions.js';
4
+
5
+ const BUILTIN_EXTENSION_ID_PREFIX = 'workbench-kit.builtin.' as const;
6
+
7
+ export type ExtensionUninstallEligibility =
8
+ | {
9
+ readonly kind: 'eligible';
10
+ }
11
+ | {
12
+ readonly diagnosticExtensionIds: readonly string[];
13
+ readonly kind: 'ineligibleTarget';
14
+ readonly reason: 'builtin' | 'notInstalled';
15
+ }
16
+ | {
17
+ readonly dependentExtensionIds: readonly string[];
18
+ readonly kind: 'blocked';
19
+ readonly unresolvedExtensionIds: readonly string[];
20
+ };
21
+
22
+ export interface ExtensionUninstallEvaluation {
23
+ getEligibility(extensionId: string): ExtensionUninstallEligibility;
24
+ }
25
+
26
+ export function createExtensionUninstallEvaluation({
27
+ canonicalDescriptions,
28
+ installedRecords,
29
+ }: {
30
+ readonly canonicalDescriptions: CanonicalExtensionDescriptionSnapshot;
31
+ readonly installedRecords: readonly InstalledExtensionRecord[];
32
+ }): ExtensionUninstallEvaluation {
33
+ const installedRecordsById = new Map<string, InstalledExtensionRecord>();
34
+ const duplicateInstalledExtensionIds = new Set<string>();
35
+ for (const record of installedRecords) {
36
+ if (installedRecordsById.has(record.id)) {
37
+ duplicateInstalledExtensionIds.add(record.id);
38
+ continue;
39
+ }
40
+ installedRecordsById.set(record.id, record);
41
+ }
42
+
43
+ const dependentsByTargetId = new Map<string, Set<string>>();
44
+ const unresolvedExtensionIdSet = new Set<string>();
45
+
46
+ for (const record of installedRecordsById.values()) {
47
+ const description = canonicalDescriptions.getDescription(record.id);
48
+ if (!description || duplicateInstalledExtensionIds.has(record.id)) {
49
+ unresolvedExtensionIdSet.add(record.id);
50
+ continue;
51
+ }
52
+
53
+ for (const dependencyId of description.manifest.extensionDependencies ?? []) {
54
+ if (dependencyId === record.id || !installedRecordsById.has(dependencyId)) {
55
+ continue;
56
+ }
57
+ const dependents = dependentsByTargetId.get(dependencyId) ?? new Set<string>();
58
+ dependents.add(record.id);
59
+ dependentsByTargetId.set(dependencyId, dependents);
60
+ }
61
+ }
62
+
63
+ const unresolvedExtensionIds = [...unresolvedExtensionIdSet].sort((left, right) =>
64
+ left.localeCompare(right),
65
+ );
66
+ const eligibilityByExtensionId = new Map<string, ExtensionUninstallEligibility>();
67
+ for (const extensionId of installedRecordsById.keys()) {
68
+ if (extensionId.startsWith(BUILTIN_EXTENSION_ID_PREFIX)) {
69
+ eligibilityByExtensionId.set(extensionId, {
70
+ diagnosticExtensionIds: [extensionId],
71
+ kind: 'ineligibleTarget',
72
+ reason: 'builtin',
73
+ });
74
+ continue;
75
+ }
76
+
77
+ const dependentExtensionIds = [...(dependentsByTargetId.get(extensionId) ?? [])].sort(
78
+ (left, right) => left.localeCompare(right),
79
+ );
80
+ eligibilityByExtensionId.set(
81
+ extensionId,
82
+ dependentExtensionIds.length === 0 && unresolvedExtensionIds.length === 0
83
+ ? { kind: 'eligible' }
84
+ : {
85
+ dependentExtensionIds,
86
+ kind: 'blocked',
87
+ unresolvedExtensionIds,
88
+ },
89
+ );
90
+ }
91
+
92
+ return {
93
+ getEligibility(extensionId) {
94
+ return (
95
+ eligibilityByExtensionId.get(extensionId) ?? {
96
+ diagnosticExtensionIds: [extensionId],
97
+ kind: 'ineligibleTarget',
98
+ reason: 'notInstalled',
99
+ }
100
+ );
101
+ },
102
+ };
103
+ }