import { useEffect, useState } from "react"; import { getConnectors, getDmRoute, getInboxRouting, getRecentChannels, getSessions, getSubscriptions, getUnrouted, setDmRoute, setInboxBinding, subscribeChannel, unsubscribeChannel, type RecentChannel, type Connector, type Subscription, type UnroutedItem, } from "../api"; import type { SessionInfo } from "../types"; import { ChannelPicker } from "./SubscriptionsChip"; import { Icon } from "./Icon"; // Inbox ▸ Configure (UX-DECISIONS §28): the former Connectors ▸ "Messaging routing" page, // relocated whole — where inbox items go out (mirror channel), how inbound messages reach // sessions (DM route, channel subscriptions), and the Unrouted dead-letter. Moving it here // also deleted a duplication: the mirror channel used to be editable BOTH on this page and // via an inline configurator on the Inbox list. const CARD = "rounded-xl2 border border-line bg-panel"; const SELECT = "px-2.5 py-1.5 rounded-lg border border-line bg-paper text-[13px] text-ink"; const BTN_ACCENT_SM = "text-[12px] px-2.5 py-1 rounded-md bg-accent text-white disabled:opacity-50"; export function InboxConfigure() { return (
{/* Unrouted = delivery FAILURES ("messages that never reached you"), so it lives with the Inbox now (§28; previously with routing under Connectors, §26). */}

Unrouted

Inbound messages and background-turn failures nothing claimed — nothing vanishes silently.

); } // Where an Unattended session's approvals/questions get mirrored as interactive buttons. Targets // the "default" route (sessions fall back to it); pick a channel separate from any you subscribe to. function InboxRoutingCard() { const [recent, setRecent] = useState([]); const [connectors, setConnectors] = useState([]); const [target, setTarget] = useState(""); // current default-binding address, e.g. "slack:C0123" const [draft, setDraft] = useState(""); const [error, setError] = useState(null); const load = () => { getRecentChannels().then(setRecent).catch(() => setRecent([])); getConnectors().then(setConnectors).catch(() => setConnectors([])); getInboxRouting() .then((bs) => { const def = bs.find((b) => b.name === "default"); setTarget(def?.channel ? `${def.channel}:${def.target}` : ""); }) .catch(() => setTarget("")); }; useEffect(() => { load(); const t = setInterval(load, 5000); return () => clearInterval(t); }, []); const save = async () => { const addr = draft.trim(); if (!addr) return; // "slack:C0123" → channel="slack", target="C0123"; a bare id assumes slack. const [platform, id] = addr.includes(":") ? addr.split(":", 2) : ["slack", addr]; const result = await setInboxBinding("default", platform, id); if (!result.ok) { setError(result.error || "Could not update Inbox routing."); return; } setError(null); setDraft(""); load(); }; const clear = async () => { const result = await setInboxBinding("default", null, ""); if (!result.ok) { setError(result.error || "Could not clear Inbox routing."); return; } setError(null); load(); }; const draftAddr = draft.trim(); const [draftPlatform, draftTarget] = draftAddr.includes(":") ? draftAddr.split(":", 2) : ["slack", draftAddr]; const slack = connectors.find((c) => c.name === "slack"); const teamId = draftPlatform === "slack" && draftTarget.includes("/") ? draftTarget.split("/", 1)[0] : null; const owners = draftPlatform !== "slack" ? [] : teamId ? slack?.workspaces?.find((w) => w.team_id === teamId)?.approval_owner_ids ?? [] : slack?.approval_owner_ids ?? []; const missingSlackOwner = draftPlatform === "slack" && draftTarget.length > 0 && owners.length === 0; // Show the channel's NAME when the recent list knows it (raw address as the fallback/tooltip). const known = recent.find((c) => c.channel === target)?.name; return (
Unattended approvals

Channel where an Unattended session posts Approve/Deny buttons. Currently mirroring to{" "} {known ? `#${known}` : target || "in-app Inbox only"} .

{target && ( )}
{missingSlackOwner && (

Choose an approval owner under Integrations → Slack before routing approvals here.

)} {error &&

{error}

}
); } // Which session handles incoming DMs to the bot. None → DMs park in the Unrouted section below. function DmRouteCard() { const [sessions, setSessions] = useState([]); const [dm, setDm] = useState(""); const load = () => { getSessions().then(setSessions).catch(() => setSessions([])); getDmRoute().then((s) => setDm(s || "")).catch(() => setDm("")); }; useEffect(() => { load(); const t = setInterval(load, 5000); return () => clearInterval(t); }, []); const real = sessions.filter((s) => !s.session_id.startsWith("__")); const choose = async (sessionId: string) => { setDm(sessionId); await setDmRoute(sessionId); load(); }; return (
Direct messages

Session that handles DMs to the bot. With none, DMs park under Unrouted below.

); } // Which sessions listen to which channels (inbound), and where each routes its Inbox (outbound). // Subscriptions can be created by the agent (it asks you via ask_user) or added here directly. function SubscriptionsCard() { const [subs, setSubs] = useState(null); const [sessions, setSessions] = useState([]); const [recent, setRecent] = useState([]); const [addSession, setAddSession] = useState(""); const [addChannel, setAddChannel] = useState(""); const load = () => { getSubscriptions().then(setSubs).catch(() => setSubs([])); getSessions().then(setSessions).catch(() => setSessions([])); getRecentChannels().then(setRecent).catch(() => setRecent([])); }; useEffect(() => { load(); const t = setInterval(load, 5000); return () => clearInterval(t); }, []); const real = sessions.filter((s) => !s.session_id.startsWith("__")); const add = async () => { if (!addSession || !addChannel.trim()) return; await subscribeChannel(addSession, addChannel.trim()); setAddChannel(""); load(); }; const remove = async (sessionId: string, channel: string) => { await unsubscribeChannel(sessionId, channel); load(); }; return (
Channel subscriptions — sessions that listen to a channel (inbound)
{subs && subs.length > 0 ? ( {subs.map((s, i) => ( ))}
Session Listens to Inbox routes to
{s.session_title} {s.channel_name ? `#${s.channel_name}` : s.channel} {s.channel_name && ( {s.channel} )} {s.collision && ( ⚠ collides )} {s.routing_target || "—"}
) : (
No channel subscriptions yet — add one below or ask a coworker to watch a channel.
)}
); } // Dead-letter view: inbound messages that had no destination (e.g. a DM with no session designated) // and background turns that failed (e.g. a dead model). Read-only — for visibility/debugging. function UnroutedTable() { const [items, setItems] = useState(null); useEffect(() => { const load = () => getUnrouted().then(setItems).catch(() => setItems([])); load(); const t = setInterval(load, 5000); return () => clearInterval(t); }, []); if (items && items.length === 0) return (
Nothing here — no dropped messages or failed turns.
); return (
{(items ?? []).map((it, i) => ( ))}
When Source Reason Message
{new Date(it.ts * 1000).toLocaleString()} {it.source} {it.reason} {it.text}
); }