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:
xintaofei
2026-04-24 10:46:33 +08:00
parent fbe272de4f
commit 61778f152b
30 changed files with 1434 additions and 11 deletions

View File

@@ -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;

View 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
}

View File

@@ -9,3 +9,4 @@ pub mod folder_command;
pub mod model_provider;
pub mod opened_tab;
pub mod prelude;
pub mod quick_message;

View File

@@ -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;

View 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 {}

View 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,
}

View File

@@ -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),
]
}
}

View File

@@ -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;

View 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(())
}

View File

@@ -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,

View File

@@ -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,

View 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>,
}

View File

@@ -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;

View 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, &params.title, &params.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(()))
}

View File

@@ -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))