1. 项目概述:从“刘海”到“智能中枢”的蜕变

作为一名长期与代码和工具链打交道的开发者,我对效率的追求近乎偏执。MacBook Pro的“刘海”设计,自问世以来就争议不断。对我而言,它最初只是一个需要开发者去适配的“视觉障碍”,一个在沉浸式全屏时偶尔会遮挡菜单栏图标的烦恼源。然而,当我开始深度使用各类AI编程助手——比如GitHub Copilot、Cursor、Claude Code,甚至是本地部署的代码大模型时,一个想法逐渐成型:这块被硬件“浪费”的顶部区域,能否被软件重新定义,变成一个专属的、全局可及的AI编程控制中心?

这个项目的核心,就是 将MacBook的“刘海”及其两侧的菜单栏区域,改造为一个高度集成的、信息密集的AI编程智能中枢 。它不再仅仅显示时间、电量或通知图标,而是实时展示我所有AI编码代理的状态、快速触发常用指令、并聚合关键上下文信息。想象一下,无需切换应用或窗口,只需瞥一眼屏幕顶部,就能知道Copilot是否正在处理我的注释、Cursor的Agent是否在后台搜索文档、或者本地模型推理的GPU占用率如何。更进一步,我可以直接点击“刘海”区域的虚拟按钮,一键让AI重构当前函数、生成单元测试,或者将选中的代码片段发送给特定的AI进行分析。

这不仅仅是另一个菜单栏工具。它解决的是现代AI辅助编程工作流中的一个核心痛点: 上下文切换与信息过载 。当AI能力分散在多个独立应用、插件和命令行工具中时,开发者很容易迷失在频繁的窗口切换和注意力分散中。本项目旨在通过一个统一的、常驻的、低干扰的全局界面,将AI能力“编织”进我的原生开发环境,让我能更流畅地“指挥”而非“伺候”这些AI助手。它适合任何已经将AI编码工具融入日常工作,并渴望进一步提升人机协作效率和心流状态的开发者。

2. 核心思路与架构设计

2.1 设计哲学:非侵入式与高信息密度

我的首要设计原则是 非侵入式 。这个控制中心绝不能成为新的干扰源。它必须像原生的菜单栏一样安静、稳定,仅在需要时提供信息,并且其视觉风格要与macOS系统深度整合,避免突兀。因此,我放弃了开发一个独立悬浮窗或Dock栏应用的方案,而是选择深度集成到系统菜单栏。

第二个原则是 高信息密度与可操作性 。有限的空间(尤其是“刘海”两侧)必须被极致利用。这意味着不能简单罗列图标,而是要设计一套精炼的视觉语言和交互逻辑。例如,一个图标可能通过颜色(绿色表示就绪,黄色表示工作中,红色表示错误)、动画(旋转表示处理中)和叠加的徽标(数字表示待处理任务数)来传递多层信息。同时,支持点击、右键菜单、甚至拖拽等交互,以触发复杂操作。

2.2 技术选型:为什么是Swift + MenuBarExtra?

要实现深度系统集成,Swift和Apple原生框架是唯一的选择。我选择了以下技术栈:

  1. SwiftUI + AppKit融合 :使用SwiftUI构建现代、声明式的UI组件,同时通过AppKit来访问底层系统API,特别是精确控制菜单栏图标和状态。SwiftUI的响应式特性非常适合实时更新AI代理状态。
  2. MenuBarExtra API (macOS 13 Ventura及以上) :这是项目的基石。 MenuBarExtra 允许创建一个常驻菜单栏的应用,其图标和菜单可以完全自定义。相较于传统的 NSStatusItem ,它更现代,与SwiftUI集成更好,管理生命周期也更方便。
  3. 进程间通信(IPC) :控制中心本身不直接运行AI模型,而是作为“指挥官”。它需要与各种AI工具通信:
    • XPC Services :用于与自家开发的Helper工具或沙盒内应用进行安全、高效的通信。
    • AppleScript / JavaScript for Automation (JXA) :用于控制一些支持自动化脚本的GUI应用(如某些IDE)。
    • Unix Domain Sockets / HTTP Localhost API :用于与本地运行的AI服务器(如Ollama、LM Studio)或提供本地API的编辑器插件通信。
  4. 状态管理与数据流 :采用 Combine 框架和 @StateObject 来管理复杂的异步状态。例如,一个 AIAgentStatus 模型会封装某个代理的名称、状态、最近活动、资源占用等,并在数据变化时自动更新UI。

注意 :选择原生开发而非Electron等跨平台方案,虽然限制了平台,但换来了无与伦比的性能、电池友好性和系统一体化体验。菜单栏应用需要极低的内存占用和CPU消耗,原生开发是保障这一点的关键。

2.3 系统架构图(概念层)

整个系统可以看作一个“星型”架构:

  • 中心枢纽 (Control Center App) :一个 MenuBarExtra 应用,负责UI展示、用户交互和统一调度。
  • 适配器层 (Adapters) :一系列后台服务或脚本,每个负责与一个特定的AI代理或工具(如Copilot CLI、Cursor Agent、Ollama API)进行通信,将异构的接口统一成中心枢纽能理解的标准化状态和指令协议。
  • AI代理与工具 :外部的各类AI服务。

中心枢纽通过适配器层轮询或接收事件,更新状态,并将用户指令路由给对应的适配器去执行。

3. 核心功能模块实现详解

3.1 状态监控模块:让“刘海”区域会说话

这是控制中心的“眼睛”。我需要实时获取各个AI代理的状态。

以本地Ollama模型为例: 我编写了一个 OllamaAdapter ,它每隔5秒通过HTTP GET请求查询 http://localhost:11434/api/tags http://localhost:11434/api/ps

struct OllamaStatus: Codable {
    let models: [ModelInfo]
}
struct ModelInfo: Codable {
    let name: String
    let size: Int64
    //...
}

class OllamaAdapter: ObservableObject {
    @Published var loadedModels: [String] = []
    @Published var currentInference: String? = nil
    private var timer: Timer?

    func startMonitoring() {
        timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in
            self.fetchStatus()
        }
    }

    private func fetchStatus() {
        // 1. 获取已加载模型列表
        fetchModels()
        // 2. 获取当前运行任务
        fetchRunningTasks()
    }

    private func fetchModels() {
        guard let url = URL(string: "http://localhost:11434/api/tags") else { return }
        URLSession.shared.dataTask(with: url) { data, _, _ in
            if let data = data {
                // 解析并更新@Published属性,触发UI更新
            }
        }.resume()
    }
}

在UI上,我设计了一个 OllamaStatusView 。当有模型加载时,“刘海”右侧会显示一个小的脑图图标;当模型正在推理时,该图标会呈现呼吸灯效果的脉冲动画;右键点击图标,则会显示一个下拉菜单,列出所有已加载的模型及其占用内存,并可以执行“卸载模型”等操作。

对于GitHub Copilot: Copilot没有官方状态API,但可以通过监听其日志文件或检查其进程是否存在来推断状态。我创建了一个 CopilotAdapter ,使用 Process API检查 github-copilot 进程,并解析其日志文件(位于 ~/Library/Logs/GitHub Copilot/ )的最新行,通过关键词匹配来判断它是“空闲”、“正在生成代码”还是“遇到错误”。

3.2 快捷指令模块:一键触发复杂工作流

这是控制中心的“双手”。我将高频操作抽象为“指令”,并绑定到菜单项或可点击的按钮上。

实现一个“解释选中代码”指令:

  1. 获取选中文本 :使用 NSPasteboard 监听系统剪贴板变化是一个方法,但更优雅的是通过辅助功能API(需要用户授权)直接获取当前前端应用(如VS Code)中选中的文本。由于授权复杂,我初期版本采用了“快捷键模拟+剪贴板”的组合方案。
  2. 指令构造与路由 :用户点击菜单项后,控制中心会执行一个预定义的AppleScript,该脚本会触发IDE(如VS Code)的“复制”命令,然后从剪贴板读取文本。
  3. 调用AI :将获取的代码片段作为上下文,构造一个Prompt(如“请用中文解释以下代码的功能:”),然后通过对应适配器(比如调用Claude的API)发送请求。
  4. 展示结果 :收到AI回复后,控制中心会通过系统通知( UNUserNotificationCenter )显示一个摘要,同时将完整回复写入一个临时文件,并用 NSWorkspace.shared.open() 在默认编辑器中打开,供用户详细阅读。
@MainActor
func explainSelectedCode() async {
    // 1. 模拟Cmd+C复制选中文本
    simulateCopyKeystroke()
    // 等待剪贴板稳定
    try? await Task.sleep(nanoseconds: 100_000_000)

    // 2. 从剪贴板获取代码
    guard let code = NSPasteboard.general.string(forType: .string),
          !code.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
        showNotification(title: "无选中内容", body: "请先选择一段代码。")
        return
    }

    // 3. 通过Claude适配器发送请求
    let prompt = "请用简洁的中文解释以下代码的功能和关键点:\n```\n\(code)\n```"
    do {
        let explanation = try await claudeAdapter.sendPrompt(prompt)
        // 4. 显示通知并打开详细结果
        showNotification(title: "代码解释已生成", body: explanation.prefix(100) + "...")
        presentFullExplanationInTempFile(explanation)
    } catch {
        showNotification(title: "解释失败", body: error.localizedDescription)
    }
}

3.3 上下文聚合器模块:信息十字路口

这是控制中心的“大脑”。不同的AI代理可能需要共享上下文。例如,我在Cursor里和Agent讨论了一个模块的设计,接下来想让Copilot Chat基于这个讨论生成代码。传统方式需要手动复制粘贴。

我在控制中心里实现了一个“上下文暂存区”。在任何地方,我都可以通过全局快捷键(如 Cmd+Shift+C )将当前选中的文本、错误信息、甚至是终端命令输出,快速保存到控制中心的上下文中。

这个上下文暂存区在菜单栏下拉菜单中有一个专属区域,以时间线的方式展示最近保存的片段。每个片段都可以被:

  • 一键发送 到指定的AI代理进行后续处理。
  • 组合 :选择多个片段,合并后发送。
  • 标记 :为片段打上“需求”、“错误”、“API文档”等标签,方便过滤。

其本质是一个带图形界面的、结构化的剪贴板历史,但专门为AI协作流程优化。

4. 界面实现与交互细节

4.1 “刘海”区域的空间利用策略

“刘海”本身是硬件盲区,但两侧的菜单栏空间是宝贵的。我的布局策略是:

  • 左侧区域 :放置“全局状态”图标。一个常驻的、代表控制中心本身的图标(我设计了一个由“{ }”括号和AI神经元组合的简约图标)。点击它展开主下拉菜单。
  • 右侧区域 :放置“动态代理状态”图标。这里只显示当前 活跃 需要关注 的AI代理。例如:
    • 默认只显示一个聚合图标(如三个点),表示系统正常。
    • 当Copilot正在生成代码时,显示Copilot图标并附加一个旋转的进度指示圈。
    • 当本地模型GPU内存占用超过80%时,显示模型图标并变为橙色警告色。
    • 当有来自AI的未读重要通知(如代码建议已就绪)时,在相应图标上显示红色徽标。

通过状态聚合和优先级显示,确保右侧区域不会因图标过多而拥挤,只在必要时提供关键信息。

4.2 下拉菜单的设计与SwiftUI实现

主下拉菜单使用SwiftUI的 Menu Divider 等组件构建,结构清晰:

MenuBarExtra("AI Control", systemImage: "brain.head.profile") { // 这是菜单栏图标
    Menu("编码代理") {
        Button("Copilot: 生成文档") {
            executeCommand(.copilotGenerateDoc)
        }
        .disabled(!copilotAdapter.isAvailable) // 根据状态禁用按钮

        Menu("Ollama") {
            ForEach(ollamaAdapter.loadedModels, id: \.self) { model in
                Button(model) {
                    switchActiveModel(to: model)
                }
            }
            Divider()
            Button("加载模型...") { showModelLoadView() }
        }
    }

    Divider()

    Menu("上下文") {
        if let latestContext = contextManager.latest {
            Text(latestContext.preview).font(.caption).lineLimit(2)
            Divider()
        }
        Button("捕获当前选中内容") { captureSelection() }
        Button("查看上下文历史...") { showContextHistoryView() }
    }

    Divider()

    Button("设置...") { openSettingsWindow() }
    Button("退出") { NSApplication.shared.terminate(nil) }
}
.menuBarExtraStyle(.menu) // 关键:将其设置为菜单栏应用

4.3 设置与配置界面

一个独立的SwiftUI视图用于配置,通过 AppStorage UserDefaults 持久化设置:

  • 代理配置 :各AI代理的API端点、密钥(使用Keychain安全存储)、轮询间隔。
  • 快捷键绑定 :为常用指令分配全局快捷键,使用 Carbon MASShortcut 等库。
  • 显示偏好 :选择哪些代理状态显示在菜单栏、通知的详细程度等。
  • 上下文管理 :设置上下文暂存区的保留时间和最大数量。

5. 开发难点与解决方案实录

5.1 难点一:获取任意应用选中文本的可靠性

最初依赖剪贴板,但发现如果其他应用复制了内容,会覆盖掉代码选区。 解决方案 是采用“模拟快捷键+焦点判断”组合拳:

  1. 在执行“捕获选中内容”指令前,先记录当前活动应用。
  2. 模拟按下 Cmd+C
  3. 立即读取剪贴板,并与之前的内容对比。如果变化,且活动应用是预设的编辑器(如VS Code、Xcode),则认为是成功捕获了选中代码。
  4. 为了更可靠,可以为常用编辑器编写特定的AppleScript或JavaScript(JXA)脚本,通过其内部API获取选中文本,但这需要为每个编辑器单独适配。

5.2 难点二:多源异步状态的管理与UI同步

多个适配器同时异步更新状态,容易导致UI闪烁或数据竞争。 解决方案 是采用响应式架构:

  • 每个 Adapter 都是一个 ObservableObject ,其核心状态用 @Published 包装。
  • 在主应用的 ViewModel 中,将这些适配器作为 @StateObject 引入。
  • 使用 Combine Publishers.MergeMany 来合并多个适配器的状态更新流,并 debounce (防抖)一下,避免过于频繁的UI刷新。
  • 所有对UI的更新都必须通过 @MainActor 确保在主线程执行。
class ControlCenterViewModel: ObservableObject {
    @Published var overallStatus: OverallStatus = .idle
    private var cancellables = Set<AnyCancellable>()

    init(copilotAdapter: CopilotAdapter, ollamaAdapter: OllamaAdapter) {
        Publishers.Merge(
            copilotAdapter.$status.map { $0.toGlobalComponent() },
            ollamaAdapter.$status.map { $0.toGlobalComponent() }
        )
        .debounce(for: .milliseconds(200), scheduler: DispatchQueue.main)
        .scan(OverallStatus.idle) { current, newComponent in
            current.updating(with: newComponent)
        }
        .assign(to: &$overallStatus) // 自动更新UI
    }
}

5.3 难点三:菜单栏应用的资源占用与生命周期

菜单栏应用需常驻,但必须“隐形”地节省资源。 踩坑点 :初期适配器轮询间隔太短(1秒),导致CPU使用率间歇性小幅升高。 优化方案

  • 自适应轮询 :根据代理状态动态调整轮询频率。例如,当所有代理空闲时,轮询间隔延长至30秒;当检测到某个代理开始工作,则临时缩短其对应适配器的轮询间隔至2秒。
  • 事件驱动补充 :尽可能使用事件驱动代替轮询。例如,监听某些AI工具生成的日志文件变化(使用 DispatchSourceFileSystemObject ),只有当日志追加时才读取并解析,而不是定时读取整个文件。
  • 空闲时休眠 :当菜单未展开,且所有代理长时间空闲时,将部分适配器监控暂停,仅保留一个最低心跳。

6. 实际使用体验与效能提升

经过数周的开发和打磨,这个“刘海控制中心”已深度融入我的工作流。

典型场景一:并行代码审查与生成 我正在写一个数据处理管道,同时打开了Copilot、Cursor和本地Claude的聊天窗口。过去,我需要来回切换查看它们的建议。现在,我只需看一眼菜单栏:Copilot图标在闪烁(正在生成备选代码),Cursor图标显示“思考中”,Claude图标旁有个数字“2”(有两条未读回复)。我直接点击Claude图标,下拉菜单里直接显示了回复摘要,我选择其中一条关于错误处理的建议,点击“发送至当前编辑器”,代码片段就被插入到正确位置。整个过程,我的视线和焦点从未离开主编辑器窗口。

典型场景二:本地模型资源管理 我正在用本地运行的CodeLlama模型重构一段代码。控制中心的Ollama图标显示为橙色,并带有“高负载”提示。我右键点击,看到该模型占用GPU内存已达7.8GB。我直接从下拉菜单选择“卸载非活跃模型”,释放内存。当后续需要另一个模型时,我再从菜单中点击加载。无需打开终端或Ollama的Web界面。

效能提升量化

  • 窗口切换次数减少 :估计减少了超过60%的 Cmd+Tab 或点击Dock切换应用的行为。
  • 上下文丢失率降低 :由于“上下文暂存区”的存在,临时需要记住并传递的信息现在都有了着落。
  • 心智负担减轻 :无需再记忆各个AI工具的状态快捷键或去特定界面查看状态,全局状态一目了然。

7. 可扩展性与未来方向

目前的架构具有良好的可扩展性。要新增对一个AI工具的支持,基本上就是:

  1. 实现一个符合 AIAgentProtocol 的适配器类。
  2. 在配置界面添加其设置项。
  3. 在UI状态管理器中注册它。

一些未来的构想

  • 工作流自动化 :将一系列指令串联成“工作流”。例如,一键完成“捕获错误日志 -> 发送给AI分析 -> 根据建议应用修复 -> 运行测试”。
  • 语音指令集成 :通过系统级快捷键唤醒语音,直接说“让Copilot为这个函数写测试”,控制中心接收指令并执行。
  • 更智能的通知 :AI代理不仅可以通知任务完成,还能对结果进行初步判断。例如,Copilot生成的代码如果检测到与项目现有风格严重不符,可以发出“风格检查”提示。
  • 生态集成 :提供插件系统,让社区可以为其他AI工具(如Midjourney for design, ChatGPT for docs)编写适配器,将这个控制中心从“AI编程中枢”扩展为更广义的“AI工作中枢”。

这个项目始于一个“为何不能物尽其用”的简单念头,最终演变为一个深刻优化我日常开发体验的核心工具。它证明了,即使是被动接受的硬件设计,通过软件创造力和对工作流的深度思考,也能转化为独特的效率优势。最让我满意的不是某个具体功能,而是那种“一切尽在掌控,却又无需刻意掌控”的无感流畅体验。AI编程代理不再是分散的、需要我去主动管理的工具,而是成为了通过屏幕顶端这个优雅的“指挥所”即可轻松调遣的智能伙伴。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐