11use tauri:: ipc:: Channel ;
22use tauri:: { AppHandle , State } ;
33
4- use crate :: repos;
4+ use crate :: repos:: { self , RunAction } ;
55use crate :: workspace:: scripts:: { ScriptContext , ScriptEvent , ScriptProcessManager } ;
66
77use super :: common:: CmdResult ;
88
9+ /// Internal `script_type` namespace for the run script after the multi-action
10+ /// refactor. Process keys for run scripts are `"run:<action_id>"` so the
11+ /// process manager can distinguish per-action lifecycles within the same
12+ /// workspace, and so `kill_others_in_repo` naturally implements per-action
13+ /// exclusive mode by filtering on the full `"run:<id>"` string.
14+ fn run_script_type ( action_id : & str ) -> String {
15+ format ! ( "run:{action_id}" )
16+ }
17+
18+ /// Resolve which `RunAction` the caller is targeting.
19+ ///
20+ /// - If `action_id` is supplied, look it up by id; error if it's gone.
21+ /// - If `action_id` is None, fall back to the first action in display order
22+ /// (legacy callers that haven't been updated yet).
23+ ///
24+ /// Returns the action plus the process key to use for it.
25+ fn resolve_run_target (
26+ repo_id : & str ,
27+ workspace_id : Option < & str > ,
28+ action_id : Option < & str > ,
29+ ) -> anyhow:: Result < RunAction > {
30+ let scripts = repos:: load_repo_scripts ( repo_id, workspace_id) ?;
31+ let action = match action_id {
32+ Some ( id) => scripts
33+ . run_actions
34+ . into_iter ( )
35+ . find ( |a| a. id == id)
36+ . ok_or_else ( || anyhow:: anyhow!( "Run action not found: {id}" ) ) ?,
37+ None => scripts
38+ . run_actions
39+ . into_iter ( )
40+ . next ( )
41+ . ok_or_else ( || anyhow:: anyhow!( "No run actions configured for repo {repo_id}" ) ) ?,
42+ } ;
43+ Ok ( action)
44+ }
45+
946#[ tauri:: command]
1047pub async fn execute_repo_script (
1148 app : AppHandle ,
1249 manager : State < ' _ , ScriptProcessManager > ,
1350 repo_id : String ,
1451 script_type : String ,
1552 workspace_id : Option < String > ,
53+ action_id : Option < String > ,
1654 channel : Channel < ScriptEvent > ,
1755) -> CmdResult < ( ) > {
56+ // Run scripts are dispatched per `action_id` against the multi-action
57+ // model. Setup / archive remain single per repo.
58+ if script_type == "run" {
59+ let ws = workspace_id. clone ( ) ;
60+ let rid = repo_id. clone ( ) ;
61+ let aid = action_id. clone ( ) ;
62+ let action = match tauri:: async_runtime:: spawn_blocking ( move || {
63+ resolve_run_target ( & rid, ws. as_deref ( ) , aid. as_deref ( ) )
64+ } )
65+ . await
66+ . map_err ( |e| anyhow:: anyhow!( "spawn_blocking join failed: {e}" ) ) ?
67+ {
68+ Ok ( a) => a,
69+ Err ( e) => {
70+ let _ = channel. send ( ScriptEvent :: Error {
71+ message : e. to_string ( ) ,
72+ } ) ;
73+ return Ok ( ( ) ) ;
74+ }
75+ } ;
76+
77+ let process_type = run_script_type ( & action. id ) ;
78+ // Per-action exclusive: same `script_type` ("run:<id>") across
79+ // workspaces in the repo gets killed; different action ids are
80+ // independent because the filter compares the full string.
81+ if action. mode == "non-concurrent" {
82+ manager. kill_others_in_repo ( & repo_id, & process_type, workspace_id. as_deref ( ) ) ;
83+ }
84+
85+ return spawn_script (
86+ app,
87+ manager,
88+ repo_id,
89+ process_type,
90+ workspace_id,
91+ action. command ,
92+ channel,
93+ )
94+ . await ;
95+ }
96+
1897 let scripts = tauri:: async_runtime:: spawn_blocking ( {
1998 let repo_id = repo_id. clone ( ) ;
2099 let ws_id = workspace_id. clone ( ) ;
@@ -25,7 +104,6 @@ pub async fn execute_repo_script(
25104
26105 let script = match script_type. as_str ( ) {
27106 "setup" => scripts. setup_script . clone ( ) ,
28- "run" => scripts. run_script . clone ( ) ,
29107 "archive" => scripts. archive_script . clone ( ) ,
30108 _ => None ,
31109 } ;
@@ -37,13 +115,27 @@ pub async fn execute_repo_script(
37115 return Ok ( ( ) ) ;
38116 } ;
39117
40- // Non-concurrent run mode: starting a run script stops any other live
41- // run script in the same repo first. Only applies to "run" — setup
42- // and archive each have their own one-off lifecycle.
43- if script_type == "run" && scripts. run_script_mode == "non-concurrent" {
44- manager. kill_others_in_repo ( & repo_id, "run" , workspace_id. as_deref ( ) ) ;
45- }
118+ spawn_script (
119+ app,
120+ manager,
121+ repo_id,
122+ script_type,
123+ workspace_id,
124+ script,
125+ channel,
126+ )
127+ . await
128+ }
46129
130+ async fn spawn_script (
131+ app : AppHandle ,
132+ manager : State < ' _ , ScriptProcessManager > ,
133+ repo_id : String ,
134+ script_type : String ,
135+ workspace_id : Option < String > ,
136+ script : String ,
137+ channel : Channel < ScriptEvent > ,
138+ ) -> CmdResult < ( ) > {
47139 let ( repo, workspace) = tauri:: async_runtime:: spawn_blocking ( {
48140 let repo_id = repo_id. clone ( ) ;
49141 let ws_id = workspace_id. clone ( ) ;
@@ -98,6 +190,9 @@ pub async fn execute_repo_script(
98190 } ;
99191 let mgr = manager. inner ( ) . clone ( ) ;
100192
193+ // Setup-completion hook keys on the literal `"setup"` script_type — run
194+ // actions (which carry a `"run:<id>"` script_type now) never trigger it.
195+ let is_setup = script_type == "setup" ;
101196 tauri:: async_runtime:: spawn_blocking ( move || {
102197 match crate :: workspace:: scripts:: run_script (
103198 & mgr,
@@ -109,7 +204,7 @@ pub async fn execute_repo_script(
109204 & context,
110205 channel. clone ( ) ,
111206 ) {
112- Ok ( Some ( 0 ) ) if script_type == "setup" => {
207+ Ok ( Some ( 0 ) ) if is_setup => {
113208 if let Some ( ws_id) = & workspace_id {
114209 if let Ok ( ts) = crate :: models:: db:: current_timestamp ( ) {
115210 let _ = crate :: models:: workspaces:: mark_setup_completed ( ws_id, & ts) ;
@@ -135,8 +230,10 @@ pub async fn stop_repo_script(
135230 repo_id : String ,
136231 script_type : String ,
137232 workspace_id : Option < String > ,
233+ action_id : Option < String > ,
138234) -> CmdResult < bool > {
139- let key = ( repo_id, script_type, workspace_id) ;
235+ let process_type = process_type_for ( & script_type, action_id. as_deref ( ) ) ;
236+ let key = ( repo_id, process_type, workspace_id) ;
140237 Ok ( manager. kill ( & key) )
141238}
142239
@@ -149,9 +246,11 @@ pub async fn write_repo_script_stdin(
149246 repo_id : String ,
150247 script_type : String ,
151248 workspace_id : Option < String > ,
249+ action_id : Option < String > ,
152250 data : String ,
153251) -> CmdResult < bool > {
154- let key = ( repo_id, script_type, workspace_id) ;
252+ let process_type = process_type_for ( & script_type, action_id. as_deref ( ) ) ;
253+ let key = ( repo_id, process_type, workspace_id) ;
155254 Ok ( manager. write_stdin ( & key, data. as_bytes ( ) ) ?)
156255}
157256
@@ -163,9 +262,127 @@ pub async fn resize_repo_script(
163262 repo_id : String ,
164263 script_type : String ,
165264 workspace_id : Option < String > ,
265+ action_id : Option < String > ,
166266 cols : u16 ,
167267 rows : u16 ,
168268) -> CmdResult < bool > {
169- let key = ( repo_id, script_type, workspace_id) ;
269+ let process_type = process_type_for ( & script_type, action_id. as_deref ( ) ) ;
270+ let key = ( repo_id, process_type, workspace_id) ;
170271 Ok ( manager. resize ( & key, cols, rows) ?)
171272}
273+
274+ /// Stop / write / resize need the SAME process-key shape `execute_repo_script`
275+ /// registered with. For "run" that means `"run:<id>"`; for any other
276+ /// script_type the original literal is used (`"setup"`, `"archive"`, or the
277+ /// terminal/agent-login UUID-namespaced strings).
278+ fn process_type_for ( script_type : & str , action_id : Option < & str > ) -> String {
279+ if script_type == "run" {
280+ if let Some ( id) = action_id {
281+ return run_script_type ( id) ;
282+ }
283+ }
284+ script_type. to_string ( )
285+ }
286+
287+ #[ tauri:: command]
288+ pub async fn create_repo_run_action (
289+ app : AppHandle ,
290+ repo_id : String ,
291+ name : String ,
292+ command : String ,
293+ mode : String ,
294+ ) -> CmdResult < repos:: RunAction > {
295+ let result = tauri:: async_runtime:: spawn_blocking ( {
296+ let repo_id = repo_id. clone ( ) ;
297+ move || repos:: create_repo_run_action ( & repo_id, name. trim ( ) , command. trim ( ) , & mode)
298+ } )
299+ . await
300+ . map_err ( |e| anyhow:: anyhow!( "spawn_blocking join failed: {e}" ) ) ??;
301+
302+ crate :: ui_sync:: publish (
303+ & app,
304+ crate :: ui_sync:: UiMutationEvent :: RepoRunActionsChanged {
305+ repo_id : repo_id. clone ( ) ,
306+ } ,
307+ ) ;
308+ Ok ( result)
309+ }
310+
311+ #[ tauri:: command]
312+ pub async fn update_repo_run_action (
313+ app : AppHandle ,
314+ repo_id : String ,
315+ action_id : String ,
316+ name : String ,
317+ command : String ,
318+ mode : String ,
319+ ) -> CmdResult < ( ) > {
320+ tauri:: async_runtime:: spawn_blocking ( {
321+ let action_id = action_id. clone ( ) ;
322+ move || repos:: update_repo_run_action ( & action_id, name. trim ( ) , command. trim ( ) , & mode)
323+ } )
324+ . await
325+ . map_err ( |e| anyhow:: anyhow!( "spawn_blocking join failed: {e}" ) ) ??;
326+
327+ crate :: ui_sync:: publish (
328+ & app,
329+ crate :: ui_sync:: UiMutationEvent :: RepoRunActionsChanged { repo_id } ,
330+ ) ;
331+ Ok ( ( ) )
332+ }
333+
334+ #[ tauri:: command]
335+ pub async fn delete_repo_run_action (
336+ app : AppHandle ,
337+ repo_id : String ,
338+ action_id : String ,
339+ ) -> CmdResult < ( ) > {
340+ tauri:: async_runtime:: spawn_blocking ( {
341+ let action_id = action_id. clone ( ) ;
342+ move || repos:: delete_repo_run_action ( & action_id)
343+ } )
344+ . await
345+ . map_err ( |e| anyhow:: anyhow!( "spawn_blocking join failed: {e}" ) ) ??;
346+
347+ crate :: ui_sync:: publish (
348+ & app,
349+ crate :: ui_sync:: UiMutationEvent :: RepoRunActionsChanged { repo_id } ,
350+ ) ;
351+ Ok ( ( ) )
352+ }
353+
354+ #[ tauri:: command]
355+ pub async fn reorder_repo_run_actions (
356+ app : AppHandle ,
357+ repo_id : String ,
358+ ordered_ids : Vec < String > ,
359+ ) -> CmdResult < ( ) > {
360+ tauri:: async_runtime:: spawn_blocking ( {
361+ let repo_id = repo_id. clone ( ) ;
362+ move || repos:: reorder_repo_run_actions ( & repo_id, & ordered_ids)
363+ } )
364+ . await
365+ . map_err ( |e| anyhow:: anyhow!( "spawn_blocking join failed: {e}" ) ) ??;
366+
367+ crate :: ui_sync:: publish (
368+ & app,
369+ crate :: ui_sync:: UiMutationEvent :: RepoRunActionsChanged { repo_id } ,
370+ ) ;
371+ Ok ( ( ) )
372+ }
373+
374+ #[ tauri:: command]
375+ pub async fn set_workspace_active_run_action (
376+ workspace_id : String ,
377+ action_id : Option < String > ,
378+ ) -> CmdResult < ( ) > {
379+ tauri:: async_runtime:: spawn_blocking ( move || {
380+ crate :: models:: workspaces:: update_workspace_active_run_action (
381+ & workspace_id,
382+ action_id. as_deref ( ) ,
383+ )
384+ } )
385+ . await
386+ . map_err ( |e| anyhow:: anyhow!( "spawn_blocking join failed: {e}" ) ) ??;
387+ Ok ( ( ) )
388+ }
0 commit comments