comfyui-mcp 0.52.180 → 0.52.182
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/orchestrator/panel-tools.js +164 -18
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/services/extra-paths.js +11 -0
- package/dist/services/extra-paths.js.map +1 -1
- package/dist/services/rail-slot-connect.js +195 -0
- package/dist/services/rail-slot-connect.js.map +1 -0
- package/dist/services/slot-compat.js +62 -6
- package/dist/services/slot-compat.js.map +1 -1
- package/package.json +1 -1
|
@@ -47,6 +47,7 @@ import { isPreExecutorRefusal } from "../services/panel-refusal.js";
|
|
|
47
47
|
import { hostLinksAlreadyReindexed, isLandedUnexpose, laterSlotsAfterUnexpose, laterSlotsFromUnexposePayload, unexposeHostLinkShiftNote, } from "../services/unexpose-host-link-shift.js";
|
|
48
48
|
import { retryConnectAgainstLiveGraph } from "../services/connect-live-graph.js";
|
|
49
49
|
import { verifyPrimitiveForceInputAfterConnect } from "../services/primitive-force-input-connect.js";
|
|
50
|
+
import { retryRailSlotConnect } from "../services/rail-slot-connect.js";
|
|
50
51
|
import { retryWildcardSlotConnect } from "../services/wildcard-slot-connect.js";
|
|
51
52
|
import { retryExposeSubgraphInput } from "../services/expose-ae-wildcard.js";
|
|
52
53
|
import { applyLiveRootViewing, callAndRememberViewing, callWithRememberedSubgraph, clearStaleSubgraphIdentity, noteConfirmedViewingFromToolResult, parseViewingScope, } from "../services/subgraph-viewing-scope.js";
|
|
@@ -5911,6 +5912,35 @@ function forgetStaleSubgraphIdentity(ctx) {
|
|
|
5911
5912
|
clearStaleSubgraphIdentity(ctx.tabId);
|
|
5912
5913
|
ctx.bridge?.clearPromotedSubgraphIdentity?.(ctx.tabId);
|
|
5913
5914
|
}
|
|
5915
|
+
/** Tabs whose promoted-container mapping is unverified after a queue-busy
|
|
5916
|
+
* mutation refusal. `graph_get_subgraph` can stay indeterminate for ordinary
|
|
5917
|
+
* root nodes until something walks the live graph (#2730). */
|
|
5918
|
+
const staleSubgraphMappingTabs = new Set();
|
|
5919
|
+
function noteStaleSubgraphMapping(tabId) {
|
|
5920
|
+
if (tabId)
|
|
5921
|
+
staleSubgraphMappingTabs.add(tabId);
|
|
5922
|
+
}
|
|
5923
|
+
export function __resetStaleSubgraphMappingForTest() {
|
|
5924
|
+
staleSubgraphMappingTabs.clear();
|
|
5925
|
+
}
|
|
5926
|
+
/** Root graph identity from a live query even when `is_subgraph` is missing.
|
|
5927
|
+
* A missing container bit is indeterminate, but a published root identity is
|
|
5928
|
+
* still enough to spend one mapping refresh before failing closed (#2730). */
|
|
5929
|
+
function parseRootGraphIdentity(payload) {
|
|
5930
|
+
if (!payload)
|
|
5931
|
+
return null;
|
|
5932
|
+
const viewing = payload.viewing;
|
|
5933
|
+
if (!viewing || typeof viewing !== "object" || Array.isArray(viewing))
|
|
5934
|
+
return null;
|
|
5935
|
+
const rec = viewing;
|
|
5936
|
+
if (rec.scope !== "root")
|
|
5937
|
+
return null;
|
|
5938
|
+
const graphIdentity = rec.graph_identity;
|
|
5939
|
+
if (typeof graphIdentity !== "string" || graphIdentity.length === 0 || graphIdentity.length > 256) {
|
|
5940
|
+
return null;
|
|
5941
|
+
}
|
|
5942
|
+
return graphIdentity;
|
|
5943
|
+
}
|
|
5914
5944
|
/** Read the active viewing scope and node's explicit container bit before
|
|
5915
5945
|
* attempting promoted-widget resolution. The pinpoint detail projection carries
|
|
5916
5946
|
* the node bit even when the widget list is empty (which is how a fresh rgthree
|
|
@@ -6395,6 +6425,23 @@ function definitiveNonPromotedNodeType(res) {
|
|
|
6395
6425
|
const unwrapped = type.slice(1, -1).trim();
|
|
6396
6426
|
return unwrapped || null;
|
|
6397
6427
|
}
|
|
6428
|
+
/** Panel `graph_get_subgraph` flattens parentheses in the node type before
|
|
6429
|
+
* embedding it in the definitive ordinary-node refusal (panel#1941), so a live
|
|
6430
|
+
* `Power Lora Loader (rgthree)` is reported as `Power Lora Loader rgthree`.
|
|
6431
|
+
* The scope probe still publishes the real type. Compare after that flatten;
|
|
6432
|
+
* the write fence must keep the unflattened scope type (#2394). */
|
|
6433
|
+
function flattenDefinitiveOrdinaryNodeType(type) {
|
|
6434
|
+
return type.replace(/[()]/g, " ").replace(/\s+/g, " ").trim();
|
|
6435
|
+
}
|
|
6436
|
+
function sameDefinitiveOrdinaryNodeType(fromSubgraphRead, fromScopeProbe) {
|
|
6437
|
+
if (!fromSubgraphRead)
|
|
6438
|
+
return false;
|
|
6439
|
+
if (fromSubgraphRead === fromScopeProbe)
|
|
6440
|
+
return true;
|
|
6441
|
+
const left = flattenDefinitiveOrdinaryNodeType(fromSubgraphRead);
|
|
6442
|
+
const right = flattenDefinitiveOrdinaryNodeType(fromScopeProbe);
|
|
6443
|
+
return left.length > 0 && left === right;
|
|
6444
|
+
}
|
|
6398
6445
|
/** The promoted-write refresh is deliberately narrower than the broad panel
|
|
6399
6446
|
* call retry classifier. Only known transport/reconnect shapes may consume the
|
|
6400
6447
|
* one explicit refresh; application refusals must not be replayed. */
|
|
@@ -6627,12 +6674,15 @@ async function readPromotedTargetScope(ctx, nodeId, options = PROMOTED_PREFLIGHT
|
|
|
6627
6674
|
limit: 1,
|
|
6628
6675
|
}, undefined, undefined, undefined, options);
|
|
6629
6676
|
if (probe.isError)
|
|
6630
|
-
return null;
|
|
6677
|
+
return { scope: null, rootGraphIdentity: null };
|
|
6631
6678
|
const payload = parseToolResultJson(probe);
|
|
6632
6679
|
if (parseViewingScope(payload?.viewing)?.scope === "root") {
|
|
6633
6680
|
rememberLiveRootViewing(ctx, payload?.viewing);
|
|
6634
6681
|
}
|
|
6635
|
-
return
|
|
6682
|
+
return {
|
|
6683
|
+
scope: parseVerifiedQueriedNodeScope(payload, nodeId),
|
|
6684
|
+
rootGraphIdentity: parseRootGraphIdentity(payload),
|
|
6685
|
+
};
|
|
6636
6686
|
}
|
|
6637
6687
|
function currentPromotedBindingError(ctx, binding) {
|
|
6638
6688
|
if (ctx.tabId !== binding.tabId)
|
|
@@ -7158,12 +7208,43 @@ async function preparePromotedWidgetWrite(ctx, nodeId, widget) {
|
|
|
7158
7208
|
return promotedWriteRefusal(widget, "the panel connection identity could not be read");
|
|
7159
7209
|
}
|
|
7160
7210
|
}
|
|
7211
|
+
// #2730 — after a queue-busy mutation refusal the panel's subgraph registry
|
|
7212
|
+
// can stay stale until a graph walk. Refresh once before classifying, then
|
|
7213
|
+
// re-probe so an ordinary root node can take the fast path without a manual
|
|
7214
|
+
// panel_graph_outline.
|
|
7215
|
+
let mappingRefreshed = false;
|
|
7216
|
+
const refreshSubgraphMapping = async () => {
|
|
7217
|
+
if (mappingRefreshed)
|
|
7218
|
+
return null;
|
|
7219
|
+
mappingRefreshed = true;
|
|
7220
|
+
const outline = await ctx.call({ cmd: "graph_outline" }, undefined, undefined, undefined, PROMOTED_PREFLIGHT_READ_OPTIONS);
|
|
7221
|
+
if (outline.isError) {
|
|
7222
|
+
return promotedWriteRefusal(widget, "graph_outline could not refresh the subgraph mapping");
|
|
7223
|
+
}
|
|
7224
|
+
staleSubgraphMappingTabs.delete(ctx.tabId);
|
|
7225
|
+
const payload = parseToolResultJson(outline);
|
|
7226
|
+
if (parseViewingScope(payload?.viewing)?.scope === "root") {
|
|
7227
|
+
rememberLiveRootViewing(ctx, payload?.viewing);
|
|
7228
|
+
forgetStaleSubgraphIdentity(ctx);
|
|
7229
|
+
}
|
|
7230
|
+
const drift = panelBindingDriftReason(ctx, "after the subgraph mapping refresh", hasIdentityApi, identityBefore, tabBefore);
|
|
7231
|
+
if (drift)
|
|
7232
|
+
return ordinaryBindingRefusal(widget, drift);
|
|
7233
|
+
return null;
|
|
7234
|
+
};
|
|
7235
|
+
if (staleSubgraphMappingTabs.has(ctx.tabId)) {
|
|
7236
|
+
const refreshError = await refreshSubgraphMapping();
|
|
7237
|
+
if (refreshError)
|
|
7238
|
+
return refreshError;
|
|
7239
|
+
}
|
|
7161
7240
|
// #2394 — a root-level rgthree Power Lora Loader may have no lora_N widget
|
|
7162
7241
|
// yet. Prove the addressed node is an ordinary root node before asking the
|
|
7163
7242
|
// promoted-container classifier to resolve a target that is intentionally
|
|
7164
7243
|
// absent. An unreadable scope probe is not permission for an outer write;
|
|
7165
7244
|
// it falls through to the existing conservative graph_get_subgraph path.
|
|
7166
|
-
|
|
7245
|
+
let targetProbe = await readPromotedTargetScope(ctx, nodeId, PROMOTED_PREFLIGHT_READ_OPTIONS);
|
|
7246
|
+
let targetScope = targetProbe.scope;
|
|
7247
|
+
const rootGraphIdentity = targetProbe.rootGraphIdentity;
|
|
7167
7248
|
// #2518 — a live root query names the current root workflow instance. Do not
|
|
7168
7249
|
// enter the promoted-subgraph identity path for an ordinary (or unproven)
|
|
7169
7250
|
// root node; a truncated StringConcatenate pinpoint used to fall through,
|
|
@@ -7199,7 +7280,7 @@ async function preparePromotedWidgetWrite(ctx, nodeId, widget) {
|
|
|
7199
7280
|
if (!targetScope.nodeIdentity)
|
|
7200
7281
|
return null;
|
|
7201
7282
|
const expectedNodeType = definitiveNonPromotedNodeType(sub);
|
|
7202
|
-
if (expectedNodeType
|
|
7283
|
+
if (!sameDefinitiveOrdinaryNodeType(expectedNodeType, targetScope.nodeType)) {
|
|
7203
7284
|
return promotedWriteRefusal(widget, "the ordinary node type changed between the scope and subgraph reads");
|
|
7204
7285
|
}
|
|
7205
7286
|
if (ctx.tabExpectedNodeTypeFenceCapability?.() !== true) {
|
|
@@ -7219,6 +7300,23 @@ async function preparePromotedWidgetWrite(ctx, nodeId, widget) {
|
|
|
7219
7300
|
return promotedSubgraphReadRefusal(widget, sub, "graph_get_subgraph could not determine whether the addressed node is a promoted container");
|
|
7220
7301
|
}
|
|
7221
7302
|
if (firstReadKind === "permanent") {
|
|
7303
|
+
// #2730 — a stale subgraph registry is a permanent-looking miss for an
|
|
7304
|
+
// ordinary root node. Refresh the mapping once and re-probe. Still
|
|
7305
|
+
// unverifiable → refuse. Do not spend the transient subgraph re-read on
|
|
7306
|
+
// a permanent application error.
|
|
7307
|
+
if (!mappingRefreshed && rootGraphIdentity !== null) {
|
|
7308
|
+
const refreshError = await refreshSubgraphMapping();
|
|
7309
|
+
if (refreshError)
|
|
7310
|
+
return refreshError;
|
|
7311
|
+
targetProbe = await readPromotedTargetScope(ctx, nodeId, PROMOTED_PREFLIGHT_READ_OPTIONS);
|
|
7312
|
+
targetScope = targetProbe.scope;
|
|
7313
|
+
if (targetScope?.activeView === "root" && targetScope.node === "ordinary") {
|
|
7314
|
+
const driftAfterMapping = panelBindingDriftReason(ctx, "after the subgraph mapping refresh", hasIdentityApi, identityBefore, tabBefore);
|
|
7315
|
+
if (driftAfterMapping)
|
|
7316
|
+
return ordinaryBindingRefusal(widget, driftAfterMapping);
|
|
7317
|
+
return ordinaryWritePlanFromScope(widget, targetScope, tabBefore, identityBefore);
|
|
7318
|
+
}
|
|
7319
|
+
}
|
|
7222
7320
|
return promotedSubgraphReadRefusal(widget, sub, "graph_get_subgraph could not determine whether the addressed node is a promoted container");
|
|
7223
7321
|
}
|
|
7224
7322
|
// A binding/subgraph registration transition can make the first read
|
|
@@ -9985,7 +10083,21 @@ function refreshFenceFromOwnReply(ctx, reply) {
|
|
|
9985
10083
|
if (!uuid)
|
|
9986
10084
|
return null;
|
|
9987
10085
|
try {
|
|
9988
|
-
|
|
10086
|
+
const destPath = saveDestWorkflowPath(parsed);
|
|
10087
|
+
const destTab = destPath ? uniqueLiveTabForSaveDest(ctx, destPath) : undefined;
|
|
10088
|
+
const adopted = refreshWorkflowUuid(ctx, parsed);
|
|
10089
|
+
// #2768 — a SCOPE ctx.tabId never becomes dest. workflow_list stamps the
|
|
10090
|
+
// routed tab's advertised identity (#1815), so dest must hold the new uuid
|
|
10091
|
+
// or the next list/current-mode call is refused as a stale instance.
|
|
10092
|
+
if (destTab && destTab !== ctx.tabId) {
|
|
10093
|
+
try {
|
|
10094
|
+
ctx.bridge.refreshWorkflowUuid?.(destTab, uuid);
|
|
10095
|
+
}
|
|
10096
|
+
catch {
|
|
10097
|
+
// dest restamp is best-effort; the session fence is the ctx adoption
|
|
10098
|
+
}
|
|
10099
|
+
}
|
|
10100
|
+
return adopted ? { status: "refreshed", uuid, before } : null;
|
|
9989
10101
|
}
|
|
9990
10102
|
catch {
|
|
9991
10103
|
return null; // never surface a throw here as worse than the existing fallback
|
|
@@ -10071,6 +10183,25 @@ function routingIsUnsavedPredecessor(ctx) {
|
|
|
10071
10183
|
const pin = ctx.workflowTarget?.get(ctx.tabId);
|
|
10072
10184
|
return typeof pin?.path === "string" && pin.path.startsWith("tmp:");
|
|
10073
10185
|
}
|
|
10186
|
+
/** The saved path this session was bound to before Save-As, if any. */
|
|
10187
|
+
function sessionSavedPath(ctx) {
|
|
10188
|
+
const pin = ctx.workflowTarget?.get(ctx.tabId);
|
|
10189
|
+
if (typeof pin?.path === "string" && pin.path.trim() && !pin.path.startsWith("tmp:")) {
|
|
10190
|
+
return canonicalSavedWorkflowPath(pin.path) ?? pin.path.trim();
|
|
10191
|
+
}
|
|
10192
|
+
return savedPathFromTabId(ctx.tabId) ?? undefined;
|
|
10193
|
+
}
|
|
10194
|
+
/**
|
|
10195
|
+
* #2768 — this Save-As replaced the canvas this session was editing.
|
|
10196
|
+
* `workflow_instance_changed` is the panel's own signal; dest path ≠ the
|
|
10197
|
+
* session's saved path is the same fact on a reply that omits the flag.
|
|
10198
|
+
*/
|
|
10199
|
+
function saveReplacedSessionCanvas(ctx, parsed, destPath) {
|
|
10200
|
+
if (parsed.workflow_instance_changed === true)
|
|
10201
|
+
return true;
|
|
10202
|
+
const from = sessionSavedPath(ctx);
|
|
10203
|
+
return Boolean(from && from !== destPath);
|
|
10204
|
+
}
|
|
10074
10205
|
function destPinnedTarget(pinned, destPath, parsed) {
|
|
10075
10206
|
let filename;
|
|
10076
10207
|
for (const v of [parsed.filename, parsed.workflow]) {
|
|
@@ -10106,9 +10237,14 @@ function adoptPinnedDestPath(ctx, destPath, parsed, destTab) {
|
|
|
10106
10237
|
* (`panel_set_todo`, `panel_canvas`) still address the old id.
|
|
10107
10238
|
*
|
|
10108
10239
|
* Follow dest when the current address is dead, when it already aliases onto
|
|
10109
|
-
* dest (same-socket tmp:→wf: / Save-As rename),
|
|
10240
|
+
* dest (same-socket tmp:→wf: / Save-As rename), when it is the unsaved
|
|
10110
10241
|
* predecessor of dest (pinned tmp: canvas whose first save published a
|
|
10111
|
-
* `workflows/…` routing key)
|
|
10242
|
+
* `workflows/…` routing key), or when this Save-As replaced the canvas this
|
|
10243
|
+
* session was editing (#2768 — dest path differs, or the panel set
|
|
10244
|
+
* `workflow_instance_changed`). Leaving the session on the source id after
|
|
10245
|
+
* that is a stale fence: dest is live, the source id may still canReach, and
|
|
10246
|
+
* panel_list_workflows / mode:"current" then fail with instance mismatch.
|
|
10247
|
+
* A live pin on a DIFFERENT saved tab that this save did not replace is left
|
|
10112
10248
|
* alone — that is #1917 / #884.
|
|
10113
10249
|
*
|
|
10114
10250
|
* Matching is on the dest path the save reply proved, and only when exactly
|
|
@@ -10132,7 +10268,10 @@ function repointRoutingAfterSave(ctx, reply) {
|
|
|
10132
10268
|
return;
|
|
10133
10269
|
}
|
|
10134
10270
|
const live = liveRoutingTab(ctx);
|
|
10135
|
-
const follow = !live ||
|
|
10271
|
+
const follow = !live ||
|
|
10272
|
+
live === destTab ||
|
|
10273
|
+
routingIsUnsavedPredecessor(ctx) ||
|
|
10274
|
+
saveReplacedSessionCanvas(ctx, parsed, destPath);
|
|
10136
10275
|
if (!follow)
|
|
10137
10276
|
return;
|
|
10138
10277
|
if (isScopeAddress(ctx.tabId)) {
|
|
@@ -13810,8 +13949,13 @@ export function makePanelToolCtx(bridge, tabId, workflowTargets, onRunTicketOpen
|
|
|
13810
13949
|
// panel#1489 — reads are NOT refused here: they carry no unknown outcome,
|
|
13811
13950
|
// so they are dispatched on the bounded budget above instead.
|
|
13812
13951
|
const blocked = graphCmdBlockedByRunningPrompt(cmd);
|
|
13813
|
-
if (blocked)
|
|
13952
|
+
if (blocked) {
|
|
13953
|
+
// #2730 — a fenced mutation never reached the panel, so the subgraph
|
|
13954
|
+
// registry can stay stale until something walks the live graph. Mark
|
|
13955
|
+
// this tab so the next idle write refreshes mapping once.
|
|
13956
|
+
noteStaleSubgraphMapping(ctx.tabId);
|
|
13814
13957
|
return fail(blocked);
|
|
13958
|
+
}
|
|
13815
13959
|
// #2527 — a timed-out mutation may still be applying. Graph reads wait for
|
|
13816
13960
|
// that settlement (or disclose the outstanding receipt) so they cannot
|
|
13817
13961
|
// certify a stale widget value as current.
|
|
@@ -17643,7 +17787,7 @@ export function buildPanelToolDefs() {
|
|
|
17643
17787
|
return fail(err);
|
|
17644
17788
|
}
|
|
17645
17789
|
}),
|
|
17646
|
-
def("panel_connect", "Connect an output slot of one node to an input slot of another in the user's open graph. Slots accept a name ('MODEL', 'samples') or numeric index. If both slot args are omitted the panel picks the first type-compatible pairing. On failure the error lists every slot with its type and [connected] flag — re-check with panel_query_graph ({ids:[node_id], fields:'detail'}). LiteGraph wildcard-to-wildcard (`*` → `*`) pairings are compatible (a PrimitiveNode 'connect to widget input' output can land on LogicIF.when_true / when_false so the primitive becomes typed from the destination). A frontend PrimitiveNode only serializes through a target widget; connecting one to a forceInput-only / non-widget STRING is refused (panel_run would omit the required input) — use a backend STRING producer such as PrimitiveStringMultiline instead. Undoable.", {
|
|
17790
|
+
def("panel_connect", "Connect an output slot of one node to an input slot of another in the user's open graph. Slots accept a name ('MODEL', 'samples') or numeric index. If both slot args are omitted the panel picks the first type-compatible pairing. On failure the error lists every slot with its type and [connected] flag — re-check with panel_query_graph ({ids:[node_id], fields:'detail'}). LiteGraph wildcard-to-wildcard (`*` → `*`) pairings are compatible (a PrimitiveNode 'connect to widget input' output can land on LogicIF.when_true / when_false so the primitive becomes typed from the destination). An exposed subgraph INT rail (e.g. Scene Seed) is compatible with another INT widget input (LocalWildcardText.seed) — numeric widget min/max/step on the rail socket are not a different type. A frontend PrimitiveNode only serializes through a target widget; connecting one to a forceInput-only / non-widget STRING is refused (panel_run would omit the required input) — use a backend STRING producer such as PrimitiveStringMultiline instead. Undoable.", {
|
|
17647
17791
|
from_node_id: nodeId().describe("Source node id."),
|
|
17648
17792
|
from_output: slotRef
|
|
17649
17793
|
.optional()
|
|
@@ -17676,7 +17820,8 @@ export function buildPanelToolDefs() {
|
|
|
17676
17820
|
};
|
|
17677
17821
|
const call = (cmd, timeoutMs) => ctx.call(cmd, timeoutMs);
|
|
17678
17822
|
const connected = await retryConnectAgainstLiveGraph(connectArgs, call);
|
|
17679
|
-
const
|
|
17823
|
+
const afterRail = await retryRailSlotConnect(connectArgs, connected, call);
|
|
17824
|
+
const afterWildcard = await retryWildcardSlotConnect(connectArgs, afterRail, call);
|
|
17680
17825
|
return verifyPrimitiveForceInputAfterConnect(connectArgs, afterWildcard, call);
|
|
17681
17826
|
}),
|
|
17682
17827
|
def("panel_disconnect", "Disconnect an input slot of a node in the user's open graph. Undoable with Ctrl+Z.", {
|
|
@@ -20505,13 +20650,14 @@ export function buildPanelToolDefs() {
|
|
|
20505
20650
|
// attempting rebindWorkflowFence's independent workflow_list round trip,
|
|
20506
20651
|
// which can be refused by the exact fence this repairs.
|
|
20507
20652
|
//
|
|
20508
|
-
// #2419 — BEFORE the fence refresh so the stamp lands on the
|
|
20509
|
-
// address. Save-As mints a new tab id; the fence repair re-stamps
|
|
20510
|
-
// graph commands, but session-scoped commands (set_todo, graph_canvas
|
|
20511
|
-
// still address the old id unless routing is re-pointed.
|
|
20512
|
-
// when the current address is dead, aliases onto dest,
|
|
20513
|
-
// unsaved tmp: predecessor of dest
|
|
20514
|
-
//
|
|
20653
|
+
// #2419 / #2768 — BEFORE the fence refresh so the stamp lands on the
|
|
20654
|
+
// dest address. Save-As mints a new tab id; the fence repair re-stamps
|
|
20655
|
+
// graph commands, but session-scoped commands (set_todo, graph_canvas,
|
|
20656
|
+
// workflow_list) still address the old id unless routing is re-pointed.
|
|
20657
|
+
// Follow dest when the current address is dead, aliases onto dest, is
|
|
20658
|
+
// the unsaved tmp: predecessor of dest, or this Save-As replaced the
|
|
20659
|
+
// canvas this session was editing. A live pin on a different saved tab
|
|
20660
|
+
// that this save did not replace is left alone (#1917 / #884).
|
|
20515
20661
|
repointRoutingAfterSave(ctx, res);
|
|
20516
20662
|
const fenceRebind = refreshFenceFromOwnReply(ctx, res) ?? (await rebindWorkflowFence(ctx));
|
|
20517
20663
|
let canMutateNow;
|