diff --git a/migrations/sqlite-drizzle/0020_pin_session_name.sql b/migrations/sqlite-drizzle/0020_pin_session_name.sql new file mode 100644 index 00000000000..7971945061c --- /dev/null +++ b/migrations/sqlite-drizzle/0020_pin_session_name.sql @@ -0,0 +1 @@ +ALTER TABLE `agent_session` ADD `is_name_manually_edited` integer DEFAULT false NOT NULL; diff --git a/migrations/sqlite-drizzle/meta/_journal.json b/migrations/sqlite-drizzle/meta/_journal.json index d4b149b9699..c33315b8a7b 100644 --- a/migrations/sqlite-drizzle/meta/_journal.json +++ b/migrations/sqlite-drizzle/meta/_journal.json @@ -140,6 +140,13 @@ "when": 1777636561235, "tag": "0019_parallel_annihilus", "breakpoints": true + }, + { + "idx": 20, + "version": "6", + "when": 1778064000000, + "tag": "0020_pin_session_name", + "breakpoints": true } ], "version": "7" diff --git a/packages/shared/data/api/schemas/agents.ts b/packages/shared/data/api/schemas/agents.ts index 9fc03fb2eeb..78fe0dfd2ed 100644 --- a/packages/shared/data/api/schemas/agents.ts +++ b/packages/shared/data/api/schemas/agents.ts @@ -131,7 +131,8 @@ export const AGENT_MUTABLE_FIELDS = { /** Pick-set for session mutable fields — superset of AGENT_MUTABLE_FIELDS. */ export const SESSION_MUTABLE_FIELDS = { ...AGENT_MUTABLE_FIELDS, - slashCommands: true + slashCommands: true, + isNameManuallyEdited: true } as const export const AgentEntitySchema = AgentBaseSchema.extend({ @@ -152,6 +153,7 @@ export const AgentSessionEntitySchema = AgentBaseSchema.extend({ agentId: z.string(), agentType: z.enum(['claude-code']), slashCommands: z.array(SlashCommandSchema).optional(), + isNameManuallyEdited: z.boolean().optional(), createdAt: z.string(), updatedAt: z.string() }) diff --git a/src/main/data/db/schemas/agentSession.ts b/src/main/data/db/schemas/agentSession.ts index dda6b881833..1bb0a7b00e3 100644 --- a/src/main/data/db/schemas/agentSession.ts +++ b/src/main/data/db/schemas/agentSession.ts @@ -24,6 +24,7 @@ export const agentSessionTable = sqliteTable( allowedTools: text({ mode: 'json' }).$type().notNull().default(sql`'[]'`), slashCommands: text({ mode: 'json' }).$type().notNull().default(sql`'[]'`), configuration: text({ mode: 'json' }).$type().notNull().default(sql`'{}'`), + isNameManuallyEdited: integer({ mode: 'boolean' }).notNull().default(false), sortOrder: integer().notNull().default(0), ...createUpdateTimestamps }, diff --git a/src/main/data/services/AgentSessionService.ts b/src/main/data/services/AgentSessionService.ts index 276c2458684..67a1465e814 100644 --- a/src/main/data/services/AgentSessionService.ts +++ b/src/main/data/services/AgentSessionService.ts @@ -62,6 +62,7 @@ function rowToSession(row: SessionRow): AgentSessionEntity { agentType: (row.agentType === 'cherry-claw' ? 'claude-code' : row.agentType) as AgentType, accessiblePaths: row.accessiblePaths, configuration: parseConfiguration(row.configuration), + isNameManuallyEdited: row.isNameManuallyEdited, createdAt: timestampToISO(row.createdAt), updatedAt: timestampToISO(row.updatedAt) } diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index eb80b658b18..06e5d9485c7 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -1483,6 +1483,7 @@ "move_to": "Move to", "new": "New Topic", "pin": "Pin Topic", + "pin_name": "Pin Name", "prompt": { "edit": { "title": "Edit Topic Prompts" @@ -1495,7 +1496,8 @@ "title": "Search" }, "title": "Topics", - "unpin": "Unpin Topic" + "unpin": "Unpin Topic", + "unpin_name": "Unpin Name" }, "translate": "Translate", "user": "User", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 157a5d556c1..ac7c196f345 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -1483,6 +1483,7 @@ "move_to": "移动到", "new": "开始新对话", "pin": "固定话题", + "pin_name": "固定名称", "prompt": { "edit": { "title": "编辑话题提示词" @@ -1495,7 +1496,8 @@ "title": "搜索" }, "title": "话题", - "unpin": "取消固定" + "unpin": "取消固定", + "unpin_name": "取消固定名称" }, "translate": "翻译", "user": "用户", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index f68ca41b061..a8f68bdfe36 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -1483,6 +1483,7 @@ "move_to": "移動到", "new": "開始新對話", "pin": "固定話題", + "pin_name": "固定名稱", "prompt": { "edit": { "title": "編輯話題提示詞" @@ -1495,7 +1496,8 @@ "title": "搜尋" }, "title": "話題", - "unpin": "取消固定" + "unpin": "取消固定", + "unpin_name": "取消固定名稱" }, "translate": "翻譯", "user": "使用者", diff --git a/src/renderer/src/pages/agents/components/SessionItem.tsx b/src/renderer/src/pages/agents/components/SessionItem.tsx index 8dd12f704cb..ff79167ad9c 100644 --- a/src/renderer/src/pages/agents/components/SessionItem.tsx +++ b/src/renderer/src/pages/agents/components/SessionItem.tsx @@ -19,7 +19,7 @@ import { getChannelTypeIcon } from '@renderer/utils/agentSession' import { buildAgentSessionTopicId } from '@renderer/utils/agentSession' import type { MenuProps } from 'antd' import { Dropdown } from 'antd' -import { MenuIcon, Sparkles, XIcon } from 'lucide-react' +import { MenuIcon, PinIcon, PinOffIcon, Sparkles, XIcon } from 'lucide-react' import React, { memo, startTransition, useDeferredValue, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -49,7 +49,7 @@ const SessionItem = ({ session, agentId, channelType, onDelete, onPress }: Sessi const { isEditing, isSaving, startEdit, inputProps } = useInPlaceEdit({ onSave: async (value) => { if (value !== session.name) { - await updateSession({ id: session.id, name: value }) + await updateSession({ id: session.id, name: value, isNameManuallyEdited: true }) } } }) @@ -151,6 +151,14 @@ const SessionItem = ({ session, agentId, channelType, onDelete, onPress }: Sessi } } }, + { + label: session.isNameManuallyEdited ? t('chat.topics.unpin_name') : t('chat.topics.pin_name'), + key: 'pin-name', + icon: session.isNameManuallyEdited ? : , + onClick: async () => { + await updateSession({ id: session.id, isNameManuallyEdited: !session.isNameManuallyEdited }) + } + }, { label: t('settings.topic.position.label'), key: 'topic-position', @@ -178,7 +186,17 @@ const SessionItem = ({ session, agentId, channelType, onDelete, onPress }: Sessi } } ], - [agentId, dispatch, onDelete, session.id, sessionTopicId, setTopicPosition, t, targetSession.id] + [ + agentId, + dispatch, + onDelete, + session.id, + session.isNameManuallyEdited, + sessionTopicId, + setTopicPosition, + t, + targetSession.id + ] ) return ( @@ -203,6 +221,9 @@ const SessionItem = ({ session, agentId, channelType, onDelete, onPress }: Sessi ) : ( <> + {session.isNameManuallyEdited && ( + + )} {channelIcon && } diff --git a/src/renderer/src/pages/home/Tabs/components/Topics.tsx b/src/renderer/src/pages/home/Tabs/components/Topics.tsx index 2393a102fe7..d575c181150 100644 --- a/src/renderer/src/pages/home/Tabs/components/Topics.tsx +++ b/src/renderer/src/pages/home/Tabs/components/Topics.tsx @@ -307,6 +307,16 @@ export const Topics: React.FC = ({ assistant: _assistant, activeTopic, se } } }, + { + label: topic.isNameManuallyEdited ? t('chat.topics.unpin_name') : t('chat.topics.pin_name'), + key: 'pin-name', + icon: topic.isNameManuallyEdited ? : , + disabled: isRenaming(topic.id), + onClick() { + const updatedTopic = { ...topic, isNameManuallyEdited: !topic.isNameManuallyEdited } + updateTopic(updatedTopic) + } + }, { label: t('chat.topics.prompt.label'), key: 'topic-prompt', @@ -661,19 +671,24 @@ export const Topics: React.FC = ({ assistant: _assistant, activeTopic, se {editingTopicId === topic.id && isEditing ? ( e.stopPropagation()} /> ) : ( - { - setEditingTopicId(topic.id) - startEdit(topic.name) - } - }> - {topicName} - + <> + {topic.isNameManuallyEdited && ( + + )} + { + setEditingTopicId(topic.id) + startEdit(topic.name) + } + }> + {topicName} + + )} {!topic.pinned && ( & { id: string } +export type UpdateSessionForm = Partial & { id: string; isNameManuallyEdited?: boolean } export type SessionForm = CreateSessionForm | UpdateSessionForm