From fd50561af636bfd88d97c0b61fe2c5d8ff7c18e8 Mon Sep 17 00:00:00 2001 From: RaresKeY <158580472+RaresKeY@users.noreply.github.com> Date: Sat, 15 Aug 2026 05:51:54 +0000 Subject: [PATCH] fix(ui): complete exact approval continuation --- static/app.js | 4 +-- static/index.html | 10 ++++---- static/js/chat.js | 30 +++++++++++++++++----- static/js/chatRenderer.js | 9 ++++--- static/js/sessions.js | 2 +- tests/test_agent_rounds_exhausted.py | 18 ++++++++++--- tests/test_external_context_tool_gate.py | 7 +++++ tests/test_startup_session_bootstrap_js.py | 2 +- 8 files changed, 59 insertions(+), 23 deletions(-) diff --git a/static/app.js b/static/app.js index 914d66cec..d232e78a2 100644 --- a/static/app.js +++ b/static/app.js @@ -10,7 +10,7 @@ import modelsModule from './js/models.js?v=20260715startupcalm2'; import ragModule from './js/rag.js'; import presetsModule from './js/presets.js'; import searchModule from './js/search.js'; -import chatModule from './js/chat.js?v=20260815toolapproval1'; +import chatModule from './js/chat.js?v=20260815toolapproval2'; import compareModule from './js/compare/index.js?v=20260723compareicon2'; import documentModule from './js/document.js?v=20260722emailfastindex1'; import searchChatModule from './js/search-chat.js'; @@ -22,7 +22,7 @@ import { settleSessionHydration } from './js/startupShell.js'; import markdownModule from './js/markdown.js'; -import chatRenderer from './js/chatRenderer.js?v=20260815toolapproval1'; +import chatRenderer from './js/chatRenderer.js?v=20260815toolapproval2'; import sessionModule from './js/sessions.js'; import memoryModule from './js/memory.js?v=20260722memoryloading1'; import voiceRecorderModule from './js/voiceRecorder.js'; diff --git a/static/index.html b/static/index.html index a1c937ea3..6c6c9b668 100644 --- a/static/index.html +++ b/static/index.html @@ -258,8 +258,8 @@ - - + + @@ -2534,10 +2534,10 @@ - + - + @@ -2545,7 +2545,7 @@ - + diff --git a/static/js/chat.js b/static/js/chat.js index e7a9ff748..45a7ad9a1 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -8,7 +8,7 @@ import Storage from './storage.js'; import uiModule from './ui.js'; import sessionModule from './sessions.js'; -import chatRenderer from './chatRenderer.js?v=20260815toolapproval1'; +import chatRenderer from './chatRenderer.js?v=20260815toolapproval2'; import chatStream from './chatStream.js'; import { addAITTSButton } from './tts-ai.js'; import markdownModule from './markdown.js'; @@ -61,6 +61,24 @@ import { createTerminalStreamError, isRecoverableStreamError } from './chatStrea let _contextHeaderBound = false; let _pendingToolApproval = null; + function _submitToolApprovalWhenIdle(approvalId, label) { + if ( + !_pendingToolApproval + || _pendingToolApproval.approval_id !== approvalId + ) return; + if (isStreaming || _sendInFlight) { + setTimeout(() => _submitToolApprovalWhenIdle(approvalId, label), 120); + return; + } + const input = document.getElementById('message'); + if (input) { + input.value = label; + input.dispatchEvent(new Event('input', { bubbles: true })); + } + const sendButton = document.querySelector('.send-btn'); + if (sendButton) sendButton.click(); + } + document.addEventListener('odysseus:tool-approval', (event) => { const detail = event && event.detail ? event.detail : {}; const decision = String(detail.decision || '').toLowerCase(); @@ -69,12 +87,10 @@ import { createTerminalStreamError, isRecoverableStreamError } from './chatStrea approval_id: String(detail.approval_id), decision, }; - const input = document.getElementById('message'); - if (input) { - input.value = detail.label || (decision === 'approve' ? 'Allow once' : 'Deny'); - } - const sendButton = document.querySelector('.send-btn'); - if (sendButton) sendButton.click(); + _submitToolApprovalWhenIdle( + _pendingToolApproval.approval_id, + detail.label || (decision === 'approve' ? 'Allow once' : 'Deny'), + ); }); function _fmtContextNumber(n) { diff --git a/static/js/chatRenderer.js b/static/js/chatRenderer.js index ab62a6654..e00b6f369 100644 --- a/static/js/chatRenderer.js +++ b/static/js/chatRenderer.js @@ -2532,9 +2532,12 @@ export function addMessage(role, content, modelName, metadata) { const toolRounds = Object.keys(toolsByRound).map(Number); const maxRound = Math.max(toolRounds.length ? Math.max(...toolRounds) : 0, roundTexts.length); - for (let r = 0; r < maxRound; r++) { - const roundNum = r + 1; - const txt = resolveDocumentPlaceholderLinks((roundTexts[r] || '').trim(), metadata); + const firstRound = (toolsByRound[0] || []).length ? 0 : 1; + for (let roundNum = firstRound; roundNum <= maxRound; roundNum++) { + const r = roundNum - 1; + const txt = r >= 0 + ? resolveDocumentPlaceholderLinks((roundTexts[r] || '').trim(), metadata) + : ''; if (txt) { const wrap = document.createElement('div'); diff --git a/static/js/sessions.js b/static/js/sessions.js index 9938c8ccc..03c9c4f5b 100644 --- a/static/js/sessions.js +++ b/static/js/sessions.js @@ -3,7 +3,7 @@ import Storage from './storage.js'; import uiModule, { autoResize, styledPrompt } from './ui.js'; -import chatRenderer from './chatRenderer.js?v=20260815toolapproval1'; +import chatRenderer from './chatRenderer.js?v=20260815toolapproval2'; import { providerLogo } from './providers.js'; import { initModelPicker, updateModelPicker } from './modelPicker.js?v=20260722ctxheader1'; import themeModule from './theme.js'; diff --git a/tests/test_agent_rounds_exhausted.py b/tests/test_agent_rounds_exhausted.py index b79245170..7dc8e92a3 100644 --- a/tests/test_agent_rounds_exhausted.py +++ b/tests/test_agent_rounds_exhausted.py @@ -37,7 +37,7 @@ def _patch_common(monkeypatch): monkeypatch.setattr(al, "estimate_tokens", lambda *a, **k: 10, raising=False) async def _fake_exec(block, *a, **k): - return ("bash", {"output": "ok", "exit_code": 0}) + return (block.tool_type, {"output": "ok", "exit_code": 0}) monkeypatch.setattr(al, "execute_tool_block", _fake_exec, raising=False) @@ -58,8 +58,14 @@ def _run_loop(monkeypatch, round_text, max_rounds=2): def test_emits_rounds_exhausted_when_cap_hit_mid_task(monkeypatch): _patch_common(monkeypatch) - # Every round returns a tool block -> never "done" -> loop exhausts the cap. - events = _run_loop(monkeypatch, "```bash\necho hi\n```", max_rounds=2) + # Use a system-owned interaction result so this remains a loop-control test: + # Bash output is workspace-derived and now correctly pauses for exact user + # approval before a later Bash call. + events = _run_loop( + monkeypatch, + '```update_plan\n{"plan":"- [ ] keep going"}\n```', + max_rounds=2, + ) assert any(e.get("type") == "rounds_exhausted" for e in events), events @@ -84,7 +90,11 @@ def test_emits_intent_nudge_exhausted_when_cap_is_exhausted(monkeypatch): def test_emits_loop_breaker_triggered_when_loop_breaker_trips(monkeypatch): _patch_common(monkeypatch) - events = _run_loop(monkeypatch, "```bash\necho hi\n```", max_rounds=6) + events = _run_loop( + monkeypatch, + '```update_plan\n{"plan":"- [ ] keep going"}\n```', + max_rounds=6, + ) guard = next((e for e in events if e.get("type") == "loop_breaker_triggered"), None) assert guard is not None, events diff --git a/tests/test_external_context_tool_gate.py b/tests/test_external_context_tool_gate.py index 808d3e6b0..a69e38303 100644 --- a/tests/test_external_context_tool_gate.py +++ b/tests/test_external_context_tool_gate.py @@ -854,6 +854,7 @@ def test_frontend_tool_approval_uses_opaque_id_and_fixed_decisions(): root = Path(__file__).parents[1] chat = (root / "static/js/chat.js").read_text() renderer = (root / "static/js/chatRenderer.js").read_text() + index = (root / "static/index.html").read_text() assert "fd.append('tool_approval_id'" in chat assert "fd.append('tool_approval_decision'" in chat @@ -861,6 +862,12 @@ def test_frontend_tool_approval_uses_opaque_id_and_fixed_decisions(): assert "aq.kind === 'tool_approval'" in renderer assert "aq.action.content" in renderer assert "decision: String((opt && opt.value)" in renderer + assert "if (isStreaming || _sendInFlight)" in chat + assert "_submitToolApprovalWhenIdle" in chat + assert "input.dispatchEvent(new Event('input'" in chat + assert "const firstRound = (toolsByRound[0] || []).length ? 0 : 1" in renderer + assert index.count("app.js?v=20260815toolapproval2") == 2 + assert "app.js?v=20260808startupshell1" not in index def test_frontend_raw_fences_do_not_call_document_mutators(): diff --git a/tests/test_startup_session_bootstrap_js.py b/tests/test_startup_session_bootstrap_js.py index 184a720e3..86bcae232 100644 --- a/tests/test_startup_session_bootstrap_js.py +++ b/tests/test_startup_session_bootstrap_js.py @@ -23,7 +23,7 @@ _IMPORT_REWRITES = { "import uiModule, { autoResize, styledPrompt } from './ui.js';": ( "import uiModule, { autoResize, styledPrompt } from './ui.mjs';" ), - "import chatRenderer from './chatRenderer.js?v=20260722ctxheader1';": ( + "import chatRenderer from './chatRenderer.js?v=20260815toolapproval2';": ( "import chatRenderer from './chatRenderer.mjs';" ), "import { providerLogo } from './providers.js';": (