AI 应用人工智能AI Agent本地部署前端后端工作流自动化【免费下载链接】ekko-studioEkko Studio is a local-first AI workspace for multi-agent chat, coding, and visual workflows, available on desktop and the web.项目地址https://gitcode.com/gh_mirrors/he/ekko-studio点击查看免费下载导读本文围绕 Ekko Studiohermes-studio会话侧栏中「最近会话Recent」快捷入口的交互行为展开核心问题是当用户从 Recent 分组点击进入某个直聊会话时应用不应展开该会话的真实分类分组也不应覆盖用户已保存的分组折叠状态。文章将结合 变更说明文档 与客户端源码逐一拆解 Recent 分组的构建逻辑、折叠状态持久化、以及抑制分类展开机制category reveal suppression的实现细节帮助读者理解如何在多分类会话侧栏中安全地叠加一个纯快捷入口而不干扰用户的空间组织意图。一、背景Recent 分组为什么会对分类折叠造成干扰Ekko Studio 的会话侧栏位于 ChatPanel.vue并非简单的会话列表而是由三类区域叠加而成置顶会话Pinned用户手动固定的会话永远排在列表最上方最近会话Recent按updatedAt严格降序动态生成的快捷分组默认展示最近 10 条可配置真实分类分组Categorized用户创建的会话分类Category未分类会话归入未分类Uncategorized分组。Recent 分组的特殊性在于它是动态派生视图其中的会话同时存在于下方的真实分类分组中。从设计意图上看partitionRecentSessions 中注释明确写道最近是快捷入口不从真实分类中移除对应会话Recent 只负责快速定位最近活跃的会话而分类折叠状态才是用户维护的空间组织。因此在引入本变更之前存在一类体验问题当用户从 Recent 点击某个会话时路由与activeSessionId的变化会触发自动展开活跃会话所在分类的逻辑见下文 Watch 分析导致用户精心折叠的分类被强制展开甚至将折叠状态覆盖写入持久化存储破坏原有的侧栏布局。本变更docs/chat-chain-changes/2026-08-26-recent-session-category-collapse.md确立了如下行为契约Recent 保持快捷入口定位选中会话可以高亮highlight但不得改变用户的分类折叠选择直接 URL 访问或普通导航非 Recent 点击仍然可以揭示reveal活跃分类。二、Recent 分组的构建动态派生且不触碰真实分类2.1 分区函数 partitionRecentSessionsRecent 分组的核心实现在 session-category-groups.ts关键函数为partitionRecentSessionsexport interface RecentSessionPartitionT { group: VisibleSessionCategoryGroupT; remaining: T[]; } export function partitionRecentSessionsT extends RecentSessionAssignment( sessions: readonly T[], limit: number, label: string, ): RecentSessionPartitionT { const safeLimit Math.min(100, Math.max(1, Math.floor(Number(limit) || 10))); const recent [...sessions] .sort((a, b) (b.updatedAt || 0) - (a.updatedAt || 0)) .slice(0, safeLimit); return { group: { key: recent, label, sessions: recent, }, // “最近”是快捷入口不从真实分类中移除对应会话。 remaining: [...sessions], }; }要点拆解limit安全钳制Math.min(100, Math.max(1, Math.floor(Number(limit) || 10)))保证数量在 1100 之间非法输入如NaN回退到默认值 10。这个钳制逻辑与session-browser-prefsstore 中的setRecentCount完全一致。严格按活动时间排序(b.updatedAt || 0) - (a.updatedAt || 0)实现updatedAt降序无时间戳的会话按 0 处理排在最末。remaining返回完整原数组Recent 分组只是复制出前 N 条会话remaining保持全量会话因此真实分类分组categorizedSessions的构建完全不受 Recent 影响。这正是Recent 是快捷入口不从真实分类中移除对应会话这一契约的代码级体现。2.2 单元测试印证tests/client/session-category-groups.test.ts 中两条用例直接验证上述契约it(builds a dynamic recent group by strict activity time without changing real categories, () { const sessions [ { id: older, categoryId: 1, updatedAt: 100 }, { id: newest, categoryId: null, updatedAt: 300 }, { id: middle, categoryId: 2, updatedAt: 200 }, ] expect(buildRecentSessionCategoryGroup(sessions, 2, Recent)).toEqual({ key: recent, label: Recent, sessions: [sessions[1], sessions[2]], }) expect(sessions.map(s s.categoryId)).toEqual([1, null, 2]) // 原数组未被修改 }) it(keeps recent sessions in their sidebar categories, () { // partition.remaining 传入真实分类构建Recent 中的会话仍保留在原分类里 })categoryId断言验证了排序与截取不会污染会话自身的分类归属第二条用例则验证 Recent 与真实分类可以同时存在。2.3 ChatPanel 中的接线在 ChatPanel.vue 中const recentSessionPartition computed(() partitionRecentSessions( chatStore.sessions.filter((session) !session.isPinned), sessionBrowserPrefsStore.recentCount, t(chat.recent), )) const recentSessions computed(() recentSessionPartition.value.group) const nonRecentSessions computed(() recentSessionPartition.value.remaining)置顶会话被排除在 Recent 之外!session.isPinned避免与置顶区域重复recentCount来自sessionBrowserPrefsStorePinia store用户可在 UI 上通过最近数量弹窗调整unpinnedSessions由nonRecentSessions过滤得到再喂给buildVisibleSessionCategoryGroups构建真实分类分组。2.4 Recent 分组自身也是可折叠的Recent 分组头部同样提供折叠/展开开关其状态独立于真实分类折叠存储在session-browser-prefsstore 中const recentCollapsed refboolean(loadJsonboolean(RECENT_COLLAPSED_KEY, false)) function setRecentCollapsed(value: boolean) { recentCollapsed.value value saveJson(RECENT_COLLAPSED_KEY, value) }对应的持久化键为hermes_recent_sessions_collapsed_v1默认展开false每次切换立即写入localStorage。相关 store 源码位于 session-browser-prefs.ts测试见 session-browser-prefs.test.ts。三、折叠状态的持久化与自动揭示逻辑3.1 折叠集合的存取真实分类的折叠状态保存在localStorage键为hermes_chat_collapsed_categories值为分类 key 的字符串数组const COLLAPSED_CATEGORIES_STORAGE_KEY hermes_chat_collapsed_categories; function loadCollapsedCategories(): Setstring { try { const value JSON.parse(localStorage.getItem(COLLAPSED_CATEGORIES_STORAGE_KEY) || []); return new Set(Array.isArray(value) ? value.map(String) : []); } catch { return new Set(); } } function persistCollapsedCategories() { localStorage.setItem(COLLAPSED_CATEGORIES_STORAGE_KEY, JSON.stringify([...collapsedCategories.value])); }分类 key 的格式为category-${categoryId}如category-3未分类固定为category-none见activeSessionCategoryKey计算属性读取时对非法 JSON 容错回退为空Set写入时总是完整覆盖。3.2 Watch何时自动展开活跃分类侧栏通过一个组合 Watch 监听四路信号分类加载状态、分类分组结构、活跃会话 ID、活跃分类 key然后决定是否揭示展开活跃分类watch( [ () sessionCategoriesLoaded.value, () categorizedSessions.value.map((group) group.key).join(\u0000), () chatStore.activeSessionId, activeSessionCategoryKey, ], ([loaded, , sessionId, activeKey], [previousLoaded, , previousSessionId, previousActiveKey]) { if (!sessionCategoriesLoaded.value || categorizedSessions.value.length 0) return; const activeSession chatStore.sessions.find((session) session.id chatStore.activeSessionId); if (categoryRevealSuppressedSessionId.value activeSession?.id) return; // ← 抑制点 setCategoryRevealSuppressedSessionId(null); // 只有导航或分类变更才应揭示活跃会话后台列表刷新必须保留手动折叠。 const shouldReveal loaded ! previousLoaded || sessionId ! previousSessionId || activeKey ! previousActiveKey; if (shouldReveal collapsedCategories.value.has(activeKey)) { collapsedCategories.value new Set( [...collapsedCategories.value].filter((key) key ! activeKey), ); persistCollapsedCategories(); } // 首次加载无存储记录时默认只展开活跃分类所在分组 if (localStorage.getItem(COLLAPSED_CATEGORIES_STORAGE_KEY) ! null) return; const expandedKey categorizedSessions.value.some((group) group.key activeKey) ? activeKey : categorizedSessions.value[0]?.key; collapsedCategories.value new Set( categorizedSessions.value.map((group) group.key).filter((key) key ! expandedKey), ); persistCollapsedCategories(); }, { immediate: true }, )值得注意的两个防御性设计shouldReveal条件只有分类刚加载完成 / 活跃会话切换 / 活跃分类变更三类信号才会触发展开后台会话列表刷新如新消息到达、时间戳更新不会展开任何分组避免刷新一次列表折叠布局就被打乱首次初始化当hermes_chat_collapsed_categories从未写入时自动生成一个默认只展开活跃分类所在分组、其余全部折叠的初始布局随后立即持久化——这是对新用户侧栏的首屏体验优化。四、核心机制Recent 点击的揭示抑制Reveal Suppression4.1 两个入口的差异化处理ChatPanel 中区分了两种会话点击入口ChatPanel.vueasync function handleSessionClick( sessionId: string, options: { preserveCategoryCollapse?: boolean } {}, ) { if (!options.preserveCategoryCollapse) { setCategoryRevealSuppressedSessionId(null); // 普通导航清除抑制 } chatStore.clearSessionCompletedUnread(sessionId); await router.push({ name: chatStore.runtimeMode global_agent ? hermes.globalAgentSession : hermes.session, params: { sessionId }, }); if (chatStore.activeSessionId ! sessionId) { await chatStore.switchSession(sessionId); } if (mobileQuery?.matches) showSessions.value false; } async function handleRecentSessionClick(sessionId: string) { // Recent 是快捷入口选择它不得覆盖真实分类保存的折叠状态。 setCategoryRevealSuppressedSessionId(sessionId); // 先登记抑制目标 await handleSessionClick(sessionId, { preserveCategoryCollapse: true }); }关键点普通导航分类分组、置顶、搜索等调用handleSessionClick且不带preserveCategoryCollapse进入时先清除抑制标记随后 Watch 检测到活跃会话变化正常展开其真实分类Recent 点击调用handleRecentSessionClick先把sessionId登记为揭示抑制目标再以preserveCategoryCollapse: true走同一导航链路Watch 中因命中抑制条件categoryRevealSuppressedSessionId.value activeSession?.id而提前 return折叠集合保持不变。4.2 抑制标记的会话级持久化抑制标记并非仅存在内存中而是写入sessionStorage键为hermes_chat_recent_category_reveal_suppression值就是会话 IDconst RECENT_CATEGORY_REVEAL_SUPPRESSION_STORAGE_KEY hermes_chat_recent_category_reveal_suppression; function loadCategoryRevealSuppressedSessionId(): string | null { try { return sessionStorage.getItem(RECENT_CATEGORY_REVEAL_SUPPRESSION_STORAGE_KEY); } catch { return null; } } const categoryRevealSuppressedSessionId refstring | null( loadCategoryRevealSuppressedSessionId(), ); function setCategoryRevealSuppressedSessionId(sessionId: string | null) { categoryRevealSuppressedSessionId.value sessionId; try { if (sessionId) sessionStorage.setItem(RECENT_CATEGORY_REVEAL_SUPPRESSION_STORAGE_KEY, sessionId); else sessionStorage.removeItem(RECENT_CATEGORY_REVEAL_SUPPRESSION_STORAGE_KEY); } catch { /* session storage 不可用时仅保持内存行为 */ } }使用sessionStorage而非localStorage的设计考量抑制只对当前标签页内的一次 Recent 点击生效属于会话级临时状态若用户之后通过 URL 直达、刷新或从真实分类点击同一会话抑制已被清除普通导航会调用setCategoryRevealSuppressedSessionId(null)分类会正常揭示——与文档声明Direct URL 和普通导航仍可揭示活跃分类完全吻合。4.3 执行时序一次完整的 Recent 点击流程handleRecentSessionClick(sessionId)将sessionId写入sessionStorage与内存 refhandleSessionClick(..., { preserveCategoryCollapse: true })跳过清除抑制执行clearSessionCompletedUnread、router.push与switchSessionactiveSessionId变化触发 WatchWatch 中activeSession?.id categoryRevealSuppressedSessionId.value成立直接 return——不展开分类、不写折叠存储用户下一次普通导航或直接 URL 访问时抑制被清除行为恢复为揭示活跃分类。五、可配置项与存储键速查本特性涉及的全部配置与存储键整理如下均可在对应源码中核实配置/存储键类型默认值说明源码位置hermes_chat_collapsed_categorieslocalStorage[]空数组真实分类折叠状态值为分类 key 数组category-id/category-noneChatPanel.vuehermes_chat_recent_category_reveal_suppressionsessionStorage无当前标签页内抑制分类揭示的会话 IDChatPanel.vuehermes_recent_session_count_v1localStorage10Recent 分组展示的会话条数范围钳制在 1100session-browser-prefs.tshermes_recent_sessions_collapsed_v1localStoragefalseRecent 分组自身的折叠开关session-browser-prefs.tshermes_show_recent_sessions_v1localStoragetrue是否显示 Recent 分组session-browser-prefs.tshermes_human_only_v1_profilelocalStoragetrue按 profile 区分的仅人类会话过滤开关session-browser-prefs.tsRecent 条数上限的钳制逻辑同时存在于 store 的setRecentCount与partitionRecentSessions两处保证无论 UI 输入如何最终排序截取都在 1100 的安全区间内。六、行为契约总结与验证方式6.1 最终行为契约从Recent 分组点击会话会话被高亮并切换到对应对话但不展开其真实分类、不覆盖已保存的折叠状态直接 URL 访问如通过hermes.session路由携带sessionId进入或普通导航从分类分组、置顶、会话搜索等入口点击活跃分类照常揭示后台列表刷新时间戳、新消息导致的列表重排不会触发任何自动展开手动折叠始终被尊重。6.2 如何验证创建至少两个会话分类并手动折叠其中一个从 Recent 分组点击折叠分类下的会话该分类应保持折叠会话正常打开刷新页面或通过 URL 直达同一会话该分类应被展开揭示在侧栏中直接点击同一会话所在分类下的条目分类展开随后折叠状态重新持久化。相关单元测试可参考 session-category-groups.test.ts分区与保留语义与 session-browser-prefs.test.ts偏好持久化以及 e2e 用例 session-categories.spec.ts。结语本变更在 2026-08-26 变更说明 中仅用两句话概括了行为契约但其落地涉及三个层面的协同设计纯派生的 Recent 分区不触碰真实分类、严格区分触发源的自动揭示 Watch仅导航/分类变更生效、以及会话级 sessionStorage 抑制标记Recent 点击临时冻结揭示逻辑。这种快捷视图叠加于用户组织之上、且绝不干扰用户组织的模式对于任何需要在列表侧栏中提供最近/常用快捷入口的应用都具备直接的借鉴价值。赞分享AI 应用人工智能AI Agent本地部署前端后端工作流自动化【免费下载链接】ekko-studioEkko Studio is a local-first AI workspace for multi-agent chat, coding, and visual workflows, available on desktop and the web.项目地址https://gitcode.com/gh_mirrors/he/ekko-studio点击查看免费下载相关推荐Ekko Studio 会话侧栏「最近」快捷入口刷新后保留分类折叠状态的实现解析Ekko Studio 会话侧栏「最近」快捷入口刷新后保留分类折叠状态的实现解析 导读 本文深入解析 Ekko Studiohermes studio 仓库AI 应用人工智能AI Agent本地部署前端后端工作流自动化Ekko Studio 会话侧栏分类折叠状态保持机制轮询刷新下的手动折叠持久化实现解析Ekko Studio 会话侧栏分类折叠状态保持机制轮询刷新下的手动折叠持久化实现解析 在 Ekko Studiohermes studio的聊天会话侧栏AI 应用人工智能AI Agent本地部署前端后端工作流自动化VPet 桌面宠物 MOD 制作跑通第一只自定义桌宠要改哪几个文件VPet 桌面宠物 MOD 制作跑通第一只自定义桌宠要改哪几个文件 VPet 是一个开源的桌面宠物模拟器把一张张 PNG 序列帧把连续动作拆成静图拼起来桌面应用游戏开发上一篇【免费下载】 让小米P2P摄像头焕发新生videoP2Proxy项目推荐下一篇file-transfer-go API参考完整接口文档和使用示例创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
企业数字化 ERP 产品动态
相关推荐
从零手写AI代理:核心架构、工具调用与循环控制实战 1. 从零构建智能AI代理的整体设计思路1.1 为什么现在值得动手做AI代理AI代理这个词最近一年被聊得很多,但真正动手从零搭一个的人其实没想象中那么多。大部分人停留在调用一个对话接口、套个界面就结束的阶段。而所谓Agentic AI,核心区别在于:… · 2026/9/23 9:15:13
OpenClaw 2.7.1 Windows 安装指南:TaoToken 统一 Key 接入数字员工部署 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/23 9:15:13
多目标优化算法IMOCTCM在盘式制动器设计中的应用 1. 算法背景与工程价值多目标优化问题在工程设计中普遍存在,这类问题往往需要在多个相互冲突的目标之间寻找最佳平衡点。盘式制动器设计就是一个典型例子——我们需要同时考虑制动性能、热稳定性、重量控制和成本因素。传统优化方法在处理这类复杂问题时常常陷入局部… · 2026/9/23 9:15:13
ThinkPHP5架构与核心原理:从MVC到依赖注入、中间件的深度解析 引子:为什么面试官总爱问TP5的架构?近几年PHP面试题里,ThinkPHP 5(简称TP5)的架构和底层原理出现频率一直居高不下,哪怕新项目已经转向TP6、Hyperf甚至Go,TP5依然是很多团队遗留项目的底子。这个… · 2026/9/23 10:53:05
刘馨保姆级教程:3步搞定HTTP协议底层实战 刘馨保姆级教程:3步搞定HTTP协议底层实战 官方文档太厚翻不动?别急。 刘馨这套保姆级教程,专治各种“看不懂”。 直接上代码,带你从零搭建一个符合 RFC 规范的 HTTP 服务器。 项目目标:别只背概念,要能跑通… · 2026/9/23 10:52:59
个人健康保险费用预测:从线性回归到XGBoost的实战解析 简介:面向机器学习与数据分析学习者,这套个人健康保险费用预测实战包,围绕个人健康保险费用数据集,完成从EDA探索、统计检验、特征工程到回归建模的完整流程。压缩包约58KB,共22个文件,含20个Python源代码脚… · 2026/9/23 10:52:59
Pointwise图解原理:3步搞定配置,避开80%的坑 Pointwise图解原理:3步搞定配置,避开80%的坑 刚接手新项目,想搭个Pointwise评测环境?别笑,我见过太多人在这一步卡了整整半天。 Python版本冲突、依赖包装不上、配置项看不懂,光看官方文档都能让人头大。其实,… · 2026/9/23 10:52:59
cytoscape.js 集合操作详解:eles.union() 合并元素集合的用法与底层实现 cytoscape.js 集合操作详解:eles.union() 合并元素集合的用法与底层实现 【免费下载链接】cytoscape.js Graph theory (network) library for visualisation and analysis 项目地址: https://gitcode.com/gh_mirrors/cy/cytoscape.js
本篇技术指南聚焦 cytos… · 2026/9/23 10:52:52
前端改错图解原理:5步搞定Stack Trace 前端改错图解原理:5步搞定Stack Trace 刚毕业接老代码,Console 里飘着满屏红色的 Error,StackTrace 长得像天书。 别慌,别复制粘贴去搜,那只会让你更晕。 咱们得用图解原理把堆栈拆开,像剥洋葱一样找到病灶。… · 2026/9/23 10:52:52
3招搞定手机怎么下载微信面试难题实战项目解析 3招搞定手机怎么下载微信面试难题实战项目解析 面试被问“手机怎么下载微信”背后的原理,90%的人答不上来。别笑,这看似弱智的问题,实则是考察你对移动应用分发机制、安全校验及网络协议理解的试金石。我带过不少校招新人,他们背了八股文,却连一个A… · 2026/9/23 0:00:03
你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 你有新短消息请注意查收:3个新手避坑指南搞定消息系统选型 面试被问“高并发下如何保证消息不丢失”,你张口就是“用Redis”,结果面试官追问“如果Redis宕机了怎么办”,你瞬间卡壳。这种场景太常见了,很多新手在背八股文时,只记住了技术名词… · 2026/9/23 0:00:29