Skip to content

Commit 9689931

Browse files
authored
feat: support multiple run actions (#636)
1 parent 45d692d commit 9689931

32 files changed

Lines changed: 1999 additions & 242 deletions

src-tauri/src/commands/script_commands.rs

Lines changed: 229 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,99 @@
11
use tauri::ipc::Channel;
22
use tauri::{AppHandle, State};
33

4-
use crate::repos;
4+
use crate::repos::{self, RunAction};
55
use crate::workspace::scripts::{ScriptContext, ScriptEvent, ScriptProcessManager};
66

77
use 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]
1047
pub 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+
}

src-tauri/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,11 @@ pub fn run() {
331331
commands::script_commands::stop_repo_script,
332332
commands::script_commands::write_repo_script_stdin,
333333
commands::script_commands::resize_repo_script,
334+
commands::script_commands::create_repo_run_action,
335+
commands::script_commands::update_repo_run_action,
336+
commands::script_commands::delete_repo_run_action,
337+
commands::script_commands::reorder_repo_run_actions,
338+
commands::script_commands::set_workspace_active_run_action,
334339
commands::terminal_commands::spawn_terminal,
335340
commands::terminal_commands::stop_terminal,
336341
commands::terminal_commands::write_terminal_stdin,

0 commit comments

Comments
 (0)