mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-23 09:51:14 +00:00
* feat(mac): Quick Chat v2 — enable toggle, agent avatars and switching, zoom entrance, window screenshots - Settings toggle (openclaw.quickChatEnabled) gating hotkey, menu item, recorder - agent chip renders avatar image (data URIs only; remote URLs deliberately not fetched to avoid SSRF), emoji, or tinted monogram; native NSMenu agent picker routes via canonical agent:<id>:<mainKey> or global+agentId per server contract - 'main session' capsule removed; placeholder carries the identity - fade+zoom presentation (96% scale in, 97% out) via frame+alpha animators - window screenshot picker: labeled overlays on renderable windows (Peekaboo filtering + z-order hit tests), overlays torn down pre-capture, SCK window capture, ChatImageProcessor JPEG, auto-send as chat.send attachment with the draft or a default caption; ownership-token pipeline bound to presentation and route; user-initiated Screen Recording alert; cancellable bounded discovery - ⌘Return opens the accepted route's session; global-scope agent threading is a filed follow-up; locale artifacts refreshed for new strings * chore(mac): regenerate i18n inventory after rebase onto main
59 lines
2.5 KiB
Swift
59 lines
2.5 KiB
Swift
import SwiftUI
|
|
|
|
struct QuickChatWindowPickerView: View {
|
|
let candidates: [QuickChatWindowCandidate]
|
|
let onSelect: (QuickChatWindowCandidate) -> Void
|
|
let onCancel: () -> Void
|
|
|
|
@State private var hoveredWindowID: Int?
|
|
|
|
var body: some View {
|
|
ZStack(alignment: .topLeading) {
|
|
Color.black.opacity(0.18)
|
|
|
|
// Candidates arrive front-to-back for hit testing; paint back-to-front so the
|
|
// topmost window's highlight is never covered by an occluded window's cell.
|
|
ForEach(self.candidates.reversed()) { candidate in
|
|
let isHovered = self.hoveredWindowID == candidate.windowID
|
|
ZStack {
|
|
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
.fill(Color.accentColor.opacity(isHovered ? 0.16 : 0.04))
|
|
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
|
.stroke(Color.accentColor.opacity(isHovered ? 0.95 : 0.62), lineWidth: isHovered ? 3 : 2)
|
|
Text(QuickChatWindowPickerLogic.labelText(
|
|
appName: candidate.appName,
|
|
title: candidate.title))
|
|
.font(.callout.weight(.medium))
|
|
.lineLimit(1)
|
|
.truncationMode(.tail)
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 6)
|
|
.frame(maxWidth: max(80, candidate.bounds.width - 20))
|
|
.background(.regularMaterial, in: Capsule())
|
|
.shadow(radius: 4, y: 1)
|
|
}
|
|
.frame(width: candidate.bounds.width, height: candidate.bounds.height)
|
|
.position(x: candidate.bounds.midX, y: candidate.bounds.midY)
|
|
.allowsHitTesting(false)
|
|
}
|
|
}
|
|
.contentShape(Rectangle())
|
|
.onContinuousHover { phase in
|
|
switch phase {
|
|
case let .active(point):
|
|
self.hoveredWindowID = QuickChatWindowPickerLogic.hitTest(self.candidates, at: point)?.windowID
|
|
case .ended:
|
|
self.hoveredWindowID = nil
|
|
}
|
|
}
|
|
.gesture(SpatialTapGesture().onEnded { value in
|
|
if let candidate = QuickChatWindowPickerLogic.hitTest(self.candidates, at: value.location) {
|
|
self.onSelect(candidate)
|
|
} else {
|
|
self.onCancel()
|
|
}
|
|
})
|
|
.ignoresSafeArea()
|
|
}
|
|
}
|