From 36c91ce50017ae14d3c9e80a74905be9b07f7fbc Mon Sep 17 00:00:00 2001 From: HRiggs Date: Tue, 5 May 2026 20:14:06 -0400 Subject: [PATCH] feat: decline pending action --- tests/test_tools.py | 13 +++++++++++++ traderai/server.py | 4 ++++ traderai/tools.py | 14 ++++++++++++++ web/app.js | 27 ++++++++++++++++++++++++++- web/styles.css | 12 ++++++++++++ 5 files changed, 69 insertions(+), 1 deletion(-) diff --git a/tests/test_tools.py b/tests/test_tools.py index afd7f44..7d0d523 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -61,6 +61,19 @@ async def test_draft_message_creates_pending_action(): assert pending["id"] in registry.pending_actions +@pytest.mark.asyncio +async def test_decline_pending_action_removes_without_sending(): + registry = ToolRegistry(FakeUEX()) + result = await registry.draft_negotiation_message(hash="abc", message="Would you take 4500 UEC?") + action_id = result["pending_action"]["id"] + + declined = await registry.decline(action_id) + + assert declined["declined"] is True + assert declined["pending_action"]["id"] == action_id + assert action_id not in registry.pending_actions + + def test_uex_client_uses_bearer_and_secret_headers(): client = UEXClient("https://api.uexcorp.space/2.0", secret_key="secret", bearer_token="bearer") diff --git a/traderai/server.py b/traderai/server.py index ef47546..094a118 100644 --- a/traderai/server.py +++ b/traderai/server.py @@ -145,6 +145,10 @@ def create_app() -> FastAPI: async def approve(action_id: str) -> dict: return await tools.approve(action_id) + @app.post("/api/decline/{action_id}") + async def decline(action_id: str) -> dict: + return await tools.decline(action_id) + return app diff --git a/traderai/tools.py b/traderai/tools.py index aae9963..3a80133 100644 --- a/traderai/tools.py +++ b/traderai/tools.py @@ -241,6 +241,20 @@ class ToolRegistry: return {"error": f"Pending action not found: {action_id}"} return await self.uex.post(action.endpoint, action.payload, authenticated=True) + async def decline(self, action_id: str) -> dict[str, Any]: + action = self.pending_actions.pop(action_id, None) + if not action: + return {"error": f"Pending action not found: {action_id}"} + return { + "declined": True, + "pending_action": { + "id": action.id, + "label": action.label, + "endpoint": action.endpoint, + "payload": action.payload, + }, + } + async def search_marketplace_listings( self, query: str | None = None, diff --git a/web/app.js b/web/app.js index dcffdd5..ba9d0e0 100644 --- a/web/app.js +++ b/web/app.js @@ -299,7 +299,14 @@ function renderPending(actions) { const approve = document.createElement("button"); approve.textContent = "Approve"; approve.addEventListener("click", () => approveAction(action.id)); - card.append(title, endpoint, payload, approve); + const decline = document.createElement("button"); + decline.className = "decline-button"; + decline.textContent = "Decline"; + decline.addEventListener("click", () => declineAction(action.id)); + const controls = document.createElement("div"); + controls.className = "pending-controls"; + controls.append(decline, approve); + card.append(title, endpoint, payload, controls); pendingEl.appendChild(card); } } @@ -318,6 +325,24 @@ async function approveAction(id) { } } +async function declineAction(id) { + statusEl.textContent = "Declining"; + try { + const response = await fetch(`/api/decline/${id}`, { method: "POST" }); + const result = await response.json(); + if (result.error) { + addMessage("assistant warning-message", `Decline failed: ${result.error}`); + } else { + addMessage("assistant", `Declined pending action: ${result.pending_action?.label || id}`); + } + await refreshPending(); + } catch (error) { + addMessage("assistant warning-message", `Decline failed: ${error.message}`); + } finally { + statusEl.textContent = "Ready"; + } +} + async function refreshPending() { const response = await fetch("/api/pending-actions"); const result = await response.json(); diff --git a/web/styles.css b/web/styles.css index 57d436f..1725c74 100644 --- a/web/styles.css +++ b/web/styles.css @@ -447,11 +447,23 @@ pre { font-size: 12px; } +.pending-controls { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} + .pending-card button { width: 100%; padding: 10px; } +.decline-button { + border: 1px solid var(--border); + background: transparent; + color: var(--muted); +} + @media (max-width: 860px) { .shell { grid-template-columns: 1fr;