目的:让熟悉产品功能但不熟悉代码的维护者,在看到界面上任何一个细节时, 能快速定位到应当修改哪个文件、哪个函数,改动会影响什么,以及如何验证。
基线:
main分支的开源首版。文中行号为撰写时的近似行号, 代码变动后行号会漂移,应优先用文件名加类型或函数名、再加唯一代码片段的方式搜索定位,行号仅作参考。本文档与其他文档的分工:
- 本文(CODE_TO_FEATURE_MAP.md)是 UI 元素级的外观到代码对照表,兼作手动修改速查。
- PROJECT_FILE_GUIDE.md 是全量文件树的逐文件说明,解释每个文件的用途。
- FEATURES.md 是功能清单与截图;TECHNICAL_DESIGN.md 是架构约束; sessions_status_logic/ 是各家 Agent 状态解析的逐家调研。
- 修改前必读的约束在根目录 AGENTS.md。
- 项目总览
- 功能总目录
- 全量 UI 对照表(最重要)
- 3.1 收起态刘海条
- 3.2 展开面板公共框架(表头 / 底栏 / 窗口动画)
- 3.3 「会话」Tab
- 3.4 「活动」Tab 与消息预览
- 3.5 「用量」Tab
- 3.6 「工作区」Tab(文件 / Git)
- 3.7 设置页(面板内嵌 + ⌘, 系统窗口)
- 3.8 会话时间轴独立窗口
- 页面级「从外观到代码」说明
- 代码模块说明
- 数据与状态流
- 手动修改速查指南
- 可维护性与风险清单
- 完整性自检
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。
- 平台:macOS 14+,Swift 6(strict concurrency = complete),AppKit + SwiftUI 混合。
- App 形态:
LSUIElement = true(Info.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.shDEBUG 专用环境变量(都在源码里以 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 |
| 技术 | 用在哪 |
|---|---|
| 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 构建 |
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)
依赖方向(不可逆):OpsnookCore ← OpsnookAdapters / OpsnookIntegration / OpsnookUI ← App。
OpsnookUI 不依赖 OpsnookAdapters,所有跨层数据都通过 App 层的闭包注入,例如 UsageModel(loader:)。
用户双击 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 显示左翼图标 + 右翼状态
| 类别 | 位置 | 说明 |
|---|---|---|
| 构建配置 | 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 |
可以放心手改(改完跑 swift test + 肉眼验证即可):
- 所有设计 token:
OpsnookUI/DesignSystem/OpsnookColor.swift、OpsnookMetrics.swift、Timeline/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 的状态判定都有对应记录,涉及心跳、进程闸门、加密数据库等情形。
每个功能给出:入口 → 操作 → 结果 → 调用链 → 最可能要改的文件 → 验证方法。 UI 细节(每个按钮/状态)在 §3 逐一展开,这里是功能级索引。
- 在哪看到:屏幕顶部菜单栏中央,黑色 pill,与硬件刘海融合。App 启动即显示。
- 操作:无需操作;鼠标移入 → 展开面板;⌥⌘H 也可展开。
- 可见结果:左翼 = 活跃 Agent 微缩图标(≤4,多余 "+N");右翼 = 全局最高优先级状态 (空闲 3 暗点 / 绿波形+数 / 琥珀手势徽标脉冲 / 蓝「N 待输入」徽标 / 红点「N 出错」/ 绿「完成」闪烁)。
- 调用链:适配器 →
SessionStore.ingest→AggregateBuilder.build(状态权重排序)→OpsnookViewModel.aggregate→NotchRootView(collapsed 分支)→NotchCollapsedView。 窗口宽度:OpsnookMetrics.collapsedContentWidth→NotchLayout.collapsedFrame→NotchWindowController.updateFrame。 - 最可能改:样式在
NotchCollapsedView.swift;宽度/高度在OpsnookMetrics.swift(collapsedHeightReduction、collapsedRightWingWidth等)+NotchWindowController.updateFrame; 聚合规则在AggregateBuilder.swift;排序权重在AgentStatus.swift(rawValue 即权重)。 - 验证:
OPSNOOK_DEMO=1跑起来看四种状态;单测NotchLayoutTests/StateMachineTests/PanelLayoutTests。
- 入口:鼠标悬停刘海条;全局 ⌥⌘H;表头图钉可固定。
- 结果:单段曲线动画展开为 420pt(工作区 440pt)面板;移出收起(自底向上卷起); 面板作为 key 窗口(如正在输入提交信息)时不收起。
- 调用链:
mouseMoved 全局监视器→NotchWindowController.updateHoverFromCursor→recompute()→state.expanded/contentExpanded→updateFrame(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看动画;检查换页后指针在面板外不误收。
- 入口:无 UI 入口,自动运行。7 个适配器轮询本地文件/SQLite。
- 调用链:
AppContainer.registerAdaptersIfNeeded()(按目录授权注册)→ 各*Adapter.start()→AsyncStream<AdapterEmission>→AdapterRegistry.run(into:)→SessionStore.ingest→SessionReducer.apply(纯函数:状态跃迁 + 派生 AgentEvent)→publications()/events()推流。 - 最可能改:某家状态不对 →
OpsnookAdapters/<Agent>/的 Adapter + SessionParser (先读docs/sessions_status_logic/<agent>.md);跃迁/事件逻辑 →SessionReducer.swift; 衰减 →EngineConfig.swift。 - 验证:
swift test(各*AdapterTests、StateMachineTests、SessionStoreTests)。
- 入口:Claude Code 请求权限时,「会话」页对应行下方出现审批卡(琥珀)。
- 操作:点「允许」/「拒绝」。
- 结果:决策经 decision 文件写回,阻塞中的 Claude hook 进程读到后放行/拒绝;活动页记「已允许/已拒绝」。
- 调用链:Claude hook(
ClaudeHooksInstaller安装的脚本)写 request 文件到~/Library/Application Support/Opsnook/ClaudeHooks/→ClaudeCodeAdapter轮询到 →ClaudeHookRequestParser解析 → snapshot.pendingApproval →PanelView.ApprovalCard→ 点按钮 →onApprovalDecision→AppDelegate闭包 →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,即entrypoint为claude-desktop的会话没有审批通道。两者症状相同、成因不同。
- 调用链:
PanelRow点击 →onSelect→NotchWindowController.select(session)→session.focusTarget(适配器生成)→TerminalFocuser.focus(target): deepLink(codex://threads/…)→ AppleScript 定位标签页(受canScriptTerminals门控,App Store 版关闭) →ApplicationFocuserApp 级激活兜底。成功且未固定 → 面板收起。 不使用辅助功能 API:App Review Guideline 2.4.5 禁止将其用于窗口管理等非无障碍用途。 - 最可能改:目标生成 = 各适配器;匹配与降级 =
OpsnookIntegration/Focus/TerminalFocuser.swift(supportedBundleIDs)。 - 验证:
TerminalFocuserTests/ApplicationFocuserTests;真机逐个宿主点行确认。
- 入口:底栏「活动」Tab。单击条目 → 面板内 transcript 预览并定位到那条消息;右键 → 「回跳到会话」。
- 调用链:
SessionReducer派生事件 →store.events()→ActivityModel.record(倒序、7 天、300 条) →ActivityView。预览:PanelView.presentPreview→TranscriptPeekModel.present→AppContainer.loadTranscript(按品牌分发适配器)→TranscriptPeekView(滚动定位 anchor)。 - 最可能改:行样式/事件文案 =
ActivityView.swift+Formatting.swift(eventLabel/eventSymbol); 保留策略 =ActivityModel;预览气泡 =TranscriptPeekView.swift。 - 验证:
ActivityModelTests/SessionTranscriptTests;真机点条目看定位高亮。
- 调用链:切到 Tab →
UsageView.onAppear → UsageModel.refresh()→AppContainer.usageReport()→UsageReader(扫/.claude、/.codex、~/.gemini 近期文件) →UsageAggregator→UsageSnapshot→ UsageView 渲染(配额条 / 成本卡 / 7 日柱状 / 按 Agent)。 - 最可能改:视图 =
UsageView.swift;聚合 =UsageAggregator.swift;扫描 =UsageReader.swift; 单价表 =OpsnookCore/Models/ModelPricing.swift;格式化 =Formatting.swift(compactTokens/money)。 - 验证:
UsageTests/UsageParserTests;真机切 Tab 看是否与~/.codexrate_limits 一致。
- 调用链:切 Tab →
NotchRootView.onSelectTab里workspace.refresh(sessions:)→AppContainer.workspaceSnapshot(GitWorkspaceService.snapshot跑 git 命令 + transcript 改动归因) →WorkspaceModel.snapshot→WorkspaceView。操作(暂存/提交/推送/回滚/移动文件)都是 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 节。
- 入口:底栏齿轮 → 面板切到设置页;或 ⌘,(Settings Scene 回退窗口)。两处正文同源
(
OpsnookSettingsContent,App/OpsnookApp.swift:23)。 - 内容:Agent 数据访问授权列表(13 行:7 家已接入可授权/撤销 + 6 家「敬请期待」占位)、 通用(登录启动 / ⌥⌘H 提示 / 语言 / 退出)。
- 调用链(授权):点「授权」→
grantRow→DirectoryAccessModel.grant(NSOpenPanel 预定位隐藏目录) →AppContainer.grantDirectoryAccess(存书签)→ 校验数据标记 →onReload→AppDelegate.reloadAfterGrant→container.reload()(重建适配器管线)。 - 最可能改:一切设置页 UI 与文案 =
App/OpsnookApp.swift+App/Resources/Localizable.xcstrings; 授权目录定义 =AgentDirectoryAccess.swift的AgentDirectoryKey。 - 验证:
AgentDirectoryAccessTests;真机撤销→重新授权看会话消失/出现。
- 说明:不使用系统通知,也不发声。完成时收起态的完成指示做一次 1.4s 放大发光;
审批/待输入徽标脉冲。专注模式(无独立按钮时由代码
onToggleFocusMode驱动,当前 UI 入口 为收起态徽标是否脉冲的静默开关)静默这些视觉提醒,状态色仍显示。 - 代码:
OpsnookIntegration/Notifications/NotificationCoordinator.swift(cue 生成、UserDefaults 持久化); 展示 =NotchCollapsedView.CompletionIndicator+ConditionalPulse。 - 验证:
NotificationCoordinatorTests;OPSNOOK_DEMO_DECAY=1看完成闪烁。
- ⌥⌘H:
App/Windows/GlobalHotKey.swift(Carbon,键码kVK_ANSI_H+optionKey|cmdKey, init 参数即改键处)→toggleManual()。 - 登录项:设置页开关 →
LaunchAtLoginController.setEnabled→SMAppService.mainApp.register/unregister。 - 固定:表头图钉 →
NotchUIState.pinned→recompute()(pinned 时移出不收起)。
- 面板/时间轴文案:
OpsnookL10n.string(key, locale:)查OpsnookUI/Resources/{en,zh-Hans}.lproj/Localizable.strings。 - 设置页文案:
settingsLocalized(key, language)查 App targetLocalizable.xcstrings。 - 语言切换:设置页 Picker →
AppLanguageController.select(UserDefaultsOpsnook.appLanguage) →.environment(\.locale, …)即时生效(NotchRootView:139、时间轴 SessionTimelineView:45)。 - Agent 产出内容(任务标题等)一律原样不翻译。
- 入口:「会话」页会话行 hover 时行尾浮现「时间轴」胶囊按钮(仅 Claude/Codex/Qoder/WorkBuddy/Antigravity
且会话文件为 .jsonl 时出现,判定 =
SessionTimelineSupport.canOpen)。 - 结果:1280×820 深色 NSWindow(每会话一扇,重复打开前置复用),泳道时间轴: 15 类事件卡、同类堆叠、两级收起、subagent 分叉泳道、搜索淡化、类型筛选、缩放平移、 缩略图导航、事件详情(关键字段+完整 JSON+上下文)、PNG 导出。
- 调用链:hover pill →
onOpenTimeline→AppDelegate.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;真机对拍真实会话。
- 入口:「会话」页无任何会话时显示「连接你的 Agent」空态 + 发现列表 + 重扫按钮。
- 调用链:
AppDelegate启动/重扫 →container.discoverAgents()→registry.detectAll()(并发 detect)→AgentDiscoveryModel→PanelView.emptyState/discoveryRow。 Claude 特判:hooks 未装时 result 变为 needsSetup +installHooks/copyHookConfiguration动作 (AppContainer.discoverAgents:358)。 - 验证:撤销全部授权后看空态;
CodexAdapterTests里有 registry 检测测试。
通用约定(适用于本节全部条目,条目内不再重复):
- 深色恒定:整个刘海面板强制
.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=…直达该页肉眼核对。
- 对应组件:
AgentIconView(OpsnookUI/Notch/AgentIconView.swift,全文 35 行) - 结构:圆形底色
brand.avatarFill(Claude#211611/Codex#151618/Gemini#101827/其余#17181A, 定义在 AgentBrandIcon.swift:104avatarFill)+ 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)。
- 对应组件:
OpsnookBrandMark(OpsnookUI/Brand/OpsnookBrandMark.swift);图源Resources/BrandIcons/opsnook-brand.png,缺资源时降级 SF Symbol "hexagon" 琥珀色。 - 使用处:面板表头(PanelView.header)、工作区表头、⌘, 设置窗口标题行。
- 对应组件:
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。
- 对应组件:
PulseModifier/.opsnookPulse()(Waveform.swift:190)。 TimelineView 0.15s 步进,opacity = 0.7+0.3·sin。专注模式开启时调用方直接不加此 modifier (NotchCollapsedView 的ConditionalPulse(active: !notificationsMuted))。
文件:OpsnookUI/Notch/NotchCollapsedView.swift(整个收起态只有这一个视图文件)+
窗口几何 App/Windows/NotchWindowController.swift updateFrame() + OpsnookCore/Layout/NotchLayout.swift +
宽度预算 OpsnookUI/DesignSystem/OpsnookMetrics.swift。
- 所在位置:主屏顶部居中,覆盖菜单栏中段,与硬件刘海融合。
- 显示条件:App 运行期间恒显示(
panel.orderFrontRegardless(),加入所有 Spaces、全屏辅助层)。 - 尺寸:高度 = 菜单栏高度 −
OpsnookMetrics.collapsedHeightReduction(当前 0, OpsnookMetrics.swift:37,这是调整收起态高度的唯一入口,消费处 NotchWindowController.swift:611collapsedHeight = 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(宽度预算); 真机看有/无刘海两种屏。
- 位置:pill 左端,水平排列、相邻重叠 6pt(
HStack(spacing: -6))。 - 显示条件:存在状态为 needsApproval/waitingInput/error/running/idle 的会话
(
AgentStatus.appearsInNotchLeftWing,done 不显示)。按 Agent 去重、 按(状态权重↑、最近变化↓)排序取前 4(AggregateBuilder.build)。 - 每个图标:18pt 圆 = brand.avatarFill 底 +
AgentBrandIcon10.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(否则窗口宽度算不准会裁切)。
- 位置:pill 右端。取
aggregate.highestPriority(全部会话状态最小 sortWeight)分支渲染 (NotchCollapsedView.rightWing:61):- 空闲/无会话:3 个 4pt 白 22% 暗点(spacing 4)。
- 运行中:绿波形(h13)+ 运行数字(11pt medium 绿,仅 runningCount>0)。
- 完成:
CompletionIndicator(checkmark.circle.fill + "完成"common.completed,10pt 绿; 常态 72% 透明度;收到新 completionCueID 且未静音未减动效时 0.18s 放大到 1.08 + 绿光晕, 650ms 后 0.28s 归位,NotchCollapsedView.swift:124-160)。 - 待批准:琥珀胶囊徽标,仅 hand.raised.fill 图标无文字(badge(text:nil)),前景 onApproval, 未开专注模式时脉冲。具体数量只在展开面板看。
- 待输入:天蓝胶囊徽标 questionmark +
notch.waiting_input("%d 待输入"),前景#06283A,脉冲。 - 出错:6pt 红点 +
notch.errors("%d 出错")红字,不脉冲。
- 徽标通用样式:图标 9pt bold + 文字 10pt medium,padding 横 7 竖 2,Capsule 底。
- 修改指南:改某状态样式 → rightWing 对应 case;改优先级顺序 →
AgentStatusrawValue; 改文案 → strings 的notch.*/common.completed;改完成闪烁时长 → CompletionIndicator.task 内 的 0.18/650ms/0.28 三个数字,cue 持续时长在NotificationCoordinator(cueDuration: 1.4s)。 - 关联影响:右翼形态变化 → 宽度预算
collapsedRightWingWidth需同步,否则文字被裁。
文件:OpsnookUI/Panel/PanelView.swift(表头/底栏/内容路由)+ NotchRootView.swift(展开分支)
- 窗口尺寸动画
NotchWindowController.swift。
- 展开态结构(NotchRootView.swift:80):
VStack{ 顶部刘海条(NotchCollapsedView, 高=menuBarHeight, 黑底,硬件刘海盖在这里) ; PanelView }。 - 宽度:
selectedTab.panelWidth,工作区 440pt,其余 420pt(PanelView.swift:948PanelTab.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 注释)。
- 显示条件:
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")。
- 有会话:
- 位置:表头右侧、图钉左边。图标
line.3.horizontal.decrease13pt。 - 显示条件:
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 陷阱)。
- 图标:
pin.fill(已固定,琥珀) /pin(未固定,textSecondary),13pt(PanelView.swift:162)。 - 交互:点击 →
onTogglePin→NotchUIState.pinned.toggle()→ 固定后鼠标移出不收起。 - 初始值:false,不持久化(重启即复位)。
- 同款按钮在工作区表头右侧复制了一份(WorkspaceView.repositoryHeader:364)。
- 位置:面板最底部,高约 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):
gearshape14pt,选中设置页时同款高亮;.help("common.settings")。 - 活动页筛选态的特殊布局(footer:822):左侧变为「筛选清除按钮」
(漏斗图标+
activity.filter_summary("%d / %d 个 Agent · 清除"),琥珀,点击clearAgentFilter), 四个 Tab+齿轮整体靠右。 - 交互后变化:
onSelectTab→state.panelTab(枚举PanelTab)→ content 切页 + 关浮层/退设置详情/ 关消息预览(PanelView.onChange:126)+ 切到工作区时触发workspace.refresh。 - 修改指南:加/删 Tab →
PanelTab枚举 + footer 两个分支 +panelWidth;改选中样式 → tabButton。
文件:列表框架 PanelView.swift(agentsContent/agentsList/groupHeader/emptyState/discoveryRow)、
单行 PanelRow.swift、子行 SubagentRow.swift、审批卡 PanelView.ApprovalCard、
问题卡 QuestionCard.swift。数据 = OpsnookViewModel.grouped()。
- 顺序恒定:需要处理 → 运行中 → 最近完成 → 空闲(
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)。
- 文件:
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 + stringsstatus.*。
- 显示条件:hover 该行 且
SessionTimelineSupport.canOpen(session)(品牌 ∈ claude/codex/qoder/workbuddy/antigravity 且 sessionResourceURL 为 .jsonl, SessionTimelineModel.swift:10)。 - 样式(PanelRow.timelinePill:81):
chart.xyaxis.line10pt +timeline.open_button("时间轴") 11pt medium,白 10% Capsule。以透明态常驻布局(ZStack 覆盖状态区,宽度取两者较大值), 悬停只切透明度,行内零位移(PanelRow.swift:55-65 注释)。 - 交互:点击 →
onOpenTimeline(session)→ 独立时间轴窗口(§3.8)。 - 提示:
.help("timeline.open_menu")("查看会话时间轴")。
- 位置:行尾状态区左侧。显示条件:
session.subagents非空。 - 样式(PanelRow.forkPill:111):
arrow.triangle.branch9pt + 计数(有活跃时绿色活跃数/总数, 全结束时仅灰总数)+ 展开方向 chevron(down=已展开/right=收起)7pt。白 8%(活跃)/5% Capsule。 - 交互:点击 →
PanelView.toggleSubagents(手动覆盖存subagentExpandOverrides:[SessionID:Bool], 视图 @State,切页/重启丢失)→ 展开/收起子行 +onContentResize()。 - 默认展开规则(PanelView.subagentsExpanded:552):无手动覆盖时「有 running subagent 即展开, 全部结束自动收起」。
- 无障碍:
subagent.count_accessibility("%d 个子代理")。
- 文件:
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)。
- 位置:对应会话行正下方。显示条件:
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。
- 位置:会话行下方(与审批卡同位)。显示条件:
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。
- 显示条件:
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 移除,代码保留待恢复)。
- 内容自然高 ≤ 上限:整列直接定高(无滚动);> 上限:包
ScrollView固定为maxContentHeight内部滚动(PanelView.agentsList:535 的 measuredContentHeight 机制, PreferenceKeyPanelContentHeightKey)。活动/用量/工作区同构(各自的 HeightKey)。
文件:ActivityView.swift(列表+行)、TranscriptPeekView.swift(预览)、
ActivityModel.swift(数据)、TranscriptPeekModel.swift(预览状态机)、
浮层在 PanelView.agentFilterPopover:891。
ActivityView.sectionHeader:70:11pt semibold textTertiary。分桶纯函数OpsnookFormat.dayBucket(Formatting.swift:156),文案activity.today/yesterday/earlier。
- 文件:
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):
- subagent 事件:⑂ 图标 + 类型标签(
event.subagent_started/completed/failed)+子代理名 · 任务摘要; userTerminated:「你终止了会话」event.terminated_action+ 「已终止」灰徽标 (白 8% 底圆角 6,terminatedBadge:242);- 其余:类型 SF 图标 9pt + 类型标签 11pt semibold(
OpsnookFormat.eventLabel/eventSymbol, 配色 eventColor:276:完成/已允许绿、出错/已拒绝红、请求审批琥珀、已开始/子代理启动天蓝、 已终止灰)+ 本轮用户消息(event.messageContent,回退 title)。
- subagent 事件:⑂ 图标 + 类型标签(
- 次行(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.circlepath22pt +activity.empty+activity.empty_detail。 - 数据边界:事件仅内存保留(不持久化,重启清空),7 天 / 300 条上限(ActivityModel:27)。
- 位置:面板右上(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)。
- 触发:单击活动条目 →
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)。
文件:UsageView.swift 全文 + UsageModel.swift(按需加载)+
数据链 UsageReader.swift → UsageAggregator.swift → UsageSnapshot.swift。
- 加载态:spinner +
usage.loading("正在统计用量…")。切到 Tab 才触发一次model.refresh()(onAppear:28,inFlight 防重入;无自动定时刷新,重进 Tab 会再拉)。 - 空态:
chart.bar.xaxis22pt +usage.empty+usage.empty_detail。
- 显示条件:
snapshot.quotas非空,只有解析到真实 rate_limits(目前 = Codex)才显示, 不放假数据(诚实数据原则,UsageView 头注释)。 - 每行(quotaRow:112):AgentIconView(26) +
Agent 名 + 套餐名+ 右侧百分比 + 次行窗口标签 · 重置时间 [· 接近上限提示](quotaWindowLabel/quotaReset+ ≥85% 时usage.advice_high)+ 5pt 进度条(≥90% 红 / ≥70% 琥珀 / 其余绿,quotaColor:284)。
- 标题:有可计价成本时
usage.section.cost+"· 估算"usage.estimate,否则usage.section.tokens。 - 卡片(costCards:148):计价时 3 张(今日$ / 本周$ / Tokens 今日),否则 2 张(今日/本周 Tokens)。
bgElevated 圆角 10,值 17pt medium。金额
OpsnookFormat.money("$%.2f"),tokencompactTokens("1.2K/1.2M")。 - 底部免责:
usage.estimate_note(仅计价时显示)。
- 7 根圆角 3 柱,高 34 内按当日 token/最大值比例,最矮 3pt;今日柱琥珀,其余白 14%。
柱下标签 = 单字星期(
weekdayNarrow),今日显示usage.today。无 hover/tooltip。
-
每行:AgentIconView(22) + 名称 + 右侧本周 token(+计价时次行本周成本)。
-
修改指南:加一家 Agent 的用量 →
UsageReader(新扫描)+UsageAggregator; 改单价 →ModelPricing.swift;改卡片/柱状样式 → UsageView 对应函数。 -
验证:
UsageTests(聚合/计价)、UsageParserTests(各家解析)。
文件:WorkspaceView.swift(1486 行,全部 UI)+ WorkspaceModel.swift(状态与操作)+
GitWorkspaceService.swift(git 子进程)+ WorkspaceSnapshot.swift(模型)。
面板宽 440。此页有自己的表头(公共表头隐藏)。
snapshot.disabledReason != nil→ 阻塞态(lock.fill +workspace.unavailable+ 原因文案; needsAuthorization 且沙盒 → 「授权项目目录…」按钮 →model.authorize()→ AppDelegate NSOpenPanel(预定位到会话项目共同父目录,WorkspaceModel.suggestedWorkspaceRoot:129))。- 无仓库 → 空态(
workspace.empty/empty_detail+ 沙盒下「重新选择工作区目录…」按钮)。 - 有仓库未选中 → 加载态(
workspace.loading)。 - 正常 → 表头 + (文件视图 | Git 视图 | 非 git 仓库占位)。
- 占位态右上角均叠刷新按钮(placeholder:41,加载中转圈禁用)。
- 行 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 琥珀)+ 同步/推送按钮。
- 三态:已同步(checkmark
workspace.synced,灰,禁用)/ 可推送(arrow.upworkspace.push, 琥珀实底 onApproval 字)/ 推送中(自绘WorkspaceInlineSpinner+workspace.pushing,禁用)。 - 交互:点击 →
model.push(),立即置 isPushing → git push → 成功后同步重载快照再解除 loading(避免按钮闪回,见 WorkspaceModel.push:489 注释);失败 → errorRow。
- 显示条件:
repository.hasConflicts。红 12% 底圆角 9 +arrow.triangle.merge+workspace.conflict_banner("%d 个冲突文件。推送前需要先在对应仓库解决冲突。")。
- 每行:展开箭头(仅文件夹,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,拖到文件夹行onDrop→model.move(_:into:)→GitWorkspaceService.moveItem(真实 mv)→ 刷新树; - 右键 → 「在访达中显示」
workspace.reveal_in_finder→activateFileViewerSelecting。
- 文件夹单击 →
- 空文件夹:
workspace.empty_folder("此文件夹为空")。 - 滚动高度:
workspaceScrollHeight = max(280, min(maxContentHeight-96, 560))(:299)。
结构自上而下(gitBody:123,整体一个内层 ScrollView): 未提交标题 → 已暂存区 → 提交框 → 未暂存区 → 提交时间线。
- pencil.line +
workspace.worktree_title,琥珀。工作树干净时下挂绿勾 +workspace.clean。
workspace.staged_count("已暂存 · %d",绿)/unstaged_count(灰) + 右侧琥珀动作 「取消全部暂存」unstage_all/「全部暂存」stage_all→model.unstageAll()/stageAll()(乐观更新:先本地翻转 isStaged 再跑 git,setStaged:443)。
- 布局:展开 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)。
- 状态:加载中(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兜底)。
- 显示条件:
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 注释)+ 补充说明(可选)。 能输入的前提:NotchPanelcanBecomeKey=true+becomesKeyOnlyIfNeeded(NotchWindowController.swift:14 注释);输入期间面板是 key 窗口 → 不会因鼠标移出收起 (recompute:581)。 - 「提交后推送」勾选(无 upstream 禁用)+ 提交按钮(标题 =
commit_files("提交 %d 个文件") 或commit_and_push;可提交 = 有已暂存 + 信息非空(canCommit),否则灰禁用)。 - 提交成功:清空两框与勾选 → refresh;失败 → errorRow(errorText:1429 映射
workspace.error_*)。
- exclamationmark.circle.fill 红 + 错误文案 + 右侧 关闭(
model.dismissError)。
- 按 今天/昨天/更早 分组(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 节。
正文组件 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)。
- 显示条件:
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):
- 未授权:「授权」小按钮 →
grantRow,按需依次弹 NSOpenPanel(TRAE 两步:会话目录+运行时 目录,面板 message 分步标注;用户取消某步即停止)。面板预定位到目标目录上级、显示隐藏文件 (DirectoryAccessModel.grant:57)。选错文件夹(有数据标记校验的 Qoder)→ 书签已存但 显示 mismatch 错误文案,行保持未授权可重选。 - 已授权:绿「已授权 」,hover 变红「撤销授权 」,点击撤销并重建管线
(
AgentAccessGrantedControl,OpsnookApp.swift:470;该 Agent 会话即刻从面板消失)。 - 占位:「敬请期待」灰 Capsule(
settings.agent_coming_soon)。
- 未授权:「授权」小按钮 →
- 错误行:
access.lastError红字(授权失败/选错文件夹)。
settings.general标题下三行 settingRow(白 4.5% 圆角 10 卡片):- 登录时启动:Toggle(.switch)→
LaunchAtLoginController.setEnabled; detail 按状态显示settings.launch_disabled/enabled/requires_approval/not_found; 失败时底部红字 errorMessage。 - ⌥⌘H 提示:右侧只读 "⌥⌘H" 键帽样式(白 8% 圆角 6),改快捷键不在这里,在 GlobalHotKey init 参数(App/Windows/GlobalHotKey.swift:15),这行只是展示文案。
- 界面语言:Picker(宽 152)三项
language.system/simplified_chinese/english→AppLanguageController.select→ UserDefaults + 全 UI locale 即时切换。 - 退出:
settings.quit_button按钮 →NSApp.terminate。
- 登录时启动:Toggle(.switch)→
- 修改指南:加一行设置 → 复制一个
settingRow(title:detail:){trailing}+ xcstrings 加文案。
文件:窗口壳 App/Windows/TimelineWindowController.swift;根视图/工具栏/侧栏
OpsnookUI/Timeline/SessionTimelineView.swift;画布/卡片 TimelineCanvasView.swift;
详情 TimelineDetailPanel.swift;ViewModel+布局 SessionTimelineModel.swift;
视觉 token TimelineStyle.swift;导出 TimelineImageExporter.swift。
- 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 注释)。
从左到右(样式统一:白 5% 圆角 8 + 白 10% 描边小按钮):
- 侧栏开关
sidebar.left/leading→model.sidebarVisible.toggle()。 - 横版|竖版分段(
timeline.orientation_horizontal/vertical)→ 切 orientation + resetView + centerOnSelected。 - ‹ 错误 / 错误 ›:
jump(toError:true, direction:∓1),循环跳上/下一个错误事件。 - 工具调用:
jump(toError:false)跳下一个工具类事件。 - 「分组框」开关(groupFrameToggle:142):
model.showGroupFrames(默认 true), 展开的同类组画虚线框;激活白 14% 高亮。 - 缩放组:−/+步进(±0.15)+ Slider(范围
zoomRange 0.55…1.45)+ 百分比只读。 - 刷新(refreshButton:188):重新解析 session 文件,保留视图状态只增量补事件 (model.refresh:102),进行中转圈禁点。
- 导出图片
photo.badge.arrow.down→TimelineImageExporter.export:NSSavePanel 选路径 → ImageRenderer 离屏渲染完整时间轴(不受当前平移缩放影响,含标题条)→ PNG 写盘 → 访达选中;超大画布自动降 scale(像素预算 8000 万,:93);失败弹 NSAlerttimeline.export_failed。 默认文件名Timeline-<标题40字>-<yyyyMMdd-HHmmss>.png。 - 重置视图
scope:zoom=1 + 回到起点并居中选中。
- 头部:
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)。
- 背景: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);下方显示 横版/竖版 + 可见计数。
- 头部:类型圆标(图标+类型色描边)+ 类型名 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 事件钉死在轴上不参与堆叠。
- 显示条件:点卡片 toggle(
detailVisible)。头部 "事件详情" + 。 - 内容:图标+标题(可选中)→ 动作行【完整 JSON/收起 JSON、复制 JSON(点击后 1.6s 显示"已复制")、
打开文件(有 filePath 时)】→ 基础字段(时间/Agent/状态/耗时)→ 文件清单(多文件 patch,
每行可点 = 在编辑器打开,右侧 +n/−n)→ 关键字段 facts(label 走
timeline.fact.*本地化, 值等宽可选中)→ [完整 JSON(prettyPrinted,内滚 300pt)] → 上下文窗口(前后各 5 事件, 点击跳转选中并居中)。
每页给出:布局层级树(→ 渲染组件)+ 进入/离开行为 + 数据来源。收起态、展开面板与时间轴的截图见
docs/FEATURES.md第 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)。每次会话变化整体重算。
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=本地控制器直读。
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从表移除(模型随窗释放,无持久化, 重开重新解析)。
Settings Scene(OpsnookApp.body)
└─ OpsnookSettingsWindow(宽 420, bgPanel, 强制深色)
├─ 标题行: OpsnookBrandMark + "Opsnook" + "设置"
├─ Divider
└─ OpsnookSettingsContent(不含 Agent 数据访问区,未注入 directoryAccess)
- 入口:
NotchRootView(面板)、SessionTimelineView(时间轴窗口)、OpsnookSettingsContent(App 层)。 - 职责:纯渲染 + 把用户操作转成闭包回调;不直接碰 Adapters/Integration。
- 关键文件:Panel/(五个 Tab)、Notch/(收起态+头像)、Timeline/、DesignSystem/(token)、 Localization/OpsnookL10n、ViewModels/*。
- 被谁调用:App 层 NotchWindowController/TimelineWindowManager 装配。
- 修改影响:视图文件互相独立,改一个 Tab 不影响其他;唯一全局耦合是「内容高度变化必须 onContentResize / 被 withObservationTracking 跟踪」(见 §8-R1)。
- 没有路由框架。全部"导航"就三种:
NotchUIState.panelTab切页、TranscriptPeekModel.state覆盖内容区、独立 NSWindow(时间轴、⌘, 设置)。 NotchWindowController是最复杂的单文件:hover 判定、窗口 frame 动画、8 个withObservationTracking订阅(model/discovery/notifications/activity/usage/workspace/peek)。 加任何影响面板高度的新状态,都要加进对应 observe*。
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)。
- 协议
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.vscdb 的 aicoding.questTaskListSnapshot |
单一权威键(标题/状态/cwd)+ 进程闸门 | L2 |
- 共用工具:
SessionFileFingerprint(mtime/size 免重复解析)、ShellCommandClassifier(命令→时间轴 15 类)、GitHead(读分支)、AdapterLimits(标题截断)、AgentCatalog(品牌/颜色/kind 唯一来源)。 - 时间轴解析器(5 个
*TimelineParser):JSONL →SessionTimeline(事件/泳道/facts/rawLines), 工具调用按 id 配对回填状态与耗时。各家 schema 的依据见docs/sessions_status_logic/。 - 纪律:适配器不得崩溃,解析失败一律优雅降级为空结果或维持现状。
- 无任何自有网络层。唯一出站 =
GitWorkspaceService.push的 git 子进程(entitlement network.client 就为它)。UsageReader 读的 rate_limits 是 Codex 落在本地文件里的数据。
见 §6.3 表。没有自建数据库;全部是 UserDefaults + 读外部 Agent 的文件/SQLite(只读)。
- 文件访问:
SecurityScopedBookmarkStore(UserDefaults 存书签、NSLock 保护 active 表、 stale 自动刷新)+AgentDirectoryAccess(key→真实目录、数据标记校验 hasUsableData)。 - 回跳:
TerminalFocuser(deepLink → AppleScript 标签页定位 → App 级激活;支持 Terminal/iTerm2/Warp/VSCode,见supportedBundleIDs)+ApplicationFocuser(App 级激活兜底)。 - Claude hooks:
ClaudeHooksInstaller,ensureRuntime()(启动时写 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 等加固)。
- 每个适配器一个轮询循环,在
start()内部启动,间隔由各适配器自定。已做事件驱动优化, 即 FSEvents 加按需鼠标监听,空闲 CPU 占用约为 0.1%。 store.runDecayLoop()每秒衰减。无 cron/推送。
- 日志:
NSLog("Opsnook: …")散布关键路径(回跳结果、审批回传、授权失败); DEBUG 布局日志写/tmp/hive_debug.log。无日志文件轮转、无上报(隐私红线)。 - 用户可见错误:工作区 errorRow(WorkspaceFailure 枚举→本地化)、设置页授权红字、 登录项红字、时间轴导出 NSAlert。
- 双语 strings ×2 处(见 F-L);
LocalizationTests保证中英 key 一致。 - 主题:面板/时间轴强制深色;唯一"跟随系统"的是 ⌘, 窗口外的系统控件描边。设计 token 全在 OpsnookColor/OpsnookMetrics/TimelineStyle。
- 资源:BrandIcons PNG(Lobe Icons,许可证同目录)、AppIcon.appiconset。
project.yml(XcodeGen)→Opsnook-AppStorescheme,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(纯函数级)。
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]
| 变量 | 类型/默认 | 写入 | 读取(影响的 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 徽标 | 子行展开 | 否 |
| 位置 | 键/路径 | 内容 | 读写代码 | 迁移风险 |
|---|---|---|---|---|
| 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(仅用户显式触发) | 谨慎:是用户自己的配置文件 |
- 会保存:语言、专注模式、目录/工作区授权书签、时间轴窗口位置、Claude hooks 配置。
- 纯临时(重启/切页丢失):图钉、当前 Tab、活动事件列表、筛选勾选、subagent 展开覆盖、
展开的 diff/提交、提交信息草稿、时间轴内全部视图状态。
- 想把某个临时态变持久:加 UserDefaults 读写(参照 AppLanguageController 模式)。
| 接口 | 请求方 → 实现 | 入参 → 出参 | 错误处理 |
|---|---|---|---|
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 |
通用流程:改 →
cd Packages/OpsnookKit && swift test→xcodegen generate(仅当改了 project.yml)→xcodebuild -scheme Opsnook-AppStore -configuration Debug-AppStore -derivedDataPath /tmp/OpsnookBuild build CODE_SIGNING_ALLOWED=NO→ 运行核对。
- 在
Packages/OpsnookKit/Sources/OpsnookUI/Resources/zh-Hans.lproj/Localizable.strings全文搜该中文 → 得到 key → 同步改en.lproj同 key。 - 搜不到 → 是设置页文案:Xcode 打开
App/Resources/Localizable.xcstrings搜; 仍搜不到 → 是 Agent 产出内容(标题/命令/消息),设计上不翻译不可改; 或是硬编码英文("Opsnook"、"⌥⌘H"、"vs %@" 等,直接在视图文件里搜字面量)。 - 验证:
swift test(LocalizationTests 查 key 对称)+ 切中英文各看一遍。 风险:只改一种语言 → 另一语言仍旧文案;LocalizationTests 只查 key 存在,不查语义。
- 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(资源存在性)+ 肉眼。
- 全局色 →
OpsnookColor.swift(改running即全 App 绿色统一变)。 - 品牌色 →
AgentCatalog.swift各AgentBrand(colorHex:)。 - 时间轴事件色/泳道色 →
TimelineStyle.swift。 - 几何/动画时长 →
OpsnookMetrics.swift(面板宽、圆角、喇叭口、收起高度、动画曲线全在此)。 - 单点字号/间距 → 对应视图文件的
.font(.system(size:))/.padding(…)(§3 每条目已给函数名)。 - 风险:改
OpsnookColor.approval会同时影响审批徽标/图钉高亮/筛选高亮/提交按钮等所有琥珀元素。
- §3 找到该按钮条目 → 得到「事件处理」闭包名(如 onSelect/onTogglePin/onApprovalDecision)。
- 行为的真正实现沿注入链上溯:视图 → NotchRootView → NotchWindowController.configureRoot (App/Windows/NotchWindowController.swift:474,所有面板回调在这里接线)→ AppDelegate(:100 起)。
- 改实现通常在 NotchWindowController 私有方法或 AppContainer。
- 加:在目标视图文件对应 ViewBuilder 里加;若它会改变面板高度且由 @State 驱动 →
必须调用
onContentResize()(参照 toggleSubagents)。 - 删:直接删视图代码;检查其闭包参数是否还有其他调用方(Xcode ⇧⌘F)。
- 验证:展开/收起/换页各来一遍,确认底栏不被裁、无残留空白。
- 语言默认 →
AppLanguageController.init的?? .system。 - 专注模式默认 →
UserDefaultsFocusModePreferenceStore.loadFocusModeEnabled(bool 默认 false)。 - 默认 Tab →
NotchUIState.panelTab = .agents。 - 完成衰减 8s →
EngineConfig.doneDecay。 - 活动保留 7 天/300 条 →
ActivityModel.init默认参数。
- Tab 顺序/组成 →
PanelView.footer两个分支 +PanelTab枚举。 - 切页副作用(关浮层等)→
PanelView.body.onChange(of: selectedTab)+NotchRootView.onSelectTab闭包。
- 本项目无 HTTP。等价问题「改某 Agent 数据目录」→
AgentDirectoryKey.defaultDirectoryName(AgentDirectoryAccess.swift:16)+ 设置页引导文案;「改 git 命令参数」→GitWorkspaceService对应方法;「改返回结果展示」→ 对应 Snapshot 模型 + 视图。
- 新偏好:参照
AppLanguageController(UserDefaults + @Observable)。 - 改书签 key 前缀:
SecurityScopedBookmarkStoreinit,会丢用户已有授权,慎改。
- 能力矩阵 →
Capabilities.appStore(唯一预设;想开 AppleScript 要加 apple-events entitlement)。 - 目录授权流程 → DirectoryAccessModel(UI)/ AgentDirectoryAccess(解析)/ SecurityScopedBookmarkStore(存取)三层。
- 沙盒 entitlement →
App/Entitlements/Opsnook.AppStore.entitlements(改完必须真机验证沙盒行为)。
App/Windows/GlobalHotKey.swiftinit 默认参数:keyCode: UInt32(kVK_ANSI_H)、modifiers: UInt32(optionKey | cmdKey)(Carbon 键码表在 Carbon.HIToolbox.Events)。- 同步改设置页展示文案:"⌥⌘H" 硬编码在 OpsnookApp.swift:62 + xcstrings 的
settings.toggle_panel_detail。
- 收起态高度 →
OpsnookMetrics.collapsedHeightReduction(实例见 commit 34b59ed,就改这一个常量)。 - 面板宽 →
OpsnookMetrics.panelWidth/workspacePanelWidth。 - 内容最大高 → NotchWindowController.updateFrame 里
panelMaxContentHeight的-96-16估算。 - 时间轴窗口初始/最小尺寸 → TimelineWindowController.swift:32/39。
- 没有浅色主题:面板强制深色(NotchRootView:138)。若要跟随系统外观,需要移除强制 colorScheme, 并把 OpsnookColor 全表改为深浅双值的动态色。这项改动工作量较大。
PanelTab加 case +panelWidth;2. footer 加 tabButton;3. content switch 加分支新视图;- 需要数据 → 新 @Observable Model,在 AppDelegate 创建并沿 NotchWindowController→NotchRootView→ PanelView 注入;5. 高度会变 → NotchWindowController 加 observeXxxChanges;
- 文案进两份 strings;7. swift test + 真机。
- 某 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 案例)。
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(没有授权区)是有意为之。
待确认(无法从代码完全证实,已尽力核查):
- Git 页提交时间线的 Agent 过滤(
WorkspaceModel.excludedAgentIDs/visibleCommits) 在当前 WorkspaceView 中未找到 UI 入口(strings 有workspace.filter_all等遗留 key), 疑为旧版入口被移除、能力保留。查看过 WorkspaceView 全文与 PanelView,未见调用workspace.toggleAgentFilter的视图。 - 专注模式(月亮图标)在当前 PanelView 表头没有按钮(FEATURES.md 1.10 提到月亮图标,
现码只有漏斗+图钉;
onToggleFocusMode闭包与focus.enter/exit文案仍在), 疑为改版中移除入口。功能本体(NotificationCoordinator)仍工作,但用户当前无法从 UI 切换。 AgentDescriptor/DetectionMethod/AgentSetupAction等发现向导模型未逐行读 (AgentDescriptor.swift),§F-N 行为依据 PanelView/AppContainer 调用侧代码。- 五个 TimelineParser、各 SessionParser 的逐行为文档化程度止于 §5.4 表,每家内部判定细节 以 docs/sessions_status_logic/ 与测试 fixture 为准,本文不复述以免与真源漂移。
- 本文档未内嵌带编号标注的截图。
docs/FEATURES.md第 1 节的三张截图与第 4 节有对应关系, 但未做逐元素编号图,需要真机重截并标注,列为后续任务。
- 已遍历所有"页面/窗口":收起态、展开面板(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。