@@ -241,12 +241,22 @@ async def _handle_find_project_for_file(
241241async def _handle_add_dir (
242242 params : dict | None , ws_context : context .WorkspaceContext
243243) -> dict :
244- """Add a workspace directory. Discovers projects, reads configs, starts runners."""
245- from finecode .api_server .config import read_configs
244+ """Add a workspace directory. Discovers projects, reads configs, starts runners.
245+
246+ Params:
247+ dir_path: str - absolute path to the workspace directory
248+ start_runners: bool - whether to start extension runners (default true).
249+ When false, configs are read and actions collected without starting any
250+ runners. Useful when runner environments may not exist yet (e.g. before
251+ running prepare-envs).
252+ """
253+ from finecode .api_server .config import collect_actions , read_configs
246254 from finecode .api_server .runner import runner_manager
247255 from finecode .api_server .runner .runner_client import RunnerStatus
248256
257+ params = params or {}
249258 dir_path = pathlib .Path (params ["dir_path" ])
259+ start_runners : bool = params .get ("start_runners" , True )
250260 logger .trace (f"Add ws dir: { dir_path } " )
251261
252262 if dir_path in ws_context .ws_dirs_paths :
@@ -260,6 +270,21 @@ async def _handle_add_dir(
260270 project = project , ws_context = ws_context , resolve_presets = False
261271 )
262272
273+ if not start_runners :
274+ # Collect actions directly from raw config without needing runners.
275+ from finecode .api_server .config import config_models
276+ for project in new_projects :
277+ if project .status == domain .ProjectStatus .CONFIG_VALID :
278+ try :
279+ collect_actions .collect_actions (
280+ project_path = project .dir_path , ws_context = ws_context
281+ )
282+ except config_models .ConfigurationError as exc :
283+ logger .warning (
284+ f"Failed to collect actions for { project .name } : { exc .message } "
285+ )
286+ return {"projects" : [_project_to_dict (p ) for p in new_projects ]}
287+
263288 try :
264289 await runner_manager .start_runners_with_presets (
265290 projects = new_projects ,
@@ -492,6 +517,101 @@ async def _handle_runners_restart(
492517 return {}
493518
494519
520+ async def _handle_start_runners (
521+ params : dict | None , ws_context : context .WorkspaceContext
522+ ) -> dict :
523+ """Start extension runners for all (or specified) projects.
524+
525+ Complements any runners already running — only missing runners are started.
526+ Resolves presets so that ``project.actions`` reflects preset-defined handlers.
527+
528+ Params: ``{"projects": ["project_name", ...]}`` (optional, default: all projects)
529+ Result: ``{}``
530+ """
531+ from finecode .api_server .runner import runner_manager
532+
533+ params = params or {}
534+ project_names : list [str ] | None = params .get ("projects" )
535+
536+ projects = list (ws_context .ws_projects .values ())
537+ if project_names is not None :
538+ projects = [p for p in projects if p .name in project_names ]
539+
540+ try :
541+ await runner_manager .start_runners_with_presets (
542+ projects = projects ,
543+ ws_context = ws_context ,
544+ )
545+ except runner_manager .RunnerFailedToStart as exc :
546+ raise ValueError (f"Starting runners failed: { exc .message } " ) from exc
547+
548+ return {}
549+
550+
551+ async def _handle_runners_check_env (
552+ params : dict | None , ws_context : context .WorkspaceContext
553+ ) -> dict :
554+ """Check whether an environment is valid for a given project.
555+
556+ Params: ``{"project": "project_name", "env_name": "dev_workspace"}``
557+ Result: ``{"valid": bool}``
558+ """
559+ from finecode .api_server .runner import runner_manager
560+
561+ params = params or {}
562+ project_name = params .get ("project" )
563+ env_name = params .get ("env_name" )
564+
565+ if not project_name or not env_name :
566+ raise ValueError ("project and env_name are required" )
567+
568+ project = next (
569+ (p for p in ws_context .ws_projects .values () if p .name == project_name ), None
570+ )
571+ if project is None :
572+ raise ValueError (f"Project '{ project_name } ' not found" )
573+
574+ valid = await runner_manager .check_runner (
575+ runner_dir = project .dir_path , env_name = env_name
576+ )
577+ return {"valid" : valid }
578+
579+
580+ async def _handle_runners_remove_env (
581+ params : dict | None , ws_context : context .WorkspaceContext
582+ ) -> dict :
583+ """Remove an environment for a given project.
584+
585+ Stops the runner if running, then deletes the environment directory.
586+
587+ Params: ``{"project": "project_name", "env_name": "dev_workspace"}``
588+ Result: ``{}``
589+ """
590+ from finecode .api_server .runner import runner_manager
591+
592+ params = params or {}
593+ project_name = params .get ("project" )
594+ env_name = params .get ("env_name" )
595+
596+ if not project_name or not env_name :
597+ raise ValueError ("project and env_name are required" )
598+
599+ project = next (
600+ (p for p in ws_context .ws_projects .values () if p .name == project_name ), None
601+ )
602+ if project is None :
603+ raise ValueError (f"Project '{ project_name } ' not found" )
604+
605+ # Stop the runner if it is currently running.
606+ runners = ws_context .ws_projects_extension_runners .get (project .dir_path , {})
607+ runner = runners .get (env_name )
608+ if runner is not None :
609+ await runner_manager .stop_extension_runner (runner = runner )
610+
611+ runner_manager .remove_runner_venv (runner_dir = project .dir_path , env_name = env_name )
612+ return {}
613+
614+
495615async def _handle_server_reset (
496616 params : dict | None , ws_context : context .WorkspaceContext
497617) -> dict :
@@ -814,6 +934,7 @@ async def _handle_run_with_partial_results_task(
814934 "workspace/removeDir" : _handle_remove_dir ,
815935 "workspace/setConfigOverrides" : _handle_set_config_overrides ,
816936 "workspace/getProjectRawConfig" : _handle_get_project_raw_config ,
937+ "workspace/startRunners" : _handle_start_runners ,
817938 # actions/
818939 "actions/list" : _handle_list_actions ,
819940 "actions/getTree" : _handle_get_tree ,
@@ -824,6 +945,8 @@ async def _handle_run_with_partial_results_task(
824945 # runners:
825946 "runners/list" : _handle_runners_list ,
826947 "runners/restart" : _handle_runners_restart ,
948+ "runners/checkEnv" : _handle_runners_check_env ,
949+ "runners/removeEnv" : _handle_runners_remove_env ,
827950 # server/
828951 "server/reset" : _handle_server_reset ,
829952 "server/shutdown" : _stub ("server/shutdown" ),
0 commit comments