Skip to content

feat(ui): add custom step editor autocomplete#1989

Merged
yottahmd merged 3 commits intomainfrom
custom-step-autocomplete
Apr 11, 2026
Merged

feat(ui): add custom step editor autocomplete#1989
yottahmd merged 3 commits intomainfrom
custom-step-autocomplete

Conversation

@yottahmd
Copy link
Copy Markdown
Collaborator

@yottahmd yottahmd commented Apr 11, 2026

Summary

  • add inherited custom-step editor hints to the DAG details API so the UI can build document-specific schema augmentation
  • synthesize a per-document Monaco YAML schema from builtin executor types, inherited custom step types, and unsaved local step_types
  • fix step type inference/docs so dereferenced step schemas still suggest builtin and custom step names and resolve custom config fields correctly

Test plan

  • make api
  • go test ./internal/core/spec ./internal/service/frontend/api/v1 -count=1
  • cd ui && pnpm gen:api
  • cd ui && pnpm exec vitest run src/features/dags/components/dag-editor/__tests__/customStepSchema.test.ts
  • cd ui && pnpm typecheck

Why

The editor previously used a single static schema, so custom step types from step_types or base config could not autocomplete or drive config docs. The follow-up fix in this branch also addresses a regression where step type and config inference stopped working after schema dereferencing because the augmentation only patched definitions.*, not the live inline step schemas consumed by Monaco and the docs sidebar.

Summary by CodeRabbit

  • New Features
    • Editor now displays inherited custom step types from base configuration within the schema documentation sidebar.
    • Enhanced schema validation and autocomplete support for custom step types in the DAG editor.
    • Improved editor integration to provide type hints, descriptions, and input schema information for inherited step types.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 11, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5c8109e3-b49c-4784-a5b9-d727fe698218

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds editor hints support to expose inherited custom step type definitions from the base config to the DAG editor. It introduces new API types for editor metadata, backend logic to derive hints from the base config spec, and UI components that thread hints through the editor pipeline where they augment the YAML schema for improved validation and autocomplete.

Changes

Cohort / File(s) Summary
API Types & Schema
api/v1/api.gen.go, api/v1/api.yaml, ui/src/api/v1/schema.ts
Added new OpenAPI types DAGEditorHints and InheritedCustomStepTypeHint to expose inherited custom step type metadata. Extended GetDAGDetails response with optional editorHints field. Updated generated API code and TypeScript schema types to reflect new contract.
Backend Editor Hints Logic
internal/core/spec/editor_hints.go, internal/service/frontend/api/v1/dags.go
Added InheritedCustomStepTypeEditorHints function to derive editor hints from base config bytes, extracting custom step type name, target type, description, and input schema. Integrated hint derivation into getDAGDetailsData via new buildDAGEditorHints helper that handles base config loading and error cases.
Backend Tests
internal/service/frontend/api/v1/dags_internal_test.go
Added two test cases validating editor hints behavior: one verifying inherited custom step types appear in response, another confirming hints persist even when DAG YAML is invalid.
Schema Dereferencing & Context
ui/src/lib/schema-utils.ts, ui/src/contexts/SchemaContext.tsx
Extracted dereferenceSchema function into shared utility library to resolve internal $ref pointers. Extended JSONSchema interface with additional metadata fields ($id, markdownDescription, enumDescriptions, patternProperties, propertyNames). Refactored SchemaContext to use imported utility instead of local implementation.
Custom Step Type Schema Utilities
ui/src/features/dags/components/dag-editor/customStepSchema.ts
Added comprehensive module for merging inherited and local custom step type hints, extracting step types from YAML content, and building augmented DAG schemas. Implements recursive schema augmentation with custom type conditional rules and enum injection, including $ref rewriting within custom input schemas.
Editor Component Refactoring
ui/src/features/dags/components/dag-editor/DAGEditor.tsx
Refactored Monaco YAML schema handling from module-level static registration to dynamic, per-document schema management. Added modelUri and schema props to enable document-specific schema association via computed inmemory://dagu-schema/ URIs registered with monacoYaml.update.
DAG Editor Integration
ui/src/features/dags/components/dag-editor/DAGSpec.tsx, DAGEditorWithDocs.tsx, DAGSpecReadOnly.tsx
Extended DAGSpec to accept and synthesize editor hints into augmented schema by merging inherited hints with locally parsed custom types. Threaded editorHints and modelUri props through DAGEditorWithDocs and down to child DAGEditor. Updated DAGSpecReadOnly to pass model URIs for read-only editor instances.
Schema Sidebar & Hooks
ui/src/features/dags/components/dag-editor/SchemaDocSidebar/SchemaDocSidebar.tsx, ui/src/hooks/useSchemaLookup.ts
Added optional schema prop to SchemaDocSidebar and useSchemaLookup to support override-based schema lookup. Updated sidebar to use override schema when available, forcing loading: false and error: null when schema is provided directly. Refactored property rendering to use active schema.
Page & Component Integration
ui/src/pages/dags/dag/index.tsx, ui/src/features/dags/components/dag-details/DAGDetailsContent.tsx, ui/src/pages/base-config/index.tsx
Threaded editorHints through the DAG details page and content component down to the spec tab. Updated base config editor to use model URI for consistent schema registration. Updated DAGEditorWithDocs prop logic for clarity.
UI Tests
ui/src/features/dags/components/dag-editor/__tests__/customStepSchema.test.ts
Added comprehensive test suite validating custom step schema extraction, merging, and augmentation. Tests verify hint parsing, override behavior, schema building, nested $ref resolution, and error handling for invalid YAML.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Browser/UI
    participant Service as Frontend Service
    participant Spec as Spec Utils
    participant Editor as DAG Editor
    participant Monaco as Monaco YAML

    Browser->>Service: GET /dags/{fileName}
    Service->>Service: Read DAG file
    Service->>Service: Get BaseConfig bytes
    Service->>Spec: InheritedCustomStepTypeEditorHints(baseConfig)
    Spec->>Spec: Decode & extract custom step types
    Spec-->>Service: Return editor hints
    Service-->>Browser: GetDAGDetails response + editorHints

    Browser->>Editor: Render DAGSpec with editorHints
    Editor->>Editor: Extract local custom step types from YAML
    Editor->>Editor: mergeCustomStepTypeHints(inherited, local)
    Editor->>Editor: buildAugmentedDAGSchema(baseSchema, merged)
    Editor->>Monaco: Register dynamic schema (modelUri, augmentedSchema)
    Monaco-->>Editor: Schema validation/completion configured

    Browser->>Editor: User edits YAML content
    Monaco->>Monaco: Validate against augmented schema
    Monaco-->>Browser: Show custom type suggestions & validation
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(ui): add custom step editor autocomplete' directly and clearly summarizes the primary feature being added—custom step editor autocomplete for the UI.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch custom-step-autocomplete

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/core/spec/editor_hints.go`:
- Around line 56-58: The current guard that does "if entry == nil ||
entry.InputSchema == nil || entry.InputSchema.Schema() == nil { continue }"
drops inherited custom step types when InputSchema is missing; instead preserve
the hint and treat a missing schema as an empty schema object: locate the block
that checks entry, InputSchema and InputSchema.Schema() (variables/methods:
entry, InputSchema, Schema()) and replace the unconditional continue with logic
that constructs or assigns an empty schema (e.g., zero-value schema/map/struct)
when InputSchema or Schema() is nil, then proceed to include the hint in editor
type suggestions so inherited step types are not removed.

In `@internal/service/frontend/api/v1/dags.go`:
- Around line 471-473: The loop is taking the address of the range variable
field (&hint.Description), which leads to all pointers referencing the same
reused stack slot; instead, create a new local variable per iteration and take
its address (e.g., desc := hint.Description; apiHint.Description = &desc) or
allocate a new string pointer and assign it, ensuring each apiHint.Description
points to a distinct copy; update the code around the hint -> apiHint mapping
where Description is assigned to use this per-iteration local copy.

In
`@ui/src/features/dags/components/dag-editor/__tests__/customStepSchema.test.ts`:
- Around line 1-12: This new test file is missing the repository GPLv3 license
header; run the repository license tool (make addlicense) or insert the standard
GPL v3 header at the top of
ui/src/features/dags/components/dag-editor/__tests__/customStepSchema.test.ts
(the new test file importing buildAugmentedDAGSchema,
extractLocalCustomStepTypeHints, mergeCustomStepTypeHints, and schema-utils
symbols) so the header precedes the imports; prefer running `make addlicense` to
apply the correct header consistently.

In `@ui/src/features/dags/components/dag-editor/customStepSchema.ts`:
- Around line 1-4: This new TypeScript file (imports: components, parseYaml,
dereferenceSchema, JSONSchema) is missing the repository GPL v3 header; run the
repository tool to add it by executing `make addlicense` (or the project’s
license tooling) so the standard GPL header is prepended to
ui/src/features/dags/components/dag-editor/customStepSchema.ts, then re-stage
the file and update the PR.

In `@ui/src/features/dags/components/dag-editor/DAGSpec.tsx`:
- Around line 153-179: The parsedLocalStepTypes value currently contains a new
array identity on every edit which invalidates the editorSchema memo; to fix
this, stabilize effectiveLocalStepTypes by comparing extracted step-type hints
by value and only replacing the previous array when the content actually changes
(e.g., compute parsedLocalStepTypes via extractLocalCustomStepTypeHints, then
deep-equal its stepTypes to lastGoodLocalStepTypes and reuse the previous array
identity when equal). Update the logic around parsedLocalStepTypes,
setLastGoodLocalStepTypes, and effectiveLocalStepTypes so
buildAugmentedDAGSchema( baseSchema,
mergeCustomStepTypeHints(inheritedCustomStepTypes, effectiveLocalStepTypes) )
only reruns when the hint set's content changes, preserving identity to avoid
re-registering the Monaco schema.
- Around line 181-183: The Monaco model URI generation in editorModelUri
currently only uses fileName, causing models to be shared across remote nodes;
update the memoized value in editorModelUri to include the remoteNode (e.g.,
`${encodeURIComponent(fileName)}:${remoteNode}` or similar) so the in-memory URI
is namespaced per node, ensuring model/schema registrations, undo history, and
transient edits are isolated per remoteNode; reference the editorModelUri
variable and the existing useContentEditor keying to `${fileName}:${remoteNode}`
when making this change.

In `@ui/src/lib/schema-utils.ts`:
- Around line 94-125: The dereferenceSchema/processNode flow currently only
stores a resolved ref in cache after recursively processing it, causing infinite
recursion for self-referential internal $ref; fix by inserting a placeholder
into cache immediately when encountering a local ref (use the ref string as key
and a temporary empty object or sentinel JSONSchema), then call
resolveInternalRef and processNode to fill the placeholder, and finally
replace/update the cache entry with the fully-processed schema before merging
with rest. Update the logic around the `$ref` handling in dereferenceSchema
(processNode, cache, resolveInternalRef) so the cache is populated prior to
recursive processing to break cycles.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9165226d-45f2-4a30-ab92-45add989e362

📥 Commits

Reviewing files that changed from the base of the PR and between 56348b8 and 83996d2.

📒 Files selected for processing (19)
  • api/v1/api.gen.go
  • api/v1/api.yaml
  • internal/core/spec/editor_hints.go
  • internal/service/frontend/api/v1/dags.go
  • internal/service/frontend/api/v1/dags_internal_test.go
  • ui/src/api/v1/schema.ts
  • ui/src/contexts/SchemaContext.tsx
  • ui/src/features/dags/components/dag-details/DAGDetailsContent.tsx
  • ui/src/features/dags/components/dag-editor/DAGEditor.tsx
  • ui/src/features/dags/components/dag-editor/DAGEditorWithDocs.tsx
  • ui/src/features/dags/components/dag-editor/DAGSpec.tsx
  • ui/src/features/dags/components/dag-editor/DAGSpecReadOnly.tsx
  • ui/src/features/dags/components/dag-editor/SchemaDocSidebar/SchemaDocSidebar.tsx
  • ui/src/features/dags/components/dag-editor/__tests__/customStepSchema.test.ts
  • ui/src/features/dags/components/dag-editor/customStepSchema.ts
  • ui/src/hooks/useSchemaLookup.ts
  • ui/src/lib/schema-utils.ts
  • ui/src/pages/base-config/index.tsx
  • ui/src/pages/dags/dag/index.tsx

Comment thread internal/core/spec/editor_hints.go Outdated
Comment on lines +56 to +58
if entry == nil || entry.InputSchema == nil || entry.InputSchema.Schema() == nil {
continue
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not drop inherited custom step types when InputSchema is absent.

At Line 56, the guard currently continues when schema is missing, which can remove otherwise valid inherited step types from editor type suggestions. Keep the hint and use an empty schema object instead.

Suggested fix
-		if entry == nil || entry.InputSchema == nil || entry.InputSchema.Schema() == nil {
+		if entry == nil {
 			continue
 		}
 
-		schemaData, err := json.Marshal(entry.InputSchema.Schema())
-		if err != nil {
-			return nil, fmt.Errorf("marshal input schema for %q: %w", entry.Name, err)
-		}
-
-		var schemaMap map[string]any
-		if err := json.Unmarshal(schemaData, &schemaMap); err != nil {
-			return nil, fmt.Errorf("unmarshal input schema for %q: %w", entry.Name, err)
-		}
+		schemaMap := map[string]any{}
+		if entry.InputSchema != nil && entry.InputSchema.Schema() != nil {
+			schemaData, err := json.Marshal(entry.InputSchema.Schema())
+			if err != nil {
+				return nil, fmt.Errorf("marshal input schema for %q: %w", entry.Name, err)
+			}
+			if err := json.Unmarshal(schemaData, &schemaMap); err != nil {
+				return nil, fmt.Errorf("unmarshal input schema for %q: %w", entry.Name, err)
+			}
+		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/core/spec/editor_hints.go` around lines 56 - 58, The current guard
that does "if entry == nil || entry.InputSchema == nil ||
entry.InputSchema.Schema() == nil { continue }" drops inherited custom step
types when InputSchema is missing; instead preserve the hint and treat a missing
schema as an empty schema object: locate the block that checks entry,
InputSchema and InputSchema.Schema() (variables/methods: entry, InputSchema,
Schema()) and replace the unconditional continue with logic that constructs or
assigns an empty schema (e.g., zero-value schema/map/struct) when InputSchema or
Schema() is nil, then proceed to include the hint in editor type suggestions so
inherited step types are not removed.

Comment on lines +471 to +473
if hint.Description != "" {
apiHint.Description = &hint.Description
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the exact risky pattern exists in the reviewed file.
rg -nP --type=go -C2 '&\s*hint\.Description' internal/service/frontend/api/v1/dags.go
# Inspect loop context around the match.
rg -nP --type=go -C6 'for\s+_,\s+hint\s*:=\s*range\s+hints' internal/service/frontend/api/v1/dags.go

Repository: dagucloud/dagu

Length of output: 572


Avoid taking the address of a range-loop variable field.

At line 472, &hint.Description is taken from the loop variable hint. Since hint is reused across iterations in the for-range loop, the pointer will reference stale or uninitialized storage after the loop completes. All pointers in the returned slice will end up pointing to the last iteration's value (or invalid memory).

Suggested fix
 		if hint.Description != "" {
-			apiHint.Description = &hint.Description
+			desc := hint.Description
+			apiHint.Description = &desc
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/service/frontend/api/v1/dags.go` around lines 471 - 473, The loop is
taking the address of the range variable field (&hint.Description), which leads
to all pointers referencing the same reused stack slot; instead, create a new
local variable per iteration and take its address (e.g., desc :=
hint.Description; apiHint.Description = &desc) or allocate a new string pointer
and assign it, ensuring each apiHint.Description points to a distinct copy;
update the code around the hint -> apiHint mapping where Description is assigned
to use this per-iteration local copy.

Comment on lines +1 to +12
import { describe, expect, it } from 'vitest';
import {
buildAugmentedDAGSchema,
extractLocalCustomStepTypeHints,
mergeCustomStepTypeHints,
} from '../customStepSchema';
import {
dereferenceSchema,
getSchemaAtPath,
toPropertyInfo,
type JSONSchema,
} from '@/lib/schema-utils';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Run make addlicense for this new source file.

This file is new and still missing the repository-mandated GPL header.

As per coding guidelines, **/*.{go,ts,tsx,js}: Apply GPL v3 license headers on source files, managed via make addlicense.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ui/src/features/dags/components/dag-editor/__tests__/customStepSchema.test.ts`
around lines 1 - 12, This new test file is missing the repository GPLv3 license
header; run the repository license tool (make addlicense) or insert the standard
GPL v3 header at the top of
ui/src/features/dags/components/dag-editor/__tests__/customStepSchema.test.ts
(the new test file importing buildAugmentedDAGSchema,
extractLocalCustomStepTypeHints, mergeCustomStepTypeHints, and schema-utils
symbols) so the header precedes the imports; prefer running `make addlicense` to
apply the correct header consistently.

Comment on lines +1 to +4
import type { components } from '../../../../api/v1/schema';
import { parse as parseYaml } from 'yaml';
import { dereferenceSchema, type JSONSchema } from '@/lib/schema-utils';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Run make addlicense for this new source file.

This file is new and still missing the repository-mandated GPL header.

As per coding guidelines, **/*.{go,ts,tsx,js}: Apply GPL v3 license headers on source files, managed via make addlicense.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/features/dags/components/dag-editor/customStepSchema.ts` around lines
1 - 4, This new TypeScript file (imports: components, parseYaml,
dereferenceSchema, JSONSchema) is missing the repository GPL v3 header; run the
repository tool to add it by executing `make addlicense` (or the project’s
license tooling) so the standard GPL header is prepended to
ui/src/features/dags/components/dag-editor/customStepSchema.ts, then re-stage
the file and update the PR.

Comment on lines +131 to +157
function isStepLikeSchema(schema: JSONSchema): boolean {
const properties = schema.properties;
return (
schema.type === 'object' &&
!!properties &&
isRecord(properties.type) &&
isRecord(properties.config)
);
}

function augmentStepSchema(
stepSchema: JSONSchema,
customStepRules: JSONSchema[],
customTypeNames: string[],
customTypeDescriptions: string[]
) {
stepSchema.allOf = appendUniqueAllOf(stepSchema.allOf, customStepRules);

const typeSchema = stepSchema.properties?.type;
if (isRecord(typeSchema)) {
augmentExecutorTypeSchema(
typeSchema as JSONSchema,
customTypeNames,
customTypeDescriptions
);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Limit post-dereference augmentation to actual DAG step schemas.

visitSchemas() walks the user-provided inputSchema trees too, and isStepLikeSchema() matches any object that happens to define properties.type and properties.config. A nested config object with those common field names will start getting executor-type completions and custom config switching even though it is not a DAG step.

Also applies to: 159-176, 350-359

Comment on lines +153 to +179
const parsedLocalStepTypes = React.useMemo(
() => extractLocalCustomStepTypeHints(currentValue ?? serverSpec ?? ''),
[currentValue, serverSpec]
);

useEffect(() => {
if (parsedLocalStepTypes.ok) {
setLastGoodLocalStepTypes(parsedLocalStepTypes.stepTypes);
}
}, [parsedLocalStepTypes]);

const effectiveLocalStepTypes = parsedLocalStepTypes.ok
? parsedLocalStepTypes.stepTypes
: lastGoodLocalStepTypes;

const editorSchema = React.useMemo(() => {
if (!baseSchema) {
return null;
}
return buildAugmentedDAGSchema(
baseSchema,
mergeCustomStepTypeHints(
inheritedCustomStepTypes,
effectiveLocalStepTypes
)
);
}, [baseSchema, effectiveLocalStepTypes, inheritedCustomStepTypes]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep editorSchema stable when the extracted hint set hasn't changed.

extractLocalCustomStepTypeHints() returns a fresh array on every edit, so this memo invalidates even when the user only touched an unrelated field. That forces buildAugmentedDAGSchema() to clone/dereference the full schema again and makes DAGEditor re-register the Monaco schema on every keystroke.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/features/dags/components/dag-editor/DAGSpec.tsx` around lines 153 -
179, The parsedLocalStepTypes value currently contains a new array identity on
every edit which invalidates the editorSchema memo; to fix this, stabilize
effectiveLocalStepTypes by comparing extracted step-type hints by value and only
replacing the previous array when the content actually changes (e.g., compute
parsedLocalStepTypes via extractLocalCustomStepTypeHints, then deep-equal its
stepTypes to lastGoodLocalStepTypes and reuse the previous array identity when
equal). Update the logic around parsedLocalStepTypes, setLastGoodLocalStepTypes,
and effectiveLocalStepTypes so buildAugmentedDAGSchema( baseSchema,
mergeCustomStepTypeHints(inheritedCustomStepTypes, effectiveLocalStepTypes) )
only reruns when the hint set's content changes, preserving identity to avoid
re-registering the Monaco schema.

Comment on lines +181 to +183
const editorModelUri = React.useMemo(
() => `inmemory://dagu/dags/${encodeURIComponent(fileName)}.yaml`,
[fileName]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Namespace the Monaco model URI by remoteNode.

useContentEditor already keys drafts by ${fileName}:${remoteNode}, but Monaco is still keyed only by fileName. In a multi-node deployment, opening the same DAG name on two nodes will reuse one in-memory model/schema registration and leak undo history or transient edits across nodes.

Suggested fix
   const editorModelUri = React.useMemo(
-    () => `inmemory://dagu/dags/${encodeURIComponent(fileName)}.yaml`,
-    [fileName]
+    () =>
+      `inmemory://dagu/${encodeURIComponent(remoteNode)}/dags/${encodeURIComponent(fileName)}.yaml`,
+    [fileName, remoteNode]
   );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const editorModelUri = React.useMemo(
() => `inmemory://dagu/dags/${encodeURIComponent(fileName)}.yaml`,
[fileName]
const editorModelUri = React.useMemo(
() =>
`inmemory://dagu/${encodeURIComponent(remoteNode)}/dags/${encodeURIComponent(fileName)}.yaml`,
[fileName, remoteNode]
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/features/dags/components/dag-editor/DAGSpec.tsx` around lines 181 -
183, The Monaco model URI generation in editorModelUri currently only uses
fileName, causing models to be shared across remote nodes; update the memoized
value in editorModelUri to include the remoteNode (e.g.,
`${encodeURIComponent(fileName)}:${remoteNode}` or similar) so the in-memory URI
is namespaced per node, ensuring model/schema registrations, undo history, and
transient edits are isolated per remoteNode; reference the editorModelUri
variable and the existing useContentEditor keying to `${fileName}:${remoteNode}`
when making this change.

Comment on lines +94 to +125
export function dereferenceSchema(schema: JSONSchema): JSONSchema {
const cache = new Map<string, JSONSchema>();

function processNode(node: unknown): unknown {
if (!node || typeof node !== 'object') {
return node;
}

if (Array.isArray(node)) {
return node.map(processNode);
}

const obj = node as Record<string, unknown>;
if (typeof obj.$ref === 'string' && obj.$ref.startsWith('#')) {
const ref = obj.$ref;
let resolved = cache.get(ref);
if (!resolved) {
resolved = processNode(resolveInternalRef(schema, ref)) as JSONSchema;
cache.set(ref, resolved);
}
const { $ref: _ref, ...rest } = obj;
return processNode({ ...resolved, ...rest });
}

const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
result[key] = processNode(value);
}
return result;
}

return processNode(schema) as JSONSchema;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Break recursive internal $ref cycles before expanding them.

The cache is populated only after the referenced node is fully processed. A self-referential custom step_types.*.input_schema ($ref: '#' or a definition that points back to itself) will recurse until the stack blows up here.

One way to guard recursive refs
 export function dereferenceSchema(schema: JSONSchema): JSONSchema {
   const cache = new Map<string, JSONSchema>();
+  const resolving = new Set<string>();

   function processNode(node: unknown): unknown {
     if (!node || typeof node !== 'object') {
       return node;
     }
@@
     const obj = node as Record<string, unknown>;
     if (typeof obj.$ref === 'string' && obj.$ref.startsWith('#')) {
       const ref = obj.$ref;
       let resolved = cache.get(ref);
       if (!resolved) {
+        if (resolving.has(ref)) {
+          return { ...obj };
+        }
+        resolving.add(ref);
         resolved = processNode(resolveInternalRef(schema, ref)) as JSONSchema;
+        resolving.delete(ref);
         cache.set(ref, resolved);
       }
       const { $ref: _ref, ...rest } = obj;
       return processNode({ ...resolved, ...rest });
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function dereferenceSchema(schema: JSONSchema): JSONSchema {
const cache = new Map<string, JSONSchema>();
function processNode(node: unknown): unknown {
if (!node || typeof node !== 'object') {
return node;
}
if (Array.isArray(node)) {
return node.map(processNode);
}
const obj = node as Record<string, unknown>;
if (typeof obj.$ref === 'string' && obj.$ref.startsWith('#')) {
const ref = obj.$ref;
let resolved = cache.get(ref);
if (!resolved) {
resolved = processNode(resolveInternalRef(schema, ref)) as JSONSchema;
cache.set(ref, resolved);
}
const { $ref: _ref, ...rest } = obj;
return processNode({ ...resolved, ...rest });
}
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
result[key] = processNode(value);
}
return result;
}
return processNode(schema) as JSONSchema;
export function dereferenceSchema(schema: JSONSchema): JSONSchema {
const cache = new Map<string, JSONSchema>();
const resolving = new Set<string>();
function processNode(node: unknown): unknown {
if (!node || typeof node !== 'object') {
return node;
}
if (Array.isArray(node)) {
return node.map(processNode);
}
const obj = node as Record<string, unknown>;
if (typeof obj.$ref === 'string' && obj.$ref.startsWith('#')) {
const ref = obj.$ref;
let resolved = cache.get(ref);
if (!resolved) {
if (resolving.has(ref)) {
return { ...obj };
}
resolving.add(ref);
resolved = processNode(resolveInternalRef(schema, ref)) as JSONSchema;
resolving.delete(ref);
cache.set(ref, resolved);
}
const { $ref: _ref, ...rest } = obj;
return processNode({ ...resolved, ...rest });
}
const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
result[key] = processNode(value);
}
return result;
}
return processNode(schema) as JSONSchema;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/lib/schema-utils.ts` around lines 94 - 125, The
dereferenceSchema/processNode flow currently only stores a resolved ref in cache
after recursively processing it, causing infinite recursion for self-referential
internal $ref; fix by inserting a placeholder into cache immediately when
encountering a local ref (use the ref string as key and a temporary empty object
or sentinel JSONSchema), then call resolveInternalRef and processNode to fill
the placeholder, and finally replace/update the cache entry with the
fully-processed schema before merging with rest. Update the logic around the
`$ref` handling in dereferenceSchema (processNode, cache, resolveInternalRef) so
the cache is populated prior to recursive processing to break cycles.

@yottahmd yottahmd merged commit 229c9cd into main Apr 11, 2026
4 checks passed
@yottahmd yottahmd deleted the custom-step-autocomplete branch April 11, 2026 12:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant