From feb05dc8d0d2a12a799204c7e7bb3f6b9fb69232 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 16 Feb 2026 16:50:50 +0000 Subject: [PATCH] Refactored and documented the HTTP API --- server/src/http/anthropic.rs | 15 +++++- server/src/http/chat.rs | 12 ++++- server/src/http/fs.rs | 50 ------------------- server/src/http/io.rs | 94 ++++++++++++++++++++++++++++++++++++ server/src/http/mod.rs | 27 +++-------- server/src/http/model.rs | 13 ++++- server/src/http/project.rs | 16 +++++- server/src/http/search.rs | 28 ----------- server/src/http/shell.rs | 29 ----------- 9 files changed, 148 insertions(+), 136 deletions(-) delete mode 100644 server/src/http/fs.rs create mode 100644 server/src/http/io.rs delete mode 100644 server/src/http/search.rs delete mode 100644 server/src/http/shell.rs diff --git a/server/src/http/anthropic.rs b/server/src/http/anthropic.rs index e0929bd..6f6639f 100644 --- a/server/src/http/anthropic.rs +++ b/server/src/http/anthropic.rs @@ -1,6 +1,6 @@ use crate::http::context::{AppContext, OpenApiResult, bad_request}; use crate::llm::chat; -use poem_openapi::{Object, OpenApi, payload::Json}; +use poem_openapi::{Object, OpenApi, Tags, payload::Json}; use serde::Deserialize; use std::sync::Arc; @@ -9,6 +9,11 @@ struct ApiKeyPayload { api_key: String, } +#[derive(Tags)] +enum AnthropicTags { + Anthropic, +} + pub struct AnthropicApi { ctx: Arc, } @@ -19,8 +24,11 @@ impl AnthropicApi { } } -#[OpenApi] +#[OpenApi(tag = "AnthropicTags::Anthropic")] impl AnthropicApi { + /// Check whether an Anthropic API key is stored. + /// + /// Returns `true` if a non-empty key is present, otherwise `false`. #[oai(path = "/anthropic/key/exists", method = "get")] async fn get_anthropic_api_key_exists(&self) -> OpenApiResult> { let exists = @@ -28,6 +36,9 @@ impl AnthropicApi { Ok(Json(exists)) } + /// Store or update the Anthropic API key used for requests. + /// + /// Returns `true` when the key is saved successfully. #[oai(path = "/anthropic/key", method = "post")] async fn set_anthropic_api_key( &self, diff --git a/server/src/http/chat.rs b/server/src/http/chat.rs index 371f88b..de3ff1e 100644 --- a/server/src/http/chat.rs +++ b/server/src/http/chat.rs @@ -1,14 +1,22 @@ use crate::http::context::{AppContext, OpenApiResult, bad_request}; use crate::llm::chat; -use poem_openapi::{OpenApi, payload::Json}; +use poem_openapi::{OpenApi, Tags, payload::Json}; use std::sync::Arc; +#[derive(Tags)] +enum ChatTags { + Chat, +} + pub struct ChatApi { pub ctx: Arc, } -#[OpenApi] +#[OpenApi(tag = "ChatTags::Chat")] impl ChatApi { + /// Cancel the currently running chat stream, if any. + /// + /// Returns `true` once the cancellation signal is issued. #[oai(path = "/chat/cancel", method = "post")] async fn cancel_chat(&self) -> OpenApiResult> { chat::cancel_chat(&self.ctx.state).map_err(bad_request)?; diff --git a/server/src/http/fs.rs b/server/src/http/fs.rs deleted file mode 100644 index ec5d17c..0000000 --- a/server/src/http/fs.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::http::context::{AppContext, OpenApiResult, bad_request}; -use crate::io::fs; -use poem_openapi::{Object, OpenApi, payload::Json}; -use serde::Deserialize; -use std::sync::Arc; - -#[derive(Deserialize, Object)] -struct FilePathPayload { - pub path: String, -} - -#[derive(Deserialize, Object)] -struct WriteFilePayload { - pub path: String, - pub content: String, -} - -pub struct FsApi { - pub ctx: Arc, -} - -#[OpenApi] -impl FsApi { - #[oai(path = "/fs/read", method = "post")] - async fn read_file(&self, payload: Json) -> OpenApiResult> { - let content = fs::read_file(payload.0.path, &self.ctx.state) - .await - .map_err(bad_request)?; - Ok(Json(content)) - } - - #[oai(path = "/fs/write", method = "post")] - async fn write_file(&self, payload: Json) -> OpenApiResult> { - fs::write_file(payload.0.path, payload.0.content, &self.ctx.state) - .await - .map_err(bad_request)?; - Ok(Json(true)) - } - - #[oai(path = "/fs/list", method = "post")] - async fn list_directory( - &self, - payload: Json, - ) -> OpenApiResult>> { - let entries = fs::list_directory(payload.0.path, &self.ctx.state) - .await - .map_err(bad_request)?; - Ok(Json(entries)) - } -} diff --git a/server/src/http/io.rs b/server/src/http/io.rs new file mode 100644 index 0000000..4e1f63f --- /dev/null +++ b/server/src/http/io.rs @@ -0,0 +1,94 @@ +use crate::http::context::{AppContext, OpenApiResult, bad_request}; +use crate::io::fs as io_fs; +use poem_openapi::{Object, OpenApi, Tags, payload::Json}; +use serde::Deserialize; +use std::sync::Arc; + +#[derive(Tags)] +enum IoTags { + Io, +} + +#[derive(Deserialize, Object)] +struct FilePathPayload { + pub path: String, +} + +#[derive(Deserialize, Object)] +struct WriteFilePayload { + pub path: String, + pub content: String, +} + +#[derive(Deserialize, Object)] +struct SearchPayload { + query: String, +} + +#[derive(Deserialize, Object)] +struct ExecShellPayload { + pub command: String, + pub args: Vec, +} + +pub struct IoApi { + pub ctx: Arc, +} + +#[OpenApi(tag = "IoTags::Io")] +impl IoApi { + /// Read a file from the currently open project and return its contents. + #[oai(path = "/io/fs/read", method = "post")] + async fn read_file(&self, payload: Json) -> OpenApiResult> { + let content = io_fs::read_file(payload.0.path, &self.ctx.state) + .await + .map_err(bad_request)?; + Ok(Json(content)) + } + + /// Write a file to the currently open project, creating parent directories if needed. + #[oai(path = "/io/fs/write", method = "post")] + async fn write_file(&self, payload: Json) -> OpenApiResult> { + io_fs::write_file(payload.0.path, payload.0.content, &self.ctx.state) + .await + .map_err(bad_request)?; + Ok(Json(true)) + } + + /// List files and folders in a directory within the currently open project. + #[oai(path = "/io/fs/list", method = "post")] + async fn list_directory( + &self, + payload: Json, + ) -> OpenApiResult>> { + let entries = io_fs::list_directory(payload.0.path, &self.ctx.state) + .await + .map_err(bad_request)?; + Ok(Json(entries)) + } + + /// Search the currently open project for files containing the provided query string. + #[oai(path = "/io/search", method = "post")] + async fn search_files( + &self, + payload: Json, + ) -> OpenApiResult>> { + let results = crate::io::search::search_files(payload.0.query, &self.ctx.state) + .await + .map_err(bad_request)?; + Ok(Json(results)) + } + + /// Execute an allowlisted shell command in the currently open project. + #[oai(path = "/io/shell/exec", method = "post")] + async fn exec_shell( + &self, + payload: Json, + ) -> OpenApiResult> { + let output = + crate::io::shell::exec_shell(payload.0.command, payload.0.args, &self.ctx.state) + .await + .map_err(bad_request)?; + Ok(Json(output)) + } +} diff --git a/server/src/http/mod.rs b/server/src/http/mod.rs index f23cbf6..12a5506 100644 --- a/server/src/http/mod.rs +++ b/server/src/http/mod.rs @@ -2,26 +2,22 @@ pub mod anthropic; pub mod assets; pub mod chat; pub mod context; -pub mod fs; pub mod health; +pub mod io; pub mod model; pub mod project; -pub mod search; -pub mod shell; pub mod ws; use anthropic::AnthropicApi; use chat::ChatApi; use context::AppContext; -use fs::FsApi; +use io::IoApi; use model::ModelApi; use poem::EndpointExt; use poem::{Route, get}; use poem_openapi::OpenApiService; use project::ProjectApi; -use search::SearchApi; -use shell::ShellApi; use std::sync::Arc; pub fn build_routes(ctx: AppContext) -> impl poem::Endpoint { @@ -40,26 +36,17 @@ pub fn build_routes(ctx: AppContext) -> impl poem::Endpoint { .data(ctx_arc) } -type ApiTuple = ( - ProjectApi, - ModelApi, - AnthropicApi, - FsApi, - SearchApi, - ShellApi, - ChatApi, -); +type ApiTuple = (ProjectApi, ModelApi, AnthropicApi, IoApi, ChatApi); type ApiService = OpenApiService; +/// All HTTP methods are documented by OpenAPI at /docs pub fn build_openapi_service(ctx: Arc) -> (ApiService, ApiService) { let api = ( ProjectApi { ctx: ctx.clone() }, ModelApi { ctx: ctx.clone() }, AnthropicApi::new(ctx.clone()), - FsApi { ctx: ctx.clone() }, - SearchApi { ctx: ctx.clone() }, - ShellApi { ctx: ctx.clone() }, + IoApi { ctx: ctx.clone() }, ChatApi { ctx: ctx.clone() }, ); @@ -70,9 +57,7 @@ pub fn build_openapi_service(ctx: Arc) -> (ApiService, ApiService) { ProjectApi { ctx: ctx.clone() }, ModelApi { ctx: ctx.clone() }, AnthropicApi::new(ctx.clone()), - FsApi { ctx: ctx.clone() }, - SearchApi { ctx: ctx.clone() }, - ShellApi { ctx: ctx.clone() }, + IoApi { ctx: ctx.clone() }, ChatApi { ctx }, ); diff --git a/server/src/http/model.rs b/server/src/http/model.rs index f9e6eab..e344f0a 100644 --- a/server/src/http/model.rs +++ b/server/src/http/model.rs @@ -1,10 +1,15 @@ use crate::http::context::{AppContext, OpenApiResult, bad_request}; use crate::io::fs; use crate::llm::chat; -use poem_openapi::{Object, OpenApi, param::Query, payload::Json}; +use poem_openapi::{Object, OpenApi, Tags, param::Query, payload::Json}; use serde::Deserialize; use std::sync::Arc; +#[derive(Tags)] +enum ModelTags { + Model, +} + #[derive(Deserialize, Object)] struct ModelPayload { model: String, @@ -14,20 +19,24 @@ pub struct ModelApi { pub ctx: Arc, } -#[OpenApi] +#[OpenApi(tag = "ModelTags::Model")] impl ModelApi { + /// Get the currently selected model preference, if any. #[oai(path = "/model", method = "get")] async fn get_model_preference(&self) -> OpenApiResult>> { let result = fs::get_model_preference(self.ctx.store.as_ref()).map_err(bad_request)?; Ok(Json(result)) } + /// Persist the selected model preference. #[oai(path = "/model", method = "post")] async fn set_model_preference(&self, payload: Json) -> OpenApiResult> { fs::set_model_preference(payload.0.model, self.ctx.store.as_ref()).map_err(bad_request)?; Ok(Json(true)) } + /// Fetch available model names from an Ollama server. + /// Optionally override the base URL via query string. #[oai(path = "/ollama/models", method = "get")] async fn get_ollama_models( &self, diff --git a/server/src/http/project.rs b/server/src/http/project.rs index 80e5721..2a188ed 100644 --- a/server/src/http/project.rs +++ b/server/src/http/project.rs @@ -1,9 +1,14 @@ use crate::http::context::{AppContext, OpenApiResult, bad_request}; use crate::io::fs; -use poem_openapi::{Object, OpenApi, payload::Json}; +use poem_openapi::{Object, OpenApi, Tags, payload::Json}; use serde::Deserialize; use std::sync::Arc; +#[derive(Tags)] +enum ProjectTags { + Project, +} + #[derive(Deserialize, Object)] struct PathPayload { path: String, @@ -13,8 +18,11 @@ pub struct ProjectApi { pub ctx: Arc, } -#[OpenApi] +#[OpenApi(tag = "ProjectTags::Project")] impl ProjectApi { + /// Get the currently open project path (if any). + /// + /// Returns null when no project is open. #[oai(path = "/project", method = "get")] async fn get_current_project(&self) -> OpenApiResult>> { let result = fs::get_current_project(&self.ctx.state, self.ctx.store.as_ref()) @@ -22,6 +30,9 @@ impl ProjectApi { Ok(Json(result)) } + /// Open a project and set it as the current project. + /// + /// Persists the selected path for later sessions. #[oai(path = "/project", method = "post")] async fn open_project(&self, payload: Json) -> OpenApiResult> { let confirmed = fs::open_project(payload.0.path, &self.ctx.state, self.ctx.store.as_ref()) @@ -30,6 +41,7 @@ impl ProjectApi { Ok(Json(confirmed)) } + /// Close the current project and clear the stored selection. #[oai(path = "/project", method = "delete")] async fn close_project(&self) -> OpenApiResult> { fs::close_project(&self.ctx.state, self.ctx.store.as_ref()).map_err(bad_request)?; diff --git a/server/src/http/search.rs b/server/src/http/search.rs deleted file mode 100644 index c65c41c..0000000 --- a/server/src/http/search.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::http::context::{AppContext, OpenApiResult, bad_request}; -use poem_openapi::{Object, OpenApi, payload::Json}; -use serde::Deserialize; -use std::sync::Arc; - -pub struct SearchApi { - pub ctx: Arc, -} - -#[derive(Deserialize, Object)] -struct SearchPayload { - query: String, -} - -#[OpenApi] -impl SearchApi { - #[oai(path = "/fs/search", method = "post")] - async fn search_files( - &self, - payload: Json, - ) -> OpenApiResult>> { - let ctx = self.ctx.clone(); - let results = crate::io::search::search_files(payload.0.query, &ctx.state) - .await - .map_err(bad_request)?; - Ok(Json(results)) - } -} diff --git a/server/src/http/shell.rs b/server/src/http/shell.rs deleted file mode 100644 index 2c6c1c0..0000000 --- a/server/src/http/shell.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::http::context::{AppContext, OpenApiResult, bad_request}; -use poem_openapi::{Object, OpenApi, payload::Json}; -use serde::Deserialize; -use std::sync::Arc; - -#[derive(Deserialize, Object)] -struct ExecShellPayload { - pub command: String, - pub args: Vec, -} - -pub struct ShellApi { - pub ctx: Arc, -} - -#[OpenApi] -impl ShellApi { - #[oai(path = "/shell/exec", method = "post")] - async fn exec_shell( - &self, - payload: Json, - ) -> OpenApiResult> { - let output = - crate::io::shell::exec_shell(payload.0.command, payload.0.args, &self.ctx.state) - .await - .map_err(bad_request)?; - Ok(Json(output)) - } -}