@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
@@ -13,13 +13,21 @@ import {
13
13
  Textarea,
14
14
  Toggle,
15
15
  ColorPicker,
16
+ TemplateValueInput,
17
+ DurationInput,
18
+ type DurationValue,
16
19
  } from "../../index";
17
20
 
18
21
  import type { FormFieldProps, JsonSchemaProperty } from "./types";
19
- import { getCleanDescription, NONE_SENTINEL } from "./utils";
22
+ import {
23
+ getCleanDescription,
24
+ NONE_SENTINEL,
25
+ findSecretEnvSibling,
26
+ } from "./utils";
20
27
  import { DynamicOptionsField } from "./DynamicOptionsField";
21
28
  import { JsonField } from "./JsonField";
22
29
  import { MultiTypeEditorField } from "./MultiTypeEditorField";
30
+ import { SecretEnvEditor } from "./SecretEnvEditor";
23
31
 
24
32
  /**
25
33
  * Recursive field renderer that handles all supported JSON Schema types.
@@ -33,9 +41,16 @@ export const FormField: React.FC<FormFieldProps> = ({
33
41
  formValues,
34
42
  optionsResolvers,
35
43
  templateProperties,
44
+ templateCompletionProvider,
36
45
  typeDefinitions,
37
46
  shellEnvVars,
38
47
  starterTemplates,
48
+ scriptTestRenderer,
49
+ secretNames,
50
+ acquireTypes,
51
+ acquireResetKey,
52
+ importablePackages,
53
+ siblingSecretEnv,
39
54
  onChange,
40
55
  }) => {
41
56
  const description = propSchema.description || "";
@@ -72,6 +87,34 @@ export const FormField: React.FC<FormFieldProps> = ({
72
87
  return <></>;
73
88
  }
74
89
 
90
+ // Duration field — render the DurationInput (single-unit duration
91
+ // object). Marked via `x-duration: true` or `format: "duration"`. This
92
+ // branch is intentionally additive and sits before the generic union /
93
+ // object handlers so a `for:` / threshold-window config renders the
94
+ // widget rather than the raw oneOf discriminator picker.
95
+ const isDuration =
96
+ propSchema["x-duration"] === true || propSchema.format === "duration";
97
+ if (isDuration) {
98
+ const cleanDesc = getCleanDescription(description);
99
+ return (
100
+ <div className="space-y-2">
101
+ <div>
102
+ <Label htmlFor={id}>
103
+ {label} {isRequired && "*"}
104
+ </Label>
105
+ {cleanDesc && (
106
+ <p className="text-sm text-muted-foreground mt-0.5">{cleanDesc}</p>
107
+ )}
108
+ </div>
109
+ <DurationInput
110
+ id={id}
111
+ value={value as DurationValue | undefined}
112
+ onChange={(next) => onChange(next)}
113
+ />
114
+ </div>
115
+ );
116
+ }
117
+
75
118
  // Enum handling
76
119
  if (propSchema.enum) {
77
120
  const cleanDesc = getCleanDescription(description);
@@ -135,6 +178,16 @@ export const FormField: React.FC<FormFieldProps> = ({
135
178
  typeDefinitions={typeDefinitions}
136
179
  shellEnvVars={shellEnvVars}
137
180
  starterTemplates={starterTemplates}
181
+ scriptTestRenderer={
182
+ propSchema["x-script-testable"] === true
183
+ ? scriptTestRenderer
184
+ : undefined
185
+ }
186
+ acquireTypes={acquireTypes}
187
+ acquireResetKey={acquireResetKey}
188
+ importablePackages={importablePackages}
189
+ fieldId={id}
190
+ siblingSecretEnv={siblingSecretEnv}
138
191
  onChange={onChange as (val: string | undefined) => void}
139
192
  />
140
193
  );
@@ -232,7 +285,14 @@ export const FormField: React.FC<FormFieldProps> = ({
232
285
  );
233
286
  }
234
287
 
235
- // Default string input
288
+ // Default string input. When a completion provider is supplied the
289
+ // field is templatable (e.g. automation action config), so render a
290
+ // TemplateValueInput wired to it for `{{ … }}` autocomplete; without
291
+ // one, keep the bare Input so other DynamicForm consumers are
292
+ // unaffected.
293
+ const placeholder = propSchema.default
294
+ ? `Default: ${String(propSchema.default)}`
295
+ : "";
236
296
  return (
237
297
  <div className="space-y-2">
238
298
  <div>
@@ -243,14 +303,22 @@ export const FormField: React.FC<FormFieldProps> = ({
243
303
  <p className="text-sm text-muted-foreground mt-0.5">{cleanDesc}</p>
244
304
  )}
245
305
  </div>
246
- <Input
247
- id={id}
248
- value={(value as string) || ""}
249
- onChange={(e) => onChange(e.target.value)}
250
- placeholder={
251
- propSchema.default ? `Default: ${String(propSchema.default)}` : ""
252
- }
253
- />
306
+ {templateCompletionProvider ? (
307
+ <TemplateValueInput
308
+ id={id}
309
+ value={(value as string) || ""}
310
+ onChange={(next) => onChange(next)}
311
+ placeholder={placeholder}
312
+ completionProvider={templateCompletionProvider}
313
+ />
314
+ ) : (
315
+ <Input
316
+ id={id}
317
+ value={(value as string) || ""}
318
+ onChange={(e) => onChange(e.target.value)}
319
+ placeholder={placeholder}
320
+ />
321
+ )}
254
322
  </div>
255
323
  );
256
324
  }
@@ -314,6 +382,34 @@ export const FormField: React.FC<FormFieldProps> = ({
314
382
  }
315
383
 
316
384
  // Dictionary/Record (headers)
385
+ // Secret -> env mapping: a dedicated editor (env name + secret-name
386
+ // picker) instead of the raw JSON record fallback.
387
+ if (
388
+ propSchema.type === "object" &&
389
+ propSchema.additionalProperties &&
390
+ propSchema["x-secret-env"]
391
+ ) {
392
+ const cleanDesc = getCleanDescription(description);
393
+ return (
394
+ <div className="space-y-2">
395
+ <div>
396
+ <Label htmlFor={id}>
397
+ {label} {isRequired && "*"}
398
+ </Label>
399
+ {cleanDesc && (
400
+ <p className="text-sm text-muted-foreground mt-0.5">{cleanDesc}</p>
401
+ )}
402
+ </div>
403
+ <SecretEnvEditor
404
+ id={id}
405
+ value={(value as Record<string, string> | undefined) ?? {}}
406
+ secretNames={secretNames}
407
+ onChange={(next) => onChange(next)}
408
+ />
409
+ </div>
410
+ );
411
+ }
412
+
317
413
  if (propSchema.type === "object" && propSchema.additionalProperties) {
318
414
  const cleanDesc = getCleanDescription(description);
319
415
  return (
@@ -338,6 +434,12 @@ export const FormField: React.FC<FormFieldProps> = ({
338
434
 
339
435
  // Object (Nested Form)
340
436
  if (propSchema.type === "object" && propSchema.properties) {
437
+ // Resolve the secret→env sibling within THIS object so a nested
438
+ // testable script field forwards the right mapping to the test panel.
439
+ const nestedSecretEnv = findSecretEnvSibling({
440
+ properties: propSchema.properties,
441
+ values: value as Record<string, unknown> | undefined,
442
+ });
341
443
  return (
342
444
  <div className="space-y-4 p-4 border rounded-lg bg-muted/30">
343
445
  <p className="text-sm font-semibold">{label}</p>
@@ -352,9 +454,16 @@ export const FormField: React.FC<FormFieldProps> = ({
352
454
  formValues={formValues}
353
455
  optionsResolvers={optionsResolvers}
354
456
  templateProperties={templateProperties}
457
+ templateCompletionProvider={templateCompletionProvider}
355
458
  typeDefinitions={typeDefinitions}
356
459
  shellEnvVars={shellEnvVars}
357
460
  starterTemplates={starterTemplates}
461
+ scriptTestRenderer={scriptTestRenderer}
462
+ secretNames={secretNames}
463
+ acquireTypes={acquireTypes}
464
+ acquireResetKey={acquireResetKey}
465
+ importablePackages={importablePackages}
466
+ siblingSecretEnv={nestedSecretEnv}
358
467
  onChange={(val) =>
359
468
  onChange({ ...(value as Record<string, unknown>), [key]: val })
360
469
  }
@@ -463,9 +572,15 @@ export const FormField: React.FC<FormFieldProps> = ({
463
572
  formValues={formValues}
464
573
  optionsResolvers={optionsResolvers}
465
574
  templateProperties={templateProperties}
575
+ templateCompletionProvider={templateCompletionProvider}
466
576
  typeDefinitions={typeDefinitions}
467
577
  shellEnvVars={shellEnvVars}
468
578
  starterTemplates={starterTemplates}
579
+ scriptTestRenderer={scriptTestRenderer}
580
+ secretNames={secretNames}
581
+ acquireTypes={acquireTypes}
582
+ acquireResetKey={acquireResetKey}
583
+ importablePackages={importablePackages}
469
584
  onChange={(val) => {
470
585
  const next = [...(items as unknown[])];
471
586
  next[index] = val;
@@ -524,6 +639,12 @@ export const FormField: React.FC<FormFieldProps> = ({
524
639
  const displayDiscriminatorField =
525
640
  discriminatorField.charAt(0).toUpperCase() + discriminatorField.slice(1);
526
641
 
642
+ // Secret→env sibling within the selected variant's object.
643
+ const variantSecretEnv = findSecretEnvSibling({
644
+ properties: selectedVariant.properties,
645
+ values: currentValue,
646
+ });
647
+
527
648
  return (
528
649
  <div className="space-y-3 p-3 border rounded-lg bg-background">
529
650
  {/* Discriminator selector */}
@@ -594,9 +715,16 @@ export const FormField: React.FC<FormFieldProps> = ({
594
715
  formValues={formValues}
595
716
  optionsResolvers={optionsResolvers}
596
717
  templateProperties={templateProperties}
718
+ templateCompletionProvider={templateCompletionProvider}
597
719
  typeDefinitions={typeDefinitions}
598
720
  shellEnvVars={shellEnvVars}
599
721
  starterTemplates={starterTemplates}
722
+ scriptTestRenderer={scriptTestRenderer}
723
+ secretNames={secretNames}
724
+ acquireTypes={acquireTypes}
725
+ acquireResetKey={acquireResetKey}
726
+ importablePackages={importablePackages}
727
+ siblingSecretEnv={variantSecretEnv}
600
728
  onChange={(val) => onChange({ ...currentValue, [key]: val })}
601
729
  />
602
730
  ))}
@@ -3,6 +3,7 @@ import { Plus, Trash2 } from "lucide-react";
3
3
  import { Button } from "../Button";
4
4
  import { Input } from "../Input";
5
5
  import type { TemplateProperty } from "../CodeEditor";
6
+ import { TemplateValueInput } from "../TemplateValueInput";
6
7
 
7
8
  export interface KeyValuePair {
8
9
  key: string;
@@ -28,24 +29,6 @@ export interface KeyValueEditorProps {
28
29
  templateProperties?: TemplateProperty[];
29
30
  }
30
31
 
31
- /**
32
- * Detect if cursor is inside an unclosed {{ template context.
33
- */
34
- function detectTemplateContext(text: string, cursorPos: number) {
35
- const textBefore = text.slice(0, cursorPos);
36
- const lastOpenBrace = textBefore.lastIndexOf("{{");
37
- const lastCloseBrace = textBefore.lastIndexOf("}}");
38
-
39
- if (lastOpenBrace !== -1 && lastOpenBrace > lastCloseBrace) {
40
- return {
41
- isInTemplate: true,
42
- query: textBefore.slice(lastOpenBrace + 2),
43
- startPos: lastOpenBrace,
44
- };
45
- }
46
- return { isInTemplate: false, query: "", startPos: -1 };
47
- }
48
-
49
32
  /**
50
33
  * A key/value pair editor for form data and similar use cases.
51
34
  * Supports adding/removing pairs and optional template autocomplete in values.
@@ -131,7 +114,7 @@ export const KeyValueEditor: React.FC<KeyValueEditorProps> = ({
131
114
  className="flex-1 font-mono text-sm"
132
115
  />
133
116
  <span className="text-muted-foreground">=</span>
134
- <TemplateInput
117
+ <TemplateValueInput
135
118
  id={`${id}-value-${index}`}
136
119
  value={pair.value}
137
120
  onChange={(newValue) => handleValueChange(index, newValue)}
@@ -162,153 +145,3 @@ export const KeyValueEditor: React.FC<KeyValueEditorProps> = ({
162
145
  </div>
163
146
  );
164
147
  };
165
-
166
- /**
167
- * An input with simple template autocomplete support.
168
- * Shows a dropdown when user types "{{".
169
- */
170
- const TemplateInput: React.FC<{
171
- id: string;
172
- value: string;
173
- onChange: (value: string) => void;
174
- placeholder?: string;
175
- templateProperties?: TemplateProperty[];
176
- }> = ({ id, value, onChange, placeholder, templateProperties }) => {
177
- const inputRef = React.useRef<HTMLInputElement>(null);
178
- const [showPopup, setShowPopup] = React.useState(false);
179
- const [selectedIndex, setSelectedIndex] = React.useState(0);
180
- const [templateContext, setTemplateContext] = React.useState<{
181
- query: string;
182
- startPos: number;
183
- }>({ query: "", startPos: -1 });
184
-
185
- // Filter properties based on query
186
- const filteredProperties = React.useMemo(() => {
187
- if (!templateProperties) return [];
188
- if (!templateContext.query.trim()) return templateProperties;
189
- const lowerQuery = templateContext.query.toLowerCase();
190
- return templateProperties.filter((prop) =>
191
- prop.path.toLowerCase().includes(lowerQuery),
192
- );
193
- }, [templateProperties, templateContext.query]);
194
-
195
- const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
196
- const newValue = e.target.value;
197
- onChange(newValue);
198
-
199
- if (!templateProperties || templateProperties.length === 0) return;
200
-
201
- const cursorPos = e.target.selectionStart ?? newValue.length;
202
- const context = detectTemplateContext(newValue, cursorPos);
203
-
204
- if (context.isInTemplate) {
205
- setTemplateContext({ query: context.query, startPos: context.startPos });
206
- setShowPopup(true);
207
- setSelectedIndex(0);
208
- } else {
209
- setShowPopup(false);
210
- }
211
- };
212
-
213
- const insertProperty = (prop: TemplateProperty) => {
214
- if (templateContext.startPos === -1) return;
215
-
216
- const cursorPos = inputRef.current?.selectionStart ?? value.length;
217
- const template = `{{${prop.path}}}`;
218
- const newValue =
219
- value.slice(0, templateContext.startPos) +
220
- template +
221
- value.slice(cursorPos);
222
-
223
- onChange(newValue);
224
- setShowPopup(false);
225
-
226
- // Restore focus
227
- setTimeout(() => {
228
- inputRef.current?.focus();
229
- const newPos = templateContext.startPos + template.length;
230
- inputRef.current?.setSelectionRange(newPos, newPos);
231
- }, 0);
232
- };
233
-
234
- const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
235
- if (!showPopup || filteredProperties.length === 0) return;
236
-
237
- switch (e.key) {
238
- case "ArrowDown": {
239
- e.preventDefault();
240
- setSelectedIndex((prev) =>
241
- prev < filteredProperties.length - 1 ? prev + 1 : 0,
242
- );
243
- break;
244
- }
245
- case "ArrowUp": {
246
- e.preventDefault();
247
- setSelectedIndex((prev) =>
248
- prev > 0 ? prev - 1 : filteredProperties.length - 1,
249
- );
250
- break;
251
- }
252
- case "Enter":
253
- case "Tab": {
254
- e.preventDefault();
255
- if (filteredProperties[selectedIndex]) {
256
- insertProperty(filteredProperties[selectedIndex]);
257
- }
258
- break;
259
- }
260
- case "Escape": {
261
- e.preventDefault();
262
- setShowPopup(false);
263
- break;
264
- }
265
- }
266
- };
267
-
268
- // Close popup on blur
269
- const handleBlur = () => {
270
- // Delay to allow click on popup item
271
- setTimeout(() => setShowPopup(false), 150);
272
- };
273
-
274
- return (
275
- <div className="relative flex-1">
276
- <Input
277
- ref={inputRef}
278
- id={id}
279
- value={value}
280
- onChange={handleChange}
281
- onKeyDown={handleKeyDown}
282
- onBlur={handleBlur}
283
- placeholder={placeholder}
284
- className="font-mono text-sm"
285
- />
286
- {showPopup && filteredProperties.length > 0 && (
287
- <div className="absolute z-50 top-full left-0 mt-1 w-64 max-h-48 overflow-y-auto rounded-md border border-border bg-popover shadow-lg">
288
- <div className="p-1">
289
- {filteredProperties.map((prop, index) => (
290
- <button
291
- key={prop.path}
292
- type="button"
293
- onMouseDown={(e) => {
294
- e.preventDefault();
295
- insertProperty(prop);
296
- }}
297
- className={`w-full flex items-center justify-between gap-2 px-2 py-1.5 text-xs rounded-sm text-left hover:bg-accent hover:text-accent-foreground ${
298
- index === selectedIndex
299
- ? "bg-accent text-accent-foreground"
300
- : ""
301
- }`}
302
- >
303
- <code className="font-mono truncate">{prop.path}</code>
304
- <span className="text-muted-foreground shrink-0">
305
- {prop.type}
306
- </span>
307
- </button>
308
- ))}
309
- </div>
310
- </div>
311
- )}
312
- </div>
313
- );
314
- };
@@ -1,7 +1,11 @@
1
1
  import React from "react";
2
2
  import { Label } from "../Label";
3
3
  import { Textarea } from "../Textarea";
4
- import { CodeEditor, type TemplateProperty } from "../CodeEditor";
4
+ import {
5
+ CodeEditor,
6
+ type TemplateProperty,
7
+ type AcquireTypes,
8
+ } from "../CodeEditor";
5
9
  import {
6
10
  Select,
7
11
  SelectContent,
@@ -17,7 +21,11 @@ import {
17
21
  parseFormData,
18
22
  EDITOR_TYPE_LABELS,
19
23
  } from "./utils";
20
- import type { EditorStarterTemplates, ShellEnvVar } from "./types";
24
+ import type {
25
+ EditorStarterTemplates,
26
+ ScriptTestRenderer,
27
+ ShellEnvVar,
28
+ } from "./types";
21
29
  import { pickStarterForEmptyField } from "./starterTemplateSelector";
22
30
 
23
31
  export interface MultiTypeEditorFieldProps {
@@ -54,6 +62,34 @@ export interface MultiTypeEditorFieldProps {
54
62
  * working example instead of a blank canvas.
55
63
  */
56
64
  starterTemplates?: EditorStarterTemplates;
65
+ /**
66
+ * Optional renderer for the inline script-test panel. When supplied (the
67
+ * field is `x-script-testable`), it is rendered beneath the editor for
68
+ * `typescript` / `shell` modes, with the currently-selected language as
69
+ * `kind`. The owning page wires it to the `testScript` RPC.
70
+ */
71
+ scriptTestRenderer?: ScriptTestRenderer;
72
+ /**
73
+ * Optional lazy type-acquisition resolver, forwarded to the TS/JS
74
+ * `CodeEditor` so imported npm packages autocomplete (lazy ATA).
75
+ */
76
+ acquireTypes?: AcquireTypes;
77
+ /** Install identity (lockfile hash); resets acquired types on a new install. */
78
+ acquireResetKey?: string;
79
+ /**
80
+ * Importable installed package names (`@types/*`-free), forwarded to the
81
+ * TS/JS `CodeEditor` so the import specifier itself autocompletes.
82
+ */
83
+ importablePackages?: string[];
84
+ /** Form key of this field, forwarded to {@link scriptTestRenderer}. */
85
+ fieldId?: string;
86
+ /**
87
+ * Current value of the sibling `x-secret-env` mapping (located by
88
+ * annotation in the parent config object), forwarded to
89
+ * {@link scriptTestRenderer} so the test panel injects placeholders /
90
+ * overrides for the same secrets the action declares.
91
+ */
92
+ siblingSecretEnv?: Record<string, string>;
57
93
  /** Callback when value changes */
58
94
  onChange: (value: string | undefined) => void;
59
95
  }
@@ -74,6 +110,12 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
74
110
  typeDefinitions,
75
111
  shellEnvVars,
76
112
  starterTemplates,
113
+ scriptTestRenderer,
114
+ acquireTypes,
115
+ acquireResetKey,
116
+ importablePackages,
117
+ fieldId,
118
+ siblingSecretEnv,
77
119
  onChange,
78
120
  }) => {
79
121
  // Detect initial editor type from value
@@ -336,6 +378,14 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
336
378
  />
337
379
  )}
338
380
 
381
+ {/*
382
+ Native-code editors (javascript / typescript / shell) intentionally
383
+ do NOT receive `templateProperties`: `{{ }}` template syntax is for
384
+ text/markup fields only. Code fields access run context through
385
+ their language's native mechanism instead — a typed `context`
386
+ object (driven by `typeDefinitions`) for TS/JS, and `$`-prefixed
387
+ env vars (driven by `shellEnvVars`) for shell.
388
+ */}
339
389
  {selectedType === "javascript" && (
340
390
  <CodeEditor
341
391
  id={id}
@@ -343,8 +393,10 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
343
393
  onChange={onChange}
344
394
  language="javascript"
345
395
  minHeight="150px"
346
- templateProperties={templateProperties}
347
396
  typeDefinitions={typeDefinitions}
397
+ acquireTypes={acquireTypes}
398
+ acquireResetKey={acquireResetKey}
399
+ importablePackages={importablePackages}
348
400
  />
349
401
  )}
350
402
 
@@ -355,8 +407,10 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
355
407
  onChange={onChange}
356
408
  language="typescript"
357
409
  minHeight="150px"
358
- templateProperties={templateProperties}
359
410
  typeDefinitions={typeDefinitions}
411
+ acquireTypes={acquireTypes}
412
+ acquireResetKey={acquireResetKey}
413
+ importablePackages={importablePackages}
360
414
  />
361
415
  )}
362
416
 
@@ -367,10 +421,26 @@ export const MultiTypeEditorField: React.FC<MultiTypeEditorFieldProps> = ({
367
421
  onChange={onChange}
368
422
  language="shell"
369
423
  minHeight="150px"
370
- templateProperties={templateProperties}
371
424
  shellEnvVars={shellEnvVars}
372
425
  />
373
426
  )}
427
+
428
+ {/* Inline script-test panel for testable code fields. Shell maps to
429
+ the `shell` test kind; both typescript and javascript map to the
430
+ `typescript` runner kind (the ESM runner handles both). */}
431
+ {scriptTestRenderer !== undefined &&
432
+ (selectedType === "typescript" ||
433
+ selectedType === "javascript" ||
434
+ selectedType === "shell") &&
435
+ scriptTestRenderer({
436
+ fieldId: fieldId ?? id,
437
+ kind: selectedType === "shell" ? "shell" : "typescript",
438
+ script: value ?? "",
439
+ secretEnv:
440
+ siblingSecretEnv && Object.keys(siblingSecretEnv).length > 0
441
+ ? siblingSecretEnv
442
+ : undefined,
443
+ })}
374
444
  </div>
375
445
  );
376
446
  };
@@ -417,8 +487,10 @@ const RawEditor: React.FC<{
417
487
  if (!templateProperties) return [];
418
488
  if (!templateContext.query.trim()) return templateProperties;
419
489
  const lowerQuery = templateContext.query.toLowerCase();
420
- return templateProperties.filter((prop) =>
421
- prop.path.toLowerCase().includes(lowerQuery),
490
+ return templateProperties.filter(
491
+ (prop) =>
492
+ prop.path.toLowerCase().includes(lowerQuery) ||
493
+ (prop.templateRef?.toLowerCase().includes(lowerQuery) ?? false),
422
494
  );
423
495
  }, [templateProperties, templateContext.query]);
424
496
 
@@ -481,7 +553,7 @@ const RawEditor: React.FC<{
481
553
  const textarea = textareaRef.current;
482
554
  if (!textarea || templateContext.startPos === -1) return;
483
555
 
484
- const template = `{{${prop.path}}}`;
556
+ const template = `{{${prop.templateRef ?? prop.path}}}`;
485
557
  const cursorPos = textarea.selectionStart ?? 0;
486
558
  const newValue =
487
559
  value.slice(0, templateContext.startPos) +
@@ -580,7 +652,9 @@ const RawEditor: React.FC<{
580
652
  : ""
581
653
  }`}
582
654
  >
583
- <code className="font-mono truncate">{prop.path}</code>
655
+ <code className="font-mono truncate">
656
+ {prop.templateRef ?? prop.path}
657
+ </code>
584
658
  <span className="text-muted-foreground shrink-0">
585
659
  {prop.type}
586
660
  </span>