Add has_intermediate_output flag for nodes with interactive UI#13048
Add has_intermediate_output flag for nodes with interactive UI#13048
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
✅ Files skipped from review due to trivial changes (4)
📝 WalkthroughWalkthroughIntroduces a schema-level boolean 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@comfy_api/latest/_io.py`:
- Around line 1469-1478: The new has_intermediate_output field in IO.Schema
shifts positional parameters and breaks source-compatibility for third-party
nodes using positional IO.Schema(...) calls; to fix, make
has_intermediate_output keyword-only or move it to the end of the
dataclass/fields list so existing positional arguments keep their
meanings—specifically update the IO.Schema declaration to declare
has_intermediate_output with field(kw_only=True) (or place it after
is_output_node and other existing fields) so existing callers of IO.Schema and
parameters like is_output_node remain unaffected.
In `@server.py`:
- Around line 712-713: The branch that sets info['has_intermediate_output'] only
when obj_class.HAS_INTERMEDIATE_OUTPUT is True causes inconsistent shapes
between legacy nodes and GET_NODE_INFO_V1; change the logic so info always
contains the key (set info['has_intermediate_output'] = bool(getattr(obj_class,
'HAS_INTERMEDIATE_OUTPUT', False))) or initialize
info['has_intermediate_output'] = False then set True when the attribute exists
on obj_class, ensuring every node returned by /object_info includes the
has_intermediate_output field consistently.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 487a4d40-f012-4775-a855-3829410f7b4c
📒 Files selected for processing (5)
comfy_api/latest/_io.pycomfy_execution/graph.pycomfy_extras/nodes_images.pycomfy_extras/nodes_painter.pyserver.py
| has_intermediate_output: bool=False | ||
| """Flags this node as having intermediate output that should persist across page refreshes. | ||
| Nodes with this flag behave like output nodes (their UI results are cached and resent | ||
| to the frontend) but do NOT automatically get added to the execution list. This means | ||
| they will only execute if they are on the dependency path of a real output node. | ||
| Use this for nodes with interactive/operable UI regions that produce intermediate outputs | ||
| (e.g., Image Crop, Painter) rather than final outputs (e.g., Save Image). | ||
| """ |
There was a problem hiding this comment.
❓ Verification inconclusive
Script executed:
#!/bin/bash
python - <<'PY'
import ast
from pathlib import Path
class Visitor(ast.NodeVisitor):
def __init__(self, path: Path):
self.path = path
def visit_Call(self, node: ast.Call):
func = node.func
is_schema = (
isinstance(func, ast.Name) and func.id == "Schema"
) or (
isinstance(func, ast.Attribute) and func.attr == "Schema"
)
if is_schema and node.args:
print(f"{self.path}:{node.lineno}: positional_args={len(node.args)}")
self.generic_visit(node)
for path in Path(".").rglob("*.py"):
if any(part in {".git", "__pycache__", ".venv", "venv", "node_modules"} for part in path.parts):
continue
try:
tree = ast.parse(path.read_text(encoding="utf-8"))
except Exception:
continue
Visitor(path).visit(tree)
PYRepository: Comfy-Org/ComfyUI
Repository: Comfy-Org/ComfyUI
Exit code: 0
Script executed:
# Find the Schema class definition and inspect field order
grep -n "class Schema" comfy_api/latest/_io.pyRepository: Comfy-Org/ComfyUI
Repository: Comfy-Org/ComfyUI
Exit code: 0
stdout:
1431:class Schema:
Script executed:
# Examine the Schema class fields around line 1469-1478 and surrounding context
sed -n '1350,1490p' comfy_api/latest/_io.py | cat -nRepository: Comfy-Org/ComfyUI
Repository: Comfy-Org/ComfyUI
Exit code: 0
stdout:
1 name: str=None
2 display_name: str=None
3 description: str=None
4 python_module: Any=None
5 category: str=None
6 output_node: bool=None
7 has_intermediate_output: bool=None
8 deprecated: bool=None
9 experimental: bool=None
10 dev_only: bool=None
11 api_node: bool=None
12 price_badge: dict | None = None
13 search_aliases: list[str]=None
14 essentials_category: str=None
15
16
17 `@dataclass`
18 class PriceBadgeDepends:
19 widgets: list[str] = field(default_factory=list)
20 inputs: list[str] = field(default_factory=list)
21 input_groups: list[str] = field(default_factory=list)
22
23 def validate(self) -> None:
24 if not isinstance(self.widgets, list) or any(not isinstance(x, str) for x in self.widgets):
25 raise ValueError("PriceBadgeDepends.widgets must be a list[str].")
26 if not isinstance(self.inputs, list) or any(not isinstance(x, str) for x in self.inputs):
27 raise ValueError("PriceBadgeDepends.inputs must be a list[str].")
28 if not isinstance(self.input_groups, list) or any(not isinstance(x, str) for x in self.input_groups):
29 raise ValueError("PriceBadgeDepends.input_groups must be a list[str].")
30
31 def as_dict(self, schema_inputs: list["Input"]) -> dict[str, Any]:
32 # Build lookup: widget_id -> io_type
33 input_types: dict[str, str] = {}
34 for inp in schema_inputs:
35 all_inputs = inp.get_all()
36 input_types[inp.id] = inp.get_io_type() # First input is always the parent itself
37 for nested_inp in all_inputs[1:]:
38 # For DynamicCombo/DynamicSlot, nested inputs are prefixed with parent ID
39 # to match frontend naming convention (e.g., "should_texture.enable_pbr")
40 prefixed_id = f"{inp.id}.{nested_inp.id}"
41 input_types[prefixed_id] = nested_inp.get_io_type()
42
43 # Enrich widgets with type information, raising error for unknown widgets
44 widgets_data: list[dict[str, str]] = []
45 for w in self.widgets:
46 if w not in input_types:
47 raise ValueError(
48 f"PriceBadge depends_on.widgets references unknown widget '{w}'. "
49 f"Available widgets: {list(input_types.keys())}"
50 )
51 widgets_data.append({"name": w, "type": input_types[w]})
52
53 return {
54 "widgets": widgets_data,
55 "inputs": self.inputs,
56 "input_groups": self.input_groups,
57 }
58
59
60 `@dataclass`
61 class PriceBadge:
62 expr: str
63 depends_on: PriceBadgeDepends = field(default_factory=PriceBadgeDepends)
64 engine: str = field(default="jsonata")
65
66 def validate(self) -> None:
67 if self.engine != "jsonata":
68 raise ValueError(f"Unsupported PriceBadge.engine '{self.engine}'. Only 'jsonata' is supported.")
69 if not isinstance(self.expr, str) or not self.expr.strip():
70 raise ValueError("PriceBadge.expr must be a non-empty string.")
71 self.depends_on.validate()
72
73 def as_dict(self, schema_inputs: list["Input"]) -> dict[str, Any]:
74 return {
75 "engine": self.engine,
76 "depends_on": self.depends_on.as_dict(schema_inputs),
77 "expr": self.expr,
78 }
79
80
81 `@dataclass`
82 class Schema:
83 """Definition of V3 node properties."""
84
85 node_id: str
86 """ID of node - should be globally unique. If this is a custom node, add a prefix or postfix to avoid name clashes."""
87 display_name: str = None
88 """Display name of node."""
89 category: str = "sd"
90 """The category of the node, as per the "Add Node" menu."""
91 inputs: list[Input] = field(default_factory=list)
92 outputs: list[Output] = field(default_factory=list)
93 hidden: list[Hidden] = field(default_factory=list)
94 description: str=""
95 """Node description, shown as a tooltip when hovering over the node."""
96 search_aliases: list[str] = field(default_factory=list)
97 """Alternative names for search. Useful for synonyms, abbreviations, or old names after renaming."""
98 is_input_list: bool = False
99 """A flag indicating if this node implements the additional code necessary to deal with OUTPUT_IS_LIST nodes.
100
101 All inputs of ``type`` will become ``list[type]``, regardless of how many items are passed in. This also affects ``check_lazy_status``.
102
103 From the docs:
104
105 A node can also override the default input behaviour and receive the whole list in a single call. This is done by setting a class attribute `INPUT_IS_LIST` to ``True``.
106
107 Comfy Docs: https://docs.comfy.org/custom-nodes/backend/lists#list-processing
108 """
109 is_output_node: bool=False
110 """Flags this node as an output node, causing any inputs it requires to be executed.
111
112 If a node is not connected to any output nodes, that node will not be executed. Usage::
113
114 From the docs:
115
116 By default, a node is not considered an output. Set ``OUTPUT_NODE = True`` to specify that it is.
117
118 Comfy Docs: https://docs.comfy.org/custom-nodes/backend/server_overview#output-node
119 """
120 has_intermediate_output: bool=False
121 """Flags this node as having intermediate output that should persist across page refreshes.
122
123 Nodes with this flag behave like output nodes (their UI results are cached and resent
124 to the frontend) but do NOT automatically get added to the execution list. This means
125 they will only execute if they are on the dependency path of a real output node.
126
127 Use this for nodes with interactive/operable UI regions that produce intermediate outputs
128 (e.g., Image Crop, Painter) rather than final outputs (e.g., Save Image).
129 """
130 is_deprecated: bool=False
131 """Flags a node as deprecated, indicating to users that they should find alternatives to this node."""
132 is_experimental: bool=False
133 """Flags a node as experimental, informing users that it may change or not work as expected."""
134 is_dev_only: bool=False
135 """Flags a node as dev-only, hiding it from search/menus unless dev mode is enabled."""
136 is_api_node: bool=False
137 """Flags a node as an API node. See: https://docs.comfy.org/tutorials/api-nodes/overview."""
138 price_badge: PriceBadge | None = None
139 """Optional client-evaluated pricing badge declaration for this node."""
140 not_idempotent: bool=False
141 """Flags a node as not idempotent; when True, the node will run and not reuse the cached outputs when identical inputs are provided on a different node in the graph."""
Keep IO.Schema positional construction source-compatible.
Schema is a public node API. Inserting has_intermediate_output shifts every positional argument after is_output_node, so third-party nodes that call IO.Schema(...) positionally will silently bind arguments to the wrong parameters.
Consider appending has_intermediate_output at the end of the field list, or make it keyword-only via field(kw_only=True), to preserve source compatibility with external custom nodes.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@comfy_api/latest/_io.py` around lines 1469 - 1478, The new
has_intermediate_output field in IO.Schema shifts positional parameters and
breaks source-compatibility for third-party nodes using positional
IO.Schema(...) calls; to fix, make has_intermediate_output keyword-only or move
it to the end of the dataclass/fields list so existing positional arguments keep
their meanings—specifically update the IO.Schema declaration to declare
has_intermediate_output with field(kw_only=True) (or place it after
is_output_node and other existing fields) so existing callers of IO.Schema and
parameters like is_output_node remain unaffected.
has_intermediate_output flag for nodes with interactive UI5cd11fd to
fbc3b0f
Compare
Nodes like Image Crop have interactive UI regions that produce intermediate outputs. Previously, these outputs were lost on page refresh because cached intermediate nodes were skipped during execution list building.
The new
has_intermediate_outputflag behaves likeOUTPUT_NODE(cached UI is resent to the frontend) but does not automatically seed the execution list, so it won't force evaluation of unevaluated branches (e.g. Switch).Add the flag to ImageCropV2 and Painter as the first use case.
before
https://github.com/user-attachments/assets/4b6b3def-abee-49a2-b5a4-42fafafd8fd6
after
2026-03-18.15-06-16.mp4