mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-23 21:12:22 +02:00
Merge branch 'pr-126-fresh' into HEAD
This commit is contained in:
@@ -1,8 +1,5 @@
|
||||
"""WorkflowExportable: shares a scheduled-task/workflow recipe (steps, schedule
|
||||
shape, actions, model). The workflow store lives on the eric/workflow branch and
|
||||
is NOT on eric/dev yet, so every store touch is lazy: on a build without it,
|
||||
export finds nothing and import fails with a clear message, and the module still
|
||||
imports cleanly. It lights up the moment the workflow forward-port lands.
|
||||
shape, actions, model).
|
||||
|
||||
Safety: an imported workflow must never silently start running on someone else's
|
||||
machine, so the schedule is forced off on import (the importer re-arms it). The
|
||||
@@ -12,6 +9,8 @@ from __future__ import annotations
|
||||
|
||||
from backend.apps.swarm.exportable import DepRef, ExportContext, RemapTable
|
||||
from backend.apps.swarm.models import EntityType, Requirement, RequirementKind
|
||||
from backend.apps.workflows import storage
|
||||
from backend.apps.workflows.models import Workflow
|
||||
|
||||
P_BUILTIN_MODES = {"agent", "ask", "plan", "view-builder", "skill-builder"}
|
||||
|
||||
@@ -52,10 +51,7 @@ class WorkflowExportable:
|
||||
|
||||
@classmethod
|
||||
def load(cls, local_id: str) -> "WorkflowExportable | None":
|
||||
store = p_store()
|
||||
if store is None:
|
||||
return None
|
||||
wf = store.get_workflow(local_id)
|
||||
wf = storage.get_workflow(local_id)
|
||||
if wf is None:
|
||||
return None
|
||||
data = wf.model_dump(mode="json")
|
||||
@@ -92,38 +88,15 @@ class WorkflowExportable:
|
||||
|
||||
@classmethod
|
||||
def import_(cls, payload: dict, files: dict[str, bytes], remap: RemapTable) -> str:
|
||||
store = p_store()
|
||||
model = p_model()
|
||||
if store is None or model is None:
|
||||
from backend.apps.swarm.ziputil import BundleError
|
||||
raise BundleError("this build doesn't support workflows yet; please update OpenSwarm")
|
||||
clean = sanitize_workflow(payload)
|
||||
clean.pop("id", None) # fresh id via the model's default_factory
|
||||
wf = model(**clean)
|
||||
store.save_workflow(wf)
|
||||
wf = Workflow(**clean)
|
||||
storage.save_workflow(wf)
|
||||
return wf.id
|
||||
|
||||
@classmethod
|
||||
def rollback(cls, local_id: str) -> None:
|
||||
store = p_store()
|
||||
if store is not None:
|
||||
try:
|
||||
store.delete_workflow(local_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def p_store():
|
||||
try:
|
||||
from backend.apps.workflows import storage
|
||||
return storage
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def p_model():
|
||||
try:
|
||||
from backend.apps.workflows.models import Workflow
|
||||
return Workflow
|
||||
except Exception:
|
||||
return None
|
||||
try:
|
||||
storage.delete_workflow(local_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -9,6 +9,8 @@ import FileDownloadIcon from '@mui/icons-material/FileDownload';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { fetchWorkflows } from '@/shared/state/workflowsSlice';
|
||||
|
||||
import ImportDigest, { DigestHandle } from './ImportDigest';
|
||||
import ImportModal from './ImportModal';
|
||||
@@ -43,6 +45,8 @@ const delay = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
||||
const ImportEntryPoint: React.FC = () => {
|
||||
const c = useClaudeTokens();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
const dashboardId = useAppSelector((s) => s.tempState.lastDashboardId) || undefined;
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const digestRef = useRef<DigestHandle | null>(null);
|
||||
const depth = useRef(0);
|
||||
@@ -56,10 +60,12 @@ const ImportEntryPoint: React.FC = () => {
|
||||
(rootType: string, rootId: string, name: string) => {
|
||||
const msg = rootType === 'app' ? `Added ${name} to your Apps` : `Added ${name}`;
|
||||
setToast({ msg, sev: 'success' });
|
||||
// A workflow has no route of its own, so nothing would pull it in: an open Workflows hub only fetches on mount and would keep showing a stale list. Import drops dashboard_id, and /list keeps unassigned workflows for every dashboard, so this surfaces it wherever the user is.
|
||||
if (rootType === 'workflow') dispatch(fetchWorkflows(dashboardId));
|
||||
const to = DEST[rootType]?.(rootId);
|
||||
if (to) navigate(to);
|
||||
},
|
||||
[navigate],
|
||||
[navigate, dispatch, dashboardId],
|
||||
);
|
||||
|
||||
const commitAndFinish = useCallback(
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { CSSProperties } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { deleteWorkflow } from '@/shared/state/workflowsSlice';
|
||||
import { isScheduleActive, describeSchedule } from '@/app/pages/Workflows/scheduleUtils';
|
||||
import ShareButton from '@/app/components/share/ShareButton';
|
||||
import { colorForWorkflow, useWC } from './uiKit';
|
||||
import WorkflowTitle from './WorkflowTitle';
|
||||
import type { AppNav } from './types';
|
||||
@@ -18,6 +19,7 @@ const LeftRail: React.FC<{ nav: AppNav }> = ({ nav }) => {
|
||||
const items = useAppSelector((s) => s.workflows.items);
|
||||
const trashCount = useAppSelector((s) => s.workflows.deleted.length);
|
||||
const [query, setQuery] = useState('');
|
||||
const [hovered, setHovered] = useState<string | null>(null);
|
||||
|
||||
const workflows = useMemo(() => Object.values(items)
|
||||
.filter((w) => !w.unsaved)
|
||||
@@ -93,6 +95,8 @@ const LeftRail: React.FC<{ nav: AppNav }> = ({ nav }) => {
|
||||
<div
|
||||
key={w.id}
|
||||
onClick={() => nav.selectWorkflow(w.id)}
|
||||
onMouseEnter={() => setHovered(w.id)}
|
||||
onMouseLeave={() => setHovered((h) => (h === w.id ? null : h))}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 9, padding: '5px 9px', borderRadius: 8, cursor: 'pointer', background: isSel ? WC.selBg : 'transparent' }}
|
||||
>
|
||||
<div style={{ width: 8, height: 8, borderRadius: '50%', flex: 'none', background: colorForWorkflow(w), opacity: active ? 1 : 0.35 }} />
|
||||
@@ -104,6 +108,22 @@ const LeftRail: React.FC<{ nav: AppNav }> = ({ nav }) => {
|
||||
{active ? describeSchedule(w.schedule) : 'Paused'}
|
||||
</div>
|
||||
</div>
|
||||
{/* Faded rather than unmounted on hover-out: ShareButton owns the modal's open state, so unmounting it would close the modal the moment the pointer left the row for the dialog. Also keeps the row from reflowing on hover. */}
|
||||
<span
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flex: 'none',
|
||||
opacity: hovered === w.id ? 1 : 0,
|
||||
pointerEvents: hovered === w.id ? 'auto' : 'none',
|
||||
transition: 'opacity 0.12s',
|
||||
}}
|
||||
>
|
||||
<ShareButton
|
||||
target={{ kind: 'workflow', id: w.id, name: w.title || 'Untitled workflow' }}
|
||||
iconFontSize={13}
|
||||
/>
|
||||
</span>
|
||||
<div
|
||||
onClick={(e) => { e.stopPropagation(); onDelete(w.id); }}
|
||||
style={{ width: 22, height: 22, borderRadius: 6, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', color: WC.faint, flex: 'none' }}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useAppDispatch } from '@/shared/hooks';
|
||||
import { closeWorkflowsApp, setWorkflowsHubPosition, setWorkflowsHubSize } from '@/shared/state/dashboardLayoutSlice';
|
||||
import EventRepeatIcon from '@mui/icons-material/EventRepeat';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { useWC, FONT_SERIF } from './uiKit';
|
||||
import { setWorkflowsHubPosition, setWorkflowsHubSize } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { useWC } from './uiKit';
|
||||
import WorkflowsAppContent from './WorkflowsAppContent';
|
||||
|
||||
type ResizeDir = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
|
||||
@@ -58,7 +54,6 @@ const WorkflowsAppCard: React.FC<Props> = ({
|
||||
onCardSelect, onDragStart, onDragMove, onDragEnd, onBringToFront,
|
||||
}) => {
|
||||
const WC = useWC();
|
||||
const c = useClaudeTokens();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const panRef = useRef({ panX, panY });
|
||||
@@ -216,31 +211,14 @@ const WorkflowsAppCard: React.FC<Props> = ({
|
||||
transition: noTransition ? 'none' : 'box-shadow 0.3s ease, border-color 0.2s ease',
|
||||
}}
|
||||
>
|
||||
{/* TITLE BAR (drag handle) */}
|
||||
<div
|
||||
onPointerDown={onHeaderPointerDown}
|
||||
onPointerMove={onHeaderPointerMove}
|
||||
onPointerUp={onHeaderPointerUp}
|
||||
style={{ height: 42, flex: 'none', display: 'flex', alignItems: 'center', padding: '0 16px', borderBottom: `1px solid ${WC.line}`, background: WC.panel, gap: 14, cursor: isDragging ? 'grabbing' : 'grab', touchAction: 'none', userSelect: 'none' }}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<EventRepeatIcon sx={{ fontSize: 18, color: WC.accent, display: 'block' }} />
|
||||
<span style={{ fontFamily: FONT_SERIF, fontSize: 14.5, fontWeight: 500, color: WC.ink, letterSpacing: '-0.01em', lineHeight: 1, transform: 'translateY(2.5px)' }}>Workflows</span>
|
||||
</div>
|
||||
<div style={{ flex: 1 }} />
|
||||
<IconButton
|
||||
aria-label="Close"
|
||||
data-no-drag
|
||||
size="small"
|
||||
onClick={(e) => { e.stopPropagation(); dispatch(closeWorkflowsApp()); }}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
sx={{ color: c.text.tertiary, '&:hover': { color: c.status.error, bgcolor: `${c.status.error}14` } }}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<WorkflowsAppContent />
|
||||
<WorkflowsAppContent
|
||||
header={{
|
||||
onPointerDown: onHeaderPointerDown,
|
||||
onPointerMove: onHeaderPointerMove,
|
||||
onPointerUp: onHeaderPointerUp,
|
||||
dragging: isDragging,
|
||||
}}
|
||||
/>
|
||||
|
||||
{HANDLE_DEFS.map(({ dir, css }) => (
|
||||
<div
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import EventRepeatIcon from '@mui/icons-material/EventRepeat';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { clearWorkflowsAppTarget } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { clearWorkflowsAppTarget, closeWorkflowsApp } from '@/shared/state/dashboardLayoutSlice';
|
||||
import {
|
||||
fetchWorkflows, fetchAllRuns, fetchPausedState, fetchActiveRuns, fetchDeletedWorkflows,
|
||||
} from '@/shared/state/workflowsSlice';
|
||||
import { fetchMissedRuns } from '@/shared/state/missedRunsSlice';
|
||||
import { FONT_SANS, useWC } from './uiKit';
|
||||
import type { AppMode, CalView, AppNav } from './types';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import ShareButton from '@/app/components/share/ShareButton';
|
||||
import { FONT_SANS, FONT_SERIF, useWC } from './uiKit';
|
||||
import type { AppMode, CalView, AppNav, CardHeader } from './types';
|
||||
import LeftRail from './LeftRail';
|
||||
import HomeView from './HomeView';
|
||||
import CalendarView from './CalendarView';
|
||||
@@ -14,9 +19,10 @@ import DetailView from './DetailView';
|
||||
import ComposeView from './ComposeView';
|
||||
import TrashView from './TrashView';
|
||||
|
||||
// The three-pane Workflows body, independent of how it's framed (canvas card). Holds nav + data; the card chrome (title bar drag handle, resize) wraps it.
|
||||
const WorkflowsAppContent: React.FC = () => {
|
||||
// The three-pane Workflows body plus its title bar. The card wraps this with drag/resize geometry and passes the drag handlers in; the title bar lives here because Share needs to know which workflow is open.
|
||||
const WorkflowsAppContent: React.FC<{ header: CardHeader }> = ({ header }) => {
|
||||
const WC = useWC();
|
||||
const c = useClaudeTokens();
|
||||
const dispatch = useAppDispatch();
|
||||
const target = useAppSelector((s) => s.dashboardLayout.workflowsAppTarget);
|
||||
const dashboardId = useAppSelector((s) => s.tempState.lastDashboardId) || undefined;
|
||||
@@ -26,6 +32,10 @@ const WorkflowsAppContent: React.FC = () => {
|
||||
const [calView, setCalView] = useState<CalView>('month');
|
||||
const [refDate, setRefDate] = useState<Date>(() => new Date());
|
||||
|
||||
// goHome leaves selectedId set, so gate on the mode too or Share lingers in the title bar after leaving the workflow.
|
||||
const shared = useAppSelector((s) => (selectedId ? s.workflows.items[selectedId] : undefined));
|
||||
const selected = mode === 'detail' ? shared : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchWorkflows(dashboardId));
|
||||
dispatch(fetchAllRuns(200));
|
||||
@@ -56,13 +66,53 @@ const WorkflowsAppContent: React.FC = () => {
|
||||
}), [mode, selectedId, calView, refDate, dashboardId, dispatch]);
|
||||
|
||||
return (
|
||||
<div style={{ flex: 1, display: 'flex', minHeight: 0, fontFamily: FONT_SANS, color: WC.ink, background: WC.page }}>
|
||||
<LeftRail nav={nav} />
|
||||
{mode === 'home' && <HomeView nav={nav} />}
|
||||
{mode === 'calendar' && <CalendarView nav={nav} />}
|
||||
{mode === 'detail' && selectedId && <DetailView workflowId={selectedId} nav={nav} />}
|
||||
{mode === 'new' && <ComposeView nav={nav} />}
|
||||
{mode === 'trash' && <TrashView />}
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0, fontFamily: FONT_SANS, color: WC.ink, background: WC.page }}>
|
||||
{/* TITLE BAR (drag handle) */}
|
||||
<div
|
||||
onPointerDown={header.onPointerDown}
|
||||
onPointerMove={header.onPointerMove}
|
||||
onPointerUp={header.onPointerUp}
|
||||
style={{ height: 42, flex: 'none', display: 'flex', alignItems: 'center', padding: '0 16px', borderBottom: `1px solid ${WC.line}`, background: WC.panel, gap: 14, cursor: header.dragging ? 'grabbing' : 'grab', touchAction: 'none', userSelect: 'none' }}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<EventRepeatIcon sx={{ fontSize: 18, color: WC.accent, display: 'block' }} />
|
||||
<span style={{ fontFamily: FONT_SERIF, fontSize: 14.5, fontWeight: 500, color: WC.ink, letterSpacing: '-0.01em', lineHeight: 1, transform: 'translateY(2.5px)' }}>Workflows</span>
|
||||
</div>
|
||||
<div style={{ flex: 1 }} />
|
||||
{selected && (
|
||||
// The share dialog portals to the body but its events still bubble the React tree, so stop them here or dragging the card follows a click inside the modal.
|
||||
<span
|
||||
data-no-drag
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ display: 'flex' }}
|
||||
>
|
||||
<ShareButton
|
||||
target={{ kind: 'workflow', id: selected.id, name: selected.title || 'Untitled workflow' }}
|
||||
iconFontSize={17}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<IconButton
|
||||
aria-label="Close"
|
||||
data-no-drag
|
||||
size="small"
|
||||
onClick={(e) => { e.stopPropagation(); dispatch(closeWorkflowsApp()); }}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
sx={{ color: c.text.tertiary, '&:hover': { color: c.status.error, bgcolor: `${c.status.error}14` } }}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, display: 'flex', minHeight: 0 }}>
|
||||
<LeftRail nav={nav} />
|
||||
{mode === 'home' && <HomeView nav={nav} />}
|
||||
{mode === 'calendar' && <CalendarView nav={nav} />}
|
||||
{mode === 'detail' && selectedId && <DetailView workflowId={selectedId} nav={nav} />}
|
||||
{mode === 'new' && <ComposeView nav={nav} />}
|
||||
{mode === 'trash' && <TrashView />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import type { PointerEvent } from 'react';
|
||||
|
||||
export type AppMode = 'home' | 'calendar' | 'detail' | 'new' | 'trash';
|
||||
export type CalView = 'week' | 'month';
|
||||
|
||||
// The card owns drag geometry but the title bar renders inside the content (it needs nav state to know which workflow to share), so the card hands its drag handlers down.
|
||||
export interface CardHeader {
|
||||
onPointerDown: (e: PointerEvent) => void;
|
||||
onPointerMove: (e: PointerEvent) => void;
|
||||
onPointerUp: (e: PointerEvent) => void;
|
||||
dragging: boolean;
|
||||
}
|
||||
|
||||
// Navigation + ephemeral UI state for the Workflows app window. Data lives in Redux; this is only "where am I looking right now".
|
||||
export interface AppNav {
|
||||
mode: AppMode;
|
||||
|
||||
Reference in New Issue
Block a user