Agent Zero WebUI 侧边栏组件架构指南:模块划分、状态管理与本地契约解析
Agent Zero 的 WebUI 左侧边栏(Sidebar)是聊天与任务导航的核心入口,承载会话列表、调度任务列表、快捷操作与偏好设置。本文基于仓库中 webui/components/sidebar/AGENTS.md 这一组件 DOX(Developer Documentation)文档,结合其下 8 个源码文件,系统讲解侧边栏的模块归属、共享状态机制、父子上下文树渲染、任务列表与调度器集成、偏好面板实现,以及全部 17 条本地契约(Local Contracts)的行为约束与底层实现依据,帮助你安全地二次开发、扩展或排查该组件。
组件定位与模块所有权(Ownership)
侧边栏组件在 WebUI 中承担四类职责:左侧边栏整体布局(shell)、聊天/任务列表、顶部操作区、底部偏好设置区。官方 DOX 中明确了每个子目录的"所有权"边界:
- left-sidebar.html 与 sidebar-store.js:拥有侧边栏外壳(shell)与共享状态;
- top-section/:拥有顶部区(header)与快捷操作(quick actions);
- chats/:拥有聊天列表 UI 与状态;
- tasks/:拥有任务列表 UI 与状态;
- bottom/:拥有侧边栏下部控件与偏好设置面板。
从目录结构看,每个子模块都遵循"一个 store + 一个 HTML 组件"的配对模式,例如 chats/chats-store.js 对应 chats/chats-list.html,tasks/tasks-store.js 对应 tasks/tasks-list.html,bottom/preferences/preferences-store.js 对应 bottom/preferences/preferences-panel.html。所有 store 都通过 /js/AlpineStore.js 的 createStore(name, model) 注册为 Alpine 全局 store,供 HTML 模板以 $store.sidebar、$store.chats、$store.tasks、$store.preferences、$store.sidebarBottom 访问。
侧边栏外壳与共享状态:sidebar-store.js
外壳由 left-sidebar.html 组装,其布局分三段:顶部 left-panel-top(包含 sidebar-top.html、chats-list.html、tasks-list.html)、底部 left-panel-bottom(包含 sidebar-bottom.html),并在面板首尾预留了 sidebar-start / sidebar-end 两个扩展插槽,供插件注入内容。整个面板宽 250px,桌面端常驻,移动端(≤768px)变为固定定位抽屉,通过 #left-panel.hidden { margin-left: -250px } 实现滑出/收起。
sidebar-store.js 是侧边栏的共享状态中枢,管理以下关键状态:
| 状态字段 | 默认值 | 说明 |
|---|---|---|
isOpen |
true |
侧边栏是否展开,移动端默认收起 |
menuOpen / dropdownStyle |
false / {} |
顶部快捷操作下拉菜单的开关与定位样式 |
rowMenuOpenId / rowMenuKind / rowMenuStyle |
"" / "chat" / {} |
行内溢出菜单(chat/task 行"更多"按钮)状态与定位 |
rowListExtensions |
{ chat: {}, task: {} } |
插件注册的列表排序/分隔符扩展 |
sectionStates |
tasks/chatActions/preferences 均 false |
各区块折叠状态,持久化到 localStorage(键 sidebarSections) |
响应式行为与 768px 断点
isMobile() 以 window.innerWidth <= 768 判定移动端,与偏好 store 中的 _isMobileViewport 判定一致,是全组件共享的断点。handleResize() 在移动端强制收起侧边栏并关闭所有菜单,且 init() 注册了全局 resize 监听,destroy() 负责移除监听——这是组件生命周期管理的一个可复用范本。
行内溢出菜单的智能定位
rowMenuPos(triggerElement) 实现了"防溢出"定位算法:菜单固定宽 180px,通过比较触发点下方可用空间(spaceBelow < 96)与上方空间决定向上还是向下展开,并用 padding(8px)保证菜单不超出视口左右边缘。这是契约中"避免文本或控件溢出固定侧边栏宽度"(见下文契约 13)的直接实现,菜单采用 position: fixed 以逃脱侧边栏的 overflow: hidden 裁剪。
聊天列表:父子上下文树与乐观删除
chats-store.js 管理 contexts(上下文数组)、selected(当前选中 id)、expandedParents(展开状态)与 deletedContextIds(删除墓碑集合)。
数据来源与排序
上下文列表由 applyContexts(contextsList) 从同步快照(WebSocket 状态同步)灌入,按 created_at 降序排列,并过滤掉已被本地删除(tombstone)的条目。topLevelContexts() 过滤掉所有 parent_context_id 非空的条目作为顶级列表,childContexts(parentId) 则按父 id 聚拢子级。
父子上下文的缩进渲染
在 chats-list.html 中,顶级条目渲染为 chat-tree-item,每个条目内的展开按钮(chat-expand-btn)只在 hasChildren(context.id) 时显示,且采用 position: absolute; left: 2px 的 parent-only leading slot 定位,chat-has-children 时给文本区额外 padding-left: 24px,从而"不占用普通聊天行文本的正常边距"。子级渲染在嵌套的 chat-child-list 中,通过 .chat-child-indent(18px 缩进块)体现层级关系。所有子级都可以独立点击选中,即使其父级处于折叠状态(从顶级列表"隐藏但保持可选")。
展开状态的智能水合
applyContexts() 在同步元数据变化时会自动修正展开状态:如果选中条目带 parent_context_id,则自动展开其父级;如果选中条目本身有子级且用户从未切换过(expandedParents[selectedId] === undefined),则自动展开一次。这正是契约 5 "恢复的选中父聊天在上下文水合期间自动展开一次,除非用户已手动切换"的实现。
乐观删除与墓碑机制
killChat(id) 是契约 17 的完整落地:先在同一个 Alpine 渲染批次中把行从 contexts 移除并写入 deletedContextIds(墓碑),同时触发 switchFromContext 选择回退聊天;随后才异步调用 /chat_remove 删除服务端数据。若请求失败,则清除墓碑并把行按时间排序插回列表。墓碑的关键作用是:即使后续收到乱序的 poll 或 push 快照(其中仍包含该 context),applyContexts 的 .filter((context) => !this.deletedContextIds[context?.id]) 也会阻止其重新插入页面。
会话选择与恢复
init() 支持两种恢复路径:优先读取 URL 参数 ?ctxid=abc("在新窗口打开"场景),随后回退到 sessionStorage 的 lastSelectedChat。选中状态同时持久化到 sessionStorage,配合 getContext()/setContext() 完成与聊天主区域的联动。
任务列表:调度器上下文专属入口
tasks-store.js 与 tasks-list.html 实现 Tasks 区块。其核心契约是:Tasks 列表仅承载 scheduler 支持的 task 上下文,不得用于 chat-bound 的并行子级。数据由 applyTasks() 从全局 poll() 灌入,按 created_at 降序,与聊天列表共用 sidebarStore.sortRows("task", rows) 的扩展排序管道。
任务行渲染了 task_name(回退 Task #${no})、调度状态徽章(idle/running/disabled/error,见 .scheduler-status-* 样式类)以及四个行内操作:查看详情(openDetail → scheduler store 的 showTaskDetail)、清空聊天(reset → chatsStore.resetChat)、删除任务(deleteTask → scheduler store 的 deleteTaskFromSidebar)、更多菜单。选中任务通过 selectTask(taskId) 转发到 chatsStore.selectChat,实现任务与会话的无缝切换。
区块头是可折叠的(.section-header-collapsible),点击调用 sidebarStore.toggleSection('tasks'),折叠状态持久化并在展开/收起时通过 Bootstrap Collapse.getOrCreateInstance 驱动动画。CSS 中 #tasks-section 的 max-height: 40% 限制了展开时的最大视口占比。
底部偏好面板:偏好状态与副作用
preferences-store.js 集中管理用户偏好,每一项都采用"getter/setter + 副作用函数"模式,设置即持久化(localStorage)并立即生效:
| 偏好 | 默认值 | 可选值 | 副作用 |
|---|---|---|---|
darkMode |
true |
布尔 | 切换 body 的 dark-mode/light-mode 类 |
autoScroll |
true |
布尔 | 预留接口(_applyAutoScroll 暂为空实现) |
speech |
false |
布尔 | 关闭时调用 ttsService.stop() |
showUtils |
false |
布尔 | 切换 documentElement 的 show-utility-messages 类 |
chatWidth |
"55" |
"40"/"55"/"80"/"full" |
设置 CSS 变量 --chat-max-width(em 或 100%) |
detailMode |
"current" |
"collapsed"/"list"/"current"/"expanded" |
调用 applyModeSteps() 展开/折叠进程组 |
实例级界面可见性(UI Visibility)
UI_VISIBILITY_DEFAULTS 定义了四类控件的移动端/桌面端独立开关:projectSelector、time、connectionStatus、rightCanvasRail。normalizeUiVisibility() 用默认值兜底合并不完整输入,isUiControlVisible(control) 依据 _isMobileViewport(768px 断点)选择生效分支。这支撑了契约 14 中"chat-top 控件与右侧 canvas rail 拥有独立移动端/桌面端状态"的要求。
Process-Detail 与工具消息偏好
detailMode 的四个档位分别控制进程组(process group)步骤的展示粒度,_applyDetailMode 通过 process-group-dom.js 的 applyModeSteps(detailMode, showUtils, chatHistory) 对全部既有 DOM 生效,且支持传入显式的 chatHistory 渲染目标——对应契约 15 的"异步展开钩子 + 显式渲染目标 + 原子交换"约束。showUtils 偏好同时作用于单条工具步骤和仅含工具步骤的进程组外层 chrome(.show-utility-messages 类),避免隐藏工具运行后在转录中留下空标题(契约 16)。
版本信息
sidebar-bottom-store.js 从 globalThis.gitinfo(index.html 注入)读取 version 与 commit_time,拼成 Version x.y.z <commit_time> 显示在侧边栏底部。契约 12 要求该时间戳以 UTC 格式展示、无时区后缀且保持单行——对应 CSS 中 #a0version 的 white-space: nowrap。
扩展机制:插槽、行菜单与列表排序
侧边栏是插件扩展的重灾区,DOX 与源码共同明确了三层扩展面:
-
插槽(Slots):
left-sidebar.html提供sidebar-start/sidebar-end/sidebar-row-actions-menu;sidebar-top.html提供sidebar-top-wrapper-start/sidebar-top-wrapper-end;quick-actions.html通过x-teleport提供sidebar-quick-actions-dropdown-start/end;chats-list.html提供sidebar-chats-list-start/end/chats-header-controls;tasks-list.html提供sidebar-tasks-list-start/end。quick-actions 区采用 5 列 grid 布局,插件通过x-extension向#quick-actions注入按钮即可自动参与排布。 -
行溢出菜单(sidebar-row-actions-menu):契约 11 明确规定
sidebar-row-actions-menu插槽拥有"插件贡献的行菜单操作"——即插件的菜单项渲染在这个统一下拉容器内,而不是 patch 到各聊天/任务 store。菜单的开关、定位、点击外部关闭(@click.window)与 Esc 关闭(@keydown.escape.window)全部由sidebar-store的rowMenuToggle/rowMenuClose/rowMenuClick/rowMenuPos统一管理。 -
列表排序扩展(Row List Extensions):契约 11 后半段要求"列表顺序插件通过 sidebar store 注册稳定排序和分隔符回调,而不是修补 chat/task store 或注入行 DOM"。实现上,插件调用
sidebarStore.registerRowListExtension(kind, name, extension)(kind 为"chat"或"task")注册{ sort, dividerBefore }回调;sortRows(kind, rows)按注册顺序链式应用各扩展的sort()(无 sort 则原样传递),hasRowDividerBefore(kind, item, index, rows)汇总判断是否在行前绘制分隔线(sidebar-list-divider类)。聊天/任务模板中通过$store.sidebar.sortRows(...)与hasRowDividerBefore(...)消费这些扩展,实现完全解耦。
行操作按钮的显示时机
契约 9 要求行操作按钮只在指针悬停或触摸设备选中时占用布局宽度。chats-list.html 中的 CSS 精确实现了这一点:
.device-pointer .chat-container .chat-list-action-btn,
.device-touch .chat-container:not(.chat-selected) .chat-list-action-btn {
display: none;
}
.device-pointer .chat-container:hover .chat-list-action-btn,
.device-touch .chat-container.chat-selected .chat-list-action-btn {
display: inline-flex;
}
即桌面端非悬停时、触摸端未选中时按钮均不渲染空间,避免常驻按钮挤压聊天名称的可用宽度。
布局对齐:项目气泡与区块缩进的"回收"策略
契约 8 规定聊天与任务两个列表要共享同一段左侧内容缩进,使各自行内的项目彩色气泡(.project-color-ball,颜色来自 context.project.color,运行中附加 heartbeat 动画)垂直对齐,而区块标题(section-header)保持标准缩进。源码中 #chats-section 与 #tasks-section 均设置 margin-inline-start: calc(0px - var(--spacing-sm)); width: calc(100% + var(--spacing-sm)) 来"回收"外层 padding,再通过 #chats-section .section-header-row, #tasks-section .section-header { margin-inline-start: var(--spacing-sm) } 将标题推回标准缩进。CSS 中还通过 display: contents 扁平化 x-component 包装层,保证内部滚动容器能正确继承 flex 链。
工作动画的作用域隔离
契约 7 要求运行中的父/子聊天共享 chat-working-bubble 动画,但该动画必须与任务、连接状态指示器隔离。chats-list.html 中动画仅挂在聊天列表的 .project-color-ball.heartbeat 上(@keyframes chat-working-bubble,1500ms 循环的旋转-缩放变形);任务列表虽然复用了 .heartbeat 类名,但其工作状态由任务自身的 running 标志与调度状态徽章表达,不继承该 keyframes 的作用域,从而避免视觉混淆。
与 WebSocket 状态同步的协作约定
契约 2 要求聊天/任务列表的更新与 WebSocket 状态同步兼容。从实现看:聊天列表的数据流完全由同步快照驱动(applyContexts),切换会话在 push 模式下会经 setContext(id) 触发新的 state_request,仅当同步模式为 DEGRADED 时才退化为调用 globalThis.poll() 轮询兜底;任务列表则直接由 poll() 驱动(tasks-store.init() 注释明确说明"data is driven by poll() in index.js")。因此对这两个 store 的任何改动都必须保持"以快照/轮询数据为唯一事实源"的约束,不能引入本地写穿(local write-through)逻辑破坏同步一致性。
工作指引与验证清单
DOX 给出的协作原则是:导航与状态变更必须与 WebSocket 同步以及聊天/项目 store 协调(对应 webui/AGENTS.md 中 WebUI 组件开发的总体纪律)。修改侧边栏后,官方推荐的冒烟验证项为:
- 侧边栏折叠/展开(桌面端与移动端抽屉行为、768px 断点);
- 聊天列表(新建、选中、展开/折叠子级、乐观删除与失败回滚);
- 任务列表(调度任务的状态徽章、详情/清空/删除操作、区块折叠);
- 快捷操作(quick actions 按钮与下拉定位);
- 偏好面板(dark mode、宽度、Detail、Speech、工具消息开关的即时生效与持久化)。
小结
侧边栏是 Agent Zero WebUI 中"状态同步、插件扩展与用户偏好"三者交汇最密集的组件:sidebar-store 提供外壳与共享的菜单/排序状态,chats-store 与 tasks-store 分别承载会话树与调度任务,preferences-store 以副作用模式统一管理偏好,而 17 条本地契约则将响应式、扩展、动画隔离、乐观删除等行为规范固化下来。开发者只要遵循"数据来自同步快照/轮询""扩展走插槽与 rowListExtensions""状态变更经 store 协调"三条主线,即可安全地在 webui/components/sidebar 内进行二次开发。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.24 K638- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python650
SlideSCIPPT插件,支持素材库、AI助手、一键添加图片标题,复制粘贴位置、一键图片对齐、一键插入Markdown(加粗、超链接等行内样式、代码块、LaTeX等块级样式)、便捷导出图片!C#180
hello-agents📚 《从零开始构建智能体》——从零开始的智能体原理与实践教程Python52774
new-apiAI模型聚合管理中转分发系统,一个应用管理您的所有AI模型,支持将多种大模型转为统一格式调用,支持OpenAI、Claude、Gemini等格式,可供个人或者企业内部管理与分发渠道使用。🍥 A Unified AI Model Management & Distribution System. Aggregate all your LLMs into one app and access them via an OpenAI-compatible API, with native support for Claude (Messages) and Gemini formats.Go22545
JeecgBoot🔥企业级低代码平台集成了AI应用平台,帮助企业快速实现低代码开发和构建AI应用!前后端分离架构 SpringBoot,SpringCloud、Mybatis,Ant Design4、 Vue3.0、TS+vite!强大的代码生成器让前后端代码一键生成,无需写任何代码! 引领AI低代码开发模式: AI生成->OnlineCoding-> 代码生成-> 手工MERGE,显著的提高效率,又不失灵活~Java36351