Address PR review feedback (#1433)

- always dispose AI design session (try/finally) + rejecting-agent test
- prune expired design rate-limit entries per check
- apply workflowId precedence in createTaskWithReservedId
- compare-and-set migrated project default (no pre-transaction snapshot)
- reject fragment ids in setDefaultWorkflowId at the write boundary
- guard stripApprovalBypassFlags against malformed template nodes
- hoist duplicate stepToFragmentIr call in migration loop
- auto-layout preserves unplaced nodes (strictColumnForY)
- second same-pair connection births a failure edge for condition-capable sources
- insertFragment expands foreach templates into child nodes (lossless round-trip)
- copyIrWithFreshIds remaps namespaced foreach child layout keys
- discard stale AI-edit results after workflow switch
- i18n: returnEmptyString:false so empty locale placeholders fall back to en
- test fixes: schema-version titles, migrate idempotency assertion, TaskForm post-resolve + cross-mode regression, deterministic waits
This commit is contained in:
gsxdsm
2026-06-05 13:56:05 -07:00
parent 58902ae38c
commit 563cb5b372
19 changed files with 518 additions and 63 deletions

View File

@@ -743,6 +743,11 @@ function InnerEditor({
const activeWorkflow = useMemo(() => workflows.find((w) => w.id === activeId), [workflows, activeId]);
const isBuiltin = !!activeWorkflow && isBuiltinWorkflowId(activeWorkflow.id);
// Live mirror of the active workflow id, readable inside async callbacks that
// captured an earlier value before an await (e.g. the AI-design round-trip).
const activeIdRef = useRef(activeId);
activeIdRef.current = activeId;
// Trivial-graph palette hint (R9): a user-owned workflow whose graph carries no
// user-authored node yet (everything is start/end/column-band — column bands map
// to data.kind "start"). Disappears as soon as any user node exists; never shows
@@ -1216,14 +1221,26 @@ function InnerEditor({
}
const controller = new AbortController();
aiEditAbortRef.current = controller;
// Capture the target workflow up-front: if the user switches the active
// workflow during the (long) design round-trip, we must NOT apply the result
// to whatever workflow happens to be active when it resolves.
const targetWorkflow = activeWorkflow;
setAiEditBusy(true);
setAiEditError(null);
try {
const result = await designWorkflow(
{ prompt: trimmed, workflowId: activeWorkflow.id },
{ prompt: trimmed, workflowId: targetWorkflow.id },
projectId,
controller.signal,
);
// The active workflow changed mid-flight → discard the stale result.
if (activeIdRef.current !== targetWorkflow.id) {
addToast(
t("workflows.aiStaleDiscarded", "Discarded AI design — you switched workflows"),
"warning",
);
return;
}
// Always confirm before the destructive replace.
const ok = await confirm({
title: t("workflows.aiReplaceTitle", "Replace graph?"),
@@ -1238,14 +1255,14 @@ function InnerEditor({
// Map the returned IR through irToFlow on a definition-shaped object,
// mirroring the active-workflow load effect's mapping.
const flow = irToFlow({
...activeWorkflow,
...targetWorkflow,
ir: result.ir,
layout: result.layout,
});
setNodes(flow.nodes);
setEdges(flow.edges);
setColumns(columnsOf({ ...activeWorkflow, ir: result.ir }));
setFields(fieldsOf({ ...activeWorkflow, ir: result.ir }));
setColumns(columnsOf({ ...targetWorkflow, ir: result.ir }));
setFields(fieldsOf({ ...targetWorkflow, ir: result.ir }));
setSelectedNodeId(null);
setSelectedEdgeId(null);
setValidationError(null);