Files
openclaw/apps/macos/Sources/OpenClaw/QuickChatWindowPickerView.swift
Peter Steinberger 8c8ab1ea69 feat(mac): Quick Chat v2 — enable toggle, agent switching, zoom entrance, window screenshots (#109952)
* 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
2026-07-17 18:55:13 +01:00

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()
}
}