Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions comfy_cli/command/workflow_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,14 @@ def add_node_cmd(
"--allow-deprecated", show_default=False, help="Add the node even though the catalog marks it deprecated."
),
] = False,
text: Annotated[
Comment thread
christian-byrne marked this conversation as resolved.
str | None,
typer.Option(
"--text",
show_default=False,
help="Body text for a Note / MarkdownNote (annotation nodes; no catalog entry). Invalid for other classes.",
),
] = None,
actor: ActorOpt = "cli",
base_version: BaseVersionOpt = 0,
stdout: StdoutOpt = False,
Expand Down Expand Up @@ -232,6 +240,7 @@ def add_node_cmd(
actor=actor,
base_version=base_version,
allow_deprecated=allow_deprecated,
text=text,
)
except workflow_ops.DeprecatedNodeType as e:
renderer.error(
Expand All @@ -245,10 +254,11 @@ def add_node_cmd(
# Same envelope shape as `nodes show` so a caller can self-correct from
# the error alone. (The old hint pointed at `comfy nodes types`, which
# lists connection types — MODEL/LATENT/IMAGE — not class_types.)
if e.ui_only and e.class_type in workflow_ops.NOTE_NODE_TYPES:
hint = workflow_ops.note_insert_hint(e.class_type)
elif e.ui_only:
hint = "use a real node class; to annotate the graph, set a title/widget on an existing node instead"
if e.ui_only:
hint = (
"use a real node class; to annotate the graph, add a `Note` or `MarkdownNote` with --text "
"(the only UI-only classes this surface can mint)"
)
elif e.subgraph_id:
hint = "pick a node CLASS from `comfy nodes search <text>`; a subgraph instance cannot be added"
elif e.close_matches:
Expand Down
5 changes: 3 additions & 2 deletions comfy_cli/error_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -720,8 +720,9 @@ class ErrorCode:
ErrorCode(
"ui_only_node_skipped",
"Warning (not fatal): `workflow capture` skipped a UI-only node (Note/MarkdownNote/"
"Reroute/GetNode/SetNode/PrimitiveNode) — those never reach the API and `apply` "
"refuses to mint them. Data flow through the node was spliced to the real source.",
"Reroute/GetNode/SetNode/PrimitiveNode) — those never reach the API, so capture "
"omits them by design even though `apply` can mint Note/MarkdownNote. Data flow "
"through the node was spliced to the real source.",
"expected for annotated workflows; the recipe rebuilds the executable graph, not canvas decoration",
),
ErrorCode(
Expand Down
30 changes: 28 additions & 2 deletions comfy_cli/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
MIN_H = 60.0
ORIGIN = (40.0, 60.0)
DEFAULT_SIZE = (210.0, 100.0)
# The two UI-only node types add_node can mint without a catalog entry: pure
# annotation nodes with no slots and one positional multiline `text` widget.
# Defined here (the leaf module) so both the planner and workflow_ops read one
# constant; workflow_ops re-exports it. The full UI-only set lives in
# workflow_ops.UI_ONLY_NODE_TYPES — this is the authorable subset of it.
AUTHORABLE_VIRTUAL_NODE_TYPES = frozenset({"Note", "MarkdownNote"})
_MARGIN = 10.0
_GUARD = 1000 # bounded collision-shift loop
_ORDER_SWEEPS = 4 # barycentre passes for crossing reduction; converges well before this
Expand Down Expand Up @@ -259,6 +265,18 @@ def estimate_size(
return [w, max(h, MIN_H)]


def note_size(class_type: str) -> list[float]:
"""Persisted size for an authorable annotation node (`Note` / `MarkdownNote`).

These have no catalog entry, no slots, and one multiline text widget, so the
catalog-driven path in `estimate_size` cannot be fed from `graph.node`. Both
`workflow_ops.add_node` and `assign_positions` call this so the planner's
footprint and the size stamped onto the node agree — the same invariant the
catalog path keeps by calling `estimate_size` with identical arguments.
"""
return estimate_size(0, 0, 1, n_multiline=1, title=class_type, widget_labels=("text",))


def _pair(value) -> tuple[float, float] | None:
"""Read a geometry pair, tolerating both shapes litegraph serialises.

Expand Down Expand Up @@ -398,8 +416,16 @@ def assign_positions(workflow: dict, graph, specs: list) -> list:
for i, spec in enumerate(out):
if not (isinstance(spec, dict) and spec.get("op") == "add_node"):
continue
m = graph.node(spec.get("class_type") or "")
if m is not None:
class_type = spec.get("class_type")
# Name decides before the catalog (mirrors workflow_ops.add_node): a
# backend class that happens to be called "Note" still sizes as a note.
# `isinstance` first — a malformed spec (`class_type: ["Note"]`) must
# fall to DEFAULT_SIZE and let apply_specs report it, not TypeError here.
is_note = isinstance(class_type, str) and class_type in AUTHORABLE_VIRTUAL_NODE_TYPES
m = None if is_note else graph.node(class_type or "")
if is_note:
size = note_size(class_type)
elif m is not None:
widget_names = tuple(graph.widget_order(spec["class_type"]))
# object_info already marks multiline inputs; the catalog parses it into
# PortOptions.multiline. Match by name because widget_order is the render
Expand Down
3 changes: 3 additions & 0 deletions comfy_cli/tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ def _resolve_posthog_token() -> str:
"repository",
"commit",
"file",
# `workflow add-node --text`: a note body is verbatim user content, like
# `--prompt` — analytics only learns that the option was supplied.
"text",
}
)

Expand Down
Loading
Loading