feat(sidebar): add per-folder color swatch with picker and neutral conversation rail

- Add `color` column to folder table with migration backfill and hash-based assignment on folder creation
- Expose `update_folder_color` via Tauri command and `/update_folder_color` HTTP route
- Render a color swatch before each folder name in the sidebar header; offer a 10-color palette (9 hues plus a theme-aware foreground sentinel) through the folder context menu
- Show the folder header "new conversation" button only on hover
- Drop the expanded-state tint on folder name and count badge; use a fixed neutral rail color for conversation items
This commit is contained in:
xintaofei
2026-04-23 23:02:58 +08:00
parent b7eeeb0be4
commit 1eeb5041a8
23 changed files with 300 additions and 31 deletions

View File

@@ -567,6 +567,19 @@ pub async fn reorder_folders(
.map_err(AppCommandError::from)
}
#[cfg(feature = "tauri-runtime")]
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
pub async fn update_folder_color(
db: tauri::State<'_, AppDatabase>,
folder_id: i32,
color: String,
) -> Result<crate::models::FolderDetail, AppCommandError> {
folder_service::update_folder_color(&db.conn, folder_id, &color)
.await
.map_err(AppCommandError::from)?
.ok_or_else(|| AppCommandError::not_found("Folder not found"))
}
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
pub async fn create_folder_directory(path: String) -> Result<(), AppCommandError> {
std::fs::create_dir_all(&path).map_err(AppCommandError::io)

View File

@@ -16,6 +16,7 @@ pub struct Model {
pub deleted_at: Option<DateTimeUtc>,
pub is_open: bool,
pub sort_order: i32,
pub color: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -0,0 +1,60 @@
use sea_orm_migration::prelude::*;
use sea_orm_migration::sea_orm::{ConnectionTrait, DbBackend, Statement};
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Folder::Table)
.add_column(
ColumnDef::new(Folder::Color)
.string()
.not_null()
.default("#22c55e"),
)
.to_owned(),
)
.await?;
// Backfill existing rows with a palette color chosen by (id - 1) mod N,
// so existing workspaces get visually distinct swatches after migration.
let conn = manager.get_connection();
let sql = "UPDATE folder SET color = CASE ((id - 1) % 9) \
WHEN 0 THEN '#ef4444' \
WHEN 1 THEN '#f97316' \
WHEN 2 THEN '#eab308' \
WHEN 3 THEN '#84cc16' \
WHEN 4 THEN '#22c55e' \
WHEN 5 THEN '#06b6d4' \
WHEN 6 THEN '#8b5cf6' \
WHEN 7 THEN '#d946ef' \
WHEN 8 THEN '#ec4899' \
END";
conn.execute(Statement::from_string(DbBackend::Sqlite, sql.to_string()))
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Folder::Table)
.drop_column(Folder::Color)
.to_owned(),
)
.await
}
}
#[derive(DeriveIden)]
enum Folder {
Table,
Color,
}

View File

@@ -12,6 +12,7 @@ mod m20260406_000001_agent_setting_model_provider;
mod m20260420_000001_opened_tabs;
mod m20260422_000001_folder_sort_order;
mod m20260423_000001_drop_folder_parent_branch;
mod m20260424_000001_folder_color;
pub struct Migrator;
#[async_trait::async_trait]
@@ -30,6 +31,7 @@ impl MigratorTrait for Migrator {
Box::new(m20260420_000001_opened_tabs::Migration),
Box::new(m20260422_000001_folder_sort_order::Migration),
Box::new(m20260423_000001_drop_folder_parent_branch::Migration),
Box::new(m20260424_000001_folder_color::Migration),
]
}
}

View File

@@ -10,6 +10,35 @@ use crate::db::error::DbError;
use crate::models::agent::AgentType;
use crate::models::{FolderDetail, FolderHistoryEntry};
/// Palette kept in sync with the frontend swatch picker. Changes here must be
/// mirrored in `src/components/conversations/sidebar-conversation-list.tsx`.
/// `"foreground"` is a theme-aware sentinel the frontend resolves to
/// `var(--sidebar-foreground)`.
pub const FOLDER_COLOR_PALETTE: &[&str] = &[
"#ef4444",
"#f97316",
"#eab308",
"#84cc16",
"#22c55e",
"#06b6d4",
"#8b5cf6",
"#d946ef",
"#ec4899",
"foreground",
];
fn pick_folder_color(folder_id: i32, folder_name: &str) -> String {
let mut name_hash: u32 = 0;
for c in folder_name.chars() {
name_hash = name_hash.wrapping_mul(31).wrapping_add(c as u32);
}
let combined = (folder_id as u32)
.wrapping_mul(2654435761)
.wrapping_add(name_hash);
let idx = (combined as usize) % FOLDER_COLOR_PALETTE.len();
FOLDER_COLOR_PALETTE[idx].to_string()
}
fn to_entry(m: folder::Model) -> FolderHistoryEntry {
FolderHistoryEntry {
id: m.id,
@@ -34,6 +63,7 @@ fn to_detail(m: folder::Model) -> FolderDetail {
default_agent_type,
last_opened_at: m.last_opened_at,
sort_order: m.sort_order,
color: m.color,
}
}
@@ -81,7 +111,7 @@ pub async fn add_folder(
.unwrap_or(0);
let active = folder::ActiveModel {
id: NotSet,
name: Set(name),
name: Set(name.clone()),
path: Set(path.to_string()),
git_branch: Set(None),
default_agent_type: Set(None),
@@ -91,13 +121,42 @@ pub async fn add_folder(
deleted_at: Set(None),
is_open: Set(true),
sort_order: Set(max_order + 1),
// Temporary placeholder — we overwrite below with a hash derived
// from the final auto-assigned id so each new folder gets a
// deterministic, well-distributed palette color.
color: Set(FOLDER_COLOR_PALETTE[0].to_string()),
};
active.insert(conn).await?
let inserted = active.insert(conn).await?;
let assigned = pick_folder_color(inserted.id, &name);
let mut active = inserted.into_active_model();
active.color = Set(assigned);
active.update(conn).await?
};
Ok(to_entry(model))
}
pub async fn update_folder_color(
conn: &DatabaseConnection,
folder_id: i32,
color: &str,
) -> Result<Option<FolderDetail>, DbError> {
let row = folder::Entity::find_by_id(folder_id)
.filter(folder::Column::DeletedAt.is_null())
.one(conn)
.await?;
let Some(row) = row else {
return Ok(None);
};
let mut active = row.into_active_model();
active.color = Set(color.to_string());
active.updated_at = Set(Utc::now());
let updated = active.update(conn).await?;
Ok(Some(to_detail(updated)))
}
pub async fn list_folders(conn: &DatabaseConnection) -> Result<Vec<FolderHistoryEntry>, DbError> {
let rows = folder::Entity::find()
.filter(folder::Column::DeletedAt.is_null())

View File

@@ -221,6 +221,7 @@ mod tauri_app {
folders::open_folder_by_id,
folders::remove_folder_from_workspace,
folders::reorder_folders,
folders::update_folder_color,
folders::add_folder_to_history,
folders::remove_folder_from_history,
folders::create_folder_directory,

View File

@@ -20,6 +20,7 @@ pub struct FolderDetail {
pub default_agent_type: Option<AgentType>,
pub last_opened_at: DateTime<Utc>,
pub sort_order: i32,
pub color: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]

View File

@@ -140,6 +140,25 @@ pub async fn reorder_folders(
Ok(Json(()))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateFolderColorParams {
pub folder_id: i32,
pub color: String,
}
pub async fn update_folder_color(
Extension(state): Extension<Arc<AppState>>,
Json(params): Json<UpdateFolderColorParams>,
) -> Result<Json<FolderDetail>, AppCommandError> {
let db = &state.db;
let folder = folder_service::update_folder_color(&db.conn, params.folder_id, &params.color)
.await
.map_err(AppCommandError::from)?
.ok_or_else(|| AppCommandError::not_found("Folder not found"))?;
Ok(Json(folder))
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PathParams {

View File

@@ -107,6 +107,10 @@ pub fn build_router(state: Arc<AppState>, token: String, static_dir: std::path::
post(handlers::folders::remove_folder_from_workspace),
)
.route("/reorder_folders", post(handlers::folders::reorder_folders))
.route(
"/update_folder_color",
post(handlers::folders::update_folder_color),
)
.route(
"/add_folder_to_history",
post(handlers::folders::add_folder_to_history),

View File

@@ -125,10 +125,7 @@ export const SidebarConversationCard = memo(function SidebarConversationCard({
<span
aria-hidden
className={cn(
"pointer-events-none absolute z-0",
isOpenInTab
? "bg-sidebar-primary/85"
: "bg-sidebar-primary/30"
"pointer-events-none absolute z-0 bg-sidebar-border"
)}
style={{
top: "-0.0625rem",

View File

@@ -22,6 +22,7 @@ import {
GitBranch,
ListChecks,
Loader2,
Palette,
Plus,
Rocket,
XCircle,
@@ -36,6 +37,7 @@ import {
openProjectBootWindow,
updateConversationTitle,
updateConversationStatus,
updateFolderColor,
deleteConversation,
} from "@/lib/api"
import { isDesktop, openFileDialog } from "@/lib/platform"
@@ -58,6 +60,9 @@ import {
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
} from "@/components/ui/context-menu"
import {
AlertDialog,
@@ -106,6 +111,31 @@ function compareByCreatedAtDesc(
return right.id - left.id
}
// Sentinel stored in the DB that resolves to the current sidebar foreground
// color — the swatch then always reads as the folder name does, across themes.
const FOREGROUND_SWATCH = "foreground"
// Kept in sync with Rust-side `FOLDER_COLOR_PALETTE` in
// `src-tauri/src/db/service/folder_service.rs`. Nine well-separated hues
// spanning the color wheel (skipping the blue band that reads as muddy),
// plus a theme-aware neutral that tracks the sidebar text color.
const FOLDER_SWATCH_PALETTE = [
"#ef4444", // red
"#f97316", // orange
"#eab308", // yellow
"#84cc16", // lime
"#22c55e", // green
"#06b6d4", // cyan
"#8b5cf6", // violet
"#d946ef", // fuchsia
"#ec4899", // pink
FOREGROUND_SWATCH,
] as const
function resolveSwatchColor(swatch: string): string {
return swatch === FOREGROUND_SWATCH ? "var(--sidebar-foreground)" : swatch
}
function formatRelative(iso: string): string {
const ts = parseTimestamp(iso)
if (!ts) return ""
@@ -129,11 +159,13 @@ const FolderHeader = memo(function FolderHeader({
count,
expanded,
importing,
color,
onToggle,
onRemoveFromWorkspace,
onNewConversation,
onImport,
onManageConversations,
onChangeColor,
isDragging,
t,
}: {
@@ -142,11 +174,13 @@ const FolderHeader = memo(function FolderHeader({
count: number
expanded: boolean
importing: boolean
color: string
onToggle: (folderId: number) => void
onRemoveFromWorkspace: (folderId: number) => void
onNewConversation: (folderId: number) => void
onImport: (folderId: number) => void
onManageConversations: (folderId: number) => void
onChangeColor: (folderId: number, color: string) => void
isDragging?: boolean
t: ReturnType<typeof useTranslations>
}) {
@@ -156,7 +190,7 @@ const FolderHeader = memo(function FolderHeader({
<div className={cn("relative h-[2rem]", isDragging && "opacity-60")}>
<div
className={cn(
"flex h-[1.9375rem] w-full items-center",
"group flex h-[1.9375rem] w-full items-center",
"rounded-full",
"transition-colors duration-150",
isDragging
@@ -193,12 +227,15 @@ const FolderHeader = memo(function FolderHeader({
<ChevronRight className="h-[0.6875rem] w-[0.6875rem]" />
)}
</span>
<div className="flex min-w-0 flex-1 items-center gap-[0.375rem]">
<div className="flex min-w-0 flex-1 items-center gap-[0.5rem]">
<span
aria-hidden
className="inline-block h-[0.5rem] w-[0.5rem] shrink-0 rounded-[0.125rem]"
style={{ backgroundColor: resolveSwatchColor(color) }}
/>
<span
className={cn(
"min-w-0 flex-shrink truncate text-left text-[0.875rem] font-semibold tracking-[-0.00625rem]",
"transition-colors duration-150",
expanded && "text-sidebar-primary"
"min-w-0 flex-shrink truncate text-left text-[0.875rem] font-semibold tracking-[-0.00625rem]"
)}
>
{folderName}
@@ -208,10 +245,7 @@ const FolderHeader = memo(function FolderHeader({
"inline-flex shrink-0 items-center justify-center",
"h-[0.9375rem] min-w-[1rem] rounded-[0.3125rem] px-[0.25rem]",
"text-[0.625rem] font-semibold leading-none tabular-nums",
"transition-colors duration-150",
expanded
? "bg-sidebar-primary/20 text-sidebar-primary"
: "bg-[color-mix(in_oklab,var(--sidebar-accent),var(--sidebar-foreground)_6%)] text-muted-foreground/80"
"bg-[color-mix(in_oklab,var(--sidebar-accent),var(--sidebar-foreground)_6%)] text-muted-foreground/80"
)}
>
{count}
@@ -229,10 +263,8 @@ const FolderHeader = memo(function FolderHeader({
className={cn(
"mr-[0.25rem] flex h-[1.25rem] w-[1.25rem] shrink-0 items-center justify-center",
"rounded-[0.25rem] cursor-pointer outline-none text-muted-foreground/80",
"transition-colors duration-150",
expanded
? "hover:text-sidebar-primary"
: "hover:text-sidebar-foreground"
"opacity-0 group-hover:opacity-100 focus-visible:opacity-100",
"transition-opacity duration-150 hover:text-sidebar-foreground"
)}
>
<Plus className="h-[0.75rem] w-[0.75rem]" />
@@ -257,6 +289,35 @@ const FolderHeader = memo(function FolderHeader({
<ListChecks className="h-4 w-4" />
{t("folderHeaderMenu.manageConversations")}
</ContextMenuItem>
<ContextMenuSub>
<ContextMenuSubTrigger>
<Palette className="h-4 w-4" />
{t("folderHeaderMenu.changeColor")}
</ContextMenuSubTrigger>
<ContextMenuSubContent className="min-w-[9rem] p-2">
<div className="grid grid-cols-10 gap-1">
{FOLDER_SWATCH_PALETTE.map((swatch) => {
const active = swatch.toLowerCase() === color.toLowerCase()
return (
<button
key={swatch}
type="button"
title={swatch}
aria-label={swatch}
onClick={() => onChangeColor(folderId, swatch)}
className={cn(
"h-[1.125rem] w-[1.125rem] cursor-pointer rounded-[0.25rem]",
"outline-none ring-offset-1 ring-offset-popover",
"transition-[box-shadow,transform] duration-100 hover:scale-110",
active && "ring-2 ring-foreground/60"
)}
style={{ backgroundColor: resolveSwatchColor(swatch) }}
/>
)
})}
</div>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSeparator />
<ContextMenuItem
variant="destructive"
@@ -281,11 +342,13 @@ interface FolderGroupItemProps {
sortMode: SidebarSortMode
selectedConversation: { id: number; agentType: string } | null
openTabConversationKeys: Set<string>
color: string
onToggle: (folderId: number) => void
onRemoveFromWorkspace: (folderId: number) => void
onNewConversationForFolder: (folderId: number) => void
onImport: (folderId: number) => void
onManageConversations: (folderId: number) => void
onChangeColor: (folderId: number, color: string) => void
onSelect: (id: number, agentType: string) => void
onDoubleClick: (id: number, agentType: string) => void
onRename: (id: number, newTitle: string) => Promise<void>
@@ -311,11 +374,13 @@ function FolderGroupItem({
sortMode,
selectedConversation,
openTabConversationKeys,
color,
onToggle,
onRemoveFromWorkspace,
onNewConversationForFolder,
onImport,
onManageConversations,
onChangeColor,
onSelect,
onDoubleClick,
onRename,
@@ -379,11 +444,13 @@ function FolderGroupItem({
count={conversations.length}
expanded={expanded}
importing={importing}
color={color}
onToggle={handleToggle}
onRemoveFromWorkspace={onRemoveFromWorkspace}
onNewConversation={onNewConversationForFolder}
onImport={onImport}
onManageConversations={onManageConversations}
onChangeColor={onChangeColor}
isDragging={dragging}
t={t}
/>
@@ -449,6 +516,7 @@ export function SidebarConversationList({
removeFolderFromWorkspace,
reorderFolders,
openFolder,
refreshFolder,
} = useAppWorkspace()
const refreshing = loading
const { activeFolder } = useActiveFolder()
@@ -464,8 +532,9 @@ export function SidebarConversationList({
const { addTask, updateTask } = useTaskContext()
const folderIndex = useMemo(() => {
const map = new Map<number, { name: string; path: string }>()
for (const f of allFolders) map.set(f.id, { name: f.name, path: f.path })
const map = new Map<number, { name: string; path: string; color: string }>()
for (const f of allFolders)
map.set(f.id, { name: f.name, path: f.path, color: f.color })
return map
}, [allFolders])
@@ -513,6 +582,19 @@ export function SidebarConversationList({
setFolderExpanded(loadFolderExpanded())
}, [])
const handleChangeFolderColor = useCallback(
async (folderId: number, color: string) => {
try {
await updateFolderColor(folderId, color)
await refreshFolder(folderId)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
toast.error(t("toasts.changeFolderColorFailed", { message: msg }))
}
},
[refreshFolder, t]
)
const scrollRootRef = useRef<OverlayScrollbarsComponentRef>(null)
const scrollToActiveRef = useRef<() => void>(() => {})
const pendingScrollRef = useRef(false)
@@ -961,6 +1043,7 @@ export function SidebarConversationList({
sortMode={sortMode}
selectedConversation={selectedConversation}
openTabConversationKeys={openTabConversationKeys}
color={folderIndex.get(folderId)?.color ?? "#22c55e"}
onToggle={toggleFolder}
onRemoveFromWorkspace={handleRemoveFolder}
onNewConversationForFolder={
@@ -968,6 +1051,7 @@ export function SidebarConversationList({
}
onImport={handleImportForFolder}
onManageConversations={handleManageConversations}
onChangeColor={handleChangeFolderColor}
onSelect={handleSelect}
onDoubleClick={handleDoubleClick}
onRename={handleRename}

View File

@@ -782,7 +782,8 @@
"folderRemoved": "تمت إزالة المجلد {name}",
"openFolderFailed": "فشل فتح المجلد",
"removeFolderFailed": "فشل إزالة المجلد: {message}",
"reorderFoldersFailed": "فشل إعادة ترتيب المجلدات: {message}"
"reorderFoldersFailed": "فشل إعادة ترتيب المجلدات: {message}",
"changeFolderColorFailed": "فشل تغيير اللون: {message}"
},
"statsLabel": "{folders} مجلدات · {convos} محادثة",
"reorderHandle": "اسحب لإعادة الترتيب",
@@ -802,6 +803,7 @@
"removeFolderConfirmDescription": "إزالة \"{name}\" من مساحة العمل؟ سيتم إغلاق علامات التبويب والمحطات المرتبطة.",
"folderHeaderMenu": {
"manageConversations": "إدارة المحادثات…",
"changeColor": "تغيير اللون",
"removeFromWorkspace": "إزالة من مساحة العمل"
},
"manageConversations": {

View File

@@ -782,7 +782,8 @@
"folderRemoved": "Ordner {name} entfernt",
"openFolderFailed": "Ordner konnte nicht geöffnet werden",
"removeFolderFailed": "Ordner konnte nicht entfernt werden: {message}",
"reorderFoldersFailed": "Ordner konnten nicht neu sortiert werden: {message}"
"reorderFoldersFailed": "Ordner konnten nicht neu sortiert werden: {message}",
"changeFolderColorFailed": "Farbe konnte nicht geändert werden: {message}"
},
"statsLabel": "{folders} Ordner · {convos} Konversationen",
"reorderHandle": "Zum Neuordnen ziehen",
@@ -802,6 +803,7 @@
"removeFolderConfirmDescription": "\"{name}\" aus dem Arbeitsbereich entfernen? Zugehörige Tabs und Terminals werden geschlossen.",
"folderHeaderMenu": {
"manageConversations": "Konversationen verwalten…",
"changeColor": "Farbe ändern",
"removeFromWorkspace": "Aus Arbeitsbereich entfernen"
},
"manageConversations": {

View File

@@ -782,7 +782,8 @@
"folderRemoved": "Removed folder {name}",
"openFolderFailed": "Failed to open folder",
"removeFolderFailed": "Failed to remove folder: {message}",
"reorderFoldersFailed": "Failed to reorder folders: {message}"
"reorderFoldersFailed": "Failed to reorder folders: {message}",
"changeFolderColorFailed": "Failed to change folder color: {message}"
},
"statsLabel": "{folders} folders · {convos} conversations",
"reorderHandle": "Drag to reorder",
@@ -802,6 +803,7 @@
"removeFolderConfirmDescription": "Remove \"{name}\" from the workspace? Its tabs and terminals will close.",
"folderHeaderMenu": {
"manageConversations": "Manage conversations…",
"changeColor": "Change color",
"removeFromWorkspace": "Remove from workspace"
},
"manageConversations": {

View File

@@ -782,7 +782,8 @@
"folderRemoved": "Carpeta {name} eliminada",
"openFolderFailed": "Error al abrir carpeta",
"removeFolderFailed": "Error al eliminar carpeta: {message}",
"reorderFoldersFailed": "Error al reordenar carpetas: {message}"
"reorderFoldersFailed": "Error al reordenar carpetas: {message}",
"changeFolderColorFailed": "Error al cambiar el color: {message}"
},
"statsLabel": "{folders} carpetas · {convos} conversaciones",
"reorderHandle": "Arrastrar para reordenar",
@@ -802,6 +803,7 @@
"removeFolderConfirmDescription": "¿Eliminar \"{name}\" del espacio de trabajo? Sus pestañas y terminales se cerrarán.",
"folderHeaderMenu": {
"manageConversations": "Gestionar conversaciones…",
"changeColor": "Cambiar color",
"removeFromWorkspace": "Quitar del espacio de trabajo"
},
"manageConversations": {

View File

@@ -782,7 +782,8 @@
"folderRemoved": "Dossier {name} retiré",
"openFolderFailed": "Échec de l'ouverture du dossier",
"removeFolderFailed": "Échec de la suppression du dossier : {message}",
"reorderFoldersFailed": "Échec du réordonnancement des dossiers : {message}"
"reorderFoldersFailed": "Échec du réordonnancement des dossiers : {message}",
"changeFolderColorFailed": "Échec du changement de couleur : {message}"
},
"statsLabel": "{folders} dossiers · {convos} conversations",
"reorderHandle": "Glisser pour réorganiser",
@@ -802,6 +803,7 @@
"removeFolderConfirmDescription": "Retirer \"{name}\" de l'espace de travail ? Les onglets et terminaux associés seront fermés.",
"folderHeaderMenu": {
"manageConversations": "Gérer les conversations…",
"changeColor": "Changer la couleur",
"removeFromWorkspace": "Retirer de l'espace de travail"
},
"manageConversations": {

View File

@@ -782,7 +782,8 @@
"folderRemoved": "フォルダ {name} を削除しました",
"openFolderFailed": "フォルダを開けませんでした",
"removeFolderFailed": "フォルダの削除に失敗しました: {message}",
"reorderFoldersFailed": "フォルダの並べ替えに失敗しました: {message}"
"reorderFoldersFailed": "フォルダの並べ替えに失敗しました: {message}",
"changeFolderColorFailed": "色の変更に失敗しました: {message}"
},
"statsLabel": "{folders} フォルダ · {convos} 会話",
"reorderHandle": "ドラッグして並べ替え",
@@ -802,6 +803,7 @@
"removeFolderConfirmDescription": "\"{name}\" をワークスペースから削除しますか?関連するタブとターミナルが閉じられます。",
"folderHeaderMenu": {
"manageConversations": "会話の管理…",
"changeColor": "色を変更",
"removeFromWorkspace": "ワークスペースから削除"
},
"manageConversations": {

View File

@@ -782,7 +782,8 @@
"folderRemoved": "폴더 {name}을(를) 제거했습니다",
"openFolderFailed": "폴더를 열 수 없습니다",
"removeFolderFailed": "폴더 제거 실패: {message}",
"reorderFoldersFailed": "폴더 순서 변경 실패: {message}"
"reorderFoldersFailed": "폴더 순서 변경 실패: {message}",
"changeFolderColorFailed": "색상 변경 실패: {message}"
},
"statsLabel": "{folders}개 폴더 · {convos}개 대화",
"reorderHandle": "드래그하여 순서 변경",
@@ -802,6 +803,7 @@
"removeFolderConfirmDescription": "워크스페이스에서 \"{name}\"을(를) 제거하시겠습니까? 관련 탭과 터미널이 닫힙니다.",
"folderHeaderMenu": {
"manageConversations": "대화 관리…",
"changeColor": "색상 변경",
"removeFromWorkspace": "워크스페이스에서 제거"
},
"manageConversations": {

View File

@@ -782,7 +782,8 @@
"folderRemoved": "Pasta {name} removida",
"openFolderFailed": "Falha ao abrir pasta",
"removeFolderFailed": "Falha ao remover pasta: {message}",
"reorderFoldersFailed": "Falha ao reordenar pastas: {message}"
"reorderFoldersFailed": "Falha ao reordenar pastas: {message}",
"changeFolderColorFailed": "Falha ao alterar a cor: {message}"
},
"statsLabel": "{folders} pastas · {convos} conversas",
"reorderHandle": "Arraste para reordenar",
@@ -802,6 +803,7 @@
"removeFolderConfirmDescription": "Remover \"{name}\" do espaço de trabalho? As abas e terminais relacionados serão fechados.",
"folderHeaderMenu": {
"manageConversations": "Gerenciar conversas…",
"changeColor": "Alterar cor",
"removeFromWorkspace": "Remover do espaço de trabalho"
},
"manageConversations": {

View File

@@ -782,7 +782,8 @@
"folderRemoved": "已移除文件夹 {name}",
"openFolderFailed": "打开文件夹失败",
"removeFolderFailed": "移除文件夹失败:{message}",
"reorderFoldersFailed": "重新排序文件夹失败:{message}"
"reorderFoldersFailed": "重新排序文件夹失败:{message}",
"changeFolderColorFailed": "修改颜色失败:{message}"
},
"statsLabel": "{folders} 个文件夹 · {convos} 个会话",
"reorderHandle": "拖拽排序",
@@ -802,6 +803,7 @@
"removeFolderConfirmDescription": "从工作区移除 \"{name}\"?其相关 Tab 与终端将会关闭。",
"folderHeaderMenu": {
"manageConversations": "会话管理…",
"changeColor": "修改颜色",
"removeFromWorkspace": "从工作区移除"
},
"manageConversations": {

View File

@@ -782,7 +782,8 @@
"folderRemoved": "已移除資料夾 {name}",
"openFolderFailed": "開啟資料夾失敗",
"removeFolderFailed": "移除資料夾失敗:{message}",
"reorderFoldersFailed": "重新排序資料夾失敗:{message}"
"reorderFoldersFailed": "重新排序資料夾失敗:{message}",
"changeFolderColorFailed": "修改顏色失敗:{message}"
},
"statsLabel": "{folders} 個資料夾 · {convos} 個對話",
"reorderHandle": "拖拽排序",
@@ -802,6 +803,7 @@
"removeFolderConfirmDescription": "從工作區移除 \"{name}\"?相關分頁與終端機將會關閉。",
"folderHeaderMenu": {
"manageConversations": "會話管理…",
"changeColor": "修改顏色",
"removeFromWorkspace": "從工作區移除"
},
"manageConversations": {

View File

@@ -644,6 +644,13 @@ export async function reorderFolders(ids: number[]): Promise<void> {
return getTransport().call("reorder_folders", { ids })
}
export async function updateFolderColor(
folderId: number,
color: string
): Promise<FolderDetail> {
return getTransport().call("update_folder_color", { folderId, color })
}
export async function importLocalConversations(
folderId: number
): Promise<ImportResult> {

View File

@@ -163,6 +163,7 @@ export interface FolderDetail {
default_agent_type: AgentType | null
last_opened_at: string
sort_order: number
color: string
}
export interface OpenedTab {