@checkstack/ui 1.11.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 (45) hide show
  1. package/.storybook/main.ts +43 -0
  2. package/CHANGELOG.md +181 -0
  3. package/package.json +4 -4
  4. package/scripts/generate-stdlib-types.ts +23 -0
  5. package/src/components/ActionCard.tsx +96 -8
  6. package/src/components/CodeEditor/CodeEditor.tsx +95 -14
  7. package/src/components/CodeEditor/TypefoxEditor.tsx +279 -123
  8. package/src/components/CodeEditor/generated/builtin-modules.json +1 -0
  9. package/src/components/CodeEditor/importSpecifiers.test.ts +286 -0
  10. package/src/components/CodeEditor/importSpecifiers.ts +267 -0
  11. package/src/components/CodeEditor/index.ts +24 -0
  12. package/src/components/CodeEditor/monacoTsService.ts +217 -0
  13. package/src/components/CodeEditor/popoutTitle.test.ts +37 -0
  14. package/src/components/CodeEditor/popoutTitle.ts +31 -0
  15. package/src/components/CodeEditor/scriptDiagnostics.test.ts +135 -0
  16. package/src/components/CodeEditor/scriptDiagnostics.ts +172 -0
  17. package/src/components/CodeEditor/types.ts +59 -0
  18. package/src/components/CodeEditor/validateScripts.ts +132 -0
  19. package/src/components/Dialog.tsx +32 -11
  20. package/src/components/DurationInput.tsx +121 -0
  21. package/src/components/DynamicForm/DynamicForm.tsx +25 -1
  22. package/src/components/DynamicForm/FormField.tsx +109 -1
  23. package/src/components/DynamicForm/MultiTypeEditorField.tsx +67 -2
  24. package/src/components/DynamicForm/SecretEnvEditor.tsx +315 -0
  25. package/src/components/DynamicForm/index.ts +6 -0
  26. package/src/components/DynamicForm/secretEnv.logic.test.ts +126 -0
  27. package/src/components/DynamicForm/secretEnv.logic.ts +87 -0
  28. package/src/components/DynamicForm/types.ts +72 -1
  29. package/src/components/DynamicForm/utils.ts +32 -0
  30. package/src/components/Popover.tsx +6 -1
  31. package/src/components/ScriptTestPanel.logic.test.ts +139 -0
  32. package/src/components/ScriptTestPanel.logic.ts +137 -0
  33. package/src/components/ScriptTestPanel.tsx +394 -0
  34. package/src/components/Sheet.tsx +21 -6
  35. package/src/components/TimeOfDayInput.tsx +116 -0
  36. package/src/components/comboboxInteraction.ts +39 -0
  37. package/src/components/portalContainer.ts +24 -0
  38. package/src/index.ts +4 -0
  39. package/stories/ActionCard.stories.tsx +60 -0
  40. package/stories/CodeEditor.stories.tsx +47 -2
  41. package/stories/DurationInput.stories.tsx +59 -0
  42. package/stories/ScriptTestPanel.stories.tsx +106 -0
  43. package/stories/SecretEnvEditor.stories.tsx +80 -0
  44. package/stories/TimeOfDayInput.stories.tsx +34 -0
  45. package/tsconfig.json +1 -0
@@ -19,16 +19,15 @@
19
19
  // (Monarch grammars for ~80 languages) without pulling in the extension host
20
20
  // (no SharedArrayBuffer / COOP / COEP needed).
21
21
  import "@codingame/monaco-vscode-standalone-languages";
22
- // The named imports below ALSO trigger this package's side-effect registration
23
- // of the standalone TypeScript language features (defaults + ts.worker). We use
24
- // them to configure the TS/JS language services + inject ambient context types.
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
25
  import {
26
26
  typescriptDefaults,
27
27
  javascriptDefaults,
28
- ScriptTarget,
29
- ModuleKind,
30
- ModuleResolutionKind,
31
- } from "@codingame/monaco-vscode-standalone-typescript-language-features";
28
+ ensureStandaloneWorkerFactory,
29
+ markVscodeServicesReady,
30
+ } from "./monacoTsService";
32
31
  // Named import also triggers the side-effect registration of the REAL VS Code
33
32
  // JSON language service (proper highlighting + completion + folding), replacing
34
33
  // the hand-rolled `json-template` Monarch grammar. We turn its built-in
@@ -41,20 +40,8 @@ import { jsonDefaults } from "@codingame/monaco-vscode-standalone-json-language-
41
40
  // `languageStatusServiceOverride` below).
42
41
  import getLanguagesServiceOverride from "@codingame/monaco-vscode-languages-service-override";
43
42
 
44
- // Worker entry URLs, bundled and resolved by Vite via the `?worker&url`
45
- // suffix. We import them as URL STRINGS (not Worker constructors) because
46
- // monaco-languageclient's worker factory consumes `loader().url.toString()`.
47
- // Vite's STATIC `?worker&url` resolution is required here: a runtime
48
- // `new URL(specifier, import.meta.url)` would resolve the bare specifier
49
- // relative to THIS source file (e.g. core/ui/src/components/CodeEditor/...)
50
- // and 404. The editor worker comes from the monaco-editor drop-in
51
- // (@codingame/monaco-vscode-editor-api); the TypeScript worker (which also
52
- // serves JavaScript) from the standalone language-features package.
53
- import editorWorkerUrl from "@codingame/monaco-vscode-editor-api/esm/vs/editor/editor.worker.js?worker&url";
54
- import tsWorkerUrl from "@codingame/monaco-vscode-standalone-typescript-language-features/worker?worker&url";
55
- import jsonWorkerUrl from "@codingame/monaco-vscode-standalone-json-language-features/worker?worker&url";
56
-
57
- import { useEffect, useId, useRef, useState } from "react";
43
+
44
+ import { type CSSProperties, useEffect, useId, useRef, useState } from "react";
58
45
  import * as monaco from "@codingame/monaco-vscode-editor-api";
59
46
  import { MonacoEditorReactComp } from "@typefox/monaco-editor-react";
60
47
  import { extractBracketKeyGroups } from "./bracketKeyGroups";
@@ -68,132 +55,115 @@ import {
68
55
  matchShellEnvVarTrigger,
69
56
  } from "./shellEnvVarMatcher";
70
57
  import type {
58
+ AcquireTypes,
71
59
  CodeEditorLanguage,
72
60
  EditorMarker,
73
61
  ShellEnvVar,
74
62
  TemplateProperty,
75
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";
76
78
  import {
77
79
  type EditorAppConfig,
78
80
  type TextContents,
79
81
  } from "monaco-languageclient/editorApp";
80
82
  import { type MonacoVscodeApiConfig } from "monaco-languageclient/vscodeApiWrapper";
81
- import {
82
- // `useWorkerFactory` is a plain library registration function, not a React
83
- // hook. We alias away the `use` prefix so the `react-hooks/rules-of-hooks`
84
- // lint rule (which keys purely off the identifier name) does not misfire.
85
- useWorkerFactory as registerWorkerFactory,
86
- Worker,
87
- type WorkerFactoryConfig,
88
- type WorkerLoader,
89
- } from "monaco-languageclient/workerFactory";
90
-
91
- // The logger type originates from `@codingame/monaco-vscode-log-service-override`,
92
- // which is not a direct dependency of this package. We derive it from the
93
- // `WorkerFactoryConfig` we already import so we never reach for a transitive
94
- // specifier (and never need an `any`).
95
- type WorkerFactoryLogger = WorkerFactoryConfig["logger"];
96
-
97
- const editorWorkerLoader: WorkerLoader = () =>
98
- new Worker(editorWorkerUrl, { type: "module" });
99
-
100
- const tsWorkerLoader: WorkerLoader = () =>
101
- new Worker(tsWorkerUrl, { type: "module" });
102
-
103
- const jsonWorkerLoader: WorkerLoader = () =>
104
- new Worker(jsonWorkerUrl, { type: "module" });
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;
105
99
 
106
100
  /**
107
- * Registers the worker loaders required for the standalone (classic) Monaco
108
- * setup. We only need the generic editor worker plus the TypeScript worker
109
- * (which also serves JavaScript). Mirrors the upstream `defineClassicWorkers`
110
- * helper referenced above. The underlying `useWorkerFactory` export is a
111
- * plain library registration function (not a React hook) despite its name;
112
- * it is called from module scope here, never from a component render.
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.
113
106
  */
114
- const configureStandaloneWorkerFactory = (
115
- logger?: WorkerFactoryLogger,
116
- ): void => {
117
- registerWorkerFactory({
118
- workerLoaders: {
119
- editorWorkerService: editorWorkerLoader,
120
- // Both must be defined or the worker factory errors (see upstream
121
- // helper-classic.ts).
122
- javascript: tsWorkerLoader,
123
- typescript: tsWorkerLoader,
124
- json: jsonWorkerLoader,
125
- },
126
- logger,
127
- });
128
- };
129
-
130
- // Base compiler options for the standalone TS + JS services. `types` (node +
131
- // bun-types) is added only once the stdlib bundle has loaded (see
132
- // ensureStandaloneStdlib), so the service doesn't transiently error on a
133
- // missing `node` type while the ~3 MB bundle is still fetching.
134
- const BASE_COMPILER_OPTIONS = {
135
- target: ScriptTarget.ESNext,
136
- module: ModuleKind.ESNext,
137
- moduleResolution: ModuleResolutionKind.NodeJs,
138
- lib: ["esnext"],
139
- allowNonTsExtensions: false,
140
- noEmit: true,
141
- strict: true,
142
- esModuleInterop: true,
107
+ const syncAcquireResetKey = (resetKey: string | undefined): void => {
108
+ if (resetKey === currentAcquireResetKey) return;
109
+ currentAcquireResetKey = resetKey;
110
+ acquiredSpecifiers.clear();
111
+ acquiredFilePaths.clear();
143
112
  };
144
113
 
145
114
  /**
146
- * Configure the standalone TS + JS language services ONCE at module load.
147
- * `typescriptDefaults` / `javascriptDefaults` are singletons, so doing this at
148
- * module scope (not per-mount) guarantees the first editor to mount cannot
149
- * start the service with stale defaults - the timing race the legacy monaco
150
- * editor hit.
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.
151
118
  */
152
- const configureTypeScriptDefaults = (): void => {
153
- for (const defaults of [typescriptDefaults, javascriptDefaults]) {
154
- defaults.setCompilerOptions({ ...BASE_COMPILER_OPTIONS });
155
- // 1108: a top-level `return` is valid because the runtime wraps scripts in
156
- // an async IIFE (same suppression as the legacy editor).
157
- defaults.setDiagnosticsOptions({ diagnosticCodesToIgnore: [1108] });
158
- // Push models to the worker eagerly so diagnostics/completions are ready on
159
- // the first keystroke.
160
- defaults.setEagerModelSync(true);
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
+ }
161
129
  }
162
130
  };
163
131
 
164
- configureTypeScriptDefaults();
165
-
166
132
  /**
167
- * Lazy-load the bundled `@types/node` + `bun-types` declarations into the
168
- * standalone TS service so script editors have `console`, `fetch`, `process`,
169
- * `Bun`, etc. typed (parity with the legacy editor). The ~3 MB bundle is
170
- * code-split into its own chunk and fetched once. Ported from the legacy
171
- * `monacoStdlib.ts` (without its `@monaco-editor/react` dependency). Runs at
172
- * module load; this file is browser-only so the dynamic import is safe here.
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.
173
138
  */
174
- let stdlibLoadStarted = false;
175
- const ensureStandaloneStdlib = async (): Promise<void> => {
176
- if (stdlibLoadStarted) {
177
- return;
178
- }
179
- stdlibLoadStarted = true;
180
- const stdlibModule = await import("./generated/stdlib-types.json");
181
- const bundle = stdlibModule.default;
182
- for (const defaults of [typescriptDefaults, javascriptDefaults]) {
183
- for (const [path, content] of Object.entries(bundle)) {
184
- defaults.addExtraLib(content, `file:///${path}`);
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);
185
163
  }
186
- // The @types/node + bun-types declarations now exist at their node_modules
187
- // virtual paths, so include them ambiently.
188
- defaults.setCompilerOptions({
189
- ...BASE_COMPILER_OPTIONS,
190
- types: ["node", "bun-types"],
191
- });
192
164
  }
193
165
  };
194
166
 
195
- void ensureStandaloneStdlib();
196
-
197
167
  // Turn OFF the JSON service's built-in validation. The editor content is a
198
168
  // template that renders to JSON, so we validate the template-substituted form
199
169
  // ourselves (see the json validation effect + validateJsonTemplate) to tolerate
@@ -327,6 +297,13 @@ export type TypefoxEditorProps = {
327
297
  language?: CodeEditorLanguage;
328
298
  /** Minimum editor height in pixels. Defaults to 240. */
329
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;
330
307
  /**
331
308
  * Generated ambient type definitions (the `context.d.ts`) injected as a TS
332
309
  * extra-lib so `context.*` resolves with real fields. Wired up once per
@@ -356,6 +333,28 @@ export type TypefoxEditorProps = {
356
333
  readOnly?: boolean;
357
334
  /** Accessible label / hint for the editor (surfaced via aria-label). */
358
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[];
359
358
  };
360
359
 
361
360
  /**
@@ -382,18 +381,30 @@ const languageStatusServiceOverride: monaco.editor.IEditorOverrideServices =
382
381
  : {};
383
382
  })();
384
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
+
385
392
  export const TypefoxEditor = ({
386
393
  id,
387
394
  value,
388
395
  onChange,
389
396
  language = "typescript",
390
397
  minHeight = 240,
398
+ fillHeight = false,
391
399
  typeDefinitions,
392
400
  templateProperties,
393
401
  shellEnvVars,
394
402
  markers,
395
403
  readOnly = false,
396
404
  placeholder,
405
+ acquireTypes,
406
+ acquireResetKey,
407
+ importablePackages,
397
408
  }: TypefoxEditorProps) => {
398
409
  // `MonacoEditorReactComp` captures `onTextChanged` once at editor-start, so
399
410
  // the handler it calls would otherwise close over a stale `onChange` (bound
@@ -438,6 +449,138 @@ export const TypefoxEditor = ({
438
449
  };
439
450
  }, [isTsLike, typeDefinitions, modelId]);
440
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
+
441
584
  // Type-driven bracket-notation completions. The standalone TS worker omits
442
585
  // object members whose keys aren't valid identifiers (artifact ids like
443
586
  // `integration-jira.issue`), and the built-in SuggestAdapter can't be
@@ -816,7 +959,7 @@ export const TypefoxEditor = ({
816
959
  // Plain editor, no workbench views.
817
960
  $type: "EditorService",
818
961
  },
819
- monacoWorkerFactory: configureStandaloneWorkerFactory,
962
+ monacoWorkerFactory: ensureStandaloneWorkerFactory,
820
963
  };
821
964
 
822
965
  const editorAppConfig: EditorAppConfig = {
@@ -850,9 +993,17 @@ export const TypefoxEditor = ({
850
993
  onChangeRef.current?.(textChanges.modified ?? "");
851
994
  };
852
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
+
853
1004
  return (
854
1005
  <MonacoEditorReactComp
855
- style={{ minHeight: `${minHeight}px`, height: `${minHeight}px` }}
1006
+ style={containerStyle}
856
1007
  vscodeApiConfig={vscodeApiConfig}
857
1008
  editorAppConfig={editorAppConfig}
858
1009
  onTextChanged={handleTextChanged}
@@ -862,6 +1013,11 @@ export const TypefoxEditor = ({
862
1013
  // once, so a second editor never gets its own onVscodeApiInitDone - but
863
1014
  // onEditorStartDone fires for each editor instance.
864
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();
865
1021
  }}
866
1022
  />
867
1023
  );
@@ -0,0 +1 @@
1
+ ["assert","assert/strict","async_hooks","buffer","bun","bun:bundle","bun:ffi","bun:jsc","bun:sqlite","bun:test","child_process","cluster","console","constants","crypto","dgram","diagnostics_channel","dns","dns/promises","domain","events","fs","fs/promises","http","http2","https","inspector","inspector/promises","module","net","node:assert","node:assert/strict","node:async_hooks","node:buffer","node:child_process","node:cluster","node:console","node:constants","node:crypto","node:dgram","node:diagnostics_channel","node:dns","node:dns/promises","node:domain","node:events","node:fs","node:fs/promises","node:http","node:http2","node:https","node:inspector","node:inspector/promises","node:module","node:net","node:os","node:path","node:path/posix","node:path/win32","node:perf_hooks","node:process","node:punycode","node:querystring","node:readline","node:readline/promises","node:repl","node:sea","node:stream","node:stream/consumers","node:stream/promises","node:stream/web","node:string_decoder","node:test","node:test/reporters","node:timers","node:timers/promises","node:tls","node:trace_events","node:tty","node:url","node:util","node:util/types","node:v8","node:vm","node:wasi","node:worker_threads","node:zlib","os","path","path/posix","path/win32","perf_hooks","process","punycode","querystring","readline","readline/promises","repl","stream","stream/consumers","stream/promises","stream/web","string_decoder","timers","timers/promises","tls","trace_events","tty","url","util","util/types","v8","vm","wasi","worker_threads","zlib"]