diff options
| author | soryu <soryu@soryu.co> | 2026-01-15 03:37:44 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-01-15 03:37:44 +0000 |
| commit | 764bd28d08ceaef03cd4050f9568a62d77bbcfca (patch) | |
| tree | dbd83ea7d213902f2b8021acc98798b6f3545946 /makima/src/server | |
| parent | eeafe072bc6bb81459f7d087b48fc921afe9cc11 (diff) | |
| download | soryu-764bd28d08ceaef03cd4050f9568a62d77bbcfca.tar.gz soryu-764bd28d08ceaef03cd4050f9568a62d77bbcfca.zip | |
Add repository history feature to store and suggest previously used repositories (#18)
- Add repository_history table migration with repo_type, repo_path, use_count, last_used_at
- Add RepositoryHistoryEntry model and CRUD database functions
- Create API endpoints: GET/POST/DELETE /api/v1/repository-history, GET /api/v1/repository-history/suggestions
- Update add_remote_repository and add_local_repository handlers to automatically track history
- Update frontend API with repository history types and functions
- Add Repository History section to Settings page with list of entries and delete functionality
- Add suggestions dropdown to RepositoryPanel when entering new repository URL/path
- Suggestions filter by repo type (remote vs local) and match on user input
Test plan:
- Add a remote repository to a contract - verify it appears in Settings history
- Add a local repository to a contract - verify it appears in Settings history
- Add same repository again - verify use_count increments, not duplicate
- When adding new repository, verify suggestions appear based on history
- Delete a history entry from Settings - verify it's removed
- Verify suggestions only show matching type (remote for remote, local for local)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'makima/src/server')
| -rw-r--r-- | makima/src/server/handlers/contracts.rs | 32 | ||||
| -rw-r--r-- | makima/src/server/handlers/mod.rs | 1 | ||||
| -rw-r--r-- | makima/src/server/handlers/repository_history.rs | 173 | ||||
| -rw-r--r-- | makima/src/server/mod.rs | 15 | ||||
| -rw-r--r-- | makima/src/server/openapi.rs | 18 |
5 files changed, 234 insertions, 5 deletions
diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs index a3aa00a..3ce29e1 100644 --- a/makima/src/server/handlers/contracts.rs +++ b/makima/src/server/handlers/contracts.rs @@ -631,6 +631,22 @@ pub async fn add_remote_repository( Ok(repo) => { // Update supervisor task with repository info if this is a primary repo update_supervisor_with_repo_if_needed(pool, id, auth.owner_id, &repo).await; + + // Track repository in history for future suggestions + if let Err(e) = repository::add_or_update_repository_history( + pool, + auth.owner_id, + &req.name, + Some(&req.repository_url), + None, + "remote", + ) + .await + { + // Log but don't fail the request if history tracking fails + tracing::warn!("Failed to track repository in history: {}", e); + } + (StatusCode::CREATED, Json(repo)).into_response() } Err(e) => { @@ -705,6 +721,22 @@ pub async fn add_local_repository( Ok(repo) => { // Update supervisor task with repository info if this is a primary repo update_supervisor_with_repo_if_needed(pool, id, auth.owner_id, &repo).await; + + // Track repository in history for future suggestions + if let Err(e) = repository::add_or_update_repository_history( + pool, + auth.owner_id, + &req.name, + None, + Some(&req.local_path), + "local", + ) + .await + { + // Log but don't fail the request if history tracking fails + tracing::warn!("Failed to track repository in history: {}", e); + } + (StatusCode::CREATED, Json(repo)).into_response() } Err(e) => { diff --git a/makima/src/server/handlers/mod.rs b/makima/src/server/handlers/mod.rs index 0ce6c85..b5650fd 100644 --- a/makima/src/server/handlers/mod.rs +++ b/makima/src/server/handlers/mod.rs @@ -14,6 +14,7 @@ pub mod mesh_daemon; pub mod mesh_merge; pub mod mesh_supervisor; pub mod mesh_ws; +pub mod repository_history; pub mod templates; pub mod transcript_analysis; pub mod users; diff --git a/makima/src/server/handlers/repository_history.rs b/makima/src/server/handlers/repository_history.rs new file mode 100644 index 0000000..c788d84 --- /dev/null +++ b/makima/src/server/handlers/repository_history.rs @@ -0,0 +1,173 @@ +//! HTTP handlers for repository history management. +//! Provides endpoints for listing, suggesting, and deleting repository history entries. + +use axum::{ + extract::{Path, Query, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use uuid::Uuid; + +use crate::db::models::{RepositoryHistoryListResponse, RepositorySuggestionsQuery}; +use crate::db::repository; +use crate::server::auth::Authenticated; +use crate::server::messages::ApiError; +use crate::server::state::SharedState; + +/// List all repository history entries for the authenticated user. +/// Returns entries ordered by use_count DESC, last_used_at DESC. +#[utoipa::path( + get, + path = "/api/v1/settings/repository-history", + responses( + (status = 200, description = "List of repository history entries", body = RepositoryHistoryListResponse), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Settings" +)] +pub async fn list_repository_history( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + match repository::list_repository_history_for_owner(pool, auth.owner_id).await { + Ok(entries) => { + let total = entries.len() as i64; + Json(RepositoryHistoryListResponse { entries, total }).into_response() + } + Err(e) => { + tracing::error!("Failed to list repository history: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} + +/// Get repository suggestions based on history. +/// Optionally filter by source_type (remote/local) and search query. +#[utoipa::path( + get, + path = "/api/v1/settings/repository-history/suggestions", + params( + ("source_type" = Option<String>, Query, description = "Filter by source type: 'remote' or 'local'"), + ("query" = Option<String>, Query, description = "Search query to filter by name or URL/path"), + ("limit" = Option<i32>, Query, description = "Limit results (default: 10)") + ), + responses( + (status = 200, description = "List of repository suggestions", body = RepositoryHistoryListResponse), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Settings" +)] +pub async fn get_repository_suggestions( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Query(params): Query<RepositorySuggestionsQuery>, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + let limit = params.limit.unwrap_or(10).min(50); // Cap at 50 for safety + + match repository::get_repository_suggestions( + pool, + auth.owner_id, + params.source_type.as_deref(), + params.query.as_deref(), + limit, + ) + .await + { + Ok(entries) => { + let total = entries.len() as i64; + Json(RepositoryHistoryListResponse { entries, total }).into_response() + } + Err(e) => { + tracing::error!("Failed to get repository suggestions: {}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} + +/// Delete a repository history entry. +#[utoipa::path( + delete, + path = "/api/v1/settings/repository-history/{id}", + params( + ("id" = Uuid, Path, description = "Repository history entry ID") + ), + responses( + (status = 204, description = "Entry deleted"), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Entry not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Settings" +)] +pub async fn delete_repository_history( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path(id): Path<Uuid>, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + match repository::delete_repository_history(pool, id, auth.owner_id).await { + Ok(true) => StatusCode::NO_CONTENT.into_response(), + Ok(false) => ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Repository history entry not found")), + ) + .into_response(), + Err(e) => { + tracing::error!("Failed to delete repository history {}: {}", id, e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs index 27ee06c..0eba009 100644 --- a/makima/src/server/mod.rs +++ b/makima/src/server/mod.rs @@ -18,7 +18,7 @@ use tower_http::trace::TraceLayer; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; -use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contracts, file_ws, files, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, templates, transcript_analysis, users, versions}; +use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contracts, file_ws, files, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, repository_history, templates, transcript_analysis, users, versions}; use crate::server::openapi::ApiDoc; use crate::server::state::SharedState; @@ -191,6 +191,19 @@ pub fn make_router(state: SharedState) -> Router { // Template endpoints .route("/templates", get(templates::list_templates)) .route("/templates/{id}", get(templates::get_template)) + // Settings endpoints + .route( + "/settings/repository-history", + get(repository_history::list_repository_history), + ) + .route( + "/settings/repository-history/suggestions", + get(repository_history::get_repository_suggestions), + ) + .route( + "/settings/repository-history/{id}", + axum::routing::delete(repository_history::delete_repository_history), + ) .with_state(state); let swagger = SwaggerUi::new("/swagger-ui") diff --git a/makima/src/server/openapi.rs b/makima/src/server/openapi.rs index c4f0f19..afa114b 100644 --- a/makima/src/server/openapi.rs +++ b/makima/src/server/openapi.rs @@ -10,15 +10,16 @@ use crate::db::models::{ Daemon, DaemonDirectoriesResponse, DaemonDirectory, DaemonListResponse, File, FileListResponse, FileSummary, MergeCommitRequest, MergeCompleteCheckResponse, MergeResolveRequest, MergeResultResponse, MergeSkipRequest, MergeStartRequest, MergeStatusResponse, - MeshChatConversation, MeshChatHistoryResponse, MeshChatMessageRecord, SendMessageRequest, Task, - TaskEventListResponse, TaskListResponse, TaskSummary, TaskWithSubtasks, TranscriptEntry, - UpdateContractRequest, UpdateFileRequest, UpdateTaskRequest, + MeshChatConversation, MeshChatHistoryResponse, MeshChatMessageRecord, + RepositoryHistoryEntry, RepositoryHistoryListResponse, RepositorySuggestionsQuery, + SendMessageRequest, Task, TaskEventListResponse, TaskListResponse, TaskSummary, + TaskWithSubtasks, TranscriptEntry, UpdateContractRequest, UpdateFileRequest, UpdateTaskRequest, }; use crate::server::auth::{ ApiKey, ApiKeyInfoResponse, CreateApiKeyRequest, CreateApiKeyResponse, RefreshApiKeyRequest, RefreshApiKeyResponse, RevokeApiKeyResponse, }; -use crate::server::handlers::{api_keys, contract_chat, contracts, files, listen, mesh, mesh_chat, mesh_merge, users}; +use crate::server::handlers::{api_keys, contract_chat, contracts, files, listen, mesh, mesh_chat, mesh_merge, repository_history, users}; use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage, TranscriptMessage}; #[derive(OpenApi)] @@ -94,6 +95,10 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage contract_chat::contract_chat_handler, contract_chat::get_contract_chat_history, contract_chat::clear_contract_chat_history, + // Repository history/settings endpoints + repository_history::list_repository_history, + repository_history::get_repository_suggestions, + repository_history::delete_repository_history, ), components( schemas( @@ -166,6 +171,10 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage AddLocalRepositoryRequest, CreateManagedRepositoryRequest, ChangePhaseRequest, + // Repository history schemas + RepositoryHistoryEntry, + RepositoryHistoryListResponse, + RepositorySuggestionsQuery, ) ), tags( @@ -175,6 +184,7 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage (name = "Contracts", description = "Contract management with workflow phases"), (name = "API Keys", description = "API key management for programmatic access"), (name = "Users", description = "User account management"), + (name = "Settings", description = "User settings including repository history"), ) )] pub struct ApiDoc; |
