From 257c025f925425b3c52d53ddd2a65c8269f9b78d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 17 Nov 2023 12:31:41 +0800 Subject: [PATCH 01/19] Update to activate extension app when using hotkey to show widgets --- Core/Sources/Service/Service.swift | 17 ++++++++++++++++- .../Sources/XcodeInspector/XcodeInspector.swift | 3 +++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Core/Sources/Service/Service.swift b/Core/Sources/Service/Service.swift index 0582aed4..df774dc8 100644 --- a/Core/Sources/Service/Service.swift +++ b/Core/Sources/Service/Service.swift @@ -1,3 +1,4 @@ +import AppKit import Combine import Dependencies import Foundation @@ -83,7 +84,10 @@ final class GlobalShortcutManager { setupShortcutIfNeeded() KeyboardShortcuts.onKeyUp(for: .showHideWidget) { [guiController] in - if XcodeInspector.shared.activeXcode == nil, + let isXCodeActive = XcodeInspector.shared.activeXcode != nil + let isExtensionActive = NSApplication.shared.isActive + + if !isXCodeActive, !guiController.viewStore.state.suggestionWidgetState.chatPanelState.isPanelDisplayed, UserDefaults.shared.value(for: \.showHideWidgetShortcutGlobally) { @@ -91,6 +95,17 @@ final class GlobalShortcutManager { } else { guiController.viewStore.send(.suggestionWidget(.circularWidget(.widgetClicked))) } + + if !isExtensionActive { + Task { + try await Task.sleep(nanoseconds: 150_000_000) + NSApplication.shared.activate(ignoringOtherApps: true) + } + } else if let previous = XcodeInspector.shared.previousActiveApplication, + !previous.isActive + { + previous.runningApplication.activate() + } } XcodeInspector.shared.$activeApplication.sink { app in diff --git a/Tool/Sources/XcodeInspector/XcodeInspector.swift b/Tool/Sources/XcodeInspector/XcodeInspector.swift index cd3f54fd..748af74d 100644 --- a/Tool/Sources/XcodeInspector/XcodeInspector.swift +++ b/Tool/Sources/XcodeInspector/XcodeInspector.swift @@ -14,6 +14,7 @@ public final class XcodeInspector: ObservableObject { private var activeXcodeCancellable = Set() @Published public internal(set) var activeApplication: AppInstanceInspector? + @Published public internal(set) var previousActiveApplication: AppInstanceInspector? @Published public internal(set) var activeXcode: XcodeAppInstanceInspector? @Published public internal(set) var latestActiveXcode: XcodeAppInstanceInspector? @Published public internal(set) var xcodes: [XcodeAppInstanceInspector] = [] @@ -110,6 +111,7 @@ public final class XcodeInspector: ObservableObject { setActiveXcode(new) } } else { + previousActiveApplication = activeApplication activeApplication = AppInstanceInspector(runningApplication: app) } } @@ -145,6 +147,7 @@ public final class XcodeInspector: ObservableObject { @MainActor func setActiveXcode(_ xcode: XcodeAppInstanceInspector) { + previousActiveApplication = activeApplication activeApplication = xcode xcode.refresh() for task in activeXcodeObservations { task.cancel() } From 4d6b46a9a85b41f08b1a553afd8a1e1fd3d0d743 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 17 Nov 2023 14:51:44 +0800 Subject: [PATCH 02/19] Move GlobalShortcutManager to its own file --- .../Service/GlobalShortcutManager.swift | 75 +++++++++++++++++++ Core/Sources/Service/Service.swift | 72 ------------------ 2 files changed, 75 insertions(+), 72 deletions(-) create mode 100644 Core/Sources/Service/GlobalShortcutManager.swift diff --git a/Core/Sources/Service/GlobalShortcutManager.swift b/Core/Sources/Service/GlobalShortcutManager.swift new file mode 100644 index 00000000..6447ebf2 --- /dev/null +++ b/Core/Sources/Service/GlobalShortcutManager.swift @@ -0,0 +1,75 @@ +import AppKit +import Combine +import Foundation +import KeyboardShortcuts +import XcodeInspector + +extension KeyboardShortcuts.Name { + static let showHideWidget = Self("ShowHideWidget") +} + +@MainActor +final class GlobalShortcutManager { + let guiController: GraphicalUserInterfaceController + private var cancellable = Set() + + nonisolated init(guiController: GraphicalUserInterfaceController) { + self.guiController = guiController + } + + func start() { + KeyboardShortcuts.userDefaults = .shared + setupShortcutIfNeeded() + + KeyboardShortcuts.onKeyUp(for: .showHideWidget) { [guiController] in + let isXCodeActive = XcodeInspector.shared.activeXcode != nil + let isExtensionActive = NSApplication.shared.isActive + + if !isXCodeActive, + !guiController.viewStore.state.suggestionWidgetState.chatPanelState.isPanelDisplayed, + UserDefaults.shared.value(for: \.showHideWidgetShortcutGlobally) + { + guiController.viewStore.send(.openChatPanel(forceDetach: true)) + } else { + guiController.viewStore.send(.toggleWidgets) + } + + if !isExtensionActive { + Task { + try await Task.sleep(nanoseconds: 150_000_000) + NSApplication.shared.activate(ignoringOtherApps: true) + } + } else if let previous = XcodeInspector.shared.previousActiveApplication, + !previous.isActive + { + previous.runningApplication.activate() + } + } + + XcodeInspector.shared.$activeApplication.sink { app in + if !UserDefaults.shared.value(for: \.showHideWidgetShortcutGlobally) { + let shouldBeEnabled = if let app, app.isXcode || app.isExtensionService { + true + } else { + false + } + if shouldBeEnabled { + self.setupShortcutIfNeeded() + } else { + self.removeShortcutIfNeeded() + } + } else { + self.setupShortcutIfNeeded() + } + }.store(in: &cancellable) + } + + func setupShortcutIfNeeded() { + KeyboardShortcuts.enable(.showHideWidget) + } + + func removeShortcutIfNeeded() { + KeyboardShortcuts.disable(.showHideWidget) + } +} + diff --git a/Core/Sources/Service/Service.swift b/Core/Sources/Service/Service.swift index df774dc8..4702ee68 100644 --- a/Core/Sources/Service/Service.swift +++ b/Core/Sources/Service/Service.swift @@ -1,8 +1,5 @@ -import AppKit -import Combine import Dependencies import Foundation -import KeyboardShortcuts import Workspace import WorkspaceSuggestionService import XcodeInspector @@ -16,10 +13,6 @@ import ProService public static let shared = TheActor() } -extension KeyboardShortcuts.Name { - static let showHideWidget = Self("ShowHideWidget") -} - /// The running extension service. public final class Service { public static let shared = Service() @@ -70,68 +63,3 @@ public final class Service { } } -@MainActor -final class GlobalShortcutManager { - let guiController: GraphicalUserInterfaceController - private var cancellable = Set() - - nonisolated init(guiController: GraphicalUserInterfaceController) { - self.guiController = guiController - } - - func start() { - KeyboardShortcuts.userDefaults = .shared - setupShortcutIfNeeded() - - KeyboardShortcuts.onKeyUp(for: .showHideWidget) { [guiController] in - let isXCodeActive = XcodeInspector.shared.activeXcode != nil - let isExtensionActive = NSApplication.shared.isActive - - if !isXCodeActive, - !guiController.viewStore.state.suggestionWidgetState.chatPanelState.isPanelDisplayed, - UserDefaults.shared.value(for: \.showHideWidgetShortcutGlobally) - { - guiController.viewStore.send(.openChatPanel(forceDetach: true)) - } else { - guiController.viewStore.send(.suggestionWidget(.circularWidget(.widgetClicked))) - } - - if !isExtensionActive { - Task { - try await Task.sleep(nanoseconds: 150_000_000) - NSApplication.shared.activate(ignoringOtherApps: true) - } - } else if let previous = XcodeInspector.shared.previousActiveApplication, - !previous.isActive - { - previous.runningApplication.activate() - } - } - - XcodeInspector.shared.$activeApplication.sink { app in - if !UserDefaults.shared.value(for: \.showHideWidgetShortcutGlobally) { - let shouldBeEnabled = if let app, app.isXcode || app.isExtensionService { - true - } else { - false - } - if shouldBeEnabled { - self.setupShortcutIfNeeded() - } else { - self.removeShortcutIfNeeded() - } - } else { - self.setupShortcutIfNeeded() - } - }.store(in: &cancellable) - } - - func setupShortcutIfNeeded() { - KeyboardShortcuts.enable(.showHideWidget) - } - - func removeShortcutIfNeeded() { - KeyboardShortcuts.disable(.showHideWidget) - } -} - From d81a5fb8711b2b7ef8a09c2f0da94f556141f286 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 17 Nov 2023 14:52:04 +0800 Subject: [PATCH 03/19] Add an action toggleWIdgets to GUI --- .../GUI/GraphicalUserInterfaceController.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift index a1a80b00..02855973 100644 --- a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift +++ b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift @@ -6,6 +6,7 @@ import ComposableArchitecture import Dependencies import Environment import Preferences +import SuggestionModel import SuggestionWidget #if canImport(ProChatTabs) @@ -54,7 +55,8 @@ struct GUI: ReducerProtocol { case openChatPanel(forceDetach: Bool) case createChatGPTChatTabIfNeeded case sendCustomCommandToActiveChat(CustomCommand) - + case toggleWidgets + case suggestionWidget(WidgetFeature.Action) static func promptToCodeGroup(_ action: PromptToCodeGroup.Action) -> Self { @@ -192,6 +194,13 @@ struct GUI: ReducerProtocol { } } + case .toggleWidgets: + return .run { send in + await send( + .suggestionWidget(.circularWidget(.widgetClicked)) + ) + } + case let .suggestionWidget(.chatPanel(.chatTab(id, .tabContentUpdated))): #if canImport(ChatTabPersistent) // when a tab is updated, persist it. From bc57cf1050fc1832c631c73cee2377c81cfd0b76 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 17 Nov 2023 15:03:13 +0800 Subject: [PATCH 04/19] Update to change key window when toggling widget with hotkey --- .../GraphicalUserInterfaceController.swift | 19 +++++++++++++------ .../Service/GlobalShortcutManager.swift | 2 +- .../FeatureReducers/WidgetFeature.swift | 18 +++++++++++++++++- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift index 02855973..8def280a 100644 --- a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift +++ b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift @@ -55,8 +55,8 @@ struct GUI: ReducerProtocol { case openChatPanel(forceDetach: Bool) case createChatGPTChatTabIfNeeded case sendCustomCommandToActiveChat(CustomCommand) - case toggleWidgets - + case toggleWidgetsHotkeyPressed + case suggestionWidget(WidgetFeature.Action) static func promptToCodeGroup(_ action: PromptToCodeGroup.Action) -> Self { @@ -194,11 +194,18 @@ struct GUI: ReducerProtocol { } } - case .toggleWidgets: + case .toggleWidgetsHotkeyPressed: + let hasChat = state.chatTabGroup.selectedTabInfo != nil + let hasPromptToCode = state.promptToCodeGroup.activePromptToCode != nil + return .run { send in - await send( - .suggestionWidget(.circularWidget(.widgetClicked)) - ) + await send(.suggestionWidget(.circularWidget(.widgetClicked))) + + if hasPromptToCode { + await send(.suggestionWidget(.updateKeyWindow(.sharedPanel))) + } else if hasChat { + await send(.suggestionWidget(.updateKeyWindow(.chatPanel))) + } } case let .suggestionWidget(.chatPanel(.chatTab(id, .tabContentUpdated))): diff --git a/Core/Sources/Service/GlobalShortcutManager.swift b/Core/Sources/Service/GlobalShortcutManager.swift index 6447ebf2..b6b7321d 100644 --- a/Core/Sources/Service/GlobalShortcutManager.swift +++ b/Core/Sources/Service/GlobalShortcutManager.swift @@ -31,7 +31,7 @@ final class GlobalShortcutManager { { guiController.viewStore.send(.openChatPanel(forceDetach: true)) } else { - guiController.viewStore.send(.toggleWidgets) + guiController.viewStore.send(.toggleWidgetsHotkeyPressed) } if !isExtensionActive { diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift index 7e5510c0..5e58de32 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift @@ -21,6 +21,11 @@ public struct WidgetFeature: ReducerProtocol { public var sharedPanelWindowState = WindowState() public var tabWindowState = WindowState() } + + public enum WindowCanBecomeKey: Equatable { + case sharedPanel + case chatPanel + } public struct State: Equatable { var focusingDocumentURL: URL? @@ -112,6 +117,7 @@ public struct WidgetFeature: ReducerProtocol { case updateWindowOpacity case updateFocusingDocumentURL case updateWindowOpacityFinished + case updateKeyWindow(WindowCanBecomeKey) case panel(PanelFeature.Action) case chatPanel(ChatPanelFeature.Action) @@ -578,11 +584,21 @@ public struct WidgetFeature: ReducerProtocol { await send(.updateWindowOpacityFinished) } .cancellable(id: DebounceKey.updateWindowOpacity, cancelInFlight: true) - + case .updateWindowOpacityFinished: state.lastUpdateWindowOpacityTime = Date() return .none + case let .updateKeyWindow(window): + return .run { _ in + switch window { + case .chatPanel: + await windows.chatPanelWindow.makeKeyAndOrderFront(nil) + case .sharedPanel: + await windows.sharedPanelWindow.makeKeyAndOrderFront(nil) + } + } + case .circularWidget: return .none From 4b1271abfbe05aefada5e612d3ed8903c4391290 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 17 Nov 2023 15:43:11 +0800 Subject: [PATCH 05/19] Fix activation logic --- .../Service/GlobalShortcutManager.swift | 12 ----------- .../FeatureReducers/WidgetFeature.swift | 21 ++++++++++++------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/Core/Sources/Service/GlobalShortcutManager.swift b/Core/Sources/Service/GlobalShortcutManager.swift index b6b7321d..3ed6a69c 100644 --- a/Core/Sources/Service/GlobalShortcutManager.swift +++ b/Core/Sources/Service/GlobalShortcutManager.swift @@ -23,7 +23,6 @@ final class GlobalShortcutManager { KeyboardShortcuts.onKeyUp(for: .showHideWidget) { [guiController] in let isXCodeActive = XcodeInspector.shared.activeXcode != nil - let isExtensionActive = NSApplication.shared.isActive if !isXCodeActive, !guiController.viewStore.state.suggestionWidgetState.chatPanelState.isPanelDisplayed, @@ -33,17 +32,6 @@ final class GlobalShortcutManager { } else { guiController.viewStore.send(.toggleWidgetsHotkeyPressed) } - - if !isExtensionActive { - Task { - try await Task.sleep(nanoseconds: 150_000_000) - NSApplication.shared.activate(ignoringOtherApps: true) - } - } else if let previous = XcodeInspector.shared.previousActiveApplication, - !previous.isActive - { - previous.runningApplication.activate() - } } XcodeInspector.shared.$activeApplication.sink { app in diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift index 5e58de32..049879f2 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift @@ -21,7 +21,7 @@ public struct WidgetFeature: ReducerProtocol { public var sharedPanelWindowState = WindowState() public var tabWindowState = WindowState() } - + public enum WindowCanBecomeKey: Equatable { case sharedPanel case chatPanel @@ -153,8 +153,8 @@ public struct WidgetFeature: ReducerProtocol { } case .circularWidget(.widgetClicked): - let isDisplayingContent = state._circularWidgetState.isDisplayingContent - if isDisplayingContent { + let wasDisplayingContent = state._circularWidgetState.isDisplayingContent + if wasDisplayingContent { state.panelState.sharedPanelState.isPanelDisplayed = false state.panelState.suggestionPanelState.isPanelDisplayed = false state.chatPanelState.isPanelDisplayed = false @@ -163,11 +163,16 @@ public struct WidgetFeature: ReducerProtocol { state.panelState.suggestionPanelState.isPanelDisplayed = true state.chatPanelState.isPanelDisplayed = true } + let isDisplayingContent = state._circularWidgetState.isDisplayingContent return .run { _ in - guard isDisplayingContent else { return } - if let app = activeApplicationMonitor.previousApp, app.isXcode { - try await Task.sleep(nanoseconds: 200_000_000) - app.activate() + if isDisplayingContent, !(await NSApplication.shared.isActive) { + try await Task.sleep(nanoseconds: 50_000_000) + await NSApplication.shared.activate(ignoringOtherApps: true) + } else if !isDisplayingContent, + let app = xcodeInspector.previousActiveApplication + { + try await Task.sleep(nanoseconds: 20_000_000) + app.runningApplication.activate() } } @@ -584,7 +589,7 @@ public struct WidgetFeature: ReducerProtocol { await send(.updateWindowOpacityFinished) } .cancellable(id: DebounceKey.updateWindowOpacity, cancelInFlight: true) - + case .updateWindowOpacityFinished: state.lastUpdateWindowOpacityTime = Date() return .none From 5a3db201197bbb0ba1aaf8f5dda9ea36fd89b336 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 17 Nov 2023 16:20:50 +0800 Subject: [PATCH 06/19] Support activating selecting chat tab in various situations --- Core/Sources/ChatGPTChatTab/Chat.swift | 10 +++++++ .../ChatGPTChatTab/ChatGPTChatTab.swift | 6 +++++ Core/Sources/ChatGPTChatTab/ChatPanel.swift | 17 ++++++------ .../FeatureReducers/ChatPanelFeature.swift | 27 +++++++++++++++---- .../FeatureReducers/WidgetFeature.swift | 6 ++++- Tool/Sources/ChatTab/ChatTab.swift | 1 + Tool/Sources/ChatTab/ChatTabItem.swift | 4 +++ 7 files changed, 57 insertions(+), 14 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/Chat.swift b/Core/Sources/ChatGPTChatTab/Chat.swift index d5ec598b..70d5597c 100644 --- a/Core/Sources/ChatGPTChatTab/Chat.swift +++ b/Core/Sources/ChatGPTChatTab/Chat.swift @@ -32,6 +32,11 @@ struct Chat: ReducerProtocol { var history: [ChatMessage] = [] @BindingState var isReceivingMessage = false var chatMenu = ChatMenu.State() + @BindingState var focusedField: Field? = .textField + + enum Field: String, Hashable { + case textField + } } enum Action: Equatable, BindableAction { @@ -45,6 +50,7 @@ struct Chat: ReducerProtocol { case deleteMessageButtonTapped(MessageID) case resendMessageButtonTapped(MessageID) case setAsExtraPromptButtonTapped(MessageID) + case focusOnTextField case observeChatService case observeHistoryChange @@ -127,6 +133,10 @@ struct Chat: ReducerProtocol { return .run { _ in await service.setMessageAsExtraPrompt(id: id) } + + case .focusOnTextField: + state.focusedField = .textField + return .none case .observeChatService: return .run { send in diff --git a/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift b/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift index ffde45a9..796934a5 100644 --- a/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift +++ b/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift @@ -116,6 +116,12 @@ public class ChatGPTChatTab: ChatTab { public func start() { chatTabViewStore.send(.updateTitle("Chat")) + chatTabViewStore.publisher.focusTrigger.removeDuplicates().sink { [weak self] _ in + Task { @MainActor [weak self] in + self?.viewStore.send(.focusOnTextField) + } + }.store(in: &cancellable) + service.$systemPrompt.removeDuplicates().sink { _ in Task { @MainActor [weak self] in self?.chatTabViewStore.send(.tabContentUpdated) diff --git a/Core/Sources/ChatGPTChatTab/ChatPanel.swift b/Core/Sources/ChatGPTChatTab/ChatPanel.swift index d6e7dc5b..af42e170 100644 --- a/Core/Sources/ChatGPTChatTab/ChatPanel.swift +++ b/Core/Sources/ChatGPTChatTab/ChatPanel.swift @@ -498,16 +498,13 @@ struct FunctionMessage: View { struct ChatPanelInputArea: View { let chat: StoreOf - @FocusState var isInputAreaFocused: Bool + @FocusState var focusedField: Chat.State.Field? var body: some View { HStack { clearButton textEditor } - .onAppear { - isInputAreaFocused = true - } .padding(8) .background(.ultraThickMaterial) } @@ -538,8 +535,11 @@ struct ChatPanelInputArea: View { @MainActor var textEditor: some View { HStack(spacing: 0) { - WithViewStore(chat, removeDuplicates: { $0.typedMessage == $1.typedMessage }) { - viewStore in + WithViewStore( + chat, + removeDuplicates: { + $0.typedMessage == $1.typedMessage && $0.focusedField == $1.focusedField + }) { viewStore in ZStack(alignment: .center) { // a hack to support dynamic height of TextEditor Text( @@ -560,7 +560,8 @@ struct ChatPanelInputArea: View { .padding(.top, 1) .padding(.bottom, -1) } - .focused($isInputAreaFocused) + .focused($focusedField, equals: .textField) + .bind(viewStore.$focusedField, to: $focusedField) .padding(8) .fixedSize(horizontal: false, vertical: true) } @@ -595,7 +596,7 @@ struct ChatPanelInputArea: View { .keyboardShortcut(KeyEquivalent.return, modifiers: [.shift]) Button(action: { - isInputAreaFocused = true + focusedField = .textField }) { EmptyView() } diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift index a62c5d8a..8140bad9 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift @@ -70,6 +70,7 @@ public struct ChatPanelFeature: ReducerProtocol { case switchToNextTab case switchToPreviousTab case moveChatTab(from: Int, to: Int) + case focusActiveChatTab case chatTab(id: String, action: ChatTabItem.Action) } @@ -117,8 +118,9 @@ public struct ChatPanelFeature: ReducerProtocol { state.chatPanelInASeparateWindow = true } state.isPanelDisplayed = true - return .run { _ in + return .run { send in await activateExtensionService() + await send(.focusActiveChatTab) } case let .updateChatTabInfo(chatTabInfo): @@ -172,14 +174,18 @@ public struct ChatPanelFeature: ReducerProtocol { return .none } state.chatTabGroup.selectedTabId = id - return .none + return .run { send in + await send(.focusActiveChatTab) + } case let .appendAndSelectTab(tab): guard !state.chatTabGroup.tabInfo.contains(where: { $0.id == tab.id }) else { return .none } state.chatTabGroup.tabInfo.append(tab) state.chatTabGroup.selectedTabId = tab.id - return .none + return .run { send in + await send(.focusActiveChatTab) + } case .switchToNextTab: let selectedId = state.chatTabGroup.selectedTabId @@ -192,7 +198,9 @@ public struct ChatPanelFeature: ReducerProtocol { } let targetId = state.chatTabGroup.tabInfo[nextIndex].id state.chatTabGroup.selectedTabId = targetId - return .none + return .run { send in + await send(.focusActiveChatTab) + } case .switchToPreviousTab: let selectedId = state.chatTabGroup.selectedTabId @@ -205,7 +213,9 @@ public struct ChatPanelFeature: ReducerProtocol { } let targetId = state.chatTabGroup.tabInfo[previousIndex].id state.chatTabGroup.selectedTabId = targetId - return .none + return .run { send in + await send(.focusActiveChatTab) + } case let .moveChatTab(from, to): guard from >= 0, from < state.chatTabGroup.tabInfo.endIndex, to >= 0, @@ -217,6 +227,13 @@ public struct ChatPanelFeature: ReducerProtocol { state.chatTabGroup.tabInfo.remove(at: from) state.chatTabGroup.tabInfo.insert(tab, at: to) return .none + + case .focusActiveChatTab: + let id = state.chatTabGroup.selectedTabInfo?.id + guard let id else { return .none } + return .run { send in + await send(.chatTab(id: id, action: .focus)) + } case let .chatTab(id, .close): return .run { send in diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift index 049879f2..ebb40d97 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift @@ -164,7 +164,11 @@ public struct WidgetFeature: ReducerProtocol { state.chatPanelState.isPanelDisplayed = true } let isDisplayingContent = state._circularWidgetState.isDisplayingContent - return .run { _ in + return .run { send in + if isDisplayingContent { + await send(.chatPanel(.focusActiveChatTab)) + } + if isDisplayingContent, !(await NSApplication.shared.isActive) { try await Task.sleep(nanoseconds: 50_000_000) await NSApplication.shared.activate(ignoringOtherApps: true) diff --git a/Tool/Sources/ChatTab/ChatTab.swift b/Tool/Sources/ChatTab/ChatTab.swift index 1c09ef5e..cc10c240 100644 --- a/Tool/Sources/ChatTab/ChatTab.swift +++ b/Tool/Sources/ChatTab/ChatTab.swift @@ -6,6 +6,7 @@ import SwiftUI public struct ChatTabInfo: Identifiable, Equatable { public var id: String public var title: String + public var focusTrigger: Int = 0 public init(id: String, title: String) { self.id = id diff --git a/Tool/Sources/ChatTab/ChatTabItem.swift b/Tool/Sources/ChatTab/ChatTabItem.swift index 128af1e7..f54f8085 100644 --- a/Tool/Sources/ChatTab/ChatTabItem.swift +++ b/Tool/Sources/ChatTab/ChatTabItem.swift @@ -21,6 +21,7 @@ public struct ChatTabItem: ReducerProtocol { case openNewTab(AnyChatTabBuilder) case tabContentUpdated case close + case focus } public init() {} @@ -37,6 +38,9 @@ public struct ChatTabItem: ReducerProtocol { return .none case .close: return .none + case .focus: + state.focusTrigger += 1 + return .none } } } From 72bc2f2b0c1e407864f41cc86a8449e9c1269009 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 17 Nov 2023 16:51:34 +0800 Subject: [PATCH 07/19] Auto focus on text field in chat in various situations --- Core/Sources/ChatGPTChatTab/Chat.swift | 3 ++- .../Sources/FocusedCodeFinder/FocusedCodeFinder.swift | 11 +++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/Chat.swift b/Core/Sources/ChatGPTChatTab/Chat.swift index 70d5597c..9bfbe68b 100644 --- a/Core/Sources/ChatGPTChatTab/Chat.swift +++ b/Core/Sources/ChatGPTChatTab/Chat.swift @@ -32,7 +32,7 @@ struct Chat: ReducerProtocol { var history: [ChatMessage] = [] @BindingState var isReceivingMessage = false var chatMenu = ChatMenu.State() - @BindingState var focusedField: Field? = .textField + @BindingState var focusedField: Field? enum Field: String, Hashable { case textField @@ -95,6 +95,7 @@ struct Chat: ReducerProtocol { await send(.isReceivingMessageChanged) await send(.systemPromptChanged) await send(.extraSystemPromptChanged) + await send(.focusOnTextField) } case .sendButtonTapped: diff --git a/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift index ee37adea..e938cc5e 100644 --- a/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift @@ -67,14 +67,17 @@ public struct UnknownLanguageFocusedCodeFinder: FocusedCodeFinder { guard !activeDocumentContext.lines.isEmpty else { return .empty } // when user is not selecting any code. - if containingRange.start == containingRange.end { + if containingRange.start == containingRange.end{ // search up and down for up to `proposedSearchRange * 2 + 1` lines. let lines = activeDocumentContext.lines let proposedLineCount = proposedSearchRange * 2 + 1 let startLineIndex = max(containingRange.start.line - proposedSearchRange, 0) - let endLineIndex = max( - startLineIndex, - min(startLineIndex + proposedLineCount - 1, lines.count - 1) + let endLineIndex = min( + max( + startLineIndex, + min(startLineIndex + proposedLineCount - 1, lines.count - 1) + ), + lines.count - 1 ) let focusedLines = lines[startLineIndex...endLineIndex] From ebce40141b020c5741b1ad7babf890259822243f Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 17 Nov 2023 18:17:52 +0800 Subject: [PATCH 08/19] Unify implementation of app activation --- .../GraphicalUserInterfaceController.swift | 26 +++----- .../Service/GUI/WidgetDataSource.swift | 16 ++--- .../WindowBaseCommandHandler.swift | 3 +- .../FeatureReducers/ChatPanelFeature.swift | 8 +-- .../FeatureReducers/PanelFeature.swift | 5 +- .../FeatureReducers/PromptToCodeGroup.swift | 3 +- .../FeatureReducers/WidgetFeature.swift | 27 +++++--- .../SuggestionWidget/ModuleDependency.swift | 27 -------- Tool/Package.swift | 9 +++ Tool/Sources/AppActivator/AppActivator.swift | 61 +++++++++++++++++++ Tool/Sources/Environment/Environment.swift | 9 --- .../FocusedCodeFinder/FocusedCodeFinder.swift | 3 +- 12 files changed, 114 insertions(+), 83 deletions(-) create mode 100644 Tool/Sources/AppActivator/AppActivator.swift diff --git a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift index 8def280a..1b85ac3f 100644 --- a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift +++ b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift @@ -1,4 +1,5 @@ import ActiveApplicationMonitor +import AppActivator import AppKit import ChatGPTChatTab import ChatTab @@ -68,7 +69,8 @@ struct GUI: ReducerProtocol { #endif } - @Dependency(\.chatTabPool) var chatTabPool: ChatTabPool + @Dependency(\.chatTabPool) var chatTabPool + @Dependency(\.activateThisApp) var activateThisApp public enum Debounce: Hashable { case updateChatTabOrder @@ -137,6 +139,11 @@ struct GUI: ReducerProtocol { .chatPanel(.presentChatPanel(forceDetach: forceDetach)) ) ) + await send(.suggestionWidget(.updateKeyWindow(.chatPanel))) + + if await !NSApplication.shared.isActive { + activateThisApp() + } } case .createChatGPTChatTabIfNeeded: @@ -195,17 +202,8 @@ struct GUI: ReducerProtocol { } case .toggleWidgetsHotkeyPressed: - let hasChat = state.chatTabGroup.selectedTabInfo != nil - let hasPromptToCode = state.promptToCodeGroup.activePromptToCode != nil - return .run { send in await send(.suggestionWidget(.circularWidget(.widgetClicked))) - - if hasPromptToCode { - await send(.suggestionWidget(.updateKeyWindow(.sharedPanel))) - } else if hasChat { - await send(.suggestionWidget(.updateKeyWindow(.chatPanel))) - } } case let .suggestionWidget(.chatPanel(.chatTab(id, .tabContentUpdated))): @@ -278,12 +276,8 @@ public final class GraphicalUserInterfaceController { Task { let handler = PseudoCommandHandler() await handler.acceptPromptToCode() - if let app = ActiveApplicationMonitor.shared.previousApp, - app.isXcode, - !promptToCode.isContinuous - { - try await Task.sleep(nanoseconds: 200_000_000) - app.activate() + if !promptToCode.isContinuous { + NSWorkspace.activatePreviousActiveXcode() } } } diff --git a/Core/Sources/Service/GUI/WidgetDataSource.swift b/Core/Sources/Service/GUI/WidgetDataSource.swift index 8b449020..7fa244e8 100644 --- a/Core/Sources/Service/GUI/WidgetDataSource.swift +++ b/Core/Sources/Service/GUI/WidgetDataSource.swift @@ -1,4 +1,6 @@ import ActiveApplicationMonitor +import AppActivator +import AppKit import ChatService import ComposableArchitecture import Foundation @@ -39,24 +41,14 @@ extension WidgetDataSource: SuggestionWidgetDataSource { Task { let handler = PseudoCommandHandler() await handler.rejectSuggestions() - if let app = ActiveApplicationMonitor.shared.previousApp, - app.isXcode - { - try await Task.sleep(nanoseconds: 200_000_000) - app.activate() - } + NSWorkspace.activatePreviousActiveXcode() } }, onAcceptSuggestionTapped: { Task { let handler = PseudoCommandHandler() await handler.acceptSuggestion() - if let app = ActiveApplicationMonitor.shared.previousApp, - app.isXcode - { - try await Task.sleep(nanoseconds: 200_000_000) - app.activate() - } + NSWorkspace.activatePreviousActiveXcode() } } ) diff --git a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift index d4668628..a2fde81e 100644 --- a/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift +++ b/Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift @@ -1,3 +1,4 @@ +import AppKit import ChatService import Environment import Foundation @@ -409,7 +410,7 @@ extension WindowBaseCommandHandler { }() as (String, CursorRange) let viewStore = Service.shared.guiController.viewStore - + let customCommandTemplateProcessor = CustomCommandTemplateProcessor() let newExtraSystemPrompt = extraSystemPrompt.map(customCommandTemplateProcessor.process) let newPrompt = prompt.map(customCommandTemplateProcessor.process) diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift index 8140bad9..84446c9d 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/ChatPanelFeature.swift @@ -77,8 +77,8 @@ public struct ChatPanelFeature: ReducerProtocol { @Dependency(\.suggestionWidgetControllerDependency) var suggestionWidgetControllerDependency @Dependency(\.xcodeInspector) var xcodeInspector - @Dependency(\.activatePreviouslyActiveXcode) var activatePreviouslyActiveXcode - @Dependency(\.activateExtensionService) var activateExtensionService + @Dependency(\.activatePreviousActiveXcode) var activatePreviouslyActiveXcode + @Dependency(\.activateThisApp) var activateExtensionService @Dependency(\.chatTabBuilderCollection) var chatTabBuilderCollection public var body: some ReducerProtocol { @@ -88,7 +88,7 @@ public struct ChatPanelFeature: ReducerProtocol { state.isPanelDisplayed = false return .run { _ in - await activatePreviouslyActiveXcode() + activatePreviouslyActiveXcode() } case .closeActiveTabClicked: @@ -119,7 +119,7 @@ public struct ChatPanelFeature: ReducerProtocol { } state.isPanelDisplayed = true return .run { send in - await activateExtensionService() + activateExtensionService() await send(.focusActiveChatTab) } diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift index e090a676..8bee035f 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift @@ -37,6 +37,7 @@ public struct PanelFeature: ReducerProtocol { @Dependency(\.suggestionWidgetControllerDependency) var suggestionWidgetControllerDependency @Dependency(\.xcodeInspector) var xcodeInspector + @Dependency(\.activateThisApp) var activateThisApp var windows: WidgetWindows { suggestionWidgetControllerDependency.windows } public var body: some ReducerProtocol { @@ -121,9 +122,7 @@ public struct PanelFeature: ReducerProtocol { await send(.displayPanelContent) if hasPromptToCode { - // looks like we need a delay. - try await Task.sleep(nanoseconds: 150_000_000) - await NSApplication.shared.activate(ignoringOtherApps: true) + activateThisApp() await windows.sharedPanelWindow.makeKey() } }.animation(.easeInOut(duration: 0.2)) diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift index 9012535a..d3b407b2 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift @@ -80,6 +80,7 @@ public struct PromptToCodeGroup: ReducerProtocol { } @Dependency(\.promptToCodeServiceFactory) var promptToCodeServiceFactory + @Dependency(\.activatePreviousActiveXcode) var activatePreviousActiveXcode public var body: some ReducerProtocol { Reduce { state, action in @@ -143,7 +144,7 @@ public struct PromptToCodeGroup: ReducerProtocol { case .cancelButtonTapped: state.promptToCodes.remove(id: id) return .run { _ in - try await Environment.makeXcodeActive() + activatePreviousActiveXcode() } default: return .none diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift index ebb40d97..986336aa 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift @@ -1,4 +1,5 @@ import ActiveApplicationMonitor +import AppActivator import AsyncAlgorithms import AXNotificationStream import ComposableArchitecture @@ -133,6 +134,8 @@ public struct WidgetFeature: ReducerProtocol { @Dependency(\.activeApplicationMonitor) var activeApplicationMonitor @Dependency(\.xcodeInspector) var xcodeInspector @Dependency(\.mainQueue) var mainQueue + @Dependency(\.activateThisApp) var activateThisApp + @Dependency(\.activatePreviousActiveApp) var activatePreviousActiveApp public enum DebounceKey: Hashable { case updateWindowOpacity @@ -163,20 +166,26 @@ public struct WidgetFeature: ReducerProtocol { state.panelState.suggestionPanelState.isPanelDisplayed = true state.chatPanelState.isPanelDisplayed = true } + let isDisplayingContent = state._circularWidgetState.isDisplayingContent + let hasChat = state.chatPanelState.chatTabGroup.selectedTabInfo != nil + let hasPromptToCode = state.panelState.sharedPanelState.content + .promptToCodeGroup.activePromptToCode != nil + return .run { send in if isDisplayingContent { + if hasPromptToCode { + await send(.updateKeyWindow(.sharedPanel)) + } else if hasChat { + await send(.updateKeyWindow(.chatPanel)) + } await send(.chatPanel(.focusActiveChatTab)) } - + if isDisplayingContent, !(await NSApplication.shared.isActive) { - try await Task.sleep(nanoseconds: 50_000_000) - await NSApplication.shared.activate(ignoringOtherApps: true) - } else if !isDisplayingContent, - let app = xcodeInspector.previousActiveApplication - { - try await Task.sleep(nanoseconds: 20_000_000) - app.runningApplication.activate() + activateThisApp() + } else if !isDisplayingContent { + activatePreviousActiveApp() } } @@ -599,7 +608,7 @@ public struct WidgetFeature: ReducerProtocol { return .none case let .updateKeyWindow(window): - return .run { _ in + return .run { send in switch window { case .chatPanel: await windows.chatPanelWindow.makeKeyAndOrderFront(nil) diff --git a/Core/Sources/SuggestionWidget/ModuleDependency.swift b/Core/Sources/SuggestionWidget/ModuleDependency.swift index 0a38ee6e..3fbebeab 100644 --- a/Core/Sources/SuggestionWidget/ModuleDependency.swift +++ b/Core/Sources/SuggestionWidget/ModuleDependency.swift @@ -78,23 +78,6 @@ struct ChatTabBuilderCollectionKey: DependencyKey { static let liveValue: () -> [ChatTabBuilderCollection] = { [] } } -struct ActivatePreviouslyActiveXcodeKey: DependencyKey { - static let liveValue = { @MainActor in - @Dependency(\.activeApplicationMonitor) var activeApplicationMonitor - if let app = activeApplicationMonitor.previousApp, app.isXcode { - try? await Task.sleep(nanoseconds: 200_000_000) - app.activate() - } - } -} - -struct ActivateExtensionServiceKey: DependencyKey { - static let liveValue = { @MainActor in - try? await Task.sleep(nanoseconds: 150_000_000) - NSApplication.shared.activate(ignoringOtherApps: true) - } -} - public extension DependencyValues { var suggestionWidgetControllerDependency: SuggestionWidgetControllerDependency { get { self[SuggestionWidgetControllerDependencyKey.self] } @@ -122,15 +105,5 @@ extension DependencyValues { get { self[ActiveApplicationMonitorKey.self] } set { self[ActiveApplicationMonitorKey.self] = newValue } } - - var activatePreviouslyActiveXcode: () async -> Void { - get { self[ActivatePreviouslyActiveXcodeKey.self] } - set { self[ActivatePreviouslyActiveXcodeKey.self] = newValue } - } - - var activateExtensionService: () async -> Void { - get { self[ActivateExtensionServiceKey.self] } - set { self[ActivateExtensionServiceKey.self] = newValue } - } } diff --git a/Tool/Package.swift b/Tool/Package.swift index d8af318b..de775e9a 100644 --- a/Tool/Package.swift +++ b/Tool/Package.swift @@ -39,6 +39,7 @@ let package = Package( "ActiveApplicationMonitor", "AXExtension", "AXNotificationStream", + "AppActivator", ] ), ], @@ -111,6 +112,14 @@ let package = Package( ] ), + .target( + name: "AppActivator", + dependencies: [ + "XcodeInspector", + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + ] + ), + .target(name: "ActiveApplicationMonitor"), .target(name: "USearchIndex", dependencies: [ diff --git a/Tool/Sources/AppActivator/AppActivator.swift b/Tool/Sources/AppActivator/AppActivator.swift new file mode 100644 index 00000000..e3c7ffea --- /dev/null +++ b/Tool/Sources/AppActivator/AppActivator.swift @@ -0,0 +1,61 @@ +import AppKit +import Dependencies +import XcodeInspector + +public extension NSWorkspace { + static func activateThisApp(delay: TimeInterval = 0.5) { + Task { @MainActor in + try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) + // NSApp.activate may fail. + NSRunningApplication( + processIdentifier: ProcessInfo.processInfo.processIdentifier + )?.activate() + } + } + + static func activatePreviousActiveApp(delay: TimeInterval = 0.2) { + Task { @MainActor in + guard let app = XcodeInspector.shared.previousActiveApplication else { return } + try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) + app.runningApplication.activate() + } + } + + static func activatePreviousActiveXcode(delay: TimeInterval = 0.2) { + Task { @MainActor in + guard let app = XcodeInspector.shared.latestActiveXcode else { return } + try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) + app.runningApplication.activate() + } + } +} + +struct ActivateThisAppDependencyKey: DependencyKey { + static var liveValue: () -> Void = { NSWorkspace.activateThisApp() } +} + +struct ActivatePreviousActiveAppDependencyKey: DependencyKey { + static var liveValue: () -> Void = { NSWorkspace.activatePreviousActiveApp() } +} + +struct ActivatePreviousActiveXcodeDependencyKey: DependencyKey { + static var liveValue: () -> Void = { NSWorkspace.activatePreviousActiveXcode() } +} + +public extension DependencyValues { + var activateThisApp: () -> Void { + get { self[ActivateThisAppDependencyKey.self] } + set { self[ActivateThisAppDependencyKey.self] = newValue } + } + + var activatePreviousActiveApp: () -> Void { + get { self[ActivatePreviousActiveAppDependencyKey.self] } + set { self[ActivatePreviousActiveAppDependencyKey.self] = newValue } + } + + var activatePreviousActiveXcode: () -> Void { + get { self[ActivatePreviousActiveXcodeDependencyKey.self] } + set { self[ActivatePreviousActiveXcodeDependencyKey.self] = newValue } + } +} + diff --git a/Tool/Sources/Environment/Environment.swift b/Tool/Sources/Environment/Environment.swift index f1315daf..5beae16d 100644 --- a/Tool/Sources/Environment/Environment.swift +++ b/Tool/Sources/Environment/Environment.swift @@ -223,15 +223,6 @@ public enum Environment { } } } - - public static var makeXcodeActive: () async throws -> Void = { - let appleScript = """ - tell application "Xcode" - activate - end tell - """ - try await runAppleScript(appleScript) - } } @discardableResult diff --git a/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift b/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift index e938cc5e..5203bb51 100644 --- a/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift +++ b/Tool/Sources/FocusedCodeFinder/FocusedCodeFinder.swift @@ -67,7 +67,7 @@ public struct UnknownLanguageFocusedCodeFinder: FocusedCodeFinder { guard !activeDocumentContext.lines.isEmpty else { return .empty } // when user is not selecting any code. - if containingRange.start == containingRange.end{ + if containingRange.start == containingRange.end { // search up and down for up to `proposedSearchRange * 2 + 1` lines. let lines = activeDocumentContext.lines let proposedLineCount = proposedSearchRange * 2 + 1 @@ -80,6 +80,7 @@ public struct UnknownLanguageFocusedCodeFinder: FocusedCodeFinder { lines.count - 1 ) + guard endLineIndex >= startLineIndex else { return .empty } let focusedLines = lines[startLineIndex...endLineIndex] let contextStartLine = max(startLineIndex - 5, 0) From a0d46cabb16bdef9a9bd8aa74f699488dd0a6518 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 17 Nov 2023 21:19:16 +0800 Subject: [PATCH 09/19] Activate extension app when prompt to code is activated --- .../FeatureReducers/WidgetFeature.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift index 986336aa..efe232a5 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift @@ -196,6 +196,17 @@ public struct WidgetFeature: ReducerProtocol { Scope(state: \.panelState, action: /Action.panel) { PanelFeature() } + + Reduce { state, action in + switch action { + case .panel(.sharedPanel(.promptToCodeGroup(.activateOrCreatePromptToCode))): + return .run { send in + await send(.updateKeyWindow(.sharedPanel)) + activateThisApp() + } + default: return .none + } + } Scope(state: \.chatPanelState, action: /Action.chatPanel) { ChatPanelFeature() From 22bf25547e0743af3828fd93969a2264fe81fa98 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 17 Nov 2023 21:27:24 +0800 Subject: [PATCH 10/19] Update to control prompt to code focus state in reducer --- .../FeatureReducers/PromptToCode.swift | 10 ++++++++++ .../FeatureReducers/PromptToCodeGroup.swift | 6 ++++-- .../PromptToCodePanel.swift | 19 ++++++++++++------- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCode.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCode.swift index bfac06d0..a4f5886c 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCode.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCode.swift @@ -43,6 +43,10 @@ public struct PromptToCode: ReducerProtocol { } } } + + public enum FocusField: Equatable { + case textField + } public var id: URL { documentURL } public var history: HistoryNode @@ -63,6 +67,7 @@ public struct PromptToCode: ReducerProtocol { @BindingState public var prompt: String @BindingState public var isContinuous: Bool @BindingState public var isAttachedToSelectionRange: Bool + @BindingState public var focusedField: FocusField? = .textField public var filename: String { documentURL.lastPathComponent } public var canRevert: Bool { history != .empty } @@ -114,6 +119,7 @@ public struct PromptToCode: ReducerProtocol { public enum Action: Equatable, BindableAction { case binding(BindingAction) + case focusOnTextField case selectionRangeToggleTapped case modifyCodeButtonTapped case revertButtonTapped @@ -142,6 +148,10 @@ public struct PromptToCode: ReducerProtocol { switch action { case .binding: return .none + + case .focusOnTextField: + state.focusedField = .textField + return .none case .selectionRangeToggleTapped: state.isAttachedToSelectionRange.toggle() diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift index d3b407b2..5fff601a 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift @@ -86,8 +86,10 @@ public struct PromptToCodeGroup: ReducerProtocol { Reduce { state, action in switch action { case let .activateOrCreatePromptToCode(s): - guard state.activePromptToCode == nil else { - return .none + if let promptToCode = state.activePromptToCode { + return .run { send in + await send(.promptToCode(promptToCode.id, .focusOnTextField)) + } } return .run { send in await send(.createPromptToCode(s)) diff --git a/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift b/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift index b20190aa..0f3ae4f0 100644 --- a/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift +++ b/Core/Sources/SuggestionWidget/SuggestionPanelContent/PromptToCodePanel.swift @@ -290,7 +290,7 @@ extension PromptToCodePanel { struct Toolbar: View { let store: StoreOf - @FocusState var isInputAreaFocused: Bool + @FocusState var focusField: PromptToCode.State.FocusField? struct RevertButtonState: Equatable { var isResponding: Bool @@ -299,6 +299,7 @@ extension PromptToCodePanel { struct InputFieldState: Equatable { @BindingViewState var prompt: String + @BindingViewState var focusField: PromptToCode.State.FocusField? var isResponding: Bool } @@ -326,15 +327,12 @@ extension PromptToCodePanel { .keyboardShortcut(KeyEquivalent.return, modifiers: [.shift]) } .background { - Button(action: { isInputAreaFocused = true }) { + Button(action: { focusField = .textField }) { EmptyView() } .keyboardShortcut("l", modifiers: [.command]) } } - .onAppear { - isInputAreaFocused = true - } .padding(8) .background(.ultraThickMaterial) } @@ -366,7 +364,13 @@ extension PromptToCodePanel { var inputField: some View { WithViewStore( store, - observe: { InputFieldState(prompt: $0.$prompt, isResponding: $0.isResponding) } + observe: { + InputFieldState( + prompt: $0.$prompt, + focusField: $0.$focusedField, + isResponding: $0.isResponding + ) + } ) { viewStore in ZStack(alignment: .center) { // a hack to support dynamic height of TextEditor @@ -389,8 +393,9 @@ extension PromptToCodePanel { .opacity(viewStore.state.isResponding ? 0.5 : 1) .disabled(viewStore.state.isResponding) } + .focused($focusField, equals: .textField) + .bind(viewStore.$focusField, to: $focusField) } - .focused($isInputAreaFocused) .padding(8) .fixedSize(horizontal: false, vertical: true) } From 80acb921fcf11d1da9784bd45e4c96fdab78eac5 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 17 Nov 2023 21:38:52 +0800 Subject: [PATCH 11/19] Add missing weak self --- Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift b/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift index 796934a5..4d5f0a38 100644 --- a/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift +++ b/Core/Sources/ChatGPTChatTab/ChatGPTChatTab.swift @@ -122,13 +122,13 @@ public class ChatGPTChatTab: ChatTab { } }.store(in: &cancellable) - service.$systemPrompt.removeDuplicates().sink { _ in + service.$systemPrompt.removeDuplicates().sink { [weak self] _ in Task { @MainActor [weak self] in self?.chatTabViewStore.send(.tabContentUpdated) } }.store(in: &cancellable) - service.$extraSystemPrompt.removeDuplicates().sink { _ in + service.$extraSystemPrompt.removeDuplicates().sink { [weak self] _ in Task { @MainActor [weak self] in self?.chatTabViewStore.send(.tabContentUpdated) } @@ -140,12 +140,11 @@ public class ChatGPTChatTab: ChatTab { } }.store(in: &cancellable) - viewStore.publisher.removeDuplicates() - .sink { [weak self] _ in - Task { @MainActor [weak self] in - self?.chatTabViewStore.send(.tabContentUpdated) - } - }.store(in: &cancellable) + viewStore.publisher.removeDuplicates().sink { [weak self] _ in + Task { @MainActor [weak self] in + self?.chatTabViewStore.send(.tabContentUpdated) + } + }.store(in: &cancellable) } } From aa0fe32a508cfe55a0ccd21e501061d867974852 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 17 Nov 2023 22:14:24 +0800 Subject: [PATCH 12/19] Adjust timing issue of prompt to code --- .../FeatureReducers/PanelFeature.swift | 2 +- .../FeatureReducers/PromptToCodeGroup.swift | 44 ++++++++++++++----- .../FeatureReducers/WidgetFeature.swift | 6 +-- .../SuggestionWidget/SharedPanelView.swift | 11 +++-- 4 files changed, 44 insertions(+), 19 deletions(-) diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift index 8bee035f..589a8a11 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PanelFeature.swift @@ -111,7 +111,7 @@ public struct PanelFeature: ReducerProtocol { case .removeDisplayedContent: state.content.error = nil - state.content.promptToCodeGroup.activePromptToCode = nil + state.content.promptToCodeGroup.activeDocumentURL = nil state.content.suggestion = nil return .none diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift index 5fff601a..339b45e8 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/PromptToCodeGroup.swift @@ -18,7 +18,12 @@ public struct PromptToCodeGroup: ReducerProtocol { guard let id = activeDocumentURL else { return nil } return promptToCodes[id: id] } - set { activeDocumentURL = newValue?.id } + set { + activeDocumentURL = newValue?.id + if let id = newValue?.id { + promptToCodes[id: id] = newValue + } + } } } @@ -77,6 +82,7 @@ public struct PromptToCodeGroup: ReducerProtocol { case updateActivePromptToCode(documentURL: URL) case discardExpiredPromptToCode(documentURLs: [URL]) case promptToCode(PromptToCode.State.ID, PromptToCode.Action) + case activePromptToCode(PromptToCode.Action) } @Dependency(\.promptToCodeServiceFactory) var promptToCodeServiceFactory @@ -141,22 +147,38 @@ public struct PromptToCodeGroup: ReducerProtocol { } return .none - case let .promptToCode(id, action): - switch action { - case .cancelButtonTapped: - state.promptToCodes.remove(id: id) - return .run { _ in - activatePreviousActiveXcode() - } - default: - return .none - } + case .promptToCode: + return .none + + case .activePromptToCode: + return .none } } + .ifLet(\.activePromptToCode, action: /Action.activePromptToCode) { + PromptToCode() + .dependency(\.promptToCodeService, promptToCodeServiceFactory()) + } .forEach(\.promptToCodes, action: /Action.promptToCode, element: { PromptToCode() .dependency(\.promptToCodeService, promptToCodeServiceFactory()) }) + + Reduce { state, action in + switch action { + case let .promptToCode(id, .cancelButtonTapped): + state.promptToCodes.remove(id: id) + return .run { _ in + activatePreviousActiveXcode() + } + case .activePromptToCode(.cancelButtonTapped): + guard let id = state.activePromptToCode?.id else { return .none } + state.promptToCodes.remove(id: id) + return .run { _ in + activatePreviousActiveXcode() + } + default: return .none + } + } } } diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift index efe232a5..a12cb3aa 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift @@ -196,8 +196,8 @@ public struct WidgetFeature: ReducerProtocol { Scope(state: \.panelState, action: /Action.panel) { PanelFeature() } - - Reduce { state, action in + + Reduce { _, action in switch action { case .panel(.sharedPanel(.promptToCodeGroup(.activateOrCreatePromptToCode))): return .run { send in @@ -619,7 +619,7 @@ public struct WidgetFeature: ReducerProtocol { return .none case let .updateKeyWindow(window): - return .run { send in + return .run { _ in switch window { case .chatPanel: await windows.chatPanelWindow.makeKeyAndOrderFront(nil) diff --git a/Core/Sources/SuggestionWidget/SharedPanelView.swift b/Core/Sources/SuggestionWidget/SharedPanelView.swift index cf4bac14..c16f6cc1 100644 --- a/Core/Sources/SuggestionWidget/SharedPanelView.swift +++ b/Core/Sources/SuggestionWidget/SharedPanelView.swift @@ -57,13 +57,16 @@ struct SharedPanelView: View { ) } } else if let promptToCode = viewStore.state.promptToCode { - PromptToCodePanel(store: store.scope( - state: { _ in promptToCode }, + IfLetStore(store.scope( + state: { $0.content.promptToCodeGroup.activePromptToCode }, action: { SharedPanelFeature.Action - .promptToCodeGroup(.promptToCode(promptToCode.id, $0)) + .promptToCodeGroup(.activePromptToCode($0)) } - )) + )) { + PromptToCodePanel(store: $0) + } + } else if let suggestion = viewStore.state.suggestion { switch suggestionPresentationMode { case .nearbyTextCursor: From fa2775a16d4ab67b70fd42a768035d199ef8c29f Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 17 Nov 2023 22:19:01 +0800 Subject: [PATCH 13/19] Re activate extension after accepting prompt to code when continuous --- Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift index 1b85ac3f..981b062b 100644 --- a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift +++ b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift @@ -278,6 +278,8 @@ public final class GraphicalUserInterfaceController { await handler.acceptPromptToCode() if !promptToCode.isContinuous { NSWorkspace.activatePreviousActiveXcode() + } else { + NSWorkspace.activateThisApp() } } } From ca98d1bc8db6dc82b48667fe9fcf5f25e7b30f2d Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Fri, 17 Nov 2023 22:28:41 +0800 Subject: [PATCH 14/19] Ignore chunk id --- Tool/Sources/OpenAIService/ChatGPTService.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tool/Sources/OpenAIService/ChatGPTService.swift b/Tool/Sources/OpenAIService/ChatGPTService.swift index 640bf73a..74d1f547 100644 --- a/Tool/Sources/OpenAIService/ChatGPTService.swift +++ b/Tool/Sources/OpenAIService/ChatGPTService.swift @@ -274,7 +274,7 @@ extension ChatGPTService { do { let (trunks, cancel) = try await api() cancelTask = cancel - let proposedId = UUID().uuidString + let proposedId = UUID().uuidString + String(Date().timeIntervalSince1970) for try await trunk in trunks { guard let delta = trunk.choices?.first?.delta else { continue } @@ -290,7 +290,7 @@ extension ChatGPTService { } await memory.streamMessage( - id: trunk.id ?? proposedId, + id: proposedId, role: delta.role, content: delta.content, functionCall: functionCall From 0f3f3e6ff8635796486c55de7ae684dfebe3be06 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 18 Nov 2023 01:17:41 +0800 Subject: [PATCH 15/19] Fix cancellation --- Pro | 2 +- Tool/Sources/OpenAIService/ChatGPTService.swift | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Pro b/Pro index ce6f1630..22e5f0a2 160000 --- a/Pro +++ b/Pro @@ -1 +1 @@ -Subproject commit ce6f1630793d46564d658d511d423048edc443f3 +Subproject commit 22e5f0a2a07e6ddc1c95320c65dcac6305b8683b diff --git a/Tool/Sources/OpenAIService/ChatGPTService.swift b/Tool/Sources/OpenAIService/ChatGPTService.swift index 74d1f547..b8aa94a0 100644 --- a/Tool/Sources/OpenAIService/ChatGPTService.swift +++ b/Tool/Sources/OpenAIService/ChatGPTService.swift @@ -100,12 +100,13 @@ public class ChatGPTService: ChatGPTServiceType { return Debugger.$id.withValue(.init()) { AsyncThrowingStream { continuation in - Task(priority: .userInitiated) { + let task = Task(priority: .userInitiated) { do { var functionCall: ChatMessage.FunctionCall? var functionCallMessageID = "" var isInitialCall = true loop: while functionCall != nil || isInitialCall { + try Task.checkCancellation() isInitialCall = false if let call = functionCall { if !configuration.runFunctionsAutomatically { @@ -121,6 +122,7 @@ public class ChatGPTService: ChatGPTServiceType { #endif for try await content in stream { + try Task.checkCancellation() switch content { case let .text(text): continuation.yield(text) @@ -154,6 +156,9 @@ public class ChatGPTService: ChatGPTServiceType { continuation.finish(throwing: error) } } + continuation.onTermination = { _ in + task.cancel() + } } } } @@ -177,6 +182,7 @@ public class ChatGPTService: ChatGPTServiceType { var finalResult = message?.content var functionCall = message?.functionCall while let call = functionCall { + try Task.checkCancellation() if !configuration.runFunctionsAutomatically { break } @@ -270,12 +276,13 @@ extension ChatGPTService { #endif return AsyncThrowingStream { continuation in - Task { + let task = Task { do { let (trunks, cancel) = try await api() cancelTask = cancel let proposedId = UUID().uuidString + String(Date().timeIntervalSince1970) for try await trunk in trunks { + try Task.checkCancellation() guard let delta = trunk.choices?.first?.delta else { continue } // The api will always return a function call with JSON object. @@ -320,6 +327,10 @@ extension ChatGPTService { continuation.finish(throwing: error) } } + + continuation.onTermination = { _ in + task.cancel() + } } } From 15c8ba869931be3eb01428442f3be73dd2962e9a Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 18 Nov 2023 03:08:15 +0800 Subject: [PATCH 16/19] Add shouldEndTextWindow to configuration --- Core/Sources/ChatService/ChatService.swift | 4 ++++ .../Configuration/ChatGPTConfiguration.swift | 1 + .../UserPreferenceChatGPTConfiguration.swift | 11 ++++++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Core/Sources/ChatService/ChatService.swift b/Core/Sources/ChatService/ChatService.swift index 30a49b6f..2cbaee67 100644 --- a/Core/Sources/ChatService/ChatService.swift +++ b/Core/Sources/ChatService/ChatService.swift @@ -42,6 +42,10 @@ public final class ChatService: ObservableObject { let configuration = UserPreferenceChatGPTConfiguration().overriding() /// Used by context collector let extraConfiguration = configuration.overriding() + extraConfiguration.textWindowTerminator = { + guard let last = $0.last else { return false } + return last.isNewline || last.isPunctuation + } let memory = ContextAwareAutoManagedChatGPTMemory( configuration: extraConfiguration, functionProvider: ChatFunctionProvider() diff --git a/Tool/Sources/OpenAIService/Configuration/ChatGPTConfiguration.swift b/Tool/Sources/OpenAIService/Configuration/ChatGPTConfiguration.swift index aa441c67..9f464233 100644 --- a/Tool/Sources/OpenAIService/Configuration/ChatGPTConfiguration.swift +++ b/Tool/Sources/OpenAIService/Configuration/ChatGPTConfiguration.swift @@ -11,6 +11,7 @@ public protocol ChatGPTConfiguration { var maxTokens: Int { get } var minimumReplyTokens: Int { get } var runFunctionsAutomatically: Bool { get } + var shouldEndTextWindow: (String) -> Bool { get } } public extension ChatGPTConfiguration { diff --git a/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift b/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift index 54b7fbf2..2272d60e 100644 --- a/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift +++ b/Tool/Sources/OpenAIService/Configuration/UserPreferenceChatGPTConfiguration.swift @@ -40,6 +40,10 @@ public struct UserPreferenceChatGPTConfiguration: ChatGPTConfiguration { public var runFunctionsAutomatically: Bool { true } + + public var shouldEndTextWindow: (String) -> Bool { + { _ in true } + } public init(chatModelKey: KeyPath>? = nil) { self.chatModelKey = chatModelKey @@ -56,7 +60,7 @@ public class OverridingChatGPTConfiguration: ChatGPTConfiguration { public var minimumReplyTokens: Int? public var runFunctionsAutomatically: Bool? public var apiKey: String? - + public init( temperature: Double? = nil, modelId: String? = nil, @@ -80,6 +84,7 @@ public class OverridingChatGPTConfiguration: ChatGPTConfiguration { private let configuration: ChatGPTConfiguration public var overriding = Overriding() + public var textWindowTerminator: ((String) -> Bool)? public init( overriding configuration: any ChatGPTConfiguration, @@ -126,5 +131,9 @@ public class OverridingChatGPTConfiguration: ChatGPTConfiguration { guard let name = model?.info.apiKeyName else { return configuration.apiKey } return (try? Keychain.apiKey.get(name)) ?? configuration.apiKey } + + public var shouldEndTextWindow: (String) -> Bool { + textWindowTerminator ?? configuration.shouldEndTextWindow + } } From 1ca49936c486af7ac89d71126a6509da0d4be518 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 18 Nov 2023 03:08:39 +0800 Subject: [PATCH 17/19] Bump version to 0.27.1 --- Version.xcconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Version.xcconfig b/Version.xcconfig index 7664fb23..0a2b0d10 100644 --- a/Version.xcconfig +++ b/Version.xcconfig @@ -1,3 +1,3 @@ -APP_VERSION = 0.27.0 -APP_BUILD = 280 +APP_VERSION = 0.27.1 +APP_BUILD = 281 From ef1f42a35349e64ac3145eac6f9cd58e32773e77 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 18 Nov 2023 12:49:14 +0800 Subject: [PATCH 18/19] Workaround cooperative app activation --- .../GraphicalUserInterfaceController.swift | 4 +- .../FeatureReducers/WidgetFeature.swift | 11 ---- Tool/Sources/AppActivator/AppActivator.swift | 59 ++++++++++++++++--- 3 files changed, 53 insertions(+), 21 deletions(-) diff --git a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift index 981b062b..9f402411 100644 --- a/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift +++ b/Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift @@ -141,9 +141,7 @@ struct GUI: ReducerProtocol { ) await send(.suggestionWidget(.updateKeyWindow(.chatPanel))) - if await !NSApplication.shared.isActive { - activateThisApp() - } + activateThisApp() } case .createChatGPTChatTabIfNeeded: diff --git a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift index a12cb3aa..3465e422 100644 --- a/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift +++ b/Core/Sources/SuggestionWidget/FeatureReducers/WidgetFeature.swift @@ -197,17 +197,6 @@ public struct WidgetFeature: ReducerProtocol { PanelFeature() } - Reduce { _, action in - switch action { - case .panel(.sharedPanel(.promptToCodeGroup(.activateOrCreatePromptToCode))): - return .run { send in - await send(.updateKeyWindow(.sharedPanel)) - activateThisApp() - } - default: return .none - } - } - Scope(state: \.chatPanelState, action: /Action.chatPanel) { ChatPanelFeature() } diff --git a/Tool/Sources/AppActivator/AppActivator.swift b/Tool/Sources/AppActivator/AppActivator.swift index e3c7ffea..ed83287a 100644 --- a/Tool/Sources/AppActivator/AppActivator.swift +++ b/Tool/Sources/AppActivator/AppActivator.swift @@ -3,13 +3,27 @@ import Dependencies import XcodeInspector public extension NSWorkspace { - static func activateThisApp(delay: TimeInterval = 0.5) { + static func activateThisApp(delay: TimeInterval = 0.3) { Task { @MainActor in try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) - // NSApp.activate may fail. - NSRunningApplication( - processIdentifier: ProcessInfo.processInfo.processIdentifier - )?.activate() + + // NSApp.activate may fail. And since macOS 14, it looks like the app needs other + // apps to call `yieldActivationToApplication` to activate itself? + + let activated = NSRunningApplication.current + .activate(options: [.activateIgnoringOtherApps]) + + if activated { return } + + // Fallback solution + + let appleScript = """ + tell application "System Events" + set frontmost of the first process whose unix id is \ + \(ProcessInfo.processInfo.processIdentifier) to true + end tell + """ + try await runAppleScript(appleScript) } } @@ -20,7 +34,7 @@ public extension NSWorkspace { app.runningApplication.activate() } } - + static func activatePreviousActiveXcode(delay: TimeInterval = 0.2) { Task { @MainActor in guard let app = XcodeInspector.shared.latestActiveXcode else { return } @@ -52,10 +66,41 @@ public extension DependencyValues { get { self[ActivatePreviousActiveAppDependencyKey.self] } set { self[ActivatePreviousActiveAppDependencyKey.self] = newValue } } - + var activatePreviousActiveXcode: () -> Void { get { self[ActivatePreviousActiveXcodeDependencyKey.self] } set { self[ActivatePreviousActiveXcodeDependencyKey.self] = newValue } } } +@discardableResult +func runAppleScript(_ appleScript: String) async throws -> String { + let task = Process() + task.launchPath = "/usr/bin/osascript" + task.arguments = ["-e", appleScript] + let outpipe = Pipe() + task.standardOutput = outpipe + task.standardError = Pipe() + + return try await withUnsafeThrowingContinuation { continuation in + do { + task.terminationHandler = { _ in + do { + if let data = try outpipe.fileHandleForReading.readToEnd(), + let content = String(data: data, encoding: .utf8) + { + continuation.resume(returning: content) + return + } + continuation.resume(returning: "") + } catch { + continuation.resume(throwing: error) + } + } + try task.run() + } catch { + continuation.resume(throwing: error) + } + } +} + From 93ce52095cfaca2333893f4472d1564905ace270 Mon Sep 17 00:00:00 2001 From: Shx Guo Date: Sat, 18 Nov 2023 12:49:45 +0800 Subject: [PATCH 19/19] Update appcast.xml --- appcast.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/appcast.xml b/appcast.xml index 19c2b094..b9eea600 100644 --- a/appcast.xml +++ b/appcast.xml @@ -3,6 +3,18 @@ Copilot for Xcode + + 0.27.1 + Sat, 18 Nov 2023 12:46:36 +0800 + 281 + 0.27.1 + 12.0 + + https://github.com/intitni/CopilotForXcode/releases/tag/0.27.1 + + + + 0.27.0 Fri, 10 Nov 2023 02:34:25 +0800