Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions crates/switchyard-desktop/src/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,25 @@ pub enum Action {
id: String,
settings_file: Option<String>,
},
Install {
tool: Harness,
account: Option<String>,
route: String,
id: String,
settings_file: Option<String>,
preview_token: Option<String>,
},
PreviewRestore {
tool: Harness,
account: Option<String>,
settings_file: Option<String>,
},
Restore {
tool: Harness,
account: Option<String>,
settings_file: Option<String>,
preview_token: Option<String>,
},
Restart {},
PreviewUpdate {},
Update {
Expand Down Expand Up @@ -178,6 +197,18 @@ pub struct RouteInfo {
/// This field maps each harness to the reason it cannot use the route, or to `None`
/// when it can.
pub compatibility: BTreeMap<Harness, Option<String>>,
/// This field lists the harness settings files that select the route's public model ID.
pub dependents: Vec<Dependent>,
}

/// Dependent names harness settings that select a route, so deleting the route breaks them.
#[derive(Clone, Serialize)]
pub struct Dependent {
pub tool: Harness,
pub label: &'static str,
/// This field is `None` for the harness's own settings and names a saved account otherwise.
pub account: Option<String>,
pub files: Vec<PathBuf>,
}

/// ToolInfo describes one harness whose settings the apps can change.
Expand Down Expand Up @@ -490,6 +521,7 @@ impl Controller {
};
self.route_reviews
.retain(|key, _| listed.iter().any(|route| &route.name == key));
let selections = harness_selections();
let mut routes = Vec::with_capacity(listed.len());
for route in listed {
let revision = self.remember_route(
Expand Down Expand Up @@ -521,6 +553,11 @@ impl Controller {
.iter()
.map(|&(tool, _)| (tool, login_mode(tool, &config, &route.name).err()))
.collect(),
dependents: selections
.iter()
.filter(|(model, _)| model.as_str() == &*route.id)
.map(|(_, dependent)| dependent.clone())
.collect(),
editable: algorithm.is_some(),
generated: config
.generated_by("routes", &route.name)
Expand Down Expand Up @@ -974,6 +1011,76 @@ impl Controller {
data,
})
}
Action::Install {
tool,
account,
route,
id,
settings_file,
preview_token,
} => {
let files = install_paths(tool, account.as_deref(), settings_file.as_deref())?;
let inputs = json!({"tool":tool,"account":account,"route":route,"id":id,"settings_file":settings_file,"server_url":self.config.server_url});
self.tool_review
.as_ref()
.ok_or("Preview these settings before applying them.")?
.check(preview_token.as_deref(), &inputs)?;
let config = self.routes()?;
let route = current_route(&config, &route, Some(&id))?;
harness::install_reviewed(
tool,
&files,
&self.config.server_url,
&route.id,
config.context_window(&route.key),
login_mode(tool, &config, &route.key)?,
self.tool_review
.as_ref()
.map(|review| review.files.as_slice()),
)
.map(Reply::message)
}
Action::PreviewRestore {
tool,
account,
settings_file,
} => {
let files = install_paths(tool, account.as_deref(), settings_file.as_deref())?;
let review = Review::capture(
json!({"restore":true,"tool":tool,"account":account,"settings_file":settings_file}),
harness::review_files(&files),
)?;
let mut data = harness::preview_restore(tool, &files)?;
review.check(Some(&review.token), &review.inputs)?;
data["preview_token"] = json!(review.token);
self.tool_review = Some(review);
Ok(Reply {
message: String::new(),
data,
})
}
Action::Restore {
tool,
account,
settings_file,
preview_token,
} => {
let files = install_paths(tool, account.as_deref(), settings_file.as_deref())?;
self.tool_review.as_ref().ok_or("Preview the restore before applying it.")?.check(preview_token.as_deref(),&json!({"restore":true,"tool":tool,"account":account,"settings_file":settings_file}))?;
match harness::restore_reviewed(
&files,
self.tool_review
.as_ref()
.map(|review| review.files.as_slice()),
) {
Ok(message) => Ok(Reply::message(message)),
Err(error) if error.starts_with("Restored settings, but") => Ok(Reply {
message: error,
data: json!({"warning":true}),
}),
Err(error) => Err(error),
}
}
Action::Restart {} => server::restart_checked(&self.config).map(Reply::message),
Action::CheckConfig {} => server::check_config(&self.config)
.map(|()| Reply::message("Server config check passed.".into())),
Expand Down Expand Up @@ -1089,6 +1196,39 @@ fn target_label(target: &switchyard_runner::ConfiguredTarget) -> String {
None => format!("{} (no target has this name)", target.name),
}
}
/// This function reads each harness and account's settings once and pairs each one with the
/// model it selects, so a snapshot does not reread every settings file for every route.
fn harness_selections() -> Vec<(String, Dependent)> {
let mut selections = Vec::new();
for &(tool, label) in harness::HARNESSES {
let mut destinations = vec![(None, harness::paths(tool))];
destinations.extend(
accounts::list(tool)
.into_iter()
.map(|(name, path)| (Some(name), accounts::config_paths(tool, Some(&path)))),
);
for (account, files) in destinations {
let Ok(files) = files else {
continue;
};
let Some(model) = harness::inspect(tool, &files).ok().and_then(|status| {
Some(status.lines().next()?.strip_prefix("Model: ")?.to_owned())
}) else {
continue;
};
selections.push((
model,
Dependent {
tool,
label,
account,
files,
},
));
}
}
selections
}
fn role_info(role: &crate::server_config::Role) -> RoleInfo {
RoleInfo {
label: role.label.clone(),
Expand Down
Loading
Loading