huskies: merge 949
This commit is contained in:
@@ -31,6 +31,11 @@ use std::sync::{Arc, OnceLock};
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use super::rpc_contract::{
|
||||
BotConfigPayload, EditorSettingsResult, ForgetProjectParams, OkResult, OpenFileParams,
|
||||
OpenProjectParams, OpenProjectResult, ProjectSettingsPayload, PutEditorParams,
|
||||
SetAnthropicApiKeyParams, SetModelPreferenceParams,
|
||||
};
|
||||
use super::wire::RpcFrame;
|
||||
use crate::state::SessionState;
|
||||
use crate::store::JsonFileStore;
|
||||
@@ -119,8 +124,50 @@ static HANDLERS: &[(&str, Handler)] = &[
|
||||
("agents.get_output", |p| {
|
||||
Box::pin(handle_agents_get_output(p))
|
||||
}),
|
||||
// ── typed write methods ──────────────────────────────────────────────
|
||||
("model.set_preference", |p| {
|
||||
Box::pin(handle_model_set_preference(p))
|
||||
}),
|
||||
("anthropic.set_api_key", |p| {
|
||||
Box::pin(handle_anthropic_set_api_key(p))
|
||||
}),
|
||||
("settings.put_editor", |p| {
|
||||
Box::pin(handle_settings_put_editor(p))
|
||||
}),
|
||||
("settings.open_file", |p| {
|
||||
Box::pin(handle_settings_open_file(p))
|
||||
}),
|
||||
("settings.put_project", |p| {
|
||||
Box::pin(handle_settings_put_project(p))
|
||||
}),
|
||||
("project.open", |p| Box::pin(handle_project_open(p))),
|
||||
("project.close", |p| Box::pin(handle_project_close(p))),
|
||||
("project.forget", |p| Box::pin(handle_project_forget(p))),
|
||||
("bot_config.save", |p| Box::pin(handle_bot_config_save(p))),
|
||||
("chat.cancel", |p| Box::pin(handle_chat_cancel(p))),
|
||||
];
|
||||
|
||||
// ── typed-write helper macros ───────────────────────────────────────────────
|
||||
|
||||
/// Parse the incoming JSON params into a typed struct or return a typed error
|
||||
/// response shaped as `{"error": "..."}`.
|
||||
macro_rules! parse_params {
|
||||
($params:expr, $ty:ty) => {
|
||||
match serde_json::from_value::<$ty>($params) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return serde_json::json!({"error": format!("invalid params: {e}")}),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn ok_json() -> Value {
|
||||
serde_json::to_value(OkResult { ok: true }).unwrap_or(Value::Null)
|
||||
}
|
||||
|
||||
fn err_json(msg: impl Into<String>) -> Value {
|
||||
serde_json::json!({"error": msg.into()})
|
||||
}
|
||||
|
||||
// ── handlers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Handler for the `health.check` method.
|
||||
@@ -532,6 +579,205 @@ async fn handle_agents_get_output(params: Value) -> Value {
|
||||
}
|
||||
}
|
||||
|
||||
// ── typed write handlers ────────────────────────────────────────────────────
|
||||
|
||||
/// Handler for `model.set_preference`. Persists the user's chosen LLM model.
|
||||
async fn handle_model_set_preference(params: Value) -> Value {
|
||||
let typed = parse_params!(params, SetModelPreferenceParams);
|
||||
let Some(ctx) = RPC_CTX.get() else {
|
||||
return err_json("RPC context not initialised");
|
||||
};
|
||||
match crate::io::fs::set_model_preference(typed.model, ctx.store.as_ref()) {
|
||||
Ok(()) => ok_json(),
|
||||
Err(e) => err_json(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for `anthropic.set_api_key`. Stores an Anthropic API key.
|
||||
async fn handle_anthropic_set_api_key(params: Value) -> Value {
|
||||
let typed = parse_params!(params, SetAnthropicApiKeyParams);
|
||||
let Some(ctx) = RPC_CTX.get() else {
|
||||
return err_json("RPC context not initialised");
|
||||
};
|
||||
match crate::service::anthropic::set_api_key(ctx.store.as_ref(), typed.api_key) {
|
||||
Ok(()) => ok_json(),
|
||||
Err(e) => err_json(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for `settings.put_editor`. Updates the configured editor command.
|
||||
async fn handle_settings_put_editor(params: Value) -> Value {
|
||||
use crate::store::StoreOps;
|
||||
let typed = parse_params!(params, PutEditorParams);
|
||||
let Some(ctx) = RPC_CTX.get() else {
|
||||
return err_json("RPC context not initialised");
|
||||
};
|
||||
let trimmed = typed
|
||||
.editor_command
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string());
|
||||
match trimmed {
|
||||
Some(cmd) => {
|
||||
ctx.store.set(
|
||||
crate::service::settings::EDITOR_COMMAND_KEY,
|
||||
serde_json::json!(cmd.clone()),
|
||||
);
|
||||
if let Err(e) = ctx.store.save() {
|
||||
return err_json(e);
|
||||
}
|
||||
serde_json::to_value(EditorSettingsResult {
|
||||
editor_command: Some(cmd),
|
||||
})
|
||||
.unwrap_or(Value::Null)
|
||||
}
|
||||
None => {
|
||||
ctx.store
|
||||
.delete(crate::service::settings::EDITOR_COMMAND_KEY);
|
||||
if let Err(e) = ctx.store.save() {
|
||||
return err_json(e);
|
||||
}
|
||||
serde_json::to_value(EditorSettingsResult {
|
||||
editor_command: None,
|
||||
})
|
||||
.unwrap_or(Value::Null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for `settings.open_file`. Spawns the configured editor.
|
||||
async fn handle_settings_open_file(params: Value) -> Value {
|
||||
let typed = parse_params!(params, OpenFileParams);
|
||||
let Some(ctx) = RPC_CTX.get() else {
|
||||
return err_json("RPC context not initialised");
|
||||
};
|
||||
match crate::service::settings::open_file_in_editor(ctx.store.as_ref(), &typed.path, typed.line)
|
||||
{
|
||||
Ok(()) => ok_json(),
|
||||
Err(e) => err_json(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for `settings.put_project`. Persists project.toml scalar settings.
|
||||
async fn handle_settings_put_project(params: Value) -> Value {
|
||||
let typed = parse_params!(params, ProjectSettingsPayload);
|
||||
let Some(ctx) = RPC_CTX.get() else {
|
||||
return err_json("RPC context not initialised");
|
||||
};
|
||||
let Ok(root) = ctx.state.get_project_root() else {
|
||||
return err_json("No project open");
|
||||
};
|
||||
let domain = crate::service::settings::ProjectSettings {
|
||||
default_qa: typed.default_qa,
|
||||
default_coder_model: typed.default_coder_model,
|
||||
max_coders: typed.max_coders,
|
||||
max_retries: typed.max_retries,
|
||||
base_branch: typed.base_branch,
|
||||
rate_limit_notifications: typed.rate_limit_notifications,
|
||||
timezone: typed.timezone,
|
||||
rendezvous: typed.rendezvous,
|
||||
watcher_sweep_interval_secs: typed.watcher_sweep_interval_secs,
|
||||
watcher_done_retention_secs: typed.watcher_done_retention_secs,
|
||||
};
|
||||
if let Err(e) = crate::service::settings::validate_project_settings(&domain) {
|
||||
return err_json(e.to_string());
|
||||
}
|
||||
if let Err(e) = crate::service::settings::write_project_settings(&root, &domain) {
|
||||
return err_json(e.to_string());
|
||||
}
|
||||
match crate::service::settings::load_project_settings(&root) {
|
||||
Ok(s) => serde_json::to_value(ProjectSettingsPayload {
|
||||
default_qa: s.default_qa,
|
||||
default_coder_model: s.default_coder_model,
|
||||
max_coders: s.max_coders,
|
||||
max_retries: s.max_retries,
|
||||
base_branch: s.base_branch,
|
||||
rate_limit_notifications: s.rate_limit_notifications,
|
||||
timezone: s.timezone,
|
||||
rendezvous: s.rendezvous,
|
||||
watcher_sweep_interval_secs: s.watcher_sweep_interval_secs,
|
||||
watcher_done_retention_secs: s.watcher_done_retention_secs,
|
||||
})
|
||||
.unwrap_or(Value::Null),
|
||||
Err(e) => err_json(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for `project.open`. Opens a project on disk and updates session state.
|
||||
async fn handle_project_open(params: Value) -> Value {
|
||||
let typed = parse_params!(params, OpenProjectParams);
|
||||
let Some(ctx) = RPC_CTX.get() else {
|
||||
return err_json("RPC context not initialised");
|
||||
};
|
||||
let port = crate::http::resolve_port();
|
||||
match crate::service::project::open_project(typed.path, &ctx.state, ctx.store.as_ref(), port)
|
||||
.await
|
||||
{
|
||||
Ok(path) => serde_json::to_value(OpenProjectResult { path }).unwrap_or(Value::Null),
|
||||
Err(e) => err_json(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for `project.close`. Closes the currently open project.
|
||||
async fn handle_project_close(_params: Value) -> Value {
|
||||
let Some(ctx) = RPC_CTX.get() else {
|
||||
return err_json("RPC context not initialised");
|
||||
};
|
||||
match crate::service::project::close_project(&ctx.state, ctx.store.as_ref()) {
|
||||
Ok(()) => ok_json(),
|
||||
Err(e) => err_json(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for `project.forget`. Removes a project from the known list.
|
||||
async fn handle_project_forget(params: Value) -> Value {
|
||||
let typed = parse_params!(params, ForgetProjectParams);
|
||||
let Some(ctx) = RPC_CTX.get() else {
|
||||
return err_json("RPC context not initialised");
|
||||
};
|
||||
match crate::service::project::forget_known_project(typed.path, ctx.store.as_ref()) {
|
||||
Ok(()) => ok_json(),
|
||||
Err(e) => err_json(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for `bot_config.save`. Writes `.huskies/bot.toml`.
|
||||
async fn handle_bot_config_save(params: Value) -> Value {
|
||||
let typed = parse_params!(params, BotConfigPayload);
|
||||
let Some(ctx) = RPC_CTX.get() else {
|
||||
return err_json("RPC context not initialised");
|
||||
};
|
||||
let Ok(root) = ctx.state.get_project_root() else {
|
||||
return err_json("No project open");
|
||||
};
|
||||
let bot_dir = root.join(".huskies");
|
||||
if let Err(e) = std::fs::create_dir_all(&bot_dir) {
|
||||
return err_json(format!("create .huskies dir: {e}"));
|
||||
}
|
||||
let bot_path = bot_dir.join("bot.toml");
|
||||
match toml::to_string_pretty(&typed) {
|
||||
Ok(s) => {
|
||||
if let Err(e) = std::fs::write(&bot_path, s) {
|
||||
return err_json(format!("write bot.toml: {e}"));
|
||||
}
|
||||
}
|
||||
Err(e) => return err_json(format!("serialise bot.toml: {e}")),
|
||||
}
|
||||
serde_json::to_value(typed).unwrap_or(Value::Null)
|
||||
}
|
||||
|
||||
/// Handler for `chat.cancel`. Cancels the in-flight LLM chat stream.
|
||||
async fn handle_chat_cancel(_params: Value) -> Value {
|
||||
let Some(ctx) = RPC_CTX.get() else {
|
||||
return err_json("RPC context not initialised");
|
||||
};
|
||||
match crate::llm::chat::cancel_chat(&ctx.state) {
|
||||
Ok(()) => ok_json(),
|
||||
Err(e) => err_json(e),
|
||||
}
|
||||
}
|
||||
|
||||
// ── dispatch ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Dispatch an incoming RPC method call to the registered handler.
|
||||
|
||||
Reference in New Issue
Block a user