huskies: merge 950
This commit is contained in:
@@ -70,8 +70,8 @@ mod wire;
|
||||
pub use auth::{add_join_token, init_token_auth, init_trusted_keys};
|
||||
pub(crate) use client::connect_and_sync;
|
||||
pub use client::{RENDEZVOUS_ERROR_THRESHOLD, spawn_rendezvous_client};
|
||||
pub use rpc::init_rpc_context;
|
||||
pub(crate) use rpc::try_handle_rpc_text;
|
||||
pub use rpc::{init_rpc_agents, init_rpc_context};
|
||||
pub use server::crdt_sync_handler;
|
||||
|
||||
// Test-only re-export used by `crdt_snapshot` tests.
|
||||
|
||||
@@ -37,6 +37,7 @@ use super::rpc_contract::{
|
||||
SetAnthropicApiKeyParams, SetModelPreferenceParams,
|
||||
};
|
||||
use super::wire::RpcFrame;
|
||||
use crate::agents::AgentPool;
|
||||
use crate::state::SessionState;
|
||||
use crate::store::JsonFileStore;
|
||||
use crate::workflow::WorkflowState;
|
||||
@@ -57,6 +58,9 @@ pub struct RpcState {
|
||||
/// Global RPC context, initialised once at server startup via [`init_rpc_context`].
|
||||
static RPC_CTX: OnceLock<RpcState> = OnceLock::new();
|
||||
|
||||
/// Global agent pool, registered once at startup via [`init_rpc_agents`].
|
||||
static RPC_AGENTS: OnceLock<Arc<AgentPool>> = OnceLock::new();
|
||||
|
||||
/// Register the global RPC context.
|
||||
///
|
||||
/// Must be called before any handler that accesses project state is invoked.
|
||||
@@ -73,6 +77,14 @@ pub fn init_rpc_context(
|
||||
});
|
||||
}
|
||||
|
||||
/// Register the agent pool for use in RPC handlers.
|
||||
///
|
||||
/// Must be called after [`AgentPool`] is constructed (after `init_rpc_context`).
|
||||
/// Subsequent calls are silently ignored (OnceLock semantics).
|
||||
pub fn init_rpc_agents(agents: Arc<AgentPool>) {
|
||||
let _ = RPC_AGENTS.set(agents);
|
||||
}
|
||||
|
||||
/// Static registry mapping method names to handlers.
|
||||
///
|
||||
/// Add new handlers here. The registry is a plain slice — linear scan is
|
||||
@@ -145,6 +157,18 @@ static HANDLERS: &[(&str, Handler)] = &[
|
||||
("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))),
|
||||
// ── formerly REST-only endpoints, now RPC ────────────────────────────────
|
||||
("io.read_file", |p| Box::pin(handle_io_read_file(p))),
|
||||
("io.list_directory_absolute", |p| {
|
||||
Box::pin(handle_io_list_directory_absolute(p))
|
||||
}),
|
||||
("bot.command", |p| Box::pin(handle_bot_command(p))),
|
||||
("agents.start", |p| Box::pin(handle_agents_start(p))),
|
||||
("agents.stop", |p| Box::pin(handle_agents_stop(p))),
|
||||
("wizard.confirm_step", |p| {
|
||||
Box::pin(handle_wizard_confirm_step(p))
|
||||
}),
|
||||
("wizard.skip_step", |p| Box::pin(handle_wizard_skip_step(p))),
|
||||
];
|
||||
|
||||
// ── typed-write helper macros ───────────────────────────────────────────────
|
||||
@@ -778,6 +802,194 @@ async fn handle_chat_cancel(_params: Value) -> Value {
|
||||
}
|
||||
}
|
||||
|
||||
// ── formerly REST-only handlers ──────────────────────────────────────────────
|
||||
|
||||
/// Handler for `io.read_file`. Reads a project-scoped file and returns its content.
|
||||
///
|
||||
/// Parameters: `{ "path": string }`.
|
||||
async fn handle_io_read_file(params: Value) -> Value {
|
||||
let Some(ctx) = RPC_CTX.get() else {
|
||||
return err_json("RPC context not initialised");
|
||||
};
|
||||
let Some(path) = params.get("path").and_then(|v| v.as_str()) else {
|
||||
return err_json("missing path");
|
||||
};
|
||||
match crate::service::file_io::read_file(path.to_string(), &ctx.state).await {
|
||||
Ok(content) => Value::String(content),
|
||||
Err(e) => err_json(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for `io.list_directory_absolute`. Lists entries at an absolute path.
|
||||
///
|
||||
/// Parameters: `{ "path": string }`.
|
||||
async fn handle_io_list_directory_absolute(params: Value) -> Value {
|
||||
let Some(path) = params.get("path").and_then(|v| v.as_str()) else {
|
||||
return err_json("missing path");
|
||||
};
|
||||
match crate::service::file_io::list_directory_absolute(path.to_string()).await {
|
||||
Ok(entries) => serde_json::to_value(entries).unwrap_or(Value::Array(vec![])),
|
||||
Err(e) => err_json(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for `bot.command`. Dispatches a slash command and returns markdown output.
|
||||
///
|
||||
/// Parameters: `{ "command": string, "args"?: string }`.
|
||||
async fn handle_bot_command(params: Value) -> Value {
|
||||
let Some(ctx) = RPC_CTX.get() else {
|
||||
return err_json("RPC context not initialised");
|
||||
};
|
||||
let Some(command) = params.get("command").and_then(|v| v.as_str()) else {
|
||||
return err_json("missing command");
|
||||
};
|
||||
let args = params.get("args").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let Ok(root) = ctx.state.get_project_root() else {
|
||||
return err_json("No project open");
|
||||
};
|
||||
let Some(agents) = RPC_AGENTS.get() else {
|
||||
return err_json("Agent pool not initialised");
|
||||
};
|
||||
match crate::service::bot_command::execute(command, args, &root, agents).await {
|
||||
Ok(response) => serde_json::json!({"response": response}),
|
||||
Err(e) => err_json(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for `agents.start`. Starts an agent for a story.
|
||||
///
|
||||
/// Parameters: `{ "story_id": string, "agent_name"?: string }`.
|
||||
async fn handle_agents_start(params: Value) -> Value {
|
||||
let Some(ctx) = RPC_CTX.get() else {
|
||||
return err_json("RPC context not initialised");
|
||||
};
|
||||
let Some(story_id) = params.get("story_id").and_then(|v| v.as_str()) else {
|
||||
return err_json("missing story_id");
|
||||
};
|
||||
let agent_name = params.get("agent_name").and_then(|v| v.as_str());
|
||||
let Ok(root) = ctx.state.get_project_root() else {
|
||||
return err_json("No project open");
|
||||
};
|
||||
let Some(agents) = RPC_AGENTS.get() else {
|
||||
return err_json("Agent pool not initialised");
|
||||
};
|
||||
match crate::service::agents::start_agent(agents, &root, story_id, agent_name, None, None).await
|
||||
{
|
||||
Ok(info) => serde_json::json!({
|
||||
"story_id": info.story_id,
|
||||
"agent_name": info.agent_name,
|
||||
"status": info.status.to_string(),
|
||||
"session_id": info.session_id,
|
||||
"worktree_path": info.worktree_path,
|
||||
"base_branch": info.base_branch,
|
||||
"log_session_id": info.log_session_id,
|
||||
}),
|
||||
Err(e) => err_json(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for `agents.stop`. Stops a running agent.
|
||||
///
|
||||
/// Parameters: `{ "story_id": string, "agent_name": string }`.
|
||||
async fn handle_agents_stop(params: Value) -> Value {
|
||||
let Some(ctx) = RPC_CTX.get() else {
|
||||
return err_json("RPC context not initialised");
|
||||
};
|
||||
let Some(story_id) = params.get("story_id").and_then(|v| v.as_str()) else {
|
||||
return err_json("missing story_id");
|
||||
};
|
||||
let Some(agent_name) = params.get("agent_name").and_then(|v| v.as_str()) else {
|
||||
return err_json("missing agent_name");
|
||||
};
|
||||
let Ok(root) = ctx.state.get_project_root() else {
|
||||
return err_json("No project open");
|
||||
};
|
||||
let Some(agents) = RPC_AGENTS.get() else {
|
||||
return err_json("Agent pool not initialised");
|
||||
};
|
||||
match crate::service::agents::stop_agent(agents, &root, story_id, agent_name).await {
|
||||
Ok(()) => Value::Bool(true),
|
||||
Err(e) => err_json(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialise a [`crate::io::wizard::WizardState`] into the frontend's expected JSON shape.
|
||||
fn wizard_state_to_value(state: &crate::io::wizard::WizardState) -> Value {
|
||||
let steps: Vec<Value> = state
|
||||
.steps
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let step_str = serde_json::to_value(s.step)
|
||||
.ok()
|
||||
.and_then(|v| v.as_str().map(String::from))
|
||||
.unwrap_or_default();
|
||||
let status_str = serde_json::to_value(&s.status)
|
||||
.ok()
|
||||
.and_then(|v| v.as_str().map(String::from))
|
||||
.unwrap_or_default();
|
||||
serde_json::json!({
|
||||
"step": step_str,
|
||||
"label": s.step.label(),
|
||||
"status": status_str,
|
||||
"content": s.content,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
serde_json::json!({
|
||||
"steps": steps,
|
||||
"current_step_index": state.current_step_index(),
|
||||
"completed": state.completed,
|
||||
})
|
||||
}
|
||||
|
||||
/// Handler for `wizard.confirm_step`. Confirms the current wizard step.
|
||||
///
|
||||
/// Parameters: `{ "step": string }`.
|
||||
async fn handle_wizard_confirm_step(params: Value) -> Value {
|
||||
let Some(ctx) = RPC_CTX.get() else {
|
||||
return err_json("RPC context not initialised");
|
||||
};
|
||||
let Some(step_str) = params.get("step").and_then(|v| v.as_str()) else {
|
||||
return err_json("missing step");
|
||||
};
|
||||
let Ok(root) = ctx.state.get_project_root() else {
|
||||
return err_json("No project open");
|
||||
};
|
||||
let quoted = format!("\"{step_str}\"");
|
||||
let step = match serde_json::from_str::<crate::io::wizard::WizardStep>("ed) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return err_json(format!("Unknown wizard step: {step_str}")),
|
||||
};
|
||||
match crate::service::wizard::mark_step_confirmed(&root, step) {
|
||||
Ok(state) => wizard_state_to_value(&state),
|
||||
Err(e) => err_json(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for `wizard.skip_step`. Skips the current wizard step.
|
||||
///
|
||||
/// Parameters: `{ "step": string }`.
|
||||
async fn handle_wizard_skip_step(params: Value) -> Value {
|
||||
let Some(ctx) = RPC_CTX.get() else {
|
||||
return err_json("RPC context not initialised");
|
||||
};
|
||||
let Some(step_str) = params.get("step").and_then(|v| v.as_str()) else {
|
||||
return err_json("missing step");
|
||||
};
|
||||
let Ok(root) = ctx.state.get_project_root() else {
|
||||
return err_json("No project open");
|
||||
};
|
||||
let quoted = format!("\"{step_str}\"");
|
||||
let step = match serde_json::from_str::<crate::io::wizard::WizardStep>("ed) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return err_json(format!("Unknown wizard step: {step_str}")),
|
||||
};
|
||||
match crate::service::wizard::mark_step_skipped(&root, step) {
|
||||
Ok(state) => wizard_state_to_value(&state),
|
||||
Err(e) => err_json(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
// ── dispatch ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Dispatch an incoming RPC method call to the registered handler.
|
||||
|
||||
Reference in New Issue
Block a user