From 825b59b875c1d80504ac43e1b20906da314e2f28 Mon Sep 17 00:00:00 2001 From: haikdc Date: Sat, 18 Apr 2026 05:10:05 -0700 Subject: [PATCH] [hAIk]: remove outputsSlice; migrate all 19 consumer files from state.outputs to state.apps; rewrite useViewWorkspace to use bridge thunks; remove all auto-run support --- .../src/app/components/Layout/AppShell.tsx | 4 +- .../src/app/components/Layout/Sidebar.tsx | 2 +- .../app/components/useCommandPickerItems.tsx | 8 +- .../src/app/pages/AgentChat/ViewBubble.tsx | 6 +- .../composer/OpenSwarmMentionAdapter.ts | 8 +- .../app/pages/Commands/hooks/useCommands.tsx | 12 +- .../src/app/pages/Dashboard/Dashboard.tsx | 2 +- .../app/pages/Dashboard/DashboardHeader.tsx | 4 +- .../app/pages/Dashboard/DashboardViewCard.tsx | 58 +----- .../app/pages/Dashboard/ViewPickerPanel.tsx | 8 +- .../pages/Dashboard/hooks/useDashboardInit.ts | 4 +- .../pages/Dashboard/useDashboardToolbar.ts | 6 +- .../app/pages/Dashboard/viewCardConstants.ts | 4 +- .../app/pages/Tools/hooks/useToolsState.ts | 12 +- frontend/src/app/pages/Views/ViewCard.tsx | 5 +- frontend/src/app/pages/Views/ViewEditor.tsx | 63 +----- .../src/app/pages/Views/ViewRunDialog.tsx | 22 +- frontend/src/app/pages/Views/Views.tsx | 54 ++--- .../src/app/pages/Views/hooks/useAutoRun.ts | 167 +-------------- .../src/app/pages/Views/hooks/useViewSave.ts | 30 ++- .../app/pages/Views/hooks/useViewWorkspace.ts | 83 ++++---- .../shared/backend-bridge/apps/app_builder.ts | 77 ++++--- frontend/src/shared/state/outputsSlice.ts | 192 ------------------ frontend/src/shared/state/store.ts | 2 - 24 files changed, 198 insertions(+), 635 deletions(-) delete mode 100644 frontend/src/shared/state/outputsSlice.ts diff --git a/frontend/src/app/components/Layout/AppShell.tsx b/frontend/src/app/components/Layout/AppShell.tsx index 1bccee7d..d52c3849 100644 --- a/frontend/src/app/components/Layout/AppShell.tsx +++ b/frontend/src/app/components/Layout/AppShell.tsx @@ -9,7 +9,7 @@ import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt'; import Settings from '@/app/pages/Settings/Settings'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; import { LIST_DASHBOARDS } from '@/shared/backend-bridge/apps/dashboards'; -import { fetchOutputs } from '@/shared/state/outputsSlice'; +import { LIST_APPS } from '@/shared/backend-bridge/apps/app_builder'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { useUpdateNotification } from './hooks/useUpdateNotification'; import { useSidebarResize } from './hooks/useSidebarResize'; @@ -41,7 +41,7 @@ const AppShell: React.FC = () => { useEffect(() => { dispatch(LIST_DASHBOARDS()); - dispatch(fetchOutputs()); + dispatch(LIST_APPS()); }, [dispatch]); return ( diff --git a/frontend/src/app/components/Layout/Sidebar.tsx b/frontend/src/app/components/Layout/Sidebar.tsx index ca6ff2d7..7b716e94 100644 --- a/frontend/src/app/components/Layout/Sidebar.tsx +++ b/frontend/src/app/components/Layout/Sidebar.tsx @@ -47,7 +47,7 @@ const Sidebar: React.FC = ({ showUpdateDot }) => { const dashboardList = Object.values(dashboardItems).sort( (a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(), ); - const appsList = Object.values(useAppSelector((s) => s.outputs.items)).sort( + const appsList = Object.values(useAppSelector((s) => s.apps.items)).sort( (a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(), ); diff --git a/frontend/src/app/components/useCommandPickerItems.tsx b/frontend/src/app/components/useCommandPickerItems.tsx index dd32ed89..55070bfd 100644 --- a/frontend/src/app/components/useCommandPickerItems.tsx +++ b/frontend/src/app/components/useCommandPickerItems.tsx @@ -7,7 +7,7 @@ import BuildOutlinedIcon from '@mui/icons-material/BuildOutlined'; import ViewQuiltOutlinedIcon from '@mui/icons-material/ViewQuiltOutlined'; import { useAppSelector, useAppDispatch } from '@/shared/hooks'; import { fetchBuiltinTools, fetchTools } from '@/shared/state/toolsSlice'; -import { fetchOutputs } from '@/shared/state/outputsSlice'; +import { LIST_APPS } from '@/shared/backend-bridge/apps/app_builder'; import { CommandPickerItem, MODE_ICON_MAP } from './commandPickerTypes'; import { getToolGroupIcon } from './CommandPickerIcons'; @@ -17,16 +17,16 @@ export function useCommandPickerItems(trigger: '/' | '@', filter: string) { const modesMap = useAppSelector((s) => s.modes.items); const builtinTools = useAppSelector((s) => s.tools.builtinTools); const customTools = useAppSelector((s) => s.tools.items); - const outputItems = useAppSelector((s) => s.outputs.items); + const outputItems = useAppSelector((s) => s.apps.items); const toolsLoaded = useAppSelector((s) => s.tools.loaded); const builtinLoaded = useAppSelector((s) => s.tools.builtinLoaded); - const outputsLoaded = useAppSelector((s) => s.outputs.loaded); + const outputsLoaded = useAppSelector((s) => s.apps.loaded); useEffect(() => { if (!builtinLoaded) dispatch(fetchBuiltinTools()); if (!toolsLoaded) dispatch(fetchTools()); - if (!outputsLoaded) dispatch(fetchOutputs()); + if (!outputsLoaded) dispatch(LIST_APPS()); }, [dispatch, builtinLoaded, toolsLoaded, outputsLoaded]); const items: CommandPickerItem[] = useMemo(() => { diff --git a/frontend/src/app/pages/AgentChat/ViewBubble.tsx b/frontend/src/app/pages/AgentChat/ViewBubble.tsx index 90aab881..c1fb48ca 100644 --- a/frontend/src/app/pages/AgentChat/ViewBubble.tsx +++ b/frontend/src/app/pages/AgentChat/ViewBubble.tsx @@ -7,7 +7,7 @@ import Icon from '@mui/material/Icon'; import OpenInFullIcon from '@mui/icons-material/OpenInFull'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { useAppSelector } from '@/shared/hooks'; -import { SERVE_BASE } from '@/shared/state/outputsSlice'; +import { getAppServeUrl } from '@/shared/backend-bridge/apps/app_builder'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import ViewPreview from '../Views/ViewPreview'; import { StreamingPlaceholder, ViewBubbleDialog } from './ViewBubbleParts'; @@ -25,7 +25,7 @@ const ViewBubble: React.FC = ({ toolInput, toolResult, isStreaming }) => const outputId = toolInput?.output_id; const inputData = toolInput?.input_data || {}; - const outputsMap = useAppSelector((state) => state.outputs.items); + const outputsMap = useAppSelector((state) => state.apps.items); const output = outputId ? outputsMap[outputId] : null; const parsedResult = useMemo(() => { @@ -40,7 +40,7 @@ const ViewBubble: React.FC = ({ toolInput, toolResult, isStreaming }) => const outputColor = c.accent.primary; const outputIcon = output?.icon || 'view_quilt'; const hasPreview = !!frontendCode.trim(); - const serveUrl = outputId ? `${SERVE_BASE}/${outputId}/serve/index.html` : undefined; + const serveUrl = outputId ? getAppServeUrl(outputId) : undefined; const inputEntries = Object.entries(inputData); if (isStreaming && !hasPreview) { diff --git a/frontend/src/app/pages/AgentChat/composer/OpenSwarmMentionAdapter.ts b/frontend/src/app/pages/AgentChat/composer/OpenSwarmMentionAdapter.ts index 42f53ba8..53a87ecc 100644 --- a/frontend/src/app/pages/AgentChat/composer/OpenSwarmMentionAdapter.ts +++ b/frontend/src/app/pages/AgentChat/composer/OpenSwarmMentionAdapter.ts @@ -6,7 +6,7 @@ import type { } from '@assistant-ui/core'; import { useAppSelector, useAppDispatch } from '@/shared/hooks'; import { fetchBuiltinTools, fetchTools } from '@/shared/state/toolsSlice'; -import { fetchOutputs } from '@/shared/state/outputsSlice'; +import { LIST_APPS } from '@/shared/backend-bridge/apps/app_builder'; export interface MentionItemMetadata { itemType: 'skill' | 'mode' | 'file' | 'tool-group' | 'output'; @@ -44,16 +44,16 @@ export function useOpenSwarmMentionAdapter(): Unstable_MentionAdapter { const modesMap = useAppSelector((s) => s.modes.items); const builtinTools = useAppSelector((s) => s.tools.builtinTools); const customTools = useAppSelector((s) => s.tools.items); - const outputItems = useAppSelector((s) => s.outputs.items); + const outputItems = useAppSelector((s) => s.apps.items); const toolsLoaded = useAppSelector((s) => s.tools.loaded); const builtinLoaded = useAppSelector((s) => s.tools.builtinLoaded); - const outputsLoaded = useAppSelector((s) => s.outputs.loaded); + const outputsLoaded = useAppSelector((s) => s.apps.loaded); useEffect(() => { if (!builtinLoaded) dispatch(fetchBuiltinTools()); if (!toolsLoaded) dispatch(fetchTools()); - if (!outputsLoaded) dispatch(fetchOutputs()); + if (!outputsLoaded) dispatch(LIST_APPS()); }, [dispatch, builtinLoaded, toolsLoaded, outputsLoaded]); const { categories, itemsByCategory, allItems } = useMemo(() => { diff --git a/frontend/src/app/pages/Commands/hooks/useCommands.tsx b/frontend/src/app/pages/Commands/hooks/useCommands.tsx index bafca945..31afa512 100644 --- a/frontend/src/app/pages/Commands/hooks/useCommands.tsx +++ b/frontend/src/app/pages/Commands/hooks/useCommands.tsx @@ -6,9 +6,9 @@ import ViewQuiltOutlinedIcon from '@mui/icons-material/ViewQuiltOutlined'; import { useAppSelector, useAppDispatch } from '@/shared/hooks'; import { fetchBuiltinTools, fetchTools } from '@/shared/state/toolsSlice'; import { getToolGroupIcon } from '@/app/components/CommandPicker'; -import { fetchOutputs } from '@/shared/state/outputsSlice'; +import { LIST_APPS } from '@/shared/backend-bridge/apps/app_builder'; import { fetchSkills } from '@/shared/state/skillsSlice'; -import { fetchModes } from '@/shared/state/modesSlice'; +import { LIST_MODES } from '@/shared/state/modesSlice'; import { SlashCommand, AtCommand, SHORTCUTS } from '../commandsTypes'; export function useCommands() { @@ -17,20 +17,20 @@ export function useCommands() { const modesMap = useAppSelector((state) => state.modes.items); const builtinTools = useAppSelector((state) => state.tools.builtinTools); const customTools = useAppSelector((state) => state.tools.items); - const outputItems = useAppSelector((state) => state.outputs.items); + const outputItems = useAppSelector((state) => state.apps.items); const skillsLoaded = useAppSelector((state) => state.skills.loaded); const modesLoaded = useAppSelector((state) => state.modes.loaded); const builtinLoaded = useAppSelector((state) => state.tools.builtinLoaded); const toolsLoaded = useAppSelector((state) => state.tools.loaded); - const outputsLoaded = useAppSelector((state) => state.outputs.loaded); + const outputsLoaded = useAppSelector((state) => state.apps.loaded); useEffect(() => { if (!skillsLoaded) dispatch(fetchSkills()); - if (!modesLoaded) dispatch(fetchModes()); + if (!modesLoaded) dispatch(LIST_MODES()); if (!builtinLoaded) dispatch(fetchBuiltinTools()); if (!toolsLoaded) dispatch(fetchTools()); - if (!outputsLoaded) dispatch(fetchOutputs()); + if (!outputsLoaded) dispatch(LIST_APPS()); }, [dispatch, skillsLoaded, modesLoaded, builtinLoaded, toolsLoaded, outputsLoaded]); const slashCommands: SlashCommand[] = useMemo(() => [ diff --git a/frontend/src/app/pages/Dashboard/Dashboard.tsx b/frontend/src/app/pages/Dashboard/Dashboard.tsx index 213e8d7f..4c7261e8 100644 --- a/frontend/src/app/pages/Dashboard/Dashboard.tsx +++ b/frontend/src/app/pages/Dashboard/Dashboard.tsx @@ -51,7 +51,7 @@ const DashboardInner: React.FC = () => { const browserHomepage = useAppSelector((s) => s.settings.data.browser_homepage); const expandNewChats = useAppSelector((s) => s.settings.data.expand_new_chats_in_dashboard); const autoRevealSubAgents = useAppSelector((s) => s.settings.data.auto_reveal_sub_agents); - const outputs = useAppSelector((s) => s.outputs.items); + const outputs = useAppSelector((s) => s.apps.items); const glowingAgentCards = useAppSelector((s) => s.dashboardLayout.glowingAgentCards); const glowingBrowserCards = useAppSelector((s) => s.dashboardLayout.glowingBrowserCards); const pendingBrowserUrl = useAppSelector((s) => s.tempState.pendingBrowserUrl); diff --git a/frontend/src/app/pages/Dashboard/DashboardHeader.tsx b/frontend/src/app/pages/Dashboard/DashboardHeader.tsx index b64d3c13..58889a63 100644 --- a/frontend/src/app/pages/Dashboard/DashboardHeader.tsx +++ b/frontend/src/app/pages/Dashboard/DashboardHeader.tsx @@ -9,7 +9,7 @@ import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import type { AgentSession } from '@/shared/state/agentsSlice'; import type { CardPosition, ViewCardPosition, BrowserCardPosition } from '@/shared/state/dashboardLayoutSlice'; -import type { Output } from '@/shared/state/outputsSlice'; +import type { App } from '@/shared/backend-bridge/apps/app_builder'; import type { CanvasActions } from './useCanvasControls'; import { STATUS_DOT, cleanUrl, CategoryGroup, ItemRow } from './DashboardHeaderParts'; @@ -19,7 +19,7 @@ interface DashboardHeaderProps { cards: Record; viewCards: Record; browserCards: Record; - outputs: Record; + outputs: Record; dashboardId: string | undefined; canvasActions: CanvasActions; onHighlightCard?: (cardId: string) => void; diff --git a/frontend/src/app/pages/Dashboard/DashboardViewCard.tsx b/frontend/src/app/pages/Dashboard/DashboardViewCard.tsx index 81196f1f..0012dc1b 100644 --- a/frontend/src/app/pages/Dashboard/DashboardViewCard.tsx +++ b/frontend/src/app/pages/Dashboard/DashboardViewCard.tsx @@ -1,6 +1,6 @@ import React, { useState, useRef } from 'react'; import Box from '@mui/material/Box'; -import { Output, autoRunOutput, autoRunAgentOutput, executeOutput, getBackendCode, SERVE_BASE } from '@/shared/state/outputsSlice'; +import { EXECUTE_APP, getAppServeUrl } from '@/shared/backend-bridge/apps/app_builder'; import { removeViewCard } from '@/shared/state/dashboardLayoutSlice'; import { useAppDispatch } from '@/shared/hooks'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; @@ -24,9 +24,6 @@ const DashboardViewCard: React.FC = ({ const [inputData, setInputData] = useState>(() => getDefault(output.input_schema)); const [backendResult, setBackendResult] = useState | null>(null); - const [autoRunning, setAutoRunning] = useState(false); - - const hasAutoRun = !!(output.auto_run_config?.enabled && output.auto_run_config?.prompt); const { isDragging, localDragPos, justDraggedRef, @@ -48,49 +45,12 @@ const DashboardViewCard: React.FC = ({ previewRef.current?.reload(); }; - const handleAutoRun = async (e: React.MouseEvent) => { + const handleExecute = async (e: React.MouseEvent) => { e.stopPropagation(); - if (!output.auto_run_config?.prompt) return; - setAutoRunning(true); - - const config = output.auto_run_config; - const forcedToolNames = config.forced_tools?.flatMap((ft) => ft.tools) ?? []; - try { - if (forcedToolNames.length > 0) { - await dispatch(autoRunAgentOutput({ - prompt: config.prompt, - input_schema: output.input_schema, - output_id: output.id, - model: config.model, - forced_tools: forcedToolNames, - context_paths: config.context_paths, - })).unwrap(); - - const execRes = await dispatch(executeOutput({ - output_id: output.id, - input_data: inputData, - })).unwrap(); - setInputData(execRes.input_data); - setBackendResult(execRes.backend_result); - } else { - const res = await dispatch(autoRunOutput({ - prompt: config.prompt, - input_schema: output.input_schema, - backend_code: getBackendCode(output) ?? undefined, - context_paths: config.context_paths, - forced_tools: forcedToolNames.length > 0 ? forcedToolNames : undefined, - model: config.model, - })).unwrap(); - if (res.input_data) { - setInputData(res.input_data); - setBackendResult(res.backend_result); - } - } - } catch { - } finally { - setAutoRunning(false); - } + const res = await dispatch(EXECUTE_APP({ app_id: output.id, input_data: inputData })).unwrap(); + setBackendResult(res.backend_result as Record | null); + } catch {} }; const mdDx = (!isDragging && isSelected && multiDragDelta) ? multiDragDelta.dx : 0; @@ -169,14 +129,14 @@ const DashboardViewCard: React.FC = ({ @@ -186,7 +146,7 @@ const DashboardViewCard: React.FC = ({ )} ; viewSearch: string; onSearchChange: (q: string) => void; - filteredOutputs: Output[]; - outputList: Output[]; - onSelect: (output: Output) => void; + filteredOutputs: App[]; + outputList: App[]; + onSelect: (output: App) => void; c: ClaudeTokens; } diff --git a/frontend/src/app/pages/Dashboard/hooks/useDashboardInit.ts b/frontend/src/app/pages/Dashboard/hooks/useDashboardInit.ts index acb31b76..cbb3ef2b 100644 --- a/frontend/src/app/pages/Dashboard/hooks/useDashboardInit.ts +++ b/frontend/src/app/pages/Dashboard/hooks/useDashboardInit.ts @@ -10,7 +10,7 @@ import { addBrowserCard, EXPANDED_CARD_MIN_H, } from '@/shared/state/dashboardLayoutSlice'; -import { fetchOutputs } from '@/shared/state/outputsSlice'; +import { LIST_APPS } from '@/shared/backend-bridge/apps/app_builder'; import { dashboardWs } from '@/shared/ws/WebSocketManager'; import { initBrowserCommandHandler } from '@/shared/browserCommandHandler'; import { clearPendingBrowserUrl, clearPendingFocusAgentId } from '@/shared/state/tempStateSlice'; @@ -52,7 +52,7 @@ export function useDashboardInit(deps: InitDeps) { dispatch(GET_ALL_SESSIONS(dashboardId)); dispatch(GET_HISTORY({})); dispatch(fetchLayout(dashboardId)); - dispatch(fetchOutputs()); + dispatch(LIST_APPS()); dashboardWs.connect(); const cleanupBrowserHandler = initBrowserCommandHandler(); return () => { cleanupBrowserHandler(); dashboardWs.disconnect(); }; diff --git a/frontend/src/app/pages/Dashboard/useDashboardToolbar.ts b/frontend/src/app/pages/Dashboard/useDashboardToolbar.ts index b39d3f14..e3d3831b 100644 --- a/frontend/src/app/pages/Dashboard/useDashboardToolbar.ts +++ b/frontend/src/app/pages/Dashboard/useDashboardToolbar.ts @@ -5,7 +5,7 @@ import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; import { clearHistorySearch } from '@/shared/state/agentsSlice'; import { GET_HISTORY } from '@/shared/backend-bridge/apps/agents'; -import type { Output } from '@/shared/state/outputsSlice'; +import type { App } from '@/shared/backend-bridge/apps/app_builder'; import type { Props } from './toolbarShared'; import { TOOLBAR_OWNER_ID, HISTORY_PAGE_SIZE } from './toolbarShared'; @@ -28,7 +28,7 @@ export function useDashboardToolbar({ const [historyOpen, setHistoryOpen] = useState(false); const [historyQuery, setHistoryQuery] = useState(''); const shortcut = useAppSelector((s) => s.settings.data.new_agent_shortcut); - const outputs = useAppSelector((s) => s.outputs.items); + const outputs = useAppSelector((s) => s.apps.items); const historySearchState = useAppSelector((s) => s.agents.historySearch); const outputList = useMemo(() => Object.values(outputs), [outputs]); @@ -82,7 +82,7 @@ export function useDashboardToolbar({ } }, [historyOpen, viewPickerOpen, onCancel, handleCloseHistory]); - const handleSelectView = useCallback((output: Output) => { + const handleSelectView = useCallback((output: App) => { onAddView(output.id); setViewPickerOpen(false); setViewSearch(''); diff --git a/frontend/src/app/pages/Dashboard/viewCardConstants.ts b/frontend/src/app/pages/Dashboard/viewCardConstants.ts index ddc4c353..754ab79d 100644 --- a/frontend/src/app/pages/Dashboard/viewCardConstants.ts +++ b/frontend/src/app/pages/Dashboard/viewCardConstants.ts @@ -1,4 +1,4 @@ -import type { Output } from '@/shared/state/outputsSlice'; +import type { App } from '@/shared/backend-bridge/apps/app_builder'; export type ResizeDir = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'; @@ -24,7 +24,7 @@ export const HANDLE_DEFS: { dir: ResizeDir; sx: Record }[] = [ ]; export interface ViewCardProps { - output: Output; + output: App; cardX: number; cardY: number; cardWidth: number; diff --git a/frontend/src/app/pages/Tools/hooks/useToolsState.ts b/frontend/src/app/pages/Tools/hooks/useToolsState.ts index 99bf7f56..a2e07474 100644 --- a/frontend/src/app/pages/Tools/hooks/useToolsState.ts +++ b/frontend/src/app/pages/Tools/hooks/useToolsState.ts @@ -6,17 +6,17 @@ import { ToolDefinition, BuiltinTool, } from '@/shared/state/toolsSlice'; import { searchRegistry, fetchRegistryStats, fetchServerDetail, clearDetail, McpServer } from '@/shared/state/mcpRegistrySlice'; -import { fetchOutputs, updateOutput } from '@/shared/state/outputsSlice'; +import { LIST_APPS, UPDATE_APP } from '@/shared/backend-bridge/apps/app_builder'; import { INTEGRATIONS, Integration, CATEGORY_ORDER } from '../integrations'; import { ToolForm, emptyForm, serverToToolForm, serverToMcpConfig, groupTools } from '../toolUtils'; -import { API_BASE } from '@/shared/config'; + export function useToolsState() { const dispatch = useAppDispatch(); const { items, builtinTools, builtinPermissions, loading } = useAppSelector((s) => s.tools); const { servers: regServers, total: regTotal, loading: regLoading, stats: regStats, detail: regDetail, detailLoading: regDetailLoading } = useAppSelector((s) => s.mcpRegistry); const devMode = useAppSelector((s) => s.settings.data.dev_mode); - const outputItems = useAppSelector((s) => s.outputs.items); + const outputItems = useAppSelector((s) => s.apps.items); const outputs = useMemo(() => Object.values(outputItems), [outputItems]); const allTools = Object.values(items); @@ -70,7 +70,7 @@ export function useToolsState() { const coreSectionEnabled = useMemo(() => !coreTools.every((t) => builtinPermissions[t.name] === 'deny'), [coreTools, builtinPermissions]); const deferredSectionEnabled = useMemo(() => !deferredTools.every((t) => builtinPermissions[t.name] === 'deny'), [deferredTools, builtinPermissions]); const viewsSectionEnabled = useMemo(() => !outputs.every((o) => o.permission === 'deny'), [outputs]); const browserSectionEnabled = useMemo(() => browserTools.length > 0 && !browserTools.every((t) => builtinPermissions[t.name] === 'deny'), [browserTools, builtinPermissions]); - useEffect(() => { dispatch(fetchTools()); dispatch(fetchBuiltinTools()); dispatch(fetchBuiltinPermissions()); dispatch(fetchOutputs()); }, [dispatch]); + useEffect(() => { dispatch(fetchTools()); dispatch(fetchBuiltinTools()); dispatch(fetchBuiltinPermissions()); dispatch(LIST_APPS()); }, [dispatch]); const handleIntegrationToggle = async (integration: Integration) => { const existing = getInstalledIntegration(integration); @@ -223,10 +223,10 @@ export function useToolsState() { const handleEditInstall = (srv: McpServer) => { setRegistryOpen(false); setEditingId(null); setForm(serverToToolForm(srv)); setDialogOpen(true); }; const handleSectionEnabledChange = async (tls: BuiltinTool[], enabled: boolean) => { const perms: Record = {}; for (const t of tls) perms[t.name] = enabled ? 'always_allow' : 'deny'; await dispatch(updateBuiltinPermissions(perms)); }; - const handleViewsSectionEnabledChange = async (enabled: boolean) => { for (const out of outputs) await dispatch(updateOutput({ id: out.id, permission: enabled ? 'ask' : 'deny' })); }; + const handleViewsSectionEnabledChange = async (enabled: boolean) => { for (const out of outputs) await dispatch(UPDATE_APP({ appId: out.id, permission: enabled ? 'ask' : 'deny' })); }; const handleBuiltinPermissionChange = async (toolName: string, policy: string) => { await dispatch(updateBuiltinPermissions({ [toolName]: policy })); }; const handleBuiltinCategoryPermissionChange = async (toolNames: string[], policy: string) => { const perms: Record = {}; for (const name of toolNames) perms[name] = policy; await dispatch(updateBuiltinPermissions(perms)); }; - const handleViewPermissionChange = async (viewId: string, permission: string) => { await dispatch(updateOutput({ id: viewId, permission })); }; + const handleViewPermissionChange = async (viewId: string, permission: string) => { await dispatch(UPDATE_APP({ appId: viewId, permission })); }; const toggleCategory = (cat: string) => setCollapsedCategories((p) => ({ ...p, [cat]: !p[cat] })); const toggleBuiltinExpand = (name: string) => setExpandedBuiltin((p) => (p === name ? null : name)); return { items, builtinPermissions, loading, outputs, devMode, regServers, regTotal, regLoading, regStats, regDetail, regDetailLoading, allTools, tools, uninstalledIntegrations, getIntegrationForTool, coreTools, deferredTools, browserTools, browserDelegationTools, browserActionTools, groupedCore, groupedDeferred, coreSectionEnabled, deferredSectionEnabled, viewsSectionEnabled, browserSectionEnabled, dialogOpen, setDialogOpen, editingId, form, setForm, collapsedCategories, toggleCategory, expandedBuiltin, toggleBuiltinExpand, coreSectionOpen, setCoreSectionOpen, deferredSectionOpen, setDeferredSectionOpen, customSectionOpen, setCustomSectionOpen, menuAnchor, handleMenuOpen, handleMenuClose, registryOpen, setRegistryOpen, regQuery, regSort, regSource, expandedServer, snackbar, setSnackbar, mcpConfigOpen, setMcpConfigOpen, mcpConfigServer, mcpAuthType, setMcpAuthType, mcpCredentials, setMcpCredentials, mcpConfigJson, setMcpConfigJson, mcpConfigError, setMcpConfigError, expandedToolId, setExpandedToolId, discovering, integrationLoading, credDialogOpen, setCredDialogOpen, credDialogIntegration, credDialogValues, setCredDialogValues, credDialogSaving, expandedServices, setExpandedServices, expandedSchema, setExpandedSchema, viewsSectionOpen, setViewsSectionOpen, browserSectionOpen, setBrowserSectionOpen, browserCollapsed, setBrowserCollapsed, builtinSectionOpen, setBuiltinSectionOpen, handleIntegrationToggle, handleDirectConnect, handleOAuthConnect, openCredentialsDialog, handleCredentialsSave, handleDisconnectIntegration, handleDiscover, handlePermissionChange, handleGroupPermissionChange, handleBulkReadOnly, handleResetPermissions, handleSave, handleDelete, openEdit, openCreate, openRegistryBrowser, handleRegSearch, handleLoadMore, handleRegSort, handleRegSourceFilter, handleExpandServer, openMcpConfigDialog, handleMcpConfigSave, handleInstall, handleEditInstall, handleSectionEnabledChange, handleViewsSectionEnabledChange, handleBuiltinPermissionChange, handleBuiltinCategoryPermissionChange, handleViewPermissionChange }; diff --git a/frontend/src/app/pages/Views/ViewCard.tsx b/frontend/src/app/pages/Views/ViewCard.tsx index 7f03b71e..d4e9b031 100644 --- a/frontend/src/app/pages/Views/ViewCard.tsx +++ b/frontend/src/app/pages/Views/ViewCard.tsx @@ -3,15 +3,14 @@ import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import IconButton from '@mui/material/IconButton'; import Tooltip from '@mui/material/Tooltip'; -import EditIcon from '@mui/icons-material/Edit'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import Icon from '@mui/material/Icon'; -import { Output } from '@/shared/state/outputsSlice'; +import type { App } from '@/shared/backend-bridge/apps/app_builder'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; interface Props { - output: Output; + output: App; onClick: () => void; onDelete: () => void; onRun: () => void; diff --git a/frontend/src/app/pages/Views/ViewEditor.tsx b/frontend/src/app/pages/Views/ViewEditor.tsx index b3d59e39..cfea1e54 100644 --- a/frontend/src/app/pages/Views/ViewEditor.tsx +++ b/frontend/src/app/pages/Views/ViewEditor.tsx @@ -1,24 +1,22 @@ import React, { useState, useRef, useCallback, useEffect, PointerEvent as ReactPointerEvent } from 'react'; -import { Box, Typography, Button, IconButton, TextField, Tabs, Tab, Tooltip, Switch, CircularProgress } from '@mui/material'; -import { Save as SaveIcon, PlayArrow as PlayArrowIcon, Add as AddIcon, Bolt as BoltIcon, Refresh as RefreshIcon, CheckCircleOutline as CheckCircleOutlineIcon, AutoFixHigh as AutoFixHighIcon } from '@mui/icons-material'; +import { Box, Typography, Button, IconButton, TextField, Tabs, Tab, Tooltip, CircularProgress } from '@mui/material'; +import { Save as SaveIcon, PlayArrow as PlayArrowIcon, Add as AddIcon, Refresh as RefreshIcon, CheckCircleOutline as CheckCircleOutlineIcon, AutoFixHigh as AutoFixHighIcon } from '@mui/icons-material'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; -import { Output } from '@/shared/state/outputsSlice'; +import type { App } from '@/shared/backend-bridge/apps/app_builder'; import AgentChat from '../AgentChat/AgentChat'; -import ChatInput from '../AgentChat/ChatInput'; import ViewPreview, { ViewPreviewHandle } from './ViewPreview'; import InputSchemaForm, { getDefault, getStubbed } from './InputSchemaForm'; import CodeEditor from './CodeEditor'; import { ElementSelectionProvider } from '@/app/components/ElementSelectionContext'; import { FileTreeItem, getEditorLanguage } from './FileTree'; -import { AutoRunLog } from './AutoRunLog'; import { ConsolePanel } from './ConsolePanel'; import { useViewWorkspace } from './hooks/useViewWorkspace'; import { useAutoRun } from './hooks/useAutoRun'; import { useViewSave } from './hooks/useViewSave'; -interface Props { output: Output | null; onClose: () => void; } +interface Props { output: App | null; onClose: () => void; } -const TAB_PREVIEW = 0, TAB_CODE = 1, TAB_TEST_INPUT = 2, TAB_AUTO_RUN = 3, TAB_CONSOLE = 4; +const TAB_PREVIEW = 0, TAB_CODE = 1, TAB_TEST_INPUT = 2, TAB_CONSOLE = 3; const SIDEBAR_MIN = 280, SIDEBAR_MAX = 800; const ViewEditor: React.FC = ({ output, onClose }) => { @@ -42,8 +40,7 @@ const ViewEditor: React.FC = ({ output, onClose }) => { const ar = useAutoRun(output, createdIdRef, ws.files, ws.name, setActiveTab); const save = useViewSave({ output, name: ws.name, description: ws.description, files: ws.files, - testInput: ar.testInput, onClose, previewRef, getAutoRunConfig: ar.getAutoRunConfig, - autoRunEnabled: ar.autoRunEnabled, autoRunMode: ar.autoRunMode, autoRunModel: ar.autoRunModel, + testInput: ar.testInput, onClose, previewRef, createdIdRef, setCreatedId, }); @@ -86,12 +83,6 @@ const ViewEditor: React.FC = ({ output, onClose }) => { sx={{ flex: 1, maxWidth: 220, '& .MuiInput-input': { fontSize: '0.9rem', fontWeight: 600, color: c.text.primary }, '& .MuiInput-underline:before': { borderColor: 'transparent' }, '& .MuiInput-underline:hover:before': { borderColor: c.border.medium } }} /> ws.setDescription(e.target.value)} placeholder="Description" variant="standard" size="small" sx={{ flex: 2, '& .MuiInput-input': { fontSize: '0.78rem', color: c.text.muted }, '& .MuiInput-underline:before': { borderColor: 'transparent' } }} /> - {ar.autoRunEnabled && ( - - )} {save.saveStatus === 'unsaved' && Unsaved changes} {save.saveStatus === 'saving' && Saving…} {save.saveStatus === 'saved' && Saved} @@ -107,7 +98,6 @@ const ViewEditor: React.FC = ({ output, onClose }) => { - {showConsole && } {activeTab === TAB_PREVIEW && (<> @@ -176,47 +166,6 @@ const ViewEditor: React.FC = ({ output, onClose }) => { )} {activeTab === TAB_CONSOLE && } - - - ar.setAutoRunEnabled(v)} size="small" - sx={{ '& .MuiSwitch-switchBase.Mui-checked': { color: '#f59e0b' }, '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { bgcolor: '#f59e0b' } }} /> - - {ar.autoRunEnabled ? 'Auto Run enabled' : 'Auto Run disabled'} - - - {ar.autoRunEnabled && ( - - )} - - - {ar.autoRunEnabled ? ( - - - Describe what data to generate for this app. When triggered, an LLM will produce input data matching your schema and populate the preview. - - {}} mode={ar.autoRunMode} onModeChange={ar.setAutoRunMode} model={ar.autoRunModel} onModelChange={ar.setAutoRunModel} /> - - {(ar.autoRunSessionId || ar.autoRunMessages.length > 0) && ( - - )} - - ) : ( - - - Enable Auto Run to generate live data for this app - - Configure a prompt that describes what data to generate. An LLM will produce input matching your schema and populate the preview automatically. - - - )} - - diff --git a/frontend/src/app/pages/Views/ViewRunDialog.tsx b/frontend/src/app/pages/Views/ViewRunDialog.tsx index adacfa16..ff0cfb7c 100644 --- a/frontend/src/app/pages/Views/ViewRunDialog.tsx +++ b/frontend/src/app/pages/Views/ViewRunDialog.tsx @@ -7,14 +7,14 @@ import Button from '@mui/material/Button'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import CircularProgress from '@mui/material/CircularProgress'; -import { Output, executeOutput, OutputExecuteResult, getFrontendCode, getBackendCode, buildServeUrl, SERVE_BASE } from '@/shared/state/outputsSlice'; +import { EXECUTE_APP, getAppServeUrl, App, AppExecuteResult } from '@/shared/backend-bridge/apps/app_builder'; import { useAppDispatch } from '@/shared/hooks'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import InputSchemaForm, { getDefault } from './InputSchemaForm'; import ViewPreview from './ViewPreview'; interface Props { - output: Output; + output: App; onClose: () => void; } @@ -24,14 +24,14 @@ const ViewRunDialog: React.FC = ({ output, onClose }) => { const defaultInput = useMemo(() => getDefault(output.input_schema), [output.input_schema]); const [inputData, setInputData] = useState>(defaultInput); - const [result, setResult] = useState(null); + const [result, setResult] = useState(null); const [running, setRunning] = useState(false); const handleRun = async () => { setRunning(true); try { const res = await dispatch( - executeOutput({ output_id: output.id, input_data: inputData }) + EXECUTE_APP({ app_id: output.id, input_data: inputData }) ).unwrap(); setResult(res); } finally { @@ -109,15 +109,15 @@ const ViewRunDialog: React.FC = ({ output, onClose }) => { )} {result ? ( | null} /> ) : ( )} @@ -133,7 +133,7 @@ const ViewRunDialog: React.FC = ({ output, onClose }) => { disabled={running} sx={{ bgcolor: c.accent.primary, '&:hover': { bgcolor: c.accent.hover } }} > - {running ? 'Running...' : getBackendCode(output) ? 'Execute & Preview' : 'Preview'} + {running ? 'Running...' : output.files?.['backend.py'] ? 'Execute & Preview' : 'Preview'} diff --git a/frontend/src/app/pages/Views/Views.tsx b/frontend/src/app/pages/Views/Views.tsx index 7de1b83a..9ed37574 100644 --- a/frontend/src/app/pages/Views/Views.tsx +++ b/frontend/src/app/pages/Views/Views.tsx @@ -5,7 +5,7 @@ import Typography from '@mui/material/Typography'; import Button from '@mui/material/Button'; import AddIcon from '@mui/icons-material/Add'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; -import { fetchOutputs, deleteOutput, Output } from '@/shared/state/outputsSlice'; +import { LIST_APPS, DELETE_APP, App } from '@/shared/backend-bridge/apps/app_builder'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import ViewCard from './ViewCard'; import ViewEditor from './ViewEditor'; @@ -16,32 +16,32 @@ const Views: React.FC = () => { const dispatch = useAppDispatch(); const navigate = useNavigate(); const { id: routeId } = useParams<{ id: string }>(); - const items = useAppSelector((state) => state.outputs.items); - const loading = useAppSelector((state) => state.outputs.loading); - const loaded = useAppSelector((state) => state.outputs.loaded); - const outputs = useMemo(() => Object.values(items), [items]); + const items = useAppSelector((state) => state.apps.items); + const loading = useAppSelector((state) => state.apps.loading); + const loaded = useAppSelector((state) => state.apps.loaded); + const apps = useMemo(() => Object.values(items), [items]); const [editorOpen, setEditorOpen] = useState(false); - const [editingOutput, setEditingOutput] = useState(null); - const [runOutput, setRunOutput] = useState(null); + const [editingApp, setEditingApp] = useState(null); + const [runApp, setRunApp] = useState(null); useEffect(() => { - dispatch(fetchOutputs()); + dispatch(LIST_APPS()); }, [dispatch]); useEffect(() => { if (!loaded) return; if (routeId === 'new') { - setEditingOutput(null); + setEditingApp(null); setEditorOpen(true); } else if (routeId && items[routeId]) { - setEditingOutput(items[routeId]); + setEditingApp(items[routeId]); setEditorOpen(true); } else if (routeId && routeId !== 'new') { navigate('/apps', { replace: true }); } else if (!routeId) { setEditorOpen(false); - setEditingOutput(null); + setEditingApp(null); } }, [routeId, loaded, items, navigate]); @@ -49,23 +49,23 @@ const Views: React.FC = () => { navigate('/apps/new'); }; - const handleEditView = (output: Output) => { - navigate(`/apps/${output.id}`); + const handleEditView = (app: App) => { + navigate(`/apps/${app.id}`); }; const handleDeleteView = (id: string) => { - dispatch(deleteOutput(id)); + dispatch(DELETE_APP(id)); }; const handleEditorClose = () => { setEditorOpen(false); - setEditingOutput(null); - dispatch(fetchOutputs()); + setEditingApp(null); + dispatch(LIST_APPS()); navigate('/apps'); }; if (editorOpen) { - return ; + return ; } return ( @@ -118,7 +118,7 @@ const Views: React.FC = () => { Loading... - ) : outputs.length === 0 ? ( + ) : apps.length === 0 ? ( { gap: 2.5, }} > - {outputs.map((output) => ( + {apps.map((app) => ( handleEditView(output)} - onDelete={() => handleDeleteView(output.id)} - onRun={() => setRunOutput(output)} + key={app.id} + output={app} + onClick={() => handleEditView(app)} + onDelete={() => handleDeleteView(app.id)} + onRun={() => setRunApp(app)} /> ))} )} - {runOutput && ( + {runApp && ( setRunOutput(null)} + output={runApp} + onClose={() => setRunApp(null)} /> )} diff --git a/frontend/src/app/pages/Views/hooks/useAutoRun.ts b/frontend/src/app/pages/Views/hooks/useAutoRun.ts index 2939a3a7..a8b48e2f 100644 --- a/frontend/src/app/pages/Views/hooks/useAutoRun.ts +++ b/frontend/src/app/pages/Views/hooks/useAutoRun.ts @@ -1,18 +1,11 @@ -import React, { useState, useEffect, useRef, useMemo } from 'react'; -import { useAppDispatch, useAppSelector } from '@/shared/hooks'; -import { - Output, OutputExecuteResult, - executeOutput, autoRunOutput, autoRunAgentOutput, - cleanupAutoRunAgent, AutoRunConfig, -} from '@/shared/state/outputsSlice'; -import { ChatInputHandle } from '../../AgentChat/ChatInput'; +import React, { useState, useMemo } from 'react'; +import { useAppDispatch } from '@/shared/hooks'; +import { EXECUTE_APP, App, AppExecuteResult } from '@/shared/backend-bridge/apps/app_builder'; import { getDefault } from '../InputSchemaForm'; import type { ConsoleEntry } from '../ConsolePanel'; -const TAB_PREVIEW = 0; - export function useAutoRun( - output: Output | null, + app: App | null, createdIdRef: React.MutableRefObject, files: Record, name: string, @@ -27,171 +20,29 @@ export function useAutoRun( const testInputDefault = useMemo(() => getDefault(parsedSchema), [parsedSchema]); const [testInput, setTestInput] = useState>(testInputDefault); - useEffect(() => { setTestInput(getDefault(parsedSchema)); }, [schemaText]); - const [executeResult, setExecuteResult] = useState(null); + const [executeResult, setExecuteResult] = useState(null); const [consoleEntry, setConsoleEntry] = useState(null); const [hasNewConsoleOutput, setHasNewConsoleOutput] = useState(false); - const savedAutoRun = output?.auto_run_config; - const [autoRunEnabled, setAutoRunEnabled] = useState(savedAutoRun?.enabled ?? false); - const [autoRunMode, setAutoRunMode] = useState(savedAutoRun?.mode ?? 'agent'); - const [autoRunModel, setAutoRunModel] = useState(savedAutoRun?.model ?? 'sonnet'); - const [autoRunning, setAutoRunning] = useState(false); - const autoRunInputRef = useRef(null); - const autoRunInitialized = useRef(false); - - const [autoRunSessionId, setAutoRunSessionId] = useState(null); - const autoRunLogEndRef = useRef(null); - - const autoRunSession = useAppSelector((state) => - autoRunSessionId ? state.agents.sessions[autoRunSessionId] : null - ); - const autoRunMessages = autoRunSession?.messages ?? []; - const autoRunSessionStatus = autoRunSession?.status ?? null; - - useEffect(() => { - if (autoRunInitialized.current || !savedAutoRun) return; - if (!autoRunEnabled) return; - autoRunInitialized.current = true; - const timer = setTimeout(() => { - autoRunInputRef.current?.setContent( - savedAutoRun.prompt || '', - savedAutoRun.context_paths?.map((cp) => ({ path: cp.path, type: (cp.type as 'file' | 'directory') || 'file' })), - savedAutoRun.forced_tools, - ); - }, 100); - return () => clearTimeout(timer); - }, [savedAutoRun, autoRunEnabled]); - - const getAutoRunConfig = (): AutoRunConfig => { - const config = autoRunInputRef.current?.getConfig(); - return { - enabled: autoRunEnabled, - prompt: config?.prompt ?? '', - context_paths: config?.contextPaths?.map((cp) => ({ path: cp.path, type: cp.type })) ?? [], - forced_tools: (config?.forcedTools ?? []).map(({ label, tools, iconKey }) => ({ label, tools, iconKey })), - mode: autoRunMode, - model: autoRunModel, - }; - }; - const handleRunPreview = async () => { - const eid = output?.id ?? createdIdRef.current; + const eid = app?.id ?? createdIdRef.current; if (!eid) { setExecuteResult(null); return; } setConsoleEntry({ timestamp: Date.now(), inputData: testInput, stdout: null, stderr: null, backendResult: null, error: null, source: 'execute', running: true }); setHasNewConsoleOutput(true); try { - const res = await dispatch(executeOutput({ output_id: eid, input_data: testInput })).unwrap(); + const res = await dispatch(EXECUTE_APP({ app_id: eid, input_data: testInput })).unwrap(); setExecuteResult(res); - setConsoleEntry({ timestamp: Date.now(), inputData: res.input_data, stdout: res.stdout ?? null, stderr: res.stderr ?? null, backendResult: res.backend_result, error: res.error, source: 'execute' }); + setConsoleEntry({ timestamp: Date.now(), inputData: testInput, stdout: res.stdout ?? null, stderr: res.stderr ?? null, backendResult: res.backend_result as Record | null, error: res.error, source: 'execute' }); } catch (e: any) { setConsoleEntry({ timestamp: Date.now(), inputData: testInput, stdout: null, stderr: null, backendResult: null, error: e?.message || 'Execution failed', source: 'execute' }); } }; - const handleAutoRun = async () => { - const config = autoRunInputRef.current?.getConfig(); - if (!config?.prompt?.trim()) return; - setAutoRunning(true); - let schema: Record; - try { schema = JSON.parse(schemaText); } catch { schema = { type: 'object', properties: {} }; } - const forcedToolNames = config.forcedTools.flatMap((ft) => ft.tools); - const eid = output?.id ?? createdIdRef.current; - if (forcedToolNames.length > 0 && eid) { - try { - const res = await dispatch(autoRunAgentOutput({ - prompt: config.prompt, input_schema: schema, output_id: eid, model: autoRunModel, - forced_tools: forcedToolNames, - context_paths: config.contextPaths.map((cp) => ({ path: cp.path, type: cp.type })), - })).unwrap(); - setAutoRunSessionId(res.session_id); - } catch { setAutoRunning(false); } - } else { - try { - const backendCode = files['backend.py'] ?? null; - const res = await dispatch(autoRunOutput({ - prompt: config.prompt, input_schema: schema, - backend_code: backendCode || undefined, - context_paths: config.contextPaths.map((cp) => ({ path: cp.path, type: cp.type })), - forced_tools: forcedToolNames.length > 0 ? forcedToolNames : undefined, - model: autoRunModel, - })).unwrap(); - if (res.input_data) { - setTestInput(res.input_data); - setExecuteResult({ - output_id: output?.id ?? createdIdRef.current ?? '', output_name: name, - frontend_code: files['index.html'] ?? '', input_data: res.input_data, - backend_result: res.backend_result, stdout: res.stdout ?? null, - stderr: res.stderr ?? null, error: res.error, - }); - setConsoleEntry({ timestamp: Date.now(), inputData: res.input_data, stdout: res.stdout ?? null, stderr: res.stderr ?? null, backendResult: res.backend_result, error: res.error, source: 'auto-run' }); - setHasNewConsoleOutput(true); - setActiveTab(TAB_PREVIEW); - } - } catch {} - setAutoRunning(false); - } - }; - - useEffect(() => { - if (!autoRunSessionId || !autoRunSessionStatus) return; - if (autoRunSessionStatus !== 'completed' && autoRunSessionStatus !== 'error' && autoRunSessionStatus !== 'stopped') return; - let extracted = false; - for (const msg of autoRunMessages) { - if (msg.role !== 'tool_call' || typeof msg.content !== 'object') continue; - const tc = msg.content as { tool?: string; input?: Record }; - if (tc.tool !== 'RenderOutput' || !tc.input?.input_data) continue; - setTestInput(tc.input.input_data); - setExecuteResult({ - output_id: output?.id ?? createdIdRef.current ?? '', output_name: name, - frontend_code: files['index.html'] ?? '', input_data: tc.input.input_data, - backend_result: null, stdout: null, stderr: null, error: null, - }); - setConsoleEntry({ timestamp: Date.now(), inputData: tc.input.input_data, stdout: null, stderr: null, backendResult: null, error: null, source: 'agent' }); - setHasNewConsoleOutput(true); - setActiveTab(TAB_PREVIEW); - extracted = true; - break; - } - if (!extracted && autoRunSessionStatus === 'error') { - const lastSys = [...autoRunMessages].reverse().find((m) => m.role === 'system'); - if (lastSys) { - const errMsg = typeof lastSys.content === 'string' ? lastSys.content : JSON.stringify(lastSys.content); - setExecuteResult({ - output_id: output?.id ?? createdIdRef.current ?? '', output_name: name, - frontend_code: files['index.html'] ?? '', input_data: {}, - backend_result: null, stdout: null, stderr: null, error: errMsg, - }); - setConsoleEntry({ timestamp: Date.now(), inputData: {}, stdout: null, stderr: null, backendResult: null, error: errMsg, source: 'agent' }); - setHasNewConsoleOutput(true); - } - } - setAutoRunning(false); - cleanupAutoRunAgent(autoRunSessionId).catch(() => {}); - setTimeout(() => setAutoRunSessionId(null), 300); - }, [autoRunSessionId, autoRunSessionStatus]); - - useEffect(() => { - autoRunLogEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, [autoRunMessages.length]); - - useEffect(() => { - return () => { - if (autoRunSessionId) cleanupAutoRunAgent(autoRunSessionId).catch(() => {}); - }; - }, [autoRunSessionId]); - return { schemaText, parsedSchema, testInput, setTestInput, executeResult, consoleEntry, hasNewConsoleOutput, setHasNewConsoleOutput, - autoRunEnabled, setAutoRunEnabled, - autoRunMode, setAutoRunMode, - autoRunModel, setAutoRunModel, - autoRunning, autoRunInputRef, - autoRunSessionId, autoRunMessages, autoRunSessionStatus, - autoRunLogEndRef, getAutoRunConfig, - handleRunPreview, handleAutoRun, + handleRunPreview, }; } diff --git a/frontend/src/app/pages/Views/hooks/useViewSave.ts b/frontend/src/app/pages/Views/hooks/useViewSave.ts index 2a649d59..4f5964bf 100644 --- a/frontend/src/app/pages/Views/hooks/useViewSave.ts +++ b/frontend/src/app/pages/Views/hooks/useViewSave.ts @@ -1,21 +1,17 @@ import React, { useState, useEffect, useRef } from 'react'; import { useAppDispatch } from '@/shared/hooks'; -import { createOutput, updateOutput, Output, AutoRunConfig } from '@/shared/state/outputsSlice'; +import { CREATE_APP, UPDATE_APP, App } from '@/shared/backend-bridge/apps/app_builder'; import { ViewPreviewHandle } from '../ViewPreview'; import { captureViewThumbnail } from '../captureViewThumbnail'; interface UseViewSaveParams { - output: Output | null; + output: App | null; name: string; description: string; files: Record; testInput: Record; onClose: () => void; previewRef: React.RefObject; - getAutoRunConfig: () => AutoRunConfig; - autoRunEnabled: boolean; - autoRunMode: string; - autoRunModel: string; createdIdRef: React.MutableRefObject; setCreatedId: (id: string) => void; } @@ -23,7 +19,6 @@ interface UseViewSaveParams { export function useViewSave(params: UseViewSaveParams) { const { output, name, description, files, testInput, onClose, previewRef, - getAutoRunConfig, autoRunEnabled, autoRunMode, autoRunModel, createdIdRef, setCreatedId, } = params; const dispatch = useAppDispatch(); @@ -39,23 +34,22 @@ export function useViewSave(params: UseViewSaveParams) { const buildBody = () => { let schema: Record; try { schema = JSON.parse(files['schema.json'] ?? '{}'); } catch { schema = { type: 'object', properties: {} }; } - const outputFiles = { ...files }; - delete outputFiles['meta.json']; - delete outputFiles['schema.json']; - delete outputFiles['SKILL.md']; + const appFiles = { ...files }; + delete appFiles['meta.json']; + delete appFiles['schema.json']; + delete appFiles['SKILL.md']; return { name: name || 'Untitled App', description, icon: 'view_quilt', input_schema: schema, - files: outputFiles, - auto_run_config: getAutoRunConfig(), + files: appFiles, }; }; - const captureThumbnailAsync = (outputId: string) => { + const captureThumbnailAsync = (appId: string) => { captureViewThumbnail(files['index.html'] ?? '', testInput, files) - .then((thumbnail) => { if (thumbnail) dispatch(updateOutput({ id: outputId, thumbnail })); }) + .then((thumbnail) => { if (thumbnail) dispatch(UPDATE_APP({ appId, thumbnail })); }) .catch(() => {}); }; @@ -71,10 +65,10 @@ export function useViewSave(params: UseViewSaveParams) { const eid = output?.id ?? createdIdRef.current; let savedId: string; if (eid) { - await dispatch(updateOutput({ id: eid, ...body })).unwrap(); + await dispatch(UPDATE_APP({ appId: eid, ...body })).unwrap(); savedId = eid; } else { - const created = await dispatch(createOutput(body)).unwrap(); + const created = await dispatch(CREATE_APP(body)).unwrap(); savedId = created.id; createdIdRef.current = savedId; setCreatedId(savedId); @@ -108,7 +102,7 @@ export function useViewSave(params: UseViewSaveParams) { if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current); autoSaveTimerRef.current = setTimeout(() => { performSaveRef.current?.(false); }, 1500); return () => { if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current); }; - }, [files, name, description, autoRunEnabled, autoRunMode, autoRunModel]); + }, [files, name, description]); useEffect(() => { return () => { diff --git a/frontend/src/app/pages/Views/hooks/useViewWorkspace.ts b/frontend/src/app/pages/Views/hooks/useViewWorkspace.ts index 79c21789..7a3bc19a 100644 --- a/frontend/src/app/pages/Views/hooks/useViewWorkspace.ts +++ b/frontend/src/app/pages/Views/hooks/useViewWorkspace.ts @@ -1,31 +1,29 @@ import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; import { createDraftSession, removeDraftSession } from '@/shared/state/agentsSlice'; -import { Output, SERVE_BASE } from '@/shared/state/outputsSlice'; -import { API_BASE } from '@/shared/config'; +import { SEED_APP, READ_APP, WRITE_APP_FILE, DELETE_APP_FILE, App, getAppServeUrl } from '@/shared/backend-bridge/apps/app_builder'; import { ViewPreviewHandle } from '../ViewPreview'; import { buildFileTree } from '../FileTree'; -const WORKSPACE_API = `${API_BASE}/outputs/workspace`; const POLL_INTERVAL_MS = 2000; export function useViewWorkspace( - output: Output | null, + app: App | null, previewRef: React.RefObject, ) { const dispatch = useAppDispatch(); const [files, setFiles] = useState>(() => { - if (!output) return {}; - const f = { ...output.files }; - if (!f['schema.json'] && output.input_schema) { - f['schema.json'] = JSON.stringify(output.input_schema, null, 2); + if (!app) return {}; + const f = { ...app.files }; + if (!f['schema.json'] && app.input_schema) { + f['schema.json'] = JSON.stringify(app.input_schema, null, 2); } return f; }); const [fileVersion, setFileVersion] = useState(0); - const [name, setName] = useState(output?.name ?? ''); - const [description, setDescription] = useState(output?.description ?? ''); + const [name, setName] = useState(app?.name ?? ''); + const [description, setDescription] = useState(app?.description ?? ''); const [activeFile, setActiveFile] = useState('index.html'); const [workspacePath, setWorkspacePath] = useState(null); @@ -45,25 +43,22 @@ export function useViewWorkspace( if (draftCreated.current) return; draftCreated.current = true; (async () => { - const seedBody: Record = { workspace_id: stableWorkspaceId }; - if (output) { - const seedFiles: Record = { ...output.files }; - if (output.input_schema && !seedFiles['schema.json']) { - seedFiles['schema.json'] = JSON.stringify(output.input_schema, null, 2); + const seedBody: { app_id: string; files?: Record; meta?: Record } = { + app_id: stableWorkspaceId, + }; + if (app) { + const seedFiles: Record = { ...app.files }; + if (app.input_schema && !seedFiles['schema.json']) { + seedFiles['schema.json'] = JSON.stringify(app.input_schema, null, 2); } seedBody.files = seedFiles; - seedBody.meta = { name: output.name, description: output.description }; + seedBody.meta = { name: app.name, description: app.description }; } try { - const res = await fetch(`${WORKSPACE_API}/seed`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(seedBody), - }); - const data = await res.json(); - setWorkspacePath(data.path); + const result = await dispatch(SEED_APP(seedBody)).unwrap(); + setWorkspacePath(result.path); const action = dispatch(createDraftSession({ - mode: 'view-builder', setActive: false, targetDirectory: data.path, + mode: 'view-builder', setActive: false, targetDirectory: result.path, })); setInitialDraftId(action.payload.draftId); } catch { @@ -71,7 +66,7 @@ export function useViewWorkspace( setInitialDraftId(action.payload.draftId); } })(); - }, [dispatch, output, stableWorkspaceId]); + }, [dispatch, app, stableWorkspaceId]); const effectiveSessionId = useAppSelector((state) => { if (!initialDraftId) return null; @@ -95,22 +90,21 @@ export function useViewWorkspace( const pollWorkspace = useCallback(async () => { if (!workspaceId) return; try { - const res = await fetch(`${WORKSPACE_API}/${workspaceId}`); - if (!res.ok) return; - const data = await res.json(); + const data = await dispatch(READ_APP(workspaceId)).unwrap(); const fingerprint = JSON.stringify(data); if (fingerprint === lastPollRef.current) return; lastPollRef.current = fingerprint; if (data.files) { setFiles(data.files); setFileVersion(v => v + 1); } if (data.meta) { - if (data.meta.name && !nameSetByMeta.current) { + const meta = data.meta as Record; + if (meta.name && !nameSetByMeta.current) { nameSetByMeta.current = true; - setName((prev) => prev || data.meta.name); + setName((prev) => prev || meta.name); } - if (data.meta.description) setDescription((prev) => prev || data.meta.description); + if (meta.description) setDescription((prev) => prev || meta.description); } } catch {} - }, [workspaceId]); + }, [workspaceId, dispatch]); useEffect(() => { if (!workspaceId) return; @@ -130,7 +124,7 @@ export function useViewWorkspace( }, [initialDraftId, dispatch]); const workspaceServeUrl = workspaceId - ? `${SERVE_BASE}/workspace/${workspaceId}/serve/index.html` + ? getAppServeUrl(workspaceId) : undefined; const filePaths = useMemo( @@ -147,14 +141,12 @@ export function useViewWorkspace( if (existing) clearTimeout(existing); wsPushTimers.current.set(path, setTimeout(() => { wsPushTimers.current.delete(path); - fetch(`${WORKSPACE_API}/${wsId}/file/${encodeURIComponent(path)}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ content }), - }).then(() => previewRef.current?.reload()).catch(() => {}); + dispatch(WRITE_APP_FILE({ appId: wsId, filepath: path, content })) + .then(() => previewRef.current?.reload()) + .catch(() => {}); }, 300)); } - }, [previewRef]); + }, [previewRef, dispatch]); const addFile = useCallback((fileName: string) => { const trimmed = fileName.trim(); @@ -162,12 +154,9 @@ export function useViewWorkspace( setFiles(prev => ({ ...prev, [trimmed]: '' })); setActiveFile(trimmed); if (workspaceId) { - fetch(`${WORKSPACE_API}/${workspaceId}/file/${encodeURIComponent(trimmed)}`, { - method: 'PUT', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ content: '' }), - }).catch(() => {}); + dispatch(WRITE_APP_FILE({ appId: workspaceId, filepath: trimmed, content: '' })); } - }, [files, workspaceId]); + }, [files, workspaceId, dispatch]); const deleteFile = useCallback((filePath: string) => { setFiles(prev => { const next = { ...prev }; delete next[filePath]; return next; }); @@ -176,11 +165,9 @@ export function useViewWorkspace( setActiveFile(remaining[0] ?? 'index.html'); } if (workspaceId) { - fetch(`${WORKSPACE_API}/${workspaceId}/file/${encodeURIComponent(filePath)}`, { - method: 'DELETE', - }).catch(() => {}); + dispatch(DELETE_APP_FILE({ appId: workspaceId, filepath: filePath })); } - }, [activeFile, filePaths, workspaceId]); + }, [activeFile, filePaths, workspaceId, dispatch]); useEffect(() => { return () => { wsPushTimers.current.forEach(t => clearTimeout(t)); }; diff --git a/frontend/src/shared/backend-bridge/apps/app_builder.ts b/frontend/src/shared/backend-bridge/apps/app_builder.ts index a936dc94..a72cf0b8 100644 --- a/frontend/src/shared/backend-bridge/apps/app_builder.ts +++ b/frontend/src/shared/backend-bridge/apps/app_builder.ts @@ -3,6 +3,33 @@ import { API_BASE } from '@/shared/backend-bridge/base_routes'; const APP_BUILDER_API: string = `${API_BASE}/app_builder`; +export function getAppServeUrl(appId: string): string { + return `${APP_BUILDER_API}/${appId}/serve/index.html`; +} + +export interface App { + id: string; + name: string; + description: string; + icon: string; + input_schema: Record; + files: Record; + permission: string; + thumbnail?: string | null; + created_at: string; + updated_at: string; +} + +export interface AppExecuteResult { + app_id: string; + app_name: string; + frontend_code: string; + backend_result: Record | null; + stdout: string | null; + stderr: string | null; + error: string | null; +} + // --------------------------------------------------------------------------- // File serving // --------------------------------------------------------------------------- @@ -126,13 +153,13 @@ export const DELETE_APP_FILE = createAsyncThunk( const list_apps_endpoint: string = `${APP_BUILDER_API}/list`; -async function list_apps_function(): Promise<{ apps: Record[] }> { +async function list_apps_function(): Promise<{ apps: App[] }> { const res = await fetch(list_apps_endpoint, { method: 'GET', headers: { 'Content-Type': 'application/json' }, }); const data = await res.json(); - return data as { apps: Record[] }; + return data as { apps: App[] }; } export const LIST_APPS = createAsyncThunk( list_apps_endpoint, @@ -141,13 +168,13 @@ export const LIST_APPS = createAsyncThunk( const get_app_endpoint: string = `${APP_BUILDER_API}/get`; -async function get_app_function(appId: string): Promise> { +async function get_app_function(appId: string): Promise { const res = await fetch(`${APP_BUILDER_API}/${appId}`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, }); const data = await res.json(); - return data as Record; + return data as App; } export const GET_APP = createAsyncThunk( get_app_endpoint, @@ -160,16 +187,17 @@ async function create_app_function(body: { name: string; description?: string; icon?: string; + input_schema?: Record; files?: Record | null; thumbnail?: string | null; -}): Promise<{ ok: boolean; app: Record }> { +}): Promise { const res = await fetch(create_app_endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const data = await res.json(); - return data as { ok: boolean; app: Record }; + return data.app as App; } export const CREATE_APP = createAsyncThunk( create_app_endpoint, @@ -183,8 +211,11 @@ async function update_app_function(payload: { name?: string; description?: string; icon?: string; + input_schema?: Record; + files?: Record; thumbnail?: string | null; -}): Promise<{ ok: boolean; app: Record }> { + permission?: string; +}): Promise { const { appId, ...updates } = payload; const res = await fetch(`${APP_BUILDER_API}/${appId}`, { method: 'PUT', @@ -192,7 +223,7 @@ async function update_app_function(payload: { body: JSON.stringify(updates), }); const data = await res.json(); - return data as { ok: boolean; app: Record }; + return data.app as App; } export const UPDATE_APP = createAsyncThunk( update_app_endpoint, @@ -201,13 +232,12 @@ export const UPDATE_APP = createAsyncThunk( const delete_app_endpoint: string = `${APP_BUILDER_API}/delete`; -async function delete_app_function(appId: string): Promise<{ ok: boolean }> { - const res = await fetch(`${APP_BUILDER_API}/${appId}`, { +async function delete_app_function(appId: string): Promise { + await fetch(`${APP_BUILDER_API}/${appId}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, }); - const data = await res.json(); - return data as { ok: boolean }; + return appId; } export const DELETE_APP = createAsyncThunk( delete_app_endpoint, @@ -221,30 +251,17 @@ export const DELETE_APP = createAsyncThunk( const execute_app_endpoint: string = `${APP_BUILDER_API}/execute`; -async function execute_app_function(appId: string): Promise<{ +async function execute_app_function(payload: { app_id: string; - app_name: string; - frontend_code: string; - backend_result: Record | null; - stdout: string | null; - stderr: string | null; - error: string | null; -}> { + input_data: Record; +}): Promise { const res = await fetch(execute_app_endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ app_id: appId }), + body: JSON.stringify(payload), }); const data = await res.json(); - return data as { - app_id: string; - app_name: string; - frontend_code: string; - backend_result: Record | null; - stdout: string | null; - stderr: string | null; - error: string | null; - }; + return data as AppExecuteResult; } export const EXECUTE_APP = createAsyncThunk( execute_app_endpoint, diff --git a/frontend/src/shared/state/outputsSlice.ts b/frontend/src/shared/state/outputsSlice.ts deleted file mode 100644 index bf916d72..00000000 --- a/frontend/src/shared/state/outputsSlice.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; -import { API_BASE } from '@/shared/config'; - -const OUTPUTS_API = `${API_BASE}/outputs`; - -export const SERVE_BASE = `${API_BASE}/outputs`; - - -export interface AutoRunConfig { - enabled: boolean; - prompt: string; - context_paths: Array<{ path: string; type: string }>; - forced_tools: Array<{ label: string; tools: string[]; iconKey?: string }>; - mode: string; - model: string; -} - -export interface Output { - id: string; - name: string; - description: string; - icon: string; - input_schema: Record; - files: Record; - permission: string; - auto_run_config?: AutoRunConfig | null; - thumbnail?: string | null; - created_at: string; - updated_at: string; -} - -export function getFrontendCode(output: Output): string { - return output.files?.['index.html'] ?? ''; -} - -export function getBackendCode(output: Output): string | null { - return output.files?.['backend.py'] ?? null; -} - -export function buildServeUrl( - outputId: string, - inputData: Record = {}, - backendResult: Record | null = null, -): string { - const dataPayload = JSON.stringify({ i: inputData, r: backendResult }); - const encoded = btoa(unescape(encodeURIComponent(dataPayload))); - return `${SERVE_BASE}/${outputId}/serve/index.html?_d=${encodeURIComponent(encoded)}`; -} - -export interface OutputExecuteResult { - output_id: string; - output_name: string; - frontend_code: string; - input_data: Record; - backend_result: Record | null; - stdout: string | null; - stderr: string | null; - error: string | null; -} - -interface OutputsState { - items: Record; - loading: boolean; - loaded: boolean; -} - -const initialState: OutputsState = { items: {}, loading: false, loaded: false }; - -export const fetchOutputs = createAsyncThunk( - 'outputs/fetch', - async () => { - const res = await fetch(`${OUTPUTS_API}/list`); - const data = await res.json(); - return data.outputs as Output[]; - }, - { condition: (_, { getState }) => !(getState() as { outputs: OutputsState }).outputs.loading }, -); - -export const createOutput = createAsyncThunk( - 'outputs/create', - async (body: Omit) => { - const res = await fetch(`${OUTPUTS_API}/create`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - if (!res.ok) throw new Error(`Create failed: ${res.status}`); - const data = await res.json(); - return data.output as Output; - } -); - -export const updateOutput = createAsyncThunk( - 'outputs/update', - async ({ id, ...updates }: Partial & { id: string }) => { - const res = await fetch(`${OUTPUTS_API}/${id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(updates), - }); - if (!res.ok) throw new Error(`Update failed: ${res.status}`); - const data = await res.json(); - return data.output as Output; - } -); - -export const deleteOutput = createAsyncThunk('outputs/delete', async (id: string) => { - await fetch(`${OUTPUTS_API}/${id}`, { method: 'DELETE' }); - return id; -}); - -export const executeOutput = createAsyncThunk( - 'outputs/execute', - async (body: { output_id: string; input_data: Record }) => { - const res = await fetch(`${OUTPUTS_API}/execute`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - return (await res.json()) as OutputExecuteResult; - } -); - -interface AutoRunResult { - input_data: Record | null; - backend_result: Record | null; - stdout: string | null; - stderr: string | null; - error: string | null; -} - -export const autoRunOutput = createAsyncThunk( - 'outputs/autoRun', - async (body: { prompt: string; input_schema: Record; backend_code?: string | null; context_paths?: Array<{ path: string; type: string }>; forced_tools?: string[]; model?: string }) => { - const res = await fetch(`${OUTPUTS_API}/auto-run`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - return (await res.json()) as AutoRunResult; - } -); - -interface AutoRunAgentResult { - session_id: string; -} - -export const autoRunAgentOutput = createAsyncThunk( - 'outputs/autoRunAgent', - async (body: { - prompt: string; - input_schema: Record; - output_id: string; - model?: string; - forced_tools?: string[]; - context_paths?: Array<{ path: string; type: string }>; - }) => { - const res = await fetch(`${OUTPUTS_API}/auto-run-agent`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }); - if (!res.ok) throw new Error(`Auto-run agent launch failed: ${res.status}`); - return (await res.json()) as AutoRunAgentResult; - } -); - -export async function cleanupAutoRunAgent(sessionId: string): Promise { - await fetch(`${OUTPUTS_API}/auto-run-agent/${sessionId}`, { method: 'DELETE' }); -} - -const outputsSlice = createSlice({ - name: 'outputs', - initialState, - reducers: {}, - extraReducers: (builder) => { - builder - .addCase(fetchOutputs.pending, (state) => { state.loading = true; }) - .addCase(fetchOutputs.fulfilled, (state, action) => { - state.loading = false; - state.loaded = true; - state.items = {}; - for (const o of action.payload) state.items[o.id] = o; - }) - .addCase(fetchOutputs.rejected, (state) => { state.loading = false; state.loaded = true; }) - .addCase(createOutput.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; }) - .addCase(updateOutput.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; }) - .addCase(deleteOutput.fulfilled, (state, action) => { delete state.items[action.payload]; }); - }, -}); - -export default outputsSlice.reducer; diff --git a/frontend/src/shared/state/store.ts b/frontend/src/shared/state/store.ts index 33d720f1..6e78f1cd 100644 --- a/frontend/src/shared/state/store.ts +++ b/frontend/src/shared/state/store.ts @@ -7,7 +7,6 @@ import modesReducer from './modesSlice'; import settingsReducer from './settingsSlice'; import mcpRegistryReducer from './mcpRegistrySlice'; import skillRegistryReducer from './skillRegistrySlice'; -import outputsReducer from './outputsSlice'; import dashboardLayoutReducer from './dashboardLayoutSlice'; import dashboardsReducer from './dashboardsSlice'; import updateReducer from './updateSlice'; @@ -23,7 +22,6 @@ export const store = configureStore({ settings: settingsReducer, mcpRegistry: mcpRegistryReducer, skillRegistry: skillRegistryReducer, - outputs: outputsReducer, dashboardLayout: dashboardLayoutReducer, dashboards: dashboardsReducer, update: updateReducer,