首页
/ Cline Menubar 菜单栏应用:Tauri + Bun Sidecar 构建的 Hub 监控与后台会话控制台

Cline Menubar 菜单栏应用:Tauri + Bun Sidecar 构建的 Hub 监控与后台会话控制台

2026-09-06 13:58:45作者:郁楠烈Hubert

本文基于 Cline 仓库中 apps/examples/menubarREADME 及其源码实现,完整讲解 Cline Hub 菜单栏监控应用的架构分层、Sidecar 与 Tauri 主进程之间的 JSON Lines 通信协议、系统托盘动态菜单机制,以及开发、打包和 Connector CLI 恢复策略等实战细节。读完本文,你可以理解该示例应用如何把任意来源的 Agent 会话(CLI、桌面端、Hub 客户端)汇聚为一个常驻菜单栏的监控台,并能独立完成其开发调试与本地运行。

Cline Hub Monitor 预览

一、它在 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 实际调用的是其中的发现/客户端模块,如 discoveryui-client.tssession-client.ts

二、Sidecar:状态聚合与命令通道

Sidecar 是整个应用的“数据面”,由 bun 直接运行 sidecar/index.ts。其 main() 的启动流程是:

  1. 发现或启动共享 Hub:调用 ensureDetachedHubServer(process.cwd()) 获取 hubUrlhubAuthToken。失败时通过 stdout 发出 {"type":"notification", title:"Hub startup failed", ...} 并以退出码 1 终止。
  2. 发出 ready 消息:成功连接前置条件满足后,立即输出 {"type":"ready", endpoint, wsEndpoint, pid},Tauri 主进程据此记录 Hub 的 WebSocket 端点。
  3. 建立两个 Hub 客户端
    • HubUIClientclientType: "menubar-app"displayName: "Cline Menu Bar App",用于订阅 ui.notify、客户端/会话生命周期事件;
    • HubSessionClientclientType: "menubar-background-client",用于发起与中止后台运行时会话。
  4. 初始状态同步syncInitialState() 并行调用 uiClient.listClients()uiClient.listSessions() 拉取全量客户端和会话,过滤掉自身(isVisibleClient 会排除 menubar-monitor 类型)和非活跃会话(isActiveSession 只保留 running/idle 且有参与者的会话),并按 updatedAt 倒序解析出最近一次会话的 workspace/provider/model,作为后续“新会话”的上下文(lastSessionContext)。
  5. 健康轮询:每 5 秒调用一次 fetch(toHubStatusUrl(hubUrl))(带 Bearer token)刷新 hubStartedAt,并触发一次 hub_state 输出,从而让界面上的 uptime 保持新鲜。
  6. 常驻等待:注册 SIGINT/SIGTERM 清理逻辑后永久挂起,进程生命周期由 Tauri 侧的看门狗管理。

2.1 事件订阅与状态机

uiClient.subscribeUI() 注册了六个回调,分别处理 Hub 广播:

  • onNotify:把 HubUINotifyPayload 转为事件记录并立即向 stdout 发 notification 消息;
  • onClientRegistered / onClientDisconnected:维护内存中的 clients Map,记录 clientIddisplayNameclientTypeconnectedAt
  • onSessionCreated / onSessionUpdated:更新 sessions Map,同时从会话或 metadata 中提取 providermodelprompt,并从 aggregateUsage/usage 中尽力提取 inputTokensoutputTokenstotalCost(字段名做了多重回退,如 inputTokens ?? input ?? totalInputTokens);会话状态变为非活跃或被 detach 且无参与者时从 Map 中移除;
  • 事件队列 events 通过 pushEvent 维护,上限 30 条。

任何状态变化都会调用 emitState(),向 stdout 输出一条 hub_state 消息,字段包括 connectedclientssessionsclientSummaries(按客户端类型分组聚合,例如把 code-sidecar 系列类型合并展示为 “Cline Desktop”)、sessionSummaries(按 updatedAt 倒序)、eventslastWorkspaceRoothubStartedAt 与格式化后的 hubUptimeformatUptime 按 天/小时/分钟/秒 分级)。

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: falseenableTools: trueenableSpawn: falseenableTeams: trueautoApproveTools: 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/menubarapps/examples/menubar 两种布局)。
  • stdout/stderr 双线程读取:stdout 每行按 SidecarMessage(serde camelCase)反序列化并交给 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 更新 HubStateconnectedclientSummariessessionSummarieseventslastWorkspaceRoothubUptime);仅当 (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(返回 connectedclientSummariessessionSummarieseventslastWorkspaceRoothubUptimelastErrornotificationCount 与最近 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.externalBinbin/menubar-sidecarbeforeDevCommand/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 at http://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() 按以下优先级返回 ConnectorCliLaunchSpeclauncher + connectArgsPrefix + cwd):

  1. 环境变量显式指定:若设置了 CLINE_CLI_PATH,直接使用其作为 launcher,参数前缀 ["connect"]
  2. 开发构建:若仓库内存在 apps/cli/src/index.ts(兼容旧布局 sdk/apps/cli/src/index.ts),则用当前 Bun 可执行文件(非 Bun 运行时则退回 bun 命令)运行源码入口,参数前缀为 ["--conditions=development", <源码路径>, "connect"]
  3. 打包构建:回退到 PATH 中的 cline 命令,参数前缀 ["connect"]

Sidecar 启动时通过 configureMenubarConnectorCliLaunch(workspaceRoot) 把解析结果写入 @cline/shared 的全局配置,Hub 恢复 connector 时即用该规格重新连接 CLI。这也解释了 README 的表述:开发构建优先 apps/cli/src/index.ts,打包构建优先 CLINE_CLI_PATHPATH 中的 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.tssession-client.ts Sidecar 使用的 Hub UI/会话客户端
sdk/packages/core/src/hub/discovery/index.ts Hub 发现(ensureDetachedHubServertoHubStatusUrl
sdk/packages/core/src/hub/daemon/index.ts Sidecar 打包产物复用的 Hub daemon 入口

八、小结

Menubar 示例展示了 Cline Hub 生态中一个典型的“轻量消费者”实现:Sidecar 用 TypeScript 承担 Hub 协议与业务聚合,Tauri/Rust 层承担进程治理与原生 UI(托盘、系统通知、Webview),二者之间仅靠三种 JSON Lines 消息(readyhub_statenotification)和 stdin JSON 命令通信。这种分层使 UI 层可以脱离原生环境独立预览(dev:ui),也使 Sidecar 二进制能同时充当共享 Hub 的守护进程入口,是理解 Cline Hub 客户端接入方式的优质参考实现。

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