diff --git a/console/src/routes/channels-catalog.ts b/console/src/routes/channels-catalog.ts index 22333297..fc8af1a2 100644 --- a/console/src/routes/channels-catalog.ts +++ b/console/src/routes/channels-catalog.ts @@ -32,21 +32,21 @@ function countKeys(value: Record): number { } export function countDiscordGuilds(config: AdminConfig): number { - return countKeys(config.discord.guilds); + return countKeys(config.discord?.guilds ?? {}); } export function countDiscordOverrides(config: AdminConfig): number { - return Object.values(config.discord.guilds).reduce((total, guild) => { + return Object.values(config.discord?.guilds ?? {}).reduce((total, guild) => { return total + countKeys(guild.channels); }, 0); } export function countTeams(config: AdminConfig): number { - return countKeys(config.msteams.teams); + return countKeys(config.msteams?.teams ?? {}); } export function countTeamsOverrides(config: AdminConfig): number { - return Object.values(config.msteams.teams).reduce((total, team) => { + return Object.values(config.msteams?.teams ?? {}).reduce((total, team) => { return total + countKeys(team.channels); }, 0); } @@ -61,8 +61,9 @@ function describeDiscord( ): ChannelCatalogItem { const guildCount = countDiscordGuilds(config); const overrideCount = countDiscordOverrides(config); - const enabled = - config.discord.commandsOnly || config.discord.groupPolicy !== 'disabled'; + const enabled = config.discord + ? config.discord.commandsOnly || config.discord.groupPolicy !== 'disabled' + : false; const tokenConfigured = options.discordTokenConfigured === true; const active = enabled && tokenConfigured; const configured = @@ -76,7 +77,9 @@ function describeDiscord( return { kind: 'discord', label: 'Discord', - summary: `${pluralize(guildCount, 'guild default')} · ${pluralize(overrideCount, 'explicit override')}`, + summary: config.discord + ? `${pluralize(guildCount, 'guild default')} · ${pluralize(overrideCount, 'explicit override')}` + : 'Not configured', statusTone, statusLabel: statusTone === 'active' @@ -92,12 +95,13 @@ function describeWhatsApp( options: ChannelCatalogOptions, ): ChannelCatalogItem { const linked = options.whatsappLinked === true; - const enabled = - config.whatsapp.dmPolicy !== 'disabled' || - config.whatsapp.groupPolicy !== 'disabled'; + const enabled = config.whatsapp + ? config.whatsapp.dmPolicy !== 'disabled' || + config.whatsapp.groupPolicy !== 'disabled' + : false; const summary = linked ? enabled - ? `Linked device · groups ${config.whatsapp.groupPolicy}` + ? `Linked device · groups ${config.whatsapp?.groupPolicy}` : 'Linked device available but transport is off' : enabled ? 'Link device to enable WhatsApp' @@ -124,15 +128,16 @@ function describeTelegram( ): ChannelCatalogItem { const tokenConfigured = options.telegramTokenConfigured === true; const inboundEnabled = - config.telegram.dmPolicy !== 'disabled' || - config.telegram.groupPolicy !== 'disabled'; - const active = config.telegram.enabled && tokenConfigured && inboundEnabled; + config.telegram?.dmPolicy !== 'disabled' || + config.telegram?.groupPolicy !== 'disabled'; + const active = + (config.telegram?.enabled ?? false) && tokenConfigured && inboundEnabled; const configured = active || - config.telegram.enabled || + (config.telegram?.enabled ?? false) || tokenConfigured || - config.telegram.allowFrom.length > 0 || - config.telegram.groupAllowFrom.length > 0; + (config.telegram?.allowFrom?.length ?? 0) > 0 || + (config.telegram?.groupAllowFrom?.length ?? 0) > 0; const statusTone = active ? 'active' : configured @@ -142,7 +147,9 @@ function describeTelegram( return { kind: 'telegram', label: 'Telegram', - summary: `DM ${config.telegram.dmPolicy} · groups ${config.telegram.groupPolicy}`, + summary: config.telegram + ? `DM ${config.telegram.dmPolicy} · groups ${config.telegram.groupPolicy}` + : 'Not configured', statusTone, statusLabel: statusTone === 'active' @@ -160,14 +167,16 @@ function describeSlack( const botTokenConfigured = options.slackBotTokenConfigured === true; const appTokenConfigured = options.slackAppTokenConfigured === true; const active = - config.slack.enabled && botTokenConfigured && appTokenConfigured; + (config.slack?.enabled ?? false) && + botTokenConfigured && + appTokenConfigured; const configured = active || - config.slack.enabled || + (config.slack?.enabled ?? false) || botTokenConfigured || appTokenConfigured || - config.slack.allowFrom.length > 0 || - config.slack.groupAllowFrom.length > 0; + (config.slack?.allowFrom?.length ?? 0) > 0 || + (config.slack?.groupAllowFrom?.length ?? 0) > 0; const statusTone = active ? 'active' : configured @@ -177,7 +186,9 @@ function describeSlack( return { kind: 'slack', label: 'Slack', - summary: `DM ${config.slack.dmPolicy} · channels ${config.slack.groupPolicy}`, + summary: config.slack + ? `DM ${config.slack.dmPolicy} · channels ${config.slack.groupPolicy}` + : 'Not configured', statusTone, statusLabel: statusTone === 'active' @@ -194,17 +205,17 @@ function describeEmail( ): ChannelCatalogItem { const passwordConfigured = options.emailPasswordConfigured === true; const active = - config.email.enabled && + (config.email?.enabled ?? false) && passwordConfigured && - !!config.email.address && - !!config.email.imapHost && - !!config.email.smtpHost; + !!config.email?.address && + !!config.email?.imapHost && + !!config.email?.smtpHost; const configured = active || - !!config.email.address || - !!config.email.imapHost || - !!config.email.smtpHost || - config.email.allowFrom.length > 0; + !!config.email?.address || + !!config.email?.imapHost || + !!config.email?.smtpHost || + (config.email?.allowFrom?.length ?? 0) > 0; const statusTone = active ? 'active' : configured @@ -214,7 +225,7 @@ function describeEmail( return { kind: 'email', label: 'Email', - summary: config.email.address || 'No mailbox address configured yet', + summary: config.email?.address || 'No mailbox address configured yet', statusTone, statusLabel: statusTone === 'active' @@ -229,14 +240,14 @@ function describeMSTeams(config: AdminConfig): ChannelCatalogItem { const teamCount = countTeams(config); const overrideCount = countTeamsOverrides(config); const active = - config.msteams.enabled && - !!config.msteams.appId && - !!config.msteams.tenantId; + (config.msteams?.enabled ?? false) && + !!config.msteams?.appId && + !!config.msteams?.tenantId; const configured = active || - config.msteams.enabled || - !!config.msteams.appId || - !!config.msteams.tenantId || + (config.msteams?.enabled ?? false) || + !!config.msteams?.appId || + !!config.msteams?.tenantId || teamCount > 0 || overrideCount > 0; const statusTone = active @@ -248,7 +259,9 @@ function describeMSTeams(config: AdminConfig): ChannelCatalogItem { return { kind: 'msteams', label: 'Microsoft Teams', - summary: `${pluralize(teamCount, 'team default')} · ${pluralize(overrideCount, 'channel override')}`, + summary: config.msteams + ? `${pluralize(teamCount, 'team default')} · ${pluralize(overrideCount, 'channel override')}` + : 'Not configured', statusTone, statusLabel: statusTone === 'active' @@ -263,22 +276,24 @@ function describeIMessage( config: AdminConfig, options: ChannelCatalogOptions, ): ChannelCatalogItem { - const isRemote = config.imessage.backend === 'bluebubbles'; + const isRemote = config.imessage?.backend === 'bluebubbles'; const passwordConfigured = options.imessagePasswordConfigured === true; const active = isRemote - ? config.imessage.enabled && + ? (config.imessage?.enabled ?? false) && passwordConfigured && - !!config.imessage.serverUrl && - !!config.imessage.webhookPath - : config.imessage.enabled && - !!config.imessage.cliPath && - !!config.imessage.dbPath; + !!config.imessage?.serverUrl && + !!config.imessage?.webhookPath + : (config.imessage?.enabled ?? false) && + !!config.imessage?.cliPath && + !!config.imessage?.dbPath; const statusTone = active ? 'active' : 'available'; return { kind: 'imessage', label: 'iMessage', - summary: `${isRemote ? 'Remote' : 'Local'} backend · DM ${config.imessage.dmPolicy}`, + summary: config.imessage + ? `${isRemote ? 'Remote' : 'Local'} backend · DM ${config.imessage.dmPolicy}` + : 'Not configured', statusTone, statusLabel: statusTone === 'active' ? 'active' : 'available', }; diff --git a/console/src/routes/jobs.tsx b/console/src/routes/jobs.tsx index 56b7a60d..b9117dd8 100644 --- a/console/src/routes/jobs.tsx +++ b/console/src/routes/jobs.tsx @@ -730,6 +730,14 @@ export function JobsPage() {

) : null} + {allItems.length === 0 ? ( +
+

Jobs are async tasks created by the agent or triggered manually.

+ + New Job + +
+ ) : (
) : null}
+ )} ); } diff --git a/console/src/routes/mcp.tsx b/console/src/routes/mcp.tsx index 058febfc..90bc420a 100644 --- a/console/src/routes/mcp.tsx +++ b/console/src/routes/mcp.tsx @@ -101,6 +101,13 @@ export function McpPage() { const toast = useToast(); const [selectedName, setSelectedName] = useState(null); const [draft, setDraft] = useState(createDraft()); + const [showEditor, setShowEditor] = useState(false); + + const openNewServer = () => { + setSelectedName(null); + setDraft(createDraft()); + setShowEditor(true); + }; const mcpQuery = useQuery({ queryKey: ['mcp', auth.token], @@ -129,6 +136,7 @@ export function McpPage() { queryClient.setQueryData(['mcp', auth.token], payload); setSelectedName(null); setDraft(createDraft()); + setShowEditor(false); toast.success('MCP server deleted.'); }, onError: (error) => { @@ -154,226 +162,262 @@ export function McpPage() { + New server + + ) : null + } + /> + + {!mcpQuery.isLoading && + !mcpQuery.isError && + !mcpQuery.data?.servers.length && + !showEditor ? ( +
+

+ MCP servers let the agent call external tools over the Model Context + Protocol. +

- } - /> - -
- - {mcpQuery.isLoading ? ( -
Loading MCP servers...
- ) : mcpQuery.data?.servers.length ? ( -
- {mcpQuery.data.servers.map((server) => ( +
+ ) : ( +
+ + {mcpQuery.isLoading ? ( +
Loading MCP servers...
+ ) : mcpQuery.data?.servers.length ? ( +
+ {mcpQuery.data.servers.map((server) => ( + + ))} +
+ ) : ( +
+

+ MCP servers let the agent call external tools over the Model + Context Protocol. +

- ))} -
- ) : ( -
- No MCP servers are configured yet. -
- )} -
- - -
-
- - -
- - - setDraft((current) => ({ - ...current, - enabled, - })) - } - /> +
+ )} +
- {draft.transport === 'stdio' ? ( - <> - + {showEditor ? ( + +