Cline Menubar 菜单栏应用:Tauri + Bun Sidecar 构建的 Hub 监控与后台会话控制台
本文基于 Cline 仓库中 apps/examples/menubar 的 README 及其源码实现,完整讲解 Cline Hub 菜单栏监控应用的架构分层、Sidecar 与 Tauri 主进程之间的 JSON Lines 通信协议、系统托盘动态菜单机制,以及开发、打包和 Connector CLI 恢复策略等实战细节。读完本文,你可以理解该示例应用如何把任意来源的 Agent 会话(CLI、桌面端、Hub 客户端)汇聚为一个常驻菜单栏的监控台,并能独立完成其开发调试与本地运行。
一、它在 Cline 生态中的位置
Cline 的 Hub 架构中,任何客户端(CLI、VS Code、Cline Desktop、自研 Agent)都可以作为 WebSocket 客户端接入同一个 Hub 服务,Hub 会把会话事件与 ui.notify / ui.show_window 等 UI 事件广播给所有订阅者。Menubar 示例应用正是这一广播机制的一个消费者:它以 macOS 菜单栏(系统托盘)形式常驻,实时显示 Hub 的连接状态、客户端与会话数量、最近事件,并支持直接从托盘发起新的后台会话(background session)。
README 中给出的整体架构如下:
Any Client (CLI, VS Code, agents)
│
│ ws:// ui.notify / ui.show_window commands
▼
Hub WebSocket Server (@cline/core/hub/server.ts)
│
│ broadcasts ui.notify / ui.show_window events to ALL subscribers
▼
Menu Bar Sidecar (apps/examples/menubar/sidecar/index.ts) ← TypeScript/Bun process
│
│ JSON lines on stdout: hub_state / notification / ready
▼
Rust Tauri App (apps/examples/menubar/src-tauri/src/main.rs)
│
├── Hub Monitor Window (ui/index.html)
│ ● Live hub status, uptime, clients, sessions
│ ● Running session tracker and inspector
│ ● Recent events and background-session launcher
│
├── System Tray Icon with dynamic menu
│ ● Hub Connected — 3 clients, 2 sessions
│ ─────────────────
│ 5 notifications
│ ─────────────────
│ Quit Cline Hub
│
└── Logs notifications to stderr (with severity)
从源码结构看,这个架构对应三层实现:
| 层 | 文件 | 职责 |
|---|---|---|
| Sidecar(TypeScript/Bun 进程) | sidecar/index.ts | 发现或拉起 Hub、订阅 UI 事件、聚合状态、向 stdout 输出 JSON Lines |
| Tauri 主进程(Rust) | src-tauri/src/main.rs | 拉起并看管 Sidecar、解析 stdout 消息、维护托盘菜单与 Webview 窗口 |
| 监控窗口 UI | ui/index.html | 单文件监控界面,渲染状态、会话追踪、事件流并发起新会话/中止会话 |
Hub 服务端能力本身来自 SDK 的 @cline/core 包,README 架构图中标注的 @cline/core/hub/server.ts 对应仓库中 sdk/packages/core/src/hub/server 目录(含 ui-events 等模块);Sidecar 实际调用的是其中的发现/客户端模块,如 discovery、ui-client.ts、session-client.ts。
二、Sidecar:状态聚合与命令通道
Sidecar 是整个应用的“数据面”,由 bun 直接运行 sidecar/index.ts。其 main() 的启动流程是:
- 发现或启动共享 Hub:调用
ensureDetachedHubServer(process.cwd())获取hubUrl与hubAuthToken。失败时通过 stdout 发出{"type":"notification", title:"Hub startup failed", ...}并以退出码 1 终止。 - 发出 ready 消息:成功连接前置条件满足后,立即输出
{"type":"ready", endpoint, wsEndpoint, pid},Tauri 主进程据此记录 Hub 的 WebSocket 端点。 - 建立两个 Hub 客户端:
HubUIClient,clientType: "menubar-app"、displayName: "Cline Menu Bar App",用于订阅ui.notify、客户端/会话生命周期事件;HubSessionClient,clientType: "menubar-background-client",用于发起与中止后台运行时会话。
- 初始状态同步:
syncInitialState()并行调用uiClient.listClients()与uiClient.listSessions()拉取全量客户端和会话,过滤掉自身(isVisibleClient会排除menubar-monitor类型)和非活跃会话(isActiveSession只保留running/idle且有参与者的会话),并按updatedAt倒序解析出最近一次会话的 workspace/provider/model,作为后续“新会话”的上下文(lastSessionContext)。 - 健康轮询:每 5 秒调用一次
fetch(toHubStatusUrl(hubUrl))(带 Bearer token)刷新hubStartedAt,并触发一次hub_state输出,从而让界面上的 uptime 保持新鲜。 - 常驻等待:注册
SIGINT/SIGTERM清理逻辑后永久挂起,进程生命周期由 Tauri 侧的看门狗管理。
2.1 事件订阅与状态机
uiClient.subscribeUI() 注册了六个回调,分别处理 Hub 广播:
onNotify:把HubUINotifyPayload转为事件记录并立即向 stdout 发notification消息;onClientRegistered/onClientDisconnected:维护内存中的clientsMap,记录clientId、displayName、clientType、connectedAt;onSessionCreated/onSessionUpdated:更新sessionsMap,同时从会话或metadata中提取provider、model、prompt,并从aggregateUsage/usage中尽力提取inputTokens、outputTokens、totalCost(字段名做了多重回退,如inputTokens ?? input ?? totalInputTokens);会话状态变为非活跃或被 detach 且无参与者时从 Map 中移除;- 事件队列
events通过pushEvent维护,上限 30 条。
任何状态变化都会调用 emitState(),向 stdout 输出一条 hub_state 消息,字段包括 connected、clients、sessions、clientSummaries(按客户端类型分组聚合,例如把 code-sidecar 系列类型合并展示为 “Cline Desktop”)、sessionSummaries(按 updatedAt 倒序)、events、lastWorkspaceRoot、hubStartedAt 与格式化后的 hubUptime(formatUptime 按 天/小时/分钟/秒 分级)。
2.2 stdin 命令:Tauri 到 Sidecar 的反向通道
Sidecar 用 readline 监听 stdin,每行一个 JSON 命令,定义了 SidecarCommand 三种类型:
| 命令 | 字段 | 行为 |
|---|---|---|
{"type":"new_chat","prompt":"..."} |
prompt |
基于 lastSessionContext 通过 sessionClient.startRuntimeSession() 创建后台会话,再 sendRuntimeSession() 发送 prompt |
{"type":"abort_session","sessionId":"..."} |
sessionId |
调用 sessionClient.abortRuntimeSession() 请求停止指定会话 |
{"type":"shutdown_hub"} |
无 | 调用 stopLocalHubServerGracefully() 优雅关闭本地 Hub |
new_chat 的启动参数值得注意:source: "cline-menubar"、interactive: false、enableTools: true、enableSpawn: false、enableTeams: true、autoApproveTools: true,即这是一个全自动审批的非交互后台 Agent 会话。发起前还会通过 resolveProviderLaunchAuth() 校验鉴权:优先取 ProviderSettingsManager 中已存储的 API Key,其次检查该 Provider 在模型目录(Llms.MODEL_COLLECTIONS_BY_PROVIDER_ID)声明的环境变量是否已配置,两者都没有则抛出 Provider "..." has no stored auth or configured environment key for background sessions. 错误并以 notification 形式回报到 UI。
2.3 Sidecar 的“第二身份”:Hub 守护进程哨兵
sidecar/index.ts 末尾有一个关键设计:进程启动时先无条件执行 claimHubDaemonProcess()(来自 @cline/shared),若声明成功、或识别为打包后的 daemon 入口调用(--cline-hub-daemon 参数、Bun 嵌入的 $bunfs 入口、daemon-entry.js/ts 等),则改为加载 @cline/core/hub/daemon-entry(对应 sdk/packages/core/src/hub/daemon 模块),不再执行 main()。源码注释解释了原因:打包产物中 Sidecar 与 Hub daemon 共用同一份 Bun 可执行文件,daemon-hosted 会话派生的子进程(shell 命令、MCP server、hooks)若继承了守护进程哨兵环境变量,会误以为自己是 Hub daemon 并在 EADDRINUSE 上崩溃——因此在“决定人格”之前必须先消费掉该哨兵。
三、Rust/Tauri 层:进程管理、协议解析与托盘菜单
Tauri 主进程 src-tauri/src/main.rs 负责“控制面”,核心逻辑包括:
3.1 Sidecar 的拉起与看门狗
- workspace 根目录探测:
main()中先执行git -C <cwd> rev-parse --show-toplevel拿到仓库根作为workspace_root,失败则回退到当前目录。 - Sidecar 二选一:release 构建下优先查找预编译二进制
menubar-sidecar-<target_triple>(resolve_sidecar_binary会依次检查仓库内src-tauri/bin与可执行文件同级/Resources目录);开发构建则回退到用bun --conditions=development run sidecar/index.ts运行源码(脚本候选路径包含sdk/apps/examples/menubar与apps/examples/menubar两种布局)。 - stdout/stderr 双线程读取:stdout 每行按
SidecarMessage(serdecamelCase)反序列化并交给handle_sidecar_message;解析失败的非 JSON 行降级为warn级通知。stderr 每行都以[menubar-sidecar:err]前缀写入自身 stderr,并转为error级通知。 - 5 秒看门狗:
tauri::Builder.setup中额外起一个线程,每 5 秒调用一次start_sidecar();由于start_sidecar内部会先try_wait()检查子进程是否存活,Sidecar 崩溃后会被自动拉起,拉不起来则推送 “Sidecar restart failed” 错误。
3.2 三类 stdout 消息的处理
| Sidecar 消息 | 主进程行为 |
|---|---|
ready |
记录 wsEndpoint(或 endpoint),清除 last_error,刷新托盘菜单,stderr 打印 [menubar-sidecar] ready at <url> |
hub_state |
更新 HubState(connected、clientSummaries、sessionSummaries、events、lastWorkspaceRoot、hubUptime);仅当 (connected, clientCount, sessionCount) 三元组变化时才打印日志,避免刷屏;随后 refresh_tray_menu |
notification |
经 push_notification 处理:写入 stderr([notification/<severity>] title: body)、追加进最近 50 条通知队列、error 级会置 connected=false 并记录 lastError、在 macOS 上通过 osascript 触发系统通知,最后刷新托盘菜单 |
通知的 macOS 系统通知实现见 show_system_notification():它把标题与正文转义后拼成 AppleScript display notification 调用。
3.3 动态托盘菜单
build_tray_menu() 在每次状态变化后整体重建菜单,结构为:
- 状态项:
Hub Connected/Hub: error/Hub: disconnected; - 连接中时追加
Uptime: <hubUptime>以及每个客户端分组一行Label: Name, Sessions: N; - 存在错误时追加(禁用的)
Last Error: ...(超长截断到 96 字符),点击可弹 AppleScript 对话框查看完整错误; Open Dashboard:显示并聚焦 label 为main的 Webview 窗口;New Session:仅当lastWorkspaceRoot存在时可用;点击后弹出 AppleScript 输入框(prompt_for_new_chat)获取 prompt,然后经 stdin 发送new_chat命令;N notifications/No notifications:点击弹出最近一条通知详情;Quit Cline Hub:先向 Sidecar 发送shutdown_hub,等待 300ms 后退出。
3.4 Tauri 命令与窗口配置
主进程通过 tauri::generate_handler! 暴露三个命令:get_hub_state(返回 connected、clientSummaries、sessionSummaries、events、lastWorkspaceRoot、hubUptime、lastError、notificationCount 与最近 10 条通知)、start_new_session(prompt)、abort_session(sessionId)——后两者本质都是向 Sidecar stdin 写 JSON 命令。
窗口与打包配置见 src-tauri/tauri.conf.json:产品名 Cline Hub、应用标识 bot.cline.menubar;主窗口 label main、标题 Cline Hub Monitor,1600×900、最小 1180×720、无边框(decorations: false);frontendDist 指向 ../ui(即静态单文件页面);bundle.externalBin 为 bin/menubar-sidecar,beforeDevCommand/beforeBuildCommand 都会先执行 bun run build:sidecar:bin 构建 Sidecar 可执行文件,macOS 打包后还会用 xattr -d com.apple.quarantine 去除隔离属性。
四、监控窗口 UI:单文件页面与预览回退
ui/index.html 是一个自包含的单文件页面(样式 + 布局 + 脚本内联)。它模拟了 macOS 桌面场景:顶部系统栏、菜单栏、主监控面板,包含实时 Hub 状态、运行中会话追踪器、最近事件流以及后台会话入口。脚本部分的关键机制是:
const invoke = window.__TAURI__?.core?.invoke;
- 在 Tauri 环境中,
invoke可用,页面调用get_hub_state等命令获取真实状态; - 在纯浏览器环境(如
bun run dev:ui)中window.__TAURI__不存在,页面回退到内置的sampleState(内置客户端、会话、事件样例数据),并在界面上显示 “Preview data” 徽标——这正是 README 中 “run only the Hub Monitor UI athttp://127.0.0.1:3466/with preview data” 的实现来源。
页面还内置了数字格式化(fmtNumber 把大数渲染为 1.2k/3.4m)、成本格式化(fmtCost 输出 $x.xxx)、相对时间(timeAgo:now / Nm ago / Nh ago / Nd ago)等工具函数,用于渲染会话的 token 消耗、成本与更新时间。
五、Connector CLI 恢复策略
README 最后一段说明:菜单栏应用启动共享 Hub 时,需要向 Hub 提供“用于恢复持久化 connector 的 CLI 命令”。该策略在 sidecar/connector-cli-launch.ts 中实现,resolveMenubarConnectorCliLaunchSpec() 按以下优先级返回 ConnectorCliLaunchSpec(launcher + connectArgsPrefix + cwd):
- 环境变量显式指定:若设置了
CLINE_CLI_PATH,直接使用其作为 launcher,参数前缀["connect"]; - 开发构建:若仓库内存在
apps/cli/src/index.ts(兼容旧布局sdk/apps/cli/src/index.ts),则用当前 Bun 可执行文件(非 Bun 运行时则退回bun命令)运行源码入口,参数前缀为["--conditions=development", <源码路径>, "connect"]; - 打包构建:回退到
PATH中的cline命令,参数前缀["connect"]。
Sidecar 启动时通过 configureMenubarConnectorCliLaunch(workspaceRoot) 把解析结果写入 @cline/shared 的全局配置,Hub 恢复 connector 时即用该规格重新连接 CLI。这也解释了 README 的表述:开发构建优先 apps/cli/src/index.ts,打包构建优先 CLINE_CLI_PATH 或 PATH 中的 cline。
六、开发与运行指南
以下命令均在 apps/examples/menubar 目录下执行,脚本定义见 package.json:
| 命令 | 实际执行 | 说明 |
|---|---|---|
bun run dev:ui |
python3 -m http.server 3466 --bind 127.0.0.1 --directory ui |
仅启动 Hub Monitor UI(http://127.0.0.1:3466/),无 Tauri 时使用内置预览数据 |
bun run dev |
tauri dev |
运行完整 Tauri 应用,拉起真实 Hub Sidecar;beforeDevCommand 会先构建 Sidecar 二进制 |
bun run build |
tauri build |
打包发布版应用(含 Sidecar 外部二进制与全平台 bundle) |
bun run build:sidecar:bin |
bun run scripts/build-sidecar-bin.ts |
单独构建 Sidecar 可执行文件(src-tauri/bin/menubar-sidecar-<triple>) |
bun run test |
vitest run --config vitest.config.ts |
运行单元测试(含 connector-cli-launch.test.ts 对 CLI 解析策略的覆盖) |
bun run typecheck |
tsc --noEmit |
TypeScript 检查 |
该包声明的依赖只有 @cline/core 与 @cline/shared 两个 workspace 包(提供 Hub 客户端、Provider 设置管理、daemon 哨兵等能力),开发依赖为 @tauri-apps/cli@^2.0.0、TypeScript 与 vitest,运行 Sidecar 需要本机可用的 Bun 与 Python 3(dev:ui 依赖 http.server)。
七、关键文件索引
| 文件 | 说明 |
|---|---|
| apps/examples/menubar/README.md | 架构图与开发命令原文 |
| apps/examples/menubar/sidecar/index.ts | Sidecar 主逻辑:Hub 发现、事件订阅、状态聚合、stdin 命令、daemon 哨兵 |
| apps/examples/menubar/sidecar/connector-cli-launch.ts | Connector 恢复用 CLI 的三级解析策略 |
| apps/examples/menubar/src-tauri/src/main.rs | Tauri 主进程:Sidecar 拉起/看门狗、协议解析、托盘菜单、系统通知 |
| apps/examples/menubar/src-tauri/tauri.conf.json | 窗口、外部二进制与打包配置 |
| apps/examples/menubar/ui/index.html | 监控窗口单文件 UI(含预览数据回退) |
| sdk/packages/core/src/hub/client/ui-client.ts、session-client.ts | Sidecar 使用的 Hub UI/会话客户端 |
| sdk/packages/core/src/hub/discovery/index.ts | Hub 发现(ensureDetachedHubServer、toHubStatusUrl) |
| sdk/packages/core/src/hub/daemon/index.ts | Sidecar 打包产物复用的 Hub daemon 入口 |
八、小结
Menubar 示例展示了 Cline Hub 生态中一个典型的“轻量消费者”实现:Sidecar 用 TypeScript 承担 Hub 协议与业务聚合,Tauri/Rust 层承担进程治理与原生 UI(托盘、系统通知、Webview),二者之间仅靠三种 JSON Lines 消息(ready、hub_state、notification)和 stdin JSON 命令通信。这种分层使 UI 层可以脱离原生环境独立预览(dev:ui),也使 Sidecar 二进制能同时充当共享 Hub 的守护进程入口,是理解 Cline Hub 客户端接入方式的优质参考实现。
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 StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
