diff --git a/apps/electron/src/main/ipc.ts b/apps/electron/src/main/ipc.ts index 27b983e..48ebab5 100644 --- a/apps/electron/src/main/ipc.ts +++ b/apps/electron/src/main/ipc.ts @@ -5,7 +5,7 @@ */ import { ipcMain, nativeTheme, shell, dialog, BrowserWindow } from 'electron' -import { IPC_CHANNELS, CHANNEL_IPC_CHANNELS, CHAT_IPC_CHANNELS, AGENT_IPC_CHANNELS, ENVIRONMENT_IPC_CHANNELS, PROXY_IPC_CHANNELS, GITHUB_RELEASE_IPC_CHANNELS } from '@proma/shared' +import { IPC_CHANNELS, CHANNEL_IPC_CHANNELS, CHAT_IPC_CHANNELS, AGENT_IPC_CHANNELS, ENVIRONMENT_IPC_CHANNELS, PROXY_IPC_CHANNELS, GITHUB_RELEASE_IPC_CHANNELS, USAGE_IPC_CHANNELS } from '@proma/shared' import { USER_PROFILE_IPC_CHANNELS, SETTINGS_IPC_CHANNELS } from '../types' import type { RuntimeStatus, @@ -44,6 +44,9 @@ import type { SystemProxyDetectResult, GitHubRelease, GitHubReleaseListOptions, + UsageStats, + ConversationUsage, + UsageSettings, } from '@proma/shared' import type { UserProfile, AppSettings } from '../types' import { getRuntimeStatus, getGitRepoStatus } from './lib/runtime-init' @@ -109,6 +112,12 @@ import { listReleases as listGitHubReleases, getReleaseByTag, } from './lib/github-release-service' +import { + getUsageStats, + getConversationUsage, + getUsageSettings, + updateUsageSettings, +} from './lib/usage-service' /** * 注册 IPC 处理器 @@ -541,6 +550,17 @@ export function registerIpcHandlers(): void { } ) + // 切换 Agent 会话归档状态 + ipcMain.handle( + AGENT_IPC_CHANNELS.TOGGLE_ARCHIVE, + async (_, id: string): Promise => { + const sessions = listAgentSessions() + const current = sessions.find((s) => s.id === id) + if (!current) throw new Error(`会话不存在: ${id}`) + return updateAgentSessionMeta(id, { archived: !current.archived }) + } + ) + // ===== Agent 工作区管理相关 ===== // 确保默认工作区存在 @@ -860,6 +880,40 @@ export function registerIpcHandlers(): void { } ) + // ===== 使用统计相关 ===== + + // 获取使用量统计总览 + ipcMain.handle( + USAGE_IPC_CHANNELS.GET_USAGE_STATS, + async (_, days: number = 30): Promise => { + return getUsageStats(days) + } + ) + + // 获取指定对话的使用量详情 + ipcMain.handle( + USAGE_IPC_CHANNELS.GET_CONVERSATION_USAGE, + async (_, conversationId: string): Promise => { + return getConversationUsage(conversationId) + } + ) + + // 获取使用统计设置 + ipcMain.handle( + USAGE_IPC_CHANNELS.GET_USAGE_SETTINGS, + async (): Promise => { + return getUsageSettings() + } + ) + + // 更新使用统计设置 + ipcMain.handle( + USAGE_IPC_CHANNELS.UPDATE_USAGE_SETTINGS, + async (_, settings: UsageSettings): Promise => { + return updateUsageSettings(settings) + } + ) + console.log('[IPC] IPC 处理器注册完成') // 注册更新 IPC 处理器 diff --git a/apps/electron/src/main/lib/agent-session-manager.ts b/apps/electron/src/main/lib/agent-session-manager.ts index 563e374..89fd891 100644 --- a/apps/electron/src/main/lib/agent-session-manager.ts +++ b/apps/electron/src/main/lib/agent-session-manager.ts @@ -159,7 +159,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 6cef392..ba3bae5 100644 --- a/apps/electron/src/preload/index.ts +++ b/apps/electron/src/preload/index.ts @@ -6,7 +6,7 @@ */ import { contextBridge, ipcRenderer } from 'electron' -import { IPC_CHANNELS, CHANNEL_IPC_CHANNELS, CHAT_IPC_CHANNELS, AGENT_IPC_CHANNELS, ENVIRONMENT_IPC_CHANNELS, PROXY_IPC_CHANNELS, GITHUB_RELEASE_IPC_CHANNELS } from '@proma/shared' +import { IPC_CHANNELS, CHANNEL_IPC_CHANNELS, CHAT_IPC_CHANNELS, AGENT_IPC_CHANNELS, ENVIRONMENT_IPC_CHANNELS, PROXY_IPC_CHANNELS, GITHUB_RELEASE_IPC_CHANNELS, USAGE_IPC_CHANNELS } from '@proma/shared' import { USER_PROFILE_IPC_CHANNELS, SETTINGS_IPC_CHANNELS } from '../types' import type { RuntimeStatus, @@ -50,6 +50,9 @@ import type { SystemProxyDetectResult, GitHubRelease, GitHubReleaseListOptions, + UsageStats, + ConversationUsage, + UsageSettings, } from '@proma/shared' import type { UserProfile, AppSettings } from '../types' @@ -239,6 +242,9 @@ export interface ElectronAPI { /** 删除 Agent 会话 */ deleteAgentSession: (id: string) => Promise + /** 切换 Agent 会话归档状态 */ + toggleArchiveAgentSession: (id: string) => Promise + /** 生成 Agent 会话标题 */ generateAgentTitle: (input: AgentGenerateTitleInput) => Promise @@ -361,6 +367,20 @@ export interface ElectronAPI { // 工作区文件变化通知 onCapabilitiesChanged: (callback: () => void) => () => void onWorkspaceFilesChanged: (callback: () => void) => () => void + + // ===== 使用统计相关 ===== + + /** 获取使用量统计总览 */ + getUsageStats: (days?: number) => Promise + + /** 获取指定对话的使用量详情 */ + getConversationUsage: (conversationId: string) => Promise + + /** 获取使用统计设置 */ + getUsageSettings: () => Promise + + /** 更新使用统计设置 */ + updateUsageSettings: (settings: UsageSettings) => Promise } /** @@ -594,6 +614,10 @@ const electronAPI: ElectronAPI = { return ipcRenderer.invoke(AGENT_IPC_CHANNELS.DELETE_SESSION, id) }, + toggleArchiveAgentSession: (id: string) => { + return ipcRenderer.invoke(AGENT_IPC_CHANNELS.TOGGLE_ARCHIVE, id) + }, + generateAgentTitle: (input: AgentGenerateTitleInput) => { return ipcRenderer.invoke(AGENT_IPC_CHANNELS.GENERATE_TITLE, input) }, @@ -754,6 +778,23 @@ const electronAPI: ElectronAPI = { getReleaseByTag: (tag) => { return ipcRenderer.invoke(GITHUB_RELEASE_IPC_CHANNELS.GET_RELEASE_BY_TAG, tag) }, + + // 使用统计 + getUsageStats: (days?: number) => { + return ipcRenderer.invoke(USAGE_IPC_CHANNELS.GET_USAGE_STATS, days) + }, + + getConversationUsage: (conversationId: string) => { + return ipcRenderer.invoke(USAGE_IPC_CHANNELS.GET_CONVERSATION_USAGE, conversationId) + }, + + getUsageSettings: () => { + return ipcRenderer.invoke(USAGE_IPC_CHANNELS.GET_USAGE_SETTINGS) + }, + + updateUsageSettings: (settings: UsageSettings) => { + return ipcRenderer.invoke(USAGE_IPC_CHANNELS.UPDATE_USAGE_SETTINGS, settings) + }, } // 将 API 暴露到渲染进程的 window 对象上 diff --git a/apps/electron/src/renderer/atoms/agent-atoms.ts b/apps/electron/src/renderer/atoms/agent-atoms.ts index 63dbdcf..eca547d 100644 --- a/apps/electron/src/renderer/atoms/agent-atoms.ts +++ b/apps/electron/src/renderer/atoms/agent-atoms.ts @@ -444,6 +444,55 @@ export const currentAgentSessionDraftAtom = atom( } ) +// ===== 会话归档与搜索 ===== + +/** 归档视图切换 */ +export const showArchivedSessionsAtom = atom(false) + +/** 会话搜索关键词 */ +export const sessionSearchKeywordAtom = atom('') + +/** 获取活跃会话(按工作区过滤 + 排除归档) */ +export const activeAgentSessionsAtom = atom((get) => { + const sessions = get(agentSessionsAtom) + const currentWorkspaceId = get(currentAgentWorkspaceIdAtom) + return sessions.filter( + (s) => s.workspaceId === currentWorkspaceId && !s.archived, + ) +}) + +/** 获取归档会话 */ +export const archivedAgentSessionsAtom = atom((get) => { + const sessions = get(agentSessionsAtom) + const currentWorkspaceId = get(currentAgentWorkspaceIdAtom) + return sessions.filter( + (s) => s.workspaceId === currentWorkspaceId && s.archived, + ) +}) + +/** 过滤后的会话列表(结合工作区、归档状态、搜索关键词) */ +export const filteredAgentSessionsAtom = atom((get) => { + const sessions = get(agentSessionsAtom) + const currentWorkspaceId = get(currentAgentWorkspaceIdAtom) + const showArchived = get(showArchivedSessionsAtom) + const keyword = get(sessionSearchKeywordAtom) + + return sessions.filter((s) => { + // 工作区筛选 + if (s.workspaceId !== currentWorkspaceId) return false + + // 归档状态筛选 + if (showArchived !== !!s.archived) return false + + // 关键词搜索 + if (keyword.trim()) { + return s.title.toLowerCase().includes(keyword.toLowerCase()) + } + + return true + }) +}) + // ===== 后台任务管理 ===== /** diff --git a/apps/electron/src/renderer/components/app-shell/LeftSidebar.tsx b/apps/electron/src/renderer/components/app-shell/LeftSidebar.tsx index a0307cc..b7a3e4b 100644 --- a/apps/electron/src/renderer/components/app-shell/LeftSidebar.tsx +++ b/apps/electron/src/renderer/components/app-shell/LeftSidebar.tsx @@ -10,7 +10,7 @@ import * as React from 'react' import { useAtom, useSetAtom, useAtomValue } from 'jotai' -import { Pin, PinOff, Settings, Plus, Trash2, Pencil, ChevronDown, ChevronRight, Plug, Zap } from 'lucide-react' +import { Pin, PinOff, Settings, Plus, Trash2, Pencil, ChevronDown, ChevronRight, Plug, Zap, Archive, ArchiveRestore, Search, X } from 'lucide-react' import { cn } from '@/lib/utils' import { ModeSwitcher } from './ModeSwitcher' import { activeViewAtom } from '@/atoms/active-view' @@ -30,6 +30,9 @@ import { currentAgentWorkspaceIdAtom, agentWorkspacesAtom, workspaceCapabilitiesVersionAtom, + showArchivedSessionsAtom, + sessionSearchKeywordAtom, + filteredAgentSessionsAtom, } from '@/atoms/agent-atoms' import { userProfileAtom } from '@/atoms/user-profile' import { hasUpdateAtom } from '@/atoms/updater' @@ -52,9 +55,68 @@ import { ContextMenuSeparator, ContextMenuTrigger, } from '@/components/ui/context-menu' +import { Input } from '@/components/ui/input' import type { ActiveView } from '@/atoms/active-view' import type { ConversationMeta, AgentSessionMeta, WorkspaceCapabilities } from '@proma/shared' +// ===== 可复用的编辑状态 Hook ===== + +interface UseEditableTitleReturn { + editing: boolean + editTitle: string + inputRef: React.RefObject + startEdit: (currentTitle: string) => void + saveTitle: (currentTitle: string, onSave: (title: string) => Promise) => Promise + setEditTitle: (title: string) => void + cancelEdit: () => void +} + +function useEditableTitle(): UseEditableTitleReturn { + const [editing, setEditing] = React.useState(false) + const [editTitle, setEditTitle] = React.useState('') + const inputRef = React.useRef(null) + const justStartedEditing = React.useRef(false) + + const startEdit = React.useCallback((currentTitle: string) => { + setEditTitle(currentTitle) + setEditing(true) + justStartedEditing.current = true + setTimeout(() => { + justStartedEditing.current = false + inputRef.current?.focus() + inputRef.current?.select() + }, 300) + }, []) + + const saveTitle = React.useCallback(async ( + currentTitle: string, + onSave: (title: string) => Promise + ): Promise => { + if (justStartedEditing.current) return + const trimmed = editTitle.trim() + if (!trimmed || trimmed === currentTitle) { + setEditing(false) + return + } + await onSave(trimmed) + setEditing(false) + }, [editTitle]) + + const cancelEdit = React.useCallback(() => { + setEditing(false) + }, []) + + return { + editing, + editTitle, + inputRef, + startEdit, + saveTitle, + setEditTitle, + cancelEdit, + } +} + interface SidebarItemProps { icon: React.ReactNode label: string @@ -154,6 +216,9 @@ export function LeftSidebar({ width }: LeftSidebarProps): React.ReactElement { const agentChannelId = useAtomValue(agentChannelIdAtom) const currentWorkspaceId = useAtomValue(currentAgentWorkspaceIdAtom) const workspaces = useAtomValue(agentWorkspacesAtom) + const [showArchived, setShowArchived] = useAtom(showArchivedSessionsAtom) + const [searchKeyword, setSearchKeyword] = useAtom(sessionSearchKeywordAtom) + const filteredAgentSessions = useAtomValue(filteredAgentSessionsAtom) // 工作区能力(MCP + Skill 计数) const [capabilities, setCapabilities] = React.useState(null) @@ -342,26 +407,29 @@ export function LeftSidebar({ width }: LeftSidebarProps): React.ReactElement { /** 重命名 Agent 会话标题 */ const handleAgentRename = async (id: string, newTitle: string): Promise => { try { - await window.electronAPI.updateAgentSessionTitle(id, newTitle) + const updated = await window.electronAPI.updateAgentSessionTitle(id, newTitle) setAgentSessions((prev) => - prev.map((s) => (s.id === id ? { ...s, title: newTitle, updatedAt: Date.now() } : s)) + prev.map((s) => (s.id === id ? updated : s)) ) } catch (error) { console.error('[侧边栏] 重命名 Agent 会话失败:', error) } } - /** Agent 会话按工作区过滤 */ - const filteredAgentSessions = React.useMemo( - () => agentSessions.filter((s) => s.workspaceId === currentWorkspaceId), - [agentSessions, currentWorkspaceId] - ) + /** 切换 Agent 会话归档状态 */ + const handleToggleArchive = async (id: string): Promise => { + try { + const updated = await window.electronAPI.toggleArchiveAgentSession(id) + setAgentSessions((prev) => + prev.map((s) => (s.id === id ? updated : s)) + ) + } catch (error) { + console.error('[侧边栏] 切换归档状态失败:', error) + } + } /** Agent 会话按日期分组 */ - const agentSessionGroups = React.useMemo( - () => groupByDate(filteredAgentSessions), - [filteredAgentSessions] - ) + const agentSessionGroups = groupByDate(filteredAgentSessions) return (
- {/* Agent 模式:工作区选择器 */} + {/* Agent 模式:工作区选择器 + 归档切换 + 搜索 */} {mode === 'agent' && ( -
+
+ + {/* 归档切换 */} +
+ + +
+ + {/* 搜索输入框 */} +
+ + setSearchKeyword(e.target.value)} + className="h-8 pl-8 text-[13px] bg-foreground/[0.03] border-0 focus-visible:ring-1 focus-visible:ring-primary/30" + /> + {searchKeyword && ( + + )} +
)} @@ -465,29 +579,35 @@ export function LeftSidebar({ width }: LeftSidebarProps): React.ReactElement { )) ) : ( /* Agent 模式:Agent 会话按日期分组 */ - agentSessionGroups.map((group) => ( -
-
- {group.label} -
-
- {group.items.map((session) => ( - handleSelectAgentSession(session.id)} - onRequestDelete={() => handleRequestDelete(session.id)} - onRename={handleAgentRename} - onMouseEnter={() => setHoveredId(session.id)} - onMouseLeave={() => setHoveredId(null)} - /> - ))} + agentSessionGroups.length > 0 ? ( + agentSessionGroups.map((group) => ( +
+
+ {group.label} +
+
+ {group.items.map((session) => ( + handleSelectAgentSession(session.id)} + onRequestDelete={() => handleRequestDelete(session.id)} + onRename={handleAgentRename} + onToggleArchive={handleToggleArchive} + onMouseEnter={() => setHoveredId(session.id)} + onMouseLeave={() => setHoveredId(null)} + /> + ))} +
-
- )) + )) + ) : ( + /* 空状态 */ + + ) )}
@@ -564,6 +684,137 @@ export function LeftSidebar({ width }: LeftSidebarProps): React.ReactElement { ) } +// ===== 空状态组件 ===== + +interface EmptyStateProps { + searchKeyword: string + showArchived: boolean +} + +function EmptyState({ searchKeyword, showArchived }: EmptyStateProps): React.ReactElement { + if (searchKeyword) { + return ( +
+ +
+ 未找到 "{searchKeyword}" 相关的会话 +
+
+ ) + } + + if (showArchived) { + return ( +
+ +
暂无归档会话
+
+ ) + } + + return ( +
+
当前工作区暂无会话
+
+ 点击上方按钮创建新会话 +
+
+ ) +} + +// ===== 可复用的标题输入组件 ===== + +interface TitleInputProps { + value: string + onChange: (value: string) => void + onSave: () => void + onCancel: () => void + inputRef: React.RefObject +} + +function TitleInput({ value, onChange, onSave, onCancel, inputRef }: TitleInputProps): React.ReactElement { + const handleKeyDown = (e: React.KeyboardEvent): void => { + if (e.key === 'Enter') { + e.preventDefault() + onSave() + } else if (e.key === 'Escape') { + onCancel() + } + } + + return ( + onChange(e.target.value)} + onKeyDown={handleKeyDown} + onBlur={onSave} + onClick={(e) => e.stopPropagation()} + className="w-full bg-transparent text-[13px] leading-5 text-foreground border-b border-primary/50 outline-none px-0 py-0" + maxLength={100} + /> + ) +} + +// ===== 可复用的列表项容器 ===== + +interface ListItemContainerProps { + children: React.ReactNode + active: boolean + hovered: boolean + editing: boolean + onSelect: () => void + onStartEdit: () => void + onMouseEnter: () => void + onMouseLeave: () => void + deleteButton: React.ReactNode + extraButton?: React.ReactNode +} + +function ListItemContainer({ + children, + active, + hovered, + editing, + onSelect, + onStartEdit, + onMouseEnter, + onMouseLeave, + deleteButton, + extraButton, +}: ListItemContainerProps): React.ReactElement { + return ( +
{ + e.stopPropagation() + onStartEdit() + }} + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} + className={cn( + 'w-full flex items-center gap-2 px-3 py-[7px] rounded-[10px] transition-colors duration-100 titlebar-no-drag text-left', + active + ? 'bg-foreground/[0.08] dark:bg-foreground/[0.08] shadow-[0_1px_2px_0_rgba(0,0,0,0.05)]' + : 'hover:bg-foreground/[0.04] dark:hover:bg-foreground/[0.04]' + )} + > +
{children}
+
+ {extraButton} + {deleteButton} +
+
+ ) +} + // ===== 对话列表项 ===== interface ConversationItemProps { @@ -571,7 +822,6 @@ interface ConversationItemProps { active: boolean hovered: boolean streaming: boolean - /** 是否在标题旁显示 Pin 图标 */ showPinIcon: boolean onSelect: () => void onRequestDelete: () => void @@ -594,140 +844,78 @@ function ConversationItem({ onMouseEnter, onMouseLeave, }: ConversationItemProps): React.ReactElement { - const [editing, setEditing] = React.useState(false) - const [editTitle, setEditTitle] = React.useState('') - const inputRef = React.useRef(null) - const justStartedEditing = React.useRef(false) + const { editing, editTitle, inputRef, startEdit, saveTitle, setEditTitle, cancelEdit } = useEditableTitle() + const isPinned = !!conversation.pinned - /** 进入编辑模式 */ - const startEdit = (): void => { - setEditTitle(conversation.title) - setEditing(true) - justStartedEditing.current = true - // 延迟聚焦,等待 ContextMenu 完全关闭后再 focus - setTimeout(() => { - justStartedEditing.current = false - inputRef.current?.focus() - inputRef.current?.select() - }, 300) + const handleSave = async (): Promise => { + await saveTitle(conversation.title, (newTitle) => onRename(conversation.id, newTitle)) } - /** 保存标题 */ - const saveTitle = async (): Promise => { - // ContextMenu 关闭导致的 blur,忽略 - if (justStartedEditing.current) return - const trimmed = editTitle.trim() - if (!trimmed || trimmed === conversation.title) { - setEditing(false) - return - } - await onRename(conversation.id, trimmed) - setEditing(false) + const handleStartEdit = (): void => { + startEdit(conversation.title) } - /** 键盘事件 */ - const handleKeyDown = (e: React.KeyboardEvent): void => { - if (e.key === 'Enter') { - e.preventDefault() - saveTitle() - } else if (e.key === 'Escape') { - setEditing(false) - } - } - - const isPinned = !!conversation.pinned - return ( -
{ - e.stopPropagation() - startEdit() - }} + { + e.stopPropagation() + onRequestDelete() + }} + className="p-1 rounded-md text-foreground/30 hover:bg-destructive/10 hover:text-destructive transition-all duration-100" + title="删除对话" + > + + + } > -
- {editing ? ( - setEditTitle(e.target.value)} - onKeyDown={handleKeyDown} - onBlur={saveTitle} - onClick={(e) => e.stopPropagation()} - className="w-full bg-transparent text-[13px] leading-5 text-foreground border-b border-primary/50 outline-none px-0 py-0" - maxLength={100} - /> - ) : ( -
- {/* 流式输出绿色呼吸点指示器 */} - {streaming && ( - - - - - )} - {/* 置顶标记 */} - {showPinIcon && ( - - )} - {conversation.title} -
- )} -
- - {/* 删除按钮(始终渲染,hover 时可见) */} - -
+ {editing ? ( + + ) : ( +
+ {streaming && ( + + + + + )} + {showPinIcon && } + {conversation.title} +
+ )} +
- {/* 右键菜单 */} - onTogglePin(conversation.id)} - > + onTogglePin(conversation.id)}> {isPinned ? : } {isPinned ? '取消置顶' : '置顶对话'} - + 重命名 - + 删除对话 @@ -746,6 +934,7 @@ interface AgentSessionItemProps { onSelect: () => void onRequestDelete: () => void onRename: (id: string, newTitle: string) => Promise + onToggleArchive: (id: string) => Promise onMouseEnter: () => void onMouseLeave: () => void } @@ -758,123 +947,92 @@ function AgentSessionItem({ onSelect, onRequestDelete, onRename, + onToggleArchive, onMouseEnter, onMouseLeave, }: AgentSessionItemProps): React.ReactElement { - const [editing, setEditing] = React.useState(false) - const [editTitle, setEditTitle] = React.useState('') - const inputRef = React.useRef(null) - const justStartedEditing = React.useRef(false) + const { editing, editTitle, inputRef, startEdit, saveTitle, setEditTitle, cancelEdit } = useEditableTitle() - const startEdit = (): void => { - setEditTitle(session.title) - setEditing(true) - justStartedEditing.current = true - setTimeout(() => { - justStartedEditing.current = false - inputRef.current?.focus() - inputRef.current?.select() - }, 300) - } - - const saveTitle = async (): Promise => { - if (justStartedEditing.current) return - const trimmed = editTitle.trim() - if (!trimmed || trimmed === session.title) { - setEditing(false) - return - } - await onRename(session.id, trimmed) - setEditing(false) + const handleSave = async (): Promise => { + await saveTitle(session.title, (newTitle) => onRename(session.id, newTitle)) } - const handleKeyDown = (e: React.KeyboardEvent): void => { - if (e.key === 'Enter') { - e.preventDefault() - saveTitle() - } else if (e.key === 'Escape') { - setEditing(false) - } + const handleStartEdit = (): void => { + startEdit(session.title) } return ( -
{ - e.stopPropagation() - startEdit() - }} + { + e.stopPropagation() + onToggleArchive(session.id) + }} + className="p-1 rounded-md text-foreground/30 hover:bg-foreground/10 hover:text-foreground transition-all duration-100" + title={session.archived ? '取消归档' : '归档会话'} + > + {session.archived ? : } + + } + deleteButton={ + + } > -
- {editing ? ( - setEditTitle(e.target.value)} - onKeyDown={handleKeyDown} - onBlur={saveTitle} - onClick={(e) => e.stopPropagation()} - className="w-full bg-transparent text-[13px] leading-5 text-foreground border-b border-primary/50 outline-none px-0 py-0" - maxLength={100} - /> - ) : ( -
- {running && ( - - - - - )} - {session.title} -
- )} -
- - {/* 删除按钮(始终渲染,hover 时可见) */} - -
+ {editing ? ( + + ) : ( +
+ {running && ( + + + + + )} + {session.title} +
+ )} +
- + 重命名 + onToggleArchive(session.id)}> + {session.archived ? : } + {session.archived ? '取消归档' : '归档会话'} + - + 删除会话 diff --git a/packages/shared/src/types/agent.ts b/packages/shared/src/types/agent.ts index 71b907b..42330c7 100644 --- a/packages/shared/src/types/agent.ts +++ b/packages/shared/src/types/agent.ts @@ -174,6 +174,8 @@ export interface AgentSessionMeta { createdAt: number /** 更新时间戳 */ updatedAt: number + /** 是否已归档 */ + archived?: boolean } /** @@ -396,6 +398,8 @@ export const AGENT_IPC_CHANNELS = { UPDATE_TITLE: 'agent:update-title', /** 删除会话 */ DELETE_SESSION: 'agent:delete-session', + /** 切换归档状态 */ + TOGGLE_ARCHIVE: 'agent:toggle-archive', // 工作区管理 /** 获取工作区列表 */