= {
+ connected: t('mcp.statusConnected'),
+ connecting: t('mcp.connecting'),
+ error: t('mcp.statusError'),
+ disabled: t('mcp.statusDisabled'),
+ };
+ return (
+
+ {badgeLabel[runtime] ?? badgeLabel.disabled}
+
+ );
+ })()}
{cardVO.description ||
diff --git a/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx b/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx
index e9feefbc..4429f795 100644
--- a/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx
+++ b/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx
@@ -103,8 +103,8 @@ const PluginInstalledComponent = forwardRef<
getExtensionList();
}
- async function getExtensionList() {
- setLoading(true);
+ async function getExtensionList(silent = false) {
+ if (!silent) setLoading(true);
try {
const client = getCloudServiceClientSync();
@@ -200,12 +200,25 @@ const PluginInstalledComponent = forwardRef<
setExtensionList(extensions);
} catch (error) {
console.error('Failed to fetch extension list:', error);
- setExtensionList([]);
+ if (!silent) setExtensionList([]);
} finally {
- setLoading(false);
+ if (!silent) setLoading(false);
}
}
+ // While any MCP server is still connecting, poll quietly so the status badge
+ // transitions (connecting -> connected/error) without a manual refresh.
+ useEffect(() => {
+ const hasConnecting = extensionList.some(
+ (e) => e.type === 'mcp' && e.enabled && e.runtimeStatus === 'connecting',
+ );
+ if (!hasConnecting) return;
+ const timer = setInterval(() => {
+ getExtensionList(true);
+ }, 3000);
+ return () => clearInterval(timer);
+ }, [extensionList]);
+
useImperativeHandle(ref, () => ({
refreshPluginList: getExtensionList,
}));
diff --git a/web/src/app/infra/entities/api/index.ts b/web/src/app/infra/entities/api/index.ts
index 44edd872..aa1e2c8b 100644
--- a/web/src/app/infra/entities/api/index.ts
+++ b/web/src/app/infra/entities/api/index.ts
@@ -325,6 +325,10 @@ export interface SystemLimitation {
max_bots: number;
max_pipelines: number;
max_extensions: number;
+ /** When non-empty, every pipeline is forced to this Box sandbox-scope
+ * template (e.g. ``{global}``) and the per-pipeline "Sandbox Scope"
+ * selector is locked. Used by SaaS deployments. Empty = no restriction. */
+ force_box_session_id_template?: string;
}
export interface WizardProgress {