diff --git a/apps/electron/src/main/ipc.ts b/apps/electron/src/main/ipc.ts index 1073e2f..c0a4cb2 100644 --- a/apps/electron/src/main/ipc.ts +++ b/apps/electron/src/main/ipc.ts @@ -567,6 +567,17 @@ export function registerIpcHandlers(): void { } ) + // 切换 Agent 会话置顶状态 + ipcMain.handle( + AGENT_IPC_CHANNELS.TOGGLE_PIN, + async (_, id: string): Promise => { + const sessions = listAgentSessions() + const current = sessions.find((s) => s.id === id) + if (!current) throw new Error(`Agent 会话不存在: ${id}`) + return updateAgentSessionMeta(id, { pinned: !current.pinned }) + } + ) + // ===== Agent 工作区管理相关 ===== // 确保默认工作区存在 diff --git a/apps/electron/src/main/lib/agent-session-manager.ts b/apps/electron/src/main/lib/agent-session-manager.ts index 563e374..5e05e2d 100644 --- a/apps/electron/src/main/lib/agent-session-manager.ts +++ b/apps/electron/src/main/lib/agent-session-manager.ts @@ -66,11 +66,17 @@ function writeIndex(index: AgentSessionsIndex): void { } /** - * 获取所有会话(按 updatedAt 降序) + * 获取所有会话(置顶项优先,然后按 updatedAt 降序) */ export function listAgentSessions(): AgentSessionMeta[] { const index = readIndex() - return index.sessions.sort((a, b) => b.updatedAt - a.updatedAt) + return index.sessions.sort((a, b) => { + // 置顶项优先 + if (a.pinned && !b.pinned) return -1 + if (!a.pinned && b.pinned) return 1 + // 同为置顶或同为非置顶,按更新时间降序 + return b.updatedAt - a.updatedAt + }) } /** @@ -159,7 +165,7 @@ export function appendAgentMessage(id: string, message: AgentMessage): void { */ export function updateAgentSessionMeta( id: string, - updates: Partial>, + updates: Partial>, ): AgentSessionMeta { const index = readIndex() const idx = index.sessions.findIndex((s) => s.id === id) diff --git a/apps/electron/src/preload/index.ts b/apps/electron/src/preload/index.ts index 20c563f..c05467d 100644 --- a/apps/electron/src/preload/index.ts +++ b/apps/electron/src/preload/index.ts @@ -254,6 +254,9 @@ export interface ElectronAPI { /** 删除 Agent 会话 */ deleteAgentSession: (id: string) => Promise + /** 切换 Agent 会话置顶状态 */ + togglePinAgentSession: (id: string) => Promise + /** 生成 Agent 会话标题 */ generateAgentTitle: (input: AgentGenerateTitleInput) => Promise @@ -660,6 +663,10 @@ const electronAPI: ElectronAPI = { return ipcRenderer.invoke(AGENT_IPC_CHANNELS.DELETE_SESSION, id) }, + togglePinAgentSession: (id: string) => { + return ipcRenderer.invoke(AGENT_IPC_CHANNELS.TOGGLE_PIN, id) + }, + generateAgentTitle: (input: AgentGenerateTitleInput) => { return ipcRenderer.invoke(AGENT_IPC_CHANNELS.GENERATE_TITLE, input) }, diff --git a/apps/electron/src/renderer/components/agent/AgentHeader.tsx b/apps/electron/src/renderer/components/agent/AgentHeader.tsx index 2fd4846..3b98e74 100644 --- a/apps/electron/src/renderer/components/agent/AgentHeader.tsx +++ b/apps/electron/src/renderer/components/agent/AgentHeader.tsx @@ -7,8 +7,11 @@ import * as React from 'react' import { useAtomValue, useSetAtom } from 'jotai' -import { Pencil, Check, X } from 'lucide-react' +import { Pencil, Check, X, Pin } from 'lucide-react' import { currentAgentSessionAtom, agentSessionsAtom } from '@/atoms/agent-atoms' +import { Button } from '@/components/ui/button' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { cn } from '@/lib/utils' export function AgentHeader(): React.ReactElement | null { const session = useAtomValue(currentAgentSessionAtom) @@ -101,6 +104,27 @@ export function AgentHeader(): React.ReactElement | null { )} + + {/* 右侧按钮组 */} +
+ + + + +

{session.pinned ? '取消置顶' : '置顶会话'}

+
+
) } diff --git a/apps/electron/src/renderer/components/app-shell/LeftSidebar.tsx b/apps/electron/src/renderer/components/app-shell/LeftSidebar.tsx index b9a5379..a2ec32f 100644 --- a/apps/electron/src/renderer/components/app-shell/LeftSidebar.tsx +++ b/apps/electron/src/renderer/components/app-shell/LeftSidebar.tsx @@ -141,6 +141,8 @@ export function LeftSidebar({ width }: LeftSidebarProps): React.ReactElement { const [pendingDeleteId, setPendingDeleteId] = React.useState(null) /** 置顶区域展开/收起 */ const [pinnedExpanded, setPinnedExpanded] = React.useState(true) + /** Agent 置顶区域展开/收起 */ + const [pinnedAgentExpanded, setPinnedAgentExpanded] = React.useState(true) const setUserProfile = useSetAtom(userProfileAtom) const selectedModel = useAtomValue(selectedModelAtom) const streamingIds = useAtomValue(streamingConversationIdsAtom) @@ -289,6 +291,18 @@ export function LeftSidebar({ width }: LeftSidebarProps): React.ReactElement { } } + /** 切换 Agent 会话置顶状态 */ + const handleTogglePinAgent = async (id: string): Promise => { + try { + const updated = await window.electronAPI.togglePinAgentSession(id) + setAgentSessions((prev) => + prev.map((s) => (s.id === updated.id ? updated : s)) + ) + } catch (error) { + console.error('[侧边栏] 切换 Agent 会话置顶失败:', error) + } + } + /** 确认删除对话 */ const handleConfirmDelete = async (): Promise => { if (!pendingDeleteId) return @@ -364,6 +378,12 @@ export function LeftSidebar({ width }: LeftSidebarProps): React.ReactElement { [agentSessions, currentWorkspaceId] ) + /** 置顶 Agent 会话列表(按当前工作区过滤) */ + const pinnedAgentSessions = React.useMemo( + () => filteredAgentSessions.filter((s) => s.pinned), + [filteredAgentSessions] + ) + /** Agent 会话按日期分组 */ const agentSessionGroups = React.useMemo( () => groupByDate(filteredAgentSessions), @@ -401,19 +421,39 @@ export function LeftSidebar({ width }: LeftSidebarProps): React.ReactElement { {/* Chat 模式:导航菜单(置顶区域) */} {mode === 'chat' && ( -
- } - label="置顶对话" - suffix={ - pinnedConversations.length > 0 ? ( - pinnedExpanded - ? - : - ) : undefined - } +
+ +
+ )} + + {/* Agent 模式:导航菜单(置顶区域) */} + {mode === 'agent' && pinnedAgentSessions.length > 0 && ( +
+
)} @@ -441,6 +481,30 @@ export function LeftSidebar({ width }: LeftSidebarProps): React.ReactElement {
)} + {/* Agent 模式:置顶会话区域 */} + {mode === 'agent' && pinnedAgentExpanded && pinnedAgentSessions.length > 0 && ( +
+
+ {pinnedAgentSessions.map((session) => ( + handleSelectAgentSession(session.id)} + onRequestDelete={() => handleRequestDelete(session.id)} + onRename={handleAgentRename} + onTogglePin={handleTogglePinAgent} + onMouseEnter={() => setHoveredId(session.id)} + onMouseLeave={() => setHoveredId(null)} + /> + ))} +
+
+ )} + {/* 列表区域:根据模式切换 */}
{mode === 'chat' ? ( @@ -485,9 +549,11 @@ export function LeftSidebar({ width }: LeftSidebarProps): React.ReactElement { active={session.id === currentAgentSessionId} hovered={session.id === hoveredId} running={agentRunningIds.has(session.id)} + showPinIcon={!!session.pinned} onSelect={() => handleSelectAgentSession(session.id)} onRequestDelete={() => handleRequestDelete(session.id)} onRename={handleAgentRename} + onTogglePin={handleTogglePinAgent} onMouseEnter={() => setHoveredId(session.id)} onMouseLeave={() => setHoveredId(null)} /> @@ -750,9 +816,11 @@ interface AgentSessionItemProps { active: boolean hovered: boolean running: boolean + showPinIcon?: boolean onSelect: () => void onRequestDelete: () => void onRename: (id: string, newTitle: string) => Promise + onTogglePin?: (id: string) => void onMouseEnter: () => void onMouseLeave: () => void } @@ -762,9 +830,11 @@ function AgentSessionItem({ active, hovered, running, + showPinIcon, onSelect, onRequestDelete, onRename, + onTogglePin, onMouseEnter, onMouseLeave, }: AgentSessionItemProps): React.ReactElement { @@ -773,6 +843,8 @@ function AgentSessionItem({ const inputRef = React.useRef(null) const justStartedEditing = React.useRef(false) + const isPinned = !!session.pinned + const startEdit = (): void => { setEditTitle(session.title) setEditing(true) @@ -847,6 +919,10 @@ function AgentSessionItem({ )} + {/* 置顶标记 */} + {showPinIcon && ( + + )} {session.title}
)} @@ -870,6 +946,13 @@ function AgentSessionItem({ + onTogglePin?.(session.id)} + > + {isPinned ? : } + {isPinned ? '取消置顶' : '置顶会话'} +