DeepChat 侧边栏工作区注册:让空工作区可见、可归档、可一键开聊的实现解析
【免费下载链接】deepchat🐬DeepChat - A smart assistant that connects powerful AI to your personal world项目地址: https://gitcode.com/GitHub_Trending/dee/deepchat
本文基于 DeepChat 仓库的规格文档 docs/features/sidebar-workspace-registration/spec.md 与配套实施计划 docs/features/sidebar-workspace-registration/plan.md 展开,深入讲解主侧边栏如何合并 Project(工作区)投影与 Session(会话)投影,实现空工作区的注册展示、归档确认与空行直达新对话。读完后你将掌握:DeepChat 的版本化 Project 快照事件机制、new_environment_preferences表的排序语义、渲染端快照栅栏(fencing)防陈旧读取的写法,以及“路径即身份”的侧边栏合并规则。
一、问题背景:Session 驱动的侧边栏会“吞掉”新选的工作区
DeepChat 的主侧边栏(WindowSideBar.vue)原本只从 Session 分组出发渲染:哪些目录出现过会话,才会在 Workspace 区块出现对应的分组行。而 Project 域已经在独立地持久化“用户选中过的目录”(environment preference),projectStore.environments只会影响已存在分组的排序。
这就形成了一个断裂的交互流:
- 用户通过目录选择器选中一个目录;
- Project 域持久化并发布活跃 environment;
- 该目录下还没有任何非草稿 Session;
- 由于侧边栏由 Session 派生,列表里没有这一行——操作看起来“失败了”。
另一个被明确否决的方案是在行旁渲染全局会话数。规格文档指出:EnvironmentSummary.sessionCount是全局持久计数,而侧边栏展示的是“当前已加载的分页 + 搜索命中 + agent 过滤 + 置顶分区”之后的子集。把全局数字贴在过滤列表旁边,会让可见的层级关系自相矛盾。
二、目标与术语边界
规格给出的目标是:让用户在“浏览工作区的地方”就能注册工作区;在没有第一条 Session 之前,活跃的管理工作区就应当可见;并且可以从空工作区发起作用域正确的新草稿——同时不改变既有的目录生命周期与 Session 发现行为。
文档定义了四个必须区分清楚的术语,它们是整个合并视图的地基:
- Managed workspace(管理工作区):由
src/main/project/持有、通过projectStore.environments投影出来的活跃EnvironmentSummary。 - Chat workspace:
defaultChatWorkspacePath指向的内置对话工作区,只渲染在专门的 Chat 区块,绝不在 Workspace 下重复出现。 - Session group:按
projectDir分组的“当前已加载且经过过滤”的 Session。 - True empty workspace(真空工作区):
sessionCount === 0且没有可见 Session 的活跃管理工作区。仅仅因为 agent 过滤、置顶、搜索或分页导致“看起来空了”的分组不算真空。
职责划分同样被明确:Project 是工作区身份、活跃顺序、状态、存在性与持久计数的唯一事实源;Session 是可见会话成员关系的事实源;侧边栏只拥有合并后的视图模型;文件系统的读写仍归 Workspace 域。
三、用户交互设计
3.1 布局变化
Before:
Workspace [Group] project-a [+] [...] Session A project-b [+] [...] Session BAfter:
Workspace [Add] [Group] new-project Empty [+] [...] project-a [+] [...] Session A project-b [+] [...] Session B [...] Move to Top Move Up Move Down Move to Bottom ---------------- Archive关键交互约束:
- Add是一个紧凑的
folder-plus图标按钮,带可访问的 “Add workspace” 名称与 tooltip;它在展开的侧边栏中、日期分组和项目分组两种模式下都出现;原有的分组切换仍是表头最后一个动作。 - 空行与有内容的行共用同一套 folder 标识与持久化位置;空行显示弱化样式的Empty标签,而不是误导性的会话计数;其“新建对话”按钮始终可见(而不是 hover 才出现),让下一步操作一目了然。
在仓库源码中,这些行为已落地于 WindowSideBar.vue:表头按钮带有:disabled="isAddingWorkspace"忙锁并绑定handleAddWorkspace(约 L371-L374),空行判定由isTrueEmptyWorkspaceGroup(group)在渲染期派生(约 L455、L507)。
3.2 添加工作区流程(Add Workspace Flow)
- 用户激活Add workspace;
- 打开既有的、带类型契约的 Project 目录选择器。动作在结束前保持 disabled,避免同一侧边栏打开两个互相竞争的选择器;
- 取消不改变任何选中项、搜索词、分组模式或持久化状态;
- 成功选中时,通过
project.selectDirectory注册或重新激活该路径,把新激活的路径移动到持久化活跃顺序的顶部(不扫描所有已管理路径),并等待带版本的 Project 快照; - 侧边栏清空 Session 搜索、确定性地切换到项目分组模式、滚动让新行进入视口并把焦点还给该行;
- 该动作不创建草稿,也不替换当前 New Thread 的项目选择——注册与开始工作始终是两次显式用户动作。
边界情况的收敛规则:
- 路径已是活跃状态:不产生重复行、不打乱其显式顺序,只展示并聚焦既有行;
- 路径处于归档/已删除状态:走既有 Project 路由重新激活到顶部,并清掉生命周期墓碑(tombstone);
- 路径是内置 Chat 工作区:聚焦 Chat 区块,而不是创建重复的 Workspace 行;
- 选择器或快照失败:列表保持原样,走既有的破坏性操作通知通道;注册成功是持久的,即使后续的“揭示/聚焦”步骤失败也不回滚。
3.3 归档工作区流程(Archive Workspace Flow)
- 每个活跃管理工作区都暴露省略号菜单——哪怕它只有单行、或因重排暂时不可用而无法移动;
- 移动动作保留既有门槛;分隔线之后放置Archive;
- 归档打开与目录 Settings 相同的确认契约,说明文字声明“Session、消息和真实文件夹都会被保留”;
- 确认后调用既有的
projectStore.archiveEnvironment(path),并等待精确的 mutation 版本被 Project 快照提交;渲染端绝不直接编辑 Session 行或文件系统内容; - 成功归档后:零 Session 的工作区从 Workspace 消失;有历史的工作区保留为“历史分组”(historical group),不再参与重排和新建对话;
- 失败时保持确认框打开、保持活跃行完整、报告本地化错误;请求在途时禁用取消与重复确认。
内置 Chat 区块与历史分组永不暴露 Archive;Restore 与 Remove 仍留在 Settings 中管理。
3.4 空工作区流程(Empty Workspace Flow)
- 激活一个“真空且存在”的工作区主行,即发起一次以该路径为显式
projectDir的新对话;行上常驻的加号按钮执行同一操作; - 侧边栏必须走既有的统一
startNewConversation({ projectDir })路径,不得直接创建 Session。一次性项目意图(one-shot intent)的存在是为了保证:即使当前活跃 Session 随后关闭,agent 默认值或全局默认值也不能覆盖用户点击的工作区; - DeepChat 延续“首次提交时才持久化 Session”的语义;ACP 可以确保其既有的草稿 Session 存在,但草稿行仍然被排除在侧边栏和 environment 使用计数之外;
- 无论“第一条非草稿 Session”与“Project 快照”谁先到达,行都保持同一 path 身份:获得 Session、去掉 Empty 标签、进入正常折叠行为。
细节约束:真空行没有折叠态;有内容的行保留点击折叠与既有新建按钮;持久计数非零但当前加载子项为空的工作区仍是“有内容行”,绝不能把表头点击变成隐式的新对话。
四、生命周期与可见性矩阵
这张矩阵是验收口径的核心,规定了每种状态下侧边栏的唯一正确行为:
| 状态 | 侧边栏行为 |
|---|---|
| 项目分组、无搜索 | 将每个符合条件的活跃管理工作区与已加载 Session 分组合并 |
| 日期分组 | 日期分组保持不变;Add 成功后切换到项目分组以揭示结果 |
| Session 搜索激活 | 只显示包含命中 Session 的分组;不合成空行;Add 成功先清搜索再揭示 |
| agent 过滤、仅置顶子项、未加载分页 | 保留活跃工作区行,只挂当前可见的 Session;真空由持久计数判定 |
| 活跃且存在 | 展示、可重排、可新建对话、可归档 |
| 活跃但路径缺失 | 展示不可用指示;保留顺序与归档,但禁用新建对话 |
| 已归档且有 Session 历史 | 保留历史分组可被发现;不合成空行、不允许新建对话 |
| 已归档且无 Session 历史 | 从侧边栏隐藏;到 Settings 中管理 |
| 已删除(removed) | 永不合成行;残留的 ACP Session 发现沿用既有投影,但不获得添加/重排/新建入口 |
| 内置 Chat 路径 | 只渲染在 Chat,永不在 Workspace 重复 |
五、业务规则(15 条)
规格文档列出的 15 条业务规则中,以下数条是理解实现的关键:
- 路径是稳定的分组身份。显示名可以冲突,因此合并、折叠、重排、草稿意图一律以 path 为准,绝不用名字。
- 活跃顺序以 Project 为准。
ProjectService.selectDirectory()在选中使路径“新激活”时执行前置(prepend);渲染端只消费已提交的顺序,不得自行前置插入,也不能从 Session 活跃度推导顺序。 - 空行只存在于“项目分组且无 Session 搜索”时;搜索保持 Session 搜索语义,不悄悄扩展成工作区名搜索。
- 显式 Project 顺序在重复选中时存活;重复选中同一活跃路径对成员关系是幂等的,只有不存在显式顺序时才可能由回退的“最近使用”改变。
- 缺失、归档、已删除的路径都不能从侧边栏发起新对话。
- 内置 Chat 路径即使出现在活跃 Project 快照中,也固定在重排范围之外。
- 第一条 Session 会原地替换空态呈现——同一路径绝不允许出现一条合成行加一条 Session 派生行。
- 渲染端本地乐观态只能揭示模式或焦点变化;只有已提交的版本化 Project 快照才能新增、重新激活或移除工作区行。
- 归档的可用性与重排的可用性相互独立:单行、搜索、加载或动画期间重排可以禁用,活跃行仍可归档。
- 侧边栏归档是既有的、可逆的 Project 生命周期变更:必须先确认,且不得删除 Session、消息或文件系统内容。
- Project 快照就绪是 Project store 的状态;同一份快照携带内置 Chat 工作区身份,因此启动引导与 Project 刷新竞态期间侧边栏无法合成重复行。
- 工作区身份比较只去掉尾部分隔符,保留 POSIX 根
/与 Windows 盘符根;不做大小写折叠,也不改写分隔符风格。 - 归档或移除“手动选中的 New Thread 工作区”会使该选择失效,下一次草稿不得复用非活跃路径。
- 分组模式持久化是串行化的:失败时可见模式回滚到最近一次成功持久化的模式,所有调用方都能观察到它所等待的那次写入失败。
- 目录注册把 recent-project 与 active-order 状态写进同一个数据库事务;重新激活/新注册用一次 preference 表写入置顶,选择已活跃路径则保持其顺序。
六、源码级解析:主进程的排序与快照版本机制
6.1activateAtTop:单条 upsert 完成“置顶或保持”
排序语义落在 newEnvironmentPreferences.ts 的activateAtTop()(L71-L111)。表结构为:
CREATE TABLE IF NOT EXISTS new_environment_preferences ( path TEXT PRIMARY KEY, status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived', 'removed')), sort_order INTEGER NOT NULL DEFAULT 2147483647, archived_at INTEGER, removed_at INTEGER, updated_at INTEGER NOT NULL );核心 SQL 是一次带冲突处理的 upsert:
INSERT INTO new_environment_preferences (...) VALUES (?, 'active', COALESCE(( SELECT MIN(sort_order) - 1 FROM new_environment_preferences WHERE status = 'active' AND sort_order < 2147483647 ), 0), NULL, NULL, ?) ON CONFLICT(path) DO UPDATE SET status = 'active', sort_order = CASE WHEN new_environment_preferences.status = 'active' THEN new_environment_preferences.sort_order -- 已活跃:保持原顺序 ELSE excluded.sort_order -- 新激活:插入到当前最小显式顺序之前 END, archived_at = NULL, removed_at = NULL, updated_at = excluded.updated_at其中DEFAULT_ENVIRONMENT_SORT_ORDER = 2147483647作为“无显式顺序”的哨兵值:新路径拿到MIN(sort_order) - 1(没有任何显式顺序时退化为0),从而实现“一次写入即置顶”;已活跃路径则原样保留sort_order,同时把archived_at/removed_at墓碑清空——这正是业务规则 4(重复选中幂等)与“重新激活清除墓碑”的落地。
6.2selectDirectory:一个事务,不带无关 IO
ProjectService.selectDirectory() 的实现与计划文档完全一致:
const result = await this.deviceService.selectDirectory() if (result.canceled || result.filePaths.length === 0) { return { path: null, version: this.snapshotVersion } } // ... this.sqlitePresenter.getDatabase().transaction(() => { this.sqlitePresenter.newProjectsTable.upsert(dirPath, dirName) this.sqlitePresenter.newEnvironmentPreferencesTable.activateAtTop(dirPath) })() const version = this.bumpSnapshotVersion() return { path: dirPath, version }三个要点:取消直接返回当前版本(无副作用);recent-project upsert 与 preference 激活在一个事务内完成,且不调用getEnvironments()、不做任何针对无关路径的同步存在性检查(约束条款要求目录选择路径保持轻量);最后自增并持久化快照版本号(bumpSnapshotVersion()把projectSnapshotVersion写入 settings),返回精确版本供调用方等待。
6.3 版本化快照与 stale-read fencing
ProjectService.getSnapshot()(index.ts)把 preferences、new_environments(使用量)与 new_projects 联合投影为单一快照:由于paths集合是 usage 表与 preference 表的并集,一个零 Session 的选中路径也会以sessionCount: 0出现在environments中——这是空行得以存在的持久化前提。快照还携带defaultChatWorkspacePath,因此侧边栏不需要维护第二个本地就绪标志。
活跃环境的排序比较器compareEnvironmentSummaries()同样编码了顺序语义(L353-L375):显式顺序(sortOrder < DEFAULT_ENVIRONMENT_SORT_ORDER)优先,其次按lastUsedAt降序,最后按路径字典序兜底。
路由层 routes.ts 中,project.selectDirectory在成功时发布publishEnvironmentsChanged('select', path, version);project.archiveEnvironment则把服务层返回的精确版本原样带回渲染端(L98-L106),这支撑了“对话框必须等这个版本提交后才能关闭”的契约。契约本身定义在 project.routes.ts:projectSelectDirectoryRoute输出{ path: string | null, version },projectArchiveEnvironmentRoute输出{ updated: boolean, version },均为 zod 强类型校验。
渲染端的栅栏在 stores/ui/project.ts:
refreshProjectSnapshot(minVersion)(L139-L176)是唯一刷新负责人:用requestedSnapshotVersion单调抬升目标版本,do/while循环里若读到的快照版本落后于目标就重试;若事件先于其快照投影到达(读到“成功但不完整”的旧版本),不空转、也不发布陈旧失败,保留最后提交状态等待后续刷新——即计划文档所说的“事件与路由响应在同一 owner 下合并”;requireProjectSnapshot(version)等待精确版本提交,未提交则抛错;openFolderPicker(options)(L307-L348)正是计划中新增的select选项:默认select: true保持 New Thread 语义;select: false时只注册不改变selectedProjectPath;快照校验失败会回滚已发生的选中并重新抛出,让调用方能通知用户——“返回null仅表示用户取消了原生选择器”。
archiveEnvironment(path)(L256-L275)还做了一层投影断言:提交后若该路径既不在environments也不在archivedEnvironments中,抛出 “Archived environment is missing from the committed project snapshot”,保证确认框只在精确版本提交且生命周期投影为 archived 后才可关闭。
七、侧边栏视图模型:合并投影与揭示策略
7.1 本地合并类型
合并类型刻意保留在WindowSideBar.vue内部(除非测试证明存在第二个真实消费者):
type SidebarWorkspaceGroup = SessionGroup & { environment?: EnvironmentSummary }真空判定、生命周期、可用性与动作状态都在渲染期从这个可选environment派生,不进入共享契约。
7.2 项目模式合并算法(7 步)
- 像今天一样构建过滤后的 Session 分组,搜索激活时只保留至少含一条命中 Session 的分组;
- 在合并前先摘出 Chat/无项目分组;
- 为 active、archived、removed 三类 Project 摘要建立路径身份映射:比较时去掉普通路径的尾部分隔符、保留 POSIX 与 Windows 盘符根;不在渲染端做跨平台大小写折叠或分隔符改写(身份比较复用 src/shared/utils/filesystem.ts 的
normalizeWorkspacePath,见 project.ts 的导入); - 当 Project 元数据就绪且 Session 搜索为空时,按持久顺序遍历活跃 environments:跳过
defaultChatWorkspacePath;挂上匹配的可见 Session 分组或[];零 Session 行用environment.name命名;仅当sessionCount === 0 && sessions.length === 0才置isTrueEmpty;仅对“活跃且存在”的路径置canStartConversation; - 按 path 标记已被消费的 Session 分组,再把剩余 Session 派生分组按既有稳定顺序追加;把已知的 archived/removed 路径归类为 historical 并禁用重排/新建;
- 以 Project store 的“已提交快照就绪”作为唯一元数据就绪信号;未就绪时回退到当前纯 Session 渲染并保持重排禁用——绝不为失败的快照捏造活跃元数据;
- Session 搜索期间不合成“无匹配子项”的活跃行;既有活跃匹配仍走 Project 顺序,历史匹配排在活跃匹配之后。
日期分组保持纯 Session 语义;Add 成功处理器先切换到项目分组,再尝试聚焦结果行。
7.3 排序与拖拽
- 按 Project 顺序遍历活跃 environments,而不是对 Session 派生子集重新排序;
- Chat 工作区留在可拖拽列表之外,发送完整重排载荷时保持其隐藏位置;
- 真空行与“活跃但缺失”行都参与活跃重排;
- 历史分组永远排在活跃分组之后且不参与重排;
- Session 搜索、初始加载、置顶动画、拖拽期间重排保持禁用(与既有行为一致);
- 折叠态以 path 为键,在重排与“空→有内容”转换中保持不丢失。
7.4 头动作的九步实现契约
handleAddWorkspace的完整时序:① 不捕获任何选择器之前的分组/选中假设;② 调用projectStore.openFolderPicker({ select: false });③ 返回null则无任何副作用地返回;④ 成功后清空sessionSearchQuery;⑤ 等待sessionStore.setGroupMode('project');⑥ 等待响应式渲染、按 path 找到行并scrollIntoView({ block: 'nearest' });⑦ 聚焦该行并施加一个简短、对减少动态设置友好的揭示态;⑧ 若路径是defaultChatWorkspacePath,改为揭示/聚焦 Chat;⑨ 选择器、快照或分组失败都发通知,且无论如何释放忙锁。
其中setGroupMode(mode)被设计为幂等的确定性动作(toggleGroupMode()只是它的 UI 包装器):模态选择器之后不能用“选择器打开前的陈旧状态”去做 toggle,否则用户或另一个窗口在弹窗期间改过设置时就会切错方向;写请求串行化、单独记录“最近成功持久化的模式”、同目标调用方复用同一在途 Promise;失败的写入只把最近一次可见请求回滚到最后成功持久化的模式,且合并进同一写入的调用方会观察到真实失败而非假成功。
7.5 行状态与安全覆盖
- 真空且存在:省略
aria-expanded;主行激活与常驻加号都调用handleNewChatForProject(path); - 有内容的活跃行:保留点击折叠与 hover/focus 加号;
- 活跃但缺失:显示警告图标与本地化的“不可用”tooltip,加号隐藏/禁用,行激活不解释为草稿请求;
- 历史分组:保留折叠与 Session 访问,隐藏新建与重排入口;
- 省略号菜单对所有活跃管理分组可见,与重排门槛解耦;移动项经
canMoveProjectGroup保持禁用,Archive 放在分隔线之后; - 安全覆盖:当 Session 事件先于更新后的 Project 快照到达时,用
sessions.length > 0作为覆盖条件,防止暂时陈旧的sessionCount === 0把“有内容的表头”误变成新建对话动作——这保证了 Session 先到与 Project 先到两种事件顺序都收敛到同一行。
八、边界与兼容性矩阵
计划文档枚举的边界场景中,几个值得注意的实现结果:
- 重复活跃选择:一行(path 键控),显式顺序存活,聚焦既有行;
- 选择归档/已删除路径:
activateAtTop清掉生命周期状态,选择变更把重新激活的路径放到第一;Remove 期间被清空的常规 Session 保持未分配状态; - 选择默认 Chat:不产生 Workspace 重复行,揭示 Chat;
- 注册后路径缺失:已提交的活跃行保持可见并处于不可用态;新建对话在路径重新存在前被阻断,但归档仍可用;
- 单一活跃工作区:归档可用,四个移动项全部禁用;
- 分页:活跃表头可以早于其 Session 页渲染,滚动加载仍由既有容器驱动,表头不得触发重复 Session 抓取;
- 多窗口:每个渲染端都收到版本化 Project 事件,但只有发起侧边栏改变自己的本地搜索、分组、滚动与焦点。
九、约束与非目标
约束(保证改动面最小):
- 原生目录选择保持在既有带类型的 Project 路由与上下文隔离的 bridge 之后;
- 不新增数据库表或 schema 迁移——当前 environment preferences 已经能持久化零 Session 的选中路径(表中
new_environment_preferences早在 schema 版本 32 即存在,见 newEnvironmentPreferences.ts); - 保留版本化 Project 快照/事件的 owner 与其 stale-read fencing;
- 目录选择路径上不做针对无关路径的同步存在性检查;
- 使用 vue-i18n 与既有
DcButton、tooltip、通知、下拉、拖拽原语; - 保留侧边栏既有分页、滚动恢复、快捷键徽标、置顶动画与拖拽门槛。
非目标:不在过滤/分页侧边栏中展示全局 Session 数;不绕过原生选择器创建文件系统目录;不在选择目录后自动创建对话;不在 Workspace 表头加生命周期管理,也不给行加 Restore、Remove、重命名、默认工作区或批量归档;不搜索工作区名、不做嵌套层级或批量操作;不改动 Chat 专区块与 Settings 的目录管理面。
十、测试策略与验证
测试策略分五层,与仓库的 Vitest 组织方式对应:
- Project Store:选择器取消返回
null且不改变选中/快照状态;侧边栏注册模式(select: false)返回路径但不改selectedProjectPath;New Thread 默认模式仍选中路径;事件先于路由响应到达时合并到所请求的快照版本;失败在设置 store error 后重新抛出;重复/归档/已删除选择收敛到同一活跃快照行。 - Session Store:
setGroupMode('project')幂等且只持久化一次;并发 set/toggle 串行化且最后请求的模式胜出;持久化失败只回滚对应请求;连续失败写回滚到最后持久化的模式,同目标调用方观察到在途失败。 - Sidebar 组件:头动作的可访问性/忙锁/取消/失败/成功;从日期模式与激活搜索中成功后揭示并聚焦所选行;零 Session 活跃环境渲染而无 Session 派生分组;默认 Chat 不重复;精确 path 身份保留同名工作区;搜索隐藏空行;agent 过滤/置顶/分页不把非空路径误标为空;空行与加号派发精确的一次性
projectDir流;缺失活跃与历史分组不能新建对话;空分组参与持久化重排;单活跃分组仍可归档而移动项禁用;归档确认派发精确路径、防重复提交、失败保留行/对话框;归档行按 Session 历史收敛为隐藏或有内容的历史呈现;Session 先到与 Project 先到都产生唯一有内容行并保持折叠身份;/与 Windows 盘符根保持为工作区身份。 - Project 主进程:选择目录不做环境投影或无关存在性检查;recent 与 preference 激活在同一事务;新/重激活路径排最前而重复选择保持显式顺序;归档响应暴露精确的已提交版本。
- 集成:零使用量的选中目录以
sessionCount: 0出现在活跃快照;重复选择不产生重复行也不重置显式顺序;首个常规/ACP 非草稿 Session 推进环境投影;渲染层集成测试覆盖事件驱动的空行出现且不重挂侧边栏。
验证命令(交接前执行):
pnpm run format pnpm run i18n pnpm run lint pnpm run typecheck外加在至少 Windows 与一个 POSIX 平台上手工验证:原生选择器取消/成功、键盘焦点、从日期/搜索模式添加、重复选择、缺失路径恢复、活跃 Session 拆除,以及 DeepChat/ACP 的首条 Session 转换。
十一、回滚策略
该特性不引入任何 schema 或持久化的工作区实体:移除表头动作与合并的渲染端投影即可恢复旧 UI;特性启用期间选中的目录仍作为 Settings 与 New Thread 中的合法管理工作区存在,其最新持久化顺序原样保留。
小结
DeepChat 的侧边栏工作区注册是一个典型的“投影合并”问题:与其修改 Session 发现逻辑,不如让侧边栏同时消费两个事实源——版本化的 Project 快照提供身份、顺序与生命周期,Session store 提供可见成员——并以“path 即身份 + 精确版本栅栏”保证任何事件顺序下都收敛到唯一、正确的行呈现。核心落地点包括:new_environment_preferences 表 的单 upsert 置顶语义、ProjectService 的事务化selectDirectory与版本化快照、Project store 的单 owner 刷新与select选项、以及 WindowSideBar.vue 的渲染期派生判定。全程零新表、零新路由、零新 i18n 键,边界与回滚成本极低。
【免费下载链接】deepchat🐬DeepChat - A smart assistant that connects powerful AI to your personal world项目地址: https://gitcode.com/GitHub_Trending/dee/deepchat
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考