Skip to content

Latest commit

 

History

History
1424 lines (1212 loc) · 101 KB

File metadata and controls

1424 lines (1212 loc) · 101 KB

Opsnook 代码地图与功能对照说明书(CODE_TO_FEATURE_MAP)

目的:让熟悉产品功能但不熟悉代码的维护者,在看到界面上任何一个细节时, 能快速定位到应当修改哪个文件、哪个函数,改动会影响什么,以及如何验证。

基线:main 分支的开源首版。文中行号为撰写时的近似行号, 代码变动后行号会漂移,应优先用文件名加类型或函数名、再加唯一代码片段的方式搜索定位,行号仅作参考。

本文档与其他文档的分工:


目录

  1. 项目总览
  2. 功能总目录
  3. 全量 UI 对照表(最重要)
    • 3.1 收起态刘海条
    • 3.2 展开面板公共框架(表头 / 底栏 / 窗口动画)
    • 3.3 「会话」Tab
    • 3.4 「活动」Tab 与消息预览
    • 3.5 「用量」Tab
    • 3.6 「工作区」Tab(文件 / Git)
    • 3.7 设置页(面板内嵌 + ⌘, 系统窗口)
    • 3.8 会话时间轴独立窗口
  4. 页面级「从外观到代码」说明
  5. 代码模块说明
  6. 数据与状态流
  7. 手动修改速查指南
  8. 可维护性与风险清单
  9. 完整性自检

1. 项目总览

1.1 这是什么

Opsnook 是一个 macOS 刘海区常驻应用(Developer Tools 类):把本机所有 AI 编码 Agent (Claude Code、Codex、Antigravity、ZCode、WorkBuddy、TRAE SOLO、Qoder)的会话状态聚合到 屏幕顶部刘海处的一个黑色 pill 中,悬停展开为面板,可以:

  • 看每个 Agent 会话的 6 态状态(待批准 / 待输入 / 出错 / 运行中 / 完成 / 空闲);
  • 在面板内直接批准 / 拒绝 Claude Code 的权限请求(L3 审批直达);
  • 点击会话一键回跳到对应终端 / IDE / 桌面客户端;
  • 查看跨 Agent 活动时间线、token 用量与成本估算、本地 Git 工作区(暂存 / 提交 / 推送);
  • 把一个会话的全过程渲染成可缩放的泳道时间轴(独立窗口)。

用户是「同时跑多个 AI Agent 的开发者」。唯一交付渠道是 Mac App Store 沙盒版, 所有系统能力按沙盒降级,文件访问一律经用户授权的 security-scoped bookmark。

1.2 平台、启动与构建

  • 平台:macOS 14+,Swift 6(strict concurrency = complete),AppKit + SwiftUI 混合。
  • App 形态:LSUIElement = trueInfo.plist:33),即代理型 App, 不出现在 Dock,也没有常规主窗口;唯一常驻 UI 是刘海 NSPanel。

常用命令(在仓库根目录):

# 1) 单元测试(纯逻辑,最快的验证方式)
cd Packages/OpsnookKit && swift test

# 2) project.yml 变动后重新生成 .xcodeproj(.xcodeproj 不入库)
xcodegen generate

# 3) 构建 App Store 调试版。configuration 必须是 Debug-AppStore,传 Debug 会误报“找不到模块”
xcodebuild -project Opsnook.xcodeproj -scheme Opsnook-AppStore -configuration Debug-AppStore \
  -derivedDataPath /tmp/OpsnookBuild build CODE_SIGNING_ALLOWED=NO

# 4) 直接运行构建产物
/tmp/OpsnookBuild/Build/Products/Debug-AppStore/Opsnook.app/Contents/MacOS/Opsnook

# 5) 打包(本机自测 zip / dmg)
bash scripts/package-appstore.sh
bash scripts/make-dmg.sh

DEBUG 专用环境变量(都在源码里以 ProcessInfo.processInfo.environment 判断,仅 DEBUG 构建生效):

变量 作用 定义处
OPSNOOK_DEMO=1 注入 4 条演示会话(含审批卡) AppContainer.swift:90 seedDemoSessions()
OPSNOOK_DEMO_DECAY=1 追加演示 running→done 衰减 AppContainer.swift:449
OPSNOOK_DEBUG_EXPAND=1 启动即展开面板 NotchWindowController.swift:518 show()
OPSNOOK_DEBUG_TAB=activity|usage|workspace|settings 启动落在指定 Tab NotchWindowController.swift:234
OPSNOOK_DEBUG_LAYOUT=1 把窗口 frame 日志写到 /tmp/hive_debug.log NotchWindowController.swift:661
OPSNOOK_DEBUG_AUTOTOGGLE=1 启动后自动反复展开/收起 4 次 AppDelegate.swift:136
OPSNOOK_DEBUG_SLOWANIM=<倍数> 放慢动画倍数 NotchWindowController.swift:161
OPSNOOK_DEBUG_NOTIFICATIONS=1 打印通知 cue 日志 NotchWindowController.swift:462
OPSNOOK_CLAUDE_HOME / OPSNOOK_CODEX_HOME / OPSNOOK_GEMINI_HOME / OPSNOOK_ZCODE_HOME / OPSNOOK_WORKBUDDY_HOME / OPSNOOK_TRAE_HOME / OPSNOOK_TRAE_RUNTIME_HOME / OPSNOOK_QODER_HOME 覆盖各 Agent 数据目录(绕过沙盒授权,测试用) AppContainer.swift agentHome(_:env:)
OPSNOOK_CLAUDE_HOOK_ROOT 覆盖 Claude hook 状态目录 AppContainer.swift:114
OPSNOOK_ANTIGRAVITY_EXECUTABLE 指定 antigravity 可执行文件 AppContainer.swift:150

1.3 技术栈与用途

技术 用在哪
SwiftUI 全部界面内容(面板、收起态、时间轴窗口、设置)
AppKit(NSPanel/NSWindow/CAShapeLayer) 刘海窗口壳、窗口动画、喇叭口遮罩、时间轴窗口、NSOpenPanel/NSSavePanel
Swift Concurrency(actor/AsyncStream) SessionStore(唯一真相源 actor)、适配器轮询、Git 服务
Observation(@Observable) 所有 ViewModel;窗口控制器用 withObservationTracking 手动订阅重算窗口尺寸
Carbon HotKey API 全局快捷键 ⌥⌘H(沙盒可用、无需辅助功能授权)
Accessibility API(AXUIElement) 精确回跳到具体终端/IDE 窗口(用户在系统设置授权后启用)
SQLite(系统 libsqlite3) 读 Qoder state.vscdb、ZCode db.sqlite
SMAppService 登录时启动
XcodeGen(project.yml) 生成 Xcode 工程;Swift Package OpsnookKit 承载全部业务代码
GitHub Actions(.github/workflows/ci.yml) swift test + xcodegen + xcodebuild 构建

1.4 顶层目录职责(细到文件的版本见 PROJECT_FILE_GUIDE.md)

App/                    App target:只做生命周期、窗口、依赖装配、设置页正文。业务逻辑不进这里。
  OpsnookApp.swift        @main + 设置页正文(OpsnookSettingsContent,两处设置共用)
  AppDelegate.swift       启动装配:ViewModel、窗口控制器、热键、通知、时间轴窗口管理
  Composition/            AppContainer(依赖装配根)+ DirectoryAccessModel(沙盒授权 UI 模型)
  Windows/                NotchWindowController(刘海窗口+动画)、ScreenGeometry、GlobalHotKey、TimelineWindowController
  Config/ Entitlements/ Resources/   xcconfig、沙盒 entitlements、Info.plist、App 文案、图标
Packages/OpsnookKit/Sources/
  OpsnookCore/            纯模型+状态机+Store。不依赖 AppKit/SwiftUI,零 OS 副作用,全部可单测
  OpsnookAdapters/        7 家 Agent 的解析器/轮询器,把外部文件翻译成 Core 的 SessionSnapshot
  OpsnookIntegration/     OS 副作用:文件授权、回跳、通知、hooks、Git、登录项
  OpsnookUI/              全部 SwiftUI 视图 + ViewModel + 设计 token + 本地化资源
Packages/OpsnookKit/Tests/   按 target 分层的 Swift Testing 单测(约 300 项)
design/                 高保真设计稿(HTML,视觉契约);mockups.html 是主稿
docs/                   本文档所在;PRD、技术设计、沙盒说明、各 Agent 状态调研
scripts/                打包脚本;images/ 为文档截图
project.yml             XcodeGen 工程定义(改target/scheme/依赖在这里,不直接改 .xcodeproj)

依赖方向(不可逆):OpsnookCoreOpsnookAdapters / OpsnookIntegration / OpsnookUIApp。 OpsnookUI 不依赖 OpsnookAdapters,所有跨层数据都通过 App 层的闭包注入,例如 UsageModel(loader:)

1.5 启动到首个界面的完整调用链

用户双击 Opsnook.app
→ OpsnookApp(@main, App/OpsnookApp.swift:8)
   Scene 只有一个 Settings{}(⌘, 回退窗口),主 UI 不走 Scene
→ AppDelegate.applicationDidFinishLaunching(App/AppDelegate.swift:30)
   1. new AppContainer()(App/Composition/AppContainer.swift:53)
      - capabilities = .appStore;创建 SessionStore(actor)、AdapterRegistry、
         SecurityScopedBookmarkStore、AgentDirectoryAccess、GitWorkspaceService
   2. 创建各 ViewModel:OpsnookViewModel / ActivityModel(各自订阅 store)、
      UsageModel、WorkspaceModel(一大串 Git 操作闭包注入,AppDelegate.swift:47-97)
   3. discovery.beginScanning()
   4. new NotchWindowController(...)(AppDelegate.swift:100)
      - 创建 NotchPanel(无边框、statusBar 层级、canJoinAllSpaces)
      - contentView = PanelContainerView(喇叭口 CAShapeLayer 遮罩)
      - 内嵌 NSHostingView<NotchRootView>
      - startMouseTracking():全局+本地 mouseMoved 监视器驱动 hover
   5. controller.show() → recompute() → updateFrame() → panel.orderFrontRegardless()
      - 首个可见界面 = 收起态刘海条(NotchCollapsedView)
   6. Task: notifications.activate(store) → container.start()
      - start() 里 registerAdaptersIfNeeded():逐个 Agent 检查目录授权,已授权才注册适配器
      - registry.run(into: store):每个适配器 start() 返回 AsyncStream<AdapterEmission>,逐条 ingest
      - store.runDecayLoop():每秒跑一次衰减(done→idle 等)
   7. GlobalHotKey(⌥⌘H) → controller.toggleManual()
→ 适配器解析到会话 → store.ingest → publications() 推流
→ OpsnookViewModel.sessions/aggregate 更新(@Observable)
→ NotchWindowController.observeModelChanges() 触发 updateFrame()(窗口宽度随聚合内容自适应)
→ NotchCollapsedView 显示左翼图标 + 右翼状态

1.6 配置、开关、密钥都在哪

类别 位置 说明
构建配置 App/Config/Base.xcconfig bundle id com.opsnook.app、版本号 MARKETING_VERSION、Swift 6、hardened runtime
App/Config/AppStore.xcconfig App Store 渠道叠加项(entitlements 路径等)
沙盒 entitlements App/Entitlements/Opsnook.AppStore.entitlements app-sandbox、user-selected read-write、书签、network.client(仅 git push 出站)
签名 Team 不入库。可选文件 App/Config/Local.xcconfig 中的 DEVELOPMENT_TEAM,由 Base.xcconfig 末尾的 #include? 引入 也可在 xcodebuild 命令行传入,或在 Xcode 中选择 Team。仅编译验证时加 CODE_SIGNING_ALLOWED=NO 即可
用户偏好(UserDefaults) Opsnook.appLanguage(语言)、Opsnook.focusModeEnabled(专注模式)、OpsnookBookmark.<key>(安全书签,前缀见 SecurityScopedBookmarkStore)、NSWindow Frame OpsnookTimelineWindow(时间轴窗口位置) 全部本地,无云端
功能开关 没有远端开关;能力矩阵 = Capabilities.appStore(OpsnookCore/Capability/Capabilities.swift:40,写死一份) 业务层只读 capabilities.xxx
API 地址 / 密钥 没有。0 网络上传(隐私红线);唯一联网是用户显式点「推送」时 git 子进程出站
状态引擎参数 EngineConfig(OpsnookCore/State/EngineConfig.swift):doneDecay=8s、staleTimeout=nil
事件保留 ActivityModel init 参数:retention=7 天、maxCount=300

1.7 什么可以放心手改、什么不要直接改

可以放心手改(改完跑 swift test + 肉眼验证即可):

  • 所有设计 token:OpsnookUI/DesignSystem/OpsnookColor.swiftOpsnookMetrics.swiftTimeline/TimelineStyle.swift
  • 所有文案:OpsnookUI/Resources/{en,zh-Hans}.lproj/Localizable.strings(面板/时间轴)与 App/Resources/Localizable.xcstrings(设置页)。注意两边语言要同步修改,LocalizationTests 会校验 key 覆盖情况。
  • 各视图文件里的字号/间距/图标名(.font(.system(size:))Image(systemName:))。

不要直接改:

  • Opsnook.xcodeproj/:XcodeGen 生成物,不入库。要改工程结构应改 project.yml,再执行 xcodegen generate
  • dist/:打包产物。
  • Packages/OpsnookKit/Sources/OpsnookUI/Resources/Localizable.xcstrings:被 Package 构建排除, 真正生效的是同目录 *.lproj/Localizable.strings 两个文件。
  • OpsnookCore 中引入 AppKit 或 SwiftUI:违反分层约束,CI 与协作规范都禁止。
  • 适配器解析逻辑:在阅读 docs/sessions_status_logic/ 中的对应调研文档之前不要修改。 每家 Agent 的状态判定都有对应记录,涉及心跳、进程闸门、加密数据库等情形。

2. 功能总目录

每个功能给出:入口 → 操作 → 结果 → 调用链 → 最可能要改的文件 → 验证方法。 UI 细节(每个按钮/状态)在 §3 逐一展开,这里是功能级索引。

F-A 收起态刘海条(常驻)

  • 在哪看到:屏幕顶部菜单栏中央,黑色 pill,与硬件刘海融合。App 启动即显示。
  • 操作:无需操作;鼠标移入 → 展开面板;⌥⌘H 也可展开。
  • 可见结果:左翼 = 活跃 Agent 微缩图标(≤4,多余 "+N");右翼 = 全局最高优先级状态 (空闲 3 暗点 / 绿波形+数 / 琥珀手势徽标脉冲 / 蓝「N 待输入」徽标 / 红点「N 出错」/ 绿「完成」闪烁)。
  • 调用链:适配器 → SessionStore.ingestAggregateBuilder.build(状态权重排序)→ OpsnookViewModel.aggregateNotchRootView(collapsed 分支)→ NotchCollapsedView。 窗口宽度:OpsnookMetrics.collapsedContentWidthNotchLayout.collapsedFrameNotchWindowController.updateFrame
  • 最可能改:样式在 NotchCollapsedView.swift;宽度/高度在 OpsnookMetrics.swiftcollapsedHeightReductioncollapsedRightWingWidth 等)+ NotchWindowController.updateFrame; 聚合规则在 AggregateBuilder.swift;排序权重在 AgentStatus.swift(rawValue 即权重)。
  • 验证:OPSNOOK_DEMO=1 跑起来看四种状态;单测 NotchLayoutTests / StateMachineTests / PanelLayoutTests

F-B 面板展开 / 收起(hover、⌥⌘H、固定)

  • 入口:鼠标悬停刘海条;全局 ⌥⌘H;表头图钉可固定。
  • 结果:单段曲线动画展开为 420pt(工作区 440pt)面板;移出收起(自底向上卷起); 面板作为 key 窗口(如正在输入提交信息)时不收起。
  • 调用链:mouseMoved 全局监视器NotchWindowController.updateHoverFromCursorrecompute()state.expanded/contentExpandedupdateFrame(animated:)setPanelFrame(NSAnimationContext) → PanelContainerView.layout() 每帧重算喇叭口遮罩与底圆角。
  • 最可能改:动画时长/曲线 = OpsnookMetrics.expandResponse/collapseResponse/expandCurve/collapseCurve; 圆角 = panelCornerRadius/collapsedCornerRadius/bottomCornerRadius(forVisibleHeight:); 喇叭口 = topFlareRadius + PanelContainerView.updateShape(); hover 判定/宽限 = NotchWindowController.updateHoverFromCursor/suppressCollapseBriefly
  • 验证:OPSNOOK_DEBUG_AUTOTOGGLE=1 + OPSNOOK_DEBUG_SLOWANIM=3 看动画;检查换页后指针在面板外不误收。

F-C 多 Agent 状态引擎(6 态)

  • 入口:无 UI 入口,自动运行。7 个适配器轮询本地文件/SQLite。
  • 调用链:AppContainer.registerAdaptersIfNeeded()(按目录授权注册)→ 各 *Adapter.start()AsyncStream<AdapterEmission>AdapterRegistry.run(into:)SessionStore.ingestSessionReducer.apply(纯函数:状态跃迁 + 派生 AgentEvent)→ publications()/events() 推流。
  • 最可能改:某家状态不对 → OpsnookAdapters/<Agent>/ 的 Adapter + SessionParser (先读 docs/sessions_status_logic/<agent>.md);跃迁/事件逻辑 → SessionReducer.swift; 衰减 → EngineConfig.swift
  • 验证:swift test(各 *AdapterTestsStateMachineTestsSessionStoreTests)。

F-D Claude Code L3 审批直达

  • 入口:Claude Code 请求权限时,「会话」页对应行下方出现审批卡(琥珀)。
  • 操作:点「允许」/「拒绝」。
  • 结果:决策经 decision 文件写回,阻塞中的 Claude hook 进程读到后放行/拒绝;活动页记「已允许/已拒绝」。
  • 调用链:Claude hook(ClaudeHooksInstaller 安装的脚本)写 request 文件到 ~/Library/Application Support/Opsnook/ClaudeHooks/ClaudeCodeAdapter 轮询到 → ClaudeHookRequestParser 解析 → snapshot.pendingApproval → PanelView.ApprovalCard → 点按钮 → onApprovalDecisionAppDelegate 闭包 → AppContainer.respond(to:on:decision:)claudeAdapter.respond(写 decision 文件)+ store.recordUserAction
  • 最可能改:卡片样式 = PanelView.swift 底部 ApprovalCard(约 981 行起); hook 安装/脚本 = OpsnookIntegration/Hooks/ClaudeHooksInstaller.swift; 解析 = ClaudeCode/ClaudeHookRequestParser.swift
  • 验证:ClaudeCodeAdapterTests / ClaudeHooksInstallerTests;真机让 Claude 跑一条需审批命令。
  • 注意:两类已知问题都会让审批显示为「运行中」。一是沙盒构建的 hook 路径与 ~/.claude/settings.json 中登记的路径不匹配;二是 Claude Desktop 本身不触发 PermissionRequest hook,即 entrypointclaude-desktop 的会话没有审批通道。两者症状相同、成因不同。

F-E 一键回跳(点击会话行 / 活动条目 / 工作区「返回 Agent」)

  • 调用链:PanelRow 点击 → onSelectNotchWindowController.select(session)session.focusTarget(适配器生成)→ TerminalFocuser.focus(target): deepLink(codex://threads/…)→ AppleScript 定位标签页(受 canScriptTerminals 门控,App Store 版关闭) → ApplicationFocuser App 级激活兜底。成功且未固定 → 面板收起。 不使用辅助功能 API:App Review Guideline 2.4.5 禁止将其用于窗口管理等非无障碍用途。
  • 最可能改:目标生成 = 各适配器;匹配与降级 = OpsnookIntegration/Focus/TerminalFocuser.swiftsupportedBundleIDs)。
  • 验证:TerminalFocuserTests / ApplicationFocuserTests;真机逐个宿主点行确认。

F-F 活动时间线 + 消息预览

  • 入口:底栏「活动」Tab。单击条目 → 面板内 transcript 预览并定位到那条消息;右键 → 「回跳到会话」。
  • 调用链:SessionReducer 派生事件 → store.events()ActivityModel.record(倒序、7 天、300 条) → ActivityView。预览:PanelView.presentPreviewTranscriptPeekModel.presentAppContainer.loadTranscript(按品牌分发适配器)→ TranscriptPeekView(滚动定位 anchor)。
  • 最可能改:行样式/事件文案 = ActivityView.swift + Formatting.swift(eventLabel/eventSymbol); 保留策略 = ActivityModel;预览气泡 = TranscriptPeekView.swift
  • 验证:ActivityModelTests / SessionTranscriptTests;真机点条目看定位高亮。

F-G 用量 Tab

  • 调用链:切到 Tab → UsageView.onAppear → UsageModel.refresh()AppContainer.usageReport()UsageReader(扫 /.claude、/.codex、~/.gemini 近期文件) → UsageAggregatorUsageSnapshot → UsageView 渲染(配额条 / 成本卡 / 7 日柱状 / 按 Agent)。
  • 最可能改:视图 = UsageView.swift;聚合 = UsageAggregator.swift;扫描 = UsageReader.swift; 单价表 = OpsnookCore/Models/ModelPricing.swift;格式化 = Formatting.swift(compactTokens/money)
  • 验证:UsageTests / UsageParserTests;真机切 Tab 看是否与 ~/.codex rate_limits 一致。

F-H 工作区 Tab(文件树 + Git 面板)

  • 调用链:切 Tab → NotchRootView.onSelectTabworkspace.refresh(sessions:)AppContainer.workspaceSnapshotGitWorkspaceService.snapshot 跑 git 命令 + transcript 改动归因) → WorkspaceModel.snapshotWorkspaceView。操作(暂存/提交/推送/回滚/移动文件)都是 WorkspaceModel 的方法 → App 层闭包 → GitWorkspaceService 对应 git 子命令 → 成功后 refresh。
  • 最可能改:UI = WorkspaceView.swift(1486 行,本仓库最大视图);状态/乐观更新 = WorkspaceModel.swift; git 命令 = GitWorkspaceService.swift;展示模型 = WorkspaceSnapshot.swift
  • 验证:WorkspaceModelTests / GitWorkspaceServiceTests;真机在测试仓库操作。
  • 注意:两处已修复的问题不要回退。一是 git 输出超过 64KB 时的管道死锁,修法是先读满输出再 wait; 二是沙盒下必须解析真实 git 路径,不能使用 /usr/bin/git 这个 shim,详见 GitWorkspaceService.resolveGitExecutable()docs/SANDBOX.md 第 5 节。

F-I 面板内嵌设置 + ⌘, 系统设置

  • 入口:底栏齿轮 → 面板切到设置页;或 ⌘,(Settings Scene 回退窗口)。两处正文同源 (OpsnookSettingsContent,App/OpsnookApp.swift:23)。
  • 内容:Agent 数据访问授权列表(13 行:7 家已接入可授权/撤销 + 6 家「敬请期待」占位)、 通用(登录启动 / ⌥⌘H 提示 / 语言 / 退出)。
  • 调用链(授权):点「授权」→ grantRowDirectoryAccessModel.grant(NSOpenPanel 预定位隐藏目录) → AppContainer.grantDirectoryAccess(存书签)→ 校验数据标记 → onReloadAppDelegate.reloadAfterGrantcontainer.reload()(重建适配器管线)。
  • 最可能改:一切设置页 UI 与文案 = App/OpsnookApp.swift + App/Resources/Localizable.xcstrings; 授权目录定义 = AgentDirectoryAccess.swiftAgentDirectoryKey
  • 验证:AgentDirectoryAccessTests;真机撤销→重新授权看会话消失/出现。

F-J 通知(视觉 cue)与专注模式

  • 说明:不使用系统通知,也不发声。完成时收起态的完成指示做一次 1.4s 放大发光; 审批/待输入徽标脉冲。专注模式(无独立按钮时由代码 onToggleFocusMode 驱动,当前 UI 入口 为收起态徽标是否脉冲的静默开关)静默这些视觉提醒,状态色仍显示。
  • 代码:OpsnookIntegration/Notifications/NotificationCoordinator.swift(cue 生成、UserDefaults 持久化); 展示 = NotchCollapsedView.CompletionIndicator + ConditionalPulse
  • 验证:NotificationCoordinatorTestsOPSNOOK_DEMO_DECAY=1 看完成闪烁。

F-K 全局快捷键 / 登录项 / 固定

  • ⌥⌘H:App/Windows/GlobalHotKey.swift(Carbon,键码 kVK_ANSI_H + optionKey|cmdKey, init 参数即改键处)→ toggleManual()
  • 登录项:设置页开关 → LaunchAtLoginController.setEnabledSMAppService.mainApp.register/unregister
  • 固定:表头图钉 → NotchUIState.pinnedrecompute()(pinned 时移出不收起)。

F-L 中英双语

  • 面板/时间轴文案:OpsnookL10n.string(key, locale:)OpsnookUI/Resources/{en,zh-Hans}.lproj/Localizable.strings
  • 设置页文案:settingsLocalized(key, language) 查 App target Localizable.xcstrings
  • 语言切换:设置页 Picker → AppLanguageController.select(UserDefaults Opsnook.appLanguage) → .environment(\.locale, …) 即时生效(NotchRootView:139、时间轴 SessionTimelineView:45)。
  • Agent 产出内容(任务标题等)一律原样不翻译。

F-M 会话时间轴查看器(独立窗口)

  • 入口:「会话」页会话行 hover 时行尾浮现「时间轴」胶囊按钮(仅 Claude/Codex/Qoder/WorkBuddy/Antigravity 且会话文件为 .jsonl 时出现,判定 = SessionTimelineSupport.canOpen)。
  • 结果:1280×820 深色 NSWindow(每会话一扇,重复打开前置复用),泳道时间轴: 15 类事件卡、同类堆叠、两级收起、subagent 分叉泳道、搜索淡化、类型筛选、缩放平移、 缩略图导航、事件详情(关键字段+完整 JSON+上下文)、PNG 导出。
  • 调用链:hover pill → onOpenTimelineAppDelegate.timelineWindows.open(session)TimelineWindowManager)→ SessionTimelineModel.load()AppContainer.loadTimeline (按品牌分发 5 个 *TimelineParser,后台线程)→ SessionTimelineView(工具栏+侧栏+画布+详情)。
  • 最可能改:布局算法(堆叠/收起/步距)= SessionTimelineModel.layout(约 305 行起); 卡片/连线/缩略图 = TimelineCanvasView.swift;配色/图标 = TimelineStyle.swift; 事件分类 = 各 *TimelineParser + ShellCommandClassifier.swift;窗口 = TimelineWindowController.swift
  • 验证:SessionTimelineModelTests、各 *TimelineParserTests;真机对拍真实会话。

F-N Agent 发现 / 接入向导(空态)

  • 入口:「会话」页无任何会话时显示「连接你的 Agent」空态 + 发现列表 + 重扫按钮。
  • 调用链:AppDelegate 启动/重扫 → container.discoverAgents()registry.detectAll() (并发 detect)→ AgentDiscoveryModelPanelView.emptyState/discoveryRow。 Claude 特判:hooks 未装时 result 变为 needsSetup + installHooks/copyHookConfiguration 动作 (AppContainer.discoverAgents:358)。
  • 验证:撤销全部授权后看空态;CodexAdapterTests 里有 registry 检测测试。

3. 全量 UI 对照表

通用约定(适用于本节全部条目,条目内不再重复):

  • 深色恒定:整个刘海面板强制 .environment(\.colorScheme, .dark)(NotchRootView.swift:138), 不跟随系统浅色主题;时间轴窗口同样强制 darkAqua(TimelineWindowController.swift:40)。
  • 颜色来源:除特别注明外全部取自 OpsnookColor(OpsnookUI/DesignSystem/OpsnookColor.swift): bgPanel #0A0A0B、bgElevated #161618、bgHover 白 5%、borderHair 白 7%、 textPrimary 白 92% / textSecondary 白 55% / textTertiary 白 35%、 状态色 approval #FFB340(琥珀)、input #64D2FF(天蓝)、errorRed #FF5F57、running #30D158(绿)、 idle #5A5A5E、onApproval #412402(琥珀底上的深棕前景)。
  • 文案来源:OpsnookL10n.string("<key>")OpsnookUI/Resources/zh-Hans.lproj/Localizable.strings (英文在 en.lproj,改文案两边都要改)。条目里直接给 key。
  • 图标:全部 SF Symbols(Image(systemName:)),品牌图标例外(Lobe PNG,见 3.0)。
  • 动画降级:波形、脉冲、完成闪烁、窗口动画都尊重系统「减弱动态效果」 (accessibilityReduceMotion / accessibilityDisplayShouldReduceMotion)。
  • 验证方式若未单独写,默认为:改完 → swift test(相关 UI 纯函数测试)→ xcodebuild … Debug-AppStore 构建运行 → 用 OPSNOOK_DEMO=1/OPSNOOK_DEBUG_TAB=… 直达该页肉眼核对。

3.0 复用原子组件(先记住这四个,后面条目大量引用)

UI:Agent 头像(圆形品牌图标,可带右下状态点)

  • 对应组件:AgentIconView(OpsnookUI/Notch/AgentIconView.swift,全文 35 行)
  • 结构:圆形底色 brand.avatarFill(Claude #211611/Codex #151618/Gemini #101827/其余 #17181A, 定义在 AgentBrandIcon.swift:104 avatarFill)+ 1px 品牌色描边(avatarStroke)+ 内嵌 AgentBrandIcon (尺寸 = 头像 58%)。传入 status 时右下角叠 32% 尺寸状态点(OpsnookColor.status(status) 填色、 面板底色描边)。
  • 品牌图标 AgentBrandIcon(OpsnookUI/Notch/AgentBrandIcon.swift):按 brand.id 查 lobeResourceName(claude→lobe-claude、codex/openai→lobe-openai、gemini/antigravity→lobe-gemini、 zcode→lobe-zai、workbuddy→lobe-codebuddy、trae→lobe-trae、qoder→lobe-qoder), 从 Resources/BrandIcons/*.png 加载;未知品牌降级 SF Symbol(fallbackSymbolName)。
  • 修改指南:换某品牌图标 → 替换 BrandIcons 下 PNG(保留同名);新品牌 → 在 lobeResourceName(for:) 加 case + 放 PNG;改头像底色/描边 → avatarFill/avatarStroke
  • 验证:PanelLayoutTests 有图标资源存在性测试(hasBundledLobeResource)。

UI:Opsnook 品牌标(表头六边形 logo)

  • 对应组件:OpsnookBrandMark(OpsnookUI/Brand/OpsnookBrandMark.swift);图源 Resources/BrandIcons/opsnook-brand.png,缺资源时降级 SF Symbol "hexagon" 琥珀色。
  • 使用处:面板表头(PanelView.header)、工作区表头、⌘, 设置窗口标题行。

UI:运行波形(4 根绿色跳动柱)

  • 对应组件:Waveform(OpsnookUI/DesignSystem/Waveform.swift)。CALayer 实现: 4 柱、0.8s 循环 keyframe(scale.y + opacity),柱宽 ≈ height*0.18(上限 2.4)、间距 2.1、 带同色 shadow 发光。减弱动效时静止在固定高度组合 [0.52,0.82,0.64,0.90]
  • 使用处:收起态右翼(h=13)、会话行 running 状态(h=10)、subagent 行(h=9)。
  • 修改指南:柱数/高度 = 调用处参数;节奏/形态 = updateAnimations()patterns/duration

UI:脉冲呼吸(徽标 1.6s 忽明忽暗)

  • 对应组件:PulseModifier / .opsnookPulse()(Waveform.swift:190)。 TimelineView 0.15s 步进,opacity = 0.7+0.3·sin。专注模式开启时调用方直接不加此 modifier (NotchCollapsedView 的 ConditionalPulse(active: !notificationsMuted))。

3.1 收起态刘海条

文件:OpsnookUI/Notch/NotchCollapsedView.swift(整个收起态只有这一个视图文件)+ 窗口几何 App/Windows/NotchWindowController.swift updateFrame() + OpsnookCore/Layout/NotchLayout.swift + 宽度预算 OpsnookUI/DesignSystem/OpsnookMetrics.swift

UI:黑色刘海 pill 本体

  • 所在位置:主屏顶部居中,覆盖菜单栏中段,与硬件刘海融合。
  • 显示条件:App 运行期间恒显示(panel.orderFrontRegardless(),加入所有 Spaces、全屏辅助层)。
  • 尺寸:高度 = 菜单栏高度 − OpsnookMetrics.collapsedHeightReduction(当前 0, OpsnookMetrics.swift:37,这是调整收起态高度的唯一入口,消费处 NotchWindowController.swift:611 collapsedHeight = max(1, layout.menuBarHeight - …))。 宽度 = 刘海宽 + 左右翼实际内容 + 2×12 内边距,上限 = 刘海宽 + 2×90 (collapsedContentWidth,OpsnookMetrics.swift:93;右翼各状态宽度预算表在 collapsedRightWingWidth(for:):74,needsApproval 28 / waitingInput 90 / error 68 / done 84 / running 44 / idle 22)。
  • 形状:顶边满宽贴屏,两侧「喇叭口」外扩弧(半径 topFlareRadius=8)收到主体侧壁, 底圆角随可见高度 10↔26 插值,由 PanelContainerView.updateShape()(NotchWindowController.swift:73) 的 CAShapeLayer 遮罩绘制,另描 1px 白 8% 玻璃边(rimLayer)。
  • 背景:视图层 Color.black(NotchRootView.swift:134);容器兜底色 #0A0A0B (PanelContainerView init:44)。
  • 交互:鼠标移入整个 pill → 展开面板(全局 mouseMoved 监视器按 panel.frame.contains(NSEvent.mouseLocation) 判定,NotchWindowController.swift:314)。无点击/右键行为。
  • 多屏/无刘海屏:取 NSScreen.main(ScreenGeometry.primaryScreen);无刘海屏 notchGap=0, pill 仍显示(中间不留空隙)。
  • 修改指南:高度 → collapsedHeightReduction;贴合宽度 → collapsedContentWidth 系列; 形状/圆角 → PanelContainerView.updateShape + OpsnookMetrics.bottomCornerRadius; 层级(会不会被菜单栏盖) → panel.level = .statusBar(NotchWindowController.swift:250)。
  • 验证:NotchLayoutTests(collapsedFrame 几何)、PanelLayoutTests(宽度预算); 真机看有/无刘海两种屏。

UI:左翼 · 活跃 Agent 微缩图标(≤4 个重叠小圆)

  • 位置:pill 左端,水平排列、相邻重叠 6pt(HStack(spacing: -6))。
  • 显示条件:存在状态为 needsApproval/waitingInput/error/running/idle 的会话 (AgentStatus.appearsInNotchLeftWing,done 不显示)。按 Agent 去重、 按(状态权重↑、最近变化↓)排序取前 4(AggregateBuilder.build)。
  • 每个图标:18pt 圆 = brand.avatarFill 底 + AgentBrandIcon 10.5pt + 1px 品牌描边 (NotchCollapsedView.miniIcon:51)。
  • 溢出:第 5 个起折叠为 +N 文本(9pt semibold,textSecondary,左 padding 8; NotchCollapsedView.swift:42)。
  • 交互:无独立交互(整条 pill hover 展开)。
  • 修改指南:最多几个 → AggregateBuilder.build(maxLeftWing:) 默认参数(AggregateBuilder.swift:5); 谁能出现 → AgentStatus.appearsInNotchLeftWing;图标大小/重叠 → miniIcon + 同步改 OpsnookMetrics.collapsedMiniIconSize/collapsedMiniIconOverlap(否则窗口宽度算不准会裁切)。

UI:右翼 · 全局状态指示(六种形态)

  • 位置:pill 右端。取 aggregate.highestPriority(全部会话状态最小 sortWeight)分支渲染 (NotchCollapsedView.rightWing:61):
    1. 空闲/无会话:3 个 4pt 白 22% 暗点(spacing 4)。
    2. 运行中:绿波形(h13)+ 运行数字(11pt medium 绿,仅 runningCount>0)。
    3. 完成:CompletionIndicator(checkmark.circle.fill + "完成"common.completed,10pt 绿; 常态 72% 透明度;收到新 completionCueID 且未静音未减动效时 0.18s 放大到 1.08 + 绿光晕, 650ms 后 0.28s 归位,NotchCollapsedView.swift:124-160)。
    4. 待批准:琥珀胶囊徽标,仅 hand.raised.fill 图标无文字(badge(text:nil)),前景 onApproval, 未开专注模式时脉冲。具体数量只在展开面板看。
    5. 待输入:天蓝胶囊徽标 questionmark + notch.waiting_input("%d 待输入"),前景 #06283A,脉冲。
    6. 出错:6pt 红点 + notch.errors("%d 出错")红字,不脉冲。
  • 徽标通用样式:图标 9pt bold + 文字 10pt medium,padding 横 7 竖 2,Capsule 底。
  • 修改指南:改某状态样式 → rightWing 对应 case;改优先级顺序 → AgentStatus rawValue; 改文案 → strings 的 notch.*/common.completed;改完成闪烁时长 → CompletionIndicator.task 内 的 0.18/650ms/0.28 三个数字,cue 持续时长在 NotificationCoordinator(cueDuration: 1.4s)
  • 关联影响:右翼形态变化 → 宽度预算 collapsedRightWingWidth 需同步,否则文字被裁。

3.2 展开面板公共框架

文件:OpsnookUI/Panel/PanelView.swift(表头/底栏/内容路由)+ NotchRootView.swift(展开分支)

  • 窗口尺寸动画 NotchWindowController.swift

UI:面板容器

  • 展开态结构(NotchRootView.swift:80):VStack{ 顶部刘海条(NotchCollapsedView, 高=menuBarHeight, 黑底,硬件刘海盖在这里) ; PanelView }
  • 宽度:selectedTab.panelWidth,工作区 440pt,其余 420pt(PanelView.swift:948 PanelTab.panelWidth; 数值在 OpsnookMetrics.panelWidth/workspacePanelWidth)。
  • 高度:按内容自适应(hostingView.fittingSize),上限 = 屏高−刘海条−96−16 (state.panelMaxContentHeight,NotchWindowController.swift:616);超限后各 Tab 内部滚动。
  • 背景 bgPanel;底部圆角 26(随收起动画插值到 10)。
  • 换页/内容变化:不做动画、即时落位(refitAfterLayout 下一 runloop 重测高, NotchWindowController.swift:340 有详细注释解释为什么);消息预览开/关例外,用展开同款曲线动画 (observePeekChanges:407)。
  • 修改风险:任何「视图内 @State 改变高度」的新 UI(如折叠/展开)都必须调 onContentResize() 通知窗口重测高,否则底栏被裁(先例:subagent 展开 toggleSubagents,PanelView.swift:558-563 注释)。

UI:表头(Opsnook 标 + 摘要 + 漏斗 + 图钉)

  • 显示条件:selectedTab != .workspace(工作区页有自己的表头)。消息预览打开时表头被 TranscriptPeekView 的头部替换。
  • 布局(PanelView.header:143):OpsnookBrandMark(16) + "Opsnook"(13pt semibold)+ 摘要 (12pt textSecondary)+ Spacer + [漏斗] + 图钉。padding 横 18 竖 13,底部 hairline 分隔。
  • 摘要文字(PanelView.summary/baseSummary:172):
    • 有会话:panel.agent_count("%d 个会话") ± panel.running_count("%d 运行") ± panel.attention_count("%d 待处理")," · " 连接;
    • 无会话但发现了 Agent:panel.detected_agents_summary("%d 个 Agent 已检测");
    • 活动页筛选中追加 activity.filtered("已筛选 %d")。

UI:漏斗按钮(活动页筛选)

  • 位置:表头右侧、图钉左边。图标 line.3.horizontal.decrease 13pt。
  • 显示条件:selectedTab == .activity 且非 transcript 预览态 且 activity.filterableAgents 非空 (PanelView.swift:151)。
  • 状态:筛选生效时琥珀色,否则 textSecondary。
  • 交互:点击 toggle showAgentFilter → 右上浮层(见 3.4);hover 有 .help("activity.filter")
  • 外部点击关闭:内容/底栏挂 simultaneousGesture(TapGesture) 关浮层(PanelView.swift:113, 注释解释了为何不用透明 overlay,非激活 NSPanel 的 hit-test 陷阱)。

UI:图钉按钮(固定面板)

  • 图标:pin.fill(已固定,琥珀) / pin(未固定,textSecondary),13pt(PanelView.swift:162)。
  • 交互:点击 → onTogglePinNotchUIState.pinned.toggle() → 固定后鼠标移出不收起。
  • 初始值:false,不持久化(重启即复位)。
  • 同款按钮在工作区表头右侧复制了一份(WorkspaceView.repositoryHeader:364)。

UI:底栏 Tab(会话 / 活动 / 用量 / 工作区 + 齿轮)

  • 位置:面板最底部,高约 42(padding 横 18 竖 10),顶部 hairline。
  • Tab 按钮(PanelView.tabButton:804):文字 11pt(选中 medium+白 14% 圆角 6 底,未选中 regular textSecondary)。文案 key:tab.agents("会话")/tab.activity/tab.usage/tab.workspace
  • 齿轮(settingsButton:864):gearshape 14pt,选中设置页时同款高亮;.help("common.settings")
  • 活动页筛选态的特殊布局(footer:822):左侧变为「筛选清除按钮」 (漏斗图标+activity.filter_summary("%d / %d 个 Agent · 清除"),琥珀,点击 clearAgentFilter), 四个 Tab+齿轮整体靠右。
  • 交互后变化:onSelectTabstate.panelTab(枚举 PanelTab)→ content 切页 + 关浮层/退设置详情/ 关消息预览(PanelView.onChange:126)+ 切到工作区时触发 workspace.refresh
  • 修改指南:加/删 Tab → PanelTab 枚举 + footer 两个分支 + panelWidth;改选中样式 → tabButton。

3.3 「会话」Tab(agents)

文件:列表框架 PanelView.swift(agentsContent/agentsList/groupHeader/emptyState/discoveryRow)、 单行 PanelRow.swift、子行 SubagentRow.swift、审批卡 PanelView.ApprovalCard、 问题卡 QuestionCard.swift。数据 = OpsnookViewModel.grouped()

UI:分组标题("需要处理 · N" 等四组)

  • 顺序恒定:需要处理 → 运行中 → 最近完成 → 空闲(PanelGroup 枚举顺序,AgentStatus.swift:58; 组归属 AgentStatus.panelGroup,注意 error 归「最近完成」、waitingInput 归「需要处理」)。
  • 样式(PanelView.groupHeader:565):SF 图标 12pt + "<组名> · <数量>" 11pt semibold; 「需要处理」琥珀色,其余 textTertiary。图标映射 groupSymbol:578 (hand.raised.fill/play.fill/checkmark/moon.zzz)。
  • 文案 key:group.needs_attention/running/recently_finished/idle
  • 空组不渲染(grouped() compactMap)。

UI:会话行(56pt 一行)

  • 文件:PanelRow.swift 全文。整行是 Button(onSelect = 回跳)。
  • 布局:AgentIconView(28, 带状态点) + 两行文字 + Spacer + [fork 徽标] + 右侧状态/计时区。 padding 横 18,高 56。
  • 标题行:session.taskTitle(无标题时回退状态文案)13pt medium textPrimary,单行尾截断; done/idle 时降为 76% 透明度(isFinished)。
  • 次行(OpsnookFormat.subtitle,Formatting.swift:182): Agent 名 · 项目名 · 分支 · git 状态(clean/dirty/"N 个文件" subtitle.changed_files), 11pt textSecondary 单行尾截断。
  • 右侧状态区(trailing:146):running = 波形(h10)+「运行中」;其余 = 状态 SF 图标 (OpsnookFormat.statusSymbol)+ 状态短语(status.* key),颜色 OpsnookColor.status(_)
  • 计时行(timeLabel:164):TimelineView 每秒刷新。running/needsApproval/waitingInput 显示 已进行时长(elapsed,"2m 04s"格式);done/error/idle 显示相对时间(relative,"5 分钟前")。
  • hover:整行无底色变化;但行尾原位浮现「时间轴」胶囊按钮(见下条),透明度 0.12s 过渡。
  • 修改指南:行高 → .frame(height: 56)(PanelRow.swift:68);次行内容 → OpsnookFormat.subtitle; 状态文案/图标 → Formatting.swift + strings status.*

UI:「时间轴」hover 胶囊按钮(会话行行尾)

  • 显示条件:hover 该行 且 SessionTimelineSupport.canOpen(session) (品牌 ∈ claude/codex/qoder/workbuddy/antigravity 且 sessionResourceURL 为 .jsonl, SessionTimelineModel.swift:10)。
  • 样式(PanelRow.timelinePill:81):chart.xyaxis.line 10pt + timeline.open_button("时间轴") 11pt medium,白 10% Capsule。以透明态常驻布局(ZStack 覆盖状态区,宽度取两者较大值), 悬停只切透明度,行内零位移(PanelRow.swift:55-65 注释)。
  • 交互:点击 → onOpenTimeline(session) → 独立时间轴窗口(§3.8)。
  • 提示:.help("timeline.open_menu")("查看会话时间轴")。

UI:fork 徽标(subagent 计数 ⑂ 2/3 ▾)

  • 位置:行尾状态区左侧。显示条件:session.subagents 非空。
  • 样式(PanelRow.forkPill:111):arrow.triangle.branch 9pt + 计数(有活跃时 绿色活跃数/总数, 全结束时仅灰总数)+ 展开方向 chevron(down=已展开/right=收起)7pt。白 8%(活跃)/5% Capsule。
  • 交互:点击 → PanelView.toggleSubagents(手动覆盖存 subagentExpandOverrides:[SessionID:Bool], 视图 @State,切页/重启丢失)→ 展开/收起子行 + onContentResize()
  • 默认展开规则(PanelView.subagentsExpanded:552):无手动覆盖时「有 running subagent 即展开, 全部结束自动收起」。
  • 无障碍:subagent.count_accessibility("%d 个子代理")。

UI:subagent 子行(40pt,树形肘线)

  • 文件:SubagentRow.swift 全文。展开时逐条渲染在父行下方。
  • 布局:28pt 宽树形连线列(竖线 x=14 + 圆角肘线汇入子头像;末行竖线不下延,isLast)+ 18pt 子头像(父品牌图标 11pt + 品牌色描边,结束态降透明)+ 子代理名(12pt medium)+ 任务摘要(11pt textSecondary 尾截断)+ 右侧状态。
  • 右侧状态(trailing:89):running = 波形(h9)+实时计时;done = 绿 checkmark+耗时; failed = 红 xmark+耗时(OpsnookFormat.duration)。
  • 交互:整行点击 = 聚焦父会话(onSelect 同父行)。
  • 数据:SubagentBrief(OpsnookCore/Models/SubagentBrief.swift),来源 Claude Task tool_use 配对 / Codex spawn_agent/close_agent 等,由各 SessionParser 解析。
  • 修改指南:肘线形状 → branch 的 Path(SubagentRow.swift:53);行高 40 → .frame(height: 40)

UI:审批卡(琥珀 L3 卡片)

  • 位置:对应会话行正下方。显示条件:session.pendingApproval != nil(仅 Claude L3 会产生)。
  • 文件:ApprovalCard(PanelView.swift:981-1015)+ 按钮样式 ApprovalButtonStyle
  • 布局:工具名(琥珀) · 命令(白) 等宽字 11pt → 可选 detail(textTertiary)→ 右对齐 「拒绝」(描边白 18%,文字 textSecondary)「允许」(琥珀实底、onApproval 文字,按下 75% 透明)。 bgElevated 圆角 10 卡片,左缩进 57(对齐行文字列)。
  • 文案 key:approval.allow("允许")/approval.deny("拒绝")。
  • 交互:点按钮 → onApprovalDecision(session, .allow/.deny) → AppContainer.respond → Claude hook decision 文件;卡片随下一轮快照消失。并发多卡互不影响(每卡带 approval.id)。
  • 验证:OPSNOOK_DEMO=1 有演示审批卡;真链路见 F-D。

UI:问题卡(AskUserQuestion 只读卡)

  • 位置:会话行下方(与审批卡同位)。显示条件:session.pendingQuestion != nil (Claude AskUserQuestion hook)。
  • 文件:QuestionCard.swift 全文。
  • 布局:每个 prompt = [可选 header 10pt 天蓝] + 问题正文 11.5pt medium + 选项列表 (3pt 圆点 + label 11pt + 可选 detail 10pt 灰,最多 2 行);底部右对齐 「去 {Agent} 回答」按钮(arrow.up.forward.app + question.answer_in,天蓝描边)。
  • 只读:不能在 Opsnook 里选答案(无回传通道),按钮 = 回跳(onJumpBack = onSelect 父会话)。
  • 修改指南:想让某选项可点 → 没有通道,不要做;改样式 → QuestionCard.swift。

UI:空态(连接你的 Agent)

  • 显示条件:model.grouped() 为空(没有任何会话)。
  • 文件:PanelView.emptyState:587 + discoveryRow:665。
  • 布局:标题 onboarding.title("连接你的 Agent" 15pt semibold) + 动态副文案 (扫描中 onboarding.scan_description / 无发现 onboarding.install_description / 已发现 onboarding.detected_description)+ 右上重扫按钮(arrow.clockwise,扫描中变 ProgressView 且禁用,.help("onboarding.rescan"))。
  • 发现列表:每行 = AgentIconView(30,未安装 42% 透明) + 名称 + 状态短语(statusText:717, 已连接(绿)/需要接入(琥珀)/未检测到(灰),key agent.connected(_version)/needs_hook/ needs_manual_config/needs_setup/not_detected)+ 右侧动作: 安装中 spinner / 「一键接入」agent.install 或「复制配置」agent.copy_config (琥珀 SetupButtonStyle,另一个安装进行中时禁用)/ 已装绿勾。 bgElevated 圆角 11 + hairline 描边卡片。
  • 反馈行:discovery.feedbackByAgentID 有值时显示(feedback.integration_completed/ configuration_copied/no_configuration_needed/failed,失败红色其余天蓝)。
  • 调用链:重扫 → onRescan → AppDelegate.rescanAgents → container.discoverAgents(); 一键接入 → onSetupAgent → AppDelegate.setupAgent → prepareIntegration(写 ~/.claude/settings.json hooks)或复制配置到剪贴板。
  • 注意:设置页里还有一套「Agent 接入」列表视图(agentIntegrationSection/agentConfigDetail, PanelView.swift:288-408)当前未挂载(settingsRoot 注释说明 2026-06-17 移除,代码保留待恢复)。

UI:列表滚动

  • 内容自然高 ≤ 上限:整列直接定高(无滚动);> 上限:包 ScrollView 固定为 maxContentHeight 内部滚动(PanelView.agentsList:535 的 measuredContentHeight 机制, PreferenceKey PanelContentHeightKey)。活动/用量/工作区同构(各自的 HeightKey)。

3.4 「活动」Tab 与消息预览

文件:ActivityView.swift(列表+行)、TranscriptPeekView.swift(预览)、 ActivityModel.swift(数据)、TranscriptPeekModel.swift(预览状态机)、 浮层在 PanelView.agentFilterPopover:891

UI:日期分组标题(今天 / 昨天 / 更早)

  • ActivityView.sectionHeader:70:11pt semibold textTertiary。分桶纯函数 OpsnookFormat.dayBucket(Formatting.swift:156),文案 activity.today/yesterday/earlier

UI:活动条目行

  • 文件:ActivityRow(ActivityView.swift:99-285)。整行 Button。
  • 布局:左侧 26pt 时间轴列(1.5pt 白 20% 竖线贯穿 + 24pt AgentIconView 盖在上面形成断点; subagent 事件改用 40pt subagentAxis,主轴线仍在 x≈12,分叉肘线引出 18pt fork 小节点, ActivityView.swift:159-193)+ 文字块 + 右侧相对时间(10pt textTertiary)。
  • 主行三种形态(primaryLine:198):
    1. subagent 事件:⑂ 图标 + 类型标签(event.subagent_started/completed/failed)+ 子代理名 · 任务摘要
    2. userTerminated:「你终止了会话」event.terminated_action + 「已终止」灰徽标 (白 8% 底圆角 6,terminatedBadge:242);
    3. 其余:类型 SF 图标 9pt + 类型标签 11pt semibold(OpsnookFormat.eventLabel/eventSymbol, 配色 eventColor:276:完成/已允许绿、出错/已拒绝红、请求审批琥珀、已开始/子代理启动天蓝、 已终止灰)+ 本轮用户消息(event.messageContent,回退 title)。
  • 次行(subtitle:251):常规 = Agent · 项目 · 分支;subagent = 属于「父标题(24 字截断)」 · 耗时event.subagent_belongs_to)。
  • 分隔线:只画在每轮最早事件(sessionStarted)底部,两轮之间恰一条线 (showsDivider,ActivityView.swift:27 注释)。
  • hover:整行 bgHover。
  • 交互:单击 → onPreview → 消息预览(下条);右键 → 上下文菜单一项「回跳到会话」 (activity.jump_back + arrow.up.forward.app 图标)→ selectEvent:活会话走 focus 链路, 已衰减会话退化为消息预览(PanelView.swift:444-467)。
  • 空态(ActivityView.emptyState:80):clock.arrow.circlepath 22pt + activity.empty + activity.empty_detail
  • 数据边界:事件仅内存保留(不持久化,重启清空),7 天 / 300 条上限(ActivityModel:27)。

UI:按 Agent 筛选浮层

  • 位置:面板右上(padding top 44 / trailing 14 的 ZStack overlay,PanelView.swift:118)。
  • 显示条件:漏斗点开 且 activity Tab。
  • 布局(agentFilterPopover:891):宽 214,#1B1C1E 圆角 12 + 白 12% 描边 + 大阴影。 首行 = activity.filter_by_agent("按 Agent 筛选") + 右侧「全选」琥珀按钮; 每行 = AgentIconView(20) + 名称 + 勾选框(checkmark.square.fill 琥珀 / square 灰, 未勾选整行 55% 透明)。
  • 交互:点行 toggle activity.excludedAgentIDs(排除集语义,空集=全部显示); 「全选」= clearAgentFilter() 清空排除集。点浮层外任意内容处关闭。
  • 关联影响:筛选改变列表高度 → 窗口高度由 observeActivityChanges 跟踪 excludedAgentIDs 重算 (NotchWindowController.swift:351 注释记录了漏跟踪导致顶部被裁的 bug)。

UI:消息预览(transcript 内嵌页)

  • 触发:单击活动条目 → TranscriptPeekModel.present。loading 期间界面不变 (不显示加载态,通常数 ms;PanelView.content 注释:221),.loaded 后整个内容区被替换。
  • 头部(TranscriptPeekView.header:25):左「‹ 返回」peek.back(→ model.dismiss)+ 中间会话标题(11.5pt textTertiary,中截断)+ 右「回跳」peek.jump_back 琥珀 (→ model.jumpBack = focus + dismiss)。
  • 消息列表:LazyVStack 气泡(MessageBubble:101):角色标签(我=天蓝 peek.role_user/ Agent 名/工具 peek.role_tool/系统 peek.role_system)+ 正文 12.5pt 可选中复制 (.textSelection(.enabled))。用户消息左缘 2.5pt 天蓝强调条;锚点消息(被点击的那条) 琥珀 10% 底 + 琥珀 60% 描边 + 「已定位」peek.located 小徽标,打开时瞬时滚动到中央 (scrollToAnchor:90)。
  • 高度:maxContentHeight - 46 内滚动;打开/关闭窗口高度带动画(observePeekChanges)。
  • 读不到 transcript:自动退化为直接回跳(TranscriptPeekModel.present:55)。

3.5 「用量」Tab

文件:UsageView.swift 全文 + UsageModel.swift(按需加载)+ 数据链 UsageReader.swiftUsageAggregator.swiftUsageSnapshot.swift

  • 加载态:spinner + usage.loading("正在统计用量…")。切到 Tab 才触发一次 model.refresh()(onAppear:28,inFlight 防重入;无自动定时刷新,重进 Tab 会再拉)。
  • 空态:chart.bar.xaxis 22pt + usage.empty + usage.empty_detail

UI:订阅配额区(usage.section.quota

  • 显示条件:snapshot.quotas 非空,只有解析到真实 rate_limits(目前 = Codex)才显示, 不放假数据(诚实数据原则,UsageView 头注释)。
  • 每行(quotaRow:112):AgentIconView(26) + Agent 名 + 套餐名 + 右侧百分比 + 次行 窗口标签 · 重置时间 [· 接近上限提示]quotaWindowLabel/quotaReset + ≥85% 时 usage.advice_high)+ 5pt 进度条(≥90% 红 / ≥70% 琥珀 / 其余绿,quotaColor:284)。

UI:成本/Token 卡片区

  • 标题:有可计价成本时 usage.section.cost+"· 估算"usage.estimate,否则 usage.section.tokens
  • 卡片(costCards:148):计价时 3 张(今日$ / 本周$ / Tokens 今日),否则 2 张(今日/本周 Tokens)。 bgElevated 圆角 10,值 17pt medium。金额 OpsnookFormat.money("$%.2f"),token compactTokens("1.2K/1.2M")。
  • 底部免责:usage.estimate_note(仅计价时显示)。

UI:7 日趋势柱状图(trendChart:178)

  • 7 根圆角 3 柱,高 34 内按当日 token/最大值比例,最矮 3pt;今日柱琥珀,其余白 14%。 柱下标签 = 单字星期(weekdayNarrow),今日显示 usage.today。无 hover/tooltip。

UI:按 Agent 明细(usage.section.by_agent

  • 每行:AgentIconView(22) + 名称 + 右侧本周 token(+计价时次行本周成本)。

  • 修改指南:加一家 Agent 的用量 → UsageReader(新扫描)+ UsageAggregator; 改单价 → ModelPricing.swift;改卡片/柱状样式 → UsageView 对应函数。

  • 验证:UsageTests(聚合/计价)、UsageParserTests(各家解析)。

3.6 「工作区」Tab(文件 / Git)

文件:WorkspaceView.swift(1486 行,全部 UI)+ WorkspaceModel.swift(状态与操作)+ GitWorkspaceService.swift(git 子进程)+ WorkspaceSnapshot.swift(模型)。 面板宽 440。此页有自己的表头(公共表头隐藏)。

顶层状态机(WorkspaceView.body:23)

  1. snapshot.disabledReason != nil → 阻塞态(lock.fill + workspace.unavailable + 原因文案; needsAuthorization 且沙盒 → 「授权项目目录…」按钮 → model.authorize() → AppDelegate NSOpenPanel(预定位到会话项目共同父目录,WorkspaceModel.suggestedWorkspaceRoot:129))。
  2. 无仓库 → 空态(workspace.empty/empty_detail + 沙盒下「重新选择工作区目录…」按钮)。
  3. 有仓库未选中 → 加载态(workspace.loading)。
  4. 正常 → 表头 + (文件视图 | Git 视图 | 非 git 仓库占位)。
  • 占位态右上角均叠刷新按钮(placeholder:41,加载中转圈禁用)。

UI:工作区表头(repositoryHeader:305)

  • 行 1:OpsnookBrandMark + "Opsnook" + "·" + 仓库切换 Menu(folder 图标 + 仓库名 + ▾, 白 6% 圆角 7;菜单项 = 各仓库名 + 分隔线 + 「重新选择工作区目录…」)+ Spacer + 文件|Git 段控件(paneToggle:382,folder/arrow.triangle.branch 图标 + workspace.pane_files/ pane_git,选中白 14% 高亮,.fixedSize() 防换行)+ 刷新按钮 + 图钉。
  • 行 2(仅 git 仓库,branchStrip:453):分支 pill(arrow.branch 天蓝 + 分支名等宽字白 6% 底)+ upstream("vs origin/main" textTertiary;无 upstream 时显示琥珀色提示 workspace.no_upstream)+ Spacer + ahead/behind 计数(↑n ↓n 琥珀)+ 同步/推送按钮。

UI:同步/推送按钮(syncBadge:420)

  • 三态:已同步(checkmark workspace.synced,灰,禁用)/ 可推送(arrow.up workspace.push, 琥珀实底 onApproval 字)/ 推送中(自绘 WorkspaceInlineSpinner + workspace.pushing,禁用)。
  • 交互:点击 → model.push(),立即置 isPushing → git push → 成功后同步重载快照再解除 loading(避免按钮闪回,见 WorkspaceModel.push:489 注释);失败 → errorRow。

UI:冲突横幅(conflictBanner:508)

  • 显示条件:repository.hasConflicts。红 12% 底圆角 9 + arrow.triangle.merge + workspace.conflict_banner("%d 个冲突文件。推送前需要先在对应仓库解决冲突。")。

文件视图(pane == .files,默认)

UI:文件树行(directoryRow:200)

  • 每行:展开箭头(仅文件夹,chevron.down/right 9pt;子项加载中 mini spinner)+ 图标(folder.fill 天蓝 / doc 灰)+ 名称(12.5pt,文件夹 medium)+(仅文件)行尾 arrow.up.right.square 提示可打开。层级缩进每级 14pt。hover 整行 bgHover。顶部 hairline。
  • 交互:
    • 文件夹单击 → toggleDirectory 原地展开/收起(子项懒加载缓存 directoryChildren);
    • 文件双击 → onOpenFile(entry.url)NSWorkspace.open(系统默认编辑器);
    • 拖拽:文件可 onDrag,拖到文件夹行 onDropmodel.move(_:into:)GitWorkspaceService.moveItem(真实 mv)→ 刷新树;
    • 右键 → 「在访达中显示」workspace.reveal_in_finderactivateFileViewerSelecting
  • 空文件夹:workspace.empty_folder("此文件夹为空")。
  • 滚动高度:workspaceScrollHeight = max(280, min(maxContentHeight-96, 560))(:299)。

Git 视图(pane == .git)

结构自上而下(gitBody:123,整体一个内层 ScrollView): 未提交标题 → 已暂存区 → 提交框 → 未暂存区 → 提交时间线。

UI:「未提交 · N 文件」标题(worktreeHeader:529)

  • pencil.line + workspace.worktree_title,琥珀。工作树干净时下挂绿勾 + workspace.clean

UI:已暂存/未暂存小节头(subsectionHeader:1291)

  • workspace.staged_count("已暂存 · %d",绿)/unstaged_count(灰) + 右侧琥珀动作 「取消全部暂存」unstage_all/「全部暂存」stage_allmodel.unstageAll()/stageAll() (乐观更新:先本地翻转 isStaged 再跑 git,setStaged:443)。

UI:变更文件行(fileRow:584)

  • 布局:展开 chevron + 暂存勾选框(checkmark.square.fill 绿/square 灰,冲突文件禁用, 点击 stage/unstage 单文件)+ 改动归因头像(≤2 个 18pt AgentIconView 重叠,hover 提示全名; 无法归因不显示,changedByAvatars:652,数据来自 WorkspaceFileAttribution 解析 transcript)+ 状态字母徽标(M/A/D/R/? 等宽 10pt,色 = added/untracked 绿、deleted/conflicted 红、 renamed/copied 天蓝、modified 琥珀,statusColor:1380)+ 文件名(等宽 12pt)+ 目录(9.5pt 灰)+ [冲突徽标 注意:workspace.conflict_badge] + 右侧 增删比例条(46×6 绿/红 Capsule,changeBar:690)
    • +n/−n 数字。
  • 交互:点行(除勾选框) → toggleFile 展开 diff 详情(懒加载 loadFileDiffIfNeeded)。

UI:文件 diff 详情(fileDetail:712)

  • 状态:加载中(mini spinner + workspace.diff_loading)/ 有内容(diffView)/ 无 diff(workspace.no_diff;未跟踪文件 workspace.untracked_no_diff)。
  • diffView(:749):最多渲染前 80 行(lines.prefix(80))。hunk 行天蓝 @ 白 3% 底; 普通行 = 旧/新行号(34pt 右对齐白 20%)+ 前缀 +/− + 内容,加行绿 8% 底、删行红 8% 底,等宽 11pt。
  • 底部动作条(fileActions:791):「在编辑器打开」workspace.open_editor / 「返回 Agent」workspace.focus_terminal(取仓库第一个 agent 上下文回跳)/ 右侧 回滚二段确认:初始「回滚此文件」琥珀(requestRevert 只置 pendingRevertFileID)→ 变为「取消」+「确认回滚」红(confirmRevert → git checkout/clean 该文件)。 未跟踪文件 canRevert=false(workspace.error_untracked_revert 兜底)。

UI:提交框(composer:848)

  • 显示条件:repository.files 非空。#0E0E10 圆角 10 卡片,可提交时琥珀 35% 描边。
  • 头行:arrow.triangle.branch + workspace.commit_to("提交到 %@分支") + 右侧统计 (workspace.commit_stat("%d 个文件 · +%d -%d")或 workspace.no_staged)。
  • 两个输入框(TextField .plain):主信息(占位 workspace.commit_message_placeholder, 聚焦即隐藏占位,自绘 ZStack 占位,composer:869 注释)+ 补充说明(可选)。 能输入的前提:NotchPanel canBecomeKey=true + becomesKeyOnlyIfNeeded (NotchWindowController.swift:14 注释);输入期间面板是 key 窗口 → 不会因鼠标移出收起 (recompute:581)。
  • 「提交后推送」勾选(无 upstream 禁用)+ 提交按钮(标题 = commit_files("提交 %d 个文件") 或 commit_and_push;可提交 = 有已暂存 + 信息非空(canCommit),否则灰禁用)。
  • 提交成功:清空两框与勾选 → refresh;失败 → errorRow(errorText:1429 映射 workspace.error_*)。

UI:错误行(errorRow:962)

  • exclamationmark.circle.fill 红 + 错误文案 + 右侧 关闭(model.dismissError)。

UI:提交时间线(commitTimeline:985)

  • 按 今天/昨天/更早 分组(dayLabel)。空态 workspace.no_commits
  • 提交行(commitRow:1063):左 34pt 时间线列(1.5 竖线 + 节点,能按作者名匹配到 Agent 时 显示其头像,否则三环圆点,timelineGutter:1139)+ 标题(13pt,最多 2 行)+ 作者 · 短hash · N 文件 + 增删条/数字 + [「待推送」绿徽标 workspace.unpushed] + 右侧时刻(HH:mm,OpsnookFormat.clock)+ 展开 chevron。
  • 交互:点行展开文件清单(commitDetail)→ 点文件行再展开该文件 diff (toggleCommitFile 懒加载,缓存键 hash:path)。
  • Agent 过滤:model.excludedAgentIDs 会按作者名过滤提交(visibleCommits:1006), 注意:当前 Git 页没有提供过滤 UI 入口,模型层具备该能力但视图未提供开关,待确认项见第 8 节。

3.7 设置页

正文组件 OpsnookSettingsContent(App/OpsnookApp.swift:23-466)同时用于: ① 面板内嵌设置(齿轮进入,PanelView.settingsContent → settingsRoot → 注入的 settingsPage); ② ⌘, 系统设置窗口(OpsnookSettingsWindow,OpsnookApp.swift:524,标题行 + 同一正文, 宽 420 深色)。文案在 App target 的 App/Resources/Localizable.xcstrings (读取函数 settingsLocalized,OpsnookApp.swift:561)。

UI:「Agent 数据访问」区(仅沙盒显示)

  • 显示条件:directoryAccess.isSandboxed(App Store 渠道恒 true;⌘, 窗口没传 directoryAccess, 不显示此区)。
  • 标题 settings.agent_access + 说明 settings.agent_access_detail
  • 列表容器:ScrollbarlessScrollView(OpsnookUI/DesignSystem/ScrollbarlessScrollView.swift, 自绘 NSScrollView + 翻转 clipView,永不画滚动条;为什么不用 SwiftUI ScrollView 见文件头注释), 固定视高 = 4 行(行高 56 + 间距 8,OpsnookApp.swift:173-179),其余滚动。
  • 13 行(agentAccessRows:183):前 7 家已接入(Claude Code/Codex/Antigravity/ZCode/WorkBuddy/ TRAE SOLO/Qoder,各带 AgentDirectoryKey)+ 6 家占位(GitHub Copilot/Cursor/Windsurf/Cline/ Gemini CLI/Aider,directoryKey=nil)。
  • 每行(agentAccessRow:237):AgentIconView(30) + 名称 + 形态小标签(clientChip, settings.client_desktop/cli/desktop_cli)+ 次行路径(~/.claude 等;TRAE 显示 "…(需 2 个目录)" settings.agent_access_detail_with_runtime;占位行显示 settings.agent_planned_detail)+ [ⓘ 按钮] + 尾部控件。
  • ⓘ 说明气泡(agentInfoButton:290):仅 TRAE / Qoder 行显示(目录布局特殊),点击弹 popover(宽 248,文案 settings.agent_access_info_trae/qoder),再点或点外关闭。
  • 尾部控件三态(agentRowTrailing:328):
    1. 未授权:「授权」小按钮 → grantRow,按需依次弹 NSOpenPanel(TRAE 两步:会话目录+运行时 目录,面板 message 分步标注;用户取消某步即停止)。面板预定位到目标目录上级、显示隐藏文件 (DirectoryAccessModel.grant:57)。选错文件夹(有数据标记校验的 Qoder)→ 书签已存但 显示 mismatch 错误文案,行保持未授权可重选。
    2. 已授权:绿「已授权 」,hover 变红「撤销授权 」,点击撤销并重建管线 (AgentAccessGrantedControl,OpsnookApp.swift:470;该 Agent 会话即刻从面板消失)。
    3. 占位:「敬请期待」灰 Capsule(settings.agent_coming_soon)。
  • 错误行:access.lastError 红字(授权失败/选错文件夹)。

UI:「通用」区

  • settings.general 标题下三行 settingRow(白 4.5% 圆角 10 卡片):
    1. 登录时启动:Toggle(.switch)→ LaunchAtLoginController.setEnabled; detail 按状态显示 settings.launch_disabled/enabled/requires_approval/not_found; 失败时底部红字 errorMessage。
    2. ⌥⌘H 提示:右侧只读 "⌥⌘H" 键帽样式(白 8% 圆角 6),改快捷键不在这里,在 GlobalHotKey init 参数(App/Windows/GlobalHotKey.swift:15),这行只是展示文案。
    3. 界面语言:Picker(宽 152)三项 language.system/simplified_chinese/englishAppLanguageController.select → UserDefaults + 全 UI locale 即时切换。
    4. 退出:settings.quit_button 按钮 → NSApp.terminate
  • 修改指南:加一行设置 → 复制一个 settingRow(title:detail:){trailing} + xcstrings 加文案。

3.8 会话时间轴独立窗口

文件:窗口壳 App/Windows/TimelineWindowController.swift;根视图/工具栏/侧栏 OpsnookUI/Timeline/SessionTimelineView.swift;画布/卡片 TimelineCanvasView.swift; 详情 TimelineDetailPanel.swift;ViewModel+布局 SessionTimelineModel.swift; 视觉 token TimelineStyle.swift;导出 TimelineImageExporter.swift

UI:窗口本体

  • 1280×820(min 980×640),titled/closable/miniaturizable/resizable,标题=会话标题、 副标题=项目名,darkAqua 外观、透明标题栏,frame 自动保存名 OpsnookTimelineWindow (TimelineWindowController.swift:31-45)。每会话一扇窗(windows:[SessionID:NSWindow]), 重复打开前置复用;打开时 NSApp.activate()(LSUIElement App 需显式激活)。
  • 加载态:spinner + timeline.loading;空态:clock.badge.questionmark + timeline.empty (老会话主文件只剩 summary 行时落这里)。
  • 语言:直读 AppLanguageController.shared.locale(切语言即时生效,SessionTimelineView.swift:8 注释)。

UI:工具栏(46pt,toolbar:72)

从左到右(样式统一:白 5% 圆角 8 + 白 10% 描边小按钮):

  1. 侧栏开关 sidebar.left/leadingmodel.sidebarVisible.toggle()
  2. 横版|竖版分段(timeline.orientation_horizontal/vertical)→ 切 orientation + resetView + centerOnSelected。
  3. ‹ 错误 / 错误 ›:jump(toError:true, direction:∓1),循环跳上/下一个错误事件。
  4. 工具调用:jump(toError:false) 跳下一个工具类事件。
  5. 「分组框」开关(groupFrameToggle:142):model.showGroupFrames(默认 true), 展开的同类组画虚线框;激活白 14% 高亮。
  6. 缩放组:−/+步进(±0.15)+ Slider(范围 zoomRange 0.55…1.45)+ 百分比只读。
  7. 刷新(refreshButton:188):重新解析 session 文件,保留视图状态只增量补事件 (model.refresh:102),进行中转圈禁点。
  8. 导出图片 photo.badge.arrow.downTimelineImageExporter.export:NSSavePanel 选路径 → ImageRenderer 离屏渲染完整时间轴(不受当前平移缩放影响,含标题条)→ PNG 写盘 → 访达选中;超大画布自动降 scale(像素预算 8000 万,:93);失败弹 NSAlert timeline.export_failed。 默认文件名 Timeline-<标题40字>-<yyyyMMdd-HHmmss>.png
  9. 重置视图 scope:zoom=1 + 回到起点并居中选中。

UI:左侧控制台(sidebar:305,宽 244,可整体滚动)

  • 头部:timeline.console("控制台") + 匹配/可见计数(timeline.match_count/visible_count)。
  • 搜索框(searchSection:344):占位 timeline.search_placeholder("搜索摘要、命令、文件、JSON"); 有文字时尾部 清空。搜索不隐藏卡片,不匹配的淡化到 28% 透明、匹配的琥珀描边 (TimelineEventCard dimmed/highlighted)。可搜索文本 = 标题+预览+facts+原始 JSON(apply:114)。
  • 事件类型网格(typeFilterSection:381):2 列 15 个类型 chip + 右上「全选」。 每个 chip 左半 = 图标+名称,点击跳到下一个该类事件(jump(toKind:),隐藏中会先恢复显示); 右半眼睛 eye/eye.slash = 显隐开关(toggleKind,至少保留一类)。激活态 = 类型色 16% 底 + 48% 描边。15 类的图标/颜色/文案 key 全在 TimelineStyle.swift(color/symbol/kindLabelKey)。
  • 泳道列表(laneSection:460,标题 "Agent / Subagent"):主泳道行 = Agent 头像 + 名称 + 事件数;右侧漏斗 = 「只看此泳道」(laneFilter)。子泳道行 = ⑂ 圆标(泳道色)+ 名称 + 事件数; 右侧 chevron = 折叠/展开该子泳道(collapsedLanes)。整行点击也 toggle laneFilter (激活 = 琥珀 7% 底)。laneFilter 非 nil 时标题右侧出现「全部泳道」清除按钮。 子泳道颜色 = TimelineStyle.lanePalette 按序轮换(laneColor:168)。

UI:画布(TimelineCanvasView)

  • 背景:42pt 细网格(白 3%,不随缩放)。
  • 内容层 TimelineCanvasContent(交互画布与图片导出共用):
    • 泳道连线(linesLayer:282):主泳道横线(泳道色 34%);子泳道 git 图风格:实线肘形分叉 (spawn 节点垂直落到泳道,圆角 14)+ 虚线汇回(merge 节点,dash [6,6])。
    • 分组虚线框(白 16%,dash[5,4])、堆叠连线(白 10% dash[3,5])。
    • 节点圆点:按事件类型着色,选中放大(r 4.8→7)。
    • 起点标记:主泳道起点前的 Agent 头像圆标(startMarker:436)。
    • 折叠泳道占位(collapsedStubs:458):琥珀虚线胶囊 "⑂ <名> 已折叠 · N 个事件",点击展开。
  • 手势:拖拽平移(highPriorityGesture,按住卡片也能拖,:31 注释);捏合缩放; 点击卡片 = 选中+开关详情。
  • 缩略图(minimap:105,右下 174pt 浮窗):事件散点(类型色 4pt)+ 白框视口指示, 点/拖任意处画布跳转(navigateMinimap);下方显示 横版/竖版 + 可见计数。

UI:事件卡片(TimelineEventCard:504,宽 258)

  • 头部:类型圆标(图标+类型色描边)+ 类型名 11pt bold + 时间 HH:mm:ss + 右侧状态点+状态词 (success/running 绿、failed 红、cancelled 灰、pending 琥珀,timeline.status.*)。
  • 主体:标题(12.5pt semibold,2 行)+ 预览行(命令类等宽字体)。
  • 底部:泳道名(子泳道用泳道色)+ [收起/展开胶囊] + 耗时(TimelineStyle.duration, ms/1.2s/45s/2m 30s 分级)。
  • 边框优先级(borderColor:560):选中琥珀 > 失败红 > 搜索命中琥珀 > 子泳道色 > hover 白 18% > hairline。 子泳道卡另叠泳道色 12% 底。hover 底色 #121214→#18181B
  • 收起/展开胶囊(accessoryButton:651,琥珀小 Capsule):
    • 组锚点卡:展开态显数字 "N"(点击收起后变 "+N"),收起该列同类堆叠;
    • 人工输入卡:timeline.collapse_turn("收起本轮")/expand_turn("展开本轮 · %d"), 收起该轮全部工具步骤。搜索/跳转命中被收起事件时自动展开(model.select:477)。
  • 布局算法要点(SessionTimelineModel.layout:305,改布局先读这里的注释): 横版步距 288/道距 168,竖版 148/314;同泳道连续同类工具事件堆叠(上限 8), 横版首泳道向上堆、其余向下;spawn/merge 事件钉死在轴上不参与堆叠。

UI:右侧详情面板(TimelineDetailPanel,宽 340)

  • 显示条件:点卡片 toggle(detailVisible)。头部 "事件详情" + 。
  • 内容:图标+标题(可选中)→ 动作行【完整 JSON/收起 JSON、复制 JSON(点击后 1.6s 显示"已复制")、 打开文件(有 filePath 时)】→ 基础字段(时间/Agent/状态/耗时)→ 文件清单(多文件 patch, 每行可点 = 在编辑器打开,右侧 +n/−n)→ 关键字段 facts(label 走 timeline.fact.* 本地化, 值等宽可选中)→ [完整 JSON(prettyPrinted,内滚 300pt)] → 上下文窗口(前后各 5 事件, 点击跳转选中并居中)。

4. 页面级「从外观到代码」说明

每页给出:布局层级树(→ 渲染组件)+ 进入/离开行为 + 数据来源。收起态、展开面板与时间轴的截图见 docs/FEATURES.md 第 1 节。

4.1 收起态刘海条

NotchPanel(NSPanel, .statusBar 层级, 无边框)
└─ PanelContainerView(CAShapeLayer 喇叭口遮罩 + 玻璃描边, 底色 #0A0A0B)
   └─ NSHostingView<NotchRootView>
      └─ NotchCollapsedView(黑底, 高 = state.collapsedHeight)
         ├─ leftWing: HStack(-6){ miniIcon ×≤4, "+N" }
         ├─ Spacer(minLength: notchGap)   ← 中间留给硬件刘海
         └─ rightWing: 六态之一(暗点/波形+数/CompletionIndicator/琥珀徽标/蓝徽标/红点文字)
  • 尺寸控制:NotchWindowController.updateFrame(collapsed 分支:646)→ NotchLayout.collapsedFrame;高度旋钮 OpsnookMetrics.collapsedHeightReduction
  • 数据:OpsnookViewModel.aggregate(AggregateSnapshot)。每次会话变化整体重算。

4.2 展开面板

NotchRootView(contentExpanded 分支)
└─ VStack(0)
   ├─ NotchCollapsedView(顶部刘海条, 高 = menuBarHeight, 黑底)
   └─ PanelView(宽 = tab.panelWidth, bgPanel)
      ├─ [header](workspace 页隐藏; 预览态被 TranscriptPeekView.header 替换)
      ├─ Divider(hairline)
      ├─ content ──┬─ agents: 分组标题+PanelRow(+SubagentRow/ApprovalCard/QuestionCard) | 空态
      │            ├─ activity: ActivityView(分组+ActivityRow)
      │            ├─ usage: UsageView(配额/卡片/趋势/明细)
      │            ├─ workspace: WorkspaceView(自带表头+文件树|Git)
      │            ├─ settings: settingsPage(App 注入 OpsnookSettingsContent)
      │            └─ (peek.loaded): TranscriptPeekView(覆盖任何 Tab)
      ├─ Divider(hairline)
      ├─ footer(Tab ×4 + 齿轮; 活动筛选态左侧多清除按钮)
      └─ [agentFilterPopover](ZStack 右上浮层)
  • 进入:hover/⌥⌘H/pinned/isKeyWindow 任一 → recompute() 展开动画(0.40s 回弹曲线)。
  • 离开:全部条件消失 → 0.30s 卷起动画(内容保持到动画结束才切回收起,contentExpanded 机制)。
  • 切 Tab:即时定高(不动画);切走时关闭浮层/设置详情/消息预览。
  • 每页数据来源:agents=SessionStore 推流;activity=events 流(内存 7 天); usage=onAppear 拉一次;workspace=切入时+会话变化时 refresh;settings=本地控制器直读。

4.3 时间轴窗口

NSWindow(darkAqua, 1280×820, 每会话一扇)
└─ SessionTimelineView
   ├─ toolbar(46pt: 侧栏/横竖/错误跳转/工具跳转/分组框/缩放/刷新/导出/重置)
   ├─ Divider
   └─ HStack(0)
      ├─ [sidebar 244pt](控制台: 搜索/类型chips/泳道)
      ├─ TimelineCanvasView(网格 + TimelineCanvasContent(连线/卡片) + minimap 浮窗)
      └─ [TimelineDetailPanel 340pt](事件详情)
  • 进入:.task { model.load() } → 后台解析 JSONL → 首帧定位到主泳道起点(focusStart)。
  • 离开:关窗 → TimelineWindowManager.windowWillClose 从表移除(模型随窗释放,无持久化, 重开重新解析)。

4.4 ⌘, 系统设置窗口

Settings Scene(OpsnookApp.body)
└─ OpsnookSettingsWindow(宽 420, bgPanel, 强制深色)
   ├─ 标题行: OpsnookBrandMark + "Opsnook" + "设置"
   ├─ Divider
   └─ OpsnookSettingsContent(不含 Agent 数据访问区,未注入 directoryAccess)

5. 代码模块说明

5.1 UI/视图层(OpsnookUI)

  • 入口:NotchRootView(面板)、SessionTimelineView(时间轴窗口)、OpsnookSettingsContent(App 层)。
  • 职责:纯渲染 + 把用户操作转成闭包回调;不直接碰 Adapters/Integration。
  • 关键文件:Panel/(五个 Tab)、Notch/(收起态+头像)、Timeline/、DesignSystem/(token)、 Localization/OpsnookL10n、ViewModels/*。
  • 被谁调用:App 层 NotchWindowController/TimelineWindowManager 装配。
  • 修改影响:视图文件互相独立,改一个 Tab 不影响其他;唯一全局耦合是「内容高度变化必须 onContentResize / 被 withObservationTracking 跟踪」(见 §8-R1)。

5.2 导航 / 窗口(App/Windows)

  • 没有路由框架。全部"导航"就三种:NotchUIState.panelTab 切页、TranscriptPeekModel.state 覆盖内容区、独立 NSWindow(时间轴、⌘, 设置)。
  • NotchWindowController 是最复杂的单文件:hover 判定、窗口 frame 动画、8 个 withObservationTracking 订阅(model/discovery/notifications/activity/usage/workspace/peek)。 加任何影响面板高度的新状态,都要加进对应 observe*。

5.3 状态管理(OpsnookCore/State + Store)

  • SessionStore(actor,唯一真相源):ingest(emission) → reducer → 发布 AsyncStream<StorePublication>(sessions+aggregate)与 AsyncStream<AgentEvent>
  • SessionReducer(纯函数):快照→会话合并(nil 字段沿用旧值)、状态跃迁事件派生、 「首见 done 视为历史→idle」、「首见回填 lastActivityAt」、subagent 生命周期事件、 terminatedByUser 判定。所有状态语义 bug 先看这里。
  • AggregateBuilder:收起态聚合纯函数。UsageAggregator:用量聚合。
  • EngineConfig:doneDecay=8s(完成→空闲),staleTimeout=nil(不自动判 idle)。

5.4 业务/适配器层(OpsnookAdapters)

  • 协议 AgentAdapter(AgentAdapter.swift):descriptor / detect() / start() → AsyncStream <AdapterEmission> / stop() / loadTranscript(for:)。注册与分发 = AdapterRegistry(actor)。
  • 7 个实现(数据源 → 状态判定,细节以 docs/sessions_status_logic/<agent>.md 为权威):
适配器 数据源 运行中判据要点 L 级
ClaudeCodeAdapter ~/.claude/projects/**.jsonl + hook 状态目录(Opsnook 容器 App Support/Opsnook/ClaudeHooks) JSONL 尾部事件 + hook 请求文件;审批/问题卡来自 PermissionRequest/AskUserQuestion L2/L3
CodexAdapter ~/.codex/sessions/**.jsonl + session_index(thread_name 标题) rollout 尾部 + 桌面客户端进程保活(codexClientRunning) L2
AntigravityAdapter ~/.gemini/antigravity{,-cli}/brain transcript 目录区分桌面/CLI 两 kind;INVOKE_SUBAGENT 登记子代理 L2
ZCodeAdapter ~/.zcode/cli/db/db.sqlite ZCodeStatusMap:消息/finish_reason → 6 态 L2
WorkBuddyAdapter ~/.workbuddy/projects/**.jsonl + sessions/<pid>.json 心跳 Responses-API transcript + 心跳保活 L2
TraeAdapter ~/.trae-cn/memory/projects/** 摘要 +(可选)运行时目录探针 TraeRuntimeProbe 读 SessionUpdated 日志 tag + 进程存活(database.db 加密不可读全文) L2
QoderAdapter ~/Library/Application Support/Qoder/User/globalStorage/state.vscdbaicoding.questTaskListSnapshot 单一权威键(标题/状态/cwd)+ 进程闸门 L2
  • 共用工具:SessionFileFingerprint(mtime/size 免重复解析)、ShellCommandClassifier (命令→时间轴 15 类)、GitHead(读分支)、AdapterLimits(标题截断)、 AgentCatalog(品牌/颜色/kind 唯一来源)。
  • 时间轴解析器(5 个 *TimelineParser):JSONL → SessionTimeline(事件/泳道/facts/rawLines), 工具调用按 id 配对回填状态与耗时。各家 schema 的依据见 docs/sessions_status_logic/
  • 纪律:适配器不得崩溃,解析失败一律优雅降级为空结果或维持现状。

5.5 网络/API

  • 无任何自有网络层。唯一出站 = GitWorkspaceService.push 的 git 子进程(entitlement network.client 就为它)。UsageReader 读的 rate_limits 是 Codex 落在本地文件里的数据。

5.6 数据持久化

见 §6.3 表。没有自建数据库;全部是 UserDefaults + 读外部 Agent 的文件/SQLite(只读)。

5.7 权限与系统集成(OpsnookIntegration)

  • 文件访问:SecurityScopedBookmarkStore(UserDefaults 存书签、NSLock 保护 active 表、 stale 自动刷新)+ AgentDirectoryAccess(key→真实目录、数据标记校验 hasUsableData)。
  • 回跳:TerminalFocuser(deepLink → AppleScript 标签页定位 → App 级激活;支持 Terminal/iTerm2/Warp/VSCode,见 supportedBundleIDs)+ ApplicationFocuser(App 级激活兜底)。
  • Claude hooks:ClaudeHooksInstallerensureRuntime()(启动时写 Opsnook 自己的状态目录, 安全)/prepareIntegration()(用户显式触发,合并写 ~/.claude/settings.json)/ manualConfigurationSnippet()/isIntegrationInstalled()
  • 通知:NotificationCoordinator(仅视觉 cue,无系统通知)。登录项:LaunchAtLoginController
  • Git:GitWorkspaceService(actor;snapshot/stage/unstage/commit/push/revert/diff/ directoryEntries/moveItem;runGit 先读输出再 wait 防 64KB 管道死锁; GIT_TERMINAL_PROMPT=0 等加固)。

5.8 后台任务

  • 每个适配器一个轮询循环,在 start() 内部启动,间隔由各适配器自定。已做事件驱动优化, 即 FSEvents 加按需鼠标监听,空闲 CPU 占用约为 0.1%。
  • store.runDecayLoop() 每秒衰减。无 cron/推送。

5.9 错误处理与日志

  • 日志:NSLog("Opsnook: …") 散布关键路径(回跳结果、审批回传、授权失败); DEBUG 布局日志写 /tmp/hive_debug.log。无日志文件轮转、无上报(隐私红线)。
  • 用户可见错误:工作区 errorRow(WorkspaceFailure 枚举→本地化)、设置页授权红字、 登录项红字、时间轴导出 NSAlert。

5.10 国际化 / 主题 / 资源

  • 双语 strings ×2 处(见 F-L);LocalizationTests 保证中英 key 一致。
  • 主题:面板/时间轴强制深色;唯一"跟随系统"的是 ⌘, 窗口外的系统控件描边。设计 token 全在 OpsnookColor/OpsnookMetrics/TimelineStyle。
  • 资源:BrandIcons PNG(Lobe Icons,许可证同目录)、AppIcon.appiconset。

5.11 构建 / 发布 / 测试 / CI

  • project.yml(XcodeGen)→ Opsnook-AppStore scheme,Debug-AppStore/Release-AppStore 两配置。
  • CI(.github/workflows/ci.yml):swift test → xcodegen → xcodebuild build。
  • 打包:scripts/package-appstore.sh(自测 .app+zip)、scripts/make-dmg.sh(dmg); 沙盒约束与能力降级见 docs/SANDBOX.md。
  • 测试:Packages/OpsnookKit && swift test;分层 OpsnookCoreTests / AdaptersTests / IntegrationTests / UITests(纯函数级)。

6. 数据与状态流

6.1 主状态流(Mermaid)

flowchart LR
  subgraph 外部世界
    F1[~/.claude JSONL+hook文件] --> A1
    F2[~/.codex JSONL+index] --> A2
    F3[~/.gemini brain] --> A3
    F4[zcode/qoder SQLite] --> A4
    F5[workbuddy/trae 文件] --> A5
  end
  subgraph OpsnookAdapters
    A1[ClaudeCodeAdapter]; A2[CodexAdapter]; A3[AntigravityAdapter]
    A4[ZCode/QoderAdapter]; A5[WorkBuddy/TraeAdapter]
  end
  A1 & A2 & A3 & A4 & A5 -->|AdapterEmission<br>sessionUpserted/removed/health| R[AdapterRegistry.run]
  R --> S[(SessionStore actor)]
  S -->|SessionReducer.apply| S
  S -->|publications: sessions+aggregate| VM[OpsnookViewModel]
  S -->|events 流| AM[ActivityModel] & NC[NotificationCoordinator]
  VM --> NCV[NotchCollapsedView] & PV[PanelView/PanelRow]
  AM --> AV[ActivityView]
  NC -->|visualCue/focusMode| NUS[NotchUIState] --> NCV
  PV -->|onApprovalDecision| AC[AppContainer.respond] -->|decision 文件| F1
  PV -->|onSelect| TF[TerminalFocuser] -->|AX/deepLink/activate| OS[macOS]
Loading

6.2 重要状态变量清单

变量 类型/默认 写入 读取(影响的 UI) 持久化
SessionStore.sessionsByID [SessionID:AgentSession] ingest/applyDecay 一切会话 UI 否(重启由文件重建)
OpsnookViewModel.sessions/aggregate 数组/AggregateSnapshot store 推流 会话页、收起态
NotchUIState.expanded/contentExpanded Bool false NotchWindowController.recompute 窗口 frame / 视图分支
NotchUIState.pinned Bool false 图钉 移出不收起
NotchUIState.panelTab .agents 底栏 content 路由+面板宽
NotificationCoordinator.isFocusModeEnabled UserDefaults 读取 toggleFocusMode 徽标脉冲/完成闪烁静音 Opsnook.focusModeEnabled
AppLanguageController.selection .system 设置 Picker 全 UI locale Opsnook.appLanguage
ActivityModel.events/excludedAgentIDs []/空集 events 流/漏斗浮层 活动页、表头摘要、底栏
TranscriptPeekModel.state .hidden present/dismiss 内容区覆盖
UsageModel.snapshot/isLoading nil/false refresh 用量页
WorkspaceModel.snapshot/pane/selectedRepositoryID/expanded*/commitMessage/… 见文件 各操作方法 工作区页全部
AgentDiscoveryModel.items/isScanning/setupAgentID/feedbackByAgentID AppDelegate 扫描/接入 空态列表
DirectoryAccessModel.granted/lastError 启动时按书签算 grant/revoke 设置页授权区 书签在 UserDefaults
SessionTimelineModel.*(zoom/pan/search/enabledKinds/collapsed*/selected…) 见文件 工具栏/侧栏/画布 时间轴窗口 否(窗口 frame 除外)
PanelView.subagentExpandOverrides @State 空 fork 徽标 子行展开

6.3 持久化数据全表(本机 UserDefaults + 文件)

位置 键/路径 内容 读写代码 迁移风险
UserDefaults Opsnook.appLanguage "system/simplifiedChinese/english" AppLanguageController
UserDefaults Opsnook.focusModeEnabled Bool UserDefaultsFocusModePreferenceStore
UserDefaults OpsnookBookmark.claude/codex/gemini/zcode/workbuddy/trae/traeRuntime/qoder/workspace-root 安全作用域书签 Data SecurityScopedBookmarkStore(前缀见 init keyPrefix) 改 key 前缀 = 用户全部授权丢失,需重新授权
UserDefaults(系统) NSWindow Frame OpsnookTimelineWindow 时间轴窗口位置 setFrameAutosaveName
文件 ~/Library/Application Support/Opsnook/ClaudeHooks/ hook runtime 脚本 + request/decision IPC 文件 ClaudeHooksInstaller / ClaudeCodeAdapter 路径变更需同步 hook 配置里的绝对路径
外部只读 各 Agent 数据目录 见 5.4 表 各适配器 外部 App 升级可能改 schema,对拍 docs/sessions_status_logic
外部写入 ~/.claude/settings.json hooks 配置合并 ClaudeHooksInstaller.prepareIntegration(仅用户显式触发) 谨慎:是用户自己的配置文件

6.4 「临时 UI 状态」vs「会保存的数据」

  • 会保存:语言、专注模式、目录/工作区授权书签、时间轴窗口位置、Claude hooks 配置。
  • 纯临时(重启/切页丢失):图钉、当前 Tab、活动事件列表、筛选勾选、subagent 展开覆盖、 展开的 diff/提交、提交信息草稿、时间轴内全部视图状态。
    • 想把某个临时态变持久:加 UserDefaults 读写(参照 AppLanguageController 模式)。

6.5 「API」清单(本项目无 HTTP API,以下为跨层异步接口)

接口 请求方 → 实现 入参 → 出参 错误处理
container.usageReport() UsageModel.loader () → UsageSnapshot 未授权目录=空扫描
container.loadTranscript(agent:session:) TranscriptPeekModel.loader → SessionTranscript? nil→退化回跳
container.loadTimeline(for:) TimelineWindowManager.loader → SessionTimeline?(后台线程解析) nil/空→空态
container.workspaceSnapshot(sessions:) WorkspaceModel.loadSnapshot → WorkspaceSnapshot disabledReason 承载降级
container.stage/unstage/commit/push/revert/moveItem/… WorkspaceModel 各闭包 throws WorkspaceFailure → errorRow
container.respond(to:on:decision:) 审批卡 写 decision 文件 NSLog,UI 无阻塞
claudeHooksInstaller.prepareIntegration() 接入向导 合并 settings.json → receipt AgentSetupFeedback.failed

7. 手动修改速查指南(我想改什么 → 改哪里)

通用流程:改 → cd Packages/OpsnookKit && swift testxcodegen generate(仅当改了 project.yml)→ xcodebuild -scheme Opsnook-AppStore -configuration Debug-AppStore -derivedDataPath /tmp/OpsnookBuild build CODE_SIGNING_ALLOWED=NO → 运行核对。

7.1 改一段显示文字

  1. Packages/OpsnookKit/Sources/OpsnookUI/Resources/zh-Hans.lproj/Localizable.strings 全文搜该中文 → 得到 key → 同步改 en.lproj 同 key。
  2. 搜不到 → 是设置页文案:Xcode 打开 App/Resources/Localizable.xcstrings 搜; 仍搜不到 → 是 Agent 产出内容(标题/命令/消息),设计上不翻译不可改; 或是硬编码英文("Opsnook"、"⌥⌘H"、"vs %@" 等,直接在视图文件里搜字面量)。
  3. 验证:swift test(LocalizationTests 查 key 对称)+ 切中英文各看一遍。 风险:只改一种语言 → 另一语言仍旧文案;LocalizationTests 只查 key 存在,不查语义。

7.2 改图标

  • SF Symbol:在对应视图文件搜 Image(systemName: 换名字(SF Symbols.app 查名)。
  • Agent 品牌图标:换 OpsnookUI/Resources/BrandIcons/lobe-<brand>.png(保持文件名); 映射逻辑 AgentBrandIcon.lobeResourceName。产品 logo = opsnook-brand.png
  • App 图标:替换 App/Resources/Assets.xcassets/AppIcon.appiconset/icon_*.png (7 个尺寸,源 SVG 在 design/appicon/)。
  • 验证:PanelLayoutTests(资源存在性)+ 肉眼。

7.3 改颜色 / 字体 / 间距 / 圆角 / 阴影

  • 全局色 → OpsnookColor.swift(改 running 即全 App 绿色统一变)。
  • 品牌色 → AgentCatalog.swiftAgentBrand(colorHex:)
  • 时间轴事件色/泳道色 → TimelineStyle.swift
  • 几何/动画时长 → OpsnookMetrics.swift(面板宽、圆角、喇叭口、收起高度、动画曲线全在此)。
  • 单点字号/间距 → 对应视图文件的 .font(.system(size:))/.padding(…)(§3 每条目已给函数名)。
  • 风险:改 OpsnookColor.approval 会同时影响审批徽标/图钉高亮/筛选高亮/提交按钮等所有琥珀元素。

7.4 改按钮点击行为

  1. §3 找到该按钮条目 → 得到「事件处理」闭包名(如 onSelect/onTogglePin/onApprovalDecision)。
  2. 行为的真正实现沿注入链上溯:视图 → NotchRootView → NotchWindowController.configureRoot (App/Windows/NotchWindowController.swift:474,所有面板回调在这里接线)→ AppDelegate(:100 起)。
  3. 改实现通常在 NotchWindowController 私有方法或 AppContainer。

7.5 增加 / 删除一个 UI 元素

  • 加:在目标视图文件对应 ViewBuilder 里加;若它会改变面板高度且由 @State 驱动 → 必须调用 onContentResize()(参照 toggleSubagents)。
  • 删:直接删视图代码;检查其闭包参数是否还有其他调用方(Xcode ⇧⌘F)。
  • 验证:展开/收起/换页各来一遍,确认底栏不被裁、无残留空白。

7.6 改默认设置

  • 语言默认 → AppLanguageController.init?? .system
  • 专注模式默认 → UserDefaultsFocusModePreferenceStore.loadFocusModeEnabled(bool 默认 false)。
  • 默认 Tab → NotchUIState.panelTab = .agents
  • 完成衰减 8s → EngineConfig.doneDecay
  • 活动保留 7 天/300 条 → ActivityModel.init 默认参数。

7.7 改页面切换

  • Tab 顺序/组成 → PanelView.footer 两个分支 + PanelTab 枚举。
  • 切页副作用(关浮层等)→ PanelView.body.onChange(of: selectedTab) + NotchRootView.onSelectTab 闭包。

7.8 改「API」地址 / 参数 / 返回展示

  • 本项目无 HTTP。等价问题「改某 Agent 数据目录」→ AgentDirectoryKey.defaultDirectoryName (AgentDirectoryAccess.swift:16)+ 设置页引导文案;「改 git 命令参数」→ GitWorkspaceService 对应方法;「改返回结果展示」→ 对应 Snapshot 模型 + 视图。

7.9 改本地存储

  • 新偏好:参照 AppLanguageController(UserDefaults + @Observable)。
  • 改书签 key 前缀:SecurityScopedBookmarkStore init,会丢用户已有授权,慎改。

7.10 改权限逻辑

  • 能力矩阵 → Capabilities.appStore(唯一预设;想开 AppleScript 要加 apple-events entitlement)。
  • 目录授权流程 → DirectoryAccessModel(UI)/ AgentDirectoryAccess(解析)/ SecurityScopedBookmarkStore(存取)三层。
  • 沙盒 entitlement → App/Entitlements/Opsnook.AppStore.entitlements(改完必须真机验证沙盒行为)。

7.11 改快捷键

  • App/Windows/GlobalHotKey.swift init 默认参数:keyCode: UInt32(kVK_ANSI_H)modifiers: UInt32(optionKey | cmdKey)(Carbon 键码表在 Carbon.HIToolbox.Events)。
  • 同步改设置页展示文案:"⌥⌘H" 硬编码在 OpsnookApp.swift:62 + xcstrings 的 settings.toggle_panel_detail

7.12 改窗口尺寸 / 高度行为

  • 收起态高度 → OpsnookMetrics.collapsedHeightReduction(实例见 commit 34b59ed,就改这一个常量)。
  • 面板宽 → OpsnookMetrics.panelWidth/workspacePanelWidth
  • 内容最大高 → NotchWindowController.updateFrame 里 panelMaxContentHeight-96-16 估算。
  • 时间轴窗口初始/最小尺寸 → TimelineWindowController.swift:32/39。

7.13 改主题

  • 没有浅色主题:面板强制深色(NotchRootView:138)。若要跟随系统外观,需要移除强制 colorScheme, 并把 OpsnookColor 全表改为深浅双值的动态色。这项改动工作量较大。

7.14 新增一个小功能(以「面板加一个新 Tab」为例)

  1. PanelTab 加 case + panelWidth;2. footer 加 tabButton;3. content switch 加分支新视图;
  2. 需要数据 → 新 @Observable Model,在 AppDelegate 创建并沿 NotchWindowController→NotchRootView→ PanelView 注入;5. 高度会变 → NotchWindowController 加 observeXxxChanges;
  3. 文案进两份 strings;7. swift test + 真机。

7.15 排查三类常见问题

  • 某 UI 不显示:§3 查该条目「显示条件」→ 在条件处打 NSLog/断点。常见:状态不满足 (如时间轴按钮要求 .jsonl+ 支持品牌)、沙盒未授权(适配器根本没注册,查 registerAdaptersIfNeeded)、空数据走了空态分支。
  • 点击无响应:非激活 NSPanel 的 hit-test 特性,检查是否被 simultaneousGesture 抢走 (PanelView:113 注释)、按钮是否 disabled(审批安装中/推送中等)、 是否 allowsHitTesting(false)(时间轴连线层)。
  • 数据不刷新:确认对应 Model 的驱动源:usage 只在 onAppear 拉一次;workspace 要 refresh(sessions:);面板高度不更新 = 漏了 onContentResize 或漏加 withObservationTracking 字段(NotchWindowController 各 observe* 的注释里有两个历史 bug 案例)。

8. 可维护性与风险清单

R1 · 窗口高度手动同步(最易改坏):面板高度不是 SwiftUI 自动的, NotchWindowController 用 hostingView.fittingSize 量、用 8 组 withObservationTracking 跟踪所有会影响高度的模型字段。新增高度相关状态漏登记 → 内容被裁/顶出(文件内注释记录了 Files→Git、活动筛选两个先例)。@State 驱动的高度变化必须手动 onContentResize()

R2 · 非激活 NSPanel 交互陷阱:becomesKeyOnlyIfNeeded、透明 overlay 吃点击、 hover 靠全局 mouseMoved 而非 SwiftUI onHover(窗口 frame 判定)。改 hover/浮层/输入框行为前 先读 NotchWindowController:299-345 与 PanelView:110-116 注释。

R3 · 适配器对外部私有格式的依赖:7 家 Agent 的文件/DB schema 都是对方内部实现, 对方升级即可能破坏解析。防线 = 各 *Tests 的 fixture + docs/sessions_status_logic/。 修改前先跑对应测试,改后真机对拍。

R4 · Claude 审批链路时序:hook 阻塞等待 decision 文件,其路径以绝对路径写进 ~/.claude/settings.json。改动 hook 状态目录或包名会静默断链,症状是审批永远显示为运行中。 另外,Claude Desktop 本身不触发 PermissionRequest hook,症状相同但成因不同。

R5 · git 子进程:runGit 必须先读满输出再 wait,否则输出超过 64KB 时会发生管道死锁; 沙盒下 /usr/bin/git shim 调用 xcrun 的问题已由 resolveGitExecutable 处理。这两处不要简化。

R6 · 生成物:Opsnook.xcodeproj(xcodegen 生成)、dist/、DerivedData 不要手改/提交。 OpsnookUI/Resources/Localizable.xcstrings 不参与构建(真源是 .lproj/.strings)。

R7 · 并发:SessionStore/AdapterRegistry/GitWorkspaceService/WorkspaceFileAttribution 是 actor;UI 层全 @MainActor。跨界传值需 Sendable(Swift 6 strict 会编译期拦住,但改并发结构前 需要理清等待关系,decayLoop 每秒进入 store,因此不要在 ingest 中执行耗时操作)。

R8 · 隐式约定:

  • AgentStatus.rawValue 即优先级权重,插新状态必须重排全部权重;
  • 品牌 id 字符串在 AgentCatalog / SessionTimelineSupport.timelineBrands / AppContainer.loadTimeline / AgentBrandIcon.lobeResourceName 四处硬编码重复,加品牌要同步;
  • 事件只进内存,依赖活动页做审计不可靠;
  • ⌘, 设置窗口拿不到 directoryAccess(没有授权区)是有意为之。

待确认(无法从代码完全证实,已尽力核查):

  1. Git 页提交时间线的 Agent 过滤(WorkspaceModel.excludedAgentIDs/visibleCommits) 在当前 WorkspaceView 中未找到 UI 入口(strings 有 workspace.filter_all 等遗留 key), 疑为旧版入口被移除、能力保留。查看过 WorkspaceView 全文与 PanelView,未见调用 workspace.toggleAgentFilter 的视图。
  2. 专注模式(月亮图标)在当前 PanelView 表头没有按钮(FEATURES.md 1.10 提到月亮图标, 现码只有漏斗+图钉;onToggleFocusMode 闭包与 focus.enter/exit 文案仍在), 疑为改版中移除入口。功能本体(NotificationCoordinator)仍工作,但用户当前无法从 UI 切换。
  3. AgentDescriptor/DetectionMethod/AgentSetupAction 等发现向导模型未逐行读 (AgentDescriptor.swift),§F-N 行为依据 PanelView/AppContainer 调用侧代码。
  4. 五个 TimelineParser、各 SessionParser 的逐行为文档化程度止于 §5.4 表,每家内部判定细节 以 docs/sessions_status_logic/ 与测试 fixture 为准,本文不复述以免与真源漂移。
  5. 本文档未内嵌带编号标注的截图。docs/FEATURES.md 第 1 节的三张截图与第 4 节有对应关系, 但未做逐元素编号图,需要真机重截并标注,列为后续任务。

9. 完整性自检

  • 已遍历所有"页面/窗口":收起态、展开面板(5 个 Tab + 消息预览覆盖层 + 空态)、 时间轴窗口、⌘, 设置窗口、NSOpenPanel/NSSavePanel/NSAlert 系统面板(在相关条目内说明)
  • 已遍历所有可见 UI 组件(§3 共 60+ 条目,含原子组件 4 条)
  • 已记录所有按钮、菜单、输入框与交互(含右键菜单 ×2、拖拽 ×2、双击、hover 浮现、 快捷键 ⌥⌘H、捏合/拖拽手势、缩略图导航)
  • 已记录空态/加载态/错误态/禁用态/权限态(会话空态、活动空态、用量加载/空、 工作区阻塞/空/加载/非 git、时间轴加载/空、授权三态、推送三态、审批按钮按压态等)
  • 已记录关键状态变量与数据来源(§6.2/6.3 表)
  • 已记录所有跨层接口、本地存储与配置(§6.3/6.5、§1.6)
  • 已给出每个 UI 的代码定位(文件+类型/函数名+近似行号)
  • 已给出常见手动修改的定位与验证方法(§7 共 15 类)
  • 已标记无法确认的内容(§8 待确认 5 项,均注明查证过程)
  • 已抽查文档中的文件路径、类型名、函数名真实存在(生成时逐文件实读; 行号以 commit b2ab27e 为基线,漂移后请按名称搜索)
  • 页面截图 + 编号标注(待真机截图后补充,见待确认 5)

维护约定:每次改动 UI 后,顺手更新对应 §3 条目;新增文件同步更新 PROJECT_FILE_GUIDE.md。