diff --git a/apps/rowboat/app/projects/[projectId]/workflow/app.tsx b/apps/rowboat/app/projects/[projectId]/workflow/app.tsx
index 1044810f..ea948813 100644
--- a/apps/rowboat/app/projects/[projectId]/workflow/app.tsx
+++ b/apps/rowboat/app/projects/[projectId]/workflow/app.tsx
@@ -102,6 +102,8 @@ export function App({
function handleSetMode(mode: 'draft' | 'live') {
setMode(mode);
+ // Reload data to ensure we have the latest workflow data for the current mode
+ reloadData();
}
async function handleRevertToLive() {
diff --git a/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx b/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx
index 2e6f350c..7684bfec 100644
--- a/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx
+++ b/apps/rowboat/app/projects/[projectId]/workflow/components/TopBar.tsx
@@ -98,7 +98,7 @@ export function TopBar({
{showBuildModeBanner &&
- Switched to draft mode to enable Build panel. You can now make changes to your workflow.
+ Switched to draft mode. You can now make changes to your workflow.
}
diff --git a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx
index ea790de0..ff2c380d 100644
--- a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx
+++ b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx
@@ -316,23 +316,9 @@ function reducer(state: State, action: Action): State {
break;
}
default: {
- // Check if this is a workflow modification action in live mode
- const isWorkflowModification = [
- "add_agent", "add_tool", "add_prompt", "add_prompt_no_select", "add_pipeline",
- "update_agent", "update_tool", "update_prompt", "update_prompt_no_select", "update_pipeline",
- "delete_agent", "delete_tool", "delete_prompt", "delete_pipeline",
- "toggle_agent", "set_main_agent", "reorder_agents", "reorder_pipelines"
- ].includes(action.type);
-
const [nextState, patches, inversePatches] = produceWithPatches(
state.present,
(draft) => {
- // If this is a workflow modification in live mode, switch to draft
- if (isWorkflowModification && isLive) {
- draft.isLive = false;
-
- }
-
switch (action.type) {
case "select_agent":
draft.selection = {
@@ -958,7 +944,39 @@ export function WorkflowEditor({
const [activePanel, setActivePanel] = useState<'playground' | 'copilot'>('copilot');
const [isInitialState, setIsInitialState] = useState(true);
const [showBuildModeBanner, setShowBuildModeBanner] = useState(false);
+ const [showEditModal, setShowEditModal] = useState(false);
+ const [pendingAction, setPendingAction] = useState(null);
+ const [configKey, setConfigKey] = useState(0);
+ const [lastWorkflowId, setLastWorkflowId] = useState(null);
const [showTour, setShowTour] = useState(true);
+
+ // Centralized mode transition handler
+ const handleModeTransition = useCallback((newMode: 'draft' | 'live', reason: 'publish' | 'view_live' | 'switch_draft' | 'modal_switch') => {
+ // Clear any open entity configs
+ dispatch({ type: "unselect_agent" });
+
+ // Set default panel based on mode
+ setActivePanel(newMode === 'live' ? 'playground' : 'copilot');
+
+ // Force component re-render
+ setConfigKey(prev => prev + 1);
+
+ // Handle mode-specific logic
+ if (reason === 'publish') {
+ // This will be handled by the publish function itself
+ return;
+ } else {
+ // Direct mode switch
+ onChangeMode(newMode);
+
+ // If switching to draft mode, we need to ensure we have the correct draft data
+ // The parent component will update the workflow prop, but we need to wait for it
+ if (newMode === 'draft') {
+ // Force a workflow state reset when the workflow prop updates
+ setLastWorkflowId(null);
+ }
+ }
+ }, [onChangeMode]);
const copilotRef = useRef<{ handleUserMessage: (message: string) => void }>(null);
const entityListRef = useRef<{ openDataSourcesModal: () => void } | null>(null);
@@ -1191,7 +1209,6 @@ export function WorkflowEditor({
}
function handleReorderAgents(agents: z.infer[]) {
- handleWorkflowChange();
// Save order to localStorage
const orderMap = agents.reduce((acc, agent, index) => {
acc[agent.name] = index;
@@ -1204,7 +1221,6 @@ export function WorkflowEditor({
}
function handleReorderPipelines(pipelines: z.infer[]) {
- handleWorkflowChange();
// Save order to localStorage
const orderMap = pipelines.reduce((acc, pipeline, index) => {
acc[pipeline.name] = index;
@@ -1220,6 +1236,8 @@ export function WorkflowEditor({
dispatch({ type: 'set_publishing', publishing: true });
try {
await publishWorkflow(projectId, state.present.workflow);
+ // Use centralized mode transition for publish
+ handleModeTransition('live', 'publish');
// reflect live mode both internally and externally in one go
dispatch({ type: 'set_is_live', isLive: true });
onChangeMode('live');
@@ -1342,19 +1360,76 @@ export function WorkflowEditor({
])).current;
const dispatchGuarded = useCallback((action: Action) => {
+ // Intercept workflow modifications in live mode before they reach the reducer
if (WORKFLOW_MOD_ACTIONS.has((action as any).type) && isLive && !state.present.publishing) {
- onChangeMode('draft');
- setShowBuildModeBanner(true);
- setTimeout(() => setShowBuildModeBanner(false), 5000);
+ setPendingAction(action);
+ setShowEditModal(true);
+ return; // Block the action - it never reaches the reducer
}
- dispatch(action);
- }, [WORKFLOW_MOD_ACTIONS, isLive, state.present.publishing, onChangeMode, dispatch]);
+ dispatch(action); // Allow the action to proceed
+ }, [WORKFLOW_MOD_ACTIONS, isLive, state.present.publishing, dispatch]);
+
+ // Simplified modal handlers
+ const handleSwitchToDraft = useCallback(() => {
+ setShowEditModal(false);
+ setPendingAction(null); // Don't apply the pending action
+ handleModeTransition('draft', 'modal_switch');
+ setShowBuildModeBanner(true);
+ setTimeout(() => setShowBuildModeBanner(false), 5000);
+ }, [handleModeTransition]);
+
+ const handleCancelEdit = useCallback(() => {
+ setShowEditModal(false);
+ setPendingAction(null);
+ // Force re-render of config components to reset form values
+ setConfigKey(prev => prev + 1);
+ }, []);
+
+ // Single useEffect for data synchronization
+ useEffect(() => {
+ // Only sync when workflow data actually changes
+ const currentWorkflowId = `${isLive ? 'live' : 'draft'}-${workflow.lastUpdatedAt}`;
+
+ // Special case: if we're switching to draft mode and the workflow data looks like live data
+ // (same lastUpdatedAt as the previous live data), don't reset the state yet
+ if (!isLive && lastWorkflowId && lastWorkflowId.startsWith('live-') &&
+ currentWorkflowId === `draft-${workflow.lastUpdatedAt}`) {
+ // This is likely stale draft data that matches live data
+ // Don't reset the state, just update the ID
+ setLastWorkflowId(currentWorkflowId);
+ return;
+ }
+
+ if (lastWorkflowId !== currentWorkflowId) {
+ dispatch({ type: "restore_state", state: { ...state.present, workflow } });
+ setLastWorkflowId(currentWorkflowId);
+ }
+ }, [workflow, isLive, lastWorkflowId, state.present]);
+
+ // Handle the case where we switch to draft mode but get stale data
+ useEffect(() => {
+ // If we're in draft mode but the workflow data looks like live data (same lastUpdatedAt as live)
+ // and we just switched from live mode, we need to wait for fresh draft data
+ if (!isLive && lastWorkflowId && lastWorkflowId.startsWith('live-')) {
+ // We just switched from live to draft, but we might have stale data
+ // Clear the selection to prevent showing wrong data
+ dispatch({ type: "unselect_agent" });
+ }
+ }, [isLive, lastWorkflowId]);
+
+ // Additional effect to handle mode changes that might not trigger workflow prop updates
+ useEffect(() => {
+ // If we're in draft mode but the workflow state contains live data, clear selection
+ // This prevents showing wrong data while waiting for the correct workflow prop
+ if (!isLive && state.present.isLive) {
+ dispatch({ type: "unselect_agent" });
+ }
+ }, [isLive, state.present.isLive]);
function handleTogglePanel() {
if (isLive && activePanel === 'playground') {
// User is trying to switch to Build mode in live mode
- onChangeMode('draft');
- setActivePanel('copilot'); // Switch to Build mode as intended
+ handleModeTransition('draft', 'switch_draft');
setShowBuildModeBanner(true);
// Auto-hide banner after 5 seconds
setTimeout(() => setShowBuildModeBanner(false), 5000);
@@ -1363,16 +1438,6 @@ export function WorkflowEditor({
}
}
- function handleWorkflowChange() {
- if (isLive) {
- // User is making changes in live mode - switch to draft
- onChangeMode('draft');
- setShowBuildModeBanner(true);
- // Auto-hide banner after 5 seconds
- setTimeout(() => setShowBuildModeBanner(false), 5000);
- }
- }
-
const validateProjectName = (value: string) => {
if (value.length === 0) {
setProjectNameError("Project name cannot be empty");
@@ -1433,6 +1498,39 @@ export function WorkflowEditor({
onSelectPrompt: handleSelectPrompt,
}}>
+ {/* Live Workflow Edit Modal */}
+
+
+
+
+
+ Edit Live Workflow
+
+
+
+
+ Seems like you're trying to edit the live workflow. Only the draft version can be modified. Changes will not be saved.
+