feat(profiles): add typed live step config profiles

Profiles store script + default config per owner. Workflow steps reference
them with profile:, overlay keys win, and the UI marks overrides. Profile
name is immutable after create so YAML refs stay stable.

Co-authored-by: Nasyarobby Putra <nasyarobby@gmail.com>
This commit is contained in:
Cursor Agent
2026-08-21 06:12:49 +00:00
co-authored by nsrb
parent 210fdb2244
commit e84432a492
25 changed files with 1512 additions and 123 deletions
@@ -487,15 +487,23 @@ export function ConfigFields({
owner,
excludeFile,
disabled,
inheritedConfig = null,
}) {
const cfg = config && typeof config === "object" && !Array.isArray(config) ? config : {};
const inherited =
inheritedConfig && typeof inheritedConfig === "object" && !Array.isArray(inheritedConfig)
? inheritedConfig
: null;
const metaKeys = Object.keys(meta?.config ?? {});
const extraKeys = Object.keys(cfg).filter((k) => !metaKeys.includes(k));
const inheritedExtraKeys = inherited
? Object.keys(inherited).filter((k) => !metaKeys.includes(k) && !(k in cfg))
: [];
function setField(key, value) {
const spec = fieldSpec(meta, key);
const next = { ...cfg };
const required = Boolean(spec.required);
const required = Boolean(spec.required) && !inherited;
if (value === undefined || (value === "" && !required)) {
delete next[key];
} else {
@@ -504,6 +512,12 @@ export function ConfigFields({
onChange(next);
}
function resetField(key) {
const next = { ...cfg };
delete next[key];
onChange(next);
}
function renameExtra(oldKey, nextKey) {
if (!nextKey || nextKey === oldKey) return;
const next = {};
@@ -516,35 +530,70 @@ export function ConfigFields({
function addExtra() {
let key = "key";
let i = 1;
while (key in cfg) {
while (key in cfg || (inherited && key in inherited)) {
key = `key${i}`;
i += 1;
}
onChange({ ...cfg, [key]: "" });
}
function renderField(key, { extra = false, inheritedOnly = false } = {}) {
const spec = extra ? { type: "string" } : fieldSpec(meta, key);
const overridden = Boolean(inherited && Object.prototype.hasOwnProperty.call(cfg, key));
const displayValue = inheritedOnly
? inherited?.[key]
: overridden || !inherited
? cfg[key]
: inherited[key];
const unknown =
extra && metaKeys.length > 0 && !Object.prototype.hasOwnProperty.call(meta?.config ?? {}, key);
return (
<div
key={key}
className={`space-y-1 ${
overridden ? "rounded-box border border-warning/50 bg-warning/5 p-2" : ""
}`}
>
<FieldLabel name={key} required={Boolean(spec.required) && !inherited} description={spec.description}>
{overridden ? (
<span className="badge badge-warning badge-xs">overridden</span>
) : inherited && !inheritedOnly ? (
<span className="badge badge-ghost badge-xs">from profile</span>
) : null}
{unknown ? (
<span className="badge badge-error badge-xs">not in schema</span>
) : null}
{overridden ? (
<button
type="button"
className="btn btn-ghost btn-xs"
disabled={disabled}
onClick={() => resetField(key)}
>
Reset
</button>
) : null}
</FieldLabel>
<ValueEditor
value={displayValue}
onChange={(v) => setField(key, v)}
spec={extra ? { type: displayValue != null && typeof displayValue === "object" ? "object" : "string" } : spec}
script={script}
fieldKey={key}
workflows={workflows}
owner={owner}
excludeFile={excludeFile}
disabled={disabled}
/>
<ConfigRefHint value={displayValue} owner={owner} />
</div>
);
}
return (
<div className="space-y-2">
{metaKeys.map((key) => {
const spec = fieldSpec(meta, key);
return (
<div key={key} className="space-y-1">
<FieldLabel name={key} required={Boolean(spec.required)} description={spec.description} />
<ValueEditor
value={cfg[key]}
onChange={(v) => setField(key, v)}
spec={spec}
script={script}
fieldKey={key}
workflows={workflows}
owner={owner}
excludeFile={excludeFile}
disabled={disabled}
/>
<ConfigRefHint value={cfg[key]} owner={owner} />
</div>
);
})}
{metaKeys.map((key) => renderField(key))}
{extraKeys.map((key) => (
<div key={key} className="space-y-1">
<div className="flex items-center gap-1">
@@ -552,16 +601,18 @@ export function ConfigFields({
value={key}
onCommit={(nk) => renameExtra(key, nk)}
className="font-mono text-sm"
disabled={disabled}
/>
{inherited ? <span className="badge badge-warning badge-xs">overridden</span> : null}
{metaKeys.length > 0 ? (
<span className="badge badge-error badge-xs">not in schema</span>
) : null}
<button
type="button"
className="btn btn-ghost btn-xs btn-square text-error"
aria-label={`Remove ${key}`}
onClick={() => {
const next = { ...cfg };
delete next[key];
onChange(next);
}}
disabled={disabled}
onClick={() => resetField(key)}
>
<LuTrash2 className="size-3.5" />
</button>
@@ -580,6 +631,7 @@ export function ConfigFields({
<ConfigRefHint value={cfg[key]} owner={owner} />
</div>
))}
{inheritedExtraKeys.map((key) => renderField(key, { extra: true, inheritedOnly: true }))}
<button type="button" className="btn btn-ghost btn-xs" disabled={disabled} onClick={addExtra}>
<LuPlus className="size-3.5" />
Add field