import { html, nothing } from 'lit'; import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { renderMarkdown } from './base.js'; /** * InboxMixin — shared fetch, action, and render logic for the agent inbox. * Used by AgentInboxPage (full page) and HomePage (embedded section). */ export const InboxMixin = (Base) => class extends Base { static get properties() { return { ...super.properties, _inboxData: { state: true }, _inboxError: { state: true }, _inboxLoading: { state: true }, }; } constructor() { super(); this._inboxData = null; this._inboxError = null; this._inboxLoading = false; this._expanded = new Set(); this._bypassOpen = new Set(); } // ── Data ────────────────────────────────────────────────────────────────── async _loadInbox() { try { const res = await fetch('/api/inbox'); if (!res.ok) throw new Error(`HTTP ${res.status}`); this._inboxData = await res.json(); this._inboxError = null; window.dispatchEvent(new CustomEvent('inbox-count', { detail: { count: this._inboxData.total } })); } catch (e) { this._inboxError = e.message; } } // ── Actions ─────────────────────────────────────────────────────────────── async _resolveApproval(requestId, action, note = '', bypassSecs = null, bypassScope = null, toolCallId = null) { try { const body = { action, note }; if (bypassSecs !== null) { body.bypass_secs = bypassSecs; body.bypass_scope = bypassScope; } // Live items resolve by request_id (and support bypass); DB-persisted // (post-restart) items carry request_id 0 → resolve by the durable, // source-agnostic tool_call_id (bypass buttons are hidden for them). const url = requestId ? `/api/inbox/approvals/${requestId}/resolve` : `/api/tools/${toolCallId}/resolve`; const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(await res.text()); await this._loadInbox(); } catch (e) { this._inboxError = e.message; } } _rejectWithNote(requestId, toolCallId = null) { const note = prompt('Rejection reason (optional):') ?? ''; this._resolveApproval(requestId, 'reject', note, null, null, toolCallId); } /** Approve + set a timed or session bypass scoped to the tool's category or MCP server. */ _approveWithBypass(item, bypassSecs) { const scope = item.tool_category ? 'category' : item.mcp_server ? 'mcp_server' : 'all'; this._resolveApproval(item.request_id, 'approve', '', bypassSecs, scope); } /** Human-readable bypass scope label, e.g. "filesystem" or "Gmail". */ _bypassLabel(item) { if (item.tool_category) return item.tool_category; if (item.mcp_server) return item.mcp_server; return 'session'; } async _resolveClarification(requestId, inputEl) { const answer = inputEl.value.trim(); if (!answer) return; try { const res = await fetch(`/api/inbox/clarifications/${requestId}/resolve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ answer }), }); if (!res.ok) throw new Error(await res.text()); await this._loadInbox(); } catch (e) { this._inboxError = e.message; } } /** * Resolve a server-initiated MCP elicitation. On `accept` with a field, the * input value is packed into `content` ({ [field]: value }); the secret is * sent once and never echoed back into the UI. `decline`/`cancel` send no value. */ async _resolveElicitation(item, action, inputEl) { let content = null; if (action === 'accept' && item.field_name) { content = { [item.field_name]: inputEl ? inputEl.value : '' }; } try { const res = await fetch(`/api/inbox/elicitations/${item.request_id}/resolve`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action, content }), }); if (!res.ok) throw new Error(await res.text()); await this._loadInbox(); } catch (e) { this._inboxError = e.message; } } // ── Helpers ─────────────────────────────────────────────────────────────── _toggleRaw(id) { if (this._expanded.has(id)) this._expanded.delete(id); else this._expanded.add(id); this.requestUpdate(); } _toggleBypassMenu(id) { if (this._bypassOpen.has(id)) this._bypassOpen.delete(id); else this._bypassOpen.add(id); this.requestUpdate(); } _fmt(iso) { if (!iso) return ''; return new Date(iso).toLocaleString(undefined, { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit', }); } _keyArgs(args) { const entries = []; for (const key of ['path', 'command', 'url', 'origin', 'destination', 'name', 'message', 'query']) { if (args[key] !== undefined) { let val = args[key]; if (typeof val === 'object') val = JSON.stringify(val); entries.push({ key, value: String(val) }); } } return entries; } // ── Card renderers ──────────────────────────────────────────────────────── _renderApprovalCard(item) { const id = `raw-${item.request_id}`; const open = this._expanded.has(id); const label = item.context_label ?? item.source; const args = item.arguments ?? {}; const keyArgs = this._keyArgs(args); const rawJson = JSON.stringify(args, null, 2); return html`
${rawJson}
No pending requests