/** * Pending interactions card (approvals / questions) of one session — fetched * on demand over the public REST surface (`/api/v1/sessions/{id}/approvals` * and `.../questions`): the engine's interaction kernel is a process-global * singleton with no debug channel, and the session `interactions` push * stream went away with `/api/v2/ws`, so the card refreshes only when Load * is clicked. */ import { useState } from 'react'; import { useConnection } from '../connection'; import { answerQuestion, decideApproval, dismissQuestion, listPendingApprovals, listPendingQuestions, type QuestionAnswerWire, type QuestionWire, } from '../interactions/api'; import { ActionButton, Badge, ErrorLine, JsonView } from '../ui'; interface PendingInteraction { readonly id: string; /** Known kinds: 'approval' | 'question'. */ readonly kind: string; readonly payload: Record; } export function InteractionsCard({ sessionId }: { sessionId: string }) { const { config, baseUrl } = useConnection(); const [pending, setPending] = useState([]); const [error, setError] = useState(null); const api = { baseUrl, token: config.token, sessionId }; const reload = async () => { try { setError(null); const [approvals, questions] = await Promise.all([ listPendingApprovals(api), listPendingQuestions(api), ]); setPending([ ...approvals.map((p) => ({ id: p.approval_id, kind: 'approval', payload: { toolName: p.tool_name, action: p.action, display: p.tool_input_display, }, })), ...questions.map((p) => ({ id: p.question_id, kind: 'question', payload: p as unknown as Record, })), ]); } catch (error) { setError(error); } }; const decide = async (id: string, decision: 'approved' | 'rejected') => { try { await decideApproval(api, id, decision); await reload(); } catch (error) { setError(error); } }; const answer = async (id: string, answers: Readonly>) => { try { await answerQuestion(api, id, answers, 'click'); await reload(); } catch (error) { setError(error); } }; const dismiss = async (id: string) => { try { await dismissQuestion(api, id); await reload(); } catch (error) { setError(error); } }; return (
Pending interactions {pending.length > 0 ? `(${pending.length})` : ''} void reload()}>Load
{error !== null ? (
) : null} {pending.length === 0 ? (
nothing pending (click Load to check)
) : ( pending.map((item) => (
{item.kind} {item.id}
{item.kind === 'approval' ? ( <>
tool {payloadField(item.payload, 'toolName', '?')} · {payloadField(item.payload, 'action', '')}
void decide(item.id, 'approved')}> Approve void decide(item.id, 'rejected')}> Reject
) : item.kind === 'question' ? ( void answer(item.id, answers)} onDismiss={() => void dismiss(item.id)} /> ) : ( )}
)) )}
); } function QuestionView({ wire, onAnswer, onDismiss, }: { wire: QuestionWire; onAnswer: (answers: Readonly>) => void; onDismiss: () => void; }) { return ( <> {wire.questions.map((q) => (
{q.question}
{q.options.map((opt) => ( onAnswer({ [q.id]: { kind: 'single', option_id: opt.id } })} > {opt.label} ))} {q.allow_other === false ? null : ( { const raw = window.prompt(q.question); if (raw !== null) onAnswer({ [q.id]: { kind: 'other', text: raw } }); }} > Other… )}
))} {wire.questions.length === 0 ? : null}
Dismiss
); } /** * Render a wire payload field as display text: strings pass through, * numbers/booleans are stringified, anything else (or missing) falls back — * never "[object Object]". */ function payloadField(payload: Record, key: string, fallback: string): string { const value = payload[key]; if (typeof value === 'string') return value; if (typeof value === 'number' || typeof value === 'boolean') return String(value); return fallback; }