feat(settings): add quick messages management with drag-and-drop sorting
Adds a new "Quick Messages" settings page below Experts for managing reusable title/content snippets, backed by SQLite via SeaORM and exposed through both Tauri commands and the Axum web router. The list supports drag-to-reorder using the same motion/react Reorder pattern as the agent list, with translations provided across all 10 supported locales.
This commit is contained in:
@@ -9,6 +9,7 @@ pub mod model_provider;
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
pub mod notification;
|
||||
pub mod project_boot;
|
||||
pub mod quick_messages;
|
||||
pub mod system_settings;
|
||||
pub mod terminal;
|
||||
pub mod version_control;
|
||||
|
||||
55
src-tauri/src/commands/quick_messages.rs
Normal file
55
src-tauri/src/commands/quick_messages.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
use crate::db::error::DbError;
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
use crate::db::service::quick_message_service;
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
use crate::db::AppDatabase;
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
use crate::models::QuickMessageInfo;
|
||||
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn quick_messages_list(
|
||||
db: tauri::State<'_, AppDatabase>,
|
||||
) -> Result<Vec<QuickMessageInfo>, DbError> {
|
||||
quick_message_service::list(&db.conn).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn quick_messages_create(
|
||||
db: tauri::State<'_, AppDatabase>,
|
||||
title: String,
|
||||
content: String,
|
||||
) -> Result<QuickMessageInfo, DbError> {
|
||||
quick_message_service::create(&db.conn, &title, &content).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn quick_messages_update(
|
||||
db: tauri::State<'_, AppDatabase>,
|
||||
id: i32,
|
||||
title: Option<String>,
|
||||
content: Option<String>,
|
||||
) -> Result<QuickMessageInfo, DbError> {
|
||||
quick_message_service::update(&db.conn, id, title, content).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn quick_messages_delete(
|
||||
db: tauri::State<'_, AppDatabase>,
|
||||
id: i32,
|
||||
) -> Result<(), DbError> {
|
||||
quick_message_service::delete(&db.conn, id).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn quick_messages_reorder(
|
||||
db: tauri::State<'_, AppDatabase>,
|
||||
ids: Vec<i32>,
|
||||
) -> Result<(), DbError> {
|
||||
quick_message_service::reorder(&db.conn, ids).await
|
||||
}
|
||||
@@ -9,3 +9,4 @@ pub mod folder_command;
|
||||
pub mod model_provider;
|
||||
pub mod opened_tab;
|
||||
pub mod prelude;
|
||||
pub mod quick_message;
|
||||
|
||||
@@ -10,3 +10,4 @@ pub use super::folder::Entity as Folder;
|
||||
pub use super::folder_command::Entity as FolderCommand;
|
||||
pub use super::model_provider::Entity as ModelProvider;
|
||||
pub use super::opened_tab::Entity as OpenedTab;
|
||||
pub use super::quick_message::Entity as QuickMessage;
|
||||
|
||||
19
src-tauri/src/db/entities/quick_message.rs
Normal file
19
src-tauri/src/db/entities/quick_message.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "quick_message")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub title: String,
|
||||
#[sea_orm(column_type = "Text")]
|
||||
pub content: String,
|
||||
pub sort_order: i32,
|
||||
pub created_at: DateTimeUtc,
|
||||
pub updated_at: DateTimeUtc,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
70
src-tauri/src/db/migration/m20260424_000002_quick_message.rs
Normal file
70
src-tauri/src/db/migration/m20260424_000002_quick_message.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(QuickMessage::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(QuickMessage::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(QuickMessage::Title).string().not_null())
|
||||
.col(ColumnDef::new(QuickMessage::Content).text().not_null())
|
||||
.col(
|
||||
ColumnDef::new(QuickMessage::SortOrder)
|
||||
.integer()
|
||||
.not_null()
|
||||
.default(0),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(QuickMessage::CreatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(QuickMessage::UpdatedAt)
|
||||
.timestamp_with_time_zone()
|
||||
.not_null(),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_quick_message_sort_order")
|
||||
.table(QuickMessage::Table)
|
||||
.col(QuickMessage::SortOrder)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(QuickMessage::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum QuickMessage {
|
||||
Table,
|
||||
Id,
|
||||
Title,
|
||||
Content,
|
||||
SortOrder,
|
||||
CreatedAt,
|
||||
UpdatedAt,
|
||||
}
|
||||
@@ -13,6 +13,7 @@ mod m20260420_000001_opened_tabs;
|
||||
mod m20260422_000001_folder_sort_order;
|
||||
mod m20260423_000001_drop_folder_parent_branch;
|
||||
mod m20260424_000001_folder_color;
|
||||
mod m20260424_000002_quick_message;
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -32,6 +33,7 @@ impl MigratorTrait for Migrator {
|
||||
Box::new(m20260422_000001_folder_sort_order::Migration),
|
||||
Box::new(m20260423_000001_drop_folder_parent_branch::Migration),
|
||||
Box::new(m20260424_000001_folder_color::Migration),
|
||||
Box::new(m20260424_000002_quick_message::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,6 @@ pub mod folder_command_service;
|
||||
pub mod folder_service;
|
||||
pub mod import_service;
|
||||
pub mod model_provider_service;
|
||||
pub mod quick_message_service;
|
||||
pub mod sender_context_service;
|
||||
pub mod tab_service;
|
||||
|
||||
114
src-tauri/src/db/service/quick_message_service.rs
Normal file
114
src-tauri/src/db/service/quick_message_service.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use chrono::Utc;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use sea_orm::{
|
||||
ActiveModelTrait, ActiveValue::NotSet, ConnectionTrait, DbBackend, EntityTrait,
|
||||
IntoActiveModel, QueryOrder, Set, Statement,
|
||||
};
|
||||
|
||||
use crate::db::entities::quick_message;
|
||||
use crate::db::error::DbError;
|
||||
use crate::models::QuickMessageInfo;
|
||||
|
||||
fn to_info(m: quick_message::Model) -> QuickMessageInfo {
|
||||
QuickMessageInfo {
|
||||
id: m.id,
|
||||
title: m.title,
|
||||
content: m.content,
|
||||
sort_order: m.sort_order,
|
||||
created_at: m.created_at,
|
||||
updated_at: m.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list(conn: &DatabaseConnection) -> Result<Vec<QuickMessageInfo>, DbError> {
|
||||
let rows = quick_message::Entity::find()
|
||||
.order_by_asc(quick_message::Column::SortOrder)
|
||||
.all(conn)
|
||||
.await?;
|
||||
|
||||
Ok(rows.into_iter().map(to_info).collect())
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
conn: &DatabaseConnection,
|
||||
title: &str,
|
||||
content: &str,
|
||||
) -> Result<QuickMessageInfo, DbError> {
|
||||
let now = Utc::now();
|
||||
|
||||
let max_order = quick_message::Entity::find()
|
||||
.order_by_desc(quick_message::Column::SortOrder)
|
||||
.one(conn)
|
||||
.await?
|
||||
.map(|m| m.sort_order)
|
||||
.unwrap_or(-1);
|
||||
|
||||
let active = quick_message::ActiveModel {
|
||||
id: NotSet,
|
||||
title: Set(title.to_string()),
|
||||
content: Set(content.to_string()),
|
||||
sort_order: Set(max_order + 1),
|
||||
created_at: Set(now),
|
||||
updated_at: Set(now),
|
||||
};
|
||||
|
||||
let model = active.insert(conn).await?;
|
||||
Ok(to_info(model))
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
conn: &DatabaseConnection,
|
||||
id: i32,
|
||||
title: Option<String>,
|
||||
content: Option<String>,
|
||||
) -> Result<QuickMessageInfo, DbError> {
|
||||
let row = quick_message::Entity::find_by_id(id)
|
||||
.one(conn)
|
||||
.await?
|
||||
.ok_or_else(|| DbError::Migration(format!("QuickMessage {} not found", id)))?;
|
||||
|
||||
let mut active = row.into_active_model();
|
||||
if let Some(t) = title {
|
||||
active.title = Set(t);
|
||||
}
|
||||
if let Some(c) = content {
|
||||
active.content = Set(c);
|
||||
}
|
||||
active.updated_at = Set(Utc::now());
|
||||
|
||||
let model = active.update(conn).await?;
|
||||
Ok(to_info(model))
|
||||
}
|
||||
|
||||
pub async fn delete(conn: &DatabaseConnection, id: i32) -> Result<(), DbError> {
|
||||
quick_message::Entity::delete_by_id(id).exec(conn).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reorder(conn: &DatabaseConnection, ids: Vec<i32>) -> Result<(), DbError> {
|
||||
if ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let now = Utc::now();
|
||||
let now_str = now.format("%Y-%m-%d %H:%M:%S %:z").to_string();
|
||||
let case_expr = ids
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, id)| format!("WHEN {} THEN {}", id, idx))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let id_list = ids
|
||||
.iter()
|
||||
.map(|id| id.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
let sql = format!(
|
||||
"UPDATE quick_message SET sort_order = CASE id {case_expr} END, updated_at = '{now_str}' WHERE id IN ({id_list})"
|
||||
);
|
||||
conn.execute(Statement::from_string(DbBackend::Sqlite, sql))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -24,7 +24,8 @@ mod tauri_app {
|
||||
use crate::commands::{
|
||||
acp as acp_commands, chat_channel as chat_channel_commands, conversations,
|
||||
experts as experts_commands, folder_commands, folders, mcp as mcp_commands,
|
||||
model_provider as model_provider_commands, notification, project_boot, system_settings,
|
||||
model_provider as model_provider_commands, notification, project_boot,
|
||||
quick_messages as quick_messages_commands, system_settings,
|
||||
terminal as terminal_commands, version_control, windows,
|
||||
workspace_state as workspace_state_commands,
|
||||
};
|
||||
@@ -354,6 +355,11 @@ mod tauri_app {
|
||||
folder_commands::delete_folder_command,
|
||||
folder_commands::reorder_folder_commands,
|
||||
folder_commands::bootstrap_folder_commands_from_package_json,
|
||||
quick_messages_commands::quick_messages_list,
|
||||
quick_messages_commands::quick_messages_create,
|
||||
quick_messages_commands::quick_messages_update,
|
||||
quick_messages_commands::quick_messages_delete,
|
||||
quick_messages_commands::quick_messages_reorder,
|
||||
terminal_commands::terminal_spawn,
|
||||
terminal_commands::terminal_write,
|
||||
terminal_commands::terminal_resize,
|
||||
|
||||
@@ -4,6 +4,7 @@ pub mod conversation;
|
||||
pub mod folder;
|
||||
pub mod message;
|
||||
pub mod model_provider;
|
||||
pub mod quick_message;
|
||||
pub mod system;
|
||||
|
||||
pub use agent::AgentType;
|
||||
@@ -15,6 +16,7 @@ pub use conversation::{
|
||||
SidebarData,
|
||||
};
|
||||
pub use folder::{FolderCommandInfo, FolderDetail, FolderHistoryEntry, OpenedTab};
|
||||
pub use quick_message::QuickMessageInfo;
|
||||
pub use message::{
|
||||
AgentExecutionStats, AgentToolCall, ContentBlock, MessageRole, MessageTurn, TurnRole,
|
||||
TurnUsage, UnifiedMessage,
|
||||
|
||||
12
src-tauri/src/models/quick_message.rs
Normal file
12
src-tauri/src/models/quick_message.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct QuickMessageInfo {
|
||||
pub id: i32,
|
||||
pub title: String,
|
||||
pub content: String,
|
||||
pub sort_order: i32,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
@@ -10,6 +10,7 @@ pub mod git;
|
||||
pub mod mcp;
|
||||
pub mod model_provider;
|
||||
pub mod project_boot;
|
||||
pub mod quick_messages;
|
||||
pub mod system_settings;
|
||||
pub mod terminal;
|
||||
pub mod version_control;
|
||||
|
||||
86
src-tauri/src/web/handlers/quick_messages.rs
Normal file
86
src-tauri/src/web/handlers/quick_messages.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{extract::Extension, Json};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::app_error::AppCommandError;
|
||||
use crate::app_state::AppState;
|
||||
use crate::db::service::quick_message_service;
|
||||
use crate::models::QuickMessageInfo;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateQuickMessageParams {
|
||||
pub title: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateQuickMessageParams {
|
||||
pub id: i32,
|
||||
pub title: Option<String>,
|
||||
pub content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeleteQuickMessageParams {
|
||||
pub id: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReorderQuickMessagesParams {
|
||||
pub ids: Vec<i32>,
|
||||
}
|
||||
|
||||
pub async fn quick_messages_list(
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<Vec<QuickMessageInfo>>, AppCommandError> {
|
||||
let result = quick_message_service::list(&state.db.conn)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub async fn quick_messages_create(
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<CreateQuickMessageParams>,
|
||||
) -> Result<Json<QuickMessageInfo>, AppCommandError> {
|
||||
let result = quick_message_service::create(&state.db.conn, ¶ms.title, ¶ms.content)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub async fn quick_messages_update(
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<UpdateQuickMessageParams>,
|
||||
) -> Result<Json<QuickMessageInfo>, AppCommandError> {
|
||||
let result =
|
||||
quick_message_service::update(&state.db.conn, params.id, params.title, params.content)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub async fn quick_messages_delete(
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<DeleteQuickMessageParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
quick_message_service::delete(&state.db.conn, params.id)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
pub async fn quick_messages_reorder(
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<ReorderQuickMessagesParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
quick_message_service::reorder(&state.db.conn, params.ids)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
Ok(Json(()))
|
||||
}
|
||||
@@ -631,6 +631,27 @@ pub fn build_router(state: Arc<AppState>, token: String, static_dir: std::path::
|
||||
"/delete_model_provider",
|
||||
post(handlers::model_provider::delete_model_provider),
|
||||
)
|
||||
// ─── Quick Messages ───
|
||||
.route(
|
||||
"/quick_messages_list",
|
||||
post(handlers::quick_messages::quick_messages_list),
|
||||
)
|
||||
.route(
|
||||
"/quick_messages_create",
|
||||
post(handlers::quick_messages::quick_messages_create),
|
||||
)
|
||||
.route(
|
||||
"/quick_messages_update",
|
||||
post(handlers::quick_messages::quick_messages_update),
|
||||
)
|
||||
.route(
|
||||
"/quick_messages_delete",
|
||||
post(handlers::quick_messages::quick_messages_delete),
|
||||
)
|
||||
.route(
|
||||
"/quick_messages_reorder",
|
||||
post(handlers::quick_messages::quick_messages_reorder),
|
||||
)
|
||||
// ─── Terminal ───
|
||||
.route("/terminal_spawn", post(handlers::terminal::terminal_spawn))
|
||||
.route("/terminal_write", post(handlers::terminal::terminal_write))
|
||||
|
||||
Reference in New Issue
Block a user