feat(agent): one-tap "clear the proposals I didn't approve" (BoXon feedback) (#1798)
When the assistant stages several verifikat and the user approves only some,
the rest lingered as pending_operations in Granskning until the 30-day expiry —
manual per-item cleanup. Now:
- lib/agent/pending/reject-conversation-pending.ts rejects a conversation's
still-pending proposals in one update (guarded on status='pending' so it never
stamps over a committed verifikat; company-scoped; keyed on the conversation).
- POST /api/agent/conversations/[id]/reject-pending — the chat's "Rensa förslag
som inte godkänts" button (appears when the thread has staged proposals; drops
the cards from view).
- Auto-clear on archive: archiving a thread ("I'm done") clears its leftover
proposals in the PATCH, best-effort.
Kept durable-by-default (proposals still come back on resume) — the button is
explicit user intent, not an auto-reject on every panel close, so resume still
works. 10 tests (helper + endpoint 401/404/404/happy); lint + guards + scoped
typecheck clean. UI button awaits founder visual sign-off.
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Jakob Wennberg
Claude Opus 4.8
parent
13b69a2056
commit
1f7acbf144
@@ -206,6 +206,26 @@ export default function AgentChat({
|
||||
onStatus,
|
||||
}: AgentChatProps) {
|
||||
const [conversationId, setConversationId] = useState<string | null>(initialConversationId ?? null)
|
||||
// "Clear the proposals I didn't approve" — rejects the conversation's
|
||||
// still-pending staged operations in one call so they don't linger in
|
||||
// Granskning, then drops the cards from view.
|
||||
const [clearingProposals, setClearingProposals] = useState(false)
|
||||
async function handleClearProposals() {
|
||||
const convId = conversationId
|
||||
if (!convId || clearingProposals) return
|
||||
setClearingProposals(true)
|
||||
try {
|
||||
await fetch(`/api/agent/conversations/${convId}/reject-pending`, { method: 'POST' })
|
||||
// Committed proposals are already booked (their card was only a
|
||||
// confirmation); pending ones the server just rejected. Either way, drop
|
||||
// the cards so the conversation reads as resolved.
|
||||
setMessages((prev) => prev.map((m) => (m.staged ? { ...m, staged: undefined } : m)))
|
||||
} catch {
|
||||
// best-effort: leave the cards if the request failed
|
||||
} finally {
|
||||
setClearingProposals(false)
|
||||
}
|
||||
}
|
||||
// Track whether the first-turn callback has fired so the bootstrap
|
||||
// starters get exactly one notification even if a turn fires before
|
||||
// the conversation_id event (defensive: order shouldn't matter).
|
||||
@@ -867,6 +887,22 @@ export default function AgentChat({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* One-tap cleanup: when the assistant staged several proposals and the
|
||||
user only approved some, clear the rest here instead of rejecting each
|
||||
one in Granskning. */}
|
||||
{conversationId && messages.some((m) => m.staged && m.staged.length > 0) && (
|
||||
<div className="flex justify-end border-t border-border px-5 py-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleClearProposals()}
|
||||
disabled={clearingProposals}
|
||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50"
|
||||
>
|
||||
{clearingProposals ? 'Rensar…' : 'Rensa förslag som inte godkänts'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Paywall: /api/agent/invoke 403s without the ai capability. Replace
|
||||
the composer with an upsell so an already-open conversation (or a
|
||||
deep link to /chat/*) never offers an input that can't send. */}
|
||||
|
||||
Reference in New Issue
Block a user