Files
DemonGatanjieu-Anomalous_Mo…/api/ai_task_canvas.py
T
DemonGatanjieuandClaude Opus 5.5 9abd596186 What the AI ran, a canvas tab of the task's own, and pictures straight to the AI
- Each run keeps its shape read from the prompt ComfyUI queued (ai_run_map.py): boxes by role
  with their settings, typed links, plumbing wired through, and what changed since the task's
  run before. The run step shows a one-line summary, the changes as chips and the boxes (top
  down, fitted to the column, each locating its node); show_run_map puts it on the stage.
- By default the AI works in a tab of the task's own (ai_task_canvas.py, mcp_task_tab.js): the
  open workflow is copied before its first change, the tab is brought back (a click on
  ComfyUI's tab) before later changes, reads and runs, and the user's tab stays untouched.
  Per-task switch in the header, default in the Gates window; the AI is told once.
- Output pictures can be handed to the task an AI is on (gallery card menu, the inspector's
  ✦), with the user's words: the AI gets them attached with what made them (ai_pictures.py).
  search_images(with_pictures) attaches the first six.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 00:56:01 +08:00

123 lines
4.9 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
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", "add_node", "connect_nodes", "set_values", "remove_nodes", "new_workflow", "open_workflow"}
_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")
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}, or None."""
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
return {"mode": mode_of(task), "tab": (task.get("canvas") or {}).get("tab"), "title": (task.get("title") or "")[:60]}
def remember(tab):
"""Keeps the tab the page made or opened for the call's task ({path, name, made?, again?})."""
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")), "again": bool(tab.get("again"))}
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": (
("The task's earlier tab was closed, so " if opened["again"] else "")
+ 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