Skip to content

Commit 28a64a1

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/plugin-payload-unreadable-classification-109833
2 parents 8b1c51f + 7819130 commit 28a64a1

34 files changed

Lines changed: 1953 additions & 275 deletions

apps/macos/Sources/OpenClaw/AppNavigationActions.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@ enum AppNavigationActions {
1919
}
2020
}
2121

22-
static func openChat() {
22+
static func openChat(sessionKey: String? = nil, agentID: String? = nil) {
2323
NSApp.activate(ignoringOtherApps: true)
2424
Task { @MainActor in
25-
let sessionKey = await WebChatManager.shared.preferredSessionKey()
26-
WebChatManager.shared.show(sessionKey: sessionKey)
25+
let resolvedSessionKey = if let sessionKey {
26+
sessionKey
27+
} else {
28+
await WebChatManager.shared.preferredSessionKey()
29+
}
30+
WebChatManager.shared.show(sessionKey: resolvedSessionKey, agentID: agentID)
2731
}
2832
}
2933

apps/macos/Sources/OpenClaw/QuickChatController.swift

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ final class QuickChatController: NSObject, NSWindowDelegate {
3434
typealias MonitorClearer = (inout Any?) -> Void
3535
typealias HotkeyRegistrar = (@escaping () -> Void) -> Void
3636
typealias HotkeyRemover = () -> Void
37+
typealias ChatOpener = @MainActor (_ sessionKey: String?, _ agentID: String?) -> Void
3738

3839
static let shared = QuickChatController()
3940

@@ -48,6 +49,7 @@ final class QuickChatController: NSObject, NSWindowDelegate {
4849
@ObservationIgnored private let monitorClearer: MonitorClearer
4950
@ObservationIgnored private let hotkeyRegistrar: HotkeyRegistrar
5051
@ObservationIgnored private let hotkeyRemover: HotkeyRemover
52+
@ObservationIgnored private let chatOpener: ChatOpener
5153
@ObservationIgnored private let allowsHotkeyRegistrationInTests: Bool
5254
@ObservationIgnored private var panel: QuickChatPanel?
5355
@ObservationIgnored private var hostingView: NSHostingView<QuickChatView>?
@@ -82,6 +84,9 @@ final class QuickChatController: NSObject, NSWindowDelegate {
8284
hotkeyRemover: @escaping HotkeyRemover = {
8385
KeyboardShortcuts.removeHandler(for: .toggleQuickChat)
8486
},
87+
chatOpener: @escaping ChatOpener = { sessionKey, agentID in
88+
AppNavigationActions.openChat(sessionKey: sessionKey, agentID: agentID)
89+
},
8590
allowsHotkeyRegistrationInTests: Bool = false)
8691
{
8792
self.enableUI = enableUI
@@ -92,6 +97,7 @@ final class QuickChatController: NSObject, NSWindowDelegate {
9297
self.monitorClearer = monitorClearer
9398
self.hotkeyRegistrar = hotkeyRegistrar
9499
self.hotkeyRemover = hotkeyRemover
100+
self.chatOpener = chatOpener
95101
self.allowsHotkeyRegistrationInTests = allowsHotkeyRegistrationInTests
96102
super.init()
97103
}
@@ -262,21 +268,7 @@ final class QuickChatController: NSObject, NSWindowDelegate {
262268
model: self.model,
263269
onDismiss: { [weak self] in self?.dismiss() },
264270
onSendAccepted: { [weak self] openChat in
265-
guard let self else { return }
266-
// Command-Return must open the session that actually received the send;
267-
// the accepted route is immutable, unlike live model routing state.
268-
let route = self.model.lastAcceptedRoute
269-
self.dismiss()
270-
guard openChat else { return }
271-
if let route, !route.sessionKey.isEmpty {
272-
// Accepted tradeoff: in global scope the window opens the shared "global"
273-
// session without the agent discriminator; threading route.agentID through
274-
// the WebChat window contract is a follow-up if that config needs it.
275-
NSApp.activate(ignoringOtherApps: true)
276-
WebChatManager.shared.show(sessionKey: route.sessionKey)
277-
} else {
278-
AppNavigationActions.openChat()
279-
}
271+
self?.handleSendAccepted(openChat: openChat)
280272
},
281273
onShowAgentPicker: { [weak self] in
282274
self?.showAgentPicker()
@@ -293,6 +285,19 @@ final class QuickChatController: NSObject, NSWindowDelegate {
293285
})
294286
}
295287

288+
private func handleSendAccepted(openChat: Bool) {
289+
// Command-Return must open the immutable route that accepted the send,
290+
// not live model routing state that may already have changed.
291+
let route = self.model.lastAcceptedRoute
292+
self.dismiss()
293+
guard openChat else { return }
294+
if let route, !route.sessionKey.isEmpty {
295+
self.chatOpener(route.sessionKey, route.agentID)
296+
} else {
297+
self.chatOpener(nil, nil)
298+
}
299+
}
300+
296301
private func updateContentHeight(_ height: CGFloat) {
297302
let resolved = max(1, ceil(height))
298303
guard abs(resolved - self.contentHeight) > 0.5 else { return }
@@ -433,5 +438,9 @@ final class QuickChatController: NSObject, NSWindowDelegate {
433438
var hotkeyRegisteredForTesting: Bool {
434439
self.hotkeyRegistered
435440
}
441+
442+
func handleSendAcceptedForTesting(openChat: Bool) {
443+
self.handleSendAccepted(openChat: openChat)
444+
}
436445
#endif
437446
}

apps/macos/Sources/OpenClaw/WebChatManager.swift

Lines changed: 71 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,58 +17,89 @@ enum WebChatPresentation {
1717
case panel(anchorProvider: () -> NSRect?)
1818
}
1919

20+
struct WebChatRoute: Equatable, Sendable {
21+
let sessionKey: String
22+
let agentID: String?
23+
24+
init(sessionKey: String, agentID: String?) {
25+
self.sessionKey = sessionKey
26+
self.agentID = Self.normalizedAgentID(agentID)
27+
}
28+
29+
func replacingSessionKey(_ sessionKey: String) -> Self {
30+
Self(sessionKey: sessionKey, agentID: self.agentID)
31+
}
32+
33+
static func normalizedAgentID(_ agentID: String?) -> String? {
34+
let normalized = agentID?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
35+
return normalized?.isEmpty == false ? normalized : nil
36+
}
37+
}
38+
2039
@MainActor
2140
final class WebChatManager {
2241
static let shared = WebChatManager()
2342

2443
private var windowController: WebChatSwiftUIWindowController?
25-
private var windowSessionKey: String?
44+
private var windowRoute: WebChatRoute?
2645
private var panelController: WebChatSwiftUIWindowController?
27-
private var panelSessionKey: String?
28-
private var currentChatSessionKey: String?
46+
private var panelRoute: WebChatRoute?
47+
private var currentChatRoute: WebChatRoute?
2948
private var cachedPreferredSessionKey: String?
3049

3150
var onPanelVisibilityChanged: ((Bool) -> Void)?
3251

3352
var activeSessionKey: String? {
34-
self.currentChatSessionKey ?? self.panelSessionKey ?? self.windowSessionKey
53+
self.currentChatRoute?.sessionKey ?? self.panelRoute?.sessionKey ?? self.windowRoute?.sessionKey
3554
}
3655

37-
func show(sessionKey: String) {
56+
func show(sessionKey: String, agentID: String? = nil) {
57+
let route = WebChatRoute(sessionKey: sessionKey, agentID: agentID)
3858
self.closePanel()
3959
if let controller = self.windowController {
4060
// The window shell switches sessions in place (sidebar, /new);
41-
// windowSessionKey tracks those switches, so a window already on
42-
// the requested session must not be torn down and re-bootstrapped.
43-
if self.windowSessionKey == sessionKey {
61+
// full route identity tracks those switches and the global owner.
62+
if Self.shouldReuseController(currentRoute: self.windowRoute, requestedRoute: route) {
4463
controller.show()
4564
return
4665
}
4766

4867
controller.close()
4968
self.windowController = nil
50-
self.windowSessionKey = nil
69+
self.windowRoute = nil
5170
}
52-
let controller = WebChatSwiftUIWindowController(sessionKey: sessionKey, presentation: .window)
71+
let controller = WebChatSwiftUIWindowController(
72+
sessionKey: route.sessionKey,
73+
agentID: route.agentID,
74+
presentation: .window)
5375
controller.onVisibilityChanged = { [weak self] visible in
5476
self?.onPanelVisibilityChanged?(visible)
5577
}
56-
controller.onSessionKeyChanged = { [weak self] key in
57-
self?.windowSessionKey = key
58-
self?.currentChatSessionKey = key
78+
controller.onSessionKeyChanged = { [weak self, weak controller] key in
79+
guard let self, let controller, self.windowController === controller else { return }
80+
// Retaining the agent is safe: this surface has no in-window agent switcher,
81+
// and the controller pins explicit agents against gateway-default changes.
82+
let updatedRoute = (self.windowRoute ?? route).replacingSessionKey(key)
83+
self.windowRoute = updatedRoute
84+
self.currentChatRoute = updatedRoute
5985
}
6086
self.windowController = controller
61-
self.windowSessionKey = sessionKey
62-
self.currentChatSessionKey = sessionKey
87+
self.windowRoute = route
88+
self.currentChatRoute = route
6389
controller.show()
6490
}
6591

66-
func togglePanel(sessionKey: String, anchorProvider: @escaping () -> NSRect?) {
92+
func togglePanel(
93+
sessionKey: String,
94+
agentID: String? = nil,
95+
anchorProvider: @escaping () -> NSRect?)
96+
{
97+
let route = WebChatRoute(sessionKey: sessionKey, agentID: agentID)
6798
if let controller = self.panelController {
68-
if self.panelSessionKey != sessionKey {
99+
if !Self.shouldReuseController(currentRoute: self.panelRoute, requestedRoute: route) {
69100
controller.close()
70101
self.panelController = nil
71-
self.panelSessionKey = nil
102+
self.panelRoute = nil
72103
} else {
73104
if controller.isVisible {
74105
controller.close()
@@ -80,28 +111,33 @@ final class WebChatManager {
80111
}
81112

82113
let controller = WebChatSwiftUIWindowController(
83-
sessionKey: sessionKey,
114+
sessionKey: route.sessionKey,
115+
agentID: route.agentID,
84116
presentation: .panel(anchorProvider: anchorProvider))
85117
controller.onClosed = { [weak self] in
86118
self?.panelHidden()
87119
}
88120
controller.onVisibilityChanged = { [weak self] visible in
89121
self?.onPanelVisibilityChanged?(visible)
90122
}
91-
controller.onSessionKeyChanged = { [weak self] key in
92-
self?.panelSessionKey = key
93-
self?.currentChatSessionKey = key
123+
controller.onSessionKeyChanged = { [weak self, weak controller] key in
124+
guard let self, let controller, self.panelController === controller else { return }
125+
let updatedRoute = (self.panelRoute ?? route).replacingSessionKey(key)
126+
self.panelRoute = updatedRoute
127+
self.currentChatRoute = updatedRoute
94128
}
95129
self.panelController = controller
96-
self.panelSessionKey = sessionKey
97-
self.currentChatSessionKey = sessionKey
130+
self.panelRoute = route
131+
self.currentChatRoute = route
98132
controller.presentAnchored(anchorProvider: anchorProvider)
99133
}
100134

101135
func recordActiveSessionKey(_ sessionKey: String) {
102136
let trimmed = sessionKey.trimmingCharacters(in: .whitespacesAndNewlines)
103137
guard !trimmed.isEmpty else { return }
104-
self.currentChatSessionKey = trimmed
138+
let route = self.currentChatRoute ?? self.panelRoute ?? self.windowRoute
139+
self.currentChatRoute = route?.replacingSessionKey(trimmed)
140+
?? WebChatRoute(sessionKey: trimmed, agentID: nil)
105141
}
106142

107143
func closePanel() {
@@ -118,11 +154,11 @@ final class WebChatManager {
118154
func resetTunnels() {
119155
self.windowController?.close()
120156
self.windowController = nil
121-
self.windowSessionKey = nil
157+
self.windowRoute = nil
122158
self.panelController?.close()
123159
self.panelController = nil
124-
self.panelSessionKey = nil
125-
self.currentChatSessionKey = nil
160+
self.panelRoute = nil
161+
self.currentChatRoute = nil
126162
self.cachedPreferredSessionKey = nil
127163
}
128164

@@ -134,4 +170,11 @@ final class WebChatManager {
134170
self.onPanelVisibilityChanged?(false)
135171
// Keep panel controller cached so reopening doesn't re-bootstrap.
136172
}
173+
174+
static func shouldReuseController(
175+
currentRoute: WebChatRoute?,
176+
requestedRoute: WebChatRoute) -> Bool
177+
{
178+
currentRoute == requestedRoute
179+
}
137180
}

0 commit comments

Comments
 (0)