@@ -81,7 +81,7 @@ class Task(Viewer):
8181 running = param .Boolean (doc = """
8282 Whether the task is currently running.""" )
8383
84- status = param .Selector (objects = ["idle" , "running" , "success" , "error" ], default = "idle" , doc = """
84+ status = param .Selector (objects = ["idle" , "running" , "success" , "error" , "cancelled" ], default = "idle" , doc = """
8585 The current status of the task.""" )
8686
8787 steps_layout = param .ClassSelector (default = None , class_ = (ListLike , NamedListLike ), allow_None = True , doc = """
@@ -230,11 +230,14 @@ async def execute(self, context: TContext | None = None, **kwargs) -> tuple[list
230230 if not self ._prepared :
231231 await self .prepare (context )
232232 views , out_context = await self ._execute (context , ** kwargs )
233+ except asyncio .CancelledError :
234+ self .status = "cancelled"
235+ raise
233236 except Exception :
234237 self .status = "error"
235238 raise
236239 finally :
237- if self .status != "error" :
240+ if self .status not in ( "error" , "cancelled" ) :
238241 self .status = "success"
239242 self .running = False
240243 self .out_context = out_context
@@ -411,6 +414,9 @@ async def _execute(self, context: TContext, **kwargs):
411414 new = []
412415 try :
413416 new , new_context = await self ._run_task (i , task , context , ** kwargs )
417+ except asyncio .CancelledError :
418+ self .status = "cancelled"
419+ raise
414420 except MissingContextError :
415421 # Re-raise MissingContextError to allow retry logic at Plan level
416422 raise
@@ -432,8 +438,8 @@ async def _execute(self, context: TContext, **kwargs):
432438 break
433439 views += new
434440 finally :
435- self ._current = i + (0 if task .status == "error" else 1 )
436- if self .status != "error" :
441+ self ._current = i + (0 if task .status in ( "error" , "cancelled" ) else 1 )
442+ if self .status not in ( "error" , "cancelled" ) :
437443 self .status = "success"
438444 contexts = [self .context ] if self .context else []
439445 contexts += [task .out_context for task in self ]
@@ -845,6 +851,8 @@ class Report(TaskGroup):
845851 auto_execute = param .Boolean (default = False , doc = """
846852 If True, automatically execute the report on initialization.""" )
847853
854+ _active_task = param .ClassSelector (class_ = asyncio .Task , default = None , allow_None = True )
855+
848856 _tasks = param .List (item_type = Section )
849857
850858 level = 1
@@ -875,6 +883,12 @@ def _init_view(self):
875883 self ._run = IconButton (
876884 icon = "play_arrow" , on_click = self ._execute_event , margin = 0 , size = "large" ,
877885 description = "Execute Report" , loading = self .param .running ,
886+ visible = self .param ._active_task .rx .is_ (None ),
887+ )
888+ self ._stop = IconButton (
889+ icon = "stop" , on_click = self ._handle_cancel , margin = 0 , size = "large" ,
890+ description = "Stop Report" , color = "error" ,
891+ visible = self .param ._active_task .rx .is_not (None ),
878892 )
879893 self ._clear = IconButton (
880894 icon = "clear" , on_click = lambda _ : self .reset (), margin = 0 , size = "large" ,
@@ -926,6 +940,7 @@ def _init_view(self):
926940 self ._menu = Row (
927941 self ._header_title ,
928942 self ._run ,
943+ self ._stop ,
929944 self ._clear ,
930945 self ._collapse ,
931946 self ._export ,
@@ -935,6 +950,7 @@ def _init_view(self):
935950 self ._dial = SpeedDial (
936951 items = [
937952 {"label" : "Execute Report" , "icon" : "play_arrow" },
953+ {"label" : "Stop Report" , "icon" : "stop" },
938954 {"label" : "Clear Report" , "icon" : "clear" },
939955 {"label" : "Export as Notebook" , "icon" : "description" , "format" : "ipynb" },
940956 {"label" : "Export as HTML" , "icon" : "language" , "format" : "html" },
@@ -972,7 +988,12 @@ def _init_view(self):
972988 async def _trigger_event (self , item : dict ):
973989 icon = item ["icon" ]
974990 if icon == "play_arrow" :
975- await self ._execute_event ()
991+ if self ._active_task is not None and not self ._active_task .done ():
992+ self ._handle_cancel ()
993+ else :
994+ await self ._execute_event ()
995+ elif icon == "stop" :
996+ self ._handle_cancel ()
976997 elif icon == "clear" :
977998 self .reset ()
978999 elif icon in ("description" , "language" ):
@@ -990,7 +1011,7 @@ def _update_run_state(self):
9901011 @param .depends ('status' , 'views' , watch = True )
9911012 def _update_icon_visibility (self ):
9921013 """Show/hide icons based on whether report has outputs."""
993- has_outputs = self .status in ("success" , "error" ) or bool (self .views )
1014+ has_outputs = self .status in ("success" , "error" , "cancelled" ) or bool (self .views )
9941015 self ._clear .visible = has_outputs
9951016 self ._collapse .visible = has_outputs
9961017 self ._export .visible = has_outputs
@@ -1008,11 +1029,27 @@ def _update_icon_visibility(self):
10081029 }
10091030
10101031 async def _execute_event (self , event = None ):
1032+ if self ._active_task is not None and not self ._active_task .done ():
1033+ # Already running; the stop button handles cancellation
1034+ return
10111035 await asyncio .sleep (0.01 ) # yield the event loop to allow button loading state to update
1012- await self .execute ()
1036+ task = asyncio .create_task (self .execute ())
1037+ self ._active_task = task
1038+ task .add_done_callback (self ._on_execute_done )
1039+
1040+ def _on_execute_done (self , task ):
1041+ """Clear the active task reference so UI controls flip back to idle."""
1042+ if task is not self ._active_task :
1043+ return
1044+ self ._active_task = None
1045+
1046+ def _handle_cancel (self , event = None ):
1047+ """Cancel the in-flight report execution, if any."""
1048+ if self ._active_task is not None and not self ._active_task .done ():
1049+ self ._active_task .cancel ()
10131050
10141051 async def _export_report (self , item = None ):
1015- if len (self ) and self .status != "success" :
1052+ if len (self ) and self .status not in ( "success" , "cancelled" , "error" ) :
10161053 await self .execute ()
10171054 fmt = item .get ("format" , "ipynb" ) if isinstance (item , dict ) else "ipynb"
10181055 title = self .title or "Report"
@@ -1035,7 +1072,7 @@ def _open_settings(self, event=None):
10351072
10361073 def _populate_view (self ):
10371074 self ._view [:] = objects = [(task .title , task ) for task in self ]
1038- has_outputs = self .status in ("success" , "error" ) or bool (self .views )
1075+ has_outputs = self .status in ("success" , "error" , "cancelled" ) or bool (self .views )
10391076 if has_outputs :
10401077 self ._view .active = list (range (len (objects )))
10411078 else :
0 commit comments