Files
DemonGatanjieuandClaude Opus 5.5 c890627961 Hand the canvas or a node to an AI from the right-click menu
"Hand to AI..." in a node's and the empty canvas's menu opens ready wishes (run 4
and pick, find what is wrong, explain each step, add an enlargement and face and
hand fixes; explain or tune a node) and the line to change. It goes to the task an
AI is on as a message, or starts a task about the canvas: the task's reads, runs
and copy start from that tab even with another in front. A node's saved pictures
can go along, and a failed run offers "Ask the AI" with ComfyUI's words. The AI's
job sheet now follows the task's tab like a run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 15:09:27 +08:00

133 lines
5.8 KiB
Python

"""Each task's own canvas tab, kept by Anomalous rather than left to the AI: with "new" (the
default, set in the Gates window; each task can change it) the page copies the workflow open
now into a new tab named after the task before the AI's first change, and brings that tab back
before each later change, read or run (web/modules/mcp_task_tab.js); new_workflow and
open_workflow make the tab they open the task's. The user's own tab is never changed. With
"current" the AI works on whatever tab is open, as before.
mcp_server.py marks each tool call with its task (begin_call); mcp_bridge.ask_page adds the
task's canvas to the page actions that touch the canvas (spec) and keeps the tab the page
reports (remember); tell() tells the AI once when a tab was made for it.
"""
import contextvars
import json
import os
import time
import uuid
from . import ai_tasks
from .path_utils import atomic_write_json
MODES = ("new", "current")
DEFAULT = "new"
# Page actions (web/modules/mcp_bridge.js) that work on the canvas, so on the task's tab.
TAB_ACTIONS = {"describe_canvas", "set_prompt", "set_model", "add_lora", "place_combo", "check_models", "fix_models",
"run_workflow", "download_missing", "add_node", "add_nodes", "connect_nodes", "set_values", "remove_nodes", "new_workflow", "open_workflow",
"fill_slots", "job_sheet"}
_call = contextvars.ContextVar("anomalous_task_canvas", default=None)
def _path():
return os.path.join(os.path.dirname(ai_tasks.tasks_dir()), "ai_canvas.json")
def default_mode():
try:
with open(_path(), encoding="utf-8") as source:
mode = json.load(source).get("mode")
except (OSError, ValueError, AttributeError):
mode = None
return mode if mode in MODES else DEFAULT
def set_default(mode):
if mode not in MODES:
raise ValueError("canvas_tab is new or current")
atomic_write_json(_path(), {"mode": mode})
return mode
def mode_of(task):
mode = ((task or {}).get("canvas") or {}).get("mode")
return mode if mode in MODES else default_mode()
def set_mode(task_id, mode):
"""The user's choice for one task. Returns the task's canvas record."""
if mode not in MODES:
raise ValueError("canvas is new or current")
with ai_tasks._lock:
task = ai_tasks._load().get(task_id)
if not task:
raise ValueError("No such task")
if (task.get("canvas") or {}).get("mode", default_mode()) != mode:
# The user's choice: the AI hears of it, and it is one of the task's decisions (ai_decisions.py).
now = time.time()
ai_tasks._add_step(task, {"id": uuid.uuid4().hex[:10], "at": now, "ms": 0, "tool": "user_canvas", "kind": "user", "ok": True,
"state": "done", "summary": "user_canvas", "canvas": mode, "delivered": False, "results": [],
"text": "The user chose that you work " + ("in a tab of the task's own." if mode == "new" else "on the canvas they have open.")})
task["canvas"] = {**(task.get("canvas") or {}), "mode": mode}
ai_tasks._save(task)
canvas = dict(task["canvas"])
ai_tasks._notify(task_id)
return canvas
def begin_call(task_id):
"""Marks the tool call running now as task `task_id`'s; the holder says later what the page opened."""
holder = {"task": task_id, "opened": None}
_call.set(holder)
return holder
def spec(action):
"""What the page needs for page action `action` of the call running now: {mode, tab, title, from?}, or None.
`from`: the tab of a canvas the user handed over (the task's "about"), what the task's work starts from."""
holder = _call.get()
if not holder or not holder.get("task") or action not in TAB_ACTIONS:
return None
task = ai_tasks.get_task(holder["task"])
if not task:
return None
handed = next((item["tab"] for item in task.get("about") or [] if item.get("type") == "canvas"), None)
return {"mode": mode_of(task), "tab": (task.get("canvas") or {}).get("tab"), "title": (task.get("title") or "")[:60],
**({"from": handed} if handed else {})}
def remember(tab):
"""Keeps the tab the page made or opened for the call's task ({path, name, made?})."""
holder = _call.get()
if not holder or not holder.get("task") or not isinstance(tab, dict) or not isinstance(tab.get("path"), str):
return
kept = {"path": tab["path"][:300], "name": str(tab.get("name") or "")[:120], "at": time.time()}
with ai_tasks._lock:
task = ai_tasks._load().get(holder["task"])
if not task:
return
task["canvas"] = {**(task.get("canvas") or {}), "tab": kept}
ai_tasks._save(task)
ai_tasks._notify(holder["task"])
holder["opened"] = {**kept, "made": bool(tab.get("made"))}
def tell(result, holder):
"""Says in the tool's answer that Anomalous made the task a tab of its own (once, when it did)."""
opened = (holder or {}).get("opened")
if not opened or not opened["made"] or not isinstance(result, dict) or result.get("isError"):
return result
data = dict(result.get("structuredContent") or {})
data["canvas_tab"] = {"name": opened["name"], "note": (
f"Anomalous copied the user's open workflow to a new tab \"{opened['name']}\" for this task (the user's "
"setting): your canvas changes, reads and runs go there; the user's own tab stays as it was. Do not switch "
"tabs yourself.")}
result["structuredContent"] = data
content = list(result.get("content") or [])
text = {"type": "text", "text": json.dumps(data, ensure_ascii=False)}
if content and isinstance(content[0], dict) and content[0].get("type") == "text":
content[0] = text
else:
content.insert(0, text)
result["content"] = content
return result