feat(resolver): leave out run-time PromptManager parts, labelled
Maintainer option A: when a PromptManager prepend_text or append_text is fed directly by a node whose output is decided at run time (LoraManager's TriggerWord Toggle, now a runtime class, or PromptSearchList), the prompt resolves to the node's own text, as legacy showed. The side's source is runtime-parts-omitted and the resolved reason says that text added at run time, such as trigger words, is not included; the prompt block shows that line (D-NEW-R1). The toggle's output is never computed. Literal and traced parts are combined as before; an indirect run-time part, a run-time main text or an unknown feeder keep their outcome. The browser port changes identically. Ingestion is unchanged.
This commit is contained in:
+14
-4
@@ -433,10 +433,20 @@ prefix) stays `ambiguous`, and text generated at run time stays untraceable
|
||||
`tests/passthrough_graphs.py`; the browser port is compared on the named
|
||||
`select-*` chain cases and `passthrough_fuzz_cases`). Ingestion keeps the default
|
||||
profile's historical latent rule (D1), so the admin scan still reaches these files
|
||||
through its legacy-parser fallback. Known gap: a PromptManager whose `prepend_text`
|
||||
comes from LoraManager's `TriggerWord Toggle` stays `unsupported-node` naming that
|
||||
node (its output depends on run-time trigger words and version-specific widget
|
||||
handling); an unlisted node with sampler-shaped inputs (positive, negative, guider,
|
||||
through its legacy-parser fallback. Run-time PromptManager parts (maintainer option
|
||||
A, 2026-10-23): LoraManager's `TriggerWord Toggle` is a `runtime` class (its output is
|
||||
the LoRA's trigger words as filtered at run time). A PromptManager (or
|
||||
PromptManagerPair) `prepend_text` or `append_text` linked directly to a `runtime`
|
||||
node is left out: the text is the node's own, combined with any other literal or
|
||||
traced part exactly as before, the side's source is `runtime-parts-omitted`, and a
|
||||
resolved pair's reason is "Text added at run time, such as trigger words, is not
|
||||
included". The prompt block shows that line under the text (D-NEW-R1, placeholder
|
||||
copy the concept does not have; the evidence badge stays "Workflow inferred"). The
|
||||
toggle's output is never computed. A run-time node further upstream (inside a
|
||||
concatenation), a run-time main `text`, or an unknown feeder keep their outcome
|
||||
(`runtime-value` or `unsupported-node`). This is what legacy linked for the June
|
||||
hires-fix runs (`TestRunTimePromptManagerParts`, the `select-runtime-*` parity
|
||||
cases); an unlisted node with sampler-shaped inputs (positive, negative, guider,
|
||||
latent_image, a pipe, video embeds, custom-sampler parts, or a `model` link together
|
||||
with conditioning, neg_conditioning, samples or latent) is `unsupported-node`
|
||||
naming it, here and in a detailer's image ancestry; a latent that is not a link or
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -82,6 +82,19 @@ test('a zeroed conditioning negative shows as an explicit empty negative without
|
||||
}finally{scope.dispose();f.restore();}
|
||||
});
|
||||
|
||||
test('a prompt that left out run-time trigger words says so under its text (D-NEW-R1)',()=>{
|
||||
const f=dom(),scope=createScope(),container=new f.Element('div'),sent=[];
|
||||
const note='Text added at run time, such as trigger words, is not included.';
|
||||
try{
|
||||
module.mountPromptBlocks(container,{parentScope:scope,sender:{open:roles=>sent.push(roles)},prompts:{positive:{status:'resolved',text:'own text',source:'runtime-parts-omitted'},negative:{status:'resolved',text:'blur',source:'graph-inferred'}}});
|
||||
const notes=f.walk(container,node=>node.textContent===note);
|
||||
assert.equal(notes.length,1,'one note, under the positive only');
|
||||
assert.equal(notes[0].parent.attributes['aria-label'],'Positive prompt');
|
||||
container.click(f.walk(container,node=>node.dataset.sendRole==='pair')[0]);
|
||||
assert.deepEqual(sent,[{positive:'own text',negative:'blur'}],'the note is never sent');
|
||||
}finally{scope.dispose();f.restore();}
|
||||
});
|
||||
|
||||
const unresolved=message=>({status:'unresolved',text:null,source:null,...(message===undefined?{}:{message})});
|
||||
const statusLines=(f,container)=>f.walk(container,node=>node.tagName==='p'&&node.className==='prompt-text muted').map(node=>node.textContent);
|
||||
test('unsupported node status names the class',()=>{
|
||||
|
||||
@@ -488,9 +488,46 @@ def _passthrough():
|
||||
graph["60"]["inputs"]["positive"] = ["39", 0]
|
||||
cases["flux-encoder-parts"] = graph
|
||||
cases.update(_passthrough_edges())
|
||||
cases.update(_runtime_parts())
|
||||
return cases
|
||||
|
||||
|
||||
def _runtime_parts():
|
||||
"""Option A: a PromptManager part fed by a run-time node is left out."""
|
||||
cases = {"trigger-prepend": shapes.hires_fix_with_trigger_words()}
|
||||
graph = shapes.hires_fix_with_trigger_words()
|
||||
graph["2"]["inputs"].update(prepend_text="masterpiece", append_text=["8", 0])
|
||||
graph["20"]["inputs"]["latent_image"] = ["4", 0]
|
||||
cases["trigger-append-single"] = graph
|
||||
graph = shapes.hires_fix_with_trigger_words()
|
||||
graph["8"] = {"class_type": "PrimitiveString", "inputs": {"value": "trig9word"}}
|
||||
cases["traced-prepend"] = graph
|
||||
graph = shapes.hires_fix_with_trigger_words()
|
||||
graph["8"]["class_type"] = "MysteryWords"
|
||||
cases["unknown-prepend"] = graph
|
||||
graph = shapes.hires_fix_with_trigger_words()
|
||||
graph["9"] = {
|
||||
"class_type": "StringConcatenate",
|
||||
"inputs": {"string_a": "kept", "string_b": ["8", 0], "delimiter": ""},
|
||||
}
|
||||
graph["2"]["inputs"]["prepend_text"] = ["9", 0]
|
||||
cases["indirect-prepend"] = graph
|
||||
graph = shapes.hires_fix_with_trigger_words()
|
||||
graph["2"]["inputs"].update(prepend_text="", text=["8", 0])
|
||||
cases["runtime-main-text"] = graph
|
||||
graph = shapes.hires_fix_with_trigger_words()
|
||||
graph["2"]["class_type"] = "PromptManagerPair"
|
||||
graph["2"]["inputs"]["negative_text"] = shapes.NEGATIVE
|
||||
graph["10"]["inputs"]["negative"] = ["2", 1]
|
||||
graph["20"]["inputs"]["negative"] = ["2", 1]
|
||||
cases["trigger-pair"] = graph
|
||||
graph = shapes.hires_fix_with_trigger_words()
|
||||
graph["30"] = {"class_type": "PromptSearchList", "inputs": {"search": "x"}}
|
||||
graph["2"]["inputs"]["append_text"] = ["30", 0]
|
||||
cases["search-list-append"] = graph
|
||||
return {f"runtime-{name}": graph for name, graph in cases.items()}
|
||||
|
||||
|
||||
def _passthrough_edges():
|
||||
"""Lineage and floor edges: branching refiners, other slots, cycles, guiders."""
|
||||
cases = {}
|
||||
|
||||
@@ -243,6 +243,32 @@ def edit_batch(count=3):
|
||||
return graph
|
||||
|
||||
|
||||
def trigger_toggle(words="trig9word"):
|
||||
"""LoraManager's TriggerWord Toggle: its output is decided at run time."""
|
||||
return {
|
||||
"class_type": "TriggerWord Toggle (LoraManager)",
|
||||
"inputs": {
|
||||
"group_mode": True,
|
||||
"default_active": True,
|
||||
"toggle_trigger_words": {"__value__": [{"text": words, "active": True}]},
|
||||
"orinalMessage": words,
|
||||
"trigger_words": ["7", 2],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def hires_fix_with_trigger_words():
|
||||
"""``hires_fix`` whose PromptManager prepends LoRA trigger words (node 8)."""
|
||||
graph = hires_fix()
|
||||
graph["7"] = {
|
||||
"class_type": "Lora Loader (LoraManager)",
|
||||
"inputs": {"model": ["1", 0], "clip": ["1", 1], "text": "<lora:x:1>"},
|
||||
}
|
||||
graph["8"] = trigger_toggle()
|
||||
graph["2"]["inputs"]["prepend_text"] = ["8", 0]
|
||||
return graph
|
||||
|
||||
|
||||
def generated_text_chain():
|
||||
"""``hires_fix`` whose positive text comes from a run-time text generator."""
|
||||
graph = hires_fix()
|
||||
|
||||
@@ -270,7 +270,12 @@ class SpecEntriesTest(unittest.TestCase):
|
||||
)
|
||||
|
||||
# D18: decided at run time.
|
||||
self.assertEqual(table.runtime, frozenset({"PromptSearchList"}))
|
||||
# LoraManager's TriggerWord Toggle outputs the LoRA's trigger words as
|
||||
# filtered at run time (option A leaves them out of a PromptManager).
|
||||
self.assertEqual(
|
||||
table.runtime,
|
||||
frozenset({"PromptSearchList", "TriggerWord Toggle (LoraManager)"}),
|
||||
)
|
||||
|
||||
comfy = "{name}_{counter:05}_.{ext}"
|
||||
for name, image in (
|
||||
|
||||
@@ -202,6 +202,93 @@ class TestFluxThreeStage(unittest.TestCase):
|
||||
self.assertEqual(evidence["prompts"]["positive"]["text"], shapes.POSITIVE)
|
||||
|
||||
|
||||
class TestRunTimePromptManagerParts(unittest.TestCase):
|
||||
"""Maintainer option A: a run-time prepend or append is left out, labelled."""
|
||||
|
||||
def test_trigger_words_prepend_resolves_to_the_prompt_manager_text(self):
|
||||
for anchor in (None, "22"):
|
||||
with self.subTest(anchor=anchor):
|
||||
result = _inspect(
|
||||
shapes.hires_fix_with_trigger_words(), output_node_id=anchor
|
||||
)
|
||||
self.assertEqual(result["state"], "resolved")
|
||||
self.assertEqual(_texts(result), (shapes.POSITIVE, shapes.NEGATIVE))
|
||||
self.assertEqual(result["reason"], resolver.RUNTIME_PARTS_REASON)
|
||||
provenance = result["positive"]["provenance"]
|
||||
self.assertEqual(
|
||||
provenance["text_source"], resolver.RUNTIME_PARTS_SOURCE
|
||||
)
|
||||
self.assertEqual(result["positive"]["storage_text"], shapes.POSITIVE)
|
||||
# The negative is untouched by the positive's run-time part.
|
||||
self.assertEqual(
|
||||
result["negative"]["provenance"]["text_source"], "graph-inferred"
|
||||
)
|
||||
|
||||
def test_append_and_a_single_sampler_are_labelled_too(self):
|
||||
graph = shapes.hires_fix_with_trigger_words()
|
||||
graph["2"]["inputs"]["prepend_text"] = "masterpiece"
|
||||
graph["2"]["inputs"]["append_text"] = ["8", 0]
|
||||
graph["20"]["inputs"]["latent_image"] = ["4", 0]
|
||||
result = _inspect(graph, output_node_id="22")
|
||||
self.assertEqual(result["state"], "resolved")
|
||||
# The literal prepend is still combined exactly as before.
|
||||
self.assertEqual(_texts(result)[0], "masterpiece " + shapes.POSITIVE)
|
||||
self.assertEqual(result["reason"], resolver.RUNTIME_PARTS_REASON)
|
||||
|
||||
def test_the_inspector_shows_the_prompt_and_its_source(self):
|
||||
evidence = inspect_prompt_evidence(
|
||||
{"prompt": json.dumps(shapes.hires_fix_with_trigger_words())}
|
||||
)
|
||||
self.assertEqual(evidence["resolution"]["state"], "resolved")
|
||||
self.assertEqual(
|
||||
evidence["prompts"]["positive"],
|
||||
{
|
||||
"status": "resolved",
|
||||
"text": shapes.POSITIVE,
|
||||
"source": resolver.RUNTIME_PARTS_SOURCE,
|
||||
},
|
||||
)
|
||||
|
||||
def test_literal_and_traceable_parts_are_combined_as_before(self):
|
||||
graph = shapes.hires_fix_with_trigger_words()
|
||||
graph["8"] = {"class_type": "PrimitiveString", "inputs": {"value": "trig9word"}}
|
||||
result = _inspect(graph)
|
||||
self.assertEqual(_texts(result)[0], "trig9word " + shapes.POSITIVE)
|
||||
self.assertEqual(result["reason"], "")
|
||||
self.assertEqual(
|
||||
result["positive"]["provenance"]["text_source"], "graph-inferred"
|
||||
)
|
||||
|
||||
def test_only_a_run_time_feeder_is_left_out(self):
|
||||
# An unknown feeder stays unsupported, naming it.
|
||||
graph = shapes.hires_fix_with_trigger_words()
|
||||
graph["8"]["class_type"] = "MysteryWords"
|
||||
result = _inspect(graph)
|
||||
self.assertEqual(result["reason_code"], "unsupported-node")
|
||||
self.assertEqual(result["node"]["class_type"], "MysteryWords")
|
||||
# A run-time node deeper inside the part makes the whole part run time,
|
||||
# but it is not a direct feeder: never leave out a traced literal.
|
||||
graph = shapes.hires_fix_with_trigger_words()
|
||||
graph["9"] = {
|
||||
"class_type": "StringConcatenate",
|
||||
"inputs": {"string_a": "kept", "string_b": ["8", 0], "delimiter": ""},
|
||||
}
|
||||
graph["2"]["inputs"]["prepend_text"] = ["9", 0]
|
||||
result = _inspect(graph)
|
||||
self.assertEqual(result["reason_code"], "runtime-value")
|
||||
|
||||
def test_the_main_text_from_a_run_time_node_is_never_left_out(self):
|
||||
graph = shapes.hires_fix_with_trigger_words()
|
||||
graph["2"]["inputs"]["prepend_text"] = ""
|
||||
graph["2"]["inputs"]["text"] = ["8", 0]
|
||||
result = _inspect(graph)
|
||||
self.assertEqual(result["reason_code"], "runtime-value")
|
||||
|
||||
def test_ingestion_is_unchanged(self):
|
||||
result = resolve_prompt_pair({"prompt": shapes.hires_fix_with_trigger_words()})
|
||||
self.assertEqual(result["state"], "ambiguous")
|
||||
|
||||
|
||||
class TestStillAmbiguous(unittest.TestCase):
|
||||
def test_edit_batch_with_distinct_prompts_stays_ambiguous(self):
|
||||
result = _inspect(shapes.edit_batch(4))
|
||||
|
||||
@@ -194,6 +194,11 @@ MULTISTAGE_UNATTRIBUTED_REASON = "Multistage sampler ancestry is ambiguous"
|
||||
# A later stage's positive combines the earlier stages' shared prompt with more
|
||||
# conditioning (a style trigger): that shared pair is the run's prompt (floor).
|
||||
ADDED_CONDITIONING_REASON = "A later stage adds conditioning to the base prompt"
|
||||
# Maintainer option A (2026-10-23): a PromptManager prepend or append fed
|
||||
# directly by a run-time node (LoRA trigger words) is left out, never guessed;
|
||||
# the text is the node's own, labelled by this source and reason (D-NEW-R1).
|
||||
RUNTIME_PARTS_SOURCE = "runtime-parts-omitted"
|
||||
RUNTIME_PARTS_REASON = "Text added at run time, such as trigger words, is not included"
|
||||
_INCOMPATIBLE_SLOT = "Output slot has an incompatible type"
|
||||
# Before 3.2.3 a save patch overwrote every PromptManager node's saved ``text``
|
||||
# with the last-run node's prompt (comfy-metadata.js ``createContext``).
|
||||
@@ -328,6 +333,13 @@ def _reroute_value(inputs: dict) -> Any:
|
||||
return inputs[keys[0]] if len(keys) == 1 else None
|
||||
|
||||
|
||||
def _parts_reason(*sides: dict) -> str:
|
||||
"""The run-time parts reason when a side left out a PromptManager part."""
|
||||
if any(side["provenance"]["text_source"] == RUNTIME_PARTS_SOURCE for side in sides):
|
||||
return RUNTIME_PARTS_REASON
|
||||
return ""
|
||||
|
||||
|
||||
def _combined_positive(result: Dict[str, Any]) -> bool:
|
||||
"""Whether a stage failed only because its positive combines texts."""
|
||||
return result["reason_code"] == "composite" and result["positive"] is None
|
||||
@@ -537,11 +549,12 @@ class _GraphResolver:
|
||||
if adapter.compose or adapter.storage_input:
|
||||
storage_text = text
|
||||
if adapter.compose:
|
||||
before, before_path, _ = self.text(
|
||||
inputs.get("prepend_text", ""), "string", depth + 1
|
||||
omitted: list = []
|
||||
before, before_path = self.part(
|
||||
inputs.get("prepend_text", ""), depth, omitted
|
||||
)
|
||||
after, after_path, _ = self.text(
|
||||
inputs.get("append_text", ""), "string", depth + 1
|
||||
after, after_path = self.part(
|
||||
inputs.get("append_text", ""), depth, omitted
|
||||
)
|
||||
self.scan(
|
||||
len(before)
|
||||
@@ -560,6 +573,8 @@ class _GraphResolver:
|
||||
)
|
||||
)
|
||||
path = before_path + path + after_path
|
||||
if omitted:
|
||||
self.text_sources[(node_id, slot)] = RUNTIME_PARTS_SOURCE
|
||||
elif (
|
||||
name in ("StringConcatenate", "Text Concatenate")
|
||||
and slot == 0
|
||||
@@ -629,6 +644,30 @@ class _GraphResolver:
|
||||
self.tainted = outer_tainted or self.tainted
|
||||
self.active.discard(key)
|
||||
|
||||
def part(self, value: Any, depth: int, omitted: list) -> Tuple[str, list]:
|
||||
"""A PromptManager prepend or append: traced, or left out when run time.
|
||||
|
||||
Only with the semantics table (inspection), and only when the input is
|
||||
a link to a node whose output is decided at run time (``runtime``, such
|
||||
as LoraManager's TriggerWord Toggle): that part is not in the file, so
|
||||
it is left out and recorded in ``omitted``. Anything else, including a
|
||||
run-time node further upstream, keeps its trace or its failure.
|
||||
"""
|
||||
try:
|
||||
text, path, _ = self.text(value, "string", depth + 1)
|
||||
except _Unresolved as error:
|
||||
if (
|
||||
self.semantics is None
|
||||
or error.code != "runtime-value"
|
||||
or not _link(value)
|
||||
or error.node is None
|
||||
or error.node["id"] != str(value[0])[:_CLASS_NAME_LENGTH]
|
||||
):
|
||||
raise
|
||||
omitted.append(str(value[0]))
|
||||
return "", []
|
||||
return text, path
|
||||
|
||||
def historical(
|
||||
self, adapter: OutputAdapter, inputs: dict, depth: int
|
||||
) -> Tuple[str, list, str]:
|
||||
@@ -1539,7 +1578,13 @@ class _GraphResolver:
|
||||
reason=absent_reason,
|
||||
)
|
||||
negative = self.side(inputs[negative_name], sampler_id, guider_id, depth)
|
||||
return _result("resolved", positive, negative, sampler_id)
|
||||
return _result(
|
||||
"resolved",
|
||||
positive,
|
||||
negative,
|
||||
sampler_id,
|
||||
_parts_reason(positive, negative),
|
||||
)
|
||||
except _Unresolved as error:
|
||||
return _result(
|
||||
"unresolved",
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
"ControlNetApplyAdvanced", "ControlNetApplySD3", "InpaintModelConditioning", "WanImageToVideo", "ConditioningZeroOut",
|
||||
"CLIPTextEncode", "CLIPTextEncodeSDXL", "CLIPTextEncodeFlux", "CLIPTextEncodeSD3", "TextEncodeQwenImageEdit",
|
||||
"TextEncodeQwenImageEditPlus", "TextEncodeQwenImage21", "KikoTextEncode", "StringConstant", "StringConstantMultiline",
|
||||
"JoinStringMulti", "PromptSearchList", "PreviewImage", "PreviewAny", "SaveImageWebsocket",
|
||||
"JoinStringMulti", "PromptSearchList", "TriggerWord Toggle (LoraManager)", "PreviewImage", "PreviewAny", "SaveImageWebsocket",
|
||||
"Image Comparer (rgthree)", "DetailerForEach", "UltimateSDUpscale", "UltimateSDUpscaleNoUpscale",
|
||||
"ControlNetInpaintingAliMamaApply", "LTXVConditioning", "FluxKontextMultiReferenceLatentMethod", "Text to Conditioning",
|
||||
"TextInput", "DisplayText", "ClownsharKSampler_Beta", "KSampler (Efficient)", "ttN KSampler_v2",
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"runtime": ["PromptSearchList"],
|
||||
"runtime": ["PromptSearchList", "TriggerWord Toggle (LoraManager)"],
|
||||
"savers": {
|
||||
"SaveImage": {
|
||||
"strategy": "comfy-counter",
|
||||
|
||||
@@ -156,7 +156,8 @@ function generationFacts(record, listClass, noteClass) {
|
||||
return [facts, explanation];
|
||||
}
|
||||
// D21: the panel badge names the strongest resolved prompt source; any other source shows no badge.
|
||||
const EVIDENCE_BADGES = [['execution', 'Execution recorded'], ['graph-inferred', 'Workflow inferred'], ['embedded-parameters', 'Embedded parameters']];
|
||||
// A prompt that left out a run-time PromptManager part (D-NEW-R1) is still inferred from the workflow.
|
||||
const EVIDENCE_BADGES = [['execution', 'Execution recorded'], ['graph-inferred', 'Workflow inferred'], ['runtime-parts-omitted', 'Workflow inferred'], ['embedded-parameters', 'Embedded parameters']];
|
||||
function evidenceBadge(prompts) {
|
||||
const sources = new Set(Object.values(prompts).filter(role => role?.status === 'resolved').map(role => role.source));
|
||||
return EVIDENCE_BADGES.find(([source]) => sources.has(source))?.[1] ?? null;
|
||||
|
||||
@@ -32,6 +32,8 @@ export const EMPTY_POSITIVE_REASON = 'Positive prompt is empty';
|
||||
export const IDENTICAL_BRANCHES_REASON = 'Every sampler branch uses the same prompt pair';
|
||||
export const MULTISTAGE_UNATTRIBUTED_REASON = 'Multistage sampler ancestry is ambiguous';
|
||||
export const ADDED_CONDITIONING_REASON = 'A later stage adds conditioning to the base prompt';
|
||||
export const RUNTIME_PARTS_SOURCE = 'runtime-parts-omitted';
|
||||
export const RUNTIME_PARTS_REASON = 'Text added at run time, such as trigger words, is not included';
|
||||
export const MALFORMED_EVIDENCE_REASON = 'Prompt evidence is malformed';
|
||||
export const UNREADABLE_METADATA_REASON = 'Media metadata could not be read';
|
||||
export const SEMANTICS_UNAVAILABLE_REASON = 'Inspection node semantics are unavailable';
|
||||
|
||||
@@ -39,6 +39,8 @@ import {
|
||||
explained,
|
||||
isLink,
|
||||
nodeRef,
|
||||
RUNTIME_PARTS_REASON,
|
||||
RUNTIME_PARTS_SOURCE,
|
||||
readPairExecution,
|
||||
rerouteValue,
|
||||
result,
|
||||
@@ -168,11 +170,13 @@ class GraphResolver {
|
||||
[text, path, storageText] = this.text(pyGet(inputs, adapted.inputName), 'string', depth + 1);
|
||||
if (adapted.compose || adapted.storageInput) storageText = text;
|
||||
if (adapted.compose) {
|
||||
const [before, beforePath] = this.text(pyGet(inputs, 'prepend_text', ''), 'string', depth + 1);
|
||||
const [after, afterPath] = this.text(pyGet(inputs, 'append_text', ''), 'string', depth + 1);
|
||||
const omitted = [];
|
||||
const [before, beforePath] = this.part(pyGet(inputs, 'prepend_text', ''), depth, omitted);
|
||||
const [after, afterPath] = this.part(pyGet(inputs, 'append_text', ''), depth, omitted);
|
||||
this.scan(pyLen(before) + pyLen(after) + beforePath.length + path.length + afterPath.length);
|
||||
text = this.built(this.bounded([pyStrip(before), text, pyStrip(after)].filter(part => part).join(' ')));
|
||||
path = [...beforePath, ...path, ...afterPath];
|
||||
if (omitted.length) this.textSources.set(slotKey(nodeId, slot), RUNTIME_PARTS_SOURCE);
|
||||
}
|
||||
}
|
||||
} else if ((name === 'StringConcatenate' || name === 'Text Concatenate') && slot === 0 && kind === 'string') {
|
||||
@@ -403,6 +407,19 @@ class GraphResolver {
|
||||
return [text, path, text];
|
||||
}
|
||||
|
||||
/** `part`: a PromptManager prepend or append, traced, or left out when a direct run-time feeder decides it. */
|
||||
part(value, depth, omitted) {
|
||||
try {
|
||||
const [text, path] = this.text(value, 'string', depth + 1);
|
||||
return [text, path];
|
||||
} catch (error) {
|
||||
if (!(error instanceof Unresolved) || error.code !== 'runtime-value' || !isLink(value) || error.node === null ||
|
||||
error.node.id !== pySlice(String(value[0]), CLASS_NAME_LENGTH)) throw error;
|
||||
omitted.push(String(value[0]));
|
||||
return ['', []];
|
||||
}
|
||||
}
|
||||
|
||||
sourceOf(path) {
|
||||
if (!path.length) return 'graph-inferred';
|
||||
return this.textSources.get(slotKey(path[0].node_id, path[0].output_slot)) ?? 'graph-inferred';
|
||||
@@ -528,7 +545,8 @@ class GraphResolver {
|
||||
return result('absent', { positive, samplerId, reason: absentReason });
|
||||
}
|
||||
negative = this.side(inputs.get(negativeName), samplerId, guiderId, depth);
|
||||
return result('resolved', { positive, negative, samplerId });
|
||||
const omitted = [positive, negative].some(side => side.provenance.text_source === RUNTIME_PARTS_SOURCE);
|
||||
return result('resolved', { positive, negative, samplerId, reason: omitted ? RUNTIME_PARTS_REASON : '' });
|
||||
} catch (error) {
|
||||
if (!(error instanceof Unresolved)) throw error;
|
||||
return result('unresolved', { positive, negative, samplerId, reason: error.message, reasonCode: error.code, node: error.node });
|
||||
|
||||
@@ -15,6 +15,10 @@ const statuses = {
|
||||
ambiguous: 'Multiple possible prompts. Source could not be determined.',
|
||||
unresolved: 'Prompt could not be resolved.',
|
||||
};
|
||||
// D-NEW-R1 (placeholder copy, not in the concept; maintainer option A): a PromptManager prepend or append that a
|
||||
// run-time node decided (LoRA trigger words) is not in the file, so the shown text leaves it out and says so.
|
||||
export const RUNTIME_PARTS_SOURCE = 'runtime-parts-omitted';
|
||||
export const RUNTIME_PARTS_NOTE = 'Text added at run time, such as trigger words, is not included.';
|
||||
const ROLE_LABELS = Object.freeze({positive: '+ Positive prompt', negative: '\u2212 Negative prompt'});
|
||||
function el(tag, text, className) {
|
||||
const node = document.createElement(tag);
|
||||
@@ -114,6 +118,7 @@ export function mountPromptBlocks(container, {prompts, sender, parentScope, role
|
||||
// Production keeps a confirmed prompt keyboard-reachable for reading and selecting long text.
|
||||
text.tabIndex = 0;
|
||||
block.append(text);
|
||||
if (record?.source === RUNTIME_PARTS_SOURCE) block.append(el('p', RUNTIME_PARTS_NOTE, 'small muted'));
|
||||
} else block.append(el('p', typeof record?.message === 'string' && record.message ? record.message : statuses[record?.status] || statuses.unresolved, 'prompt-text muted'));
|
||||
container.append(block);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user