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)
|
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)
|
@router.delete("/{instruction_id}", status_code=204)
|
||||||
def delete_instruction(instruction_id: str):
|
def delete_instruction(instruction_id: str):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -168,6 +168,19 @@ def delete_instruction(instruction_id: str) -> None:
|
|||||||
logger.info("Instruction deleted id=%s", instruction_id)
|
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]:
|
def consume_next(agent_id: str = "unknown") -> Optional[InstructionItem]:
|
||||||
"""
|
"""
|
||||||
Atomically claim and return the next pending instruction.
|
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>
|
<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
|
Consumed
|
||||||
</span>
|
</span>
|
||||||
|
<div style="display:flex;align-items:center;gap:var(--space-2)">
|
||||||
<span id="consumed-badge" class="badge badge--muted">0</span>
|
<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>
|
||||||
<div class="panel-body panel-body--flush">
|
<div class="panel-body panel-body--flush">
|
||||||
<div id="consumed-list">
|
<div id="consumed-list">
|
||||||
@@ -151,9 +157,9 @@
|
|||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<form id="config-form">
|
<form id="config-form">
|
||||||
<div class="config-field">
|
<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" />
|
<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>
|
||||||
<div class="config-field">
|
<div class="config-field">
|
||||||
<label class="config-label" for="cfg-empty">Empty Response</label>
|
<label class="config-label" for="cfg-empty">Empty Response</label>
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export const api = {
|
|||||||
createInstruction: (content) => request('POST', '/api/instructions', { content }),
|
createInstruction: (content) => request('POST', '/api/instructions', { content }),
|
||||||
updateInstruction: (id, content) => request('PATCH', `/api/instructions/${id}`, { content }),
|
updateInstruction: (id, content) => request('PATCH', `/api/instructions/${id}`, { content }),
|
||||||
deleteInstruction: (id) => request('DELETE', `/api/instructions/${id}`),
|
deleteInstruction: (id) => request('DELETE', `/api/instructions/${id}`),
|
||||||
|
clearConsumed: () => request('DELETE', '/api/instructions/consumed'),
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
getConfig: () => request('GET', '/api/config'),
|
getConfig: () => request('GET', '/api/config'),
|
||||||
|
|||||||
@@ -104,6 +104,11 @@ function _handleEvent(event) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'history.cleared': {
|
||||||
|
const next = (state.get('instructions') || []).filter(i => i.status !== 'consumed');
|
||||||
|
state.set('instructions', next);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case 'status.changed': {
|
case 'status.changed': {
|
||||||
api.status().then(s => state.set('status', s)).catch(console.error);
|
api.status().then(s => state.set('status', s)).catch(console.error);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ export function initInstructions() {
|
|||||||
const pendingBadge = document.getElementById('pending-badge');
|
const pendingBadge = document.getElementById('pending-badge');
|
||||||
const consumedList = document.getElementById('consumed-list');
|
const consumedList = document.getElementById('consumed-list');
|
||||||
const consumedBadge = document.getElementById('consumed-badge');
|
const consumedBadge = document.getElementById('consumed-badge');
|
||||||
|
const clearHistoryBtn = document.getElementById('clear-history-btn');
|
||||||
|
|
||||||
function render(instructions) {
|
function render(instructions) {
|
||||||
if (!instructions) return;
|
if (!instructions) return;
|
||||||
@@ -190,14 +191,30 @@ export function initInstructions() {
|
|||||||
consumedList.innerHTML = '';
|
consumedList.innerHTML = '';
|
||||||
if (consumed.length === 0) {
|
if (consumed.length === 0) {
|
||||||
consumedList.innerHTML = `<div class="empty-state"><div class="empty-state__icon">◈</div>No consumed instructions yet</div>`;
|
consumedList.innerHTML = `<div class="empty-state"><div class="empty-state__icon">◈</div>No consumed instructions yet</div>`;
|
||||||
|
clearHistoryBtn.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
consumed.forEach(item => consumedList.appendChild(renderConsumedCard(item)));
|
consumed.forEach(item => consumedList.appendChild(renderConsumedCard(item)));
|
||||||
|
clearHistoryBtn.style.display = '';
|
||||||
}
|
}
|
||||||
consumedBadge.textContent = consumed.length;
|
consumedBadge.textContent = consumed.length;
|
||||||
consumedBadge.className = `badge ${consumed.length > 0 ? 'badge--amber' : 'badge--muted'}`;
|
consumedBadge.className = `badge ${consumed.length > 0 ? 'badge--amber' : 'badge--muted'}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.subscribe('instructions', render);
|
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 ──────────────────────────────────────────────────────────────
|
// ── Composer ──────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user