[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

This commit is contained in:
haikdc
2026-04-18 05:10:05 -07:00
parent a0312deaf5
commit 825b59b875
24 changed files with 198 additions and 635 deletions
@@ -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 (
@@ -47,7 +47,7 @@ const Sidebar: React.FC<SidebarProps> = ({ 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(),
);
@@ -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(() => {
@@ -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<Props> = ({ 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<Props> = ({ 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) {
@@ -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(() => {
@@ -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(() => [
@@ -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);
@@ -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<string, CardPosition>;
viewCards: Record<string, ViewCardPosition>;
browserCards: Record<string, BrowserCardPosition>;
outputs: Record<string, Output>;
outputs: Record<string, App>;
dashboardId: string | undefined;
canvasActions: CanvasActions;
onHighlightCard?: (cardId: string) => void;
@@ -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<ViewCardProps> = ({
const [inputData, setInputData] = useState<Record<string, any>>(() => getDefault(output.input_schema));
const [backendResult, setBackendResult] = useState<Record<string, any> | 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<ViewCardProps> = ({
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<string, any> | null);
} catch {}
};
const mdDx = (!isDragging && isSelected && multiDragDelta) ? multiDragDelta.dx : 0;
@@ -169,14 +129,14 @@ const DashboardViewCard: React.FC<ViewCardProps> = ({
<ViewCardHeader
name={output.name}
hasAutoRun={hasAutoRun}
autoRunning={autoRunning}
hasAutoRun={false}
autoRunning={false}
isDragging={isDragging}
onDragPointerDown={handleDragPointerDown}
onDragPointerMove={handleDragPointerMove}
onDragPointerUp={handleDragPointerUp}
onRefresh={handleRefresh}
onAutoRun={handleAutoRun}
onAutoRun={handleExecute}
onRemove={handleRemove}
/>
@@ -186,7 +146,7 @@ const DashboardViewCard: React.FC<ViewCardProps> = ({
)}
<ViewPreview
ref={previewRef}
serveUrl={`${SERVE_BASE}/${output.id}/serve/index.html`}
serveUrl={getAppServeUrl(output.id)}
frontendCode={output.files?.['index.html'] ?? ''}
inputData={inputData}
backendResult={backendResult}
@@ -5,15 +5,15 @@ import InputBase from '@mui/material/InputBase';
import Icon from '@mui/material/Icon';
import SearchIcon from '@mui/icons-material/Search';
import type { ClaudeTokens } from '@/shared/styles/claudeTokens';
import type { Output } from '@/shared/state/outputsSlice';
import type { App } from '@/shared/backend-bridge/apps/app_builder';
interface ViewPickerPanelProps {
searchInputRef: React.RefObject<HTMLInputElement>;
viewSearch: string;
onSearchChange: (q: string) => void;
filteredOutputs: Output[];
outputList: Output[];
onSelect: (output: Output) => void;
filteredOutputs: App[];
outputList: App[];
onSelect: (output: App) => void;
c: ClaudeTokens;
}
@@ -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(); };
@@ -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('');
@@ -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<string, any> }[] = [
];
export interface ViewCardProps {
output: Output;
output: App;
cardX: number;
cardY: number;
cardWidth: number;
@@ -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<string, string> = {}; 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<string, string> = {}; 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 };
+2 -3
View File
@@ -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;
+6 -57
View File
@@ -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<Props> = ({ output, onClose }) => {
@@ -42,8 +40,7 @@ const ViewEditor: React.FC<Props> = ({ 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<Props> = ({ 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 } }} />
<TextField value={ws.description} onChange={(e) => 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 && (
<Button variant="outlined" startIcon={ar.autoRunning ? <CircularProgress size={14} /> : <BoltIcon sx={{ fontSize: 16 }} />} onClick={ar.handleAutoRun} disabled={ar.autoRunning} size="small"
sx={{ borderColor: '#f59e0b40', color: '#f59e0b', textTransform: 'none', fontWeight: 500, fontSize: '0.8rem', px: 1.5, '&:hover': { borderColor: '#f59e0b', bgcolor: '#f59e0b10' } }}>
{ar.autoRunning ? 'Running…' : 'Auto Run'}
</Button>
)}
{save.saveStatus === 'unsaved' && <Typography sx={{ fontSize: '0.72rem', color: c.text.ghost, fontStyle: 'italic', whiteSpace: 'nowrap' }}>Unsaved changes</Typography>}
{save.saveStatus === 'saving' && <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}><CircularProgress size={12} sx={{ color: c.text.ghost }} /><Typography sx={{ fontSize: '0.72rem', color: c.text.ghost, whiteSpace: 'nowrap' }}>Saving</Typography></Box>}
{save.saveStatus === 'saved' && <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}><CheckCircleOutlineIcon sx={{ fontSize: 14, color: c.accent.primary }} /><Typography sx={{ fontSize: '0.72rem', color: c.accent.primary, whiteSpace: 'nowrap' }}>Saved</Typography></Box>}
@@ -107,7 +98,6 @@ const ViewEditor: React.FC<Props> = ({ output, onClose }) => {
<Tab label="Preview" value={TAB_PREVIEW} />
<Tab label="Code" value={TAB_CODE} />
<Tab label="Test Input" value={TAB_TEST_INPUT} />
<Tab label="Auto Run" value={TAB_AUTO_RUN} />
{showConsole && <Tab label="Console" value={TAB_CONSOLE} />}
</Tabs>
{activeTab === TAB_PREVIEW && (<>
@@ -176,47 +166,6 @@ const ViewEditor: React.FC<Props> = ({ output, onClose }) => {
</Box>
)}
{activeTab === TAB_CONSOLE && <ConsolePanel entry={ar.consoleEntry} c={c} />}
<Box sx={{ display: activeTab === TAB_AUTO_RUN ? 'flex' : 'none', flexDirection: 'column', height: '100%' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, px: 2, py: 1, borderBottom: `1px solid ${c.border.subtle}`, bgcolor: c.bg.secondary, flexShrink: 0 }}>
<Switch checked={ar.autoRunEnabled} onChange={(_, v) => ar.setAutoRunEnabled(v)} size="small"
sx={{ '& .MuiSwitch-switchBase.Mui-checked': { color: '#f59e0b' }, '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { bgcolor: '#f59e0b' } }} />
<Typography sx={{ fontSize: '0.82rem', fontWeight: 500, color: ar.autoRunEnabled ? c.text.primary : c.text.muted }}>
{ar.autoRunEnabled ? 'Auto Run enabled' : 'Auto Run disabled'}
</Typography>
<Box sx={{ flex: 1 }} />
{ar.autoRunEnabled && (
<Button size="small" startIcon={ar.autoRunning ? <CircularProgress size={12} /> : <BoltIcon sx={{ fontSize: 14 }} />} onClick={ar.handleAutoRun} disabled={ar.autoRunning}
sx={{ textTransform: 'none', fontSize: '0.78rem', color: '#f59e0b', fontWeight: 500, '&:hover': { bgcolor: '#f59e0b10' } }}>
{ar.autoRunning ? 'Running…' : 'Run Now'}
</Button>
)}
</Box>
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
{ar.autoRunEnabled ? (
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', p: 2, gap: 1.5, overflow: 'hidden' }}>
<Typography sx={{ color: c.text.ghost, fontSize: '0.8rem', lineHeight: 1.6, flexShrink: 0 }}>
Describe what data to generate for this app. When triggered, an LLM will produce input data matching your schema and populate the preview.
</Typography>
<ChatInput ref={ar.autoRunInputRef} autoRunMode onSend={() => {}} mode={ar.autoRunMode} onModeChange={ar.setAutoRunMode} model={ar.autoRunModel} onModelChange={ar.setAutoRunModel} />
<Button variant="contained" startIcon={save.saving ? <CircularProgress size={14} color="inherit" /> : <SaveIcon sx={{ fontSize: 16 }} />} onClick={() => save.handleSave(false)} disabled={save.saving || !ws.name.trim()} size="small"
sx={{ alignSelf: 'flex-start', flexShrink: 0, bgcolor: c.accent.primary, textTransform: 'none', fontWeight: 500, fontSize: '0.8rem', px: 2, '&:hover': { bgcolor: c.accent.hover } }}>
{save.saving ? 'Saving…' : 'Save'}
</Button>
{(ar.autoRunSessionId || ar.autoRunMessages.length > 0) && (
<AutoRunLog messages={ar.autoRunMessages} status={ar.autoRunSessionStatus} logEndRef={ar.autoRunLogEndRef} c={c} />
)}
</Box>
) : (
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 1.5 }}>
<BoltIcon sx={{ fontSize: 40, color: c.text.ghost, opacity: 0.3 }} />
<Typography sx={{ color: c.text.ghost, fontSize: '0.88rem' }}>Enable Auto Run to generate live data for this app</Typography>
<Typography sx={{ color: c.text.ghost, fontSize: '0.78rem', maxWidth: 360, textAlign: 'center', lineHeight: 1.5 }}>
Configure a prompt that describes what data to generate. An LLM will produce input matching your schema and populate the preview automatically.
</Typography>
</Box>
)}
</Box>
</Box>
</Box>
</Box>
</Box>
+11 -11
View File
@@ -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<Props> = ({ output, onClose }) => {
const defaultInput = useMemo(() => getDefault(output.input_schema), [output.input_schema]);
const [inputData, setInputData] = useState<Record<string, any>>(defaultInput);
const [result, setResult] = useState<OutputExecuteResult | null>(null);
const [result, setResult] = useState<AppExecuteResult | null>(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<Props> = ({ output, onClose }) => {
)}
{result ? (
<ViewPreview
serveUrl={`${SERVE_BASE}/${output.id}/serve/index.html`}
frontendCode={result.frontend_code}
inputData={result.input_data}
backendResult={result.backend_result}
serveUrl={getAppServeUrl(output.id)}
frontendCode={result.frontend_code}
inputData={inputData}
backendResult={result.backend_result as Record<string, any> | null}
/>
) : (
<ViewPreview
serveUrl={`${SERVE_BASE}/${output.id}/serve/index.html`}
frontendCode={getFrontendCode(output)}
serveUrl={getAppServeUrl(output.id)}
frontendCode={output.files?.['index.html'] ?? ''}
inputData={inputData}
/>
)}
@@ -133,7 +133,7 @@ const ViewRunDialog: React.FC<Props> = ({ 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'}
</Button>
</DialogActions>
</Dialog>
+27 -27
View File
@@ -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<Output | null>(null);
const [runOutput, setRunOutput] = useState<Output | null>(null);
const [editingApp, setEditingApp] = useState<App | null>(null);
const [runApp, setRunApp] = useState<App | null>(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 <ViewEditor key={editingOutput?.id ?? 'new'} output={editingOutput} onClose={handleEditorClose} />;
return <ViewEditor key={editingApp?.id ?? 'new'} output={editingApp} onClose={handleEditorClose} />;
}
return (
@@ -118,7 +118,7 @@ const Views: React.FC = () => {
<Typography sx={{ color: c.text.muted, textAlign: 'center', py: 8 }}>
Loading...
</Typography>
) : outputs.length === 0 ? (
) : apps.length === 0 ? (
<Box
sx={{
textAlign: 'center',
@@ -141,23 +141,23 @@ const Views: React.FC = () => {
gap: 2.5,
}}
>
{outputs.map((output) => (
{apps.map((app) => (
<ViewCard
key={output.id}
output={output}
onClick={() => handleEditView(output)}
onDelete={() => handleDeleteView(output.id)}
onRun={() => setRunOutput(output)}
key={app.id}
output={app}
onClick={() => handleEditView(app)}
onDelete={() => handleDeleteView(app.id)}
onRun={() => setRunApp(app)}
/>
))}
</Box>
)}
</Box>
{runOutput && (
{runApp && (
<ViewRunDialog
output={runOutput}
onClose={() => setRunOutput(null)}
output={runApp}
onClose={() => setRunApp(null)}
/>
)}
</Box>
@@ -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<string | null>,
files: Record<string, string>,
name: string,
@@ -27,171 +20,29 @@ export function useAutoRun(
const testInputDefault = useMemo(() => getDefault(parsedSchema), [parsedSchema]);
const [testInput, setTestInput] = useState<Record<string, any>>(testInputDefault);
useEffect(() => { setTestInput(getDefault(parsedSchema)); }, [schemaText]);
const [executeResult, setExecuteResult] = useState<OutputExecuteResult | null>(null);
const [executeResult, setExecuteResult] = useState<AppExecuteResult | null>(null);
const [consoleEntry, setConsoleEntry] = useState<ConsoleEntry | null>(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<ChatInputHandle>(null);
const autoRunInitialized = useRef(false);
const [autoRunSessionId, setAutoRunSessionId] = useState<string | null>(null);
const autoRunLogEndRef = useRef<HTMLDivElement>(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<string, any> | 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<string, any>;
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<string, any> };
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,
};
}
@@ -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<string, string>;
testInput: Record<string, any>;
onClose: () => void;
previewRef: React.RefObject<ViewPreviewHandle | null>;
getAutoRunConfig: () => AutoRunConfig;
autoRunEnabled: boolean;
autoRunMode: string;
autoRunModel: string;
createdIdRef: React.MutableRefObject<string | null>;
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<string, any>;
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 () => {
@@ -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<ViewPreviewHandle | null>,
) {
const dispatch = useAppDispatch();
const [files, setFiles] = useState<Record<string, string>>(() => {
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<string | null>(null);
@@ -45,25 +43,22 @@ export function useViewWorkspace(
if (draftCreated.current) return;
draftCreated.current = true;
(async () => {
const seedBody: Record<string, any> = { workspace_id: stableWorkspaceId };
if (output) {
const seedFiles: Record<string, string> = { ...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<string, string>; meta?: Record<string, unknown> } = {
app_id: stableWorkspaceId,
};
if (app) {
const seedFiles: Record<string, string> = { ...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<string, any>;
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)); };
@@ -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<string, any>;
files: Record<string, string>;
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<string, unknown> | 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<string, unknown>[] }> {
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<string, unknown>[] };
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<Record<string, unknown>> {
async function get_app_function(appId: string): Promise<App> {
const res = await fetch(`${APP_BUILDER_API}/${appId}`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
});
const data = await res.json();
return data as Record<string, unknown>;
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<string, any>;
files?: Record<string, string> | null;
thumbnail?: string | null;
}): Promise<{ ok: boolean; app: Record<string, unknown> }> {
}): Promise<App> {
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<string, unknown> };
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<string, any>;
files?: Record<string, string>;
thumbnail?: string | null;
}): Promise<{ ok: boolean; app: Record<string, unknown> }> {
permission?: string;
}): Promise<App> {
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<string, unknown> };
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<string> {
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<string, unknown> | null;
stdout: string | null;
stderr: string | null;
error: string | null;
}> {
input_data: Record<string, any>;
}): Promise<AppExecuteResult> {
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<string, unknown> | null;
stdout: string | null;
stderr: string | null;
error: string | null;
};
return data as AppExecuteResult;
}
export const EXECUTE_APP = createAsyncThunk(
execute_app_endpoint,
-192
View File
@@ -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<string, any>;
files: Record<string, string>;
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<string, any> = {},
backendResult: Record<string, any> | 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<string, any>;
backend_result: Record<string, any> | null;
stdout: string | null;
stderr: string | null;
error: string | null;
}
interface OutputsState {
items: Record<string, Output>;
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<Output, 'id' | 'created_at' | 'updated_at' | 'permission'>) => {
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<Output> & { 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<string, any> }) => {
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<string, any> | null;
backend_result: Record<string, any> | null;
stdout: string | null;
stderr: string | null;
error: string | null;
}
export const autoRunOutput = createAsyncThunk(
'outputs/autoRun',
async (body: { prompt: string; input_schema: Record<string, any>; 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<string, any>;
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<void> {
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;
-2
View File
@@ -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,