mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-18 21:40:53 +00:00
* fix(macos): show sessions after controls in tray menu When many sessions are active, the injected session rows push the toggles, action buttons, and settings items off-screen, requiring a scroll to reach them. Change findInsertIndex and findNodesInsertIndex to anchor just before the separator above 'Settings…' instead of before 'Send Heartbeats'. This ensures the controls section is always immediately visible on menu open, with sessions appearing below. * refactor: extract findAnchoredInsertIndex to eliminate duplication findInsertIndex and findNodesInsertIndex shared identical logic. Extract into a single private helper so any future anchor change (e.g. Settings item title) only needs one edit. * macOS: use structural tray menu anchor --------- Co-authored-by: Brian Ernesto <bernesto@users.noreply.github.com> Co-authored-by: ImLukeF <92253590+ImLukeF@users.noreply.github.com>
169 lines
7.0 KiB
Swift
169 lines
7.0 KiB
Swift
import AppKit
|
|
import Testing
|
|
@testable import OpenClaw
|
|
|
|
@Suite(.serialized)
|
|
@MainActor
|
|
struct MenuSessionsInjectorTests {
|
|
@Test func anchorsDynamicRowsBelowControlsAndActions() throws {
|
|
let injector = MenuSessionsInjector()
|
|
|
|
let menu = NSMenu()
|
|
menu.addItem(NSMenuItem(title: "Header", action: nil, keyEquivalent: ""))
|
|
menu.addItem(.separator())
|
|
menu.addItem(NSMenuItem(title: "Send Heartbeats", action: nil, keyEquivalent: ""))
|
|
menu.addItem(NSMenuItem(title: "Browser Control", action: nil, keyEquivalent: ""))
|
|
menu.addItem(.separator())
|
|
menu.addItem(NSMenuItem(title: "Open Dashboard", action: nil, keyEquivalent: ""))
|
|
menu.addItem(NSMenuItem(title: "Open Chat", action: nil, keyEquivalent: ""))
|
|
menu.addItem(.separator())
|
|
menu.addItem(NSMenuItem(title: "Settings…", action: nil, keyEquivalent: ""))
|
|
|
|
let footerSeparatorIndex = try #require(menu.items.lastIndex(where: { $0.isSeparatorItem }))
|
|
#expect(injector.testingFindInsertIndex(in: menu) == footerSeparatorIndex)
|
|
#expect(injector.testingFindNodesInsertIndex(in: menu) == footerSeparatorIndex)
|
|
}
|
|
|
|
@Test func injectsDisconnectedMessage() {
|
|
let injector = MenuSessionsInjector()
|
|
injector.setTestingControlChannelConnected(false)
|
|
injector.setTestingSnapshot(nil, errorText: nil)
|
|
|
|
let menu = NSMenu()
|
|
menu.addItem(NSMenuItem(title: "Header", action: nil, keyEquivalent: ""))
|
|
menu.addItem(.separator())
|
|
menu.addItem(NSMenuItem(title: "Send Heartbeats", action: nil, keyEquivalent: ""))
|
|
|
|
injector.injectForTesting(into: menu)
|
|
#expect(menu.items.contains { $0.tag == 9_415_557 })
|
|
}
|
|
|
|
@Test func injectsSessionRows() throws {
|
|
let injector = MenuSessionsInjector()
|
|
injector.setTestingControlChannelConnected(true)
|
|
|
|
let defaults = SessionDefaults(model: "anthropic/claude-opus-4-6", contextTokens: 200_000)
|
|
let rows = [
|
|
SessionRow(
|
|
id: "main",
|
|
key: "main",
|
|
kind: .direct,
|
|
displayName: nil,
|
|
provider: nil,
|
|
subject: nil,
|
|
room: nil,
|
|
space: nil,
|
|
updatedAt: Date(),
|
|
sessionId: "s1",
|
|
thinkingLevel: "low",
|
|
verboseLevel: nil,
|
|
systemSent: false,
|
|
abortedLastRun: false,
|
|
tokens: SessionTokenStats(input: 10, output: 20, total: 30, contextTokens: 200_000),
|
|
model: "claude-opus-4-6"),
|
|
SessionRow(
|
|
id: "discord:group:alpha",
|
|
key: "discord:group:alpha",
|
|
kind: .group,
|
|
displayName: nil,
|
|
provider: nil,
|
|
subject: nil,
|
|
room: nil,
|
|
space: nil,
|
|
updatedAt: Date(timeIntervalSinceNow: -60),
|
|
sessionId: "s2",
|
|
thinkingLevel: "high",
|
|
verboseLevel: "debug",
|
|
systemSent: true,
|
|
abortedLastRun: true,
|
|
tokens: SessionTokenStats(input: 50, output: 50, total: 100, contextTokens: 200_000),
|
|
model: "claude-opus-4-6"),
|
|
]
|
|
let snapshot = SessionStoreSnapshot(
|
|
storePath: "/tmp/sessions.json",
|
|
defaults: defaults,
|
|
rows: rows)
|
|
injector.setTestingSnapshot(snapshot, errorText: nil)
|
|
|
|
let usage = GatewayUsageSummary(
|
|
updatedAt: Date().timeIntervalSince1970 * 1000,
|
|
providers: [
|
|
GatewayUsageProvider(
|
|
provider: "anthropic",
|
|
displayName: "Claude",
|
|
windows: [GatewayUsageWindow(label: "5h", usedPercent: 12, resetAt: nil)],
|
|
plan: "Pro",
|
|
error: nil),
|
|
GatewayUsageProvider(
|
|
provider: "openai-codex",
|
|
displayName: "Codex",
|
|
windows: [GatewayUsageWindow(label: "day", usedPercent: 3, resetAt: nil)],
|
|
plan: nil,
|
|
error: nil),
|
|
])
|
|
injector.setTestingUsageSummary(usage, errorText: nil)
|
|
|
|
let menu = NSMenu()
|
|
menu.addItem(NSMenuItem(title: "Header", action: nil, keyEquivalent: ""))
|
|
menu.addItem(.separator())
|
|
menu.addItem(NSMenuItem(title: "Send Heartbeats", action: nil, keyEquivalent: ""))
|
|
menu.addItem(NSMenuItem(title: "Browser Control", action: nil, keyEquivalent: ""))
|
|
menu.addItem(.separator())
|
|
menu.addItem(NSMenuItem(title: "Open Dashboard", action: nil, keyEquivalent: ""))
|
|
menu.addItem(.separator())
|
|
menu.addItem(NSMenuItem(title: "Settings…", action: nil, keyEquivalent: ""))
|
|
|
|
injector.injectForTesting(into: menu)
|
|
#expect(menu.items.contains { $0.tag == 9_415_557 })
|
|
#expect(menu.items.contains { $0.tag == 9_415_557 && $0.isSeparatorItem })
|
|
let sendHeartbeatsIndex = try #require(menu.items.firstIndex(where: { $0.title == "Send Heartbeats" }))
|
|
let openDashboardIndex = try #require(menu.items.firstIndex(where: { $0.title == "Open Dashboard" }))
|
|
let firstInjectedIndex = try #require(menu.items.firstIndex(where: { $0.tag == 9_415_557 }))
|
|
let settingsIndex = try #require(menu.items.firstIndex(where: { $0.title == "Settings…" }))
|
|
#expect(sendHeartbeatsIndex < firstInjectedIndex)
|
|
#expect(openDashboardIndex < firstInjectedIndex)
|
|
#expect(firstInjectedIndex < settingsIndex)
|
|
}
|
|
|
|
@Test func `cost usage submenu does not use injector delegate`() {
|
|
let injector = MenuSessionsInjector()
|
|
injector.setTestingControlChannelConnected(true)
|
|
|
|
let summary = GatewayCostUsageSummary(
|
|
updatedAt: Date().timeIntervalSince1970 * 1000,
|
|
days: 1,
|
|
daily: [
|
|
GatewayCostUsageDay(
|
|
date: "2026-02-24",
|
|
input: 10,
|
|
output: 20,
|
|
cacheRead: 0,
|
|
cacheWrite: 0,
|
|
totalTokens: 30,
|
|
totalCost: 0.12,
|
|
missingCostEntries: 0),
|
|
],
|
|
totals: GatewayCostUsageTotals(
|
|
input: 10,
|
|
output: 20,
|
|
cacheRead: 0,
|
|
cacheWrite: 0,
|
|
totalTokens: 30,
|
|
totalCost: 0.12,
|
|
missingCostEntries: 0))
|
|
injector.setTestingCostUsageSummary(summary, errorText: nil)
|
|
|
|
let menu = NSMenu()
|
|
menu.addItem(NSMenuItem(title: "Header", action: nil, keyEquivalent: ""))
|
|
menu.addItem(.separator())
|
|
menu.addItem(NSMenuItem(title: "Send Heartbeats", action: nil, keyEquivalent: ""))
|
|
|
|
injector.injectForTesting(into: menu)
|
|
|
|
let usageCostItem = menu.items.first { $0.title == "Usage cost (30 days)" }
|
|
#expect(usageCostItem != nil)
|
|
#expect(usageCostItem?.submenu != nil)
|
|
#expect(usageCostItem?.submenu?.delegate == nil)
|
|
}
|
|
}
|