feat: decline pending action

This commit is contained in:
2026-05-05 20:14:06 -04:00
parent 761eda6155
commit 36c91ce500
5 changed files with 69 additions and 1 deletions
+13
View File
@@ -61,6 +61,19 @@ async def test_draft_message_creates_pending_action():
assert pending["id"] in registry.pending_actions 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(): def test_uex_client_uses_bearer_and_secret_headers():
client = UEXClient("https://api.uexcorp.space/2.0", secret_key="secret", bearer_token="bearer") client = UEXClient("https://api.uexcorp.space/2.0", secret_key="secret", bearer_token="bearer")
+4
View File
@@ -145,6 +145,10 @@ def create_app() -> FastAPI:
async def approve(action_id: str) -> dict: async def approve(action_id: str) -> dict:
return await tools.approve(action_id) 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 return app
+14
View File
@@ -241,6 +241,20 @@ class ToolRegistry:
return {"error": f"Pending action not found: {action_id}"} return {"error": f"Pending action not found: {action_id}"}
return await self.uex.post(action.endpoint, action.payload, authenticated=True) 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( async def search_marketplace_listings(
self, self,
query: str | None = None, query: str | None = None,
+26 -1
View File
@@ -299,7 +299,14 @@ function renderPending(actions) {
const approve = document.createElement("button"); const approve = document.createElement("button");
approve.textContent = "Approve"; approve.textContent = "Approve";
approve.addEventListener("click", () => approveAction(action.id)); 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); 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() { async function refreshPending() {
const response = await fetch("/api/pending-actions"); const response = await fetch("/api/pending-actions");
const result = await response.json(); const result = await response.json();
+12
View File
@@ -447,11 +447,23 @@ pre {
font-size: 12px; font-size: 12px;
} }
.pending-controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.pending-card button { .pending-card button {
width: 100%; width: 100%;
padding: 10px; padding: 10px;
} }
.decline-button {
border: 1px solid var(--border);
background: transparent;
color: var(--muted);
}
@media (max-width: 860px) { @media (max-width: 860px) {
.shell { .shell {
grid-template-columns: 1fr; grid-template-columns: 1fr;