diff --git a/CHANGELOG.md b/CHANGELOG.md index 662d3fc6d13..4592c1ae307 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -647,6 +647,7 @@ Docs: https://docs.openclaw.ai - Control UI/markdown fallback regression coverage: add explicit regression assertions for parser-error fallback behavior so malformed markdown no longer risks reintroducing hard-crash rendering paths in future markdown/parser upgrades. (#36445) Thanks @BinHPdev. - Web UI/config form: treat `additionalProperties: true` object schemas as editable map entries instead of unsupported fields so Accounts-style maps stay editable in form mode. (#35380, supersedes #32072) Thanks @stakeswky and @liuxiaopai-ai. - Feishu/streaming card delivery synthesis: unify snapshot and delta streaming merge semantics, apply overlap-aware final merge, suppress duplicate final text delivery (including text+media final packets), prefer topic-thread `message.reply` routing when a reply target exists, and tune card print cadence to avoid duplicate incremental rendering. (from #33245, #32896, #33840) Thanks @rexl2018, @kcinzgg, and @aerelune. +- macOS/tray menu: keep injected sessions and device rows below the controls section so toggles and action buttons stay visible even when many sessions are active. (#38079) Thanks @bernesto. - Feishu/group mention detection: carry startup-probed bot display names through monitor dispatch so `requireMention` checks compare against current bot identity instead of stale config names, fixing missed `@bot` handling in groups while preserving multi-bot false-positive guards. (#36317, #34271) Thanks @liuxiaopai-ai. - Security/dependency audit: patch transitive Hono vulnerabilities by pinning `hono` to `4.12.5` and `@hono/node-server` to `1.19.10` in production resolution paths. Thanks @shakkernerd. - Security/dependency audit: bump `tar` to `7.5.10` (from `7.5.9`) to address the high-severity hardlink path traversal advisory (`GHSA-qffp-2rhf-9h96`). Thanks @shakkernerd. diff --git a/apps/macos/Sources/OpenClaw/MenuSessionsInjector.swift b/apps/macos/Sources/OpenClaw/MenuSessionsInjector.swift index eb6271d0a8c..9f667cc6239 100644 --- a/apps/macos/Sources/OpenClaw/MenuSessionsInjector.swift +++ b/apps/macos/Sources/OpenClaw/MenuSessionsInjector.swift @@ -1099,38 +1099,33 @@ extension MenuSessionsInjector { // MARK: - Width + placement private func findInsertIndex(in menu: NSMenu) -> Int? { - // Insert right before the separator above "Send Heartbeats". - if let idx = menu.items.firstIndex(where: { $0.title == "Send Heartbeats" }) { - if let sepIdx = menu.items[..= 1 { return 1 } - return menu.items.count + self.findDynamicSectionInsertIndex(in: menu) } private func findNodesInsertIndex(in menu: NSMenu) -> Int? { - if let idx = menu.items.firstIndex(where: { $0.title == "Send Heartbeats" }) { - if let sepIdx = menu.items[.. Int? { + // Keep controls and action buttons visible by inserting dynamic rows at the + // built-in footer boundary, not by matching localized menu item titles. + if let footerSeparatorIndex = menu.items.lastIndex(where: { item in + item.isSeparatorItem && !self.isInjectedItem(item) + }) { + return footerSeparatorIndex } - if let sepIdx = menu.items.firstIndex(where: { $0.isSeparatorItem }) { - return sepIdx + if let firstBaseItemIndex = menu.items.firstIndex(where: { !self.isInjectedItem($0) }) { + return min(firstBaseItemIndex + 1, menu.items.count) } - if menu.items.count >= 1 { return 1 } return menu.items.count } + private func isInjectedItem(_ item: NSMenuItem) -> Bool { + item.tag == self.tag || item.tag == self.nodesTag + } + private func initialWidth(for menu: NSMenu) -> CGFloat { if let openWidth = self.menuOpenWidth { return max(300, openWidth) @@ -1236,5 +1231,13 @@ extension MenuSessionsInjector { func injectForTesting(into menu: NSMenu) { self.inject(into: menu) } + + func testingFindInsertIndex(in menu: NSMenu) -> Int? { + self.findInsertIndex(in: menu) + } + + func testingFindNodesInsertIndex(in menu: NSMenu) -> Int? { + self.findNodesInsertIndex(in: menu) + } } #endif diff --git a/apps/macos/Tests/OpenClawIPCTests/MenuSessionsInjectorTests.swift b/apps/macos/Tests/OpenClawIPCTests/MenuSessionsInjectorTests.swift index 186675f1eea..b1d01b9650e 100644 --- a/apps/macos/Tests/OpenClawIPCTests/MenuSessionsInjectorTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/MenuSessionsInjectorTests.swift @@ -5,7 +5,26 @@ import Testing @Suite(.serialized) @MainActor struct MenuSessionsInjectorTests { - @Test func `injects disconnected message`() { + @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) @@ -19,7 +38,7 @@ struct MenuSessionsInjectorTests { #expect(menu.items.contains { $0.tag == 9_415_557 }) } - @Test func `injects session rows`() { + @Test func injectsSessionRows() throws { let injector = MenuSessionsInjector() injector.setTestingControlChannelConnected(true) @@ -88,10 +107,22 @@ struct MenuSessionsInjectorTests { 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`() {