import { useEffect, useRef, useState } from "react"; import { api } from "../api/client"; import type { InboxItemDto } from "../api/types"; import "./richscripts-actions.js"; type Widget = { submit: (text: string) => Promise; destroy: () => void }; type Props = { onOpenConversation: (id: number) => void }; declare global { interface Window { RichScriptsActions: { mount: (options: Record) => Widget }; } } /** First integration uses registered READ actions and the existing authenticated API. * Command mode is explicit: no model calls, no duplicated credentials or write paths. */ export default function InboxActionAssistant({ onOpenConversation }: Props) { const host = useRef(null); const widget = useRef(null); const open = useRef(onOpenConversation); open.current = onOpenConversation; const [results, setResults] = useState([]); const [searched, setSearched] = useState(false); const [feedback, setFeedback] = useState(""); useEffect(() => { let disposed = false; const schema = (properties: Record) => ({ type: "object", properties, required: Object.keys(properties) }); widget.current = window.RichScriptsActions.mount({ target: host.current, title: "Inbox action assistant", subtitle: "Command mode · Real workspace data · Read only · No model calls", welcome: 'Try “Show open conversations”, “Show needs human conversations”, “Find Nora”, or “Open conversation 123”. No changes or messages are sent.', resolve: async ({ message }: { message: string }) => { const text = message.trim(); const list = text.match(/^(?:show|list) (all|open|resolved|closed|pending|needs human) (?:conversations|chats)$/i); if (list) return { action: "find_conversations", arguments: { status: list[1].toLowerCase().replace("closed", "resolved").replace("needs human", "pending"), query: "" } }; const find = text.match(/^find\s+(.{1,100})$/i); if (find) return { action: "find_conversations", arguments: { status: "all", query: find[1].trim() } }; const thread = text.match(/^open (?:conversation|chat) #?(\d+)$/i); if (thread && Number.isSafeInteger(Number(thread[1])) && Number(thread[1]) > 0) return { action: "open_conversation", arguments: { id: Number(thread[1]) } }; return { message: 'Command mode supports listing conversations, finding a customer, and opening a conversation by number. Resolving, deleting and sending messages are unavailable here.' }; }, actions: [ { name: "find_conversations", readOnly: true, parameters: schema({ status: { type: "string", enum: ["all", "open", "resolved", "pending"] }, query: { type: "string", maxLength: 100 } }), execute: async (args: { status: string; query: string }) => { const rows = await api.getInbox({ take: 50, statusFilter: args.status, q: args.query }); if (disposed) return "Assistant closed."; setResults(rows); setSearched(true); setFeedback(""); return rows.length ? `Showing ${rows.length} matching conversations below (up to 50). Choose a result to open it.` : "No matching conversations in your accessible workspace."; } }, { name: "open_conversation", readOnly: true, parameters: schema({ id: { type: "integer", minimum: 1, maximum: Number.MAX_SAFE_INTEGER } }), execute: async (args: { id: number }) => { await api.getConversation(args.id); // Re-check access; never navigate from an unchecked identifier. if (!disposed) open.current(args.id); return `Opened conversation #${args.id}.`; } }, ], }); return () => { disposed = true; widget.current?.destroy(); widget.current = null; }; }, []); async function ask(text: string) { if (!await widget.current?.submit(text)) setFeedback("Wait for the current request, or check the assistant's error message."); else setFeedback(""); } return
Inbox action assistantSearch and open conversations with short commands

Read-only developer preview using your existing workspace permissions. Command parsing runs locally; no AI provider is called.

{feedback &&

{feedback}

} {searched &&

{results.length ? "Matching conversations" : "No matching conversations"}

{results.map(row => )}
}
; }