import type { ReactNode } from "react"; import { useTranslation } from "react-i18next"; import type { NtfyNotificationEvent } from "@fusion/core"; import type { SectionBaseProps } from "./context"; /** Default event set used when a provider has no explicit `*Events` override. */ export const DEFAULT_NTFY_EVENTS: NtfyNotificationEvent[] = [ "in-review", "merged", "failed", "awaiting-approval", "awaiting-user-review", "planning-awaiting-input", "cli-agent-awaiting-input", "gridlock", "fallback-used", "memory-dreams-processed", "message:agent-to-user", "message:agent-to-agent", "message:room", "oauth-token-expired", ]; export const NOTIFICATION_EVENT_OPTIONS: Array<{ event: NtfyNotificationEvent; label: string; description: string; }> = [ { event: "in-review", label: "Task completed (in-review)", description: "When a task moves to In Review (ready for review)" }, { event: "merged", label: "Task merged", description: "When a task is successfully merged to main" }, { event: "failed", label: "Task failed", description: "When a task fails during execution (high priority)" }, { event: "awaiting-approval", label: "Plan needs approval", description: "When a task specification needs manual approval before execution" }, { event: "awaiting-user-review", label: "User review needed", description: "When an agent hands off a task for human review (high priority)" }, { event: "planning-awaiting-input", label: "Planning needs input", description: "When planning mode is waiting for your response to continue" }, // FNXC:ToolPermissionNotifications 2026-06-27-00:00: Settings must expose CLI-agent awaiting-input alerts separately from plan approval so operators can opt into external notifications for blocked terminal tool permissions. { event: "cli-agent-awaiting-input", label: "CLI agent needs input", description: "When a CLI agent is blocked on a tool permission or terminal input prompt" }, { event: "gridlock", label: "Pipeline gridlocked", description: "When all schedulable todo tasks are blocked and work cannot advance" }, { event: "fallback-used", label: "Fallback model used (recovered)", description: "When Fusion recovers from a retryable model failure by switching to a fallback model" }, { event: "task-created", label: "Agent created a task", description: "When an agent files a new task on the board" }, { event: "memory-dreams-processed", label: "DREAMS.md entry added", description: "When manual dream processing writes a new entry to project or agent DREAMS.md" }, { event: "message:agent-to-user", label: "Agent → user message", description: "An agent sent you a direct message" }, { event: "message:agent-to-agent", label: "Agent → agent message", description: "Agents are talking to each other (including replies)" }, { event: "message:room", label: "Agent message in room", description: "An agent posted a reply in a chat room you're watching" }, { event: "oauth-token-expired", label: "OAuth token expired", description: "Notify when a provider OAuth token (Codex, Claude, etc.) expires." }, ]; export type TestNotificationProvider = "ntfy" | "webhook" | "ntfy-message" | "ntfy-room"; export interface NotificationsSectionProps extends SectionBaseProps { scopeBanner: ReactNode; testNotificationLoading: Record; testNotificationResult: Record; onTestProviderNotification: (provider: TestNotificationProvider) => void; } export function NotificationsSection({ scopeBanner, form, setForm, testNotificationLoading, testNotificationResult, onTestProviderNotification, }: NotificationsSectionProps) { const { t } = useTranslation("app"); return (<> {scopeBanner}

{t("settings.notifications.notifications", "Notifications")}

{t("settings.notifications.stickyOnlySuppressesRecoveredFailuresTerminalOnlyWaits", "Sticky-only suppresses recovered failures; terminal-only waits for paused/in-review failed tasks; all restores legacy alerts.")}
{ const parsed = Number(e.target.value); setForm((f) => ({ ...f, failureNotificationDelayMs: Number.isFinite(parsed) && parsed >= 0 ? parsed : 0, })); }}/> {t("settings.notifications.howLongAFailureMustPersistBeforeA", " How long a failure must persist before a push notification is sent. 0 = notify immediately. ")}
{t("settings.notifications.ntfy", "ntfy")}
{form.ntfyEnabled && (
{ const val = e.target.value; setForm((f) => ({ ...f, ntfyTopic: val || undefined })); }}/> {t("settings.notifications.yourNtfyShTopicName164Alphanumeric", " Your ntfy.sh topic name (1\u201364 alphanumeric/hyphen/underscore characters).")}{" "} {t("settings.notifications.learnMoreAboutNtfySh", " Learn more about ntfy.sh ")} {form.ntfyTopic && !/^[a-zA-Z0-9_-]{1,64}$/.test(form.ntfyTopic) && ({t("settings.notifications.topicMustBe164AlphanumericHyphenOr", " Topic must be 1\u201364 alphanumeric, hyphen, or underscore characters ")})}
{t("settings.notifications.advanced", "Advanced")}
{ const value = e.target.value; setForm((f) => ({ ...f, ntfyBaseUrl: value || undefined })); }}/> {t("settings.notifications.leaveBlankToKeepTheDefaultServerHttps", " Leave blank to keep the default server: https://ntfy.sh. Custom servers must use http:// or https://. ")} { const value = e.target.value; setForm((f) => ({ ...f, ntfyAccessToken: value || undefined })); }}/> {t("settings.notifications.leaveBlankToPublishWithoutAuthenticationWhenSet", " Leave blank to publish without authentication. When set, Fusion sends an Authorization Bearer header with ntfy requests. ")}
{NOTIFICATION_EVENT_OPTIONS.map(({ event, label, description }) => { const checked = form.ntfyEvents?.includes(event) ?? true; return (
{description}
); })}
{ const val = e.target.value; setForm((f) => ({ ...f, ntfyDashboardHost: val || undefined })); }}/> {t("settings.notifications.baseURLForDeepLinksInNotificationsWhen", " Base URL for deep links in notifications. When set, clicking a notification opens the dashboard directly to the task. ")} {form.ntfyDashboardHost && !/^https?:\/\/.+/.test(form.ntfyDashboardHost) && ({t("settings.notifications.mustBeAValidURLStartingWithHttp", " Must be a valid URL starting with http:// or https:// ")})}
{(testNotificationResult["ntfy"] || testNotificationResult["ntfy-message"] || testNotificationResult["ntfy-room"]) && (
{testNotificationResult["ntfy"] && ({t("settings.notifications.general", " General: ")}{testNotificationResult["ntfy"].message} )} {testNotificationResult["ntfy-message"] && ({t("settings.notifications.messageInbox", " Message inbox: ")}{testNotificationResult["ntfy-message"].message} )} {testNotificationResult["ntfy-room"] && ({t("settings.notifications.roomReply", " Room reply: ")}{testNotificationResult["ntfy-room"].message} )}
)}
)}
{t("settings.notifications.webhook", "Webhook")}
{form.webhookEnabled && (
{ const val = e.target.value; setForm((f) => ({ ...f, webhookUrl: val || undefined })); }}/>
{NOTIFICATION_EVENT_OPTIONS.map(({ event, label, description }) => { const currentEvents = form.webhookEvents ?? [...DEFAULT_NTFY_EVENTS]; const checked = currentEvents.includes(event); return (
{description}
); })}
{testNotificationResult["webhook"] && (
{testNotificationResult["webhook"].message}
)}
)}
); } export default NotificationsSection;