ci fixes: cargo fmt, eslint entities, max-lines split
CI on 6ffbe97 failed on two auto-fixable gates. Both fixed:
* cargo fmt --all — rustfmt applied across the surface touched
by the last ~20 commits (world.rs, security_scan.rs,
routes/{missions,nodes,terminal}.rs, fleet_herdr.rs,
mission_workspace.rs, benchmark_runner.rs, mission_refiner.rs,
lib.rs, tests/mission_orchestrator.rs, cm-db/repo/{missions,teams}.rs,
bins/clawmates-node/src/main.rs)
* eslint apostrophe escapes in HerdrSessions + MissionWizard
* eslint max-lines: extracted EditMissionModal + RefineDiffModal
(each ~200 LoC) into their own files. MissionCanvas drops from
1424 to 1026, comfortably under both the 1250 eslint cap and the
1500 CI budget.
New files:
frontend/src/components/dashboard/EditMissionModal.tsx (211 LoC)
frontend/src/components/dashboard/RefineDiffModal.tsx (208 LoC)
Verified locally: cargo fmt --check clean, cargo check clean,
mission_orchestrator test 3/3 pass, tsc + eslint --quiet both silent.
This commit is contained in:
@@ -509,10 +509,7 @@ async fn handle_frame(
|
|||||||
// polls that pane's agent_status; `herdr_read` scrapes its recent
|
// polls that pane's agent_status; `herdr_read` scrapes its recent
|
||||||
// transcript. Node just shells out to the `herdr` binary — the
|
// transcript. Node just shells out to the `herdr` binary — the
|
||||||
// Herdr background daemon is expected to already be running.
|
// Herdr background daemon is expected to already be running.
|
||||||
op @ ("herdr_dispatch"
|
op @ ("herdr_dispatch" | "herdr_status" | "herdr_read" | "herdr_workspaces"
|
||||||
| "herdr_status"
|
|
||||||
| "herdr_read"
|
|
||||||
| "herdr_workspaces"
|
|
||||||
| "herdr_snapshot") => {
|
| "herdr_snapshot") => {
|
||||||
if let Some(id) = v.get("id").and_then(Value::as_u64) {
|
if let Some(id) = v.get("id").and_then(Value::as_u64) {
|
||||||
let (ok, output) = herdr_op(op, &v).await;
|
let (ok, output) = herdr_op(op, &v).await;
|
||||||
@@ -675,11 +672,16 @@ fn spawn_container_pty(
|
|||||||
/// specific agent container on this node.
|
/// specific agent container on this node.
|
||||||
pub(crate) enum PtyTarget {
|
pub(crate) enum PtyTarget {
|
||||||
Host,
|
Host,
|
||||||
Container { container: String, session: String },
|
Container {
|
||||||
|
container: String,
|
||||||
|
session: String,
|
||||||
|
},
|
||||||
/// Custom argv (Herdr Live Pane uses this to spawn `herdr` directly
|
/// Custom argv (Herdr Live Pane uses this to spawn `herdr` directly
|
||||||
/// so the browser xterm attaches straight into the node's Herdr TUI
|
/// so the browser xterm attaches straight into the node's Herdr TUI
|
||||||
/// instead of a login shell).
|
/// instead of a login shell).
|
||||||
Command { argv: Vec<String> },
|
Command {
|
||||||
|
argv: Vec<String>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PtyTarget {
|
impl PtyTarget {
|
||||||
@@ -1008,13 +1010,7 @@ async fn herdr_op(op: &str, v: &Value) -> (bool, String) {
|
|||||||
let escaped = prompt.replace('\'', "'\\''");
|
let escaped = prompt.replace('\'', "'\\''");
|
||||||
format!("{cli} '{escaped}'")
|
format!("{cli} '{escaped}'")
|
||||||
};
|
};
|
||||||
let (rok, rout) = run(vec![
|
let (rok, rout) = run(vec!["pane".into(), "run".into(), pane_id.clone(), launch]).await;
|
||||||
"pane".into(),
|
|
||||||
"run".into(),
|
|
||||||
pane_id.clone(),
|
|
||||||
launch,
|
|
||||||
])
|
|
||||||
.await;
|
|
||||||
let payload = serde_json::json!({
|
let payload = serde_json::json!({
|
||||||
"pane_id": pane_id,
|
"pane_id": pane_id,
|
||||||
"split": split_out,
|
"split": split_out,
|
||||||
|
|||||||
@@ -235,9 +235,7 @@ async fn exec_target(
|
|||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
mission_id: Uuid,
|
mission_id: Uuid,
|
||||||
) -> Result<(String, std::path::PathBuf), String> {
|
) -> Result<(String, std::path::PathBuf), String> {
|
||||||
let repo_id: Option<Uuid> = sqlx::query_scalar(
|
let repo_id: Option<Uuid> = sqlx::query_scalar("SELECT repo_id FROM missions WHERE id = $1")
|
||||||
"SELECT repo_id FROM missions WHERE id = $1",
|
|
||||||
)
|
|
||||||
.bind(mission_id)
|
.bind(mission_id)
|
||||||
.fetch_optional(pool)
|
.fetch_optional(pool)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -56,10 +56,17 @@ pub async fn dispatch(
|
|||||||
.call_timeout(node_id, "herdr_dispatch", args, 60)
|
.call_timeout(node_id, "herdr_dispatch", args, 60)
|
||||||
.await?;
|
.await?;
|
||||||
if !out.ok {
|
if !out.ok {
|
||||||
return Err(format!("node rejected dispatch: {}", truncate(&out.output, 400)));
|
return Err(format!(
|
||||||
|
"node rejected dispatch: {}",
|
||||||
|
truncate(&out.output, 400)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let payload: Value = serde_json::from_str(&out.output)
|
let payload: Value = serde_json::from_str(&out.output).map_err(|e| {
|
||||||
.map_err(|e| format!("dispatch payload not json: {e}: {}", truncate(&out.output, 200)))?;
|
format!(
|
||||||
|
"dispatch payload not json: {e}: {}",
|
||||||
|
truncate(&out.output, 200)
|
||||||
|
)
|
||||||
|
})?;
|
||||||
let pane_id = payload
|
let pane_id = payload
|
||||||
.get("pane_id")
|
.get("pane_id")
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
@@ -80,18 +87,9 @@ pub async fn dispatch(
|
|||||||
/// Read the current agent state of a pane. Returns raw pane.get JSON
|
/// Read the current agent state of a pane. Returns raw pane.get JSON
|
||||||
/// so the caller can inspect any field (agent, agent_status, cwd,
|
/// so the caller can inspect any field (agent, agent_status, cwd,
|
||||||
/// process metadata).
|
/// process metadata).
|
||||||
pub async fn status(
|
pub async fn status(hub: Arc<NodeHub>, node_id: NodeId, pane_id: &str) -> Result<Value, String> {
|
||||||
hub: Arc<NodeHub>,
|
|
||||||
node_id: NodeId,
|
|
||||||
pane_id: &str,
|
|
||||||
) -> Result<Value, String> {
|
|
||||||
let out = hub
|
let out = hub
|
||||||
.call_timeout(
|
.call_timeout(node_id, "herdr_status", json!({ "pane_id": pane_id }), 20)
|
||||||
node_id,
|
|
||||||
"herdr_status",
|
|
||||||
json!({ "pane_id": pane_id }),
|
|
||||||
20,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
if !out.ok {
|
if !out.ok {
|
||||||
return Err(format!("status failed: {}", truncate(&out.output, 300)));
|
return Err(format!("status failed: {}", truncate(&out.output, 300)));
|
||||||
@@ -103,10 +101,7 @@ pub async fn status(
|
|||||||
/// Fetch the full session snapshot from a node's Herdr daemon
|
/// Fetch the full session snapshot from a node's Herdr daemon
|
||||||
/// (`herdr api snapshot`). Returns raw JSON so the frontend can render
|
/// (`herdr api snapshot`). Returns raw JSON so the frontend can render
|
||||||
/// workspaces + tabs + panes + agent states without a schema hop.
|
/// workspaces + tabs + panes + agent states without a schema hop.
|
||||||
pub async fn snapshot(
|
pub async fn snapshot(hub: Arc<NodeHub>, node_id: NodeId) -> Result<Value, String> {
|
||||||
hub: Arc<NodeHub>,
|
|
||||||
node_id: NodeId,
|
|
||||||
) -> Result<Value, String> {
|
|
||||||
let out = hub
|
let out = hub
|
||||||
.call_timeout(node_id, "herdr_snapshot", json!({}), 15)
|
.call_timeout(node_id, "herdr_snapshot", json!({}), 15)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -156,7 +151,9 @@ pub async fn wait_for_completion(
|
|||||||
let mut ever_working = false;
|
let mut ever_working = false;
|
||||||
loop {
|
loop {
|
||||||
if started.elapsed() > Duration::from_secs(timeout_secs) {
|
if started.elapsed() > Duration::from_secs(timeout_secs) {
|
||||||
return Err(format!("pane {pane_id} did not complete in {timeout_secs}s"));
|
return Err(format!(
|
||||||
|
"pane {pane_id} did not complete in {timeout_secs}s"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let s = status(hub.clone(), node_id, pane_id).await?;
|
let s = status(hub.clone(), node_id, pane_id).await?;
|
||||||
let state = s
|
let state = s
|
||||||
|
|||||||
@@ -450,10 +450,7 @@ pub fn router(state: AppState) -> Router {
|
|||||||
"/api/missions/{id}/status",
|
"/api/missions/{id}/status",
|
||||||
axum::routing::patch(routes::missions::set_status),
|
axum::routing::patch(routes::missions::set_status),
|
||||||
)
|
)
|
||||||
.route(
|
.route("/api/missions/{id}/refine", post(routes::missions::refine))
|
||||||
"/api/missions/{id}/refine",
|
|
||||||
post(routes::missions::refine),
|
|
||||||
)
|
|
||||||
.route(
|
.route(
|
||||||
"/api/missions/{id}/herdr-dispatch",
|
"/api/missions/{id}/herdr-dispatch",
|
||||||
post(routes::missions::herdr_dispatch),
|
post(routes::missions::herdr_dispatch),
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ pub async fn refine(
|
|||||||
.map_err(|e| format!("load mission: {e}"))?
|
.map_err(|e| format!("load mission: {e}"))?
|
||||||
.ok_or_else(|| "mission not found".to_string())?;
|
.ok_or_else(|| "mission not found".to_string())?;
|
||||||
if mission.status != "draft" {
|
if mission.status != "draft" {
|
||||||
return Err(format!("mission is {}, refine only allowed on draft", mission.status));
|
return Err(format!(
|
||||||
|
"mission is {}, refine only allowed on draft",
|
||||||
|
mission.status
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let raw = mission.description.unwrap_or_default();
|
let raw = mission.description.unwrap_or_default();
|
||||||
if raw.trim().is_empty() {
|
if raw.trim().is_empty() {
|
||||||
@@ -50,7 +53,10 @@ pub async fn refine(
|
|||||||
|
|
||||||
let refined = call_gemini(&mission.title, &mission.template_kind, &phase_kinds, &raw).await?;
|
let refined = call_gemini(&mission.title, &mission.template_kind, &phase_kinds, &raw).await?;
|
||||||
|
|
||||||
Ok(RefineResult { original: raw, refined })
|
Ok(RefineResult {
|
||||||
|
original: raw,
|
||||||
|
refined,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn call_gemini(
|
async fn call_gemini(
|
||||||
|
|||||||
@@ -74,13 +74,7 @@ pub async fn ensure_checkout(
|
|||||||
|
|
||||||
async fn clone(path: &std::path::Path, url: &str) -> Result<(), String> {
|
async fn clone(path: &std::path::Path, url: &str) -> Result<(), String> {
|
||||||
let out = Command::new("git")
|
let out = Command::new("git")
|
||||||
.args([
|
.args(["clone", "--depth", "1", url, &path.display().to_string()])
|
||||||
"clone",
|
|
||||||
"--depth",
|
|
||||||
"1",
|
|
||||||
url,
|
|
||||||
&path.display().to_string(),
|
|
||||||
])
|
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("spawn git clone: {e}"))?;
|
.map_err(|e| format!("spawn git clone: {e}"))?;
|
||||||
@@ -99,7 +93,15 @@ async fn clone(path: &std::path::Path, url: &str) -> Result<(), String> {
|
|||||||
|
|
||||||
async fn fetch_and_reset(path: &std::path::Path, branch: &str) -> Result<(), String> {
|
async fn fetch_and_reset(path: &std::path::Path, branch: &str) -> Result<(), String> {
|
||||||
let fetch = Command::new("git")
|
let fetch = Command::new("git")
|
||||||
.args(["-C", &path.display().to_string(), "fetch", "--depth", "1", "origin", branch])
|
.args([
|
||||||
|
"-C",
|
||||||
|
&path.display().to_string(),
|
||||||
|
"fetch",
|
||||||
|
"--depth",
|
||||||
|
"1",
|
||||||
|
"origin",
|
||||||
|
branch,
|
||||||
|
])
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("spawn git fetch: {e}"))?;
|
.map_err(|e| format!("spawn git fetch: {e}"))?;
|
||||||
|
|||||||
@@ -431,8 +431,7 @@ pub async fn set_status(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if prior.status == "draft" && body.status == "running" {
|
if prior.status == "draft" && body.status == "running" {
|
||||||
if let Err(e) =
|
if let Err(e) = crate::mission_orchestrator::on_launch(
|
||||||
crate::mission_orchestrator::on_launch(
|
|
||||||
&state.pool,
|
&state.pool,
|
||||||
user.workspace_id,
|
user.workspace_id,
|
||||||
user.user_id,
|
user.user_id,
|
||||||
|
|||||||
@@ -364,7 +364,8 @@ async fn bridge_terminal(hub: Arc<NodeHub>, node_id: NodeId, socket: WebSocket)
|
|||||||
} else {
|
} else {
|
||||||
Some(c.command.as_slice())
|
Some(c.command.as_slice())
|
||||||
};
|
};
|
||||||
hub.open_pty(node_id, sid, cols, rows, None, None, cmd).await
|
hub.open_pty(node_id, sid, cols, rows, None, None, cmd)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
"webrtc_offer" => {
|
"webrtc_offer" => {
|
||||||
hub.webrtc_offer(
|
hub.webrtc_offer(
|
||||||
|
|||||||
@@ -402,7 +402,15 @@ async fn bridge_node(
|
|||||||
match c.kind.as_str() {
|
match c.kind.as_str() {
|
||||||
"resize" => hub.terminal_resize(node_id, sid, cols, rows).await,
|
"resize" => hub.terminal_resize(node_id, sid, cols, rows).await,
|
||||||
"fallback" => {
|
"fallback" => {
|
||||||
hub.open_pty(node_id, sid, cols, rows, Some(&container), Some(&session), None)
|
hub.open_pty(
|
||||||
|
node_id,
|
||||||
|
sid,
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
Some(&container),
|
||||||
|
Some(&session),
|
||||||
|
None,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
"webrtc_offer" => {
|
"webrtc_offer" => {
|
||||||
|
|||||||
@@ -586,4 +586,3 @@ pub async fn world_replay(
|
|||||||
json!({ "events": events, "hours": hours, "count": rows.len() }),
|
json!({ "events": events, "hours": hours, "count": rows.len() }),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,10 @@ pub async fn run(pool: &PgPool, mission_id: Uuid, phase_id: Uuid) -> Result<usiz
|
|||||||
|
|
||||||
// ── Per-tool runners ────────────────────────────────────────────
|
// ── Per-tool runners ────────────────────────────────────────────
|
||||||
|
|
||||||
async fn run_cargo_audit(container: &str, workdir: &std::path::Path) -> Result<Vec<Finding>, String> {
|
async fn run_cargo_audit(
|
||||||
|
container: &str,
|
||||||
|
workdir: &std::path::Path,
|
||||||
|
) -> Result<Vec<Finding>, String> {
|
||||||
let out = docker_exec_json(
|
let out = docker_exec_json(
|
||||||
container,
|
container,
|
||||||
workdir,
|
workdir,
|
||||||
@@ -293,9 +296,7 @@ async fn load_phase_config(pool: &PgPool, phase_id: Uuid) -> Result<Value, Strin
|
|||||||
/// scanners need a source tree. Returning a clear error surfaces
|
/// scanners need a source tree. Returning a clear error surfaces
|
||||||
/// that gap instead of silently reporting zero findings.
|
/// that gap instead of silently reporting zero findings.
|
||||||
async fn exec_target(pool: &PgPool, mission_id: Uuid) -> Result<(String, PathBuf), String> {
|
async fn exec_target(pool: &PgPool, mission_id: Uuid) -> Result<(String, PathBuf), String> {
|
||||||
let repo_id: Option<Uuid> = sqlx::query_scalar(
|
let repo_id: Option<Uuid> = sqlx::query_scalar("SELECT repo_id FROM missions WHERE id = $1")
|
||||||
"SELECT repo_id FROM missions WHERE id = $1",
|
|
||||||
)
|
|
||||||
.bind(mission_id)
|
.bind(mission_id)
|
||||||
.fetch_optional(pool)
|
.fetch_optional(pool)
|
||||||
.await
|
.await
|
||||||
@@ -311,7 +312,9 @@ async fn exec_target(pool: &PgPool, mission_id: Uuid) -> Result<(String, PathBuf
|
|||||||
.unwrap_or_else(|_| "clawmates-runtime".to_string());
|
.unwrap_or_else(|_| "clawmates-runtime".to_string());
|
||||||
let root = std::env::var("CLAWMATES_MISSIONS_ROOT")
|
let root = std::env::var("CLAWMATES_MISSIONS_ROOT")
|
||||||
.unwrap_or_else(|_| "/var/lib/clawmates-missions".to_string());
|
.unwrap_or_else(|_| "/var/lib/clawmates-missions".to_string());
|
||||||
let workdir = PathBuf::from(root).join(mission_id.to_string()).join("repo");
|
let workdir = PathBuf::from(root)
|
||||||
|
.join(mission_id.to_string())
|
||||||
|
.join("repo");
|
||||||
Ok((container, workdir))
|
Ok((container, workdir))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,7 +338,11 @@ async fn docker_exec_raw(
|
|||||||
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
|
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn docker_exec_json(container: &str, workdir: &std::path::Path, cmd: &[String]) -> Result<Value, String> {
|
async fn docker_exec_json(
|
||||||
|
container: &str,
|
||||||
|
workdir: &std::path::Path,
|
||||||
|
cmd: &[String],
|
||||||
|
) -> Result<Value, String> {
|
||||||
let raw = docker_exec_raw(container, workdir, cmd).await?;
|
let raw = docker_exec_raw(container, workdir, cmd).await?;
|
||||||
let trimmed = raw.trim();
|
let trimmed = raw.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
@@ -353,4 +360,3 @@ fn static_tool_name(s: &str) -> &'static str {
|
|||||||
_ => "unknown",
|
_ => "unknown",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -97,10 +97,7 @@ async fn reap_stuck_runs(pool: &PgPool) -> Result<(), sqlx::Error> {
|
|||||||
let _ = cm_db::repo::topology_runs::fail(
|
let _ = cm_db::repo::topology_runs::fail(
|
||||||
pool,
|
pool,
|
||||||
id,
|
id,
|
||||||
&format!(
|
&format!("reaped: no step records after {}s", REAP_STUCK_AFTER_SECS),
|
||||||
"reaped: no step records after {}s",
|
|
||||||
REAP_STUCK_AFTER_SECS
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
@@ -312,4 +309,3 @@ async fn drive<E: TurnExecutor>(
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -144,9 +144,8 @@ async fn on_launch_materializes_team_from_template() {
|
|||||||
assert_eq!(stamped_risk, "medium");
|
assert_eq!(stamped_risk, "medium");
|
||||||
|
|
||||||
// One agent per role — three total.
|
// One agent per role — three total.
|
||||||
let agent_count: i64 = sqlx::query_scalar(
|
let agent_count: i64 =
|
||||||
"SELECT count(*)::bigint FROM team_members WHERE team_id = $1",
|
sqlx::query_scalar("SELECT count(*)::bigint FROM team_members WHERE team_id = $1")
|
||||||
)
|
|
||||||
.bind(team_id)
|
.bind(team_id)
|
||||||
.fetch_one(&pool)
|
.fetch_one(&pool)
|
||||||
.await
|
.await
|
||||||
@@ -178,12 +177,13 @@ async fn on_launch_materializes_team_from_template() {
|
|||||||
.fetch_one(&pool)
|
.fetch_one(&pool)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(seeded_count, 3, "expected every claw to have seeded lineage");
|
assert_eq!(
|
||||||
|
seeded_count, 3,
|
||||||
|
"expected every claw to have seeded lineage"
|
||||||
|
);
|
||||||
|
|
||||||
// Mission row was updated to point at the new team.
|
// Mission row was updated to point at the new team.
|
||||||
let bound_team_id: Uuid = sqlx::query_scalar(
|
let bound_team_id: Uuid = sqlx::query_scalar("SELECT team_id FROM missions WHERE id = $1")
|
||||||
"SELECT team_id FROM missions WHERE id = $1",
|
|
||||||
)
|
|
||||||
.bind(mission_id)
|
.bind(mission_id)
|
||||||
.fetch_one(&pool)
|
.fetch_one(&pool)
|
||||||
.await
|
.await
|
||||||
@@ -207,12 +207,14 @@ async fn on_launch_is_idempotent() {
|
|||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(team_a, team_b, "second invocation should return the same team_id");
|
assert_eq!(
|
||||||
|
team_a, team_b,
|
||||||
|
"second invocation should return the same team_id"
|
||||||
|
);
|
||||||
|
|
||||||
// Still exactly three agents — no duplication.
|
// Still exactly three agents — no duplication.
|
||||||
let agent_count: i64 = sqlx::query_scalar(
|
let agent_count: i64 =
|
||||||
"SELECT count(*)::bigint FROM team_members WHERE team_id = $1",
|
sqlx::query_scalar("SELECT count(*)::bigint FROM team_members WHERE team_id = $1")
|
||||||
)
|
|
||||||
.bind(team_a)
|
.bind(team_a)
|
||||||
.fetch_one(&pool)
|
.fetch_one(&pool)
|
||||||
.await
|
.await
|
||||||
@@ -246,9 +248,7 @@ async fn on_launch_no_template_returns_none() {
|
|||||||
assert!(result.is_none(), "no template + no team should return None");
|
assert!(result.is_none(), "no template + no team should return None");
|
||||||
|
|
||||||
// Mission stays with team_id NULL.
|
// Mission stays with team_id NULL.
|
||||||
let team_id: Option<Uuid> = sqlx::query_scalar(
|
let team_id: Option<Uuid> = sqlx::query_scalar("SELECT team_id FROM missions WHERE id = $1")
|
||||||
"SELECT team_id FROM missions WHERE id = $1",
|
|
||||||
)
|
|
||||||
.bind(mission_id)
|
.bind(mission_id)
|
||||||
.fetch_one(&pool)
|
.fetch_one(&pool)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -297,11 +297,7 @@ pub async fn update_meta(
|
|||||||
/// Hard-delete a mission. Cascades via FKs on mission_phases /
|
/// Hard-delete a mission. Cascades via FKs on mission_phases /
|
||||||
/// mission_tasks / mission_artifacts / benchmark_snapshots (all
|
/// mission_tasks / mission_artifacts / benchmark_snapshots (all
|
||||||
/// declared ON DELETE CASCADE in 0047).
|
/// declared ON DELETE CASCADE in 0047).
|
||||||
pub async fn delete(
|
pub async fn delete(pool: &PgPool, id: Uuid, workspace_id: Uuid) -> Result<u64, DbError> {
|
||||||
pool: &PgPool,
|
|
||||||
id: Uuid,
|
|
||||||
workspace_id: Uuid,
|
|
||||||
) -> Result<u64, DbError> {
|
|
||||||
let r = sqlx::query("DELETE FROM missions WHERE id = $1 AND workspace_id = $2")
|
let r = sqlx::query("DELETE FROM missions WHERE id = $1 AND workspace_id = $2")
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.bind(workspace_id)
|
.bind(workspace_id)
|
||||||
|
|||||||
@@ -317,4 +317,3 @@ pub async fn set_team_runtime_config(
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
// Modal for editing an in-flight mission's title + description. Only
|
||||||
|
// available when the mission is in 'draft' status (backend enforces
|
||||||
|
// too; the button is hidden past draft in MissionCanvas).
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { updateMission, type MissionDetail } from "@/lib/api/missions";
|
||||||
|
|
||||||
|
const mono =
|
||||||
|
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace";
|
||||||
|
|
||||||
|
export function EditMissionModal({
|
||||||
|
mission,
|
||||||
|
onClose,
|
||||||
|
onSaved,
|
||||||
|
}: {
|
||||||
|
mission: MissionDetail;
|
||||||
|
onClose: () => void;
|
||||||
|
onSaved: () => void;
|
||||||
|
}) {
|
||||||
|
const [title, setTitle] = useState(mission.title);
|
||||||
|
const [description, setDescription] = useState(mission.description ?? "");
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const dirty =
|
||||||
|
title.trim() !== mission.title || description !== (mission.description ?? "");
|
||||||
|
const save = async () => {
|
||||||
|
if (!dirty) {
|
||||||
|
onClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!title.trim()) {
|
||||||
|
setError("title is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setBusy(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await updateMission(mission.id, {
|
||||||
|
title: title.trim() !== mission.title ? title.trim() : undefined,
|
||||||
|
description:
|
||||||
|
description !== (mission.description ?? "") ? description : undefined,
|
||||||
|
});
|
||||||
|
onSaved();
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : "save failed");
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal
|
||||||
|
onClick={onClose}
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
inset: 0,
|
||||||
|
background: "rgba(0,0,0,.55)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
zIndex: 900,
|
||||||
|
padding: 24,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
width: "min(620px, 100%)",
|
||||||
|
background: "#141419",
|
||||||
|
borderRadius: 12,
|
||||||
|
border: "1px solid rgba(255,255,255,.08)",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "12px 18px",
|
||||||
|
borderBottom: "1px solid rgba(255,255,255,.06)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: mono,
|
||||||
|
fontSize: 10.5,
|
||||||
|
letterSpacing: ".14em",
|
||||||
|
color: "#7cd6e0",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit mission
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ padding: 18, display: "flex", flexDirection: "column", gap: 12 }}>
|
||||||
|
<label style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: mono,
|
||||||
|
fontSize: 10,
|
||||||
|
letterSpacing: ".12em",
|
||||||
|
color: "#a0a0a8",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Title
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
disabled={busy}
|
||||||
|
autoFocus
|
||||||
|
style={{
|
||||||
|
padding: "8px 12px",
|
||||||
|
borderRadius: 8,
|
||||||
|
border: "1px solid rgba(255,255,255,.1)",
|
||||||
|
background: "#0a0a0d",
|
||||||
|
color: "#f3f3f5",
|
||||||
|
fontSize: 14,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: mono,
|
||||||
|
fontSize: 10,
|
||||||
|
letterSpacing: ".12em",
|
||||||
|
color: "#a0a0a8",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Description (Markdown; use Refine to structure)
|
||||||
|
</span>
|
||||||
|
<textarea
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
disabled={busy}
|
||||||
|
rows={12}
|
||||||
|
style={{
|
||||||
|
padding: "8px 12px",
|
||||||
|
borderRadius: 8,
|
||||||
|
border: "1px solid rgba(255,255,255,.1)",
|
||||||
|
background: "#0a0a0d",
|
||||||
|
color: "#f3f3f5",
|
||||||
|
fontFamily: mono,
|
||||||
|
fontSize: 12,
|
||||||
|
resize: "vertical",
|
||||||
|
minHeight: 160,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
{error && (
|
||||||
|
<div style={{ color: "#ff8a7a", fontSize: 12 }}>{error}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "12px 18px",
|
||||||
|
borderTop: "1px solid rgba(255,255,255,.06)",
|
||||||
|
display: "flex",
|
||||||
|
gap: 8,
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={busy}
|
||||||
|
style={{
|
||||||
|
padding: "6px 14px",
|
||||||
|
borderRadius: 8,
|
||||||
|
border: "1px solid rgba(255,255,255,.1)",
|
||||||
|
background: "transparent",
|
||||||
|
color: "#a0a0a8",
|
||||||
|
fontSize: 12,
|
||||||
|
cursor: "pointer",
|
||||||
|
opacity: busy ? 0.5 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={save}
|
||||||
|
disabled={busy || !dirty}
|
||||||
|
style={{
|
||||||
|
padding: "6px 14px",
|
||||||
|
borderRadius: 8,
|
||||||
|
border: "1px solid rgba(127,208,160,.5)",
|
||||||
|
background: "rgba(127,208,160,.12)",
|
||||||
|
color: "#7fd0a0",
|
||||||
|
fontSize: 12,
|
||||||
|
cursor: dirty ? "pointer" : "not-allowed",
|
||||||
|
opacity: busy || !dirty ? 0.5 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{busy ? "Saving…" : "Save"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -147,7 +147,7 @@ export function HerdrSessions() {
|
|||||||
<p style={{ fontSize: 13, color: "#8a8a92", margin: "6px 0 22px", lineHeight: 1.5 }}>
|
<p style={{ fontSize: 13, color: "#8a8a92", margin: "6px 0 22px", lineHeight: 1.5 }}>
|
||||||
Every online fleet node is running a persistent Herdr daemon. This view
|
Every online fleet node is running a persistent Herdr daemon. This view
|
||||||
shows their live workspaces + agent states. Click Open to render the
|
shows their live workspaces + agent states. Click Open to render the
|
||||||
node's Herdr TUI in your browser (WebRTC direct where possible).
|
node's Herdr TUI in your browser (WebRTC direct where possible).
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))", gap: 14 }}>
|
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))", gap: 14 }}>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
setMissionStatus,
|
setMissionStatus,
|
||||||
triggerBenchmark,
|
triggerBenchmark,
|
||||||
triggerSecurityScan,
|
triggerSecurityScan,
|
||||||
updateMission,
|
|
||||||
type MissionDetail,
|
type MissionDetail,
|
||||||
type MissionStatus,
|
type MissionStatus,
|
||||||
type PhaseKind,
|
type PhaseKind,
|
||||||
@@ -28,9 +27,11 @@ import {
|
|||||||
type TaskStatus,
|
type TaskStatus,
|
||||||
type TemplateKind,
|
type TemplateKind,
|
||||||
} from "@/lib/api/missions";
|
} from "@/lib/api/missions";
|
||||||
|
import { EditMissionModal } from "./EditMissionModal";
|
||||||
import { MarkdownBlock } from "./MarkdownBlock";
|
import { MarkdownBlock } from "./MarkdownBlock";
|
||||||
import { MissionLivePane } from "./MissionLivePane";
|
import { MissionLivePane } from "./MissionLivePane";
|
||||||
import { MissionWizard } from "./MissionWizard";
|
import { MissionWizard } from "./MissionWizard";
|
||||||
|
import { RefineDiffModal } from "./RefineDiffModal";
|
||||||
|
|
||||||
const mono =
|
const mono =
|
||||||
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace";
|
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace";
|
||||||
@@ -988,405 +989,6 @@ function Empty({ label }: { label: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function EditMissionModal({
|
|
||||||
mission,
|
|
||||||
onClose,
|
|
||||||
onSaved,
|
|
||||||
}: {
|
|
||||||
mission: MissionDetail;
|
|
||||||
onClose: () => void;
|
|
||||||
onSaved: () => void;
|
|
||||||
}) {
|
|
||||||
const [title, setTitle] = useState(mission.title);
|
|
||||||
const [description, setDescription] = useState(mission.description ?? "");
|
|
||||||
const [busy, setBusy] = useState(false);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const dirty =
|
|
||||||
title.trim() !== mission.title || description !== (mission.description ?? "");
|
|
||||||
const save = async () => {
|
|
||||||
if (!dirty) {
|
|
||||||
onClose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!title.trim()) {
|
|
||||||
setError("title is required");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setBusy(true);
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
await updateMission(mission.id, {
|
|
||||||
title: title.trim() !== mission.title ? title.trim() : undefined,
|
|
||||||
description:
|
|
||||||
description !== (mission.description ?? "") ? description : undefined,
|
|
||||||
});
|
|
||||||
onSaved();
|
|
||||||
} catch (e) {
|
|
||||||
setError(e instanceof Error ? e.message : "save failed");
|
|
||||||
} finally {
|
|
||||||
setBusy(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="dialog"
|
|
||||||
aria-modal
|
|
||||||
onClick={onClose}
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
inset: 0,
|
|
||||||
background: "rgba(0,0,0,.55)",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
zIndex: 900,
|
|
||||||
padding: 24,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
style={{
|
|
||||||
width: "min(620px, 100%)",
|
|
||||||
background: "#141419",
|
|
||||||
borderRadius: 12,
|
|
||||||
border: "1px solid rgba(255,255,255,.08)",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: "12px 18px",
|
|
||||||
borderBottom: "1px solid rgba(255,255,255,.06)",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 10,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontFamily: mono,
|
|
||||||
fontSize: 10.5,
|
|
||||||
letterSpacing: ".14em",
|
|
||||||
color: "#7cd6e0",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Edit mission
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div style={{ padding: 18, display: "flex", flexDirection: "column", gap: 12 }}>
|
|
||||||
<label style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontFamily: mono,
|
|
||||||
fontSize: 10,
|
|
||||||
letterSpacing: ".12em",
|
|
||||||
color: "#a0a0a8",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Title
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={title}
|
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
|
||||||
disabled={busy}
|
|
||||||
autoFocus
|
|
||||||
style={{
|
|
||||||
padding: "8px 12px",
|
|
||||||
borderRadius: 8,
|
|
||||||
border: "1px solid rgba(255,255,255,.1)",
|
|
||||||
background: "#0a0a0d",
|
|
||||||
color: "#f3f3f5",
|
|
||||||
fontSize: 14,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontFamily: mono,
|
|
||||||
fontSize: 10,
|
|
||||||
letterSpacing: ".12em",
|
|
||||||
color: "#a0a0a8",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Description (Markdown; use Refine to structure)
|
|
||||||
</span>
|
|
||||||
<textarea
|
|
||||||
value={description}
|
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
|
||||||
disabled={busy}
|
|
||||||
rows={12}
|
|
||||||
style={{
|
|
||||||
padding: "8px 12px",
|
|
||||||
borderRadius: 8,
|
|
||||||
border: "1px solid rgba(255,255,255,.1)",
|
|
||||||
background: "#0a0a0d",
|
|
||||||
color: "#f3f3f5",
|
|
||||||
fontFamily: mono,
|
|
||||||
fontSize: 12,
|
|
||||||
resize: "vertical",
|
|
||||||
minHeight: 160,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
{error && (
|
|
||||||
<div style={{ color: "#ff8a7a", fontSize: 12 }}>{error}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: "12px 18px",
|
|
||||||
borderTop: "1px solid rgba(255,255,255,.06)",
|
|
||||||
display: "flex",
|
|
||||||
gap: 8,
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClose}
|
|
||||||
disabled={busy}
|
|
||||||
style={{
|
|
||||||
padding: "6px 14px",
|
|
||||||
borderRadius: 8,
|
|
||||||
border: "1px solid rgba(255,255,255,.1)",
|
|
||||||
background: "transparent",
|
|
||||||
color: "#a0a0a8",
|
|
||||||
fontSize: 12,
|
|
||||||
cursor: "pointer",
|
|
||||||
opacity: busy ? 0.5 : 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={save}
|
|
||||||
disabled={busy || !dirty}
|
|
||||||
style={{
|
|
||||||
padding: "6px 14px",
|
|
||||||
borderRadius: 8,
|
|
||||||
border: "1px solid rgba(127,208,160,.5)",
|
|
||||||
background: "rgba(127,208,160,.12)",
|
|
||||||
color: "#7fd0a0",
|
|
||||||
fontSize: 12,
|
|
||||||
cursor: dirty ? "pointer" : "not-allowed",
|
|
||||||
opacity: busy || !dirty ? 0.5 : 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{busy ? "Saving…" : "Save"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function RefineDiffModal({
|
|
||||||
original,
|
|
||||||
refined,
|
|
||||||
busy,
|
|
||||||
onAccept,
|
|
||||||
onCancel,
|
|
||||||
onUndoAfterAccept,
|
|
||||||
}: {
|
|
||||||
original: string;
|
|
||||||
refined: string;
|
|
||||||
busy: boolean;
|
|
||||||
onAccept: () => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
onUndoAfterAccept: () => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
role="dialog"
|
|
||||||
aria-modal
|
|
||||||
onClick={onCancel}
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
inset: 0,
|
|
||||||
background: "rgba(0,0,0,.6)",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
zIndex: 900,
|
|
||||||
padding: 24,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
style={{
|
|
||||||
width: "min(1100px, 100%)",
|
|
||||||
height: "min(720px, calc(100vh - 48px))",
|
|
||||||
background: "#141419",
|
|
||||||
borderRadius: 12,
|
|
||||||
border: "1px solid rgba(255,255,255,.08)",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: "12px 18px",
|
|
||||||
borderBottom: "1px solid rgba(255,255,255,.06)",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 10,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontFamily: mono,
|
|
||||||
fontSize: 10.5,
|
|
||||||
letterSpacing: ".14em",
|
|
||||||
color: "#ffb44a",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Refine — review before/after
|
|
||||||
</span>
|
|
||||||
<span style={{ flex: 1 }} />
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onUndoAfterAccept}
|
|
||||||
disabled={busy}
|
|
||||||
title="Restore the original description (drops the refinement)"
|
|
||||||
style={{
|
|
||||||
padding: "5px 12px",
|
|
||||||
borderRadius: 8,
|
|
||||||
border: "1px solid rgba(255,255,255,.1)",
|
|
||||||
background: "transparent",
|
|
||||||
color: "#a0a0a8",
|
|
||||||
fontSize: 12,
|
|
||||||
cursor: "pointer",
|
|
||||||
opacity: busy ? 0.5 : 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Restore original
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onCancel}
|
|
||||||
disabled={busy}
|
|
||||||
style={{
|
|
||||||
padding: "5px 12px",
|
|
||||||
borderRadius: 8,
|
|
||||||
border: "1px solid rgba(255,255,255,.1)",
|
|
||||||
background: "transparent",
|
|
||||||
color: "#a0a0a8",
|
|
||||||
fontSize: 12,
|
|
||||||
cursor: "pointer",
|
|
||||||
opacity: busy ? 0.5 : 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onAccept}
|
|
||||||
disabled={busy}
|
|
||||||
style={{
|
|
||||||
padding: "5px 14px",
|
|
||||||
borderRadius: 8,
|
|
||||||
border: "1px solid rgba(127,208,160,.5)",
|
|
||||||
background: "rgba(127,208,160,.12)",
|
|
||||||
color: "#7fd0a0",
|
|
||||||
fontSize: 12,
|
|
||||||
cursor: "pointer",
|
|
||||||
opacity: busy ? 0.5 : 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{busy ? "Applying…" : "Accept"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
minHeight: 0,
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr 1fr",
|
|
||||||
gap: 1,
|
|
||||||
background: "rgba(255,255,255,.06)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DiffPane
|
|
||||||
title="Before"
|
|
||||||
color="#8a8a92"
|
|
||||||
body={original}
|
|
||||||
renderAsMarkdown={false}
|
|
||||||
/>
|
|
||||||
<DiffPane
|
|
||||||
title="After"
|
|
||||||
color="#7fd0a0"
|
|
||||||
body={refined}
|
|
||||||
renderAsMarkdown
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DiffPane({
|
|
||||||
title,
|
|
||||||
color,
|
|
||||||
body,
|
|
||||||
renderAsMarkdown,
|
|
||||||
}: {
|
|
||||||
title: string;
|
|
||||||
color: string;
|
|
||||||
body: string;
|
|
||||||
renderAsMarkdown: boolean;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
background: "#0e0e12",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
minHeight: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: "8px 14px",
|
|
||||||
borderBottom: "1px solid rgba(255,255,255,.05)",
|
|
||||||
fontFamily: mono,
|
|
||||||
fontSize: 10,
|
|
||||||
letterSpacing: ".14em",
|
|
||||||
color,
|
|
||||||
textTransform: "uppercase",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
<div style={{ flex: 1, minHeight: 0, overflow: "auto", padding: 14 }}>
|
|
||||||
{renderAsMarkdown ? (
|
|
||||||
<MarkdownBlock source={body} />
|
|
||||||
) : (
|
|
||||||
<pre
|
|
||||||
style={{
|
|
||||||
margin: 0,
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
wordBreak: "break-word",
|
|
||||||
fontFamily: mono,
|
|
||||||
fontSize: 12,
|
|
||||||
color: "#cfcfd5",
|
|
||||||
lineHeight: 1.55,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{body}
|
|
||||||
</pre>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const iconBtn: React.CSSProperties = {
|
const iconBtn: React.CSSProperties = {
|
||||||
width: 30,
|
width: 30,
|
||||||
|
|||||||
@@ -431,7 +431,7 @@ export function MissionWizard({
|
|||||||
</div>
|
</div>
|
||||||
<div style={hintStyle}>
|
<div style={hintStyle}>
|
||||||
Executes in a Herdr pane on a fleet node, using that
|
Executes in a Herdr pane on a fleet node, using that
|
||||||
node's local CLI (claude / kimi / codex). Operator-visible,
|
node's local CLI (claude / kimi / codex). Operator-visible,
|
||||||
live pane view in the mission canvas.
|
live pane view in the mission canvas.
|
||||||
</div>
|
</div>
|
||||||
{runtimeKind === "local_herdr" && (
|
{runtimeKind === "local_herdr" && (
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
// Diff modal for the mission Refine flow. Shows the original
|
||||||
|
// description (raw) side-by-side with the LLM-refined version
|
||||||
|
// (rendered as Markdown). Accept / Cancel / Restore-original.
|
||||||
|
|
||||||
|
import { MarkdownBlock } from "./MarkdownBlock";
|
||||||
|
|
||||||
|
const mono =
|
||||||
|
"ui-monospace, SFMono-Regular, SF Mono, Menlo, Monaco, Consolas, monospace";
|
||||||
|
|
||||||
|
export function RefineDiffModal({
|
||||||
|
original,
|
||||||
|
refined,
|
||||||
|
busy,
|
||||||
|
onAccept,
|
||||||
|
onCancel,
|
||||||
|
onUndoAfterAccept,
|
||||||
|
}: {
|
||||||
|
original: string;
|
||||||
|
refined: string;
|
||||||
|
busy: boolean;
|
||||||
|
onAccept: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
onUndoAfterAccept: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="dialog"
|
||||||
|
aria-modal
|
||||||
|
onClick={onCancel}
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
inset: 0,
|
||||||
|
background: "rgba(0,0,0,.6)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
zIndex: 900,
|
||||||
|
padding: 24,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
style={{
|
||||||
|
width: "min(1100px, 100%)",
|
||||||
|
height: "min(720px, calc(100vh - 48px))",
|
||||||
|
background: "#141419",
|
||||||
|
borderRadius: 12,
|
||||||
|
border: "1px solid rgba(255,255,255,.08)",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "12px 18px",
|
||||||
|
borderBottom: "1px solid rgba(255,255,255,.06)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontFamily: mono,
|
||||||
|
fontSize: 10.5,
|
||||||
|
letterSpacing: ".14em",
|
||||||
|
color: "#ffb44a",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Refine — review before/after
|
||||||
|
</span>
|
||||||
|
<span style={{ flex: 1 }} />
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onUndoAfterAccept}
|
||||||
|
disabled={busy}
|
||||||
|
title="Restore the original description (drops the refinement)"
|
||||||
|
style={{
|
||||||
|
padding: "5px 12px",
|
||||||
|
borderRadius: 8,
|
||||||
|
border: "1px solid rgba(255,255,255,.1)",
|
||||||
|
background: "transparent",
|
||||||
|
color: "#a0a0a8",
|
||||||
|
fontSize: 12,
|
||||||
|
cursor: "pointer",
|
||||||
|
opacity: busy ? 0.5 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Restore original
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={busy}
|
||||||
|
style={{
|
||||||
|
padding: "5px 12px",
|
||||||
|
borderRadius: 8,
|
||||||
|
border: "1px solid rgba(255,255,255,.1)",
|
||||||
|
background: "transparent",
|
||||||
|
color: "#a0a0a8",
|
||||||
|
fontSize: 12,
|
||||||
|
cursor: "pointer",
|
||||||
|
opacity: busy ? 0.5 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onAccept}
|
||||||
|
disabled={busy}
|
||||||
|
style={{
|
||||||
|
padding: "5px 14px",
|
||||||
|
borderRadius: 8,
|
||||||
|
border: "1px solid rgba(127,208,160,.5)",
|
||||||
|
background: "rgba(127,208,160,.12)",
|
||||||
|
color: "#7fd0a0",
|
||||||
|
fontSize: 12,
|
||||||
|
cursor: "pointer",
|
||||||
|
opacity: busy ? 0.5 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{busy ? "Applying…" : "Accept"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 0,
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "1fr 1fr",
|
||||||
|
gap: 1,
|
||||||
|
background: "rgba(255,255,255,.06)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DiffPane
|
||||||
|
title="Before"
|
||||||
|
color="#8a8a92"
|
||||||
|
body={original}
|
||||||
|
renderAsMarkdown={false}
|
||||||
|
/>
|
||||||
|
<DiffPane
|
||||||
|
title="After"
|
||||||
|
color="#7fd0a0"
|
||||||
|
body={refined}
|
||||||
|
renderAsMarkdown
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DiffPane({
|
||||||
|
title,
|
||||||
|
color,
|
||||||
|
body,
|
||||||
|
renderAsMarkdown,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
color: string;
|
||||||
|
body: string;
|
||||||
|
renderAsMarkdown: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "#0e0e12",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
minHeight: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "8px 14px",
|
||||||
|
borderBottom: "1px solid rgba(255,255,255,.05)",
|
||||||
|
fontFamily: mono,
|
||||||
|
fontSize: 10,
|
||||||
|
letterSpacing: ".14em",
|
||||||
|
color,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, minHeight: 0, overflow: "auto", padding: 14 }}>
|
||||||
|
{renderAsMarkdown ? (
|
||||||
|
<MarkdownBlock source={body} />
|
||||||
|
) : (
|
||||||
|
<pre
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
fontFamily: mono,
|
||||||
|
fontSize: 12,
|
||||||
|
color: "#cfcfd5",
|
||||||
|
lineHeight: 1.55,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{body}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user