@checkstack/ui 1.10.0 → 1.12.0

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 (72) hide show
  1. package/.storybook/main.ts +43 -0
  2. package/CHANGELOG.md +565 -0
  3. package/package.json +15 -7
  4. package/scripts/generate-stdlib-types.ts +25 -2
  5. package/src/components/ActionCard.tsx +309 -0
  6. package/src/components/CodeEditor/CodeEditor.tsx +132 -9
  7. package/src/components/CodeEditor/TypefoxEditor.tsx +1024 -0
  8. package/src/components/CodeEditor/bracketKeyGroups.test.ts +120 -0
  9. package/src/components/CodeEditor/bracketKeyGroups.ts +205 -0
  10. package/src/components/CodeEditor/generateTypeDefinitions.ts +4 -4
  11. package/src/components/CodeEditor/generated/builtin-modules.json +1 -0
  12. package/src/components/CodeEditor/importSpecifiers.test.ts +286 -0
  13. package/src/components/CodeEditor/importSpecifiers.ts +267 -0
  14. package/src/components/CodeEditor/index.ts +26 -0
  15. package/src/components/CodeEditor/monacoTsService.ts +217 -0
  16. package/src/components/CodeEditor/popoutTitle.test.ts +37 -0
  17. package/src/components/CodeEditor/popoutTitle.ts +31 -0
  18. package/src/components/CodeEditor/scriptContext.test.ts +41 -0
  19. package/src/components/CodeEditor/scriptContext.ts +76 -1
  20. package/src/components/CodeEditor/scriptDiagnostics.test.ts +135 -0
  21. package/src/components/CodeEditor/scriptDiagnostics.ts +172 -0
  22. package/src/components/CodeEditor/templateValidation.ts +51 -0
  23. package/src/components/CodeEditor/types.ts +168 -0
  24. package/src/components/CodeEditor/validateJsonTemplate.test.ts +61 -0
  25. package/src/components/CodeEditor/validateJsonTemplate.ts +26 -0
  26. package/src/components/CodeEditor/validateScripts.ts +132 -0
  27. package/src/components/CodeEditor/validateXmlTemplate.test.ts +34 -0
  28. package/src/components/CodeEditor/validateXmlTemplate.ts +35 -0
  29. package/src/components/CodeEditor/validateYamlTemplate.test.ts +39 -0
  30. package/src/components/CodeEditor/validateYamlTemplate.ts +28 -0
  31. package/src/components/Dialog.tsx +32 -11
  32. package/src/components/DurationInput.tsx +121 -0
  33. package/src/components/DynamicForm/DynamicForm.tsx +27 -1
  34. package/src/components/DynamicForm/FormField.tsx +138 -10
  35. package/src/components/DynamicForm/KeyValueEditor.tsx +2 -169
  36. package/src/components/DynamicForm/MultiTypeEditorField.tsx +83 -9
  37. package/src/components/DynamicForm/SecretEnvEditor.tsx +315 -0
  38. package/src/components/DynamicForm/index.ts +6 -0
  39. package/src/components/DynamicForm/secretEnv.logic.test.ts +126 -0
  40. package/src/components/DynamicForm/secretEnv.logic.ts +87 -0
  41. package/src/components/DynamicForm/types.ts +83 -1
  42. package/src/components/DynamicForm/utils.ts +32 -0
  43. package/src/components/Popover.tsx +6 -1
  44. package/src/components/ScriptTestPanel.logic.test.ts +139 -0
  45. package/src/components/ScriptTestPanel.logic.ts +137 -0
  46. package/src/components/ScriptTestPanel.tsx +394 -0
  47. package/src/components/Sheet.tsx +21 -6
  48. package/src/components/TemplateInput.tsx +104 -0
  49. package/src/components/TemplateInputToggle.tsx +111 -0
  50. package/src/components/TemplateValueInput.test.ts +98 -0
  51. package/src/components/TemplateValueInput.tsx +470 -0
  52. package/src/components/TimeOfDayInput.tsx +116 -0
  53. package/src/components/VariablePicker.tsx +271 -0
  54. package/src/components/comboboxInteraction.ts +39 -0
  55. package/src/components/portalContainer.ts +24 -0
  56. package/src/hooks/useInitOnceForKey.test.ts +27 -0
  57. package/src/hooks/useInitOnceForKey.ts +21 -18
  58. package/src/index.ts +9 -0
  59. package/stories/ActionCard.stories.tsx +122 -0
  60. package/stories/Alert.stories.tsx +5 -5
  61. package/stories/CodeEditor.stories.tsx +47 -2
  62. package/stories/DurationInput.stories.tsx +59 -0
  63. package/stories/ScriptTestPanel.stories.tsx +106 -0
  64. package/stories/SecretEnvEditor.stories.tsx +80 -0
  65. package/stories/TemplateInputToggle.stories.tsx +77 -0
  66. package/stories/TemplateValueInput.stories.tsx +65 -0
  67. package/stories/TimeOfDayInput.stories.tsx +34 -0
  68. package/stories/VariablePicker.stories.tsx +109 -0
  69. package/tsconfig.json +1 -0
  70. package/src/components/CodeEditor/MonacoEditor.tsx +0 -616
  71. package/src/components/CodeEditor/monacoStdlib.ts +0 -62
  72. package/src/components/CodeEditor/monacoWorkers.ts +0 -118
@@ -0,0 +1,1024 @@
1
+ /// <reference types="vite/client" />
2
+ // Stage 1 foundation for the monaco-editor -> @typefox/monaco-editor-react
3
+ // migration. This is an ISOLATED test vehicle: it renders a single plain
4
+ // TypeScript editor with completions + diagnostics powered by the STANDALONE
5
+ // (classic) TypeScript language features. It is intentionally NOT wired into
6
+ // the existing `CodeEditor` / `MonacoEditor.tsx` and must NOT be re-exported
7
+ // from the package barrel (`src/index.ts`) because the underlying
8
+ // `@codingame/monaco-vscode-*` stack is browser-only and would break any
9
+ // SSR/test path that imports the barrel.
10
+ //
11
+ // API + config shape verified against the INSTALLED package types
12
+ // (@typefox/monaco-editor-react@7.7.0, monaco-languageclient@10.7.0, all
13
+ // @codingame/monaco-vscode-*@25.1.2) and the canonical standalone/classic
14
+ // worker registration in the upstream repo:
15
+ // https://github.com/TypeFox/monaco-languageclient/blob/main/packages/client/test/support/helper-classic.ts
16
+ // https://github.com/TypeFox/monaco-languageclient/blob/main/packages/client/test/worker/workerLoaders.test.ts
17
+
18
+ // Side-effect import registers the classic Monaco language contributions
19
+ // (Monarch grammars for ~80 languages) without pulling in the extension host
20
+ // (no SharedArrayBuffer / COOP / COEP needed).
21
+ import "@codingame/monaco-vscode-standalone-languages";
22
+ // TS/JS language-service setup (compiler options, ambient stdlib types, worker
23
+ // factory) lives in the shared `monacoTsService` module so the headless
24
+ // `validateScripts` validator can reuse the exact same configured singletons.
25
+ import {
26
+ typescriptDefaults,
27
+ javascriptDefaults,
28
+ ensureStandaloneWorkerFactory,
29
+ markVscodeServicesReady,
30
+ } from "./monacoTsService";
31
+ // Named import also triggers the side-effect registration of the REAL VS Code
32
+ // JSON language service (proper highlighting + completion + folding), replacing
33
+ // the hand-rolled `json-template` Monarch grammar. We turn its built-in
34
+ // (raw-text) validation OFF and validate the template-substituted form instead
35
+ // (see validateJsonTemplate), so templates work in any position - including
36
+ // unquoted ones like a numeric `"timeout": {{x}}`.
37
+ import { jsonDefaults } from "@codingame/monaco-vscode-standalone-json-language-features";
38
+ // Default export is `getServiceOverride()`, returning a service-id -> descriptor
39
+ // map. We register ONLY its `ILanguageStatusService` entry (see
40
+ // `languageStatusServiceOverride` below).
41
+ import getLanguagesServiceOverride from "@codingame/monaco-vscode-languages-service-override";
42
+
43
+
44
+ import { type CSSProperties, useEffect, useId, useRef, useState } from "react";
45
+ import * as monaco from "@codingame/monaco-vscode-editor-api";
46
+ import { MonacoEditorReactComp } from "@typefox/monaco-editor-react";
47
+ import { extractBracketKeyGroups } from "./bracketKeyGroups";
48
+ import { validateJsonTemplate } from "./validateJsonTemplate";
49
+ import { validateYamlTemplate } from "./validateYamlTemplate";
50
+ import { validateXmlTemplate } from "./validateXmlTemplate";
51
+ import type { TemplateDiagnostic } from "./templateValidation";
52
+ import { detectAutoClosedBraces, detectOpenTemplate } from "./templateUtils";
53
+ import {
54
+ buildShellEnvVarInsertText,
55
+ matchShellEnvVarTrigger,
56
+ } from "./shellEnvVarMatcher";
57
+ import type {
58
+ AcquireTypes,
59
+ CodeEditorLanguage,
60
+ EditorMarker,
61
+ ShellEnvVar,
62
+ TemplateProperty,
63
+ } from "./types";
64
+ import {
65
+ importSpecifierCompletionContext,
66
+ mergeImportCompletionEntries,
67
+ parseBareImportSpecifiers,
68
+ planAcquisitions,
69
+ } from "./importSpecifiers";
70
+ // Authoritative, build-time-derived list of importable runtime built-in
71
+ // specifiers (`node:fs`, bare `fs`, `bun`, `bun:test`, ...). Generated from the
72
+ // SAME bundled `@types/node` + `bun-types` declarations the editor injects (see
73
+ // scripts/generate-stdlib-types.ts -> extractBuiltinModuleSpecifiers), so the
74
+ // import-name completions never drift from the runtime stdlib. Imported as a
75
+ // plain JSON module (tiny: ~115 names); the bulky type bodies stay in the
76
+ // separately code-split stdlib-types.json.
77
+ import builtinModulesJson from "./generated/builtin-modules.json";
78
+ import {
79
+ type EditorAppConfig,
80
+ type TextContents,
81
+ } from "monaco-languageclient/editorApp";
82
+ import { type MonacoVscodeApiConfig } from "monaco-languageclient/vscodeApiWrapper";
83
+ // ─── Lazy Automatic Type Acquisition (ATA) registry ─────────────────────────
84
+ //
85
+ // The TS/JS language services are singletons, so acquired package types are
86
+ // registered ONCE and shared across every editor instance (a package imported
87
+ // in one script editor is then typed in all of them — harmless, since the
88
+ // declarations are the same install). State is module-scoped:
89
+ //
90
+ // - `acquiredFilePaths`: virtual paths already passed to addExtraLib (dedupe
91
+ // so two editors importing the same package don't double-register a file).
92
+ // - `acquiredSpecifiers`: package names already acquired (skip the fetch).
93
+ // - `acquireResetKey`: the install identity (lockfile hash) the current
94
+ // acquired-set belongs to; when it changes, the set is reset so types
95
+ // refresh against the new install.
96
+ const acquiredFilePaths = new Set<string>();
97
+ const acquiredSpecifiers = new Set<string>();
98
+ let currentAcquireResetKey: string | undefined;
99
+
100
+ /**
101
+ * Reset the acquired-set when the install identity changes. The already-
102
+ * registered extra-libs are left in place (disposing them is unnecessary —
103
+ * the new install re-registers the same virtual paths, and addExtraLib
104
+ * overwrites by path), but the specifier set clears so each package is
105
+ * re-fetched against the new hash.
106
+ */
107
+ const syncAcquireResetKey = (resetKey: string | undefined): void => {
108
+ if (resetKey === currentAcquireResetKey) return;
109
+ currentAcquireResetKey = resetKey;
110
+ acquiredSpecifiers.clear();
111
+ acquiredFilePaths.clear();
112
+ };
113
+
114
+ /**
115
+ * Register one acquired package's declaration files with both the TS and JS
116
+ * services, deduped by virtual path. Paths are `node_modules/...`-relative;
117
+ * we mount each at `file:///<path>` so NodeJs + `@types` resolution finds it.
118
+ */
119
+ const registerAcquiredFiles = (
120
+ files: ReadonlyArray<{ path: string; content: string }>,
121
+ ): void => {
122
+ for (const file of files) {
123
+ const uri = `file:///${file.path}`;
124
+ if (acquiredFilePaths.has(uri)) continue;
125
+ acquiredFilePaths.add(uri);
126
+ for (const defaults of [typescriptDefaults, javascriptDefaults]) {
127
+ defaults.addExtraLib(file.content, uri);
128
+ }
129
+ }
130
+ };
131
+
132
+ /**
133
+ * Acquire types for every NEW bare specifier in `source`, against the given
134
+ * resolver. Pure planning (`parseBareImportSpecifiers` / `planAcquisitions`)
135
+ * is unit-tested; this thin async glue is intentionally untested (no DOM /
136
+ * network in unit tests). A specifier is marked acquired even when it returns
137
+ * no files, so a typeless package isn't re-fetched on every keystroke.
138
+ */
139
+ const runTypeAcquisition = async ({
140
+ source,
141
+ acquireTypes,
142
+ resetKey,
143
+ }: {
144
+ source: string;
145
+ acquireTypes: AcquireTypes;
146
+ resetKey: string | undefined;
147
+ }): Promise<void> => {
148
+ syncAcquireResetKey(resetKey);
149
+ const specifiers = parseBareImportSpecifiers(source);
150
+ const toAcquire = planAcquisitions({
151
+ specifiers,
152
+ acquired: acquiredSpecifiers,
153
+ });
154
+ for (const specifier of toAcquire) {
155
+ // Mark first so concurrent/keystroke re-runs don't double-fetch.
156
+ acquiredSpecifiers.add(specifier);
157
+ try {
158
+ const files = await acquireTypes(specifier);
159
+ registerAcquiredFiles(files);
160
+ } catch {
161
+ // A failed fetch un-marks so a later edit can retry.
162
+ acquiredSpecifiers.delete(specifier);
163
+ }
164
+ }
165
+ };
166
+
167
+ // Turn OFF the JSON service's built-in validation. The editor content is a
168
+ // template that renders to JSON, so we validate the template-substituted form
169
+ // ourselves (see the json validation effect + validateJsonTemplate) to tolerate
170
+ // `{{ }}` in any position. Highlighting + completion from the service stay on.
171
+ jsonDefaults.setDiagnosticsOptions({ validate: false });
172
+
173
+ // Monaco language id per editor language. Matches the ids registered by
174
+ // @codingame/monaco-vscode-standalone-languages (verified: shell is "shell").
175
+ // JSON editors with templates use the custom `json-template` language (below).
176
+ const MONACO_LANGUAGE_ID: Record<CodeEditorLanguage, string> = {
177
+ typescript: "typescript",
178
+ javascript: "javascript",
179
+ json: "json",
180
+ yaml: "yaml",
181
+ xml: "xml",
182
+ markdown: "markdown",
183
+ shell: "shell",
184
+ };
185
+
186
+ // File extension for the model uri, so the language service / grammar keys off
187
+ // a sensible filename.
188
+ const LANGUAGE_FILE_EXT: Record<CodeEditorLanguage, string> = {
189
+ typescript: "ts",
190
+ javascript: "js",
191
+ json: "json",
192
+ yaml: "yaml",
193
+ xml: "xml",
194
+ markdown: "md",
195
+ shell: "sh",
196
+ };
197
+
198
+ const isTsLikeLanguage = (language: CodeEditorLanguage): boolean =>
199
+ language === "typescript" || language === "javascript";
200
+
201
+ // Per-language template-aware validators (markdown has none - no structure to
202
+ // validate). Each validates the template-substituted form so `{{ }}` is
203
+ // tolerated anywhere; see templateValidation.ts.
204
+ const TEMPLATE_VALIDATORS: Partial<
205
+ Record<CodeEditorLanguage, (text: string) => TemplateDiagnostic[]>
206
+ > = {
207
+ json: validateJsonTemplate,
208
+ yaml: validateYamlTemplate,
209
+ xml: validateXmlTemplate,
210
+ };
211
+
212
+ // Variable-like tokens (`{{ template }}` expressions, shell `$env` refs) are
213
+ // highlighted via inline decorations rather than per-language grammars: this
214
+ // works for any language (yaml / xml / markdown have no template grammar; shell
215
+ // doesn't color `$VAR` inside strings) and keeps the color consistent. #9cdcfe
216
+ // is the vs-dark `variable` token color, matching json-template's grammar. The
217
+ // CSS class is injected once.
218
+ const VARIABLE_TOKEN_CLASS = "checkstack-editor-variable";
219
+ let variableTokenStyleInjected = false;
220
+ const ensureVariableTokenStyle = (): void => {
221
+ if (variableTokenStyleInjected) {
222
+ return;
223
+ }
224
+ variableTokenStyleInjected = true;
225
+ const style = document.createElement("style");
226
+ // `!important` so the decoration color always wins over the underlying token
227
+ // color (.mtkN), making `{{ }}` look identical inside and outside strings.
228
+ style.textContent = `.${VARIABLE_TOKEN_CLASS}{color:#9cdcfe !important;}`;
229
+ document.head.append(style);
230
+ };
231
+
232
+ // Apply (and keep in sync) inline decorations for every match of `pattern` in
233
+ // the model. Returns a disposable that removes the decorations + listener.
234
+ // `pattern` must be a global (/g) regex; its lastIndex is reset per pass.
235
+ const installRegexDecorations = ({
236
+ model,
237
+ pattern,
238
+ className,
239
+ }: {
240
+ model: monaco.editor.ITextModel;
241
+ pattern: RegExp;
242
+ className: string;
243
+ }): monaco.IDisposable => {
244
+ ensureVariableTokenStyle();
245
+ const compute = (): monaco.editor.IModelDeltaDecoration[] => {
246
+ const text = model.getValue();
247
+ const decorations: monaco.editor.IModelDeltaDecoration[] = [];
248
+ pattern.lastIndex = 0;
249
+ let match = pattern.exec(text);
250
+ while (match !== null) {
251
+ const start = model.getPositionAt(match.index);
252
+ const end = model.getPositionAt(match.index + match[0].length);
253
+ decorations.push({
254
+ range: new monaco.Range(
255
+ start.lineNumber,
256
+ start.column,
257
+ end.lineNumber,
258
+ end.column,
259
+ ),
260
+ options: { inlineClassName: className },
261
+ });
262
+ // Guard against a zero-length match looping forever.
263
+ if (pattern.lastIndex === match.index) {
264
+ pattern.lastIndex += 1;
265
+ }
266
+ match = pattern.exec(text);
267
+ }
268
+ return decorations;
269
+ };
270
+ let decorationIds = model.deltaDecorations([], compute());
271
+ const subscription = model.onDidChangeContent(() => {
272
+ decorationIds = model.deltaDecorations(decorationIds, compute());
273
+ });
274
+ return {
275
+ dispose: () => {
276
+ subscription.dispose();
277
+ model.deltaDecorations(decorationIds, []);
278
+ },
279
+ };
280
+ };
281
+
282
+ const findModelById = (
283
+ modelId: string,
284
+ ): monaco.editor.ITextModel | undefined =>
285
+ monaco.editor
286
+ .getModels()
287
+ .find((candidate) => candidate.uri.toString().includes(modelId));
288
+
289
+ export type TypefoxEditorProps = {
290
+ /** Stable identity for the underlying editor app + model uri. */
291
+ id: string;
292
+ /** Initial source rendered in the editor. */
293
+ value: string;
294
+ /** Notified with the latest editor text whenever it changes. */
295
+ onChange?: (value: string) => void;
296
+ /** Editor language. Defaults to `typescript`. */
297
+ language?: CodeEditorLanguage;
298
+ /** Minimum editor height in pixels. Defaults to 240. */
299
+ minHeight?: number;
300
+ /**
301
+ * When true, the editor container fills its flex parent (`height: 100%`)
302
+ * instead of using a fixed `minHeight` px height, so it grows to fit a tall
303
+ * flex column (e.g. the popout dialog body). `minHeight` is still applied as
304
+ * a floor. Defaults to false, preserving the inline fixed-height behaviour.
305
+ */
306
+ fillHeight?: boolean;
307
+ /**
308
+ * Generated ambient type definitions (the `context.d.ts`) injected as a TS
309
+ * extra-lib so `context.*` resolves with real fields. Wired up once per
310
+ * editor at mount, keyed by a unique path - no addExtraLib race.
311
+ * Only used for `typescript` / `javascript` editors.
312
+ */
313
+ typeDefinitions?: string;
314
+ /**
315
+ * Template properties for non-script editors. When provided, typing `{{`
316
+ * autocompletes the available `{{ path }}` references. Only used for
317
+ * markup/text editors (json / yaml / xml / markdown).
318
+ */
319
+ templateProperties?: TemplateProperty[];
320
+ /**
321
+ * Environment-variable hints for `shell` editors. When provided, typing `$`
322
+ * or `${` autocompletes the variable names. Only used when `language` is
323
+ * `shell`.
324
+ */
325
+ shellEnvVars?: ShellEnvVar[];
326
+ /**
327
+ * Externally-computed diagnostics rendered as inline squiggles under a
328
+ * dedicated marker owner (so they coexist with monaco's own markers).
329
+ * Positions are 1-based (monaco convention) - e.g. YAML definition validation.
330
+ */
331
+ markers?: EditorMarker[];
332
+ /** Render the editor read-only. */
333
+ readOnly?: boolean;
334
+ /** Accessible label / hint for the editor (surfaced via aria-label). */
335
+ placeholder?: string;
336
+ /**
337
+ * Lazy Automatic Type Acquisition resolver. When provided (TS/JS editors),
338
+ * bare `import`/`require` specifiers in the buffer are parsed (debounced)
339
+ * and each NEW package's `.d.ts` closure is fetched + registered so e.g.
340
+ * `import { debounce } from "lodash"` autocompletes. Injected by the
341
+ * consumer so this component stays plugin-agnostic.
342
+ */
343
+ acquireTypes?: AcquireTypes;
344
+ /**
345
+ * Install identity (lockfile hash). When it changes, the shared acquired-set
346
+ * resets so types refresh against the new install.
347
+ */
348
+ acquireResetKey?: string;
349
+ /**
350
+ * Importable installed package NAMES (TS/JS editors). When provided, the
351
+ * editor suggests these as completions while the cursor is inside an import
352
+ * specifier string (`import {} from "lod"` -> `lodash`) - solving the
353
+ * lazy-ATA catch-22 where no module is registered yet. Must already exclude
354
+ * `@types/*` companions (you import `lodash`, never `@types/lodash`).
355
+ * Injected by the consumer so this component stays plugin-agnostic.
356
+ */
357
+ importablePackages?: string[];
358
+ };
359
+
360
+ /**
361
+ * Isolated editor used to validate the Typefox/monaco-vscode stack in the
362
+ * browser. Dark theme, no minimap, automatic layout, word-based suggestions
363
+ * disabled. For `typescript`/`javascript` it injects the `context` types +
364
+ * bracket completions; for markup/text languages it offers `{{ }}` template
365
+ * completions.
366
+ */
367
+ // The standalone ("classic") service set omits `ILanguageStatusService`, but
368
+ // the JSON language features register a language-status indicator (the active
369
+ // formatter) on editor focus and throw "LanguageStatusService.addStatus is not
370
+ // supported" without it. We register ONLY that service: the full languages
371
+ // override would also swap `ILanguageService` for the workbench impl and pull
372
+ // in the files-service override, both of which conflict with the standalone
373
+ // language setup. The override map is keyed by the service-decorator id
374
+ // (`createDecorator('ILanguageStatusService')`), so we pick that one entry.
375
+ const LANGUAGE_STATUS_SERVICE_ID = "ILanguageStatusService";
376
+ const languageStatusServiceOverride: monaco.editor.IEditorOverrideServices =
377
+ (() => {
378
+ const all = getLanguagesServiceOverride();
379
+ return LANGUAGE_STATUS_SERVICE_ID in all
380
+ ? { [LANGUAGE_STATUS_SERVICE_ID]: all[LANGUAGE_STATUS_SERVICE_ID] }
381
+ : {};
382
+ })();
383
+
384
+ // Always-available runtime built-in import specifiers (Node + Bun), derived at
385
+ // build time from the bundled stdlib types. These are importable in the script
386
+ // sandbox regardless of the installed-package allowlist (the sandbox is a Bun
387
+ // subprocess; Bun provides Node's builtins + its own `bun:` modules), and their
388
+ // types are already loaded ambiently via the stdlib bundle - so completing one
389
+ // needs no lazy acquisition. The JSON is a plain `string[]`.
390
+ const BUILTIN_MODULE_SPECIFIERS: readonly string[] = builtinModulesJson;
391
+
392
+ export const TypefoxEditor = ({
393
+ id,
394
+ value,
395
+ onChange,
396
+ language = "typescript",
397
+ minHeight = 240,
398
+ fillHeight = false,
399
+ typeDefinitions,
400
+ templateProperties,
401
+ shellEnvVars,
402
+ markers,
403
+ readOnly = false,
404
+ placeholder,
405
+ acquireTypes,
406
+ acquireResetKey,
407
+ importablePackages,
408
+ }: TypefoxEditorProps) => {
409
+ // `MonacoEditorReactComp` captures `onTextChanged` once at editor-start, so
410
+ // the handler it calls would otherwise close over a stale `onChange` (bound
411
+ // to the value/sibling-config at mount time). Routing through a ref that we
412
+ // keep current on every render means content changes always invoke the
413
+ // latest `onChange` — without this, editing one DynamicForm field reverts
414
+ // sibling fields (e.g. a shell action's `env`) to their mount-time values.
415
+ const onChangeRef = useRef(onChange);
416
+ onChangeRef.current = onChange;
417
+
418
+ // Unique-per-instance id so multiple editors never share a model or clobber
419
+ // each other's extra-lib.
420
+ const reactId = useId();
421
+ const modelId = `${id}-${reactId.replaceAll(":", "")}`;
422
+ const modelUri = `/workspace/${modelId}.${LANGUAGE_FILE_EXT[language]}`;
423
+
424
+ const isTsLike = isTsLikeLanguage(language);
425
+ const hasTemplates =
426
+ templateProperties !== undefined && templateProperties.length > 0;
427
+ const languageId = MONACO_LANGUAGE_ID[language];
428
+
429
+ // Set once the wrapper has initialised the VS Code services, so the
430
+ // completion providers below register against a ready languages registry.
431
+ const [apiReady, setApiReady] = useState(false);
432
+
433
+ useEffect(() => {
434
+ if (!isTsLike || typeDefinitions === undefined) {
435
+ return;
436
+ }
437
+ // Inject this editor's ambient `context` types. addExtraLib keys by path
438
+ // and re-syncs the worker on change, so the types are reliably picked up
439
+ // (no race with model load). NOTE: extra-libs are ambient/global to the TS
440
+ // service, so two TS editors with *different* `context` shapes mounted at
441
+ // once would collide on the `context` identifier - per-editor isolation is
442
+ // a Stage 6 concern, irrelevant to the single-editor test vehicle here.
443
+ const lib = typescriptDefaults.addExtraLib(
444
+ typeDefinitions,
445
+ `file:///context-${modelId}.d.ts`,
446
+ );
447
+ return () => {
448
+ lib.dispose();
449
+ };
450
+ }, [isTsLike, typeDefinitions, modelId]);
451
+
452
+ // Lazy Automatic Type Acquisition (ATA). For TS/JS editors with an injected
453
+ // `acquireTypes` resolver, parse the buffer's bare import/require specifiers
454
+ // (debounced) and fetch + register each NEW package's `.d.ts` closure, so
455
+ // `import { x } from "pkg"` autocompletes. The acquired-set is module-scoped
456
+ // and shared across editors (the declarations are install-global); the pure
457
+ // parse/plan steps are unit-tested in importSpecifiers.test.ts. Re-running
458
+ // on a new `acquireTypes`/`acquireResetKey` identity is cheap and safe — the
459
+ // module-scoped acquired-set dedupes, so it never double-fetches.
460
+ useEffect(() => {
461
+ if (!apiReady || !isTsLike || acquireTypes === undefined) {
462
+ return;
463
+ }
464
+ const model = findModelById(modelId);
465
+ if (!model) {
466
+ return;
467
+ }
468
+
469
+ const acquire = (source: string): void => {
470
+ void runTypeAcquisition({
471
+ source,
472
+ acquireTypes,
473
+ resetKey: acquireResetKey,
474
+ });
475
+ };
476
+
477
+ // Run once for the initial content so existing imports resolve on open.
478
+ acquire(model.getValue());
479
+
480
+ let timer: ReturnType<typeof setTimeout> | undefined;
481
+ const subscription = model.onDidChangeContent(() => {
482
+ if (timer !== undefined) clearTimeout(timer);
483
+ timer = setTimeout(() => {
484
+ acquire(model.getValue());
485
+ }, 400);
486
+ });
487
+ return () => {
488
+ if (timer !== undefined) clearTimeout(timer);
489
+ subscription.dispose();
490
+ };
491
+ }, [apiReady, isTsLike, acquireTypes, acquireResetKey, modelId]);
492
+
493
+ // Controlled-value sync. `value` is only seeded into the model at mount
494
+ // (codeResources), so the editor is otherwise uncontrolled. Apply external
495
+ // `value` changes — a sibling editor's edits (the inline ↔ popout pair share
496
+ // one controlled `value`), a YAML→Visual reset, a loaded definition — to this
497
+ // model. Guarded by an equality check: the user's own edit round-trips
498
+ // `value === model.getValue()`, so the actively-edited editor is a no-op and
499
+ // there is no feedback loop; only a background editor whose `value` prop
500
+ // diverged gets updated.
501
+ useEffect(() => {
502
+ if (!apiReady) return;
503
+ const model = findModelById(modelId);
504
+ if (!model || model.getValue() === value) return;
505
+ model.setValue(value);
506
+ }, [apiReady, modelId, value]);
507
+
508
+ // Import-specifier name completions. Lazy ATA only registers a package's
509
+ // types AFTER its name is in the buffer, so while the user is still TYPING
510
+ // the specifier (`import {} from "lod"`) no module exists yet and the TS
511
+ // worker offers nothing. This provider fills that gap: when the cursor is
512
+ // inside an import/require string (detected by the unit-tested
513
+ // `importSpecifierCompletionContext`), it suggests:
514
+ // - the always-available runtime built-ins (`node:fs`, `bun`, ...), so
515
+ // they appear even with an empty allowlist; AND
516
+ // - the injected installed-package names (already `@types/*`-free).
517
+ // Selecting a built-in inserts an already-typed module; selecting an
518
+ // installed package triggers the ATA loop to load its closure. The list is
519
+ // merged + deduped + sorted by `mergeImportCompletionEntries` (a unit-tested
520
+ // pure helper). Built-ins read as "Node.js" / "Bun built-in" via `detail`.
521
+ // Scoped to THIS model; only the import-string position triggers it, so it
522
+ // never pollutes normal completions. Always registered (built-ins are
523
+ // always available), independent of the allowlist.
524
+ useEffect(() => {
525
+ if (!apiReady || !isTsLike) {
526
+ return;
527
+ }
528
+ const entries = mergeImportCompletionEntries({
529
+ builtins: BUILTIN_MODULE_SPECIFIERS,
530
+ installedPackages: importablePackages ?? [],
531
+ });
532
+
533
+ const provideCompletionItems = (
534
+ model: monaco.editor.ITextModel,
535
+ position: monaco.Position,
536
+ ): monaco.languages.CompletionList => {
537
+ if (!model.uri.toString().includes(modelId)) {
538
+ return { suggestions: [] };
539
+ }
540
+ const lineUpToCursor = model
541
+ .getLineContent(position.lineNumber)
542
+ .slice(0, position.column - 1);
543
+ const ctx = importSpecifierCompletionContext(lineUpToCursor);
544
+ if (!ctx) {
545
+ return { suggestions: [] };
546
+ }
547
+ // Replace the whole partial specifier (between the quotes) without
548
+ // touching the quotes themselves.
549
+ const range = {
550
+ startLineNumber: position.lineNumber,
551
+ startColumn: ctx.replaceFromColumn,
552
+ endLineNumber: position.lineNumber,
553
+ endColumn: position.column,
554
+ };
555
+ return {
556
+ suggestions: entries.map((entry) => ({
557
+ label: entry.name,
558
+ kind: monaco.languages.CompletionItemKind.Module,
559
+ detail: entry.detail,
560
+ insertText: entry.name,
561
+ filterText: entry.name,
562
+ range,
563
+ })),
564
+ // The list is the full known set; let monaco filter by `partial`.
565
+ incomplete: false,
566
+ };
567
+ };
568
+
569
+ const disposables = (["typescript", "javascript"] as const).map((lang) =>
570
+ monaco.languages.registerCompletionItemProvider(lang, {
571
+ // Opening quotes start a specifier; `:` advances into a `node:`/`bun:`
572
+ // builtin; `/` advances into a scoped name or subpath.
573
+ triggerCharacters: ['"', "'", "/", ":"],
574
+ provideCompletionItems,
575
+ }),
576
+ );
577
+ return () => {
578
+ for (const disposable of disposables) {
579
+ disposable.dispose();
580
+ }
581
+ };
582
+ }, [apiReady, isTsLike, importablePackages, modelId]);
583
+
584
+ // Type-driven bracket-notation completions. The standalone TS worker omits
585
+ // object members whose keys aren't valid identifiers (artifact ids like
586
+ // `integration-jira.issue`), and the built-in SuggestAdapter can't be
587
+ // overridden to insert them, so we register our own provider: typing
588
+ // `<objectExpression>.` lists the keys and accepting one rewrites the dot to
589
+ // `["key"]` (mirrors VS Code's `obj."a-b"` -> `obj["a-b"]`). The groups are
590
+ // derived from the injected `context.d.ts` itself, so no separate prop is
591
+ // threaded. Scoped to THIS editor's model so multiple editors don't cross-feed.
592
+ useEffect(() => {
593
+ if (!apiReady || !isTsLike || typeDefinitions === undefined) {
594
+ return;
595
+ }
596
+ const groups = extractBracketKeyGroups({ typeDefinitions });
597
+ if (groups.length === 0) {
598
+ return;
599
+ }
600
+
601
+ const escapeRegExp = (input: string): string =>
602
+ input.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
603
+
604
+ const provideCompletionItems = (
605
+ model: monaco.editor.ITextModel,
606
+ position: monaco.Position,
607
+ ): monaco.languages.CompletionList => {
608
+ if (!model.uri.toString().includes(modelId)) {
609
+ return { suggestions: [] };
610
+ }
611
+ const textBefore = model
612
+ .getLineContent(position.lineNumber)
613
+ .slice(0, position.column - 1);
614
+
615
+ for (const { objectExpression, keys } of groups) {
616
+ // Match `<objectExpression>.<query>` at the cursor, ensuring the
617
+ // expression isn't the tail of a longer identifier.
618
+ const match = new RegExp(
619
+ String.raw`(?:^|[^\w$.])${escapeRegExp(objectExpression)}\.([\w$]*)$`,
620
+ ).exec(textBefore);
621
+ if (!match) {
622
+ continue;
623
+ }
624
+ const query = match[1] ?? "";
625
+ const queryStartColumn = position.column - query.length;
626
+ const dotColumn = queryStartColumn - 1;
627
+
628
+ return {
629
+ suggestions: keys.map((key) => ({
630
+ label: `["${key}"]`,
631
+ kind: monaco.languages.CompletionItemKind.Property,
632
+ detail: objectExpression,
633
+ insertText: `["${key}"]`,
634
+ filterText: key,
635
+ range: {
636
+ startLineNumber: position.lineNumber,
637
+ startColumn: queryStartColumn,
638
+ endLineNumber: position.lineNumber,
639
+ endColumn: position.column,
640
+ },
641
+ // Delete the triggering `.` so `obj.` becomes `obj["key"]`.
642
+ additionalTextEdits: [
643
+ {
644
+ range: {
645
+ startLineNumber: position.lineNumber,
646
+ startColumn: dotColumn,
647
+ endLineNumber: position.lineNumber,
648
+ endColumn: dotColumn + 1,
649
+ },
650
+ text: "",
651
+ },
652
+ ],
653
+ })),
654
+ };
655
+ }
656
+ return { suggestions: [] };
657
+ };
658
+
659
+ const disposables = (["typescript", "javascript"] as const).map((lang) =>
660
+ monaco.languages.registerCompletionItemProvider(lang, {
661
+ triggerCharacters: ["."],
662
+ provideCompletionItems,
663
+ }),
664
+ );
665
+ return () => {
666
+ for (const disposable of disposables) {
667
+ disposable.dispose();
668
+ }
669
+ };
670
+ }, [apiReady, isTsLike, typeDefinitions, modelId]);
671
+
672
+ // Template `{{ }}` completion for markup/text editors (json / yaml / xml /
673
+ // markdown). Typing `{{` lists the available `{{ path }}` references; ported
674
+ // from the legacy MonacoEditor template provider (uses the same tested
675
+ // `detectOpenTemplate` / `detectAutoClosedBraces` helpers). Registered for
676
+ // THIS editor's resolved language id and scoped to its model.
677
+ useEffect(() => {
678
+ if (!apiReady || isTsLike || !hasTemplates) {
679
+ return;
680
+ }
681
+ const properties = templateProperties ?? [];
682
+
683
+ const provideCompletionItems = (
684
+ model: monaco.editor.ITextModel,
685
+ position: monaco.Position,
686
+ ): monaco.languages.CompletionList => {
687
+ if (!model.uri.toString().includes(modelId)) {
688
+ return { suggestions: [] };
689
+ }
690
+ const content = model.getValue();
691
+ const cursorOffset = model.getOffsetAt(position);
692
+
693
+ const openTemplate = detectOpenTemplate({ content, cursorOffset });
694
+ if (!openTemplate.isInTemplate) {
695
+ return { suggestions: [] };
696
+ }
697
+
698
+ const query = openTemplate.query.toLowerCase();
699
+ const startColumn = openTemplate.startColumn;
700
+ // Monaco may have auto-closed with `}}` after the cursor; extend the
701
+ // replaced range over it so we don't leave dangling braces.
702
+ const endColumn =
703
+ position.column + detectAutoClosedBraces({ content, cursorOffset });
704
+
705
+ const suggestions = properties
706
+ .filter(
707
+ (prop) => query === "" || prop.path.toLowerCase().includes(query),
708
+ )
709
+ .map((prop, index) => ({
710
+ label: `{{${prop.path}}}`,
711
+ kind: monaco.languages.CompletionItemKind.Variable,
712
+ detail: prop.type,
713
+ documentation: prop.description,
714
+ insertText: `{{${prop.path}}}`,
715
+ // Leading space sorts these above the editor's own suggestions.
716
+ sortText: ` ${String(index).padStart(4, "0")}`,
717
+ filterText: `{{${query}${prop.path}`,
718
+ preselect: index === 0,
719
+ range: {
720
+ startLineNumber: position.lineNumber,
721
+ startColumn,
722
+ endLineNumber: position.lineNumber,
723
+ endColumn,
724
+ },
725
+ }));
726
+
727
+ return { suggestions, incomplete: false };
728
+ };
729
+
730
+ const provider = monaco.languages.registerCompletionItemProvider(
731
+ languageId,
732
+ { triggerCharacters: ["{"], provideCompletionItems },
733
+ );
734
+ return () => {
735
+ provider.dispose();
736
+ };
737
+ }, [
738
+ apiReady,
739
+ isTsLike,
740
+ hasTemplates,
741
+ templateProperties,
742
+ languageId,
743
+ modelId,
744
+ ]);
745
+
746
+ // Highlight `{{ ... }}` template expressions (template editors). The
747
+ // `[^{}]*` body (not `[^}]*`) stops an unclosed `{{` from swallowing text up
748
+ // to a later `}}`.
749
+ useEffect(() => {
750
+ if (!apiReady || isTsLike || !hasTemplates) {
751
+ return;
752
+ }
753
+ const model = findModelById(modelId);
754
+ if (!model) {
755
+ return;
756
+ }
757
+ const handle = installRegexDecorations({
758
+ model,
759
+ pattern: /\{\{[^{}]*\}\}/g,
760
+ className: VARIABLE_TOKEN_CLASS,
761
+ });
762
+ return () => {
763
+ handle.dispose();
764
+ };
765
+ }, [apiReady, isTsLike, hasTemplates, modelId]);
766
+
767
+ // Highlight shell variable references (`$NAME` / `${NAME}`) - the shell
768
+ // grammar doesn't color these inside double-quoted strings, where ours live.
769
+ useEffect(() => {
770
+ if (!apiReady || language !== "shell") {
771
+ return;
772
+ }
773
+ const model = findModelById(modelId);
774
+ if (!model) {
775
+ return;
776
+ }
777
+ const handle = installRegexDecorations({
778
+ model,
779
+ pattern: /\$\{[A-Za-z_]\w*\}|\$[A-Za-z_]\w*/g,
780
+ className: VARIABLE_TOKEN_CLASS,
781
+ });
782
+ return () => {
783
+ handle.dispose();
784
+ };
785
+ }, [apiReady, language, modelId]);
786
+
787
+ // Shell `$env` completion. For `shell` editors, typing `$` or `${` suggests
788
+ // the provided variable names (and brace-closes `${name}` correctly). Ported
789
+ // from the legacy MonacoEditor shell provider; uses the tested
790
+ // `matchShellEnvVarTrigger` / `buildShellEnvVarInsertText` helpers.
791
+ useEffect(() => {
792
+ if (!apiReady || language !== "shell") {
793
+ return;
794
+ }
795
+ if (shellEnvVars === undefined || shellEnvVars.length === 0) {
796
+ return;
797
+ }
798
+ const envVars = shellEnvVars;
799
+
800
+ const provideCompletionItems = (
801
+ model: monaco.editor.ITextModel,
802
+ position: monaco.Position,
803
+ ): monaco.languages.CompletionList => {
804
+ if (!model.uri.toString().includes(modelId)) {
805
+ return { suggestions: [] };
806
+ }
807
+ const lineText = model.getLineContent(position.lineNumber);
808
+ const textBefore = lineText.slice(0, position.column - 1);
809
+ const match = matchShellEnvVarTrigger(textBefore);
810
+ if (!match) {
811
+ return { suggestions: [] };
812
+ }
813
+ const startColumn = position.column - match.prefixLength;
814
+ // `{` auto-closes to `}` in shell, so a braced `${` leaves a `}` right
815
+ // after the cursor. Extend the replace range over it so an accepted
816
+ // `${NAME}` doesn't leave a stray brace (`${NAME}}`).
817
+ const hasAutoClosedBrace =
818
+ match.form === "braced" && lineText[position.column - 1] === "}";
819
+ const endColumn = hasAutoClosedBrace
820
+ ? position.column + 1
821
+ : position.column;
822
+ // filterText must match the text already in the replace range (`${` for
823
+ // braced, `$` for bare) or monaco fuzzy-filters every item out.
824
+ const filterPrefix = match.form === "braced" ? "${" : "$";
825
+
826
+ const suggestions = envVars
827
+ .filter(
828
+ (v) => match.query === "" || v.name.toUpperCase().includes(match.query),
829
+ )
830
+ .map((v, index) => ({
831
+ label: `$${v.name}`,
832
+ kind: monaco.languages.CompletionItemKind.Variable,
833
+ detail: v.example ? `e.g. ${v.example}` : "shell env var",
834
+ // Full name in the (wrapping) docs panel so long CHECKSTACK_* names
835
+ // stay legible even when the suggest-list label truncates.
836
+ documentation: {
837
+ value: [`\`$${v.name}\``, v.description].filter(Boolean).join("\n\n"),
838
+ },
839
+ insertText: buildShellEnvVarInsertText(match, v.name),
840
+ sortText: ` ${String(index).padStart(4, "0")}`,
841
+ filterText: `${filterPrefix}${v.name}`,
842
+ range: {
843
+ startLineNumber: position.lineNumber,
844
+ startColumn,
845
+ endLineNumber: position.lineNumber,
846
+ endColumn,
847
+ },
848
+ }));
849
+
850
+ return { suggestions, incomplete: false };
851
+ };
852
+
853
+ const provider = monaco.languages.registerCompletionItemProvider("shell", {
854
+ triggerCharacters: ["$", "{"],
855
+ provideCompletionItems,
856
+ });
857
+ return () => {
858
+ provider.dispose();
859
+ };
860
+ }, [apiReady, language, shellEnvVars, modelId]);
861
+
862
+ // External validation markers (inline squiggles). Applied under a dedicated
863
+ // owner so they coexist with monaco's own language markers. Ported from the
864
+ // legacy editor; used e.g. for YAML definition validation in AutomationEditPage.
865
+ useEffect(() => {
866
+ if (!apiReady) {
867
+ return;
868
+ }
869
+ const model = monaco.editor
870
+ .getModels()
871
+ .find((candidate) => candidate.uri.toString().includes(modelId));
872
+ if (!model) {
873
+ return;
874
+ }
875
+ const toSeverity = (
876
+ severity: EditorMarker["severity"],
877
+ ): monaco.MarkerSeverity => {
878
+ if (severity === "warning") {
879
+ return monaco.MarkerSeverity.Warning;
880
+ }
881
+ if (severity === "info") {
882
+ return monaco.MarkerSeverity.Info;
883
+ }
884
+ return monaco.MarkerSeverity.Error;
885
+ };
886
+ monaco.editor.setModelMarkers(
887
+ model,
888
+ "external-validation",
889
+ (markers ?? []).map((marker) => ({
890
+ startLineNumber: marker.startLineNumber,
891
+ startColumn: marker.startColumn,
892
+ endLineNumber: marker.endLineNumber,
893
+ endColumn: marker.endColumn,
894
+ message: marker.message,
895
+ severity: toSeverity(marker.severity),
896
+ })),
897
+ );
898
+ return () => {
899
+ monaco.editor.setModelMarkers(model, "external-validation", []);
900
+ };
901
+ }, [apiReady, markers, modelId]);
902
+
903
+ // Template-aware validation for markup languages (json / yaml / xml). The
904
+ // language services' own validation is off (json) or absent (yaml/xml);
905
+ // instead we validate the template-substituted form so `{{ }}` is allowed in
906
+ // any position while real structural errors are still flagged. Recomputed on
907
+ // every edit. Under a dedicated owner so it coexists with the external
908
+ // `markers`.
909
+ useEffect(() => {
910
+ const validate = TEMPLATE_VALIDATORS[language];
911
+ if (!apiReady || !validate) {
912
+ return;
913
+ }
914
+ const model = findModelById(modelId);
915
+ if (!model) {
916
+ return;
917
+ }
918
+ const owner = "template-validation";
919
+ const runValidation = (): void => {
920
+ const diagnostics = validate(model.getValue());
921
+ monaco.editor.setModelMarkers(
922
+ model,
923
+ owner,
924
+ diagnostics.map((diagnostic) => {
925
+ const start = model.getPositionAt(diagnostic.offset);
926
+ const end = model.getPositionAt(
927
+ diagnostic.offset + diagnostic.length,
928
+ );
929
+ return {
930
+ startLineNumber: start.lineNumber,
931
+ startColumn: start.column,
932
+ endLineNumber: end.lineNumber,
933
+ endColumn: end.column,
934
+ message: diagnostic.message,
935
+ severity: monaco.MarkerSeverity.Error,
936
+ };
937
+ }),
938
+ );
939
+ };
940
+ runValidation();
941
+ const subscription = model.onDidChangeContent(() => {
942
+ runValidation();
943
+ });
944
+ return () => {
945
+ subscription.dispose();
946
+ monaco.editor.setModelMarkers(model, owner, []);
947
+ };
948
+ }, [apiReady, language, modelId]);
949
+
950
+ const vscodeApiConfig: MonacoVscodeApiConfig = {
951
+ // 'classic' is the standalone axis (no extension host); 'extended' is the
952
+ // extension-host axis we deliberately avoid in this migration.
953
+ $type: "classic",
954
+ // Register the missing ILanguageStatusService (see the override comment
955
+ // above) so focusing a JSON editor doesn't throw "addStatus is not
956
+ // supported".
957
+ serviceOverrides: { ...languageStatusServiceOverride },
958
+ viewsConfig: {
959
+ // Plain editor, no workbench views.
960
+ $type: "EditorService",
961
+ },
962
+ monacoWorkerFactory: ensureStandaloneWorkerFactory,
963
+ };
964
+
965
+ const editorAppConfig: EditorAppConfig = {
966
+ // Unique per instance (modelId includes a useId suffix) so multiple editors
967
+ // sharing the same `id` prop don't collide in the wrapper's app registry.
968
+ id: modelId,
969
+ codeResources: {
970
+ modified: {
971
+ text: value,
972
+ uri: modelUri,
973
+ enforceLanguageId: languageId,
974
+ },
975
+ },
976
+ editorOptions: {
977
+ // 'vs-dark' is the builtin classic dark theme. (The VS Code
978
+ // 'Default Dark Modern' theme would require the extension-host
979
+ // theme-defaults extension, which the standalone setup omits.)
980
+ theme: "vs-dark",
981
+ minimap: { enabled: false },
982
+ automaticLayout: true,
983
+ // Force completions to come from the TS language service rather than
984
+ // naive word matching.
985
+ wordBasedSuggestions: "off",
986
+ scrollBeyondLastLine: false,
987
+ readOnly,
988
+ ariaLabel: placeholder ?? "Code editor",
989
+ },
990
+ };
991
+
992
+ const handleTextChanged = (textChanges: TextContents): void => {
993
+ onChangeRef.current?.(textChanges.modified ?? "");
994
+ };
995
+
996
+ // In `fillHeight` mode the container takes its parent's height so Monaco's
997
+ // `automaticLayout` resizes to fill a tall flex column (the popout body);
998
+ // `minHeight` stays as a floor. Otherwise the inline fixed-px behaviour is
999
+ // preserved exactly.
1000
+ const containerStyle: CSSProperties = fillHeight
1001
+ ? { minHeight: `${minHeight}px`, height: "100%" }
1002
+ : { minHeight: `${minHeight}px`, height: `${minHeight}px` };
1003
+
1004
+ return (
1005
+ <MonacoEditorReactComp
1006
+ style={containerStyle}
1007
+ vscodeApiConfig={vscodeApiConfig}
1008
+ editorAppConfig={editorAppConfig}
1009
+ onTextChanged={handleTextChanged}
1010
+ onEditorStartDone={() => {
1011
+ // Per-editor ready signal for the completion providers. We use this
1012
+ // (not onVscodeApiInitDone) because the vscode API initialises globally
1013
+ // once, so a second editor never gets its own onVscodeApiInitDone - but
1014
+ // onEditorStartDone fires for each editor instance.
1015
+ setApiReady(true);
1016
+ // The monaco-vscode services are now up. Let the headless script
1017
+ // validator know it may safely use the worker (it must never init the
1018
+ // services itself - that would collide with this wrapper's one-time
1019
+ // init and throw "Services are already initialized").
1020
+ markVscodeServicesReady();
1021
+ }}
1022
+ />
1023
+ );
1024
+ };