首页
/ Agent Zero WebUI 侧边栏组件架构指南:模块划分、状态管理与本地契约解析

Agent Zero WebUI 侧边栏组件架构指南:模块划分、状态管理与本地契约解析

2026-09-14 14:03:18作者:舒璇辛Bertina

Agent Zero 的 WebUI 左侧边栏(Sidebar)是聊天与任务导航的核心入口,承载会话列表、调度任务列表、快捷操作与偏好设置。本文基于仓库中 webui/components/sidebar/AGENTS.md 这一组件 DOX(Developer Documentation)文档,结合其下 8 个源码文件,系统讲解侧边栏的模块归属、共享状态机制、父子上下文树渲染、任务列表与调度器集成、偏好面板实现,以及全部 17 条本地契约(Local Contracts)的行为约束与底层实现依据,帮助你安全地二次开发、扩展或排查该组件。

组件定位与模块所有权(Ownership)

侧边栏组件在 WebUI 中承担四类职责:左侧边栏整体布局(shell)聊天/任务列表顶部操作区底部偏好设置区。官方 DOX 中明确了每个子目录的"所有权"边界:

  • left-sidebar.htmlsidebar-store.js:拥有侧边栏外壳(shell)与共享状态;
  • top-section/:拥有顶部区(header)与快捷操作(quick actions);
  • chats/:拥有聊天列表 UI 与状态;
  • tasks/:拥有任务列表 UI 与状态;
  • bottom/:拥有侧边栏下部控件与偏好设置面板。

从目录结构看,每个子模块都遵循"一个 store + 一个 HTML 组件"的配对模式,例如 chats/chats-store.js 对应 chats/chats-list.htmltasks/tasks-store.js 对应 tasks/tasks-list.htmlbottom/preferences/preferences-store.js 对应 bottom/preferences/preferences-panel.html。所有 store 都通过 /js/AlpineStore.jscreateStore(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.htmlchats-list.htmltasks-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("在新窗口打开"场景),随后回退到 sessionStoragelastSelectedChat。选中状态同时持久化到 sessionStorage,配合 getContext()/setContext() 完成与聊天主区域的联动。

任务列表:调度器上下文专属入口

tasks-store.jstasks-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)、清空聊天(resetchatsStore.resetChat)、删除任务(deleteTask → scheduler store 的 deleteTaskFromSidebar)、更多菜单。选中任务通过 selectTask(taskId) 转发到 chatsStore.selectChat,实现任务与会话的无缝切换。

区块头是可折叠的(.section-header-collapsible),点击调用 sidebarStore.toggleSection('tasks'),折叠状态持久化并在展开/收起时通过 Bootstrap Collapse.getOrCreateInstance 驱动动画。CSS 中 #tasks-sectionmax-height: 40% 限制了展开时的最大视口占比。

底部偏好面板:偏好状态与副作用

preferences-store.js 集中管理用户偏好,每一项都采用"getter/setter + 副作用函数"模式,设置即持久化(localStorage)并立即生效:

偏好 默认值 可选值 副作用
darkMode true 布尔 切换 bodydark-mode/light-mode
autoScroll true 布尔 预留接口(_applyAutoScroll 暂为空实现)
speech false 布尔 关闭时调用 ttsService.stop()
showUtils false 布尔 切换 documentElementshow-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 定义了四类控件的移动端/桌面端独立开关:projectSelectortimeconnectionStatusrightCanvasRailnormalizeUiVisibility() 用默认值兜底合并不完整输入,isUiControlVisible(control) 依据 _isMobileViewport(768px 断点)选择生效分支。这支撑了契约 14 中"chat-top 控件与右侧 canvas rail 拥有独立移动端/桌面端状态"的要求。

Process-Detail 与工具消息偏好

detailMode 的四个档位分别控制进程组(process group)步骤的展示粒度,_applyDetailMode 通过 process-group-dom.jsapplyModeSteps(detailMode, showUtils, chatHistory) 对全部既有 DOM 生效,且支持传入显式的 chatHistory 渲染目标——对应契约 15 的"异步展开钩子 + 显式渲染目标 + 原子交换"约束。showUtils 偏好同时作用于单条工具步骤和仅含工具步骤的进程组外层 chrome(.show-utility-messages 类),避免隐藏工具运行后在转录中留下空标题(契约 16)。

版本信息

sidebar-bottom-store.jsglobalThis.gitinfo(index.html 注入)读取 versioncommit_time,拼成 Version x.y.z <commit_time> 显示在侧边栏底部。契约 12 要求该时间戳以 UTC 格式展示、无时区后缀且保持单行——对应 CSS 中 #a0versionwhite-space: nowrap

扩展机制:插槽、行菜单与列表排序

侧边栏是插件扩展的重灾区,DOX 与源码共同明确了三层扩展面:

  1. 插槽(Slots)left-sidebar.html 提供 sidebar-start/sidebar-end/sidebar-row-actions-menusidebar-top.html 提供 sidebar-top-wrapper-start/sidebar-top-wrapper-endquick-actions.html 通过 x-teleport 提供 sidebar-quick-actions-dropdown-start/endchats-list.html 提供 sidebar-chats-list-start/end/chats-header-controlstasks-list.html 提供 sidebar-tasks-list-start/end。quick-actions 区采用 5 列 grid 布局,插件通过 x-extension#quick-actions 注入按钮即可自动参与排布。

  2. 行溢出菜单(sidebar-row-actions-menu):契约 11 明确规定 sidebar-row-actions-menu 插槽拥有"插件贡献的行菜单操作"——即插件的菜单项渲染在这个统一下拉容器内,而不是 patch 到各聊天/任务 store。菜单的开关、定位、点击外部关闭(@click.window)与 Esc 关闭(@keydown.escape.window)全部由 sidebar-storerowMenuToggle/rowMenuClose/rowMenuClick/rowMenuPos 统一管理。

  3. 列表排序扩展(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-storetasks-store 分别承载会话树与调度任务,preferences-store 以副作用模式统一管理偏好,而 17 条本地契约则将响应式、扩展、动画隔离、乐观删除等行为规范固化下来。开发者只要遵循"数据来自同步快照/轮询""扩展走插槽与 rowListExtensions""状态变更经 store 协调"三条主线,即可安全地在 webui/components/sidebar 内进行二次开发。

登录后查看全文
热门项目推荐
相关项目推荐