diff --git a/app/api/routes_instructions.py b/app/api/routes_instructions.py index 145f254..1937a24 100644 --- a/app/api/routes_instructions.py +++ b/app/api/routes_instructions.py @@ -57,6 +57,14 @@ def update_instruction(instruction_id: str, body: UpdateInstructionRequest): return InstructionCreateResponse(item=item) +@router.delete("/consumed", status_code=200) +def clear_consumed_instructions(): + """Delete all consumed instructions. Pending instructions are never affected.""" + count = instruction_service.clear_consumed() + event_service.broadcast("history.cleared", {"count": count}) + return {"cleared": count} + + @router.delete("/{instruction_id}", status_code=204) def delete_instruction(instruction_id: str): try: diff --git a/app/services/instruction_service.py b/app/services/instruction_service.py index 95d2b1f..830d70f 100644 --- a/app/services/instruction_service.py +++ b/app/services/instruction_service.py @@ -168,6 +168,19 @@ def delete_instruction(instruction_id: str) -> None: logger.info("Instruction deleted id=%s", instruction_id) +def clear_consumed() -> int: + """Delete all consumed instructions. Returns the number of rows deleted.""" + with get_write_conn() as conn: + row = conn.execute( + "SELECT COUNT(*) FROM instructions WHERE status = 'consumed'" + ).fetchone() + count = row[0] if row else 0 + conn.execute("DELETE FROM instructions WHERE status = 'consumed'") + + logger.info("Consumed instructions cleared count=%d", count) + return count + + def consume_next(agent_id: str = "unknown") -> Optional[InstructionItem]: """ Atomically claim and return the next pending instruction. diff --git a/static/index.html b/static/index.html index aa468bb..f1c95f0 100644 --- a/static/index.html +++ b/static/index.html @@ -111,7 +111,13 @@ Consumed - 0 +
+ 0 + +
@@ -151,9 +157,9 @@
- + - Server enforces this as the minimum wait before returning an empty response. Agent may request longer but never shorter. + How long the tool waits for a new instruction before returning an empty response. Set exclusively here — agents cannot override this.
diff --git a/static/js/api.js b/static/js/api.js index 38927c9..cbd4c95 100644 --- a/static/js/api.js +++ b/static/js/api.js @@ -35,6 +35,7 @@ export const api = { createInstruction: (content) => request('POST', '/api/instructions', { content }), updateInstruction: (id, content) => request('PATCH', `/api/instructions/${id}`, { content }), deleteInstruction: (id) => request('DELETE', `/api/instructions/${id}`), + clearConsumed: () => request('DELETE', '/api/instructions/consumed'), // Config getConfig: () => request('GET', '/api/config'), diff --git a/static/js/events.js b/static/js/events.js index 9149929..499aaea 100644 --- a/static/js/events.js +++ b/static/js/events.js @@ -104,6 +104,11 @@ function _handleEvent(event) { } break; } + case 'history.cleared': { + const next = (state.get('instructions') || []).filter(i => i.status !== 'consumed'); + state.set('instructions', next); + break; + } case 'status.changed': { api.status().then(s => state.set('status', s)).catch(console.error); break; diff --git a/static/js/instructions.js b/static/js/instructions.js index 2898c81..c681f63 100644 --- a/static/js/instructions.js +++ b/static/js/instructions.js @@ -162,10 +162,11 @@ function renderConsumedCard(item) { // ── List renderers ──────────────────────────────────────────────────────── export function initInstructions() { - const pendingList = document.getElementById('pending-list'); - const pendingBadge = document.getElementById('pending-badge'); - const consumedList = document.getElementById('consumed-list'); - const consumedBadge = document.getElementById('consumed-badge'); + const pendingList = document.getElementById('pending-list'); + const pendingBadge = document.getElementById('pending-badge'); + const consumedList = document.getElementById('consumed-list'); + const consumedBadge = document.getElementById('consumed-badge'); + const clearHistoryBtn = document.getElementById('clear-history-btn'); function render(instructions) { if (!instructions) return; @@ -190,14 +191,30 @@ export function initInstructions() { consumedList.innerHTML = ''; if (consumed.length === 0) { consumedList.innerHTML = `
No consumed instructions yet
`; + clearHistoryBtn.style.display = 'none'; } else { consumed.forEach(item => consumedList.appendChild(renderConsumedCard(item))); + clearHistoryBtn.style.display = ''; } consumedBadge.textContent = consumed.length; consumedBadge.className = `badge ${consumed.length > 0 ? 'badge--amber' : 'badge--muted'}`; } state.subscribe('instructions', render); + + // Clear history button + clearHistoryBtn.addEventListener('click', async () => { + if (!confirm('Clear all consumed instruction history? This cannot be undone.')) return; + clearHistoryBtn.disabled = true; + try { + const res = await api.clearConsumed(); + toast(`Cleared ${res.cleared} consumed instruction${res.cleared !== 1 ? 's' : ''}`, 'info'); + } catch (e) { + toast(e.message, 'error'); + } finally { + clearHistoryBtn.disabled = false; + } + }); } // ── Composer ──────────────────────────────────────────────────────────────