Skip to content

Commit 930bd2d

Browse files
committed
Merge branch 'feature/custom-open-chat' into develop
2 parents b39a32b + 6759c8c commit 930bd2d

15 files changed

Lines changed: 199 additions & 37 deletions

File tree

Copilot for Xcode.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
C882175C294187EF00A22FD3 /* Client in Frameworks */ = {isa = PBXBuildFile; productRef = C882175B294187EF00A22FD3 /* Client */; };
5454
C89E75C32A46FB32000DD64F /* AppDelegate+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C89E75C22A46FB32000DD64F /* AppDelegate+Menu.swift */; };
5555
C8C8B60929AFA35F00034BEE /* CopilotForXcodeExtensionService.app in Embed XPCService */ = {isa = PBXBuildFile; fileRef = C861E60E2994F6070056CB02 /* CopilotForXcodeExtensionService.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
56-
C8DCF00029CE11D500FDDDD7 /* ChatWithSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8DCEFFF29CE11D500FDDDD7 /* ChatWithSelection.swift */; };
56+
C8DCF00029CE11D500FDDDD7 /* OpenChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8DCEFFF29CE11D500FDDDD7 /* OpenChat.swift */; };
5757
C8DD9CB12BC673F80036641C /* CloseIdleTabsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8DD9CB02BC673F80036641C /* CloseIdleTabsCommand.swift */; };
5858
/* End PBXBuildFile section */
5959

@@ -236,7 +236,7 @@
236236
C887BC832965D96000931567 /* DEVELOPMENT.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = DEVELOPMENT.md; sourceTree = "<group>"; };
237237
C89E75C22A46FB32000DD64F /* AppDelegate+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Menu.swift"; sourceTree = "<group>"; };
238238
C8CD828229B88006008D044D /* TestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestPlan.xctestplan; sourceTree = "<group>"; };
239-
C8DCEFFF29CE11D500FDDDD7 /* ChatWithSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatWithSelection.swift; sourceTree = "<group>"; };
239+
C8DCEFFF29CE11D500FDDDD7 /* OpenChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenChat.swift; sourceTree = "<group>"; };
240240
C8DD9CB02BC673F80036641C /* CloseIdleTabsCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseIdleTabsCommand.swift; sourceTree = "<group>"; };
241241
C8F103292A7A365000D28F4F /* launchAgent.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = launchAgent.plist; sourceTree = "<group>"; };
242242
/* End PBXFileReference section */
@@ -320,7 +320,7 @@
320320
C8009C022941C576007AA7E8 /* RealtimeSuggestionCommand.swift */,
321321
C800DBB0294C624D00B04CAC /* PrefetchSuggestionsCommand.swift */,
322322
C8758E6F29F04BFF00D29C1C /* CustomCommand.swift */,
323-
C8DCEFFF29CE11D500FDDDD7 /* ChatWithSelection.swift */,
323+
C8DCEFFF29CE11D500FDDDD7 /* OpenChat.swift */,
324324
C8DD9CB02BC673F80036641C /* CloseIdleTabsCommand.swift */,
325325
C861A6A229E5503F005C41A3 /* PromptToCodeCommand.swift */,
326326
C81458972939EFDC00135263 /* Info.plist */,
@@ -675,7 +675,7 @@
675675
isa = PBXSourcesBuildPhase;
676676
buildActionMask = 2147483647;
677677
files = (
678-
C8DCF00029CE11D500FDDDD7 /* ChatWithSelection.swift in Sources */,
678+
C8DCF00029CE11D500FDDDD7 /* OpenChat.swift in Sources */,
679679
C81458942939EFDC00135263 /* SourceEditorExtension.swift in Sources */,
680680
C8DD9CB12BC673F80036641C /* CloseIdleTabsCommand.swift in Sources */,
681681
C8758E7029F04BFF00D29C1C /* CustomCommand.swift in Sources */,

Core/Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ let package = Package(
126126
"PromptToCodeService",
127127
"ServiceUpdateMigration",
128128
"ChatGPTChatTab",
129+
"PlusFeatureFlag",
129130
.product(name: "XPCShared", package: "Tool"),
130131
.product(name: "SuggestionProvider", package: "Tool"),
131132
.product(name: "Workspace", package: "Tool"),

Core/Sources/HostApp/FeatureSettings/Chat/ChatSettingsGeneralSectionView.swift

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ struct ChatSettingsGeneralSectionView: View {
2828
@AppStorage(
2929
\.disableFloatOnTopWhenTheChatPanelIsDetached
3030
) var disableFloatOnTopWhenTheChatPanelIsDetached
31+
@AppStorage(\.openChatMode) var openChatMode
32+
@AppStorage(\.openChatInBrowserURL) var openChatInBrowserURL
33+
@AppStorage(\.openChatInBrowserInInAppBrowser) var openChatInBrowserInInAppBrowser
3134

3235
init() {}
3336
}
@@ -39,6 +42,8 @@ struct ChatSettingsGeneralSectionView: View {
3942

4043
var body: some View {
4144
VStack {
45+
openChatSettingsForm
46+
SettingsDivider("Conversation")
4247
chatSettingsForm
4348
SettingsDivider("UI")
4449
uiForm
@@ -47,6 +52,45 @@ struct ChatSettingsGeneralSectionView: View {
4752
}
4853
}
4954

55+
@ViewBuilder
56+
var openChatSettingsForm: some View {
57+
Form {
58+
Picker(
59+
"Open Chat Mode",
60+
selection: $settings.openChatMode
61+
) {
62+
ForEach(OpenChatMode.allCases, id: \.rawValue) { mode in
63+
switch mode {
64+
case .chatPanel:
65+
Text("Open chat panel").tag(mode)
66+
case .browser:
67+
Text("Open web page in browser").tag(mode)
68+
}
69+
}
70+
}
71+
72+
if settings.openChatMode == .browser {
73+
TextField(
74+
"Chat web page URL",
75+
text: $settings.openChatInBrowserURL,
76+
prompt: Text("https://")
77+
)
78+
.textFieldStyle(.roundedBorder)
79+
.disableAutocorrection(true)
80+
.autocorrectionDisabled(true)
81+
82+
#if canImport(ProHostApp)
83+
WithFeatureEnabled(\.browserTab) {
84+
Toggle(
85+
"Open web page in chat panel",
86+
isOn: $settings.openChatInBrowserInInAppBrowser
87+
)
88+
}
89+
#endif
90+
}
91+
}
92+
}
93+
5094
@ViewBuilder
5195
var chatSettingsForm: some View {
5296
Form {

Core/Sources/Service/GUI/GraphicalUserInterfaceController.swift

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ struct GUI {
5555
enum Action {
5656
case start
5757
case openChatPanel(forceDetach: Bool)
58-
case createChatGPTChatTabIfNeeded
58+
case createAndSwitchToChatGPTChatTabIfNeeded
59+
case createAndSwitchToBrowserTabIfNeeded(url: URL)
5960
case sendCustomCommandToActiveChat(CustomCommand)
6061
case toggleWidgetsHotkeyPressed
6162

@@ -145,11 +146,22 @@ struct GUI {
145146
activateThisApp()
146147
}
147148

148-
case .createChatGPTChatTabIfNeeded:
149-
if state.chatTabGroup.tabInfo.contains(where: {
149+
case .createAndSwitchToChatGPTChatTabIfNeeded:
150+
if let selectedTabInfo = state.chatTabGroup.selectedTabInfo,
151+
chatTabPool.getTab(of: selectedTabInfo.id) is ChatGPTChatTab
152+
{
153+
// Already in ChatGPT tab
154+
return .none
155+
}
156+
157+
if let firstChatGPTTabInfo = state.chatTabGroup.tabInfo.first(where: {
150158
chatTabPool.getTab(of: $0.id) is ChatGPTChatTab
151159
}) {
152-
return .none
160+
return .run { send in
161+
await send(.suggestionWidget(.chatPanel(.tabClicked(
162+
id: firstChatGPTTabInfo.id
163+
))))
164+
}
153165
}
154166
return .run { send in
155167
if let (_, chatTabInfo) = await chatTabPool.createTab(for: nil) {
@@ -159,6 +171,53 @@ struct GUI {
159171
}
160172
}
161173

174+
case let .createAndSwitchToBrowserTabIfNeeded(url):
175+
#if canImport(BrowserChatTab)
176+
func match(_ tabURL: URL?) -> Bool {
177+
guard let tabURL else { return false }
178+
return tabURL == url
179+
|| tabURL.absoluteString.hasPrefix(url.absoluteString)
180+
}
181+
182+
if let selectedTabInfo = state.chatTabGroup.selectedTabInfo,
183+
let tab = chatTabPool.getTab(of: selectedTabInfo.id) as? BrowserChatTab,
184+
match(tab.url)
185+
{
186+
// Already in the target Browser tab
187+
return .none
188+
}
189+
190+
if let firstChatGPTTabInfo = state.chatTabGroup.tabInfo.first(where: {
191+
guard let tab = chatTabPool.getTab(of: $0.id) as? BrowserChatTab,
192+
match(tab.url)
193+
else { return false }
194+
return true
195+
}) {
196+
return .run { send in
197+
await send(.suggestionWidget(.chatPanel(.tabClicked(
198+
id: firstChatGPTTabInfo.id
199+
))))
200+
}
201+
}
202+
203+
return .run { send in
204+
if let (_, chatTabInfo) = await chatTabPool.createTab(
205+
for: .init(BrowserChatTab.urlChatBuilder(
206+
url: url,
207+
externalDependency: ChatTabFactory
208+
.externalDependenciesForBrowserChatTab()
209+
))
210+
) {
211+
await send(
212+
.suggestionWidget(.chatPanel(.appendAndSelectTab(chatTabInfo)))
213+
)
214+
}
215+
}
216+
217+
#else
218+
return .none
219+
#endif
220+
162221
case let .sendCustomCommandToActiveChat(command):
163222
@Sendable func stopAndHandleCommand(_ tab: ChatGPTChatTab) async {
164223
if tab.service.isReceivingMessage {
@@ -320,7 +379,7 @@ public final class GraphicalUserInterfaceController {
320379
suggestionDependency.suggestionWidgetDataSource = widgetDataSource
321380
suggestionDependency.onOpenChatClicked = { [weak self] in
322381
Task { [weak self] in
323-
await self?.store.send(.createChatGPTChatTabIfNeeded).finish()
382+
await self?.store.send(.createAndSwitchToChatGPTChatTabIfNeeded).finish()
324383
self?.store.send(.openChatPanel(forceDetach: false))
325384
}
326385
}
@@ -337,10 +396,7 @@ public final class GraphicalUserInterfaceController {
337396
}
338397

339398
public func openGlobalChat() {
340-
Task {
341-
await self.store.send(.createChatGPTChatTabIfNeeded).finish()
342-
store.send(.openChatPanel(forceDetach: true))
343-
}
399+
PseudoCommandHandler().openChat(forceDetach: true)
344400
}
345401
}
346402

Core/Sources/Service/SuggestionCommandHandler/PseudoCommandHandler.swift

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import ActiveApplicationMonitor
22
import AppKit
3+
import Dependencies
4+
import PlusFeatureFlag
35
import Preferences
46
import SuggestionInjector
57
import SuggestionModel
@@ -210,7 +212,13 @@ struct PseudoCommandHandler {
210212
guard let focusElement = application.focusedElement,
211213
focusElement.description == "Source Editor"
212214
else { return }
213-
guard let (content, lines, _, cursorPosition, cursorOffset) = await getFileContent(sourceEditor: nil)
215+
guard let (
216+
content,
217+
lines,
218+
_,
219+
cursorPosition,
220+
cursorOffset
221+
) = await getFileContent(sourceEditor: nil)
214222
else {
215223
PresentInWindowSuggestionPresenter()
216224
.presentErrorMessage("Unable to get file content.")
@@ -266,7 +274,13 @@ struct PseudoCommandHandler {
266274
guard let focusElement = application.focusedElement,
267275
focusElement.description == "Source Editor"
268276
else { return }
269-
guard let (content, lines, _, cursorPosition, cursorOffset) = await getFileContent(sourceEditor: nil)
277+
guard let (
278+
content,
279+
lines,
280+
_,
281+
cursorPosition,
282+
cursorOffset
283+
) = await getFileContent(sourceEditor: nil)
270284
else {
271285
PresentInWindowSuggestionPresenter()
272286
.presentErrorMessage("Unable to get file content.")
@@ -301,6 +315,46 @@ struct PseudoCommandHandler {
301315
await filespace.reset()
302316
PresentInWindowSuggestionPresenter().discardSuggestion(fileURL: documentURL)
303317
}
318+
319+
func openChat(forceDetach: Bool) {
320+
switch UserDefaults.shared.value(for: \.openChatMode) {
321+
case .chatPanel:
322+
let store = Service.shared.guiController.store
323+
Task { @MainActor in
324+
await store.send(.createAndSwitchToChatGPTChatTabIfNeeded).finish()
325+
store.send(.openChatPanel(forceDetach: false))
326+
}
327+
case .browser:
328+
let urlString = UserDefaults.shared.value(for: \.openChatInBrowserURL)
329+
let openInApp = {
330+
if !UserDefaults.shared.value(for: \.openChatInBrowserInInAppBrowser) {
331+
return false
332+
}
333+
return isFeatureAvailable(\.browserTab)
334+
}()
335+
guard let url = URL(string: urlString) else {
336+
let alert = NSAlert()
337+
alert.messageText = "Invalid URL"
338+
alert.informativeText = "The URL provided is not valid."
339+
alert.alertStyle = .warning
340+
alert.runModal()
341+
return
342+
}
343+
344+
if openInApp {
345+
let store = Service.shared.guiController.store
346+
Task { @MainActor in
347+
await store.send(.createAndSwitchToBrowserTabIfNeeded(url: url)).finish()
348+
store.send(.openChatPanel(forceDetach: false))
349+
}
350+
} else {
351+
Task {
352+
@Dependency(\.openURL) var openURL
353+
await openURL(url)
354+
}
355+
}
356+
}
357+
}
304358
}
305359

306360
extension PseudoCommandHandler {

Core/Sources/Service/SuggestionCommandHandler/SuggestionCommandHandler.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ protocol SuggestionCommandHandler {
1919
@ServiceActor
2020
func generateRealtimeSuggestions(editor: EditorContent) async throws -> UpdatedContent?
2121
@ServiceActor
22-
func chatWithSelection(editor: EditorContent) async throws -> UpdatedContent?
23-
@ServiceActor
2422
func promptToCode(editor: EditorContent) async throws -> UpdatedContent?
2523
@ServiceActor
2624
func customCommand(id: String, editor: EditorContent) async throws -> UpdatedContent?

Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -262,15 +262,6 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
262262
return try await presentSuggestions(editor: editor)
263263
}
264264

265-
func chatWithSelection(editor: EditorContent) async throws -> UpdatedContent? {
266-
Task { @MainActor in
267-
let store = Service.shared.guiController.store
268-
store.send(.createChatGPTChatTabIfNeeded)
269-
store.send(.openChatPanel(forceDetach: false))
270-
}
271-
return nil
272-
}
273-
274265
func promptToCode(editor: EditorContent) async throws -> UpdatedContent? {
275266
Task {
276267
do {

Core/Sources/Service/XPCService.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,13 @@ public class XPCService: NSObject, XPCServiceProtocol {
140140
}
141141
}
142142

143-
public func chatWithSelection(
143+
public func openChat(
144144
editorContent: Data,
145145
withReply reply: @escaping (Data?, Error?) -> Void
146146
) {
147-
replyWithUpdatedContent(editorContent: editorContent, withReply: reply) { handler, editor in
148-
try await handler.chatWithSelection(editor: editor)
149-
}
147+
let handler = PseudoCommandHandler()
148+
handler.openChat(forceDetach: false)
149+
reply(nil, nil)
150150
}
151151

152152
public func promptToCode(
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import SuggestionModel
33
import Foundation
44
import XcodeKit
55

6-
class ChatWithSelectionCommand: NSObject, XCSourceEditorCommand, CommandType {
6+
class OpenChatCommand: NSObject, XCSourceEditorCommand, CommandType {
77
var name: String { "Open Chat" }
88

99
func perform(
@@ -13,7 +13,7 @@ class ChatWithSelectionCommand: NSObject, XCSourceEditorCommand, CommandType {
1313
completionHandler(nil)
1414
Task {
1515
let service = try getService()
16-
_ = try await service.chatWithSelection(editorContent: .init(invocation))
16+
_ = try await service.openChat(editorContent: .init(invocation))
1717
}
1818
}
1919
}

EditorExtension/SourceEditorExtension.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class SourceEditorExtension: NSObject, XCSourceEditorExtension {
1717
PreviousSuggestionCommand(),
1818
PromptToCodeCommand(),
1919
AcceptPromptToCodeCommand(),
20-
ChatWithSelectionCommand(),
20+
OpenChatCommand(),
2121
].map(makeCommandDefinition)
2222
}
2323

0 commit comments

Comments
 (0)