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

View File

@@ -0,0 +1,5 @@
import { QuickMessagesSettings } from "@/components/settings/quick-messages-settings"
export default function SettingsQuickMessagesPage() {
return <QuickMessagesSettings />
}

View File

@@ -0,0 +1,583 @@
"use client"
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
type PointerEvent,
type ReactNode,
} from "react"
import { GripVertical, Loader2, Plus, Save, Trash2 } from "lucide-react"
import { Reorder, useDragControls } from "motion/react"
import { useTranslations } from "next-intl"
import { toast } from "sonner"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable"
import { Textarea } from "@/components/ui/textarea"
import { cn } from "@/lib/utils"
import {
quickMessagesCreate,
quickMessagesDelete,
quickMessagesList,
quickMessagesReorder,
quickMessagesUpdate,
} from "@/lib/api"
import type { QuickMessage } from "@/lib/types"
const LEFT_MIN_WIDTH = 280
const RIGHT_MIN_WIDTH = 420
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value))
}
function toPercent(pixels: number, totalPixels: number): number {
if (totalPixels <= 0) return 0
return (pixels / totalPixels) * 100
}
interface QuickMessageReorderItemProps {
message: QuickMessage
selected: boolean
reordering: boolean
onSelect: (id: number) => void
onDragStart: () => void
onDragEnd: () => void
children: (
startDrag: (event: PointerEvent<HTMLButtonElement>) => void
) => ReactNode
}
function QuickMessageReorderItem({
message,
selected,
reordering,
onSelect,
onDragStart,
onDragEnd,
children,
}: QuickMessageReorderItemProps) {
const dragControls = useDragControls()
const startDrag = useCallback(
(event: PointerEvent<HTMLButtonElement>) => {
event.preventDefault()
event.stopPropagation()
dragControls.start(event)
},
[dragControls]
)
return (
<Reorder.Item
as="section"
value={message}
data-quick-message-id={message.id}
drag={reordering ? false : "y"}
dragListener={false}
dragControls={dragControls}
dragMomentum={false}
layout="position"
className={cn(
"rounded-lg border bg-card p-2.5 transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40",
selected && "border-primary/60 bg-primary/5"
)}
tabIndex={0}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onClick={() => onSelect(message.id)}
onKeyDown={(event) => {
if (event.target !== event.currentTarget) return
if (event.key !== "Enter" && event.key !== " ") return
event.preventDefault()
onSelect(message.id)
}}
>
{children(startDrag)}
</Reorder.Item>
)
}
export function QuickMessagesSettings() {
const t = useTranslations("QuickMessagesSettings")
const [messages, setMessages] = useState<QuickMessage[]>([])
const [loading, setLoading] = useState(true)
const [loadError, setLoadError] = useState<string | null>(null)
const [selectedId, setSelectedId] = useState<number | null>(null)
const [searchQuery, setSearchQuery] = useState("")
const [draftTitle, setDraftTitle] = useState("")
const [draftContent, setDraftContent] = useState("")
const [saving, setSaving] = useState(false)
const [creating, setCreating] = useState(false)
const [deleteTargetId, setDeleteTargetId] = useState<number | null>(null)
const [deleting, setDeleting] = useState(false)
const [reordering, setReordering] = useState(false)
const pendingOrderRef = useRef<number[] | null>(null)
const panelContainerRef = useRef<HTMLDivElement | null>(null)
const [panelContainerWidth, setPanelContainerWidth] = useState(0)
const titleInputRef = useRef<HTMLInputElement | null>(null)
const refresh = useCallback(async () => {
setLoading(true)
setLoadError(null)
try {
const list = await quickMessagesList()
setMessages(list)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
setLoadError(message)
setMessages([])
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
refresh().catch((err) => {
console.error("[QuickMessagesSettings] initial refresh failed:", err)
})
}, [refresh])
useEffect(() => {
const container = panelContainerRef.current
if (!container) return
const updateWidth = (next: number) => {
setPanelContainerWidth((prev) =>
Math.abs(prev - next) < 1 ? prev : next
)
}
updateWidth(container.getBoundingClientRect().width)
const observer = new ResizeObserver((entries) => {
updateWidth(
entries[0]?.contentRect.width ?? container.getBoundingClientRect().width
)
})
observer.observe(container)
return () => {
observer.disconnect()
}
}, [])
const selectedMessage = useMemo(
() => messages.find((m) => m.id === selectedId) ?? null,
[messages, selectedId]
)
useEffect(() => {
if (selectedMessage) {
setDraftTitle(selectedMessage.title)
setDraftContent(selectedMessage.content)
} else {
setDraftTitle("")
setDraftContent("")
}
}, [selectedMessage])
useEffect(() => {
if (selectedId === null && messages.length > 0) {
setSelectedId(messages[0].id)
}
}, [selectedId, messages])
const filteredMessages = useMemo(() => {
const q = searchQuery.trim().toLowerCase()
if (!q) return messages
return messages.filter(
(m) =>
m.title.toLowerCase().includes(q) || m.content.toLowerCase().includes(q)
)
}, [messages, searchQuery])
const isDirty = useMemo(() => {
if (!selectedMessage) return false
return (
draftTitle !== selectedMessage.title ||
draftContent !== selectedMessage.content
)
}, [selectedMessage, draftTitle, draftContent])
const persistReorder = useCallback(
async (ids: number[]) => {
if (ids.length === 0) return
setReordering(true)
try {
await quickMessagesReorder(ids)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(t("toasts.saveOrderFailed"), { description: message })
await refresh()
} finally {
setReordering(false)
}
},
[refresh, t]
)
const handleReorder = useCallback((next: QuickMessage[]) => {
const reordered = next.map((m, index) => ({ ...m, sort_order: index }))
setMessages(reordered)
pendingOrderRef.current = reordered.map((m) => m.id)
}, [])
const handleCreate = useCallback(async () => {
setCreating(true)
try {
const created = await quickMessagesCreate({ title: "", content: "" })
setMessages((prev) => [...prev, created])
setSelectedId(created.id)
toast.success(t("toasts.created"))
requestAnimationFrame(() => {
titleInputRef.current?.focus()
})
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(t("toasts.createFailed"), { description: message })
} finally {
setCreating(false)
}
}, [t])
const handleSave = useCallback(async () => {
if (!selectedMessage) return
setSaving(true)
try {
const updated = await quickMessagesUpdate({
id: selectedMessage.id,
title: draftTitle,
content: draftContent,
})
setMessages((prev) =>
prev.map((m) => (m.id === updated.id ? updated : m))
)
toast.success(t("toasts.saved"))
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(t("toasts.saveFailed"), { description: message })
} finally {
setSaving(false)
}
}, [selectedMessage, draftTitle, draftContent, t])
const handleDelete = useCallback(async () => {
if (deleteTargetId === null) return
const target = deleteTargetId
setDeleting(true)
try {
await quickMessagesDelete(target)
setMessages((prev) => {
const next = prev.filter((m) => m.id !== target)
if (selectedId === target) {
setSelectedId(next[0]?.id ?? null)
}
return next
})
toast.success(t("toasts.deleted"))
setDeleteTargetId(null)
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
toast.error(t("toasts.deleteFailed"), { description: message })
} finally {
setDeleting(false)
}
}, [deleteTargetId, selectedId, t])
const safeContainerWidth =
panelContainerWidth > 0 ? panelContainerWidth : 1200
const leftMinSize = clamp(
toPercent(LEFT_MIN_WIDTH, safeContainerWidth),
5,
95
)
const rightMinSize = clamp(
toPercent(RIGHT_MIN_WIDTH, safeContainerWidth),
5,
95
)
const leftMaxSize = Math.max(leftMinSize, 100 - rightMinSize)
if (loading) {
return (
<div className="h-full flex items-center justify-center text-sm text-muted-foreground">
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
{t("loading")}
</div>
)
}
const deleteTargetMessage =
deleteTargetId !== null
? (messages.find((m) => m.id === deleteTargetId) ?? null)
: null
return (
<div className="h-full flex flex-col p-3 md:p-4">
<div className="flex items-center justify-between gap-3 pb-4">
<div>
<h2 className="text-base font-semibold">{t("title")}</h2>
<p className="text-xs text-muted-foreground mt-1">
{t("description")}
</p>
</div>
</div>
{loadError && (
<div className="mb-3 rounded-md border border-red-500/30 bg-red-500/5 px-3 py-2 text-xs text-red-400">
{loadError}
</div>
)}
<div ref={panelContainerRef} className="flex-1 min-h-0 min-w-0">
<ResizablePanelGroup
direction="horizontal"
className="h-full min-h-0 min-w-0"
>
<ResizablePanel
defaultSize={34}
minSize={leftMinSize}
maxSize={leftMaxSize}
>
<div className="min-h-0 h-full min-w-0 rounded-lg border bg-card flex flex-col overflow-hidden lg:rounded-r-none">
<div className="border-b p-3 space-y-2.5">
<div className="flex items-center gap-2">
<Input
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
placeholder={t("searchPlaceholder")}
/>
<Button
size="sm"
onClick={() => {
handleCreate().catch((err) => {
console.error(
"[QuickMessagesSettings] create failed:",
err
)
})
}}
disabled={creating}
>
{creating ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Plus className="h-3.5 w-3.5" />
)}
{t("actions.new")}
</Button>
</div>
</div>
{filteredMessages.length === 0 ? (
<div className="flex-1 flex items-center justify-center text-xs text-muted-foreground px-4 text-center">
{messages.length === 0
? t("emptyList")
: t("searchPlaceholder")}
</div>
) : (
<Reorder.Group
as="div"
axis="y"
values={messages}
onReorder={handleReorder}
className="flex-1 min-h-0 overflow-y-auto space-y-2 p-2"
>
{filteredMessages.map((m) => (
<QuickMessageReorderItem
key={m.id}
message={m}
selected={selectedId === m.id}
reordering={reordering}
onSelect={(id) => setSelectedId(id)}
onDragStart={() => {
/* no-op: list re-render handles dragging state */
}}
onDragEnd={() => {
const order = pendingOrderRef.current
pendingOrderRef.current = null
if (order && !reordering) {
persistReorder(order).catch((err) => {
console.error(
"[QuickMessagesSettings] reorder failed:",
err
)
})
}
}}
>
{(startDrag) => (
<div className="flex items-center gap-2 overflow-hidden">
<button
type="button"
className="text-muted-foreground cursor-grab active:cursor-grabbing rounded p-0.5 hover:bg-muted"
title={t("actions.dragSort")}
aria-label={t("actions.dragSortMessage", {
name: m.title || t("untitled"),
})}
onPointerDown={startDrag}
onClick={(event) => event.stopPropagation()}
disabled={reordering}
>
<GripVertical className="h-3.5 w-3.5" />
</button>
<div className="min-w-0 flex-1">
<div className="text-sm font-medium truncate">
{m.title || (
<span className="italic text-muted-foreground">
{t("untitled")}
</span>
)}
</div>
{m.content && (
<div className="text-[11px] text-muted-foreground truncate mt-0.5">
{m.content}
</div>
)}
</div>
</div>
)}
</QuickMessageReorderItem>
))}
</Reorder.Group>
)}
</div>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={66} minSize={rightMinSize}>
<div className="h-full flex-1 min-h-0 min-w-0 rounded-lg border bg-card overflow-hidden lg:rounded-l-none lg:border-l-0">
{selectedMessage ? (
<div className="h-full flex flex-col">
<div className="flex-1 overflow-y-auto p-4 space-y-4">
<div className="space-y-1.5">
<Label htmlFor="quick-message-title" className="text-xs">
{t("fields.title")}
</Label>
<Input
id="quick-message-title"
ref={titleInputRef}
value={draftTitle}
onChange={(event) => setDraftTitle(event.target.value)}
placeholder={t("fields.titlePlaceholder")}
/>
</div>
<div className="space-y-1.5">
<Label
htmlFor="quick-message-content"
className="text-xs"
>
{t("fields.content")}
</Label>
<Textarea
id="quick-message-content"
value={draftContent}
onChange={(event) =>
setDraftContent(event.target.value)
}
placeholder={t("fields.contentPlaceholder")}
className="min-h-[260px]"
/>
</div>
</div>
<div className="border-t px-4 py-3 flex items-center justify-between gap-2">
<Button
size="sm"
variant="outline"
onClick={() => setDeleteTargetId(selectedMessage.id)}
className="text-red-500 hover:text-red-500"
>
<Trash2 className="h-3.5 w-3.5" />
{t("actions.delete")}
</Button>
<Button
size="sm"
onClick={() => {
handleSave().catch((err) => {
console.error(
"[QuickMessagesSettings] save failed:",
err
)
})
}}
disabled={saving || !isDirty}
>
{saving ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Save className="h-3.5 w-3.5" />
)}
{t("actions.save")}
</Button>
</div>
</div>
) : (
<div className="h-full flex items-center justify-center text-xs text-muted-foreground">
{t("emptySelection")}
</div>
)}
</div>
</ResizablePanel>
</ResizablePanelGroup>
</div>
<AlertDialog
open={deleteTargetId !== null}
onOpenChange={(open) => {
if (!open) setDeleteTargetId(null)
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("confirmDelete.title")}</AlertDialogTitle>
<AlertDialogDescription>
{t("confirmDelete.message", {
name: deleteTargetMessage?.title || t("untitled"),
})}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={deleting}>
{t("confirmDelete.cancel")}
</AlertDialogCancel>
<AlertDialogAction
onClick={(event) => {
event.preventDefault()
handleDelete().catch((err) => {
console.error("[QuickMessagesSettings] delete failed:", err)
})
}}
disabled={deleting}
>
{deleting ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : null}
{t("confirmDelete.confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
)
}

View File

@@ -14,6 +14,7 @@ import {
Globe,
Keyboard,
Menu,
MessageSquareText,
SendHorizontal,
Palette,
PlugZap,
@@ -41,6 +42,7 @@ interface SettingsNavItem {
| "mcp"
| "skills"
| "experts"
| "quick_messages"
| "shortcuts"
| "version_control"
| "chat_channels"
@@ -70,6 +72,11 @@ const SETTINGS_NAV_ITEMS: SettingsNavItem[] = [
labelKey: "experts",
icon: Sparkles,
},
{
href: "/settings/quick-messages",
labelKey: "quick_messages",
icon: MessageSquareText,
},
{
href: "/settings/agents",
labelKey: "agents",

View File

@@ -47,7 +47,8 @@
"chat_channels": "قنوات المحادثة",
"web_service": "خدمة الويب",
"model_providers": "مزودو النماذج",
"experts": "الخبراء"
"experts": "الخبراء",
"quick_messages": "رسائل سريعة"
}
},
"AppearanceSettings": {
@@ -2108,5 +2109,43 @@
"disableFailed": "فشل تعطيل الخبير",
"openFolderFailed": "فشل فتح المجلد"
}
},
"QuickMessagesSettings": {
"title": "رسائل سريعة",
"description": "إدارة مقتطفات الرسائل القابلة لإعادة الاستخدام. اسحب لإعادة الترتيب.",
"loading": "جارٍ تحميل الرسائل السريعة…",
"emptyList": "لا توجد رسائل سريعة بعد. انقر على \"جديد\" لإنشاء واحدة.",
"emptySelection": "اختر رسالة سريعة للتعديل.",
"searchPlaceholder": "ابحث حسب العنوان أو المحتوى",
"untitled": "بدون عنوان",
"actions": {
"new": "جديد",
"save": "حفظ",
"delete": "حذف",
"dragSort": "اسحب لإعادة الترتيب",
"dragSortMessage": "اسحب لإعادة ترتيب الرسالة السريعة: {name}"
},
"fields": {
"title": "العنوان",
"titlePlaceholder": "أعطِ هذه الرسالة عنوانًا قصيرًا",
"content": "المحتوى",
"contentPlaceholder": "اكتب محتوى الرسالة هنا"
},
"confirmDelete": {
"title": "حذف الرسالة السريعة؟",
"message": "سيؤدي ذلك إلى حذف \"{name}\" نهائيًا. هل أنت متأكد؟",
"cancel": "إلغاء",
"confirm": "حذف"
},
"toasts": {
"loadFailed": "فشل تحميل الرسائل السريعة",
"createFailed": "فشل إنشاء الرسالة السريعة",
"saveFailed": "فشل حفظ الرسالة السريعة",
"deleteFailed": "فشل حذف الرسالة السريعة",
"saveOrderFailed": "فشل حفظ الترتيب",
"created": "تم إنشاء الرسالة السريعة",
"saved": "تم حفظ الرسالة السريعة",
"deleted": "تم حذف الرسالة السريعة"
}
}
}

View File

@@ -47,7 +47,8 @@
"chat_channels": "Chat-Kanäle",
"web_service": "Webdienst",
"model_providers": "Modellanbieter",
"experts": "Experten"
"experts": "Experten",
"quick_messages": "Schnellnachrichten"
}
},
"AppearanceSettings": {
@@ -2108,5 +2109,43 @@
"disableFailed": "Deaktivieren des Experten fehlgeschlagen",
"openFolderFailed": "Ordner konnte nicht geöffnet werden"
}
},
"QuickMessagesSettings": {
"title": "Schnellnachrichten",
"description": "Verwalte wiederverwendbare Nachrichtenbausteine. Ziehe zum Neuordnen.",
"loading": "Schnellnachrichten werden geladen…",
"emptyList": "Noch keine Schnellnachrichten. Klicke auf \"Neu\", um eine zu erstellen.",
"emptySelection": "Wähle eine Schnellnachricht zum Bearbeiten aus.",
"searchPlaceholder": "Nach Titel oder Inhalt suchen",
"untitled": "Ohne Titel",
"actions": {
"new": "Neu",
"save": "Speichern",
"delete": "Löschen",
"dragSort": "Zum Neuordnen ziehen",
"dragSortMessage": "Schnellnachricht neu ordnen: {name}"
},
"fields": {
"title": "Titel",
"titlePlaceholder": "Gib dieser Nachricht einen kurzen Titel",
"content": "Inhalt",
"contentPlaceholder": "Gib hier den Nachrichteninhalt ein"
},
"confirmDelete": {
"title": "Schnellnachricht löschen?",
"message": "Dadurch wird \"{name}\" dauerhaft gelöscht. Bist du sicher?",
"cancel": "Abbrechen",
"confirm": "Löschen"
},
"toasts": {
"loadFailed": "Laden der Schnellnachrichten fehlgeschlagen",
"createFailed": "Erstellen der Schnellnachricht fehlgeschlagen",
"saveFailed": "Speichern der Schnellnachricht fehlgeschlagen",
"deleteFailed": "Löschen der Schnellnachricht fehlgeschlagen",
"saveOrderFailed": "Reihenfolge konnte nicht gespeichert werden",
"created": "Schnellnachricht erstellt",
"saved": "Schnellnachricht gespeichert",
"deleted": "Schnellnachricht gelöscht"
}
}
}

View File

@@ -47,7 +47,8 @@
"chat_channels": "Chat Channels",
"web_service": "Web Service",
"model_providers": "Model Providers",
"experts": "Experts"
"experts": "Experts",
"quick_messages": "Quick Messages"
}
},
"AppearanceSettings": {
@@ -2108,5 +2109,43 @@
"disableFailed": "Failed to disable expert",
"openFolderFailed": "Failed to open folder"
}
},
"QuickMessagesSettings": {
"title": "Quick Messages",
"description": "Manage reusable message snippets. Drag to reorder.",
"loading": "Loading quick messages…",
"emptyList": "No quick messages yet. Click \"New\" to create one.",
"emptySelection": "Select a quick message to edit.",
"searchPlaceholder": "Search by title or content",
"untitled": "Untitled",
"actions": {
"new": "New",
"save": "Save",
"delete": "Delete",
"dragSort": "Drag to reorder",
"dragSortMessage": "Drag to reorder quick message: {name}"
},
"fields": {
"title": "Title",
"titlePlaceholder": "Give this message a short title",
"content": "Content",
"contentPlaceholder": "Write the message content here"
},
"confirmDelete": {
"title": "Delete quick message?",
"message": "This will permanently delete \"{name}\". Are you sure?",
"cancel": "Cancel",
"confirm": "Delete"
},
"toasts": {
"loadFailed": "Failed to load quick messages",
"createFailed": "Failed to create quick message",
"saveFailed": "Failed to save quick message",
"deleteFailed": "Failed to delete quick message",
"saveOrderFailed": "Failed to save order",
"created": "Quick message created",
"saved": "Quick message saved",
"deleted": "Quick message deleted"
}
}
}

View File

@@ -47,7 +47,8 @@
"chat_channels": "Canales de chat",
"web_service": "Servicio Web",
"model_providers": "Proveedores de Modelos",
"experts": "Expertos"
"experts": "Expertos",
"quick_messages": "Mensajes rápidos"
}
},
"AppearanceSettings": {
@@ -2108,5 +2109,43 @@
"disableFailed": "Error al deshabilitar el experto",
"openFolderFailed": "Error al abrir la carpeta"
}
},
"QuickMessagesSettings": {
"title": "Mensajes rápidos",
"description": "Gestiona fragmentos de mensajes reutilizables. Arrastra para reordenar.",
"loading": "Cargando mensajes rápidos…",
"emptyList": "Aún no hay mensajes rápidos. Haz clic en \"Nuevo\" para crear uno.",
"emptySelection": "Selecciona un mensaje rápido para editar.",
"searchPlaceholder": "Buscar por título o contenido",
"untitled": "Sin título",
"actions": {
"new": "Nuevo",
"save": "Guardar",
"delete": "Eliminar",
"dragSort": "Arrastra para reordenar",
"dragSortMessage": "Arrastra para reordenar mensaje rápido: {name}"
},
"fields": {
"title": "Título",
"titlePlaceholder": "Asigna un título corto a este mensaje",
"content": "Contenido",
"contentPlaceholder": "Escribe aquí el contenido del mensaje"
},
"confirmDelete": {
"title": "¿Eliminar mensaje rápido?",
"message": "Esto eliminará permanentemente \"{name}\". ¿Estás seguro?",
"cancel": "Cancelar",
"confirm": "Eliminar"
},
"toasts": {
"loadFailed": "Error al cargar mensajes rápidos",
"createFailed": "Error al crear el mensaje rápido",
"saveFailed": "Error al guardar el mensaje rápido",
"deleteFailed": "Error al eliminar el mensaje rápido",
"saveOrderFailed": "Error al guardar el orden",
"created": "Mensaje rápido creado",
"saved": "Mensaje rápido guardado",
"deleted": "Mensaje rápido eliminado"
}
}
}

View File

@@ -47,7 +47,8 @@
"chat_channels": "Canaux de chat",
"web_service": "Service Web",
"model_providers": "Fournisseurs de Modèles",
"experts": "Experts"
"experts": "Experts",
"quick_messages": "Messages rapides"
}
},
"AppearanceSettings": {
@@ -2108,5 +2109,43 @@
"disableFailed": "Échec de la désactivation de l'expert",
"openFolderFailed": "Échec de l'ouverture du dossier"
}
},
"QuickMessagesSettings": {
"title": "Messages rapides",
"description": "Gérez des extraits de messages réutilisables. Glissez pour réorganiser.",
"loading": "Chargement des messages rapides…",
"emptyList": "Aucun message rapide. Cliquez sur « Nouveau » pour en créer un.",
"emptySelection": "Sélectionnez un message rapide à modifier.",
"searchPlaceholder": "Rechercher par titre ou contenu",
"untitled": "Sans titre",
"actions": {
"new": "Nouveau",
"save": "Enregistrer",
"delete": "Supprimer",
"dragSort": "Glisser pour réorganiser",
"dragSortMessage": "Réorganiser le message rapide : {name}"
},
"fields": {
"title": "Titre",
"titlePlaceholder": "Donnez un titre court à ce message",
"content": "Contenu",
"contentPlaceholder": "Saisissez ici le contenu du message"
},
"confirmDelete": {
"title": "Supprimer le message rapide ?",
"message": "Cela supprimera définitivement « {name} ». Êtes-vous sûr ?",
"cancel": "Annuler",
"confirm": "Supprimer"
},
"toasts": {
"loadFailed": "Échec du chargement des messages rapides",
"createFailed": "Échec de la création du message rapide",
"saveFailed": "Échec de l'enregistrement du message rapide",
"deleteFailed": "Échec de la suppression du message rapide",
"saveOrderFailed": "Échec de l'enregistrement de l'ordre",
"created": "Message rapide créé",
"saved": "Message rapide enregistré",
"deleted": "Message rapide supprimé"
}
}
}

View File

@@ -47,7 +47,8 @@
"chat_channels": "チャットチャンネル",
"web_service": "Webサービス",
"model_providers": "モデルプロバイダー",
"experts": "エキスパート"
"experts": "エキスパート",
"quick_messages": "クイックメッセージ"
}
},
"AppearanceSettings": {
@@ -2108,5 +2109,43 @@
"disableFailed": "エキスパートの無効化に失敗しました",
"openFolderFailed": "フォルダを開けませんでした"
}
},
"QuickMessagesSettings": {
"title": "クイックメッセージ",
"description": "再利用可能なメッセージスニペットを管理します。ドラッグして並べ替えできます。",
"loading": "クイックメッセージを読み込み中…",
"emptyList": "クイックメッセージはまだありません。「新規」をクリックして作成してください。",
"emptySelection": "編集するクイックメッセージを選択してください。",
"searchPlaceholder": "タイトルまたは内容で検索",
"untitled": "無題",
"actions": {
"new": "新規",
"save": "保存",
"delete": "削除",
"dragSort": "ドラッグして並べ替え",
"dragSortMessage": "クイックメッセージを並べ替え: {name}"
},
"fields": {
"title": "タイトル",
"titlePlaceholder": "このメッセージに短いタイトルを付けてください",
"content": "内容",
"contentPlaceholder": "ここにメッセージ内容を入力してください"
},
"confirmDelete": {
"title": "クイックメッセージを削除しますか?",
"message": "「{name}」を完全に削除します。よろしいですか?",
"cancel": "キャンセル",
"confirm": "削除"
},
"toasts": {
"loadFailed": "クイックメッセージの読み込みに失敗しました",
"createFailed": "クイックメッセージの作成に失敗しました",
"saveFailed": "クイックメッセージの保存に失敗しました",
"deleteFailed": "クイックメッセージの削除に失敗しました",
"saveOrderFailed": "順序の保存に失敗しました",
"created": "クイックメッセージを作成しました",
"saved": "クイックメッセージを保存しました",
"deleted": "クイックメッセージを削除しました"
}
}
}

View File

@@ -47,7 +47,8 @@
"chat_channels": "채팅 채널",
"web_service": "웹 서비스",
"model_providers": "모델 제공업체",
"experts": "전문가"
"experts": "전문가",
"quick_messages": "빠른 메시지"
}
},
"AppearanceSettings": {
@@ -2108,5 +2109,43 @@
"disableFailed": "전문가 비활성화에 실패했습니다",
"openFolderFailed": "폴더를 열지 못했습니다"
}
},
"QuickMessagesSettings": {
"title": "빠른 메시지",
"description": "재사용 가능한 메시지 스니펫을 관리합니다. 드래그하여 순서를 변경하세요.",
"loading": "빠른 메시지 불러오는 중…",
"emptyList": "빠른 메시지가 없습니다. \"새로 만들기\"를 클릭하여 추가하세요.",
"emptySelection": "편집할 빠른 메시지를 선택하세요.",
"searchPlaceholder": "제목 또는 내용으로 검색",
"untitled": "제목 없음",
"actions": {
"new": "새로 만들기",
"save": "저장",
"delete": "삭제",
"dragSort": "드래그하여 순서 변경",
"dragSortMessage": "빠른 메시지 순서 변경: {name}"
},
"fields": {
"title": "제목",
"titlePlaceholder": "이 메시지에 짧은 제목을 지정하세요",
"content": "내용",
"contentPlaceholder": "여기에 메시지 내용을 입력하세요"
},
"confirmDelete": {
"title": "빠른 메시지를 삭제하시겠습니까?",
"message": "\"{name}\" 이(가) 영구적으로 삭제됩니다. 계속하시겠습니까?",
"cancel": "취소",
"confirm": "삭제"
},
"toasts": {
"loadFailed": "빠른 메시지를 불러오지 못했습니다",
"createFailed": "빠른 메시지를 만들지 못했습니다",
"saveFailed": "빠른 메시지를 저장하지 못했습니다",
"deleteFailed": "빠른 메시지를 삭제하지 못했습니다",
"saveOrderFailed": "순서를 저장하지 못했습니다",
"created": "빠른 메시지가 생성되었습니다",
"saved": "빠른 메시지가 저장되었습니다",
"deleted": "빠른 메시지가 삭제되었습니다"
}
}
}

View File

@@ -47,7 +47,8 @@
"chat_channels": "Canais de chat",
"web_service": "Serviço Web",
"model_providers": "Provedores de Modelos",
"experts": "Especialistas"
"experts": "Especialistas",
"quick_messages": "Mensagens rápidas"
}
},
"AppearanceSettings": {
@@ -2108,5 +2109,43 @@
"disableFailed": "Falha ao desativar o especialista",
"openFolderFailed": "Falha ao abrir a pasta"
}
},
"QuickMessagesSettings": {
"title": "Mensagens rápidas",
"description": "Gerencie trechos de mensagens reutilizáveis. Arraste para reordenar.",
"loading": "Carregando mensagens rápidas…",
"emptyList": "Ainda não há mensagens rápidas. Clique em \"Nova\" para criar uma.",
"emptySelection": "Selecione uma mensagem rápida para editar.",
"searchPlaceholder": "Pesquisar por título ou conteúdo",
"untitled": "Sem título",
"actions": {
"new": "Nova",
"save": "Salvar",
"delete": "Excluir",
"dragSort": "Arraste para reordenar",
"dragSortMessage": "Arrastar para reordenar mensagem rápida: {name}"
},
"fields": {
"title": "Título",
"titlePlaceholder": "Dê um título curto a esta mensagem",
"content": "Conteúdo",
"contentPlaceholder": "Digite aqui o conteúdo da mensagem"
},
"confirmDelete": {
"title": "Excluir mensagem rápida?",
"message": "Isso excluirá permanentemente \"{name}\". Tem certeza?",
"cancel": "Cancelar",
"confirm": "Excluir"
},
"toasts": {
"loadFailed": "Falha ao carregar mensagens rápidas",
"createFailed": "Falha ao criar mensagem rápida",
"saveFailed": "Falha ao salvar mensagem rápida",
"deleteFailed": "Falha ao excluir mensagem rápida",
"saveOrderFailed": "Falha ao salvar a ordem",
"created": "Mensagem rápida criada",
"saved": "Mensagem rápida salva",
"deleted": "Mensagem rápida excluída"
}
}
}

View File

@@ -47,7 +47,8 @@
"chat_channels": "消息渠道",
"web_service": "Web 服务",
"model_providers": "模型供应商",
"experts": "专家"
"experts": "专家",
"quick_messages": "快捷消息"
}
},
"AppearanceSettings": {
@@ -2108,5 +2109,43 @@
"disableFailed": "禁用专家失败",
"openFolderFailed": "打开目录失败"
}
},
"QuickMessagesSettings": {
"title": "快捷消息",
"description": "管理可复用的消息片段。拖拽以调整顺序。",
"loading": "加载快捷消息…",
"emptyList": "暂无快捷消息。点击“新建”创建一条。",
"emptySelection": "选择一条快捷消息进行编辑。",
"searchPlaceholder": "按标题或内容搜索",
"untitled": "未命名",
"actions": {
"new": "新建",
"save": "保存",
"delete": "删除",
"dragSort": "拖拽排序",
"dragSortMessage": "拖拽排序快捷消息:{name}"
},
"fields": {
"title": "标题",
"titlePlaceholder": "为此消息取一个简短的标题",
"content": "内容",
"contentPlaceholder": "在此输入消息内容"
},
"confirmDelete": {
"title": "删除快捷消息?",
"message": "将永久删除“{name}”。确定吗?",
"cancel": "取消",
"confirm": "删除"
},
"toasts": {
"loadFailed": "加载快捷消息失败",
"createFailed": "创建快捷消息失败",
"saveFailed": "保存快捷消息失败",
"deleteFailed": "删除快捷消息失败",
"saveOrderFailed": "保存顺序失败",
"created": "已创建快捷消息",
"saved": "已保存快捷消息",
"deleted": "已删除快捷消息"
}
}
}

View File

@@ -47,7 +47,8 @@
"chat_channels": "訊息頻道",
"web_service": "Web 服務",
"model_providers": "模型供應商",
"experts": "專家"
"experts": "專家",
"quick_messages": "快捷訊息"
}
},
"AppearanceSettings": {
@@ -2108,5 +2109,43 @@
"disableFailed": "停用專家失敗",
"openFolderFailed": "開啟目錄失敗"
}
},
"QuickMessagesSettings": {
"title": "快捷訊息",
"description": "管理可重用的訊息片段。拖曳以調整順序。",
"loading": "載入快捷訊息…",
"emptyList": "尚無快捷訊息。點擊「新增」建立一條。",
"emptySelection": "選擇一條快捷訊息進行編輯。",
"searchPlaceholder": "依標題或內容搜尋",
"untitled": "未命名",
"actions": {
"new": "新增",
"save": "儲存",
"delete": "刪除",
"dragSort": "拖曳排序",
"dragSortMessage": "拖曳排序快捷訊息:{name}"
},
"fields": {
"title": "標題",
"titlePlaceholder": "為此訊息取一個簡短的標題",
"content": "內容",
"contentPlaceholder": "在此輸入訊息內容"
},
"confirmDelete": {
"title": "刪除快捷訊息?",
"message": "將永久刪除「{name}」。確定嗎?",
"cancel": "取消",
"confirm": "刪除"
},
"toasts": {
"loadFailed": "載入快捷訊息失敗",
"createFailed": "建立快捷訊息失敗",
"saveFailed": "儲存快捷訊息失敗",
"deleteFailed": "刪除快捷訊息失敗",
"saveOrderFailed": "儲存順序失敗",
"created": "已建立快捷訊息",
"saved": "已儲存快捷訊息",
"deleted": "已刪除快捷訊息"
}
}
}

View File

@@ -63,6 +63,7 @@ import type {
ChatChannelMessageLog,
ModelProviderInfo,
PluginCheckSummary,
QuickMessage,
} from "./types"
export async function listConversations(params?: {
@@ -1258,6 +1259,42 @@ export async function bootstrapFolderCommandsFromPackageJson(
})
}
// Quick message management
export async function quickMessagesList(): Promise<QuickMessage[]> {
return getTransport().call("quick_messages_list")
}
export async function quickMessagesCreate(params: {
title: string
content: string
}): Promise<QuickMessage> {
return getTransport().call("quick_messages_create", {
title: params.title,
content: params.content,
})
}
export async function quickMessagesUpdate(params: {
id: number
title?: string
content?: string
}): Promise<QuickMessage> {
return getTransport().call("quick_messages_update", {
id: params.id,
title: params.title ?? null,
content: params.content ?? null,
})
}
export async function quickMessagesDelete(id: number): Promise<void> {
return getTransport().call("quick_messages_delete", { id })
}
export async function quickMessagesReorder(ids: number[]): Promise<void> {
return getTransport().call("quick_messages_reorder", { ids })
}
// Directory browser (for web/server mode)
export async function getHomeDirectory(): Promise<string> {

View File

@@ -769,6 +769,15 @@ export interface FolderCommand {
updated_at: string
}
export interface QuickMessage {
id: number
title: string
content: string
sort_order: number
created_at: string
updated_at: string
}
export interface GitStatusEntry {
status: string
file: string