diff --git a/tests/test_web_modal_toggle.py b/tests/test_web_modal_toggle.py index 83e425f..37bd083 100644 --- a/tests/test_web_modal_toggle.py +++ b/tests/test_web_modal_toggle.py @@ -44,6 +44,7 @@ def test_remote_modal_status_tracks_active_node_ids() -> None: source = _modal_toggle_source() assert "activeNodeId: null" in source + assert "hasStreamedProgress: false" in source assert "function setPromptActiveNode(promptId, activeNodeId)" in source assert "detail.active_node_id" in source assert "clearPromptRemoteStates(promptId)" in source @@ -59,3 +60,11 @@ def test_remote_modal_uses_distinct_ready_active_and_complete_colors() -> None: assert 'const STATE_READY = "ready";' in source assert 'const STATE_ACTIVE = "active";' in source assert 'detail.phase === "execution_success"' in source + + +def test_streamed_modal_progress_takes_precedence_over_proxy_events() -> None: + """Once streamed node progress starts, coarse proxy execution events should stop overriding it.""" + source = _modal_toggle_source() + + assert "promptState.hasStreamedProgress = true;" in source + assert "if (promptState.hasStreamedProgress && phase !== STATE_ERROR)" in source diff --git a/web/modal_toggle.js b/web/modal_toggle.js index 758d087..38266f6 100644 --- a/web/modal_toggle.js +++ b/web/modal_toggle.js @@ -248,6 +248,7 @@ function ensurePromptState(promptId) { remoteNodeIds: [], componentsByRepresentative: new Map(), activeNodeId: null, + hasStreamedProgress: false, }); } return modalPromptStates.get(promptId); @@ -371,14 +372,19 @@ function setNodesPhase(nodeIds, phase, promptId, errorMessage) { */ function registerPromptComponents(promptId, remoteNodeIds, components) { const promptState = ensurePromptState(promptId); - promptState.remoteNodeIds = [...remoteNodeIds]; - promptState.componentsByRepresentative.clear(); - promptState.activeNodeId = null; - for (const component of components) { - promptState.componentsByRepresentative.set( - String(component.representative_node_id), - component.node_ids.map((nodeIdValue) => String(nodeIdValue)), - ); + if (remoteNodeIds.length > 0) { + promptState.remoteNodeIds = [...remoteNodeIds]; + } + if (components.length > 0) { + promptState.componentsByRepresentative.clear(); + promptState.activeNodeId = null; + promptState.hasStreamedProgress = false; + for (const component of components) { + promptState.componentsByRepresentative.set( + String(component.representative_node_id), + component.node_ids.map((nodeIdValue) => String(nodeIdValue)), + ); + } } } @@ -595,6 +601,7 @@ function handleModalStatus(event) { const nextActiveNodeId = detail.active_node_id != null ? String(detail.active_node_id) : null; const previousActiveNodeId = promptState.activeNodeId; + promptState.hasStreamedProgress = true; setGlobalStatusPhase(promptId, EXECUTION_PHASE, nodeIds.length); setNodesPhase(nodeIds, STATE_READY, promptId); if (previousActiveNodeId && previousActiveNodeId !== nextActiveNodeId) { @@ -608,6 +615,7 @@ function handleModalStatus(event) { } if (detail.phase === "execution_success") { + promptState.hasStreamedProgress = true; if (promptState.activeNodeId) { setNodesPhase([promptState.activeNodeId], STATE_COMPLETE, promptId); } @@ -634,6 +642,10 @@ function handleExecutionPhase(event, phase) { if (!componentNodeIds) { return; } + const promptState = ensurePromptState(promptId); + if (promptState.hasStreamedProgress && phase !== STATE_ERROR) { + return; + } if (phase === EXECUTION_PHASE) { setGlobalStatusPhase(promptId, EXECUTION_PHASE, componentNodeIds.length); setNodesPhase(componentNodeIds, STATE_READY, promptId, detail.exception_message);