A part redrew on new elements each time a count was answered or a run ended, so a press on the page lost its click and a count being typed lost its focus. settle moves to ui_dom.js for both; a card is kept for its save, and a picture line stands while its pictures do. A press on a picture just as a new one reaches that line is still lost: the line shifts, and another picture is under the pointer by the mouseup. Tested with a real mouse on an isolated ComfyUI. tests/snapshot_bar.html now covers the page and fails 5 checks on the previous code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
34 KiB
Frontend Architecture
Read this document for changes under web/, browser entry modes, panel state,
localization, media, or explicit canvas mutations.
Bootstrap and module ownership
web/modules/ai_task_status.js derives confirmed task execution state;
web/modules/ui_ai_status.js renders the task toolbar's compact execution-status button and popover from run steps,
with a stop button beside it while a run goes. web/modules/ui_ai_run_live.js draws a running run for both the
popover and the timeline: bar and percent, time left and per step, VRAM, the console's note and last lines in a
<details>, the stop button (POST /anomalous/ai_tasks/{id}/runs/{step}/stop), and the step's size estimate
with its warnings (also kept after the run). web/modules/ui_ai_run_map.js draws a run's map under its step
(one line, the changes as chips, the boxes in a <details>, scaled to the column) and for a run_map stage block
(the panel ctx's stepOf). web/modules/mcp_task_tab.js wraps each page action in mcp_bridge.js: with the task's
mode "new", it clicks the task tab's button in .workflow-tabs (falling back to Workspace.NextOpenedWorkflow)
and waits for activeWorkflow to be it. A missing or unactivatable recorded tab fails the action;
it never substitutes the current workflow. Only a task without a recorded tab copies
app.graph.serialize() into a new tab before its first change. Missing-model downloads follow
the same task tab as reads and runs. Explicit new_workflow / open_workflow can replace the tab; the outcome
reports tab. ui_ai_task_canvas.js is the header's mode switch and tab chip.
The header shares progress with the timeline. Websocket updates also update the in-memory
step; a five-second visible-page refresh recovers missed events. The loop stops when the
workbench/page is hidden or removed, and resumes on open or browser visibility return.
Unended tasks with no active step await an update; silence never means completion.
mcp_run.js follows one native app.queuePrompt request by its host request ID and
promptQueueing/promptQueued events. A temporary api.queuePrompt wrapper preserves the
original receiver/arguments and collects only that batch's submission responses, stopping
on its first failure or completion. Cleanup restores the API and removes both listeners on
all exits. The batch receipt can return before the native queue finishes later manual work;
the native promise remains error-handled. Already-busy or unsupported host queues are refused
before calling the native method. Seed changes, widgets and serialization remain host-owned.
Share and import is the Workflows page's ⇅ (ui_share.js). Its share code
(share_code.js, no DOM) is AMB2: the workflow's lean form — node id, type, widget values,
position to 10 px, and only what differs from a fresh node of the type (LiteGraph.createNode
serialized as the template; a node pack's node always keeps its ports, so its links survive on
a computer still lacking the pack), links by slot, groups, the anomalous_* extras — deflated
and spelled as Chinese characters (14 bits each, U+4E00..U+8DFF, a first byte marking the
padding) or base64url. Encoding rebuilds the workflow from the lean form and compares
(sameWorkflow); subgraphs, native reroutes or any difference make the code hold the whole
workflow. AMB0/AMB1 codes still decode. The same dialog imports a pasted code or workflow JSON,
a dropped or chosen workflow file or image (app.handleFile), or a backup .zip
(ui_backup.js). It is independent of the paused Recipe package import/export.
ComfyUI loads JavaScript in the extension WEB_DIRECTORY as ES modules.
web/main.js registers Anomalous.ModelBrowser and coordinates host hooks.
browser.js defines the browser class and binds extracted feature methods;
browser_entry.js owns the single browser instance and all entry presentation,
including window.anomalous_open_voice(character), which the Anomalous_TTS node's
character menu calls to open one character's voice card (browser.openVoice; the
node pack documents it in its docs/INTERFACE.md §6);
interface_settings.js owns locale and theme preferences. A syntax error or duplicate
top-level declaration in any imported module can prevent registration and make
the entire entry disappear. For affected modules, validate syntax and module linking
via Node's experimental VM modules (vm.SourceTextModule) and verify
the real ComfyUI runtime creates the configured entry. Floating trigger styling in
the ordered stylesheet bundle rooted at web/styles.css uses dynamic 1em SVG
scaling and flex centering to guarantee
consistent visual presentation across all configured trigger sizes. The unsaved
default position is defined in CSS outside the sidebar dock; browser_entry.js
drags with pointer capture and only persists real drags, and entry_controls.js
validates and clamps saved coordinates (clampFloatingTriggerPosition). Resetting
the position clears the saved coordinates so the CSS default applies again.
Web modules are imported by plain relative paths. Do not add ?v= query strings to
module imports: a different URL is a second module instance with its own state.
Freshness comes from the Cache-Control: no-cache header on this plugin's files.
Major UI panels live in web/modules/ui_*.js. Shared browser state remains on
the AnomalousBrowser instance. Pure parsing, normalization, comparison, and
transaction helpers remain in focused modules rather than acquiring DOM state.
ui_sidebar.js assembles the browser window and places the model folder column
(ui_folder_list.js, the current type's folders only); the left
icon rail (ui_shell_rail.js), page navigation and the list column's open state
(ui_shell_nav.js), the home page (ui_home.js) and the window frame
(ui_shell_frame.js) are their own modules. Every page change goes through
owner.goTo(page). ui_settings_hub.js owns the display preferences behind
owner.displayPrefs, which the settings page (ui_settings_page.js) changes,
while ui_rail_tools.js owns the rail's tool buttons.
The Prompt Studio shortcut (its command and setting ids still say "materials") is a
native ComfyUI command/keybinding; a deferred window-key fallback invokes the same
command path only when the host did not open the studio, so handled shortcuts are not executed twice.
ui_browser_navigation.js owns shared panel hiding, recoverable detail cleanup,
and workspace return. ui_scan_page.js owns the scan page and scan_runner.js scan launch and polling (an online scan first goes through scan_connection.js: Civitai reachable, or the user picks scanning from the files),
ui_folder_manager.js is the settings page's Model folders view (grouping mode,
visibility and order, saved on each change). Public entry functions remain
browser-instance methods so existing actions share current browser state.
Update-guide content and UI lifecycle are separate modules. The header and Help open it only on explicit user action; browser close disposes it. Read update-guide.md before changing guide IDs, steps, persistence, or sidebar icon/label transitions. Do not embed guide logic in the main/sidebar bootstrap or reuse its content version as a feature flag.
The main surfaces are:
- Sidebar shell: navigation and persistent scan controls; focused child modules own the scan page, folder manager, and help dialog.
- Grid and model detail: model browsing plus coordinated detail display in
ui_detail.js(its head with the actions inmodel_detail_head.js), metadata editing inui_model_editor.js;model_widget_path.jsputs a model path into a loader's widget. The scan page's model picker isui_scan_picker.js, its settings cardui_scan_settings.js; opened afresh, the page shows the last counts at once (?quick=1) with "showing the last counts while they are updated", then the fresh ones; before any, a count slower than 400 ms shows "counting your models" instead of a blank page. "Found on Civitai" opens a list of those models with a box to find one by name (/anomalous/scan_matched, read when it opens). - Gallery: generated outputs (pictures and videos; All / Pictures / Videos beside the search
and the hidden formats in Settings,
gallery_filter.js+ui_gallery_filter.js) and the full-screen Image Detail Studio Workbench, for pictures only.ui_gallery_card.jsbuilds each output's card; its star and the workbench's Keep openui_keep_menu.js, which keeps the image as a Workflow Recipe, a combo or its prompts (image_keep.js;GET /anomalous/kept_imagessays what each image was kept as). The gallery's ★ Kept switch (ui_gallery_filter.js,gallery_filter.js) lists only the kept images (gallery_images?kept=1,kept_images.kept_keys).ui_gallery_detail.jsowns its singleton lifecycle and cache,ui_image_stage.jsowns header/zoom/filmstrip interaction, andui_image_inspector.jsowns metadata tabs. - Workspace: Combos (搭配, formerly Prompt Notes) and Workflow Recipes. The combo list, workspace/persistence, editing, and canvas use are separated
across
ui_combos.js,ui_notebooks.js,ui_notebook_editor.js, andnotebook_canvas.js; a default combo'sdata.loraNode('core', 'rgthree', 'easy'; none is 'core') says which LoRA node it is laid out with (lora_nodes.js; one this ComfyUI lacks falls back to 'core' and says so after placing), chosen under New or in its editor (ui_lora_node_choice.js, shown only when there is a choice). A Power Lora Loader in a node structure keeps its rows as the node'srowsand each row's LoRA as a slot withfield: 'lora'; its rows are made again (addNewLoraWidget) before the slots are filled. Put on canvas lays a combo out as a new wired group of nodes (Esc while it follows the pointer removes it; the activity log holds back whileowner.placingCombois set) and never changes nodes already on the canvas (values of an existing node are changed in Current node). A combo never changes a value either: what was saved is what goes down (AGENTS.md, 功能原则). A combo with a node structure (combo_structure.js,ui_combo_structure.js) keeps every picked node but subgraphs, and the links between them. Its slots are the model drop-downs (a drop-down of model files) and text boxes (a multiline text box by the node's definition); a node without slots goes back exactly as saved. A link from a node that was not picked cannot be kept: its input is stored instructure.open, and the save dialog, the editor and the card say it needs wiring. Slots are per box, so a node with several model drop-downs has several; a box fed by a link is none. Each slot's models folder is the one whose files hold all the drop-down's options. Putting it down needs every node type installed (missing ones and their packs are named first), restores the other boxes by name in their order, fills the slots by name, then links by port name and type; a slot or link that cannot be restored is reported after placing, never guessed. Values other than the slots cannot be edited: a combo puts down exactly what was saved. The canvas's node menu (getNodeMenuItems, browser_entry.js →canvas_menu.js) offers "Save as combo" for the picked nodes, or the right-clicked node when it is not among them, when any of them is not a subgraph. Nodes are kept in flow order (chainOrder, combo_slots.js) so slot numbers read along the chain; the editor groups slots into models, LoRAs and prompts in that order, renumbers names it made itself, and lists each model node's other saved values read-only (on a node with several models, by the number in the box name); nodes without slots follow under "put back as saved" with their saved values, read-only. Prompts get a read-only translation panel (translation_service.js); empty optional slots start unticked in the save dialog. - Prompts: the rail entry opens Prompt Studio (below); there is no Material Library page. Saved node values are listed, applied and deleted in Current node.
- Current node (Node Assistant)/Model Check: selected-node actions, saved parameters, and
missing-model recovery.
model_check.jsdecides each workflow model's state and applies fixes;ui_doctor.js(the page) andui_doctor_banner.js(the bar shown after a workflow opens, unless its "Don't show again" or Settings → Workflows turned the check on opening off) only render it and call it on a press. A missing model with a certain source gets Download (ui_model_download.js;model_download.jsholds the logic): the dialog names, per file, the type's folder (a choice when there are several, remembered per type inlocalStorage) and the subfolder from Settings → Model downloads — where the workflow says (so the node needs no change) or a set folder,{base}there being the model's base model, matched to an existing folder in any spelling — and the file keeps the workflow's name. When a download ends, the model lists reload, nodes still naming that model get the file (one Ctrl+Z step each), and once nothing is downloading the new files are scanned in one targeted scan.ui_node_assistant.jsowns the current-node panel and model history,ui_node_model_picker.jsowns the native-widget model replacer, andui_node_parameters.jswithnode_parameter_sets.jsown the panel's one list of saved values for the node type;ui_apply_receipt.jsis the receipt with Undo after a panel writes to a node.
ui_dom.js owns generic text, jsonResponse and settle helpers.
material_inspector.js holds the image workbench's metadata helpers and
node-parameter rendering; png_metadata.js reads a PNG's workflow in the browser. See material-library.md for the material files' API and
persistence contract.
Entry modes and host integration
One shared openBrowser() command backs the floating trigger, ComfyUI action-bar
button, and Extensions -> Anomalous Model Browser command. Entry presentations
are mutually exclusive and reuse the same browser instance. The Extensions
command remains available as a recovery path in every mode.
A native command owns the default Shift + B browser binding (Ctrl + Shift + M before v1.58.2: Chrome and Edge
take it for switching profiles). Prompt Studio has
no shortcut of its own (removed 2026-10-07): it opens from the rail, the 7 key, or by opening
the browser with a text node selected. Shortcut customization delegates
to ComfyUI's command/keybinding panel and recorder; the plugin does not install
a parallel global keyboard listener or maintain a second shortcut preference.
Opening the browser from the trigger, the action-bar button, the menu or its shortcut
(show({ followSelection: true })) follows the node selected on the canvas unless Settings
→ Opening and window turns it off (shell_open_rules.js, a localStorage preference): a node
with prompt boxes and no model drop-down opens Prompt Studio, which targets that node; any
other node opens Current node; nothing selected reopens the page used last. Other callers
(Prompt Studio closing, the doctor banner, MCP actions) open the browser as it was.
While the browser is visible, the plain keys 1–9 go to the rail's pages from the top
(ui_shell_nav.js; ComfyUI binds no plain digits). They are ignored in text fields, while
an input method is composing, and while a dialog is open; the rail's tooltips show each
number. This is in-page navigation, not a second shortcut for opening anything.
Language changes replace the registered Interface setting descriptors with freshly translated copies. ComfyUI's reactive settings tree therefore updates the open category heading, labels, tooltips, custom controls, and combo options immediately without requiring a page refresh.
Hidden reusable dialogs must not advertise an active modal state. In particular,
aria-modal is true only while the dialog is visible, so ComfyUI's global
modal guard does not suppress unrelated host shortcuts.
Panel and lifecycle state
Main content surfaces are exclusive. Opening one must hide or clean the surface it replaces, while preserving only the state required for an explicit return path. Search from the sidebar exits model detail and shows the grid. Workspace records the previously visible main-browser panel and restores it on close, falling back to the grid when the prior state is unavailable.
Recipe-to-model navigation is a special recoverable transition described in
recipes.md. It keeps the outer browser open and must not call the browser-wide
close() lifecycle.
Grid folder navigation removes old content rather than accumulating hidden grids. Before removing DOM that contains video or audio, pause the media and release sources where applicable. Closing the browser aborts active listing, disconnects observers, pauses media, and releases grid video/audio sources. Lightweight cards may remain warm for 90 seconds, after which card DOM and model payloads are released. A new listing cancels the previous request and render generation.
The full ordered model result remains available, but cards are created in
bounded animation-frame chunks. Images use lazy loading and async decoding.
Grid video sources are attached only near the viewport and then follow the
configured always or hover behavior with muted, loop, and plays-inline
attributes.
When identifying video media from a served URL, inspect the full URL with a
query-aware extension test such as /\.(mp4|webm)(?:$|\?|&|#)/i. Do not use
new URL(url).pathname, because the backend may put the actual filename in a
query parameter.
Model source component links
The Sources view (ui_model_sources.js, data in model_source_links.js) uses
inferModelFolderTypes for workflow component labels and
category-scoped, exact-path local metadata reads. Context requests are batched
to respect the endpoint's 16-item limit. Missing source links and unavailable
native model choices are independent states; dynamic choices are not treated
as evidence of absence. Native extensionless component choices and PTH/GGUF
references remain visible even when the backend cannot locate an individual file.
model_source_data.js owns library-result shaping and the Sources view's pure
main/component grouping and filtering. CLIP, text encoder, CLIP Vision, VAE, and
preview-VAE entries live in a session-local, default-collapsed disclosure; the
source-status pills and search affect both main models and components. Collapsed
component rows are not constructed. Main-model completion statistics exclude
optional component links, while the disclosure's missing-local count describes
only the currently filtered component matches.
Legacy Civitai placeholder links with non-positive model IDs are normalized to
an unfilled source before grouping, so offline inference records remain visible
under the source-needed filter.
Workflow save and Note generation always receive the complete workflow model set; summary copy receives only the visible rows. Such links may be saved to the workflow, but component sidecar writes require a resolved local target. Opening, filtering, and expanding the hub do not initiate cloud scans, hash calculation, or sidecar writes, and do not change graph/model identity recovery rules.
Localization and DOM safety
web/modules/locales.js is the canonical catalog for user-visible runtime
strings. Resolve strings at render time through the shared translator. Chinese
and English dictionaries keep identical key sets, have no duplicate keys, and
cover all statically referenced translation keys.
Dynamic filenames, paths, model names, node types, counts, and user-authored
content remain parameters rather than dictionary entries. Insert untrusted text
with textContent or escaping. Only fields intentionally supporting trusted
formatting may pass through setSafeRichHtml() in safe_dom.js; never assign
metadata directly to innerHTML.
Persistent localized nodes that survive a language change carry an i18n key and are refreshed in place or rebuilt from current state. Missing translation data must not prevent a panel or action from rendering.
Scope plugin DOM IDs and CSS classes with anomalous-. Before introducing a
dynamic ID, search styles.css for collisions. Follow the established z-index
tiers and ensure a child modal sits above the parent that would otherwise consume
its clicks.
Graph and widget mutation rules
Explicit graph mutations are transactional. Validate the entire intended change
before mutation, wrap it in graph.beforeChange() / graph.afterChange(), and
restore prior nodes, links, and widget values if any operation or callback fails.
A successful change marks graph and canvas dirty and emits the host change event
expected by dependent surfaces.
Partial Recipe Append clones serialized nodes, remaps IDs and links, supports
groups, and rolls back everything it created on failure. It never calls
loadGraphData on the live graph and rejects unsupported subgraph definitions
until they have a complete remapping path. Complete recipes use
loadGraphData intentionally so ComfyUI owns new-workflow canvas creation.
graph_splice.js decides where a LoRA loader goes (planLoraInsertion) and wires it
(spliceLora), for Current node's insert buttons and the MCP add_lora alike. The insert
buttons take several LoRAs at a time (lora_insert.js): they go in as a chain or through one
third-party node, all or none, and stand in one column. It reads
declared port types, never slot indexes or display names. After a node, every link of its
MODEL output moves behind the LoRA; before one, only that node's MODEL input. The CLIP
line goes through only when the LoRA file trains the text encoder (api/lora_info.py
reads its header): from the same node, a UNet workflow's one CLIP source, or, before a
sampler, the source the prompts wired into it share. Otherwise a model-only LoRA loader
is used. A failed connection removes the node and restores every link. Picker candidates come
from the target widget's native combo values; metadata may filter or decorate
that set but may not introduce foreign category values.
Parameter application resolves a serialized match back to a live ComfyUI node before accessing widgets or callbacks. It validates all target slots first, skips volatile values, and calls the host's widget hooks with the current four-argument contract. Serialized workflow records are never treated as live widget objects.
MCP canvas construction uses canvas_wiring.js to plan all connections against live sockets
before mutation. Automatic pairs require a unique concrete type on both sides, or same-type
sockets of the same name; wildcard, union and other ambiguous sockets require explicit selection.
An occupied input the new node feeds is spliced when the node takes that type (its old source
feeds the node). Insertion splices each type the node passes through, one output per type, and
needs an explicit downstream selection when an output branches. Final topology is checked for
cycles after planned replacements. mcp_canvas_edit.js brackets application in one native
beforeChange/afterChange transaction. On failure it removes the added node and restores prior
connections; callback failures during restoration are reported as incomplete rollback.
Asynchronous node preparation verifies the active graph, workflow key and serialized content
before writing. Successful receipts report links and output reachability, not execution success.
The activity log's limited undo is separate from ComfyUI's native Ctrl+Z for topology changes.
mcp_canvas_brief.js collapses unbranched paths within each flow, grouping IMAGE/LATENT into
the picture spine so auxiliary VAE/MASK inputs do not split it, keeps positive/negative roles,
walks each edge once (including malformed cycles), and bounds the resulting text.
Optional capabilities
web/hash_resolver.js keeps the local hash cache and may hook a compatible graph
serializer to carry model provenance. If the host API is unavailable, it disables
only that integration with a useful warning. It must not prevent main.js from registering or remove
the visible browser entry.
Network-backed enrichment is explicit and recoverable. An unavailable Civitai or translation service may produce a local error state; it must not block local browsing, editing, or already stored data.
Prompt Studio ownership
The studio edits prompts where they are: the prompt boxes of the prompt node last
selected on the canvas, or, with no such node, a positive and a negative draft
(owner.promptStudioDraft, kept while the page stays open). It is the browser's Prompts
page (owner.promptPanel, ui_prompt_composer.js): the rail's Prompts entry, the 7 key,
opening the browser with a text node selected, Current node's "Edit in Prompt Studio" and
a kept prompt all go there. "Beside the canvas" moves it into a drawer docked by the canvas
(side and width remembered) with the browser folded away; "Back to the window" returns it to
the page and Close or Esc closes the drawer. Only one studio is shown at a time; the page's
studio stops when another page is shown or the browser closes (its 400 ms poll with it) and
is made again when the page is shown. There is no assembly board, block draft or plan loading.
The target (ui_prompt_target.js) is polled every 400 ms. Selecting another node with
prompt boxes switches to it; empty canvas or a node without boxes keeps the current
one; the ✕ beside the node's name switches to the draft; a node that leaves the open graph drops back to the
draft. Each change is one applyNodeMaterialValues write (a Ctrl+Z step). The studio's
Undo takes its own writes back newest first and stops with materialUndoChanged when a
box changed elsewhere; switching target clears it. Box editors re-read their widget on
each poll and redraw unless a tag is being edited, a translation is pending or the text
view has focus.
prompt_tags.js splits a box on top-level commas and line breaks and keeps each
separator, so editing one tag leaves the rest of the text, line breaks included, as it
was; a weight is (tag:w). Inserting skips tags the box has already (same words, any
weight). Typed or edited text with letters that are not plain English (needsEnglish:
Chinese, kana, hangul, Cyrillic, accented Latin) is translated to English before it is
written; a box that holds such tags offers to translate them, a translation the box
has already removes the tag instead, and the result is written only if the box still
holds the text the request was made from. Meanings (prompt_gloss.js) are shown in the
language picked in the top bar (one of GLOSS_LANGUAGES, none by default, remembered in
local storage with the view; an older "Chinese on" setting reads as Simplified Chinese).
They are asked for in batches, kept for the session per language, and a failed lookup
leaves that box's meanings in that language blank instead of retrying. The backend
(api/translation_routes.py) asks DeepL first when Settings → Translation has a key
(DEEPL_API_KEY in config.json, sent in the Authorization header, never in a backup),
then Google's free endpoint, then MyMemory; when DeepL failed, the answer carries
deepl_error (bad_key, quota, busy or the error) beside the other provider's translation,
which Settings shows after a key is saved. It skips DeepL for a target it does not take and
lets MyMemory detect a source that is neither English nor Chinese.
The cards are three built-in ones and the saved prompts (prompt-kind material files,
prompt_material_source.js). The saved list is asked for on open, every 30 seconds
while the page is visible (scheduled after the previous request), on focus/visibility
and after a save; a newer request cancels the older one and a failed one keeps the
current cards. List summaries are not prompt bodies: a prompt's text is read through
material_prompt_data.js once per filename and timestamp, since a saved prompt's text
never changes. A clicked card opens as a reference (ui_prompt_reference.js, read only,
one at a time) between the cards and the boxes: a click on one of its tags adds that tag,
"Add all" the whole card (as the preview's Add does); the ticks follow the box's text on the
same 400 ms poll. A card's role decides its box: a positive card never goes into a
negative box or the reverse; boxes of unknown or both roles take either. A saved
card's preview renames it (update_material, tags kept) or deletes it
(delete_material, to the Recycle Bin). A card, or a box's handle with the box's text
at drag start, drags onto the canvas (prompt_card_drag.js, one undo step); over the
drawer the drag passes through to the boxes.
| Module | Owned state and responsibilities |
|---|---|
ui_prompt_composer.js |
Active drawer, docking width/side, Esc, trigger visibility and reopening the browser |
ui_prompt_workbench.js |
Top bar, view and meanings switches (remembered), saving a box as a card |
ui_prompt_target.js |
Which node or draft is edited, the poll, node/draft writes and the studio's Undo |
ui_prompt_box_editor.js |
One box: tags or text, selection, weight, edit, remove, reorder, drops, typing, translation |
ui_prompt_source_deck.js |
Built-in and saved cards, search, new-card form, rename/delete and the sync request |
ui_prompt_card_popover.js |
Card preview: hover corridor, pin, copy/add/rename/delete actions |
prompt_card_drag.js |
A card or box dragged onto the canvas: box fill or new prompt node, passing through the drawer |
prompt_tags.js |
Pure tag split/join, weights, insert/remove/replace/move keeping separators |
prompt_gloss.js |
Meaning languages, session cache and batched lookup of tags' meanings |
prompt_material_source.js |
Saved prompts as cards (paginated list, text read once) and saving a card |
prompt_composition.js |
Starter cards (PROMPT_PRESETS, also the combo group's negative), composing a saved plan's text, prompt titles and categories; no DOM |
ui_lifecycle.js |
View-scoped listeners, AbortSignal, cleanup callbacks and resizing |
Every close route (Close, Esc outside a text field, replacement) disposes the same view scope: the poll and sync timers, requests, translation and text timers, listeners and resize handlers. Esc in a studio text field only leaves the field, and Esc with a pinned card preview leaves the studio open. Closing during resize releases move/up listeners and restores body cursor and selection styles. A save already sent to the server may still complete, but must not reopen or repaint a disposed view.
Explicit English translation applies to all input languages, including kana and Korean. Transport errors and rejected bridge responses remain local errors; provider-specific validation belongs to the backend translation route.
Visual styling and theme architecture
styles.css is the single external entry and ordered import manifest for current
visual values. Its web/styles/00-*.css through 10-*.css children preserve the
original cascade order; every child import carries the same cache version so an
entry-cache hit cannot leave stale child rules. Shared --amb-*
tokens express surfaces, text, borders and control shapes; theme overrides must
be scoped to .theme-abyssal-scarlet or html.amb-theme-light rather than changing
unrelated surfaces.
Themes (interface_settings.js, Settings → Appearance → Theme and the ComfyUI setting
Anomalous.ModelBrowser.Theme): follow ComfyUI (default), dark, light, or Abyssal Scarlet.
Following ComfyUI reads dark-theme on <html>, which ComfyUI sets for a dark palette and
removes for a light one, and repaints when it changes. The light theme is the class
amb-theme-light on <html>; Abyssal Scarlet keeps theme-abyssal-scarlet. An older
Abyssal Scarlet switch (anomalous_theme_abyssal_scarlet) reads as the Abyssal choice.
Colours come from the tokens in 00-foundation-models.css, which every theme defines:
surfaces (--amb-bg-page, -panel, -card, -card-hover, -input), text
(--amb-text-main, -soft, -muted, -dim), borders (--amb-border, -strong,
-hover), the primary button (--amb-btn-primary-*) and one accent, --amb-link, for
links, selection and focus; its tints are color-mix(in srgb, var(--amb-link) N%, transparent). CSS injected from JS and inline styles use the same tokens. Literal
colours stay only where the colour is the meaning: status (red, green, amber), prompt
roles, voice emotions, canvas node colours and translucent shadows or overlays.
Tokens added for the light theme keep the dark themes exactly as they were:
rgba(var(--amb-ink-rgb), a) for tints over the page (white on dark, black on light),
--amb-danger/-warn/-ok and their -text/-soft shades for status text, --amb-bg-tooltip,
--amb-bg-float, --amb-bg-dialog, --amb-bg-sunken, --amb-scrim and --amb-note-*.
Older rules and inline styles that wrote a dark colour now write
var(--amb-lt-<role>, <that colour>): the --amb-lt-* tokens exist only in the light theme,
so every other theme falls back to the original colour. Use them when a dark literal cannot
become a shared token. Inside the plugin's windows ComfyUI's own palette variables
(--bg-color, --fg-color, --input-text …) are mapped to the plugin's tokens whenever the
two themes differ. Surfaces over images (card labels, cover pills) stay dark, and the image
viewer (.anomalous-workbench-overlay, .anomalous-gallery-viewer) gets the dark tokens back
in the light theme.
Studio drawer rules keep the source deck at the screen edge and the assembly
track next to the canvas. Common geometry is shared between dock directions;
direction-specific rules set column order and separators. The removed embedded
composer's #anomalous-container.anomalous-docked overrides must not return.
Before adding an override or !important, locate and edit the owning rule.
Older theme overrides in 08-theme-gallery-overrides.css predate the tokens; many are
now redundant and can go as the rules they override are touched.
Verification
tests/prompt_tags.mjs covers splitting, joining, weights and tag edits keeping
separators. The UI fixture simulates DOM and ComfyUI APIs and does not replace
checking the real host, layout, focus, drag gestures and theme rendering.