feat(M6): Weitere Hosts — MiniApps in Fenstern + AI-Agenten-Ausgabe-Bloecke (#364)

- Windows-Host: openMiniAppWindow-Helper + MiniAppWindowContent (windowStore);
  Oeffnen-Buttons im Chat-Block (MiniAppBlock) und Dashboard-Widget
- AI-Agenten-Host: Core-Tool send_miniapp (app/ai/miniapp_tools.py) —
  miniapp-Block in Agent-Chat (approval_request-Praezedenz), Permission
  fail-closed gegen aufrufenden User pro App; Registrierung im lifespan
- agent_loop: tool_context + agent_name (Raum-Aufloesung)
- Fix: MiniAppBlock nutzt useMiniapps (component-Feld) statt Legacy /comm/miniapps
- Tests: M6 7/7 (TDD rot->gruen), Backend-Regression 57/57, Vitest 26/26
  (4 neue Window-Tests), tsc clean, build OK
This commit is contained in:
Agent Zero
2026-08-31 01:04:33 +02:00
parent 63aa0cf788
commit 335762dd3d
11 changed files with 538 additions and 29 deletions
@@ -1,42 +1,36 @@
import React, { useState, useEffect } from 'react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import type { MessageBlock } from '@/store/commStore';
import { AppWindow, Loader2 } from 'lucide-react';
import { apiClient } from '@/api/client';
import { AppWindow, ExternalLink, Loader2 } from 'lucide-react';
import { useMiniapps, type MiniAppDef } from '@/api/miniapps';
import { MiniAppWindowContent } from '@/components/dashboard/MiniAppWindowContent';
import { openMiniAppWindow } from '@/components/dashboard/openMiniAppWindow';
interface MiniAppBlockProps {
block: MessageBlock;
}
interface MiniAppDef {
app_id: string;
name: string;
icon: string;
description: string;
plugin_name: string;
render_schema: Record<string, any>;
}
const MiniAppBlock: React.FC<MiniAppBlockProps> = ({ block }) => {
const { t } = useTranslation();
const { app_id, config } = block.block_data;
const [appDef, setAppDef] = useState<MiniAppDef | null>(null);
const [loading, setLoading] = useState(false);
// M6: universal registry endpoint (carries component/permission fields;
// the legacy /comm/miniapps list never did, hiding the window button)
const { data: miniappsData, isLoading: loading } = useMiniapps();
const appDef: MiniAppDef | null =
miniappsData?.items.find((a) => a.app_id === app_id) ?? null;
useEffect(() => {
if (!app_id) return;
setLoading(true);
apiClient.get('/comm/miniapps')
.then((res) => {
const apps: MiniAppDef[] = res.data || [];
const found = apps.find((a) => a.app_id === app_id);
setAppDef(found || null);
})
.catch(() => setAppDef(null))
.finally(() => setLoading(false));
}, [app_id]);
const canOpenInWindow = Boolean(appDef?.component);
const openInWindow = () =>
openMiniAppWindow({
appId: app_id || '',
def: appDef ?? undefined,
settings: (config as Record<string, unknown>) ?? {},
component: MiniAppWindowContent,
});
const appId: string = app_id || 'Unbekannt';
const hasConfig = config && typeof config === 'object' && Object.keys(config).length > 0;
const schema = appDef?.render_schema;
const schema = appDef?.render_schema as { fields?: { name: string; label?: string }[] } | undefined;
const hasSchema = schema && Object.keys(schema).length > 0;
if (loading) {
@@ -60,6 +54,18 @@ const MiniAppBlock: React.FC<MiniAppBlockProps> = ({ block }) => {
{appDef?.plugin_name && (
<span className="text-xs px-2 py-0.5 rounded-full bg-secondary-100 text-secondary-600">{appDef.plugin_name}</span>
)}
{canOpenInWindow && (
<button
type="button"
onClick={openInWindow}
className="p-1.5 rounded hover:bg-secondary-100 text-secondary-400 hover:text-primary-600"
aria-label={t('dashboard.openInWindow')}
title={t('dashboard.openInWindow')}
data-testid={`miniapp-open-window-${app_id}`}
>
<ExternalLink className="w-4 h-4" aria-hidden="true" />
</button>
)}
</div>
{hasSchema && schema.fields && (