From 01ca20e357d84495bf562af0965907997b0a379d Mon Sep 17 00:00:00 2001 From: Maximilian Noller Date: Mon, 13 Apr 2026 02:46:58 +0200 Subject: [PATCH 1/4] feat(console): improve empty states for Jobs, Scheduler, Plugins, and MCP pages - **Jobs**: show a centered empty state with description and "New Job" CTA when the board has no jobs at all, instead of showing five empty columns - **Scheduler**: replace bare "No scheduled work yet." with a centered CTA that includes a description and "New job" button; hide the editor panel on the right until the user clicks "New job" or selects an existing job - **Plugins**: distinguish between "no plugins installed" (shows description + documentation link) and "filter matches nothing" (keeps existing message) - **MCP**: when no servers are configured show a full-page centered empty state with description and "Add your first server" CTA instead of immediately rendering the editor form; once at least one server exists the normal list + editor layout is used - **CSS**: add `.jobs-board-empty` (full-page centered) and `.empty-state-cta` (panel-level centered with border) utility classes used by the new empty states --- console/src/routes/jobs.tsx | 9 +++ console/src/routes/mcp.tsx | 59 +++++++++++++--- console/src/routes/plugins.tsx | 15 ++++ console/src/routes/scheduler.tsx | 118 +++++++++++++++++++------------ console/src/styles.css | 33 +++++++++ 5 files changed, 181 insertions(+), 53 deletions(-) diff --git a/console/src/routes/jobs.tsx b/console/src/routes/jobs.tsx index 56b7a60d..6919b726 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..2b553f9a 100644 --- a/console/src/routes/mcp.tsx +++ b/console/src/routes/mcp.tsx @@ -101,6 +101,7 @@ export function McpPage() { const toast = useToast(); const [selectedName, setSelectedName] = useState(null); const [draft, setDraft] = useState(createDraft()); + const [showEditor, setShowEditor] = useState(false); const mcpQuery = useQuery({ queryKey: ['mcp', auth.token], @@ -154,19 +155,41 @@ export function McpPage() { { + setSelectedName(null); + setDraft(createDraft()); + setShowEditor(true); + }} + > + New server + + ) : null + } + /> + + {!mcpQuery.isLoading && !mcpQuery.data?.servers.length && !showEditor ? ( +
+

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

- } - /> - +
+ ) : (
setSelectedName(server.name)} + onClick={() => { + setSelectedName(server.name); + setShowEditor(true); + }} >
{server.name} @@ -200,12 +226,27 @@ export function McpPage() { ))}
) : ( -
- No MCP servers are configured yet. +
+

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

+
)} + {showEditor ? (
@@ -373,7 +414,9 @@ export function McpPage() { ) : null}
+ ) : null}
+ )}
); } diff --git a/console/src/routes/plugins.tsx b/console/src/routes/plugins.tsx index 41ece876..224d14b8 100644 --- a/console/src/routes/plugins.tsx +++ b/console/src/routes/plugins.tsx @@ -152,6 +152,21 @@ export function PluginsPage() { > {pluginsQuery.isLoading ? (
Loading plugins...
+ ) : pluginsQuery.data?.plugins.length === 0 ? ( +
+

+ Plugins extend HybridClaw with custom commands, tools, and + hooks. +

+ + Plugin documentation + +
) : plugins.length === 0 ? (
No plugins match this filter.
) : ( diff --git a/console/src/routes/scheduler.tsx b/console/src/routes/scheduler.tsx index cbcb79d6..465c2c8d 100644 --- a/console/src/routes/scheduler.tsx +++ b/console/src/routes/scheduler.tsx @@ -1023,6 +1023,11 @@ export function SchedulerPage() { return requestedId.trim() || null; }); const [draft, setDraft] = useState(createDraft()); + const [showEditor, setShowEditor] = useState(() => { + const requestedId = + new URLSearchParams(window.location.search).get('jobId') || ''; + return Boolean(requestedId.trim()); + }); const schedulerQuery = useQuery({ queryKey: ['scheduler', auth.token], @@ -1160,6 +1165,7 @@ export function SchedulerPage() { onClick={() => { setSelectedId(null); setDraft(createDraft()); + setShowEditor(true); }} > New job @@ -1185,7 +1191,10 @@ export function SchedulerPage() { : 'selectable-row' } type="button" - onClick={() => setSelectedId(job.id)} + onClick={() => { + setSelectedId(job.id); + setShowEditor(true); + }} >
{job.name} @@ -1203,54 +1212,73 @@ export function SchedulerPage() { ))}
) : ( -
No scheduled work yet.
+
+

+ Scheduled jobs let you run agent tasks automatically on a cron + schedule. +

+ +
)}
- {isTaskJob(selectedJob) ? ( - - pauseMutation.mutate(selectedJob.disabled ? 'resume' : 'pause') - } - onDelete={() => deleteMutation.mutate()} - /> - ) : ( - setDraft((current) => update(current))} - onSave={() => { - const nextDraft = prepareDraftForSave( - applyResolvedTarget(draft, targetControl), - ); - setDraft(nextDraft); - saveMutation.mutate(nextDraft); - }} - onCancel={() => { - if (selectedConfigJob) { - setDraft(createDraft(selectedConfigJob)); - return; + {showEditor ? ( + isTaskJob(selectedJob) ? ( + + pauseMutation.mutate(selectedJob.disabled ? 'resume' : 'pause') } - setSelectedId(null); - setDraft(createDraft()); - window.location.href = '/admin/jobs'; - }} - onPauseToggle={() => - pauseMutation.mutate( - selectedConfigJob?.disabled ? 'resume' : 'pause', - ) - } - onDelete={() => deleteMutation.mutate()} - /> - )} + onDelete={() => deleteMutation.mutate()} + /> + ) : ( + setDraft((current) => update(current))} + onSave={() => { + const nextDraft = prepareDraftForSave( + applyResolvedTarget(draft, targetControl), + ); + setDraft(nextDraft); + saveMutation.mutate(nextDraft); + }} + onCancel={() => { + if (selectedConfigJob) { + setDraft(createDraft(selectedConfigJob)); + return; + } + setSelectedId(null); + setDraft(createDraft()); + setShowEditor(false); + window.location.href = '/admin/jobs'; + }} + onPauseToggle={() => + pauseMutation.mutate( + selectedConfigJob?.disabled ? 'resume' : 'pause', + ) + } + onDelete={() => deleteMutation.mutate()} + /> + ) + ) : null}
); diff --git a/console/src/styles.css b/console/src/styles.css index 98345d09..1c99a650 100644 --- a/console/src/styles.css +++ b/console/src/styles.css @@ -1882,6 +1882,39 @@ th { background: var(--jobs-empty); } +.jobs-board-empty { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + padding: 64px 24px; + text-align: center; + color: var(--muted-foreground); +} + +.jobs-board-empty p { + margin: 0; + max-width: 300px; +} + +.empty-state-cta { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + padding: 32px 24px; + text-align: center; + color: var(--muted-foreground); + border-radius: var(--radius-md); + border: 1px solid var(--line); + background: var(--panel-muted); +} + +.empty-state-cta p { + margin: 0; + max-width: 280px; +} + .jobs-card { display: grid; gap: 8px; From 085c119bad82cc7c012877a503967696ef602e33 Mon Sep 17 00:00:00 2001 From: Maximilian Noller Date: Mon, 13 Apr 2026 02:57:38 +0200 Subject: [PATCH 2/4] fix(console): guard against absent integration keys in channels-catalog When an integration (slack, discord, telegram, etc.) is not present in the config object at all, the describe* functions in channels-catalog.ts would crash with "Cannot read properties of undefined". Use optional chaining and nullish coalescing throughout so each function safely returns an "available / not configured" state when its config section is absent rather than throwing. Exposed by the scheduler page on instances without Slack configured. --- console/src/routes/channels-catalog.ts | 100 ++++++++++++++----------- 1 file changed, 55 insertions(+), 45 deletions(-) diff --git a/console/src/routes/channels-catalog.ts b/console/src/routes/channels-catalog.ts index 22333297..01bb598b 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); } @@ -62,7 +62,7 @@ function describeDiscord( const guildCount = countDiscordGuilds(config); const overrideCount = countDiscordOverrides(config); const enabled = - config.discord.commandsOnly || config.discord.groupPolicy !== 'disabled'; + (config.discord?.commandsOnly || config.discord?.groupPolicy !== 'disabled') ?? false; const tokenConfigured = options.discordTokenConfigured === true; const active = enabled && tokenConfigured; const configured = @@ -76,7 +76,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' @@ -93,11 +95,11 @@ function describeWhatsApp( ): ChannelCatalogItem { const linked = options.whatsappLinked === true; const enabled = - config.whatsapp.dmPolicy !== 'disabled' || - config.whatsapp.groupPolicy !== 'disabled'; + config.whatsapp?.dmPolicy !== 'disabled' || + config.whatsapp?.groupPolicy !== 'disabled'; 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 +126,15 @@ 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 +144,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 +164,14 @@ 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 +181,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 +200,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 +220,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 +235,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 +254,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 +271,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', }; From 4d4dd862e750f946eff7cd1d792b19bc9cee5ffc Mon Sep 17 00:00:00 2001 From: Maximilian Noller Date: Mon, 13 Apr 2026 03:03:31 +0200 Subject: [PATCH 3/4] refactor(console): simplify empty-state code after review - Rename .jobs-board-empty to .page-empty (was also used on MCP page) - Extract openNewServer() and openNewJob() callbacks to remove 5 inline triple-setter duplications across mcp.tsx and scheduler.tsx - Fix mcp.tsx empty-state condition: guard with !mcpQuery.isError so a fetch error no longer silently shows the "Add your first server" CTA - Deduplicate URLSearchParams parse in scheduler.tsx useState initializers --- console/src/routes/channels-catalog.ts | 11 +++++++--- console/src/routes/jobs.tsx | 2 +- console/src/routes/mcp.tsx | 28 ++++++++++---------------- console/src/routes/scheduler.tsx | 27 ++++++++++--------------- console/src/styles.css | 4 ++-- 5 files changed, 33 insertions(+), 39 deletions(-) diff --git a/console/src/routes/channels-catalog.ts b/console/src/routes/channels-catalog.ts index 01bb598b..135b2607 100644 --- a/console/src/routes/channels-catalog.ts +++ b/console/src/routes/channels-catalog.ts @@ -62,7 +62,9 @@ function describeDiscord( const guildCount = countDiscordGuilds(config); const overrideCount = countDiscordOverrides(config); const enabled = - (config.discord?.commandsOnly || config.discord?.groupPolicy !== 'disabled') ?? false; + (config.discord?.commandsOnly || + config.discord?.groupPolicy !== 'disabled') ?? + false; const tokenConfigured = options.discordTokenConfigured === true; const active = enabled && tokenConfigured; const configured = @@ -128,7 +130,8 @@ function describeTelegram( const inboundEnabled = config.telegram?.dmPolicy !== 'disabled' || config.telegram?.groupPolicy !== 'disabled'; - const active = (config.telegram?.enabled ?? false) && tokenConfigured && inboundEnabled; + const active = + (config.telegram?.enabled ?? false) && tokenConfigured && inboundEnabled; const configured = active || (config.telegram?.enabled ?? false) || @@ -164,7 +167,9 @@ function describeSlack( const botTokenConfigured = options.slackBotTokenConfigured === true; const appTokenConfigured = options.slackAppTokenConfigured === true; const active = - (config.slack?.enabled ?? false) && botTokenConfigured && appTokenConfigured; + (config.slack?.enabled ?? false) && + botTokenConfigured && + appTokenConfigured; const configured = active || (config.slack?.enabled ?? false) || diff --git a/console/src/routes/jobs.tsx b/console/src/routes/jobs.tsx index 6919b726..b9117dd8 100644 --- a/console/src/routes/jobs.tsx +++ b/console/src/routes/jobs.tsx @@ -731,7 +731,7 @@ export function JobsPage() { ) : null} {allItems.length === 0 ? ( -
+

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

New Job diff --git a/console/src/routes/mcp.tsx b/console/src/routes/mcp.tsx index 2b553f9a..4e1b4a62 100644 --- a/console/src/routes/mcp.tsx +++ b/console/src/routes/mcp.tsx @@ -103,6 +103,12 @@ export function McpPage() { 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], queryFn: () => fetchMcp(auth.token), @@ -159,11 +165,7 @@ export function McpPage() { @@ -171,8 +173,8 @@ export function McpPage() { } /> - {!mcpQuery.isLoading && !mcpQuery.data?.servers.length && !showEditor ? ( -
+ {!mcpQuery.isLoading && !mcpQuery.isError && !mcpQuery.data?.servers.length && !showEditor ? ( +

MCP servers let the agent call external tools over the Model Context Protocol. @@ -180,11 +182,7 @@ export function McpPage() { @@ -234,11 +232,7 @@ export function McpPage() { diff --git a/console/src/routes/scheduler.tsx b/console/src/routes/scheduler.tsx index 465c2c8d..81188a2c 100644 --- a/console/src/routes/scheduler.tsx +++ b/console/src/routes/scheduler.tsx @@ -1018,15 +1018,12 @@ export function SchedulerPage() { const toast = useToast(); const queryClient = useQueryClient(); const [selectedId, setSelectedId] = useState(() => { - const requestedId = - new URLSearchParams(window.location.search).get('jobId') || ''; - return requestedId.trim() || null; + const requestedId = new URLSearchParams(window.location.search).get('jobId')?.trim() ?? ''; + return requestedId || null; }); const [draft, setDraft] = useState(createDraft()); const [showEditor, setShowEditor] = useState(() => { - const requestedId = - new URLSearchParams(window.location.search).get('jobId') || ''; - return Boolean(requestedId.trim()); + return Boolean(new URLSearchParams(window.location.search).get('jobId')?.trim()); }); const schedulerQuery = useQuery({ @@ -1154,6 +1151,12 @@ export function SchedulerPage() { window.history.replaceState({}, '', currentUrl.toString()); }, [selectedId]); + const openNewJob = () => { + setSelectedId(null); + setDraft(createDraft()); + setShowEditor(true); + }; + return (

{ - setSelectedId(null); - setDraft(createDraft()); - setShowEditor(true); - }} + onClick={openNewJob} > New job @@ -1220,11 +1219,7 @@ export function SchedulerPage() { diff --git a/console/src/styles.css b/console/src/styles.css index 1c99a650..4d004398 100644 --- a/console/src/styles.css +++ b/console/src/styles.css @@ -1882,7 +1882,7 @@ th { background: var(--jobs-empty); } -.jobs-board-empty { +.page-empty { display: flex; flex-direction: column; align-items: center; @@ -1892,7 +1892,7 @@ th { color: var(--muted-foreground); } -.jobs-board-empty p { +.page-empty p { margin: 0; max-width: 300px; } From 5e3b07c44154d387bb22bfe8b8bbf93c727c0ec3 Mon Sep 17 00:00:00 2001 From: Maximilian Noller Date: Mon, 13 Apr 2026 03:21:46 +0200 Subject: [PATCH 4/4] fix(console): fix three code review issues - describeDiscord: replace ineffective `?? false` with explicit `config.discord ? ... : false` guard so enabled=false (not true) when discord config section is absent - describeWhatsApp: same fix; without it a linked-but-unconfigured device showed as active with "groups undefined" in the summary - mcp.tsx: call setShowEditor(false) in deleteMutation.onSuccess so deleting the last server collapses back to the full-page empty state --- console/src/routes/channels-catalog.ts | 14 +- console/src/routes/mcp.tsx | 403 +++++++++++++------------ 2 files changed, 212 insertions(+), 205 deletions(-) diff --git a/console/src/routes/channels-catalog.ts b/console/src/routes/channels-catalog.ts index 135b2607..fc8af1a2 100644 --- a/console/src/routes/channels-catalog.ts +++ b/console/src/routes/channels-catalog.ts @@ -61,10 +61,9 @@ function describeDiscord( ): ChannelCatalogItem { const guildCount = countDiscordGuilds(config); const overrideCount = countDiscordOverrides(config); - const enabled = - (config.discord?.commandsOnly || - config.discord?.groupPolicy !== 'disabled') ?? - false; + const enabled = config.discord + ? config.discord.commandsOnly || config.discord.groupPolicy !== 'disabled' + : false; const tokenConfigured = options.discordTokenConfigured === true; const active = enabled && tokenConfigured; const configured = @@ -96,9 +95,10 @@ 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}` diff --git a/console/src/routes/mcp.tsx b/console/src/routes/mcp.tsx index 4e1b4a62..90bc420a 100644 --- a/console/src/routes/mcp.tsx +++ b/console/src/routes/mcp.tsx @@ -136,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) => { @@ -173,7 +174,10 @@ export function McpPage() { } /> - {!mcpQuery.isLoading && !mcpQuery.isError && !mcpQuery.data?.servers.length && !showEditor ? ( + {!mcpQuery.isLoading && + !mcpQuery.isError && + !mcpQuery.data?.servers.length && + !showEditor ? (

MCP servers let the agent call external tools over the Model Context @@ -188,228 +192,231 @@ export function McpPage() {

) : ( -
- - {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. +

- ))} -
- ) : ( -
-

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

- -
- )} -
- - {showEditor ? ( - -
-
- - -
- - - setDraft((current) => ({ - ...current, - enabled, - })) - } - /> +
+ )} +
- {draft.transport === 'stdio' ? ( - <> - + {showEditor ? ( + +