Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion app/macos/Runner/AIConversationWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class AIConversationWindow: NSWindow {
backing backingStoreType: NSWindow.BackingStoreType = .buffered, defer flag: Bool = false
) {
super.init(
contentRect: contentRect, styleMask: [.borderless, .utilityWindow], backing: backingStoreType,
contentRect: contentRect, styleMask: [.borderless, .utilityWindow, .resizable], backing: backingStoreType,
defer: flag)

self.isOpaque = false
Expand All @@ -23,6 +23,10 @@ class AIConversationWindow: NSWindow {
self.hasShadow = false
self.isMovableByWindowBackground = true
self.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]

// Set min and max sizes for resizing (only height, width is fixed)
self.minSize = NSSize(width: contentRect.width, height: 150)
self.maxSize = NSSize(width: contentRect.width, height: 800)
}

// Allow the window to become the key window to receive keyboard events.
Expand All @@ -34,4 +38,12 @@ class AIConversationWindow: NSWindow {
override var canBecomeMain: Bool {
return true
}

// Override to maintain fixed width while allowing height changes
override func setFrame(_ frameRect: NSRect, display flag: Bool) {
var adjustedFrame = frameRect
// Keep width fixed to minSize width
adjustedFrame.size.width = self.minSize.width
super.setFrame(adjustedFrame, display: flag)
}
}
48 changes: 29 additions & 19 deletions app/macos/Runner/AIResponseView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,12 @@ private struct MainBackgroundStyle: ViewModifier {
let cornerRadius: CGFloat

func body(content: Content) -> some View {
if #available(macOS 26.0, *) {
content.glassEffect(in: RoundedRectangle(cornerRadius: cornerRadius))
} else {
content
.background(
ControlBarVisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
)
.cornerRadius(cornerRadius)
}
content
.background(
ControlBarVisualEffectView(material: .menu, blendingMode: .withinWindow)
.opacity(0.7)
.cornerRadius(cornerRadius)
)
}
}

Expand All @@ -86,18 +83,18 @@ struct AIResponseView: View {
.clipShape(Circle())
Text("thinking")
.font(.system(size: 14, weight: .regular))
.foregroundColor(.white.opacity(0.8))
.foregroundColor(.white)
} else {
Text("AI response")
Text("Omi response")
.font(.system(size: 14, weight: .regular))
.foregroundColor(.white.opacity(0.8))
.foregroundColor(.white)
}
Spacer()
if !isLoading {
Button(action: { onAskFollowUp?() }) {
Text("Ask follow up")
.font(.system(size: 12, weight: .regular))
.foregroundColor(.primary)
.foregroundColor(.white)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Color.white.opacity(0.1))
Expand All @@ -123,10 +120,10 @@ struct AIResponseView: View {
Image(systemName: "checkmark")
Text("Copied")
}
.foregroundColor(.green)
.foregroundColor(.white)
} else {
Image(systemName: "square.on.square")
.foregroundColor(.secondary)
.foregroundColor(.white)
}
}
.buttonStyle(PlainButtonStyle())
Expand All @@ -135,7 +132,7 @@ struct AIResponseView: View {
Button(action: { onClose?() }) {
Image(systemName: "xmark")
.font(.system(size: 8, weight: .regular))
.foregroundColor(.secondary)
.foregroundColor(.white)
.frame(width: 16, height: 16)
.overlay(
Circle()
Expand All @@ -162,7 +159,7 @@ struct AIResponseView: View {
// As per the screenshot, but this is a placeholder.
// A better icon could be used if available in assets.
Image(systemName: "questionmark.circle.fill")
.foregroundColor(.red)
.foregroundColor(.white)
}

Text(userInput)
Expand All @@ -189,13 +186,26 @@ struct AIResponseView: View {
} else {
ScrollView {
Markdown(responseText)
.markdownTextStyle {
ForegroundColor(.white)
}
.frame(maxWidth: .infinity, alignment: .leading)
}

}

// Resize handle indicator at the bottom
HStack {
Spacer()
Image(systemName: "chevron.compact.down")
.foregroundColor(.white)
.font(.system(size: 12))
Spacer()
}
.padding(.top, 4)
}
.padding()
.frame(width: width, height: 300)
.frame(width: width)
.frame(minHeight: 150, maxHeight: 800)
.modifier(MainBackgroundStyle(cornerRadius: 20))
}
}
24 changes: 11 additions & 13 deletions app/macos/Runner/AskAIInputView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,23 +34,20 @@ private struct MainBackgroundStyle: ViewModifier {
let cornerRadius: CGFloat

func body(content: Content) -> some View {
if #available(macOS 26.0, *) {
content.glassEffect(in: RoundedRectangle(cornerRadius: cornerRadius))
} else {
content
.background(
ControlBarVisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
)
.cornerRadius(cornerRadius)
}
content
.background(
ControlBarVisualEffectView(material: .menu, blendingMode: .withinWindow)
.opacity(0.7)
.cornerRadius(cornerRadius)
)
}
}

struct AskAIInputView: View {
@Binding var userInput: String
@State private var localInput: String = ""
@State private var localScreenshotURL: URL?
@FocusState private var isInputFocused: Bool
let screenshotURL: URL?

var onSend: ((String, URL?) -> Void)?
var onCancel: (() -> Void)?
Expand All @@ -63,7 +60,7 @@ struct AskAIInputView: View {
onRemoveScreenshot: (() -> Void)? = nil
) {
self._userInput = userInput
self.screenshotURL = screenshotURL
self._localScreenshotURL = State(initialValue: screenshotURL)
self.width = width
self.onSend = onSend
self.onCancel = onCancel
Expand All @@ -72,7 +69,7 @@ struct AskAIInputView: View {

var body: some View {
HStack(spacing: 12) {
if let url = screenshotURL, let nsImage = NSImage(contentsOf: url) {
if let url = localScreenshotURL, let nsImage = NSImage(contentsOf: url) {
ZStack(alignment: .topTrailing) {
Button(action: {
NSWorkspace.shared.open(url)
Expand All @@ -86,6 +83,7 @@ struct AskAIInputView: View {
.buttonStyle(PlainButtonStyle())

Button(action: {
localScreenshotURL = nil
onRemoveScreenshot?()
}) {
Image(systemName: "xmark")
Expand Down Expand Up @@ -118,7 +116,7 @@ struct AskAIInputView: View {
}

Button(action: {
onSend?(localInput, screenshotURL)
onSend?(localInput, localScreenshotURL)
}) {
Image(systemName: "arrow.up.circle.fill")
.font(.system(size: 24))
Expand Down
82 changes: 37 additions & 45 deletions app/macos/Runner/FloatingChatWindowManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,34 +26,39 @@ class FloatingChatWindowManager: NSObject, ObservableObject {
// MARK: - Ask AI Window Management

func floatingButtonDidMove() {
positionAIConversationWindow()
// Only sync width when button moves (position is handled by parent/child relationship)
syncWindowWidth()
}

private func syncWindowWidth() {
guard let window = aiConversationWindow,
window.isVisible,
let button = floatingButton else { return }

let buttonWidth = button.frame.width
if abs(window.frame.width - buttonWidth) > 1.0 {
aiConversationWindowWidth = buttonWidth
let currentHeight = window.frame.height
window.setContentSize(NSSize(width: buttonWidth, height: currentHeight))
// Update min/max size to maintain fixed width
window.minSize = NSSize(width: buttonWidth, height: 150)
window.maxSize = NSSize(width: buttonWidth, height: 800)
}
}

func positionAIConversationWindow() {
guard let window = aiConversationWindow, window.isVisible else { return }
guard let window = aiConversationWindow else { return }

if let button = floatingButton {
// Synchronize width first
let buttonWidth = button.frame.width
if abs(window.frame.width - buttonWidth) > 1.0 { // Only resize if significantly different
aiConversationWindowWidth = buttonWidth
let currentHeight = window.frame.height
window.setContentSize(NSSize(width: buttonWidth, height: currentHeight))
}

// Position relative to floating control bar
let buttonFrame = button.frame
let spacing: CGFloat = 8

let windowFrame = window.frame
let newX = buttonFrame.origin.x
let newY = buttonFrame.origin.y - windowFrame.height - spacing

// Add check to prevent feedback loop
let newOrigin = NSPoint(x: newX, y: newY)
if abs(windowFrame.origin.x - newOrigin.x) > 0.1 || abs(windowFrame.origin.y - newOrigin.y) > 0.1 {
window.setFrameOrigin(newOrigin)
}
// Set initial position (subsequent moves handled by parent/child relationship)
window.setFrameOrigin(NSPoint(x: newX, y: newY))
} else {
// If no floating control bar, center the window on screen
if let screen = NSScreen.main {
Expand All @@ -66,26 +71,6 @@ class FloatingChatWindowManager: NSObject, ObservableObject {
}
}

@objc private func aiConversationWindowDidMove(_ notification: Notification) {
guard let window = notification.object as? AIConversationWindow,
window == self.aiConversationWindow,
let floatingButton = self.floatingButton else {
return
}

// Position floating button relative to this window
let windowFrame = window.frame
let spacing: CGFloat = 8
let newX = windowFrame.origin.x
let newY = windowFrame.origin.y + windowFrame.height + spacing

// Add check to prevent feedback loop
let newOrigin = NSPoint(x: newX, y: newY)
if abs(floatingButton.frame.origin.x - newOrigin.x) > 0.1 || abs(floatingButton.frame.origin.y - newOrigin.y) > 0.1 {
floatingButton.setFrameOrigin(newOrigin)
}
}

func toggleAIConversationWindow(screenshotURL: URL?) {
DispatchQueue.main.async {
// If window exists and is visible, hide it
Expand All @@ -107,13 +92,7 @@ class FloatingChatWindowManager: NSObject, ObservableObject {
let windowRect = NSRect(x: 0, y: 0, width: initialWidth, height: 300)
self.aiConversationWindow = AIConversationWindow(contentRect: windowRect, defer: false)

// Observe window move events to sync floating button
NotificationCenter.default.addObserver(
self,
selector: #selector(self.aiConversationWindowDidMove),
name: NSWindow.didMoveNotification,
object: self.aiConversationWindow
)
// No need to observe move events - parent/child relationship handles this
}

// Update view
Expand All @@ -128,9 +107,16 @@ class FloatingChatWindowManager: NSObject, ObservableObject {
self.aiConversationWindow?.setContentSize(NSSize(width: currentWidth, height: newSize.height))
}

// Position and show the window (this will handle width sync)
self.aiConversationWindow?.makeKeyAndOrderFront(nil)
// Position the window first
self.positionAIConversationWindow()

// Make chat window a child of floating button for synchronized movement
if let button = self.floatingButton, let chatWindow = self.aiConversationWindow {
button.addChildWindow(chatWindow, ordered: .below)
}

// Show the window
self.aiConversationWindow?.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
}
}
Expand Down Expand Up @@ -175,6 +161,12 @@ class FloatingChatWindowManager: NSObject, ObservableObject {
self.askAIInputText = ""
self.currentScreenshotURL = nil
self.isShowingAIResponse = false

// Remove as child window before hiding
if let button = self.floatingButton, let chatWindow = self.aiConversationWindow {
button.removeChildWindow(chatWindow)
}

self.aiConversationWindow?.orderOut(nil)
}
}
Expand Down
Loading