feat: add Clear History button to delete all consumed instructions
- Backend: instruction_service.clear_consumed() bulk-deletes consumed rows - Backend: DELETE /api/instructions/consumed route (preserves pending) - Frontend: Clear button in consumed panel header (hidden when empty) - Frontend: SSE handler for history.cleared event - instant UI update - Frontend: api.clearConsumed() fetch wrapper
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -111,7 +111,13 @@
|
||||
<svg class="icon icon--sm" viewBox="0 0 24 24"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>
|
||||
Consumed
|
||||
</span>
|
||||
<span id="consumed-badge" class="badge badge--muted">0</span>
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2)">
|
||||
<span id="consumed-badge" class="badge badge--muted">0</span>
|
||||
<button id="clear-history-btn" class="btn btn--ghost btn--sm" title="Clear all consumed instructions" style="display:none">
|
||||
<svg class="icon icon--sm" viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body panel-body--flush">
|
||||
<div id="consumed-list">
|
||||
@@ -151,9 +157,9 @@
|
||||
<div class="panel-body">
|
||||
<form id="config-form">
|
||||
<div class="config-field">
|
||||
<label class="config-label" for="cfg-wait">Min Wait (sec)</label>
|
||||
<label class="config-label" for="cfg-wait">Wait (sec)</label>
|
||||
<input id="cfg-wait" class="input input--sm" type="number" min="0" max="300" value="10" />
|
||||
<span class="config-hint">Server enforces this as the minimum wait before returning an empty response. Agent may request longer but never shorter.</span>
|
||||
<span class="config-hint">How long the tool waits for a new instruction before returning an empty response. Set exclusively here — agents cannot override this.</span>
|
||||
</div>
|
||||
<div class="config-field">
|
||||
<label class="config-label" for="cfg-empty">Empty Response</label>
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = `<div class="empty-state"><div class="empty-state__icon">◈</div>No consumed instructions yet</div>`;
|
||||
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 ──────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user