@workbench-kit/shell-react 0.0.2-prototype.0.2.6

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 (93) hide show
  1. package/package.json +49 -0
  2. package/src/chat-ai-mock-proposals.ts +39 -0
  3. package/src/chat-command-input.ts +52 -0
  4. package/src/chat-command-policy.ts +54 -0
  5. package/src/chat-command-surface.tsx +155 -0
  6. package/src/chat-view-data.ts +20 -0
  7. package/src/chat-view.tsx +220 -0
  8. package/src/command-inspector-surface.tsx +46 -0
  9. package/src/commands-view-data.ts +17 -0
  10. package/src/commands-view.tsx +43 -0
  11. package/src/createCommandWorkspaceExplorerPort.ts +86 -0
  12. package/src/devtools/WorkbenchDevtoolsPanel.tsx +124 -0
  13. package/src/devtools/WorkbenchDevtoolsShell.tsx +17 -0
  14. package/src/devtools/index.ts +11 -0
  15. package/src/devtools/use-workbench-devtools-snapshot.ts +84 -0
  16. package/src/devtools/workbench-devtools-snapshot.ts +140 -0
  17. package/src/devtools/workbench-devtools.css +82 -0
  18. package/src/editor-area-dnd.ts +69 -0
  19. package/src/editor-area-model.ts +71 -0
  20. package/src/editor-area.css +454 -0
  21. package/src/editor-area.tsx +676 -0
  22. package/src/editor-host-surface.tsx +634 -0
  23. package/src/editor-pane-visibility.ts +130 -0
  24. package/src/editor-resource.ts +36 -0
  25. package/src/editor-state-storage.ts +193 -0
  26. package/src/editor-tab-context-menu.ts +118 -0
  27. package/src/editor-view-providers.tsx +552 -0
  28. package/src/editor-workspace-file-availability.ts +15 -0
  29. package/src/editor-workspace-reconcile.tsx +28 -0
  30. package/src/explorer-context-menu.ts +114 -0
  31. package/src/explorer-reveal.ts +112 -0
  32. package/src/explorer-view-data.ts +58 -0
  33. package/src/explorer-view.tsx +224 -0
  34. package/src/extension-context-menu.ts +52 -0
  35. package/src/extension-management-model.ts +313 -0
  36. package/src/extensions-view-data.ts +19 -0
  37. package/src/extensions-view.tsx +38 -0
  38. package/src/field-remap-demo.tsx +18 -0
  39. package/src/field-remap-editor-surface.tsx +36 -0
  40. package/src/field-remap-flow-adapter.ts +240 -0
  41. package/src/field-remap-flow.tsx +369 -0
  42. package/src/field-remap-graph.tsx +559 -0
  43. package/src/field-remap-panel.tsx +175 -0
  44. package/src/field-remap-samples.ts +352 -0
  45. package/src/field-remap-tree.tsx +404 -0
  46. package/src/field-remap-view-data.ts +16 -0
  47. package/src/field-remap-view.css +676 -0
  48. package/src/field-remap-view.tsx +86 -0
  49. package/src/index.ts +199 -0
  50. package/src/jdw-lab-view-data.ts +25 -0
  51. package/src/jdw-lab-view.css +21 -0
  52. package/src/jdw-lab-view.tsx +79 -0
  53. package/src/jdw-widget-form-view.tsx +65 -0
  54. package/src/jdw-widget-preview-view.tsx +51 -0
  55. package/src/json-form-source-patch.ts +323 -0
  56. package/src/jsonata-transform.ts +36 -0
  57. package/src/keybinding-management-settings.tsx +16 -0
  58. package/src/keybinding-overrides-storage.ts +57 -0
  59. package/src/management-palette-commands.ts +38 -0
  60. package/src/management-settings-ids.ts +12 -0
  61. package/src/management-settings.tsx +75 -0
  62. package/src/preference-settings-storage.ts +58 -0
  63. package/src/provider.tsx +573 -0
  64. package/src/search-view-data.ts +15 -0
  65. package/src/search-view.tsx +62 -0
  66. package/src/shell-model.tsx +149 -0
  67. package/src/shell-secondary-actions.tsx +59 -0
  68. package/src/shell-settings-constants.ts +9 -0
  69. package/src/shell-settings.tsx +599 -0
  70. package/src/shell-titlebar-layout-controls.tsx +4 -0
  71. package/src/shell-view-host.tsx +199 -0
  72. package/src/shell.tsx +649 -0
  73. package/src/table-mapping-adapter.ts +59 -0
  74. package/src/use-active-workspace-path.ts +15 -0
  75. package/src/use-command-management.ts +191 -0
  76. package/src/use-context-key-revision.ts +18 -0
  77. package/src/use-editor.ts +107 -0
  78. package/src/use-extension-management.ts +193 -0
  79. package/src/use-keybinding-management.ts +130 -0
  80. package/src/use-persisted-workbench-appearance.ts +33 -0
  81. package/src/use-workbench-chat-command-proposals.ts +194 -0
  82. package/src/use-workbench-command-descriptors.ts +41 -0
  83. package/src/workbench-appearance-storage.ts +115 -0
  84. package/src/workbench-command-host.tsx +243 -0
  85. package/src/workbench-command-palette.ts +208 -0
  86. package/src/workbench-keybinding-bridge.ts +54 -0
  87. package/src/workbench-layout-storage.ts +145 -0
  88. package/src/workbench-profile-modal.tsx +118 -0
  89. package/src/workbench-shell-command-registration.ts +58 -0
  90. package/src/workbench-startup-gate.tsx +136 -0
  91. package/src/workbench-status-sections.ts +43 -0
  92. package/src/workbench-user-commands.ts +46 -0
  93. package/src/workspace-view-state.ts +35 -0
@@ -0,0 +1,634 @@
1
+ import {
2
+ isValidElement,
3
+ lazy,
4
+ Suspense,
5
+ useCallback,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ type ReactNode,
11
+ } from 'react';
12
+ import { createPortal } from 'react-dom';
13
+ import { IconButton, ScrollArea } from '@workbench-kit/react/primitives';
14
+ import { SplitView } from '@workbench-kit/react/workbench/split-view';
15
+ import type { WorkspaceEditorTheme } from '@workbench-kit/react/workbench/workspace/editor';
16
+ import {
17
+ EDITOR_SAVE_COMMAND_ID,
18
+ type EditorHost,
19
+ type EditorTabState,
20
+ } from '@workbench-kit/workbench-core';
21
+
22
+ import { CommandInspectorSurface } from './command-inspector-surface.js';
23
+ import {
24
+ resolveEditorDocumentViews,
25
+ type EditorDocumentViewProvider,
26
+ } from './editor-view-providers.js';
27
+ import { mimeTypeForResource, pathForResource } from './editor-resource.js';
28
+ import { FieldRemapEditorSurface } from './field-remap-editor-surface.js';
29
+ import {
30
+ editorViewModeToPaneVisibility,
31
+ getVisibleEditorPaneKinds,
32
+ paneVisibilityToPrimaryViewMode,
33
+ resolveDefaultEditorPaneVisibility,
34
+ resolveEffectiveEditorPaneVisibility,
35
+ type EditorPaneKind,
36
+ type EditorPaneVisibility,
37
+ type EditorViewMode,
38
+ } from './editor-pane-visibility.js';
39
+ import { useWorkbench } from './provider.js';
40
+ import { useEditorHost, useEditorService } from './use-editor.js';
41
+
42
+ export type { EditorViewMode } from './editor-pane-visibility.js';
43
+
44
+ const WorkspaceEditor = lazy(async () => {
45
+ const module = await import('@workbench-kit/react/workbench/workspace/editor');
46
+ return { default: module.WorkspaceEditor };
47
+ });
48
+
49
+ export interface EditorHostSurfaceProps {
50
+ activeTab: EditorTabState | undefined;
51
+ defaultViewModeForResource: ((resourceUri: string) => EditorViewMode | undefined) | undefined;
52
+ modeToolbarHost: HTMLDivElement | null;
53
+ onModeToolbarVisibleChange: (visible: boolean) => void;
54
+ theme: WorkspaceEditorTheme | undefined;
55
+ viewProviders: readonly EditorDocumentViewProvider[];
56
+ }
57
+
58
+ export function EditorHostSurface({
59
+ activeTab,
60
+ defaultViewModeForResource,
61
+ modeToolbarHost,
62
+ onModeToolbarVisibleChange,
63
+ theme,
64
+ viewProviders,
65
+ }: EditorHostSurfaceProps) {
66
+ const editorService = useEditorService();
67
+ const host = useEditorHost(activeTab?.id);
68
+ const hostFrameRef = useRef<HTMLDivElement>(null);
69
+
70
+ useEffect(() => {
71
+ if (!host || !activeTab) {
72
+ return undefined;
73
+ }
74
+
75
+ host.onDidChangeDirty = (dirty) => {
76
+ editorService.setDirty(activeTab.id, dirty);
77
+ };
78
+
79
+ return () => {
80
+ host.onDidChangeDirty = undefined;
81
+ };
82
+ }, [activeTab, editorService, host]);
83
+
84
+ if (!activeTab || !host) {
85
+ return null;
86
+ }
87
+
88
+ const rendered = host.render();
89
+
90
+ if (isMissingResourceEditorRenderPayload(rendered)) {
91
+ return (
92
+ <MissingResourceEditorSurface
93
+ message={rendered.message}
94
+ resourceUri={rendered.resourceUri}
95
+ tabId={activeTab.id}
96
+ title={host.title ?? activeTab.title}
97
+ />
98
+ );
99
+ }
100
+
101
+ if (isCommandInspectorEditorRenderPayload(rendered)) {
102
+ return (
103
+ <CommandInspectorSurface
104
+ commandId={rendered.commandId}
105
+ resourceUri={rendered.resourceUri}
106
+ tabId={activeTab.id}
107
+ />
108
+ );
109
+ }
110
+
111
+ if (isFieldRemapEditorRenderPayload(rendered)) {
112
+ return (
113
+ <FieldRemapEditorSurface
114
+ resourceUri={rendered.resourceUri}
115
+ surfaceId={rendered.surfaceId}
116
+ tabId={activeTab.id}
117
+ />
118
+ );
119
+ }
120
+
121
+ if (isTextEditorRenderPayload(rendered)) {
122
+ return (
123
+ <TextEditorSurface
124
+ defaultViewModeForResource={defaultViewModeForResource}
125
+ host={host as TextEditorHostLike}
126
+ initialContent={rendered.initialContent}
127
+ modeToolbarHost={modeToolbarHost}
128
+ mimeType={rendered.mimeType}
129
+ onModeToolbarVisibleChange={onModeToolbarVisibleChange}
130
+ resourceUri={rendered.resourceUri}
131
+ tabId={activeTab.id}
132
+ theme={theme}
133
+ viewProviders={viewProviders}
134
+ />
135
+ );
136
+ }
137
+
138
+ return (
139
+ <div
140
+ ref={hostFrameRef}
141
+ aria-label={host.title ?? activeTab.title ?? 'Editor'}
142
+ className="workbench-editor-area__host"
143
+ data-editor-host-id={activeTab.id}
144
+ >
145
+ {toReactNode(rendered)}
146
+ </div>
147
+ );
148
+ }
149
+
150
+ interface TextEditorHostLike extends EditorHost {
151
+ getContent?(): string;
152
+ markDirty?(): void;
153
+ setContent?(content: string): void;
154
+ setDirty?(dirty: boolean): void;
155
+ }
156
+
157
+ interface TextEditorRenderPayload {
158
+ initialContent: string;
159
+ kind: 'workbench-kit.builtin.editor/text';
160
+ mimeType?: string | undefined;
161
+ resourceUri: string;
162
+ }
163
+
164
+ function TextEditorSurface({
165
+ defaultViewModeForResource,
166
+ host,
167
+ initialContent,
168
+ modeToolbarHost,
169
+ mimeType,
170
+ onModeToolbarVisibleChange,
171
+ resourceUri,
172
+ tabId,
173
+ theme,
174
+ viewProviders,
175
+ }: {
176
+ defaultViewModeForResource: ((resourceUri: string) => EditorViewMode | undefined) | undefined;
177
+ host: TextEditorHostLike;
178
+ initialContent: string;
179
+ modeToolbarHost: HTMLDivElement | null;
180
+ mimeType?: string | undefined;
181
+ onModeToolbarVisibleChange: (visible: boolean) => void;
182
+ resourceUri: string;
183
+ tabId: string;
184
+ theme: WorkspaceEditorTheme | undefined;
185
+ viewProviders: readonly EditorDocumentViewProvider[];
186
+ }) {
187
+ const editorService = useEditorService();
188
+ const { executeCommand, workspaceHostPort } = useWorkbench();
189
+ const [content, setContent] = useState(initialContent);
190
+ const previousResourceUriRef = useRef<string | undefined>(undefined);
191
+ const splitSizesRef = useRef<Record<string, number>>({});
192
+ const editorDocument = useMemo(
193
+ () => ({
194
+ content,
195
+ mimeType: mimeType ?? mimeTypeForResource(resourceUri),
196
+ path: pathForResource(resourceUri),
197
+ resourceUri,
198
+ }),
199
+ [content, mimeType, resourceUri],
200
+ );
201
+ const { formProvider, previewProvider } = useMemo(
202
+ () => resolveEditorDocumentViews(editorDocument, viewProviders),
203
+ [editorDocument, viewProviders],
204
+ );
205
+ const formEligible = Boolean(formProvider);
206
+ const previewEligible = Boolean(previewProvider);
207
+ const [panePreference, setPanePreference] = useState<EditorPaneVisibility>(() =>
208
+ resolveDefaultEditorPaneVisibility(defaultViewModeForResource?.(resourceUri), {
209
+ formEligible,
210
+ }),
211
+ );
212
+ const paneVisibility = useMemo(
213
+ () => resolveEffectiveEditorPaneVisibility(panePreference, { formEligible, previewEligible }),
214
+ [formEligible, panePreference, previewEligible],
215
+ );
216
+ const codeIcon = isJsonSourceDocument(editorDocument) ? 'codicon-json' : 'codicon-code';
217
+ const modeToolbarVisible = formEligible || previewEligible;
218
+
219
+ useEffect(() => {
220
+ const resourceChanged = previousResourceUriRef.current !== resourceUri;
221
+ previousResourceUriRef.current = resourceUri;
222
+
223
+ if (resourceChanged) {
224
+ setContent(initialContent);
225
+ setPanePreference(
226
+ resolveDefaultEditorPaneVisibility(defaultViewModeForResource?.(resourceUri), {
227
+ formEligible,
228
+ }),
229
+ );
230
+ return;
231
+ }
232
+
233
+ setContent((current) => (current === initialContent ? current : initialContent));
234
+ }, [defaultViewModeForResource, formEligible, initialContent, resourceUri]);
235
+
236
+ useEffect(() => {
237
+ onModeToolbarVisibleChange(modeToolbarVisible);
238
+ }, [modeToolbarVisible, onModeToolbarVisibleChange]);
239
+
240
+ useEffect(() => {
241
+ return () => {
242
+ onModeToolbarVisibleChange(false);
243
+ };
244
+ }, [onModeToolbarVisibleChange]);
245
+
246
+ const getSplitSize = useCallback((key: string, defaultPercent: number) => {
247
+ return splitSizesRef.current[key] ?? defaultPercent;
248
+ }, []);
249
+
250
+ const setSplitSize = useCallback((key: string, percent: number) => {
251
+ splitSizesRef.current[key] = percent;
252
+ }, []);
253
+
254
+ const handleChange = useCallback(
255
+ (nextContent: string) => {
256
+ setContent(nextContent);
257
+ host.setContent?.(nextContent);
258
+ host.markDirty?.();
259
+ editorService.promotePreviewOnEdit(tabId);
260
+ },
261
+ [editorService, host, tabId],
262
+ );
263
+
264
+ const handleSave = useCallback(
265
+ (nextContent: string) => {
266
+ if (nextContent !== content) {
267
+ handleChange(nextContent);
268
+ }
269
+
270
+ void executeCommand(EDITOR_SAVE_COMMAND_ID);
271
+ },
272
+ [content, executeCommand, handleChange],
273
+ );
274
+
275
+ const sourcePane = (
276
+ <div className="workbench-editor-area__source-pane">
277
+ <Suspense
278
+ fallback={
279
+ <div className="workbench-editor-area__editor-loading" role="status">
280
+ Loading editor...
281
+ </div>
282
+ }
283
+ >
284
+ <WorkspaceEditor
285
+ file={editorDocument}
286
+ showFileBar
287
+ showHeader={false}
288
+ theme={theme}
289
+ value={content}
290
+ onChange={handleChange}
291
+ onSave={workspaceHostPort ? handleSave : undefined}
292
+ />
293
+ </Suspense>
294
+ </div>
295
+ );
296
+
297
+ const previewPane = previewProvider ? (
298
+ <ScrollArea
299
+ aria-label="Preview"
300
+ as="section"
301
+ className="workbench-editor-area__preview-pane"
302
+ orientation="vertical"
303
+ >
304
+ {toReactNode(
305
+ previewProvider.render({ document: editorDocument, onContentChange: handleChange }),
306
+ )}
307
+ </ScrollArea>
308
+ ) : null;
309
+ const formPane = formProvider
310
+ ? toReactNode(formProvider.render({ document: editorDocument, onContentChange: handleChange }))
311
+ : null;
312
+ const paneByKind: Record<EditorPaneKind, ReactNode | null> = {
313
+ code: sourcePane,
314
+ form: formPane,
315
+ preview: previewPane,
316
+ };
317
+ const visiblePaneEntries = getVisibleEditorPaneKinds(paneVisibility)
318
+ .map((kind) => ({ kind, node: paneByKind[kind] }))
319
+ .filter((entry): entry is { kind: EditorPaneKind; node: ReactNode } => entry.node !== null);
320
+ const editorBody = composeEditorPaneLayout(visiblePaneEntries, getSplitSize, setSplitSize);
321
+ const modeToolbar =
322
+ modeToolbarVisible && modeToolbarHost
323
+ ? createPortal(
324
+ <EditorViewModeIconSelect
325
+ codeIcon={codeIcon}
326
+ formEligible={formEligible}
327
+ previewEligible={previewEligible}
328
+ value={paneVisibilityToPrimaryViewMode(paneVisibility)}
329
+ onValueChange={(mode) => {
330
+ setPanePreference(editorViewModeToPaneVisibility(mode));
331
+ }}
332
+ />,
333
+ modeToolbarHost,
334
+ )
335
+ : null;
336
+
337
+ return (
338
+ <>
339
+ {modeToolbar}
340
+ <section
341
+ aria-label={host.title ?? resourceUri}
342
+ className="workbench-editor-area__text-editor"
343
+ data-resource-uri={resourceUri}
344
+ >
345
+ <div className="workbench-editor-area__text-editor-body">{editorBody}</div>
346
+ </section>
347
+ </>
348
+ );
349
+ }
350
+
351
+ function composeEditorPaneLayout(
352
+ panes: readonly { kind: EditorPaneKind; node: ReactNode }[],
353
+ getSplitSize: (key: string, defaultPercent: number) => number,
354
+ onSplitSizeChange: (key: string, percent: number) => void,
355
+ ): ReactNode {
356
+ if (panes.length === 0) {
357
+ return null;
358
+ }
359
+
360
+ if (panes.length === 1) {
361
+ return panes[0]?.node ?? null;
362
+ }
363
+
364
+ const layoutSignature = panes.map((pane) => pane.kind).join('|');
365
+
366
+ return panes.slice(1).reduce<ReactNode>((primary, entry, splitIndex) => {
367
+ const primaryKind = panes[0]?.kind;
368
+ const primaryLabel = primaryKind ? editorPaneKindLabel(primaryKind) : 'Editor pane';
369
+ const splitKey = `${layoutSignature}#${splitIndex}`;
370
+
371
+ return (
372
+ <SplitView
373
+ key={splitKey}
374
+ className="workbench-editor-area__split"
375
+ defaultPrimarySizePercent={50}
376
+ minPrimarySizePercent={25}
377
+ primarySizePercent={getSplitSize(splitKey, 50)}
378
+ onPrimarySizePercentChange={(percent) => {
379
+ onSplitSizeChange(splitKey, percent);
380
+ }}
381
+ primary={
382
+ <section aria-label={primaryLabel} className="workbench-editor-area__split-pane">
383
+ {primary}
384
+ </section>
385
+ }
386
+ secondary={
387
+ <section
388
+ aria-label={editorPaneKindLabel(entry.kind)}
389
+ className="workbench-editor-area__split-pane"
390
+ >
391
+ {entry.node}
392
+ </section>
393
+ }
394
+ />
395
+ );
396
+ }, panes[0]?.node ?? null);
397
+ }
398
+
399
+ function editorPaneKindLabel(kind: EditorPaneKind): string {
400
+ switch (kind) {
401
+ case 'code':
402
+ return 'Code';
403
+ case 'form':
404
+ return 'Form';
405
+ case 'preview':
406
+ return 'Preview';
407
+ }
408
+ }
409
+
410
+ function EditorViewModeIconSelect({
411
+ codeIcon,
412
+ formEligible,
413
+ onValueChange,
414
+ previewEligible,
415
+ value,
416
+ }: {
417
+ codeIcon: string;
418
+ formEligible: boolean;
419
+ onValueChange: (mode: EditorViewMode) => void;
420
+ previewEligible: boolean;
421
+ value: EditorViewMode;
422
+ }) {
423
+ const resolvedValue =
424
+ (value === 'form' && !formEligible) || (value === 'preview' && !previewEligible)
425
+ ? 'code'
426
+ : value;
427
+
428
+ return (
429
+ <div
430
+ aria-label="Editor view mode"
431
+ className="workbench-editor-area__view-toolbar"
432
+ data-testid="editor-view-mode-select"
433
+ role="radiogroup"
434
+ >
435
+ <EditorViewModeIconButton
436
+ checked={resolvedValue === 'code'}
437
+ icon={codeIcon}
438
+ label="Code"
439
+ onSelect={() => {
440
+ onValueChange('code');
441
+ }}
442
+ />
443
+ {formEligible ? (
444
+ <EditorViewModeIconButton
445
+ checked={resolvedValue === 'form'}
446
+ icon="codicon-symbol-field"
447
+ label="Form"
448
+ onSelect={() => {
449
+ onValueChange('form');
450
+ }}
451
+ />
452
+ ) : null}
453
+ {previewEligible ? (
454
+ <EditorViewModeIconButton
455
+ checked={resolvedValue === 'preview'}
456
+ icon="codicon-preview"
457
+ label="Preview"
458
+ onSelect={() => {
459
+ onValueChange('preview');
460
+ }}
461
+ />
462
+ ) : null}
463
+ </div>
464
+ );
465
+ }
466
+
467
+ function EditorViewModeIconButton({
468
+ checked,
469
+ icon,
470
+ label,
471
+ onSelect,
472
+ }: {
473
+ checked: boolean;
474
+ icon: string;
475
+ label: string;
476
+ onSelect: () => void;
477
+ }) {
478
+ return (
479
+ <IconButton
480
+ aria-checked={checked}
481
+ className={[
482
+ 'workbench-editor-area__view-button',
483
+ checked && 'workbench-editor-area__view-button--active',
484
+ ]
485
+ .filter(Boolean)
486
+ .join(' ')}
487
+ compact
488
+ icon={icon}
489
+ label={label}
490
+ role="radio"
491
+ onClick={onSelect}
492
+ />
493
+ );
494
+ }
495
+
496
+ function isJsonSourceDocument(document: { mimeType?: string | undefined; path: string }): boolean {
497
+ const mimeType = document.mimeType?.toLowerCase();
498
+ return document.path.toLowerCase().endsWith('.json') || Boolean(mimeType?.includes('json'));
499
+ }
500
+
501
+ interface CommandInspectorEditorRenderPayload {
502
+ readonly commandId: string;
503
+ readonly kind: 'workbench-kit.builtin.commands/inspector';
504
+ readonly resourceUri: string;
505
+ }
506
+
507
+ function isCommandInspectorEditorRenderPayload(
508
+ value: unknown,
509
+ ): value is CommandInspectorEditorRenderPayload {
510
+ if (typeof value !== 'object' || value === null) {
511
+ return false;
512
+ }
513
+
514
+ const candidate = value as Partial<CommandInspectorEditorRenderPayload>;
515
+ return (
516
+ candidate.kind === 'workbench-kit.builtin.commands/inspector' &&
517
+ typeof candidate.commandId === 'string' &&
518
+ typeof candidate.resourceUri === 'string'
519
+ );
520
+ }
521
+
522
+ interface FieldRemapEditorRenderPayload {
523
+ readonly kind: 'workbench-kit.samples.field-remap/editor';
524
+ readonly resourceUri: string;
525
+ readonly surfaceId: string;
526
+ }
527
+
528
+ function isFieldRemapEditorRenderPayload(value: unknown): value is FieldRemapEditorRenderPayload {
529
+ if (typeof value !== 'object' || value === null) {
530
+ return false;
531
+ }
532
+
533
+ const candidate = value as Partial<FieldRemapEditorRenderPayload>;
534
+ return (
535
+ candidate.kind === 'workbench-kit.samples.field-remap/editor' &&
536
+ typeof candidate.resourceUri === 'string' &&
537
+ typeof candidate.surfaceId === 'string' &&
538
+ candidate.surfaceId.length > 0
539
+ );
540
+ }
541
+
542
+ function isTextEditorRenderPayload(value: unknown): value is TextEditorRenderPayload {
543
+ if (typeof value !== 'object' || value === null) {
544
+ return false;
545
+ }
546
+
547
+ const candidate = value as Partial<TextEditorRenderPayload>;
548
+ return (
549
+ candidate.kind === 'workbench-kit.builtin.editor/text' &&
550
+ typeof candidate.resourceUri === 'string' &&
551
+ typeof candidate.initialContent === 'string'
552
+ );
553
+ }
554
+
555
+ interface MissingResourceEditorRenderPayload {
556
+ readonly kind: 'workbench-kit.builtin.editor/missing-resource';
557
+ readonly message: string;
558
+ readonly resourceUri: string;
559
+ }
560
+
561
+ function isMissingResourceEditorRenderPayload(
562
+ value: unknown,
563
+ ): value is MissingResourceEditorRenderPayload {
564
+ if (typeof value !== 'object' || value === null) {
565
+ return false;
566
+ }
567
+
568
+ const candidate = value as Partial<MissingResourceEditorRenderPayload>;
569
+ return (
570
+ candidate.kind === 'workbench-kit.builtin.editor/missing-resource' &&
571
+ typeof candidate.resourceUri === 'string' &&
572
+ typeof candidate.message === 'string'
573
+ );
574
+ }
575
+
576
+ function MissingResourceEditorSurface({
577
+ message,
578
+ resourceUri,
579
+ tabId,
580
+ title,
581
+ }: {
582
+ message: string;
583
+ resourceUri: string;
584
+ tabId: string;
585
+ title?: string | undefined;
586
+ }) {
587
+ const editorService = useEditorService();
588
+ const path = pathForResource(resourceUri);
589
+
590
+ return (
591
+ <section
592
+ aria-label={title ?? path}
593
+ className="workbench-editor-area__missing-resource"
594
+ data-resource-uri={resourceUri}
595
+ >
596
+ <div className="workbench-editor-area__missing-resource-body" role="alert">
597
+ <p className="workbench-editor-area__missing-resource-title">Unable to open editor</p>
598
+ <p className="workbench-editor-area__missing-resource-message">{message}</p>
599
+ <p className="workbench-editor-area__missing-resource-path">{path}</p>
600
+ <button
601
+ className="workbench-editor-area__missing-resource-close"
602
+ type="button"
603
+ onClick={() => {
604
+ editorService.closeEditor(tabId);
605
+ }}
606
+ >
607
+ Close tab
608
+ </button>
609
+ </div>
610
+ </section>
611
+ );
612
+ }
613
+
614
+ function toReactNode(value: unknown): ReactNode {
615
+ if (
616
+ value === undefined ||
617
+ value === null ||
618
+ typeof value === 'boolean' ||
619
+ typeof value === 'symbol' ||
620
+ typeof value === 'function'
621
+ ) {
622
+ return null;
623
+ }
624
+
625
+ if (typeof value === 'string' || typeof value === 'number' || isValidElement(value)) {
626
+ return value;
627
+ }
628
+
629
+ if (Array.isArray(value)) {
630
+ return value as ReactNode;
631
+ }
632
+
633
+ return null;
634
+ }